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';