mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
Use .gl for Anna's Archive; add manual-import test
Replace default Anna's Archive base URL from https://annas-archive.li to https://annas-archive.gl across docs, UI components, API routes, processors, services, and tests. Add comprehensive tests for the admin manual-import API route and enhance the manual-import route to fetch missing ASIN details from Audnexus and create audiobook records with proper error handling and logging. Update related test expectations and FlareSolverr test usages to reflect the new default URL.
This commit is contained in:
@@ -51,7 +51,7 @@ Ebooks are first-class citizens in RMAB, with their own request type, tracking,
|
|||||||
| Key | Default | Description |
|
| Key | Default | Description |
|
||||||
|-----|---------|-------------|
|
|-----|---------|-------------|
|
||||||
| `ebook_annas_archive_enabled` | `false` | Enable Anna's Archive downloads |
|
| `ebook_annas_archive_enabled` | `false` | Enable Anna's Archive downloads |
|
||||||
| `ebook_sidecar_base_url` | `https://annas-archive.li` | Base URL for mirror |
|
| `ebook_sidecar_base_url` | `https://annas-archive.gl` | Base URL for mirror |
|
||||||
| `ebook_sidecar_flaresolverr_url` | `` (empty) | FlareSolverr proxy URL (optional) |
|
| `ebook_sidecar_flaresolverr_url` | `` (empty) | FlareSolverr proxy URL (optional) |
|
||||||
|
|
||||||
#### Section 2: Indexer Search
|
#### Section 2: Indexer Search
|
||||||
@@ -180,18 +180,18 @@ Configure URL in Admin Settings → E-book Sidecar: `http://localhost:8191`
|
|||||||
|
|
||||||
### Method 1: ASIN Search (exact match)
|
### Method 1: ASIN Search (exact match)
|
||||||
```
|
```
|
||||||
Search: https://annas-archive.li/search?ext=epub&lang=en&q="asin:B09TWSRMCB"
|
Search: https://annas-archive.gl/search?ext=epub&lang=en&q="asin:B09TWSRMCB"
|
||||||
↓
|
↓
|
||||||
MD5 Page: https://annas-archive.li/md5/[md5]
|
MD5 Page: https://annas-archive.gl/md5/[md5]
|
||||||
↓
|
↓
|
||||||
Slow Download: https://annas-archive.li/slow_download/[md5]/0/5
|
Slow Download: https://annas-archive.gl/slow_download/[md5]/0/5
|
||||||
↓
|
↓
|
||||||
File Server: http://[server]/path/to/file.epub
|
File Server: http://[server]/path/to/file.epub
|
||||||
```
|
```
|
||||||
|
|
||||||
### Method 2: Title + Author (fallback)
|
### Method 2: Title + Author (fallback)
|
||||||
```
|
```
|
||||||
Search: https://annas-archive.li/search?q=Title+Author&ext=epub&lang=en
|
Search: https://annas-archive.gl/search?q=Title+Author&ext=epub&lang=en
|
||||||
↓ (Same flow from MD5 page)
|
↓ (Same flow from MD5 page)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ src/app/admin/settings/
|
|||||||
|
|
||||||
1. **Anna's Archive Section**
|
1. **Anna's Archive Section**
|
||||||
- Enable toggle for Anna's Archive downloads
|
- Enable toggle for Anna's Archive downloads
|
||||||
- Base URL (default: `https://annas-archive.li`)
|
- Base URL (default: `https://annas-archive.gl`)
|
||||||
- FlareSolverr URL (optional, for Cloudflare bypass)
|
- FlareSolverr URL (optional, for Cloudflare bypass)
|
||||||
|
|
||||||
2. **Indexer Search Section**
|
2. **Indexer Search Section**
|
||||||
@@ -101,7 +101,7 @@ src/app/admin/settings/
|
|||||||
| `ebook_sidecar_preferred_format` | `epub` | Preferred format |
|
| `ebook_sidecar_preferred_format` | `epub` | Preferred format |
|
||||||
| `ebook_auto_grab_enabled` | `true` | Auto-create ebook requests after audiobook downloads |
|
| `ebook_auto_grab_enabled` | `true` | Auto-create ebook requests after audiobook downloads |
|
||||||
| `ebook_kindle_fix_enabled` | `false` | Apply Kindle compatibility fixes to EPUB files |
|
| `ebook_kindle_fix_enabled` | `false` | Apply Kindle compatibility fixes to EPUB files |
|
||||||
| `ebook_sidecar_base_url` | `https://annas-archive.li` | Anna's Archive mirror |
|
| `ebook_sidecar_base_url` | `https://annas-archive.gl` | Anna's Archive mirror |
|
||||||
| `ebook_sidecar_flaresolverr_url` | `` | FlareSolverr URL |
|
| `ebook_sidecar_flaresolverr_url` | `` | FlareSolverr URL |
|
||||||
|
|
||||||
**Behavior:**
|
**Behavior:**
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ function getInitialParams(): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RecentRequestsTable({ ebookSidecarEnabled = false, annasArchiveBaseUrl = 'https://annas-archive.li' }: RecentRequestsTableProps) {
|
export function RecentRequestsTable({ ebookSidecarEnabled = false, annasArchiveBaseUrl = 'https://annas-archive.gl' }: RecentRequestsTableProps) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
// Get initial filter state from URL (only evaluated once due to lazy init)
|
// Get initial filter state from URL (only evaluated once due to lazy init)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function RequestActionsDropdown({
|
|||||||
onFetchEbook,
|
onFetchEbook,
|
||||||
onSearchTermsUpdated,
|
onSearchTermsUpdated,
|
||||||
ebookSidecarEnabled = false,
|
ebookSidecarEnabled = false,
|
||||||
annasArchiveBaseUrl = 'https://annas-archive.li',
|
annasArchiveBaseUrl = 'https://annas-archive.gl',
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
}: RequestActionsDropdownProps) {
|
}: RequestActionsDropdownProps) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|||||||
@@ -90,9 +90,9 @@ export function EbookTab({ ebook, onChange, onSuccess, onError, markAsSaved }: E
|
|||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={ebook.baseUrl || 'https://annas-archive.li'}
|
value={ebook.baseUrl || 'https://annas-archive.gl'}
|
||||||
onChange={(e) => updateEbook('baseUrl', e.target.value)}
|
onChange={(e) => updateEbook('baseUrl', e.target.value)}
|
||||||
placeholder="https://annas-archive.li"
|
placeholder="https://annas-archive.gl"
|
||||||
className="font-mono"
|
className="font-mono"
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function useEbookSettings({ ebook, onChange, onSuccess, onError, markAsSa
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
url: ebook.flaresolverrUrl,
|
url: ebook.flaresolverrUrl,
|
||||||
baseUrl: ebook.baseUrl || 'https://annas-archive.li',
|
baseUrl: ebook.baseUrl || 'https://annas-archive.gl',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ export function useEbookSettings({ ebook, onChange, onSuccess, onError, markAsSa
|
|||||||
annasArchiveEnabled: ebook.annasArchiveEnabled || false,
|
annasArchiveEnabled: ebook.annasArchiveEnabled || false,
|
||||||
indexerSearchEnabled: ebook.indexerSearchEnabled || false,
|
indexerSearchEnabled: ebook.indexerSearchEnabled || false,
|
||||||
format: ebook.preferredFormat || 'epub',
|
format: ebook.preferredFormat || 'epub',
|
||||||
baseUrl: ebook.baseUrl || 'https://annas-archive.li',
|
baseUrl: ebook.baseUrl || 'https://annas-archive.gl',
|
||||||
flaresolverrUrl: ebook.flaresolverrUrl || '',
|
flaresolverrUrl: ebook.flaresolverrUrl || '',
|
||||||
autoGrabEnabled: ebook.autoGrabEnabled ?? true,
|
autoGrabEnabled: ebook.autoGrabEnabled ?? true,
|
||||||
kindleFixEnabled: ebook.kindleFixEnabled ?? false,
|
kindleFixEnabled: ebook.kindleFixEnabled ?? false,
|
||||||
|
|||||||
@@ -155,10 +155,42 @@ export async function POST(request: NextRequest) {
|
|||||||
audiobookId = newBook.id;
|
audiobookId = newBook.id;
|
||||||
logger.info(`Created audiobook record from cache for ASIN ${asin}: ${newBook.id}`);
|
logger.info(`Created audiobook record from cache for ASIN ${asin}: ${newBook.id}`);
|
||||||
} else {
|
} else {
|
||||||
return NextResponse.json(
|
// Not in DB — fetch live from Audnexus and create a record
|
||||||
{ error: 'Audiobook not found for the given ASIN' },
|
try {
|
||||||
{ status: 404 }
|
const audibleService = getAudibleService();
|
||||||
);
|
const liveData = await audibleService.getAudiobookDetails(asin);
|
||||||
|
if (liveData) {
|
||||||
|
const newBook = await prisma.audiobook.create({
|
||||||
|
data: {
|
||||||
|
audibleAsin: asin,
|
||||||
|
title: liveData.title,
|
||||||
|
author: liveData.author,
|
||||||
|
coverArtUrl: liveData.coverArtUrl,
|
||||||
|
narrator: liveData.narrator,
|
||||||
|
series: liveData.series,
|
||||||
|
seriesPart: liveData.seriesPart,
|
||||||
|
seriesAsin: liveData.seriesAsin,
|
||||||
|
year: liveData.releaseDate
|
||||||
|
? new Date(liveData.releaseDate).getFullYear() || undefined
|
||||||
|
: undefined,
|
||||||
|
status: 'pending',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
audiobookId = newBook.id;
|
||||||
|
logger.info(`Created audiobook record from Audnexus for ASIN ${asin}: ${newBook.id}`);
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Audiobook not found for the given ASIN' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (audnexusError) {
|
||||||
|
logger.error(`Failed to fetch ASIN ${asin} from Audnexus: ${audnexusError instanceof Error ? audnexusError.message : String(audnexusError)}`);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Audiobook not found for the given ASIN' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export async function PUT(request: NextRequest) {
|
|||||||
// Anna's Archive specific settings
|
// Anna's Archive specific settings
|
||||||
{
|
{
|
||||||
key: 'ebook_sidecar_base_url',
|
key: 'ebook_sidecar_base_url',
|
||||||
value: baseUrl || 'https://annas-archive.li',
|
value: baseUrl || 'https://annas-archive.gl',
|
||||||
category: 'ebook',
|
category: 'ebook',
|
||||||
description: 'Base URL for Anna\'s Archive',
|
description: 'Base URL for Anna\'s Archive',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export async function GET(request: NextRequest) {
|
|||||||
(configMap.get('ebook_annas_archive_enabled') === undefined && configMap.get('ebook_sidecar_enabled') === 'true'),
|
(configMap.get('ebook_annas_archive_enabled') === undefined && configMap.get('ebook_sidecar_enabled') === 'true'),
|
||||||
indexerSearchEnabled: configMap.get('ebook_indexer_search_enabled') === 'true',
|
indexerSearchEnabled: configMap.get('ebook_indexer_search_enabled') === 'true',
|
||||||
// Anna's Archive specific settings
|
// Anna's Archive specific settings
|
||||||
baseUrl: configMap.get('ebook_sidecar_base_url') || 'https://annas-archive.li',
|
baseUrl: configMap.get('ebook_sidecar_base_url') || 'https://annas-archive.gl',
|
||||||
flaresolverrUrl: configMap.get('ebook_sidecar_flaresolverr_url') || '',
|
flaresolverrUrl: configMap.get('ebook_sidecar_flaresolverr_url') || '',
|
||||||
// General settings
|
// General settings
|
||||||
preferredFormat: configMap.get('ebook_sidecar_preferred_format') || 'epub',
|
preferredFormat: configMap.get('ebook_sidecar_preferred_format') || 'epub',
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ export async function POST(
|
|||||||
const isAnnasArchiveEnabled = annasArchiveEnabled === 'true';
|
const isAnnasArchiveEnabled = annasArchiveEnabled === 'true';
|
||||||
const isIndexerSearchEnabled = indexerSearchEnabled === 'true';
|
const isIndexerSearchEnabled = indexerSearchEnabled === 'true';
|
||||||
const format = preferredFormat || 'epub';
|
const format = preferredFormat || 'epub';
|
||||||
const annasBaseUrl = baseUrl || 'https://annas-archive.li';
|
const annasBaseUrl = baseUrl || 'https://annas-archive.gl';
|
||||||
|
|
||||||
// Get language code from Audible region config
|
// Get language code from Audible region config
|
||||||
const region = await configService.getAudibleRegion() as AudibleRegion;
|
const region = await configService.getAudibleRegion() as AudibleRegion;
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export async function POST(
|
|||||||
const isAnnasArchiveEnabled = annasArchiveEnabled === 'true';
|
const isAnnasArchiveEnabled = annasArchiveEnabled === 'true';
|
||||||
const isIndexerSearchEnabled = indexerSearchEnabled === 'true';
|
const isIndexerSearchEnabled = indexerSearchEnabled === 'true';
|
||||||
const format = preferredFormat || 'epub';
|
const format = preferredFormat || 'epub';
|
||||||
const annasBaseUrl = baseUrl || 'https://annas-archive.li';
|
const annasBaseUrl = baseUrl || 'https://annas-archive.gl';
|
||||||
|
|
||||||
// Get language code from Audible region config
|
// Get language code from Audible region config
|
||||||
const region = await configService.getAudibleRegion() as AudibleRegion;
|
const region = await configService.getAudibleRegion() as AudibleRegion;
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export async function processStartDirectDownload(payload: StartDirectDownloadPay
|
|||||||
// Get download configuration
|
// Get download configuration
|
||||||
const configService = getConfigService();
|
const configService = getConfigService();
|
||||||
const downloadsDir = await configService.get('download_dir') || '/downloads';
|
const downloadsDir = await configService.get('download_dir') || '/downloads';
|
||||||
const baseUrl = await configService.get('ebook_sidecar_base_url') || 'https://annas-archive.li';
|
const baseUrl = await configService.get('ebook_sidecar_base_url') || 'https://annas-archive.gl';
|
||||||
const preferredFormat = await configService.get('ebook_sidecar_preferred_format') || 'epub';
|
const preferredFormat = await configService.get('ebook_sidecar_preferred_format') || 'epub';
|
||||||
const flaresolverrUrl = await configService.get('ebook_sidecar_flaresolverr_url') || undefined;
|
const flaresolverrUrl = await configService.get('ebook_sidecar_flaresolverr_url') || undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ async function searchAnnasArchive(
|
|||||||
logger: RMABLogger
|
logger: RMABLogger
|
||||||
): Promise<EbookSearchResult | null> {
|
): Promise<EbookSearchResult | null> {
|
||||||
const configService = getConfigService();
|
const configService = getConfigService();
|
||||||
const baseUrl = await configService.get('ebook_sidecar_base_url') || 'https://annas-archive.li';
|
const baseUrl = await configService.get('ebook_sidecar_base_url') || 'https://annas-archive.gl';
|
||||||
const flaresolverrUrl = await configService.get('ebook_sidecar_flaresolverr_url') || undefined;
|
const flaresolverrUrl = await configService.get('ebook_sidecar_flaresolverr_url') || undefined;
|
||||||
|
|
||||||
// Get language code from Audible region config
|
// Get language code from Audible region config
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ async function fetchHtml(
|
|||||||
*/
|
*/
|
||||||
export async function testFlareSolverrConnection(
|
export async function testFlareSolverrConnection(
|
||||||
flaresolverrUrl: string,
|
flaresolverrUrl: string,
|
||||||
baseUrl: string = 'https://annas-archive.li'
|
baseUrl: string = 'https://annas-archive.gl'
|
||||||
): Promise<{ success: boolean; message: string; responseTime?: number }> {
|
): Promise<{ success: boolean; message: string; responseTime?: number }> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ export async function downloadEbook(
|
|||||||
author: string,
|
author: string,
|
||||||
targetDir: string,
|
targetDir: string,
|
||||||
preferredFormat: string = 'epub',
|
preferredFormat: string = 'epub',
|
||||||
baseUrl: string = 'https://annas-archive.li',
|
baseUrl: string = 'https://annas-archive.gl',
|
||||||
logger?: RMABLogger,
|
logger?: RMABLogger,
|
||||||
flaresolverrUrl?: string,
|
flaresolverrUrl?: string,
|
||||||
languageCode: string = 'en'
|
languageCode: string = 'en'
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
/**
|
||||||
|
* Component: Admin Manual Import API Route Tests
|
||||||
|
* Documentation: documentation/features/manual-import.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { createPrismaMock } from '../helpers/prisma';
|
||||||
|
|
||||||
|
let authRequest: any;
|
||||||
|
let requestBody: any;
|
||||||
|
|
||||||
|
const prismaMock = createPrismaMock();
|
||||||
|
const requireAuthMock = vi.hoisted(() => vi.fn());
|
||||||
|
const requireAdminMock = vi.hoisted(() => vi.fn());
|
||||||
|
const jobQueueMock = vi.hoisted(() => ({
|
||||||
|
addOrganizeJob: vi.fn(() => Promise.resolve()),
|
||||||
|
}));
|
||||||
|
const audibleServiceMock = vi.hoisted(() => ({
|
||||||
|
getAudiobookDetails: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// fs mock
|
||||||
|
const fsMock = vi.hoisted(() => ({
|
||||||
|
stat: vi.fn(),
|
||||||
|
readdir: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/lib/db', () => ({
|
||||||
|
prisma: prismaMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/lib/middleware/auth', () => ({
|
||||||
|
requireAuth: requireAuthMock,
|
||||||
|
requireAdmin: requireAdminMock,
|
||||||
|
AuthenticatedRequest: {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/lib/services/job-queue.service', () => ({
|
||||||
|
getJobQueueService: () => jobQueueMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/lib/integrations/audible.service', () => ({
|
||||||
|
getAudibleService: () => audibleServiceMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('fs/promises', () => fsMock);
|
||||||
|
|
||||||
|
vi.mock('path', async () => {
|
||||||
|
const actual = await vi.importActual<typeof import('path')>('path');
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
default: actual,
|
||||||
|
resolve: (...args: string[]) => actual.posix.resolve(...args),
|
||||||
|
extname: actual.posix.extname,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Admin manual-import route', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.resetModules();
|
||||||
|
|
||||||
|
authRequest = { user: { id: 'admin-1', role: 'admin' } };
|
||||||
|
requestBody = { asin: 'B00TEST0001', folderPath: '/bookdrop/author/title' };
|
||||||
|
|
||||||
|
requireAuthMock.mockImplementation((_req: any, handler: any) => handler(authRequest));
|
||||||
|
requireAdminMock.mockImplementation((_req: any, handler: any) => handler());
|
||||||
|
|
||||||
|
// Default: download_dir and media_dir not configured, bookdrop exists
|
||||||
|
prismaMock.configuration.findUnique.mockResolvedValue(null);
|
||||||
|
fsMock.stat.mockImplementation(async (p: string) => {
|
||||||
|
if (p === '/bookdrop') return { isDirectory: () => true };
|
||||||
|
if (p === '/bookdrop/author/title') return { isDirectory: () => true };
|
||||||
|
throw new Error('ENOENT');
|
||||||
|
});
|
||||||
|
fsMock.readdir.mockResolvedValue([
|
||||||
|
{ name: 'chapter1.m4b', isFile: () => true },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates audiobook from Audnexus when ASIN is not in DB or cache', async () => {
|
||||||
|
// Neither audiobook nor audibleCache has this ASIN
|
||||||
|
prismaMock.audiobook.findFirst.mockResolvedValueOnce(null);
|
||||||
|
prismaMock.audibleCache.findUnique.mockResolvedValueOnce(null);
|
||||||
|
|
||||||
|
// Audnexus returns live data
|
||||||
|
audibleServiceMock.getAudiobookDetails.mockResolvedValueOnce({
|
||||||
|
asin: 'B00TEST0001',
|
||||||
|
title: 'Live Title',
|
||||||
|
author: 'Live Author',
|
||||||
|
coverArtUrl: 'https://example.com/cover.jpg',
|
||||||
|
narrator: 'Live Narrator',
|
||||||
|
series: 'Test Series',
|
||||||
|
seriesPart: '1',
|
||||||
|
seriesAsin: 'SERIES0001',
|
||||||
|
releaseDate: '2024-01-15',
|
||||||
|
});
|
||||||
|
|
||||||
|
// audiobook.create returns the new record
|
||||||
|
prismaMock.audiobook.create.mockResolvedValueOnce({
|
||||||
|
id: 'ab-new',
|
||||||
|
audibleAsin: 'B00TEST0001',
|
||||||
|
title: 'Live Title',
|
||||||
|
author: 'Live Author',
|
||||||
|
status: 'pending',
|
||||||
|
});
|
||||||
|
|
||||||
|
// audiobook.findUnique for the verification step
|
||||||
|
prismaMock.audiobook.findUnique.mockResolvedValueOnce({
|
||||||
|
id: 'ab-new',
|
||||||
|
audibleAsin: 'B00TEST0001',
|
||||||
|
title: 'Live Title',
|
||||||
|
author: 'Live Author',
|
||||||
|
status: 'pending',
|
||||||
|
});
|
||||||
|
|
||||||
|
// No existing request
|
||||||
|
prismaMock.request.findFirst.mockResolvedValueOnce(null);
|
||||||
|
prismaMock.request.create.mockResolvedValueOnce({ id: 'req-new' });
|
||||||
|
|
||||||
|
const { POST } = await import('@/app/api/admin/manual-import/route');
|
||||||
|
const request = {
|
||||||
|
json: vi.fn().mockResolvedValue(requestBody),
|
||||||
|
nextUrl: new URL('http://localhost/api/admin/manual-import'),
|
||||||
|
};
|
||||||
|
const response = await POST(request as any);
|
||||||
|
const payload = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(payload.success).toBe(true);
|
||||||
|
expect(audibleServiceMock.getAudiobookDetails).toHaveBeenCalledWith('B00TEST0001');
|
||||||
|
expect(prismaMock.audiobook.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: expect.objectContaining({
|
||||||
|
audibleAsin: 'B00TEST0001',
|
||||||
|
title: 'Live Title',
|
||||||
|
author: 'Live Author',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 404 when ASIN is not in DB, cache, or Audnexus', async () => {
|
||||||
|
prismaMock.audiobook.findFirst.mockResolvedValueOnce(null);
|
||||||
|
prismaMock.audibleCache.findUnique.mockResolvedValueOnce(null);
|
||||||
|
audibleServiceMock.getAudiobookDetails.mockResolvedValueOnce(null);
|
||||||
|
|
||||||
|
const { POST } = await import('@/app/api/admin/manual-import/route');
|
||||||
|
const request = {
|
||||||
|
json: vi.fn().mockResolvedValue(requestBody),
|
||||||
|
nextUrl: new URL('http://localhost/api/admin/manual-import'),
|
||||||
|
};
|
||||||
|
const response = await POST(request as any);
|
||||||
|
const payload = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
expect(payload.error).toBe('Audiobook not found for the given ASIN');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 404 when Audnexus lookup throws an error', async () => {
|
||||||
|
prismaMock.audiobook.findFirst.mockResolvedValueOnce(null);
|
||||||
|
prismaMock.audibleCache.findUnique.mockResolvedValueOnce(null);
|
||||||
|
audibleServiceMock.getAudiobookDetails.mockRejectedValueOnce(new Error('Network timeout'));
|
||||||
|
|
||||||
|
const { POST } = await import('@/app/api/admin/manual-import/route');
|
||||||
|
const request = {
|
||||||
|
json: vi.fn().mockResolvedValue(requestBody),
|
||||||
|
nextUrl: new URL('http://localhost/api/admin/manual-import'),
|
||||||
|
};
|
||||||
|
const response = await POST(request as any);
|
||||||
|
const payload = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
expect(payload.error).toBe('Audiobook not found for the given ASIN');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses existing audiobook record when ASIN is in DB', async () => {
|
||||||
|
prismaMock.audiobook.findFirst.mockResolvedValueOnce({
|
||||||
|
id: 'ab-existing',
|
||||||
|
audibleAsin: 'B00TEST0001',
|
||||||
|
});
|
||||||
|
|
||||||
|
prismaMock.audiobook.findUnique.mockResolvedValueOnce({
|
||||||
|
id: 'ab-existing',
|
||||||
|
audibleAsin: 'B00TEST0001',
|
||||||
|
title: 'Existing Title',
|
||||||
|
author: 'Existing Author',
|
||||||
|
status: 'pending',
|
||||||
|
});
|
||||||
|
|
||||||
|
prismaMock.request.findFirst.mockResolvedValueOnce(null);
|
||||||
|
prismaMock.request.create.mockResolvedValueOnce({ id: 'req-1' });
|
||||||
|
|
||||||
|
const { POST } = await import('@/app/api/admin/manual-import/route');
|
||||||
|
const request = {
|
||||||
|
json: vi.fn().mockResolvedValue(requestBody),
|
||||||
|
nextUrl: new URL('http://localhost/api/admin/manual-import'),
|
||||||
|
};
|
||||||
|
const response = await POST(request as any);
|
||||||
|
const payload = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(payload.success).toBe(true);
|
||||||
|
// Should NOT have queried audibleCache for ASIN resolution
|
||||||
|
expect(prismaMock.audibleCache.findUnique).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses audibleCache when ASIN is not in audiobook table but is cached', async () => {
|
||||||
|
prismaMock.audiobook.findFirst.mockResolvedValueOnce(null);
|
||||||
|
prismaMock.audibleCache.findUnique.mockResolvedValueOnce({
|
||||||
|
asin: 'B00TEST0001',
|
||||||
|
title: 'Cached Title',
|
||||||
|
author: 'Cached Author',
|
||||||
|
coverArtUrl: 'https://example.com/cached.jpg',
|
||||||
|
narrator: 'Cached Narrator',
|
||||||
|
});
|
||||||
|
|
||||||
|
// audiobook.create from cache
|
||||||
|
prismaMock.audiobook.create.mockResolvedValueOnce({
|
||||||
|
id: 'ab-from-cache',
|
||||||
|
audibleAsin: 'B00TEST0001',
|
||||||
|
title: 'Cached Title',
|
||||||
|
author: 'Cached Author',
|
||||||
|
status: 'pending',
|
||||||
|
});
|
||||||
|
|
||||||
|
prismaMock.audiobook.findUnique.mockResolvedValueOnce({
|
||||||
|
id: 'ab-from-cache',
|
||||||
|
audibleAsin: 'B00TEST0001',
|
||||||
|
title: 'Cached Title',
|
||||||
|
author: 'Cached Author',
|
||||||
|
status: 'pending',
|
||||||
|
});
|
||||||
|
|
||||||
|
prismaMock.request.findFirst.mockResolvedValueOnce(null);
|
||||||
|
prismaMock.request.create.mockResolvedValueOnce({ id: 'req-2' });
|
||||||
|
|
||||||
|
const { POST } = await import('@/app/api/admin/manual-import/route');
|
||||||
|
const request = {
|
||||||
|
json: vi.fn().mockResolvedValue(requestBody),
|
||||||
|
nextUrl: new URL('http://localhost/api/admin/manual-import'),
|
||||||
|
};
|
||||||
|
const response = await POST(request as any);
|
||||||
|
const payload = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(payload.success).toBe(true);
|
||||||
|
// audiobook.create should have used cache data, not Audnexus
|
||||||
|
expect(prismaMock.audiobook.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: expect.objectContaining({
|
||||||
|
title: 'Cached Title',
|
||||||
|
author: 'Cached Author',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -355,7 +355,7 @@ describe('Admin settings core routes', () => {
|
|||||||
|
|
||||||
it('updates ebook settings', async () => {
|
it('updates ebook settings', async () => {
|
||||||
const request = {
|
const request = {
|
||||||
json: vi.fn().mockResolvedValue({ enabled: true, format: 'epub', baseUrl: 'https://annas-archive.li' }),
|
json: vi.fn().mockResolvedValue({ enabled: true, format: 'epub', baseUrl: 'https://annas-archive.gl' }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { PUT } = await import('@/app/api/admin/settings/ebook/route');
|
const { PUT } = await import('@/app/api/admin/settings/ebook/route');
|
||||||
|
|||||||
@@ -348,14 +348,14 @@ describe('Admin settings test routes', () => {
|
|||||||
|
|
||||||
it('tests FlareSolverr connection', async () => {
|
it('tests FlareSolverr connection', async () => {
|
||||||
testFlareSolverrMock.mockResolvedValueOnce({ success: true });
|
testFlareSolverrMock.mockResolvedValueOnce({ success: true });
|
||||||
const request = { json: vi.fn().mockResolvedValue({ url: 'http://flare', baseUrl: 'https://annas-archive.li' }) };
|
const request = { json: vi.fn().mockResolvedValue({ url: 'http://flare', baseUrl: 'https://annas-archive.gl' }) };
|
||||||
|
|
||||||
const { POST } = await import('@/app/api/admin/settings/ebook/test-flaresolverr/route');
|
const { POST } = await import('@/app/api/admin/settings/ebook/test-flaresolverr/route');
|
||||||
const response = await POST(request as any);
|
const response = await POST(request as any);
|
||||||
const payload = await response.json();
|
const payload = await response.json();
|
||||||
|
|
||||||
expect(payload.success).toBe(true);
|
expect(payload.success).toBe(true);
|
||||||
expect(testFlareSolverrMock).toHaveBeenCalledWith('http://flare', 'https://annas-archive.li');
|
expect(testFlareSolverrMock).toHaveBeenCalledWith('http://flare', 'https://annas-archive.gl');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects FlareSolverr test when URL is missing', async () => {
|
it('rejects FlareSolverr test when URL is missing', async () => {
|
||||||
@@ -382,7 +382,7 @@ describe('Admin settings test routes', () => {
|
|||||||
|
|
||||||
it('returns error when FlareSolverr test throws', async () => {
|
it('returns error when FlareSolverr test throws', async () => {
|
||||||
testFlareSolverrMock.mockRejectedValueOnce(new Error('flare down'));
|
testFlareSolverrMock.mockRejectedValueOnce(new Error('flare down'));
|
||||||
const request = { json: vi.fn().mockResolvedValue({ url: 'http://flare', baseUrl: 'https://annas-archive.li' }) };
|
const request = { json: vi.fn().mockResolvedValue({ url: 'http://flare', baseUrl: 'https://annas-archive.gl' }) };
|
||||||
|
|
||||||
const { POST } = await import('@/app/api/admin/settings/ebook/test-flaresolverr/route');
|
const { POST } = await import('@/app/api/admin/settings/ebook/test-flaresolverr/route');
|
||||||
const response = await POST(request as any);
|
const response = await POST(request as any);
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ describe('RequestActionsDropdown', () => {
|
|||||||
author: 'Author',
|
author: 'Author',
|
||||||
status: 'downloaded',
|
status: 'downloaded',
|
||||||
type: 'ebook',
|
type: 'ebook',
|
||||||
torrentUrl: JSON.stringify(['https://annas-archive.li/slow_download/abc123def456abc123def456abc123de/0/5']),
|
torrentUrl: JSON.stringify(['https://annas-archive.gl/slow_download/abc123def456abc123def456abc123de/0/5']),
|
||||||
}}
|
}}
|
||||||
onManualSearch={vi.fn().mockResolvedValue(undefined)}
|
onManualSearch={vi.fn().mockResolvedValue(undefined)}
|
||||||
onCancel={vi.fn().mockResolvedValue(undefined)}
|
onCancel={vi.fn().mockResolvedValue(undefined)}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const renderHook = <T,>(hook: () => T) => {
|
|||||||
const baseEbook = {
|
const baseEbook = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
preferredFormat: 'epub',
|
preferredFormat: 'epub',
|
||||||
baseUrl: 'https://annas-archive.li',
|
baseUrl: 'https://annas-archive.gl',
|
||||||
flaresolverrUrl: 'http://flare',
|
flaresolverrUrl: 'http://flare',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ describe('useEbookSettings', () => {
|
|||||||
expect(result.current.flaresolverrTestResult?.success).toBe(true);
|
expect(result.current.flaresolverrTestResult?.success).toBe(true);
|
||||||
// Verify baseUrl is included in the request body
|
// Verify baseUrl is included in the request body
|
||||||
const callBody = JSON.parse(fetchWithAuthMock.mock.calls[0][1].body);
|
const callBody = JSON.parse(fetchWithAuthMock.mock.calls[0][1].body);
|
||||||
expect(callBody.baseUrl).toBe('https://annas-archive.li');
|
expect(callBody.baseUrl).toBe('https://annas-archive.gl');
|
||||||
expect(callBody.url).toBe('http://flare');
|
expect(callBody.url).toBe('http://flare');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ describe('IndexersTab - Auto-load Indexers on Tab Activation', () => {
|
|||||||
ebook: {
|
ebook: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
preferredFormat: 'epub',
|
preferredFormat: 'epub',
|
||||||
baseUrl: 'https://annas-archive.li',
|
baseUrl: 'https://annas-archive.gl',
|
||||||
flaresolverrUrl: '',
|
flaresolverrUrl: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ describe('processStartDirectDownload', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
configServiceMock.get.mockImplementation(async (key: string) => {
|
configServiceMock.get.mockImplementation(async (key: string) => {
|
||||||
if (key === 'downloads_dir') return '/downloads';
|
if (key === 'downloads_dir') return '/downloads';
|
||||||
if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.li';
|
if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.gl';
|
||||||
if (key === 'ebook_sidecar_preferred_format') return 'epub';
|
if (key === 'ebook_sidecar_preferred_format') return 'epub';
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
@@ -238,7 +238,7 @@ describe('processStartDirectDownload', () => {
|
|||||||
|
|
||||||
configServiceMock.get.mockImplementation(async (key: string) => {
|
configServiceMock.get.mockImplementation(async (key: string) => {
|
||||||
if (key === 'downloads_dir') return '/downloads';
|
if (key === 'downloads_dir') return '/downloads';
|
||||||
if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.li';
|
if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.gl';
|
||||||
if (key === 'ebook_sidecar_preferred_format') return 'epub';
|
if (key === 'ebook_sidecar_preferred_format') return 'epub';
|
||||||
if (key === 'ebook_sidecar_flaresolverr_url') return 'http://flaresolverr:8191';
|
if (key === 'ebook_sidecar_flaresolverr_url') return 'http://flaresolverr:8191';
|
||||||
return null;
|
return null;
|
||||||
@@ -286,7 +286,7 @@ describe('processStartDirectDownload', () => {
|
|||||||
|
|
||||||
expect(ebookScraperMock.extractDownloadUrl).toHaveBeenCalledWith(
|
expect(ebookScraperMock.extractDownloadUrl).toHaveBeenCalledWith(
|
||||||
'https://slow.example.com/book',
|
'https://slow.example.com/book',
|
||||||
'https://annas-archive.li',
|
'https://annas-archive.gl',
|
||||||
'epub',
|
'epub',
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
'http://flaresolverr:8191'
|
'http://flaresolverr:8191'
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ describe('processSearchEbook', () => {
|
|||||||
configServiceMock.getAudibleRegion.mockResolvedValue('us');
|
configServiceMock.getAudibleRegion.mockResolvedValue('us');
|
||||||
configServiceMock.get.mockImplementation(async (key: string) => {
|
configServiceMock.get.mockImplementation(async (key: string) => {
|
||||||
if (key === 'ebook_sidecar_preferred_format') return 'epub';
|
if (key === 'ebook_sidecar_preferred_format') return 'epub';
|
||||||
if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.li';
|
if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.gl';
|
||||||
if (key === 'ebook_annas_archive_enabled') return 'true';
|
if (key === 'ebook_annas_archive_enabled') return 'true';
|
||||||
if (key === 'ebook_indexer_search_enabled') return 'false';
|
if (key === 'ebook_indexer_search_enabled') return 'false';
|
||||||
return null;
|
return null;
|
||||||
@@ -79,7 +79,7 @@ describe('processSearchEbook', () => {
|
|||||||
expect(ebookScraperMock.searchByAsin).toHaveBeenCalledWith(
|
expect(ebookScraperMock.searchByAsin).toHaveBeenCalledWith(
|
||||||
'B001ASIN',
|
'B001ASIN',
|
||||||
'epub',
|
'epub',
|
||||||
'https://annas-archive.li',
|
'https://annas-archive.gl',
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
undefined,
|
undefined,
|
||||||
'en'
|
'en'
|
||||||
@@ -124,7 +124,7 @@ describe('processSearchEbook', () => {
|
|||||||
'Another Book',
|
'Another Book',
|
||||||
'Another Author',
|
'Another Author',
|
||||||
'epub',
|
'epub',
|
||||||
'https://annas-archive.li',
|
'https://annas-archive.gl',
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
undefined,
|
undefined,
|
||||||
'en'
|
'en'
|
||||||
@@ -229,7 +229,7 @@ describe('processSearchEbook', () => {
|
|||||||
|
|
||||||
configServiceMock.get.mockImplementation(async (key: string) => {
|
configServiceMock.get.mockImplementation(async (key: string) => {
|
||||||
if (key === 'ebook_sidecar_preferred_format') return 'epub';
|
if (key === 'ebook_sidecar_preferred_format') return 'epub';
|
||||||
if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.li';
|
if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.gl';
|
||||||
if (key === 'ebook_sidecar_flaresolverr_url') return 'http://flaresolverr:8191';
|
if (key === 'ebook_sidecar_flaresolverr_url') return 'http://flaresolverr:8191';
|
||||||
if (key === 'ebook_annas_archive_enabled') return 'true';
|
if (key === 'ebook_annas_archive_enabled') return 'true';
|
||||||
if (key === 'ebook_indexer_search_enabled') return 'false';
|
if (key === 'ebook_indexer_search_enabled') return 'false';
|
||||||
@@ -255,7 +255,7 @@ describe('processSearchEbook', () => {
|
|||||||
expect(ebookScraperMock.searchByAsin).toHaveBeenCalledWith(
|
expect(ebookScraperMock.searchByAsin).toHaveBeenCalledWith(
|
||||||
'B006ASIN',
|
'B006ASIN',
|
||||||
'epub',
|
'epub',
|
||||||
'https://annas-archive.li',
|
'https://annas-archive.gl',
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
'http://flaresolverr:8191',
|
'http://flaresolverr:8191',
|
||||||
'en'
|
'en'
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ describe('E-book sidecar', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.li');
|
const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.gl');
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.responseTime).toBeTypeOf('number');
|
expect(result.responseTime).toBeTypeOf('number');
|
||||||
@@ -95,7 +95,7 @@ describe('E-book sidecar', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.li');
|
const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.gl');
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -103,7 +103,7 @@ describe('E-book sidecar', () => {
|
|||||||
it('returns error details when FlareSolverr request fails', async () => {
|
it('returns error details when FlareSolverr request fails', async () => {
|
||||||
axiosMock.post.mockRejectedValue(new Error('flare down'));
|
axiosMock.post.mockRejectedValue(new Error('flare down'));
|
||||||
|
|
||||||
const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.li');
|
const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.gl');
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
expect(result.message).toContain('flare down');
|
expect(result.message).toContain('flare down');
|
||||||
@@ -117,7 +117,7 @@ describe('E-book sidecar', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.li');
|
const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.gl');
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
expect(result.message).toContain('FlareSolverr error');
|
expect(result.message).toContain('FlareSolverr error');
|
||||||
@@ -132,7 +132,7 @@ describe('E-book sidecar', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.li');
|
const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.gl');
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
expect(result.message).toContain('FlareSolverr returned HTTP 403');
|
expect(result.message).toContain('FlareSolverr returned HTTP 403');
|
||||||
@@ -221,7 +221,7 @@ describe('E-book sidecar', () => {
|
|||||||
throw new Error(`Unexpected URL: ${url}`);
|
throw new Error(`Unexpected URL: ${url}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const promise = downloadEbook('ASIN-NO', 'Title', 'Author', '/downloads', 'pdf', 'https://annas-archive.li', undefined, 'http://flare');
|
const promise = downloadEbook('ASIN-NO', 'Title', 'Author', '/downloads', 'pdf', 'https://annas-archive.gl', undefined, 'http://flare');
|
||||||
await vi.runAllTimersAsync();
|
await vi.runAllTimersAsync();
|
||||||
const result = await promise;
|
const result = await promise;
|
||||||
|
|
||||||
@@ -417,7 +417,7 @@ describe('E-book sidecar', () => {
|
|||||||
'Author',
|
'Author',
|
||||||
'/downloads',
|
'/downloads',
|
||||||
'epub',
|
'epub',
|
||||||
'https://annas-archive.li',
|
'https://annas-archive.gl',
|
||||||
undefined,
|
undefined,
|
||||||
'http://flare'
|
'http://flare'
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user