Compare commits

...

5 Commits

Author SHA1 Message Date
kikootwo 4c1d1c89e8 Audible regions: isEnglish flag + UI warnings
Add an isEnglish flag to AUDIBLE_REGIONS and update region handling across the app. UI: populate Audible region selects from AUDIBLE_REGIONS and mark non-English regions with a '*' and an amber warning explaining limited feature support. Service: set axios default param language=english on Audible requests (simplifies/fixes locale handling) and remove the previous locale-correction flow. API: validate regions dynamically from AUDIBLE_REGIONS. Also bump package version to 1.0.2. These changes make region metadata explicit and inform users about limited support for non-English regions while forcing English content where supported.
2026-02-06 11:48:00 -05:00
kikootwo d25d93680e Merge pull request #36 from aronjanosch/feature/german-audible-region-and-regional-title
Add German Audible region
2026-02-06 11:10:14 -05:00
kikootwo d2be3f558f audible: enforce English locale and fix redirects
Add English-locale enforcement and locale-redirect handling for the Audible integration. Adds Cookie 'lc-acbus=en_US' to Audible client headers and implements handleLocaleRedirect(...) in src/lib/integrations/audible.service.ts to detect non-English culture codes in response URLs, parse Audible's <adbl-toggle-chip> locale picker to obtain the canonical English URL and re-request it, with a fallback URL rewrite (strip culture code + language=en_US). Wire the correction into fetchWithRetry so responses redirected to locale-specific pages are corrected before use. Update documentation (documentation/integrations/audible.md) to describe the behavior and bump package version to 1.0.1.
2026-02-06 02:16:44 -05:00
Aron Wiederkehr 312421a96b Add German Audible region 2026-02-05 20:09:21 +01:00
kikootwo 1140ffc8eb Use dynamic Docker pulls badge
Replace the static shields.io Docker pulls badge with a dynamic JSON badge that queries ghcr-badge.elias.eu.org for the repository's downloadCount. Keeps the same badge style and link target (the package container page) but surfaces GHCR download stats via the dynamic endpoint.
2026-02-05 13:49:31 -05:00
9 changed files with 141 additions and 27 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
[![GitHub Sponsors](https://img.shields.io/github/sponsors/kikootwo?style=for-the-badge&logo=github&logoColor=white&label=Sponsor&color=EA4AAA)](https://github.com/sponsors/kikootwo) [![GitHub Sponsors](https://img.shields.io/github/sponsors/kikootwo?style=for-the-badge&logo=github&logoColor=white&label=Sponsor&color=EA4AAA)](https://github.com/sponsors/kikootwo)
[![Build Status](https://img.shields.io/github/actions/workflow/status/kikootwo/readmeabook/build-unified-image.yml?branch=main&style=for-the-badge&logo=github&label=Build)](https://github.com/kikootwo/readmeabook/actions/workflows/build-unified-image.yml) [![Build Status](https://img.shields.io/github/actions/workflow/status/kikootwo/readmeabook/build-unified-image.yml?branch=main&style=for-the-badge&logo=github&label=Build)](https://github.com/kikootwo/readmeabook/actions/workflows/build-unified-image.yml)
[![Tests](https://img.shields.io/github/actions/workflow/status/kikootwo/readmeabook/run-tests.yml?branch=main&style=for-the-badge&logo=github&label=Tests)](https://github.com/kikootwo/readmeabook/actions/workflows/run-tests.yml) [![Tests](https://img.shields.io/github/actions/workflow/status/kikootwo/readmeabook/run-tests.yml?branch=main&style=for-the-badge&logo=github&label=Tests)](https://github.com/kikootwo/readmeabook/actions/workflows/run-tests.yml)
[![Docker Pulls](https://img.shields.io/docker/pulls/kikootwo/readmeabook?style=for-the-badge&logo=docker&logoColor=white)](https://github.com/kikootwo/readmeabook/pkgs/container/readmeabook) [![Docker Pulls](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fghcr-badge.elias.eu.org%2Fapi%2Fkikootwo%2Freadmeabook%2Freadmeabook&query=downloadCount&style=for-the-badge&logo=docker&label=Docker%20Pulls&color=2496ed)](https://github.com/kikootwo/readmeabook/pkgs/container/readmeabook)
[![License](https://img.shields.io/badge/License-AGPL%20v3-blue.svg?style=for-the-badge)](https://www.gnu.org/licenses/agpl-3.0) [![License](https://img.shields.io/badge/License-AGPL%20v3-blue.svg?style=for-the-badge)](https://www.gnu.org/licenses/agpl-3.0)
[![GitHub Stars](https://img.shields.io/github/stars/kikootwo/readmeabook?style=for-the-badge&logo=github)](https://github.com/kikootwo/readmeabook/stargazers) [![GitHub Stars](https://img.shields.io/github/stars/kikootwo/readmeabook?style=for-the-badge&logo=github)](https://github.com/kikootwo/readmeabook/stargazers)
[![Discord](https://img.shields.io/discord/1450562177277755464?style=for-the-badge&logo=discord&logoColor=white&label=Discord)](https://discord.gg/kaw6jKbKts) [![Discord](https://img.shields.io/discord/1450562177277755464?style=for-the-badge&logo=discord&logoColor=white&label=Discord)](https://discord.gg/kaw6jKbKts)
+21 -6
View File
@@ -26,11 +26,18 @@ Audiobook metadata from Audnexus API (primary) and Audible.com scraping (fallbac
Configurable Audible region for accurate metadata matching across different international Audible stores. Configurable Audible region for accurate metadata matching across different international Audible stores.
**Supported Regions:** **Supported Regions:**
- United States (`us`) - `audible.com` (default) - United States (`us`) - `audible.com` (default, English)
- Canada (`ca`) - `audible.ca` - Canada (`ca`) - `audible.ca` (English)
- United Kingdom (`uk`) - `audible.co.uk` - United Kingdom (`uk`) - `audible.co.uk` (English)
- Australia (`au`) - `audible.com.au` - Australia (`au`) - `audible.com.au` (English)
- India (`in`) - `audible.in` - India (`in`) - `audible.in` (English)
- Germany (`de`) - `audible.de` (non-English)
**`isEnglish` Flag:**
- Each region has `isEnglish: boolean` in `AudibleRegionConfig`
- Non-English regions (`isEnglish: false`) display an amber warning in all region dropdowns (setup wizard + admin settings)
- Warning text: "Many features such as search, discovery, and metadata matching are not yet fully supported for non-English regions."
- Dropdown options for non-English regions show `*` suffix (e.g., "Germany *")
**Why Regions Matter:** **Why Regions Matter:**
- Each Audible region uses different ASINs for the same audiobook - Each Audible region uses different ASINs for the same audiobook
@@ -47,7 +54,8 @@ Configurable Audible region for accurate metadata matching across different inte
- `AudibleService` loads region from config on initialization - `AudibleService` loads region from config on initialization
- Dynamically builds base URL: `AUDIBLE_REGIONS[region].baseUrl` - Dynamically builds base URL: `AUDIBLE_REGIONS[region].baseUrl`
- Audnexus API calls include region parameter: `?region={code}` - Audnexus API calls include region parameter: `?region={code}`
- IP redirect prevention: `?ipRedirectOverride=true` on all Audible requests - IP redirect prevention: `?ipRedirectOverride=true` on all Audible requests (region only)
- **Locale enforcement:** `?language=english` query parameter on all Audible requests (forces English content regardless of server IP geolocation)
- Configuration service helper: `getAudibleRegion()` returns configured region - Configuration service helper: `getAudibleRegion()` returns configured region
- **Auto-detection of region changes**: Service checks config before each request and re-initializes if region changed - **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 - **Cache clearing**: When region changes, ConfigService cache and AudibleService initialization are cleared
@@ -225,3 +233,10 @@ interface EnrichedAudibleAudiobook extends AudibleAudiobook {
- **Fix:** Added `mapRegionToABSProvider()` to convert RMAB region codes to AudiobookShelf provider values. US → `'audible'`, others → `'audible.{region}'` (e.g., `'audible.ca'`, `'audible.uk'`) - **Fix:** Added `mapRegionToABSProvider()` to convert RMAB region codes to AudiobookShelf provider values. US → `'audible'`, others → `'audible.{region}'` (e.g., `'audible.ca'`, `'audible.uk'`)
- **Location:** `src/lib/services/audiobookshelf/api.ts:14, 147` - **Location:** `src/lib/services/audiobookshelf/api.ts:14, 147`
- **Affects:** All Audiobookshelf metadata matching operations - **Affects:** All Audiobookshelf metadata matching operations
**Non-English locale pages served to users outside US (2026-02-05)**
- **Problem:** Audible uses IP geolocation to serve locale-specific pages (e.g., Spanish content for Dominican Republic IPs). `ipRedirectOverride=true` only prevents region redirects (audible.com → audible.co.uk), NOT language/locale changes.
- **Impact:** Users self-hosting from non-English-speaking countries got non-English bestsellers/new releases on their homepage.
- **Fix:** Added `language=english` query parameter to all Audible requests via axios default params. Audible respects this parameter and serves English content regardless of IP geolocation. Fails gracefully for regions where English isn't available.
- **Location:** `src/lib/integrations/audible.service.ts``initialize()` (axios default params)
- **Affects:** All Audible scraping: popular, new releases, search, detail pages
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "readmeabook", "name": "readmeabook",
"version": "1.0.0", "version": "1.0.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
@@ -6,6 +6,7 @@
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Settings, ABSLibrary } from '../../lib/types'; import { Settings, ABSLibrary } from '../../lib/types';
import { AUDIBLE_REGIONS } from '@/lib/types/audible';
interface AudiobookshelfSectionProps { interface AudiobookshelfSectionProps {
settings: Settings; settings: Settings;
@@ -161,12 +162,39 @@ export function AudiobookshelfSection({
onChange={(e) => handleAudibleRegionChange(e.target.value)} onChange={(e) => handleAudibleRegionChange(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
<option value="us">United States</option> {Object.values(AUDIBLE_REGIONS).map((region) => (
<option value="ca">Canada</option> <option key={region.code} value={region.code}>
<option value="uk">United Kingdom</option> {region.name}{!region.isEnglish ? ' *' : ''}
<option value="au">Australia</option> </option>
<option value="in">India</option> ))}
</select> </select>
{AUDIBLE_REGIONS[settings.audibleRegion as keyof typeof AUDIBLE_REGIONS]?.isEnglish === false && (
<div className="bg-amber-50 dark:bg-amber-900/20 rounded-lg p-4 border border-amber-200 dark:border-amber-800 mt-2">
<div className="flex gap-3">
<svg
className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
<div>
<p className="text-sm font-medium text-amber-900 dark:text-amber-100">
Non-English Region
</p>
<p className="text-sm text-amber-700 dark:text-amber-300 mt-1">
Many features such as search, discovery, and metadata matching are not yet fully
supported for non-English regions. You may still proceed, but expect limited
functionality.
</p>
</div>
</div>
</div>
)}
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
Select the Audible region that matches your metadata engine (Audnexus/Audible Agent) Select the Audible region that matches your metadata engine (Audnexus/Audible Agent)
configuration in Audiobookshelf. This ensures accurate book matching and metadata. configuration in Audiobookshelf. This ensures accurate book matching and metadata.
@@ -6,6 +6,7 @@
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Settings, PlexLibrary } from '../../lib/types'; import { Settings, PlexLibrary } from '../../lib/types';
import { AUDIBLE_REGIONS } from '@/lib/types/audible';
interface PlexSectionProps { interface PlexSectionProps {
settings: Settings; settings: Settings;
@@ -161,12 +162,39 @@ export function PlexSection({
onChange={(e) => handleAudibleRegionChange(e.target.value)} onChange={(e) => handleAudibleRegionChange(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
<option value="us">United States</option> {Object.values(AUDIBLE_REGIONS).map((region) => (
<option value="ca">Canada</option> <option key={region.code} value={region.code}>
<option value="uk">United Kingdom</option> {region.name}{!region.isEnglish ? ' *' : ''}
<option value="au">Australia</option> </option>
<option value="in">India</option> ))}
</select> </select>
{AUDIBLE_REGIONS[settings.audibleRegion as keyof typeof AUDIBLE_REGIONS]?.isEnglish === false && (
<div className="bg-amber-50 dark:bg-amber-900/20 rounded-lg p-4 border border-amber-200 dark:border-amber-800 mt-2">
<div className="flex gap-3">
<svg
className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
<div>
<p className="text-sm font-medium text-amber-900 dark:text-amber-100">
Non-English Region
</p>
<p className="text-sm text-amber-700 dark:text-amber-300 mt-1">
Many features such as search, discovery, and metadata matching are not yet fully
supported for non-English regions. You may still proceed, but expect limited
functionality.
</p>
</div>
</div>
</div>
)}
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
Select the Audible region that matches your metadata engine (Audnexus/Audible Agent) Select the Audible region that matches your metadata engine (Audnexus/Audible Agent)
configuration in Plex. This ensures accurate book matching and metadata. configuration in Plex. This ensures accurate book matching and metadata.
+3 -2
View File
@@ -8,11 +8,12 @@ import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middlewar
import { getConfigService } from '@/lib/services/config.service'; import { getConfigService } from '@/lib/services/config.service';
import { getAudibleService } from '@/lib/integrations/audible.service'; import { getAudibleService } from '@/lib/integrations/audible.service';
import { getJobQueueService } from '@/lib/services/job-queue.service'; import { getJobQueueService } from '@/lib/services/job-queue.service';
import { AUDIBLE_REGIONS } from '@/lib/types/audible';
import { RMABLogger } from '@/lib/utils/logger'; import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Settings.Audible'); const logger = RMABLogger.create('API.Admin.Settings.Audible');
const VALID_REGIONS = ['us', 'ca', 'uk', 'au', 'in']; const VALID_REGIONS = Object.keys(AUDIBLE_REGIONS);
export async function PUT(request: NextRequest) { export async function PUT(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -24,7 +25,7 @@ export async function PUT(request: NextRequest) {
if (!region || !VALID_REGIONS.includes(region)) { if (!region || !VALID_REGIONS.includes(region)) {
logger.warn('Invalid region provided', { region }); logger.warn('Invalid region provided', { region });
return NextResponse.json( return NextResponse.json(
{ success: false, error: 'Invalid Audible region. Must be one of: us, ca, uk, au, in' }, { success: false, error: `Invalid Audible region. Must be one of: ${VALID_REGIONS.join(', ')}` },
{ status: 400 } { status: 400 }
); );
} }
+33 -6
View File
@@ -6,7 +6,7 @@
'use client'; 'use client';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { AudibleRegion } from '@/lib/types/audible'; import { AudibleRegion, AUDIBLE_REGIONS } from '@/lib/types/audible';
interface BackendSelectionStepProps { interface BackendSelectionStepProps {
value: 'plex' | 'audiobookshelf'; value: 'plex' | 'audiobookshelf';
@@ -113,12 +113,39 @@ export function BackendSelectionStep({
onChange={(e) => onAudibleRegionChange(e.target.value as AudibleRegion)} onChange={(e) => onAudibleRegionChange(e.target.value as AudibleRegion)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
<option value="us">United States</option> {Object.values(AUDIBLE_REGIONS).map((region) => (
<option value="ca">Canada</option> <option key={region.code} value={region.code}>
<option value="uk">United Kingdom</option> {region.name}{!region.isEnglish ? ' *' : ''}
<option value="au">Australia</option> </option>
<option value="in">India</option> ))}
</select> </select>
{AUDIBLE_REGIONS[audibleRegion]?.isEnglish === false && (
<div className="bg-amber-50 dark:bg-amber-900/20 rounded-lg p-4 border border-amber-200 dark:border-amber-800 mt-2">
<div className="flex gap-3">
<svg
className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
<div>
<p className="text-sm font-medium text-amber-900 dark:text-amber-100">
Non-English Region
</p>
<p className="text-sm text-amber-700 dark:text-amber-300 mt-1">
Many features such as search, discovery, and metadata matching are not yet fully
supported for non-English regions. You may still proceed, but expect limited
functionality.
</p>
</div>
</div>
</div>
)}
<p className="text-sm text-gray-600 dark:text-gray-400"> <p className="text-sm text-gray-600 dark:text-gray-400">
Select the Audible region that matches your metadata engine (Audnexus/Audible Agent) 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. configuration in {value === 'plex' ? 'Plex' : 'Audiobookshelf'}. This ensures accurate book matching and metadata.
+2
View File
@@ -91,6 +91,7 @@ export class AudibleService {
}, },
params: { params: {
ipRedirectOverride: 'true', // Prevent IP-based region redirects ipRedirectOverride: 'true', // Prevent IP-based region redirects
language: 'english', // Force English locale (prevents IP-based language serving for non-English IPs)
}, },
}); });
@@ -110,6 +111,7 @@ export class AudibleService {
}, },
params: { params: {
ipRedirectOverride: 'true', ipRedirectOverride: 'true',
language: 'english',
}, },
}); });
this.initialized = true; this.initialized = true;
+14 -1
View File
@@ -3,13 +3,14 @@
* Documentation: documentation/integrations/audible.md * Documentation: documentation/integrations/audible.md
*/ */
export type AudibleRegion = 'us' | 'ca' | 'uk' | 'au' | 'in'; export type AudibleRegion = 'us' | 'ca' | 'uk' | 'au' | 'in' | 'de';
export interface AudibleRegionConfig { export interface AudibleRegionConfig {
code: AudibleRegion; code: AudibleRegion;
name: string; name: string;
baseUrl: string; baseUrl: string;
audnexusParam: string; audnexusParam: string;
isEnglish: boolean;
} }
export const AUDIBLE_REGIONS: Record<AudibleRegion, AudibleRegionConfig> = { export const AUDIBLE_REGIONS: Record<AudibleRegion, AudibleRegionConfig> = {
@@ -18,30 +19,42 @@ export const AUDIBLE_REGIONS: Record<AudibleRegion, AudibleRegionConfig> = {
name: 'United States', name: 'United States',
baseUrl: 'https://www.audible.com', baseUrl: 'https://www.audible.com',
audnexusParam: 'us', audnexusParam: 'us',
isEnglish: true,
}, },
ca: { ca: {
code: 'ca', code: 'ca',
name: 'Canada', name: 'Canada',
baseUrl: 'https://www.audible.ca', baseUrl: 'https://www.audible.ca',
audnexusParam: 'ca', audnexusParam: 'ca',
isEnglish: true,
}, },
uk: { uk: {
code: 'uk', code: 'uk',
name: 'United Kingdom', name: 'United Kingdom',
baseUrl: 'https://www.audible.co.uk', baseUrl: 'https://www.audible.co.uk',
audnexusParam: 'uk', audnexusParam: 'uk',
isEnglish: true,
}, },
au: { au: {
code: 'au', code: 'au',
name: 'Australia', name: 'Australia',
baseUrl: 'https://www.audible.com.au', baseUrl: 'https://www.audible.com.au',
audnexusParam: 'au', audnexusParam: 'au',
isEnglish: true,
}, },
in: { in: {
code: 'in', code: 'in',
name: 'India', name: 'India',
baseUrl: 'https://www.audible.in', baseUrl: 'https://www.audible.in',
audnexusParam: 'in', audnexusParam: 'in',
isEnglish: true,
},
de: {
code: 'de',
name: 'Germany',
baseUrl: 'https://www.audible.de',
audnexusParam: 'de',
isEnglish: false,
}, },
}; };