From e346f88f420bf779a501d9d974c2aca66d940f6f Mon Sep 17 00:00:00 2001 From: kikootwo Date: Tue, 13 Jan 2026 01:51:22 -0500 Subject: [PATCH] Add Audible region config and user password change modal Implements configurable Audible region selection in setup and admin settings, affecting all Audible API calls and triggering data refresh on change. Adds a user-facing 'Change Password' modal in the header for local users, moving password change from admin-only to all local users via a new /api/auth/change-password endpoint. Updates documentation, API routes, and context to support these features, and removes the old admin-only password change flow. --- documentation/TABLEOFCONTENTS.md | 2 +- documentation/backend/services/auth.md | 8 +- documentation/frontend/components.md | 3 +- documentation/integrations/audible.md | 53 +++- documentation/settings-pages.md | 49 +++- src/app/admin/settings/page.tsx | 221 ++++++--------- src/app/api/admin/settings/audible/route.ts | 79 ++++++ .../admin/settings/change-password/route.ts | 138 ---------- src/app/api/admin/settings/route.ts | 1 + src/app/api/auth/change-password/route.ts | 186 +++++++++++++ src/app/api/auth/me/route.ts | 2 + src/app/api/setup/complete/route.ts | 8 + src/app/setup/page.tsx | 6 + src/app/setup/steps/BackendSelectionStep.tsx | 31 +++ src/components/layout/Header.tsx | 22 ++ src/components/ui/ChangePasswordModal.tsx | 256 ++++++++++++++++++ src/contexts/AuthContext.tsx | 19 ++ src/lib/integrations/audible.service.ts | 103 ++++++- src/lib/services/auth/IAuthProvider.ts | 1 + src/lib/services/auth/LocalAuthProvider.ts | 2 + src/lib/services/auth/OIDCAuthProvider.ts | 1 + src/lib/services/auth/PlexAuthProvider.ts | 1 + src/lib/services/config.service.ts | 9 + src/lib/types/audible.ts | 48 ++++ 24 files changed, 932 insertions(+), 317 deletions(-) create mode 100644 src/app/api/admin/settings/audible/route.ts delete mode 100644 src/app/api/admin/settings/change-password/route.ts create mode 100644 src/app/api/auth/change-password/route.ts create mode 100644 src/components/ui/ChangePasswordModal.tsx create mode 100644 src/lib/types/audible.ts diff --git a/documentation/TABLEOFCONTENTS.md b/documentation/TABLEOFCONTENTS.md index 169efa9..5f7a670 100644 --- a/documentation/TABLEOFCONTENTS.md +++ b/documentation/TABLEOFCONTENTS.md @@ -92,7 +92,7 @@ **"How do I configure external services?"** → [setup-wizard.md](setup-wizard.md), [settings-pages.md](settings-pages.md) **"What's the database schema?"** → [backend/database.md](backend/database.md) **"How does authentication work?"** → [backend/services/auth.md](backend/services/auth.md) -**"How do I change the admin password?"** → [settings-pages.md](settings-pages.md), [backend/services/auth.md](backend/services/auth.md) +**"How do I change my password?"** → [backend/services/auth.md](backend/services/auth.md) (local users only - accessed via user menu in header) **"How do I delete requests?"** → [admin-features/request-deletion.md](admin-features/request-deletion.md) **"How do I deploy?"** → [deployment/docker.md](deployment/docker.md) (multi-container), [deployment/unified.md](deployment/unified.md) (all-in-one) **"How do I use the unified container?"** → [deployment/unified.md](deployment/unified.md) diff --git a/documentation/backend/services/auth.md b/documentation/backend/services/auth.md index 779bcba..62d8878 100644 --- a/documentation/backend/services/auth.md +++ b/documentation/backend/services/auth.md @@ -54,7 +54,7 @@ Handles authentication and authorization: Multiple auth providers (Plex OAuth, O **POST /api/auth/plex/switch-profile** - Switch to selected profile and complete authentication **POST /api/auth/refresh** - Get new access token (refresh token in header) **POST /api/auth/logout** - Clear client-side token -**GET /api/auth/me** - Get current user (JWT in header) +**GET /api/auth/me** - Get current user (JWT in header), includes `authProvider` field ('plex' | 'oidc' | 'local') ## JWT Structure @@ -110,10 +110,12 @@ Handles authentication and authorization: Multiple auth providers (Plex OAuth, O - Identified by: `isSetupAdmin=true` AND `plexId` starts with `local-` **Password Management:** -- POST `/api/admin/settings/change-password` - Change local admin password +- POST `/api/auth/change-password` - Change password for any local user - Requires: current password, new password (min 8 chars), confirmation -- Security: Only accessible to local admin (verified via `requireLocalAdmin` middleware) +- Security: Only accessible to users with `authProvider='local'` (Plex/OIDC users cannot change passwords) - Validates current password before allowing change +- Properly decrypts stored password hash before bcrypt comparison +- Available to all local users (setup admin, locally registered users) ## Plex Home Profile Support diff --git a/documentation/frontend/components.md b/documentation/frontend/components.md index e758f73..62f8e6f 100644 --- a/documentation/frontend/components.md +++ b/documentation/frontend/components.md @@ -23,7 +23,7 @@ src/components/ ## Key Components **Layout** -- **Header** - Top nav, search input, user menu +- **Header** ✅ - Top nav, search input, user menu with "Change Password" option (local users only), logout - **Sidebar** - Admin side nav - **Footer** - Version, links @@ -46,6 +46,7 @@ src/components/ - **Select** - Custom styling, search/filter - **Modal** ✅ - Dialog overlay with backdrop, sizes (sm/md/lg/xl/full), ESC to close, body scroll lock - **ConfirmModal** ✅ - Confirmation dialog with customizable title, message, buttons, loading state, and variant (primary/danger) +- **ChangePasswordModal** ✅ - Password change form for local users. Three fields (current, new, confirm), real-time validation, success/error states, auto-closes on success. Only accessible to users with `authProvider='local'` - **Pagination** ✅ - Traditional page navigation with prev/next buttons, smart ellipsis (shows 1...4 5 6...10) - **StickyPagination** ✅ - Minimal floating pill at bottom center with prev/next arrows, quick jump input, section label. Shows/hides based on section visibility (IntersectionObserver). Rounded-full design, backdrop blur, subtle shadow, auto-scroll on page change diff --git a/documentation/integrations/audible.md b/documentation/integrations/audible.md index 941473f..c73bd28 100644 --- a/documentation/integrations/audible.md +++ b/documentation/integrations/audible.md @@ -19,6 +19,46 @@ Audiobook metadata from Audnexus API (primary) and Audible.com scraping (fallbac - Multiple selector strategies with promotional text filtering - Extract JSON-LD structured data when available +## Region Configuration + +**Status:** ✅ Implemented + +Configurable Audible region for accurate metadata matching across different international Audible stores. + +**Supported Regions:** +- United States (`us`) - `audible.com` (default) +- Canada (`ca`) - `audible.ca` +- United Kingdom (`uk`) - `audible.co.uk` +- Australia (`au`) - `audible.com.au` +- India (`in`) - `audible.in` + +**Why Regions Matter:** +- Each Audible region uses different ASINs for the same audiobook +- Metadata engines (Audnexus/Audible Agent) in Plex/Audiobookshelf must match RMAB's region +- Mismatched regions cause poor search results and failed metadata matching + +**Configuration:** +- Key: `audible.region` (stored in database) +- Default: `us` +- Set during: Setup wizard (Backend Selection step) or Admin Settings (Library tab) +- Help text instructs users to match their metadata engine region + +**Implementation:** +- `AudibleService` loads region from config on initialization +- Dynamically builds base URL: `AUDIBLE_REGIONS[region].baseUrl` +- Audnexus API calls include region parameter: `?region={code}` +- IP redirect prevention: `?ipRedirectOverride=true` on all Audible requests +- Configuration service helper: `getAudibleRegion()` returns configured region +- **Auto-detection of region changes**: Service checks config before each request and re-initializes if region changed +- **Cache clearing**: When region changes, ConfigService cache and AudibleService initialization are cleared +- **Automatic refresh**: Changing region automatically triggers `audible_refresh` job to fetch new data + +**Files:** +- Types: `src/lib/types/audible.ts` +- Service: `src/lib/integrations/audible.service.ts` +- Config: `src/lib/services/config.service.ts` +- API: `src/app/api/admin/settings/audible/route.ts` + ## Discovery Strategy (Popular/New/Search) - Parse Audible HTML with Cheerio @@ -28,10 +68,15 @@ Audiobook metadata from Audnexus API (primary) and Audible.com scraping (fallbac ## Data Sources -1. **Best Sellers:** `https://www.audible.com/adblbestsellers` -2. **New Releases:** `https://www.audible.com/newreleases` -3. **Search:** `https://www.audible.com/search?keywords={query}` -4. **Detail Page:** `https://www.audible.com/pd/{asin}` +URLs dynamically built based on configured region: + +1. **Best Sellers:** `{baseUrl}/adblbestsellers` +2. **New Releases:** `{baseUrl}/newreleases` +3. **Search:** `{baseUrl}/search?keywords={query}&ipRedirectOverride=true` +4. **Detail Page:** `{baseUrl}/pd/{asin}?ipRedirectOverride=true` +5. **Audnexus API:** `https://api.audnex.us/books/{asin}?region={code}` + +Where `{baseUrl}` is determined by configured region (e.g., `https://www.audible.co.uk` for UK). ## Metadata Extracted diff --git a/documentation/settings-pages.md b/documentation/settings-pages.md index 713aae5..d642a46 100644 --- a/documentation/settings-pages.md +++ b/documentation/settings-pages.md @@ -6,13 +6,39 @@ Single tabbed interface for admins to view/modify system configuration post-setu ## Sections -1. **Plex** - URL, token (masked), library ID, filesystem scan trigger toggle -2. **Audiobookshelf** - URL, API token (masked), library ID, filesystem scan trigger toggle +1. **Plex** - URL, token (masked), library ID, Audible region, filesystem scan trigger toggle +2. **Audiobookshelf** - URL, API token (masked), library ID, Audible region, filesystem scan trigger toggle 3. **Prowlarr** - URL, API key (masked), indexer selection with priority, seeding time, RSS monitoring toggle 4. **Download Client** - Type, URL, credentials (masked) 5. **Paths** - Download + media directories 6. **BookDate** - AI provider, API key (encrypted), model selection, library scope, custom prompt, swipe history -7. **Account** - Local admin password change (only visible to setup admin) + +## Audible Region + +**Purpose:** Configure which Audible region to use for metadata and search to ensure accurate ASIN matching with your metadata engine. + +**Configuration:** +- Key: `audible.region` (string, default: 'us') +- Supported regions: US, Canada, UK, Australia, India +- UI: Dropdown selector in Library tab (both Plex and Audiobookshelf settings) +- No validation required (immediate save) + +**Why It Matters:** +- Each Audible region uses different ASINs for the same audiobook +- Users must match their RMAB region to their Plex/Audiobookshelf metadata engine region +- Mismatched regions cause poor search results and failed metadata matching + +**Help Text:** +"Select the Audible region that matches your metadata engine (Audnexus/Audible Agent) configuration in [Plex/Audiobookshelf]. This ensures accurate book matching and metadata." + +**Implementation:** +- Affects all Audible API calls (base URL changes per region) +- Affects all Audnexus API calls (region parameter added) +- Changes apply immediately on next API call (no restart required) +- **Automatic refresh**: Changing region automatically triggers `audible_refresh` job to fetch popular/new releases for the new region +- **Cache management**: ConfigService cache and AudibleService initialization are cleared when region changes +- **Smart re-initialization**: Service automatically detects region changes and re-initializes before each request +- See: `documentation/integrations/audible.md` for technical details ## Filesystem Scan Trigger @@ -72,19 +98,11 @@ Single tabbed interface for admins to view/modify system configuration post-setu 4. **Save:** Updates user preferences immediately 5. Accessible to all authenticated users -**Account (local admin only):** -1. Local admin can change password -2. Requires: current password, new password (min 8 chars), confirmation -3. No "Save Changes" button - uses dedicated "Change Password" button -4. Form clears after successful change -5. Only visible to users with `isSetupAdmin=true` AND `plexId` starts with `local-` - **Validation state resets when:** - Plex: URL or token modified - Prowlarr: URL or API key modified (NOT indexer config) - Download Client: URL, username, or password modified - Paths: Directory paths modified -- Account: No validation required (password change is immediate) ## API Endpoints @@ -106,6 +124,11 @@ Single tabbed interface for admins to view/modify system configuration post-setu - Updates Prowlarr URL and API key - Requires prior successful test if values changed +**PUT /api/admin/settings/audible** +- Updates Audible region +- Body: `{ region: string }` (one of: us, ca, uk, au, in) +- No validation required + **PUT /api/admin/settings/prowlarr/indexers** - Updates indexer configuration (enabled, priority, seeding time, RSS) - No test required if URL/API key unchanged @@ -133,10 +156,6 @@ Single tabbed interface for admins to view/modify system configuration post-setu - GET /api/bookdate/preferences - Get user's preferences (libraryScope, customPrompt) - PUT /api/bookdate/preferences - Update user's preferences (all authenticated users) -**Account Endpoints:** -- POST /api/admin/settings/change-password - Change local admin password (local admin only) -- GET /api/auth/is-local-admin - Check if current user is local admin (returns `{isLocalAdmin: boolean}`) - ## Features - Password visibility toggle diff --git a/src/app/admin/settings/page.tsx b/src/app/admin/settings/page.tsx index 3d116d5..e7f0935 100644 --- a/src/app/admin/settings/page.tsx +++ b/src/app/admin/settings/page.tsx @@ -34,6 +34,7 @@ interface IndexerConfig { interface Settings { backendMode: 'plex' | 'audiobookshelf'; hasLocalUsers: boolean; + audibleRegion: string; plex: { url: string; token: string; @@ -136,15 +137,7 @@ export default function AdminSettings() { const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>( null ); - const [activeTab, setActiveTab] = useState<'library' | 'auth' | 'prowlarr' | 'download' | 'paths' | 'ebook' | 'account' | 'bookdate'>('library'); - - // Password change form state - const [passwordForm, setPasswordForm] = useState({ - currentPassword: '', - newPassword: '', - confirmPassword: '', - }); - const [changingPassword, setChangingPassword] = useState(false); + const [activeTab, setActiveTab] = useState<'library' | 'auth' | 'prowlarr' | 'download' | 'paths' | 'ebook' | 'bookdate'>('library'); // BookDate configuration state const [bookdateProvider, setBookdateProvider] = useState('openai'); @@ -834,41 +827,6 @@ export default function AdminSettings() { } }; - const changePassword = async () => { - setChangingPassword(true); - setMessage(null); - - try { - const response = await fetchWithAuth('/api/admin/settings/change-password', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(passwordForm), - }); - - const data = await response.json(); - - if (data.success) { - setMessage({ type: 'success', text: 'Password changed successfully!' }); - // Clear form - setPasswordForm({ - currentPassword: '', - newPassword: '', - confirmPassword: '', - }); - setTimeout(() => setMessage(null), 5000); - } else { - setMessage({ type: 'error', text: data.error || 'Failed to change password' }); - } - } catch (error) { - setMessage({ - type: 'error', - text: error instanceof Error ? error.message : 'Failed to change password', - }); - } finally { - setChangingPassword(false); - } - }; - const saveSettings = async () => { if (!settings) return; @@ -879,6 +837,18 @@ export default function AdminSettings() { // Save settings based on active tab switch (activeTab) { case 'library': + // Save Audible region (common to both backends) + const audibleResponse = await fetchWithAuth('/api/admin/settings/audible', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ region: settings.audibleRegion }), + }); + + if (!audibleResponse.ok) { + throw new Error('Failed to save Audible region settings'); + } + + // Save backend-specific settings if (settings.backendMode === 'plex') { const plexResponse = await fetchWithAuth('/api/admin/settings/plex', { method: 'PUT', @@ -1039,7 +1009,6 @@ export default function AdminSettings() { { id: 'paths', label: 'Paths', icon: '📁' }, { id: 'ebook', label: 'E-book Sidecar', icon: '📖' }, { id: 'bookdate', label: 'BookDate', icon: '📚' }, - ...(isLocalAdmin ? [{ id: 'account', label: 'Account', icon: '🔒' }] : []), ]; return ( @@ -1250,6 +1219,37 @@ export default function AdminSettings() { + {/* Audible Region Selection */} +
+ + +

+ Select the Audible region that matches your metadata engine (Audnexus/Audible Agent) + configuration in Plex. This ensures accurate book matching and metadata. +

+
+
+ {/* Audible Region Selection */} +
+ + +

+ Select the Audible region that matches your metadata engine (Audnexus/Audible Agent) + configuration in Audiobookshelf. This ensures accurate book matching and metadata. +

+
+
-
- - )} - {/* Footer - Hide for Account, BookDate, and E-book tabs (they have their own save buttons) */} - {activeTab !== 'account' && activeTab !== 'bookdate' && activeTab !== 'ebook' && ( + {/* Footer - Hide for BookDate and E-book tabs (they have their own save buttons) */} + {activeTab !== 'bookdate' && activeTab !== 'ebook' && (
+ {/* Audible Region Selection */} +
+ + +

+ Select the Audible region that matches your metadata engine (Audnexus/Audible Agent) + configuration in {value === 'plex' ? 'Plex' : 'Audiobookshelf'}. This ensures accurate book matching and metadata. +

+
+
{ async function checkBookDate() { @@ -85,6 +90,17 @@ export function Header() { > Profile + {canChangePassword && ( + + )}
+
+ + + +

+ Password changed successfully! +

+
+
+ )} + + {/* Error message */} + {error && ( +
+
+ + + +

{error}

+
+
+ )} + + {/* Current Password */} + { + setCurrentPassword(e.target.value); + setErrors({ ...errors, currentPassword: '' }); + }} + placeholder="Enter your current password" + autoComplete="current-password" + error={errors.currentPassword} + disabled={loading || success} + /> + + {/* New Password */} + { + setNewPassword(e.target.value); + setErrors({ ...errors, newPassword: '' }); + }} + placeholder="Enter your new password" + autoComplete="new-password" + helperText="Must be at least 8 characters" + error={errors.newPassword} + disabled={loading || success} + /> + + {/* Confirm New Password */} + { + setConfirmPassword(e.target.value); + setErrors({ ...errors, confirmPassword: '' }); + }} + placeholder="Confirm your new password" + autoComplete="new-password" + error={errors.confirmPassword} + disabled={loading || success} + /> + + {/* Action Buttons */} +
+ + +
+ + + ); +} diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 835dab8..98ffe39 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -15,6 +15,7 @@ interface User { email?: string; role: string; avatarUrl?: string; + authProvider?: string | null; // 'plex' | 'oidc' | 'local' | null } interface AuthContextType { @@ -90,6 +91,24 @@ export function AuthProvider({ children }: { children: ReactNode }) { setAccessToken(storedToken); setUser(JSON.parse(storedUser)); scheduleTokenRefresh(storedToken); + + // Fetch fresh user data from server to get latest fields (e.g., authProvider) + fetch('/api/auth/me', { + headers: { + 'Authorization': `Bearer ${storedToken}`, + }, + }) + .then((res) => res.json()) + .then((data) => { + if (data.user) { + // Update user with fresh data from server + setUser(data.user); + localStorage.setItem('user', JSON.stringify(data.user)); + } + }) + .catch((error) => { + console.error('Failed to fetch fresh user data:', error); + }); } setIsLoading(false); diff --git a/src/lib/integrations/audible.service.ts b/src/lib/integrations/audible.service.ts index 1c30499..c9e6b9a 100644 --- a/src/lib/integrations/audible.service.ts +++ b/src/lib/integrations/audible.service.ts @@ -6,6 +6,8 @@ import axios, { AxiosInstance } from 'axios'; import * as cheerio from 'cheerio'; import { RMABLogger } from '../utils/logger'; +import { getConfigService } from '../services/config.service'; +import { AudibleRegion, AUDIBLE_REGIONS, DEFAULT_AUDIBLE_REGION } from '../types/audible'; // Module-level logger const logger = RMABLogger.create('Audible'); @@ -32,25 +34,92 @@ export interface AudibleSearchResult { } export class AudibleService { - private client: AxiosInstance; - private readonly baseUrl = 'https://www.audible.com'; + private client!: AxiosInstance; + private baseUrl: string = 'https://www.audible.com'; + private region: AudibleRegion = 'us'; + private initialized: boolean = false; constructor() { - this.client = axios.create({ - baseURL: this.baseUrl, - timeout: 15000, - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.9', - }, - }); + // Client will be created lazily on first use + } + + /** + * Force re-initialization (used when region config changes) + */ + public forceReinitialize(): void { + logger.info('Force re-initializing AudibleService'); + this.initialized = false; + } + + /** + * Initialize service with configured region + * Lazy initialization allows async config loading + * Automatically re-initializes if region has changed + */ + private async initialize(): Promise { + // If already initialized, check if region has changed + if (this.initialized) { + const configService = getConfigService(); + const currentRegion = await configService.getAudibleRegion(); + + // If region changed, force re-initialization + if (currentRegion !== this.region) { + logger.info(`Region changed from ${this.region} to ${currentRegion}, re-initializing`); + this.initialized = false; + } else { + return; // Region unchanged, use existing initialization + } + } + + try { + const configService = getConfigService(); + this.region = await configService.getAudibleRegion(); + this.baseUrl = AUDIBLE_REGIONS[this.region].baseUrl; + + logger.info(`Initializing Audible service with region: ${this.region} (${this.baseUrl})`); + + // Create axios client with region-specific base URL + this.client = axios.create({ + baseURL: this.baseUrl, + timeout: 15000, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.9', + }, + params: { + ipRedirectOverride: 'true', // Prevent IP-based region redirects + }, + }); + + this.initialized = true; + } catch (error) { + logger.error('Failed to initialize AudibleService', { error: error instanceof Error ? error.message : String(error) }); + // Fallback to default region + this.region = DEFAULT_AUDIBLE_REGION; + this.baseUrl = AUDIBLE_REGIONS[this.region].baseUrl; + this.client = axios.create({ + baseURL: this.baseUrl, + timeout: 15000, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.9', + }, + params: { + ipRedirectOverride: 'true', + }, + }); + this.initialized = true; + } } /** * Get popular audiobooks from best sellers (with pagination support) */ async getPopularAudiobooks(limit: number = 20): Promise { + await this.initialize(); + try { logger.info(` Fetching popular audiobooks (limit: ${limit})...`); @@ -137,6 +206,8 @@ export class AudibleService { * Get new release audiobooks (with pagination support) */ async getNewReleases(limit: number = 20): Promise { + await this.initialize(); + try { logger.info(` Fetching new releases (limit: ${limit})...`); @@ -222,6 +293,8 @@ export class AudibleService { * Search for audiobooks */ async search(query: string, page: number = 1): Promise { + await this.initialize(); + try { logger.info(` Searching for "${query}"...`); @@ -316,6 +389,8 @@ export class AudibleService { * Fallback: Audible scraping */ async getAudiobookDetails(asin: string): Promise { + await this.initialize(); + try { logger.info(` Fetching details for ASIN ${asin}...`); @@ -341,9 +416,13 @@ export class AudibleService { */ private async fetchFromAudnexus(asin: string): Promise { try { - logger.debug(`Fetching ASIN from Audnexus: ${asin}`); + const audnexusRegion = AUDIBLE_REGIONS[this.region].audnexusParam; + logger.debug(`Fetching ASIN from Audnexus: ${asin} (region: ${audnexusRegion})`); const response = await axios.get(`https://api.audnex.us/books/${asin}`, { + params: { + region: audnexusRegion, // Pass region parameter to Audnexus + }, timeout: 10000, headers: { 'User-Agent': 'ReadMeABook/1.0', diff --git a/src/lib/services/auth/IAuthProvider.ts b/src/lib/services/auth/IAuthProvider.ts index 591fca1..4f172fc 100644 --- a/src/lib/services/auth/IAuthProvider.ts +++ b/src/lib/services/auth/IAuthProvider.ts @@ -11,6 +11,7 @@ export interface UserInfo { avatarUrl?: string; role?: string; // 'admin' | 'user' isAdmin?: boolean; // Deprecated: use role instead + authProvider?: string; // 'plex' | 'oidc' | 'local' } export interface AuthTokens { diff --git a/src/lib/services/auth/LocalAuthProvider.ts b/src/lib/services/auth/LocalAuthProvider.ts index d158cf6..2fe879d 100644 --- a/src/lib/services/auth/LocalAuthProvider.ts +++ b/src/lib/services/auth/LocalAuthProvider.ts @@ -125,6 +125,7 @@ export class LocalAuthProvider implements IAuthProvider { plexId: user.plexId, username: user.plexUsername, role: user.role, + authProvider: 'local', }, tokens, }; @@ -223,6 +224,7 @@ export class LocalAuthProvider implements IAuthProvider { plexId: user.plexId, username: user.plexUsername, role: user.role, + authProvider: 'local', }, tokens, }; diff --git a/src/lib/services/auth/OIDCAuthProvider.ts b/src/lib/services/auth/OIDCAuthProvider.ts index 7a768bc..8d572db 100644 --- a/src/lib/services/auth/OIDCAuthProvider.ts +++ b/src/lib/services/auth/OIDCAuthProvider.ts @@ -455,6 +455,7 @@ export class OIDCAuthProvider implements IAuthProvider { email: user.plexEmail || undefined, avatarUrl: user.avatarUrl || undefined, isAdmin: user.role === 'admin', + authProvider: 'oidc', }, isFirstLogin: isFirstUser && shouldTriggerJobs, }; diff --git a/src/lib/services/auth/PlexAuthProvider.ts b/src/lib/services/auth/PlexAuthProvider.ts index 7fb6731..d3e247c 100644 --- a/src/lib/services/auth/PlexAuthProvider.ts +++ b/src/lib/services/auth/PlexAuthProvider.ts @@ -240,6 +240,7 @@ export class PlexAuthProvider implements IAuthProvider { email: user.plexEmail || undefined, avatarUrl: user.avatarUrl || undefined, isAdmin: user.role === 'admin', + authProvider: 'plex', }; } diff --git a/src/lib/services/config.service.ts b/src/lib/services/config.service.ts index 2070a50..ae597cb 100644 --- a/src/lib/services/config.service.ts +++ b/src/lib/services/config.service.ts @@ -6,6 +6,7 @@ import { prisma } from '@/lib/db'; import { getEncryptionService } from './encryption.service'; import { RMABLogger } from '@/lib/utils/logger'; +import { AudibleRegion, DEFAULT_AUDIBLE_REGION } from '@/lib/types/audible'; const logger = RMABLogger.create('Config'); @@ -228,6 +229,14 @@ export class ConfigurationService { return (await this.getBackendMode()) === 'audiobookshelf'; } + /** + * Get configured Audible region + */ + async getAudibleRegion(): Promise { + const region = await this.get('audible.region'); + return (region as AudibleRegion) || DEFAULT_AUDIBLE_REGION; + } + /** * Clear the cache for a specific key or all keys */ diff --git a/src/lib/types/audible.ts b/src/lib/types/audible.ts new file mode 100644 index 0000000..50d90a7 --- /dev/null +++ b/src/lib/types/audible.ts @@ -0,0 +1,48 @@ +/** + * Component: Audible Region Types + * Documentation: documentation/integrations/audible.md + */ + +export type AudibleRegion = 'us' | 'ca' | 'uk' | 'au' | 'in'; + +export interface AudibleRegionConfig { + code: AudibleRegion; + name: string; + baseUrl: string; + audnexusParam: string; +} + +export const AUDIBLE_REGIONS: Record = { + us: { + code: 'us', + name: 'United States', + baseUrl: 'https://www.audible.com', + audnexusParam: 'us', + }, + ca: { + code: 'ca', + name: 'Canada', + baseUrl: 'https://www.audible.ca', + audnexusParam: 'ca', + }, + uk: { + code: 'uk', + name: 'United Kingdom', + baseUrl: 'https://www.audible.co.uk', + audnexusParam: 'uk', + }, + au: { + code: 'au', + name: 'Australia', + baseUrl: 'https://www.audible.com.au', + audnexusParam: 'au', + }, + in: { + code: 'in', + name: 'India', + baseUrl: 'https://www.audible.in', + audnexusParam: 'in', + }, +}; + +export const DEFAULT_AUDIBLE_REGION: AudibleRegion = 'us';