mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
Add multi-download-client support and UI management
Implements support for configuring both qBittorrent and SABnzbd simultaneously, including migration from legacy config, protocol-aware routing, and protocol filtering. Adds new CRUD API routes for download clients, new UI management components, and updates setup and settings flows to use the new multi-client architecture. Updates documentation to describe the new structure and usage.
This commit is contained in:
@@ -13,6 +13,10 @@ const jobQueueMock = createJobQueueMock();
|
||||
const qbtMock = vi.hoisted(() => ({ addTorrent: vi.fn() }));
|
||||
const sabMock = vi.hoisted(() => ({ addNZB: vi.fn() }));
|
||||
|
||||
const downloadClientManagerMock = vi.hoisted(() => ({
|
||||
getClientForProtocol: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/db', () => ({
|
||||
prisma: prismaMock,
|
||||
}));
|
||||
@@ -21,6 +25,10 @@ vi.mock('@/lib/services/config.service', () => ({
|
||||
getConfigService: () => configMock,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/services/download-client-manager.service', () => ({
|
||||
getDownloadClientManager: () => downloadClientManagerMock,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/services/job-queue.service', () => ({
|
||||
getJobQueueService: () => jobQueueMock,
|
||||
}));
|
||||
@@ -33,16 +41,26 @@ vi.mock('@/lib/integrations/sabnzbd.service', () => ({
|
||||
getSABnzbdService: () => sabMock,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/integrations/prowlarr.service', () => ({
|
||||
ProwlarrService: {
|
||||
isNZBResult: vi.fn((result: any) => {
|
||||
// Detect NZB by URL pattern or protocol field
|
||||
return result.downloadUrl?.endsWith('.nzb') || result.protocol === 'usenet';
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('processDownloadTorrent', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const payload = {
|
||||
const torrentPayload = {
|
||||
requestId: 'req-1',
|
||||
audiobook: { id: 'a1', title: 'Book', author: 'Author' },
|
||||
torrent: {
|
||||
indexer: 'Indexer',
|
||||
indexerId: 1,
|
||||
title: 'Book - Author',
|
||||
size: 50 * 1024 * 1024,
|
||||
seeders: 10,
|
||||
@@ -50,20 +68,44 @@ describe('processDownloadTorrent', () => {
|
||||
downloadUrl: 'magnet:?xt=urn:btih:abc',
|
||||
guid: 'guid-1',
|
||||
format: 'M4B',
|
||||
protocol: 'torrent',
|
||||
},
|
||||
jobId: 'job-1',
|
||||
};
|
||||
|
||||
it('routes downloads to qBittorrent by default', async () => {
|
||||
configMock.get.mockResolvedValue('qbittorrent');
|
||||
const nzbPayload = {
|
||||
requestId: 'req-2',
|
||||
audiobook: { id: 'a2', title: 'Book2', author: 'Author2' },
|
||||
torrent: {
|
||||
indexer: 'UsenetIndexer',
|
||||
indexerId: 2,
|
||||
title: 'Book2 - Author2',
|
||||
size: 100 * 1024 * 1024,
|
||||
seeders: 0,
|
||||
publishDate: new Date(),
|
||||
downloadUrl: 'http://indexer.com/download/file.nzb',
|
||||
guid: 'guid-2',
|
||||
format: 'M4B',
|
||||
protocol: 'usenet',
|
||||
},
|
||||
jobId: 'job-2',
|
||||
};
|
||||
|
||||
it('routes torrent downloads to qBittorrent', async () => {
|
||||
downloadClientManagerMock.getClientForProtocol.mockResolvedValue({
|
||||
id: 'client-1',
|
||||
type: 'qbittorrent',
|
||||
enabled: true,
|
||||
});
|
||||
qbtMock.addTorrent.mockResolvedValue('hash-1');
|
||||
prismaMock.request.update.mockResolvedValue({});
|
||||
prismaMock.downloadHistory.create.mockResolvedValue({ id: 'dh-1' });
|
||||
|
||||
const { processDownloadTorrent } = await import('@/lib/processors/download-torrent.processor');
|
||||
const result = await processDownloadTorrent(payload);
|
||||
const result = await processDownloadTorrent(torrentPayload);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(downloadClientManagerMock.getClientForProtocol).toHaveBeenCalledWith('torrent');
|
||||
expect(qbtMock.addTorrent).toHaveBeenCalled();
|
||||
expect(jobQueueMock.addMonitorJob).toHaveBeenCalledWith(
|
||||
'req-1',
|
||||
@@ -74,25 +116,70 @@ describe('processDownloadTorrent', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('routes downloads to SABnzbd when configured', async () => {
|
||||
configMock.get.mockResolvedValue('sabnzbd');
|
||||
it('routes NZB downloads to SABnzbd', async () => {
|
||||
downloadClientManagerMock.getClientForProtocol.mockResolvedValue({
|
||||
id: 'client-2',
|
||||
type: 'sabnzbd',
|
||||
enabled: true,
|
||||
});
|
||||
sabMock.addNZB.mockResolvedValue('nzb-1');
|
||||
prismaMock.request.update.mockResolvedValue({});
|
||||
prismaMock.downloadHistory.create.mockResolvedValue({ id: 'dh-2' });
|
||||
|
||||
const { processDownloadTorrent } = await import('@/lib/processors/download-torrent.processor');
|
||||
const result = await processDownloadTorrent(payload);
|
||||
const result = await processDownloadTorrent(nzbPayload);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(downloadClientManagerMock.getClientForProtocol).toHaveBeenCalledWith('usenet');
|
||||
expect(sabMock.addNZB).toHaveBeenCalled();
|
||||
expect(jobQueueMock.addMonitorJob).toHaveBeenCalledWith(
|
||||
'req-1',
|
||||
'req-2',
|
||||
'dh-2',
|
||||
'nzb-1',
|
||||
'sabnzbd',
|
||||
3
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error when no client configured for protocol', async () => {
|
||||
downloadClientManagerMock.getClientForProtocol.mockResolvedValue(null);
|
||||
|
||||
const { processDownloadTorrent } = await import('@/lib/processors/download-torrent.processor');
|
||||
|
||||
await expect(processDownloadTorrent(torrentPayload)).rejects.toThrow(
|
||||
'No Torrent (qBittorrent) client configured'
|
||||
);
|
||||
|
||||
expect(downloadClientManagerMock.getClientForProtocol).toHaveBeenCalledWith('torrent');
|
||||
});
|
||||
|
||||
it('detects protocol from result and routes appropriately', async () => {
|
||||
// Torrent result
|
||||
downloadClientManagerMock.getClientForProtocol.mockResolvedValueOnce({
|
||||
id: 'client-1',
|
||||
type: 'qbittorrent',
|
||||
enabled: true,
|
||||
});
|
||||
qbtMock.addTorrent.mockResolvedValue('hash-1');
|
||||
prismaMock.request.update.mockResolvedValue({});
|
||||
prismaMock.downloadHistory.create.mockResolvedValue({ id: 'dh-1' });
|
||||
|
||||
const { processDownloadTorrent } = await import('@/lib/processors/download-torrent.processor');
|
||||
await processDownloadTorrent(torrentPayload);
|
||||
|
||||
expect(downloadClientManagerMock.getClientForProtocol).toHaveBeenCalledWith('torrent');
|
||||
|
||||
// NZB result
|
||||
downloadClientManagerMock.getClientForProtocol.mockResolvedValueOnce({
|
||||
id: 'client-2',
|
||||
type: 'sabnzbd',
|
||||
enabled: true,
|
||||
});
|
||||
sabMock.addNZB.mockResolvedValue('nzb-1');
|
||||
prismaMock.downloadHistory.create.mockResolvedValue({ id: 'dh-2' });
|
||||
|
||||
await processDownloadTorrent(nzbPayload);
|
||||
|
||||
expect(downloadClientManagerMock.getClientForProtocol).toHaveBeenCalledWith('usenet');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user