mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
Add multi-source ebook support and per-indexer categories
Introduces granular toggles for Anna's Archive and Indexer Search as ebook sources, updates settings UI to a three-section layout, and documents the new configuration. Adds per-indexer category configuration with separate tabs for audiobooks and ebooks, updates API routes and types for new settings, and ensures legacy config migration. Indexer grouping and file organization logic now support the new category structure and ebook source toggles.
This commit is contained in:
@@ -497,7 +497,7 @@ function AdminDashboardContent() {
|
||||
</h2>
|
||||
<RecentRequestsTable
|
||||
requests={requestsData.requests}
|
||||
ebookSidecarEnabled={settingsData?.ebook?.enabled || false}
|
||||
ebookSidecarEnabled={settingsData?.ebook?.annasArchiveEnabled || settingsData?.ebook?.indexerSearchEnabled || false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -103,12 +103,17 @@ export interface PathsSettings {
|
||||
|
||||
/**
|
||||
* E-book sidecar configuration
|
||||
* Supports two sources: Anna's Archive (direct HTTP) and Indexer Search (Prowlarr)
|
||||
*/
|
||||
export interface EbookSettings {
|
||||
enabled: boolean;
|
||||
preferredFormat: string;
|
||||
// Source toggles
|
||||
annasArchiveEnabled: boolean;
|
||||
indexerSearchEnabled: boolean;
|
||||
// Anna's Archive specific settings
|
||||
baseUrl: string;
|
||||
flaresolverrUrl: string;
|
||||
// General settings (shared across sources)
|
||||
preferredFormat: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,7 +148,8 @@ export interface IndexerConfig {
|
||||
seedingTimeMinutes?: number; // Torrents only
|
||||
removeAfterProcessing?: boolean; // Usenet only
|
||||
rssEnabled: boolean;
|
||||
categories?: number[];
|
||||
audiobookCategories?: number[]; // Category IDs for audiobook searches (default: [3030])
|
||||
ebookCategories?: number[]; // Category IDs for ebook searches (default: [7020])
|
||||
supportsRss?: boolean;
|
||||
}
|
||||
|
||||
@@ -158,7 +164,8 @@ export interface SavedIndexerConfig {
|
||||
seedingTimeMinutes?: number; // Torrents only
|
||||
removeAfterProcessing?: boolean; // Usenet only
|
||||
rssEnabled: boolean;
|
||||
categories: number[];
|
||||
audiobookCategories: number[]; // Category IDs for audiobook searches (default: [3030])
|
||||
ebookCategories: number[]; // Category IDs for ebook searches (default: [7020])
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -106,7 +106,8 @@ export default function AdminSettings() {
|
||||
protocol: idx.protocol,
|
||||
priority: idx.priority,
|
||||
rssEnabled: idx.rssEnabled,
|
||||
categories: idx.categories || [3030],
|
||||
audiobookCategories: idx.audiobookCategories || [3030],
|
||||
ebookCategories: idx.ebookCategories || [7020],
|
||||
};
|
||||
|
||||
// Add protocol-specific fields
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
/**
|
||||
* Component: E-book Settings Tab
|
||||
* Documentation: documentation/settings-pages.md
|
||||
*
|
||||
* Three-section layout:
|
||||
* 1. Anna's Archive - Direct HTTP downloads from Anna's Archive
|
||||
* 2. Indexer Search - Search via Prowlarr indexers (future feature)
|
||||
* 3. General Settings - Shared settings like preferred format
|
||||
*/
|
||||
|
||||
'use client';
|
||||
@@ -27,167 +32,233 @@ export function EbookTab({ ebook, onChange, onSuccess, onError, markAsSaved }: E
|
||||
updateEbook,
|
||||
testFlaresolverrConnection,
|
||||
saveSettings,
|
||||
isAnySourceEnabled,
|
||||
} = useEbookSettings({ ebook, onChange, onSuccess, onError, markAsSaved });
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-2xl">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
E-book Sidecar
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
||||
Automatically download e-books from Anna's Archive to accompany your audiobooks.
|
||||
Automatically download e-books to accompany your audiobooks.
|
||||
E-books are placed in the same folder as the audiobook files.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Enable Toggle */}
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="ebook-enabled"
|
||||
checked={ebook.enabled || false}
|
||||
onChange={(e) => updateEbook('enabled', e.target.checked)}
|
||||
className="mt-1 h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<label
|
||||
htmlFor="ebook-enabled"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-gray-100 cursor-pointer"
|
||||
>
|
||||
Enable e-book sidecar downloads
|
||||
</label>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
When enabled, the system will search for e-books matching your audiobook's ASIN
|
||||
and download them to the same folder.
|
||||
</p>
|
||||
{/* ═══════════════════════════════════════════════════════════════════════
|
||||
SECTION 1: ANNA'S ARCHIVE
|
||||
═══════════════════════════════════════════════════════════════════════ */}
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<div className="bg-gray-50 dark:bg-gray-800 px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 uppercase tracking-wider">
|
||||
Anna's Archive
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Enable Toggle */}
|
||||
<div className="flex items-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="annas-archive-enabled"
|
||||
checked={ebook.annasArchiveEnabled || false}
|
||||
onChange={(e) => updateEbook('annasArchiveEnabled', e.target.checked)}
|
||||
className="mt-1 h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<label
|
||||
htmlFor="annas-archive-enabled"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-gray-100 cursor-pointer"
|
||||
>
|
||||
Enable Anna's Archive downloads
|
||||
</label>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Download e-books directly from Anna's Archive using ASIN or title matching.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Anna's Archive specific settings - only shown when enabled */}
|
||||
{ebook.annasArchiveEnabled && (
|
||||
<>
|
||||
{/* Base URL */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Base URL
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={ebook.baseUrl || 'https://annas-archive.li'}
|
||||
onChange={(e) => updateEbook('baseUrl', e.target.value)}
|
||||
placeholder="https://annas-archive.li"
|
||||
className="font-mono"
|
||||
/>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Change this if the primary Anna's Archive mirror is unavailable.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* FlareSolverr URL */}
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
FlareSolverr URL (Optional)
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={ebook.flaresolverrUrl || ''}
|
||||
onChange={(e) => updateEbook('flaresolverrUrl', e.target.value)}
|
||||
placeholder="http://localhost:8191"
|
||||
className="font-mono flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={testFlaresolverrConnection}
|
||||
loading={testingFlaresolverr}
|
||||
variant="secondary"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
Test
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
FlareSolverr helps bypass Cloudflare protection.
|
||||
</p>
|
||||
{flaresolverrTestResult && (
|
||||
<div
|
||||
className={`mt-2 p-3 rounded-lg text-sm ${
|
||||
flaresolverrTestResult.success
|
||||
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200 border border-green-200 dark:border-green-800'
|
||||
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200 border border-red-200 dark:border-red-800'
|
||||
}`}
|
||||
>
|
||||
{flaresolverrTestResult.success ? '✓ ' : '✗ '}
|
||||
{flaresolverrTestResult.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!ebook.flaresolverrUrl && (
|
||||
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-3">
|
||||
<p className="text-sm text-amber-800 dark:text-amber-200">
|
||||
<strong>Note:</strong> Without FlareSolverr, e-book downloads may fail if Anna's Archive
|
||||
has Cloudflare protection enabled.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Format Selection */}
|
||||
{ebook.enabled && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Preferred Format
|
||||
</label>
|
||||
<select
|
||||
value={ebook.preferredFormat || 'epub'}
|
||||
onChange={(e) => updateEbook('preferredFormat', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="epub">EPUB</option>
|
||||
<option value="pdf">PDF</option>
|
||||
<option value="mobi">MOBI</option>
|
||||
<option value="azw3">AZW3</option>
|
||||
<option value="any">Any format</option>
|
||||
</select>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
EPUB is recommended for most e-readers. "Any format" will download the first available format.
|
||||
</p>
|
||||
{/* ═══════════════════════════════════════════════════════════════════════
|
||||
SECTION 2: INDEXER SEARCH
|
||||
═══════════════════════════════════════════════════════════════════════ */}
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<div className="bg-gray-50 dark:bg-gray-800 px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 uppercase tracking-wider">
|
||||
Indexer Search
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Base URL (Advanced) */}
|
||||
{ebook.enabled && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Base URL (Advanced)
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={ebook.baseUrl || 'https://annas-archive.li'}
|
||||
onChange={(e) => updateEbook('baseUrl', e.target.value)}
|
||||
placeholder="https://annas-archive.li"
|
||||
className="font-mono"
|
||||
/>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Change this if the primary Anna's Archive mirror is unavailable.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* FlareSolverr (Optional - for Cloudflare bypass) */}
|
||||
{ebook.enabled && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
FlareSolverr URL (Optional)
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={ebook.flaresolverrUrl || ''}
|
||||
onChange={(e) => updateEbook('flaresolverrUrl', e.target.value)}
|
||||
placeholder="http://localhost:8191"
|
||||
className="font-mono flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={testFlaresolverrConnection}
|
||||
loading={testingFlaresolverr}
|
||||
variant="secondary"
|
||||
className="whitespace-nowrap"
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Enable Toggle */}
|
||||
<div className="flex items-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="indexer-search-enabled"
|
||||
checked={ebook.indexerSearchEnabled || false}
|
||||
onChange={(e) => updateEbook('indexerSearchEnabled', e.target.checked)}
|
||||
className="mt-1 h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<label
|
||||
htmlFor="indexer-search-enabled"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-gray-100 cursor-pointer"
|
||||
>
|
||||
Test Connection
|
||||
</Button>
|
||||
Enable Indexer Search
|
||||
</label>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Search for e-books via Prowlarr indexers (torrent/NZB sources).
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
FlareSolverr helps bypass Cloudflare protection on Anna's Archive.
|
||||
Leave empty if not needed.
|
||||
</p>
|
||||
{flaresolverrTestResult && (
|
||||
<div
|
||||
className={`mt-2 p-3 rounded-lg text-sm ${
|
||||
flaresolverrTestResult.success
|
||||
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200 border border-green-200 dark:border-green-800'
|
||||
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200 border border-red-200 dark:border-red-800'
|
||||
}`}
|
||||
>
|
||||
{flaresolverrTestResult.success ? '✓ ' : '✗ '}
|
||||
{flaresolverrTestResult.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!ebook.flaresolverrUrl && (
|
||||
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-3">
|
||||
<p className="text-sm text-amber-800 dark:text-amber-200">
|
||||
<strong>Note:</strong> Without FlareSolverr, e-book downloads may fail if Anna's Archive
|
||||
has Cloudflare protection enabled. Success rates are typically lower without it.
|
||||
|
||||
{/* Info hint about indexer settings */}
|
||||
{ebook.indexerSearchEnabled && (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
<strong>Configure Categories:</strong> E-book category settings are configured per-indexer
|
||||
in the <span className="font-medium">Indexers</span> tab. Look for the "EBook" tab when
|
||||
editing an indexer.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Coming soon notice */}
|
||||
{ebook.indexerSearchEnabled && (
|
||||
<div className="bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg p-3">
|
||||
<p className="text-sm text-purple-800 dark:text-purple-200">
|
||||
<strong>Coming Soon:</strong> Indexer search for e-books is not yet implemented.
|
||||
Enabling this setting prepares your configuration for when the feature is released.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════════
|
||||
SECTION 3: GENERAL SETTINGS
|
||||
═══════════════════════════════════════════════════════════════════════ */}
|
||||
{isAnySourceEnabled && (
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<div className="bg-gray-50 dark:bg-gray-800 px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 uppercase tracking-wider">
|
||||
General Settings
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Preferred Format */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Preferred Format
|
||||
</label>
|
||||
<select
|
||||
value={ebook.preferredFormat || 'epub'}
|
||||
onChange={(e) => updateEbook('preferredFormat', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="epub">EPUB (Recommended)</option>
|
||||
<option value="pdf">PDF</option>
|
||||
<option value="mobi">MOBI</option>
|
||||
<option value="azw3">AZW3</option>
|
||||
<option value="any">Any format</option>
|
||||
</select>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
EPUB is recommended for most e-readers. "Any format" accepts the first available.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<h3 className="text-sm font-semibold text-blue-900 dark:text-blue-100 mb-2">
|
||||
How it works
|
||||
</h3>
|
||||
<ul className="space-y-2 text-sm text-blue-800 dark:text-blue-200">
|
||||
<li>• Searches Anna's Archive in two ways:</li>
|
||||
<li className="ml-4">1. First tries ASIN (exact match - most accurate)</li>
|
||||
<li className="ml-4">2. Falls back to title + author (with book/language filters)</li>
|
||||
<li>• Downloads matching e-book in your preferred format</li>
|
||||
<li>• Places e-book file in the same folder as the audiobook</li>
|
||||
<li>• If no match is found or download fails, audiobook download continues normally</li>
|
||||
<li>• Completely optional and non-blocking</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Warning Box */}
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
|
||||
<h3 className="text-sm font-semibold text-yellow-900 dark:text-yellow-100 mb-2">
|
||||
⚠️ Important Note
|
||||
</h3>
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
Anna's Archive is a shadow library. Use of this feature is at your own discretion and responsibility.
|
||||
Ensure compliance with your local laws and regulations.
|
||||
</p>
|
||||
</div>
|
||||
{/* How it works - only show when Anna's Archive is enabled */}
|
||||
{ebook.annasArchiveEnabled && (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<h3 className="text-sm font-semibold text-blue-900 dark:text-blue-100 mb-2">
|
||||
How Anna's Archive works
|
||||
</h3>
|
||||
<ul className="space-y-1 text-sm text-blue-800 dark:text-blue-200">
|
||||
<li>• Searches by ASIN first (exact match), then title + author</li>
|
||||
<li>• Downloads matching e-book in your preferred format</li>
|
||||
<li>• Places e-book file in the same folder as the audiobook</li>
|
||||
<li>• If no match is found, audiobook download continues normally</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||
|
||||
@@ -77,7 +77,8 @@ export function useEbookSettings({ ebook, onChange, onSuccess, onError, markAsSa
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
enabled: ebook.enabled || false,
|
||||
annasArchiveEnabled: ebook.annasArchiveEnabled || false,
|
||||
indexerSearchEnabled: ebook.indexerSearchEnabled || false,
|
||||
format: ebook.preferredFormat || 'epub',
|
||||
baseUrl: ebook.baseUrl || 'https://annas-archive.li',
|
||||
flaresolverrUrl: ebook.flaresolverrUrl || '',
|
||||
@@ -98,6 +99,11 @@ export function useEbookSettings({ ebook, onChange, onSuccess, onError, markAsSa
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to check if any ebook source is enabled
|
||||
*/
|
||||
const isAnySourceEnabled = ebook.annasArchiveEnabled || ebook.indexerSearchEnabled;
|
||||
|
||||
return {
|
||||
saving,
|
||||
testingFlaresolverr,
|
||||
@@ -105,5 +111,6 @@ export function useEbookSettings({ ebook, onChange, onSuccess, onError, markAsSa
|
||||
updateEbook,
|
||||
testFlaresolverrConnection,
|
||||
saveSettings,
|
||||
isAnySourceEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ export async function PUT(request: NextRequest) {
|
||||
return requireAuth(request, async (req: AuthenticatedRequest) => {
|
||||
return requireAdmin(req, async () => {
|
||||
try {
|
||||
// Parse request body
|
||||
const { enabled, format, baseUrl, flaresolverrUrl } = await request.json();
|
||||
// Parse request body - new structure with separate source toggles
|
||||
const { annasArchiveEnabled, indexerSearchEnabled, format, baseUrl, flaresolverrUrl } = await request.json();
|
||||
|
||||
// Validate format
|
||||
const validFormats = ['epub', 'pdf', 'mobi', 'azw3', 'any'];
|
||||
@@ -25,8 +25,8 @@ export async function PUT(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Validate baseUrl (basic check)
|
||||
if (baseUrl && !baseUrl.startsWith('http')) {
|
||||
// Validate baseUrl (basic check) - only required if Anna's Archive is enabled
|
||||
if (annasArchiveEnabled && baseUrl && !baseUrl.startsWith('http')) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Base URL must start with http:// or https://' },
|
||||
{ status: 400 }
|
||||
@@ -46,23 +46,32 @@ export async function PUT(request: NextRequest) {
|
||||
const configService = getConfigService();
|
||||
|
||||
const configs = [
|
||||
// New granular source toggles
|
||||
{
|
||||
key: 'ebook_sidecar_enabled',
|
||||
value: enabled ? 'true' : 'false',
|
||||
key: 'ebook_annas_archive_enabled',
|
||||
value: annasArchiveEnabled ? 'true' : 'false',
|
||||
category: 'ebook',
|
||||
description: 'Enable e-book sidecar downloads from Annas Archive',
|
||||
description: 'Enable e-book downloads from Anna\'s Archive',
|
||||
},
|
||||
{
|
||||
key: 'ebook_indexer_search_enabled',
|
||||
value: indexerSearchEnabled ? 'true' : 'false',
|
||||
category: 'ebook',
|
||||
description: 'Enable e-book downloads via indexer search (Prowlarr)',
|
||||
},
|
||||
// General settings
|
||||
{
|
||||
key: 'ebook_sidecar_preferred_format',
|
||||
value: format || 'epub',
|
||||
category: 'ebook',
|
||||
description: 'Preferred e-book format',
|
||||
},
|
||||
// Anna's Archive specific settings
|
||||
{
|
||||
key: 'ebook_sidecar_base_url',
|
||||
value: baseUrl || 'https://annas-archive.li',
|
||||
category: 'ebook',
|
||||
description: 'Base URL for Annas Archive',
|
||||
description: 'Base URL for Anna\'s Archive',
|
||||
},
|
||||
{
|
||||
key: 'ebook_sidecar_flaresolverr_url',
|
||||
|
||||
@@ -19,7 +19,9 @@ interface SavedIndexerConfig {
|
||||
seedingTimeMinutes?: number; // Torrents only
|
||||
removeAfterProcessing?: boolean; // Usenet only
|
||||
rssEnabled?: boolean;
|
||||
categories?: number[]; // Array of category IDs (default: [3030] for audiobooks)
|
||||
audiobookCategories?: number[]; // Array of category IDs for audiobooks (default: [3030])
|
||||
ebookCategories?: number[]; // Array of category IDs for ebooks (default: [7020])
|
||||
categories?: number[]; // Legacy field for migration
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,6 +56,12 @@ export async function GET(request: NextRequest) {
|
||||
const isAdded = !!saved;
|
||||
const isTorrent = indexer.protocol?.toLowerCase() === 'torrent';
|
||||
|
||||
// Migration: if old 'categories' field exists but new fields don't, migrate
|
||||
const migratedAudiobookCategories = saved?.audiobookCategories ||
|
||||
saved?.categories || // Legacy migration
|
||||
[3030]; // Default to audiobooks category
|
||||
const migratedEbookCategories = saved?.ebookCategories || [7020]; // Default to ebooks category
|
||||
|
||||
const config: any = {
|
||||
id: indexer.id,
|
||||
name: indexer.name,
|
||||
@@ -63,7 +71,8 @@ export async function GET(request: NextRequest) {
|
||||
isAdded, // Explicit flag for UI (new card-based interface)
|
||||
priority: saved?.priority || 10,
|
||||
rssEnabled: saved?.rssEnabled ?? false,
|
||||
categories: saved?.categories || [3030], // Default to audiobooks category
|
||||
audiobookCategories: migratedAudiobookCategories,
|
||||
ebookCategories: migratedEbookCategories,
|
||||
supportsRss: indexer.capabilities?.supportsRss !== false, // Default to true if not specified
|
||||
};
|
||||
|
||||
@@ -117,7 +126,8 @@ export async function PUT(request: NextRequest) {
|
||||
protocol: indexer.protocol,
|
||||
priority: indexer.priority,
|
||||
rssEnabled: indexer.rssEnabled || false,
|
||||
categories: indexer.categories || [3030], // Default to audiobooks if not specified
|
||||
audiobookCategories: indexer.audiobookCategories || [3030], // Default to audiobooks
|
||||
ebookCategories: indexer.ebookCategories || [7020], // Default to ebooks
|
||||
};
|
||||
|
||||
// Add protocol-specific fields
|
||||
|
||||
@@ -100,10 +100,16 @@ export async function GET(request: NextRequest) {
|
||||
chapterMergingEnabled: configMap.get('chapter_merging_enabled') === 'true',
|
||||
},
|
||||
ebook: {
|
||||
enabled: configMap.get('ebook_sidecar_enabled') === 'true',
|
||||
preferredFormat: configMap.get('ebook_sidecar_preferred_format') || 'epub',
|
||||
// New granular source toggles (with migration from legacy ebook_sidecar_enabled)
|
||||
annasArchiveEnabled: configMap.get('ebook_annas_archive_enabled') === 'true' ||
|
||||
// Migration: if old key is true and new key doesn't exist, use old value
|
||||
(configMap.get('ebook_annas_archive_enabled') === undefined && configMap.get('ebook_sidecar_enabled') === 'true'),
|
||||
indexerSearchEnabled: configMap.get('ebook_indexer_search_enabled') === 'true',
|
||||
// Anna's Archive specific settings
|
||||
baseUrl: configMap.get('ebook_sidecar_base_url') || 'https://annas-archive.li',
|
||||
flaresolverrUrl: configMap.get('ebook_sidecar_flaresolverr_url') || '',
|
||||
// General settings
|
||||
preferredFormat: configMap.get('ebook_sidecar_preferred_format') || 'epub',
|
||||
},
|
||||
general: {
|
||||
appName: configMap.get('app_name') || 'ReadMeABook',
|
||||
|
||||
@@ -22,14 +22,30 @@ export async function POST(
|
||||
try {
|
||||
const { id: parentRequestId } = await params;
|
||||
|
||||
// Check if e-book sidecar is enabled
|
||||
const ebookEnabledConfig = await prisma.configuration.findUnique({
|
||||
where: { key: 'ebook_sidecar_enabled' },
|
||||
});
|
||||
// Check which ebook sources are enabled
|
||||
const [annasArchiveConfig, indexerSearchConfig, legacyConfig] = await Promise.all([
|
||||
prisma.configuration.findUnique({ where: { key: 'ebook_annas_archive_enabled' } }),
|
||||
prisma.configuration.findUnique({ where: { key: 'ebook_indexer_search_enabled' } }),
|
||||
prisma.configuration.findUnique({ where: { key: 'ebook_sidecar_enabled' } }),
|
||||
]);
|
||||
|
||||
if (ebookEnabledConfig?.value !== 'true') {
|
||||
// Legacy migration: check old key if new keys don't exist
|
||||
const isAnnasArchiveEnabled = annasArchiveConfig?.value === 'true' ||
|
||||
(annasArchiveConfig === null && legacyConfig?.value === 'true');
|
||||
const isIndexerSearchEnabled = indexerSearchConfig?.value === 'true';
|
||||
|
||||
// If no sources are enabled, return error
|
||||
if (!isAnnasArchiveEnabled && !isIndexerSearchEnabled) {
|
||||
return NextResponse.json(
|
||||
{ error: 'E-book sidecar feature is not enabled' },
|
||||
{ error: 'E-book sidecar feature is not enabled (no sources configured)' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// If only indexer search is enabled (not yet implemented), return error
|
||||
if (!isAnnasArchiveEnabled && isIndexerSearchEnabled) {
|
||||
return NextResponse.json(
|
||||
{ error: 'E-book indexer search is not yet implemented. Enable Anna\'s Archive to fetch e-books.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ interface SelectedIndexer {
|
||||
seedingTimeMinutes?: number; // Torrents only
|
||||
removeAfterProcessing?: boolean; // Usenet only
|
||||
rssEnabled: boolean;
|
||||
categories: number[];
|
||||
audiobookCategories: number[]; // Categories for audiobook searches
|
||||
ebookCategories: number[]; // Categories for ebook searches
|
||||
}
|
||||
|
||||
export function ProwlarrStep({
|
||||
|
||||
Reference in New Issue
Block a user