mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
SABnzbd path mapping + ASIN-based request deletion
Add bidirectional path mapping and complete_dir-aware category sync to the SABnzbd integration. Introduces PathMapper usage, complete_dir extraction, calculateCategoryPath(), and ensureCategory() logic to choose empty/relative/absolute category paths; ensureCategory is invoked before adding NZBs. Update singleton factory to load download_dir and path-mapping config from DownloadClientManager and recreate the service when config is not loaded. Make DownloadClientManager pass path-mapping config into the SABnzbd service. Change request deletion to remove plex_library records by ASIN (deleteMany) with a fallback to exact title/author matches so availability checks and deletions are consistent. Update documentation and tests to reflect the new behavior and APIs.
This commit is contained in:
@@ -98,9 +98,9 @@ describe('RequestActionsDropdown', () => {
|
||||
fireEvent.click(screen.getByTitle('Actions'));
|
||||
|
||||
expect(screen.getByText('View Source')).toBeInTheDocument();
|
||||
expect(screen.getByText('Try to fetch Ebook')).toBeInTheDocument();
|
||||
expect(screen.getByText('Grab Ebook')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText('Try to fetch Ebook'));
|
||||
fireEvent.click(screen.getByText('Grab Ebook'));
|
||||
await waitFor(() => expect(onFetchEbook).toHaveBeenCalledWith('req-2'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,7 +44,8 @@ describe('IndexerConfigModal', () => {
|
||||
priority: 25,
|
||||
seedingTimeMinutes: 0,
|
||||
rssEnabled: false,
|
||||
categories: expect.arrayContaining([3030]),
|
||||
audiobookCategories: expect.arrayContaining([3030]),
|
||||
ebookCategories: expect.arrayContaining([7020]),
|
||||
})
|
||||
);
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
@@ -63,6 +64,7 @@ describe('IndexerConfigModal', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
// Find the Audiobook toggle in the category tree and click it to deselect
|
||||
const audiobookLabel = screen.getByText('Audiobook');
|
||||
const audiobookRow = audiobookLabel.closest('div')?.parentElement;
|
||||
if (!audiobookRow) {
|
||||
@@ -72,7 +74,8 @@ describe('IndexerConfigModal', () => {
|
||||
fireEvent.click(within(audiobookRow).getByRole('switch'));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Add Indexer' }));
|
||||
|
||||
expect(screen.getByText('At least one category must be selected')).toBeInTheDocument();
|
||||
// Component now shows specific error for audiobook categories
|
||||
expect(screen.getByText('At least one audiobook category must be selected')).toBeInTheDocument();
|
||||
expect(onSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
const useAuthMock = vi.hoisted(() => vi.fn());
|
||||
const useAudiobookDetailsMock = vi.hoisted(() => vi.fn());
|
||||
const createRequestMock = vi.hoisted(() => vi.fn());
|
||||
const fetchEbookMock = vi.hoisted(() => vi.fn());
|
||||
const revalidateEbookStatusMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@/contexts/AuthContext', () => ({
|
||||
useAuth: () => useAuthMock(),
|
||||
@@ -23,6 +25,11 @@ vi.mock('@/lib/hooks/useAudiobooks', () => ({
|
||||
|
||||
vi.mock('@/lib/hooks/useRequests', () => ({
|
||||
useCreateRequest: () => ({ createRequest: createRequestMock, isLoading: false }),
|
||||
useEbookStatus: () => ({
|
||||
ebookStatus: { ebookSourcesEnabled: false, hasActiveEbookRequest: false },
|
||||
revalidate: revalidateEbookStatusMock,
|
||||
}),
|
||||
useFetchEbookByAsin: () => ({ fetchEbook: fetchEbookMock, isLoading: false }),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/requests/InteractiveTorrentSearchModal', () => ({
|
||||
|
||||
@@ -13,6 +13,10 @@ const searchByRequestMock = vi.hoisted(() => vi.fn());
|
||||
const selectTorrentMock = vi.hoisted(() => vi.fn());
|
||||
const searchByAudiobookMock = vi.hoisted(() => vi.fn());
|
||||
const requestWithTorrentMock = vi.hoisted(() => vi.fn());
|
||||
const searchEbooksMock = vi.hoisted(() => vi.fn());
|
||||
const selectEbookMock = vi.hoisted(() => vi.fn());
|
||||
const searchEbooksByAsinMock = vi.hoisted(() => vi.fn());
|
||||
const selectEbookByAsinMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@/lib/hooks/useRequests', () => ({
|
||||
useInteractiveSearch: () => ({
|
||||
@@ -35,6 +39,26 @@ vi.mock('@/lib/hooks/useRequests', () => ({
|
||||
isLoading: false,
|
||||
error: null,
|
||||
}),
|
||||
useInteractiveSearchEbook: () => ({
|
||||
searchEbooks: searchEbooksMock,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
}),
|
||||
useSelectEbook: () => ({
|
||||
selectEbook: selectEbookMock,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
}),
|
||||
useInteractiveSearchEbookByAsin: () => ({
|
||||
searchEbooks: searchEbooksByAsinMock,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
}),
|
||||
useSelectEbookByAsin: () => ({
|
||||
selectEbook: selectEbookByAsinMock,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
const baseResult = {
|
||||
|
||||
@@ -119,31 +119,43 @@ describe('SABnzbdService', () => {
|
||||
});
|
||||
|
||||
it('adds NZB with mapped priority', async () => {
|
||||
clientMock.get.mockResolvedValueOnce({
|
||||
data: { status: true, nzo_ids: ['nzb-1'] },
|
||||
});
|
||||
// Mock getConfig for ensureCategory (called before adding NZB)
|
||||
clientMock.get
|
||||
.mockResolvedValueOnce({
|
||||
data: { config: { version: '1', misc: { complete_dir: '/downloads' }, categories: { books: { dir: '' } } } },
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: { status: true, nzo_ids: ['nzb-1'] },
|
||||
});
|
||||
|
||||
const service = new SABnzbdService('http://sab', 'key');
|
||||
const service = new SABnzbdService('http://sab', 'key', 'books', '/downloads');
|
||||
const nzbId = await service.addNZB('https://example.com/book.nzb', {
|
||||
category: 'books',
|
||||
priority: 'high',
|
||||
});
|
||||
|
||||
const params = clientMock.get.mock.calls[0][1].params;
|
||||
// Second call is the addurl call
|
||||
const params = clientMock.get.mock.calls[1][1].params;
|
||||
expect(nzbId).toBe('nzb-1');
|
||||
expect(params.cat).toBe('books');
|
||||
expect(params.priority).toBe('1');
|
||||
});
|
||||
|
||||
it('adds NZB with force priority', async () => {
|
||||
clientMock.get.mockResolvedValueOnce({
|
||||
data: { status: true, nzo_ids: ['nzb-9'] },
|
||||
});
|
||||
// Mock getConfig for ensureCategory (called before adding NZB)
|
||||
clientMock.get
|
||||
.mockResolvedValueOnce({
|
||||
data: { config: { version: '1', misc: { complete_dir: '/downloads' }, categories: { readmeabook: { dir: '' } } } },
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: { status: true, nzo_ids: ['nzb-9'] },
|
||||
});
|
||||
|
||||
const service = new SABnzbdService('http://sab', 'key');
|
||||
const service = new SABnzbdService('http://sab', 'key', 'readmeabook', '/downloads');
|
||||
await service.addNZB('https://example.com/book.nzb', { priority: 'force' });
|
||||
|
||||
const params = clientMock.get.mock.calls[0][1].params;
|
||||
// Second call is the addurl call
|
||||
const params = clientMock.get.mock.calls[1][1].params;
|
||||
expect(params.priority).toBe('2');
|
||||
});
|
||||
|
||||
@@ -376,12 +388,12 @@ describe('SABnzbdService', () => {
|
||||
it('creates the default category when missing', async () => {
|
||||
clientMock.get
|
||||
.mockResolvedValueOnce({
|
||||
data: { config: { version: '1', categories: {} } },
|
||||
data: { config: { version: '1', misc: { complete_dir: '/mnt/usenet/complete' }, categories: {} } },
|
||||
})
|
||||
.mockResolvedValueOnce({ data: { status: true } });
|
||||
|
||||
const service = new SABnzbdService('http://sab', 'key', 'readmeabook');
|
||||
await service.ensureCategory('/downloads');
|
||||
const service = new SABnzbdService('http://sab', 'key', 'readmeabook', '/downloads');
|
||||
await service.ensureCategory();
|
||||
|
||||
expect(clientMock.get).toHaveBeenCalledWith('/api', expect.objectContaining({
|
||||
params: expect.objectContaining({ mode: 'set_config', keyword: 'readmeabook' }),
|
||||
@@ -389,46 +401,58 @@ describe('SABnzbdService', () => {
|
||||
});
|
||||
|
||||
it('swallows errors when ensuring categories fails', async () => {
|
||||
const service = new SABnzbdService('http://sab', 'key', 'readmeabook');
|
||||
const service = new SABnzbdService('http://sab', 'key', 'readmeabook', '/downloads');
|
||||
const configSpy = vi.spyOn(service, 'getConfig').mockRejectedValue(new Error('bad config'));
|
||||
|
||||
await expect(service.ensureCategory('/downloads')).resolves.toBeUndefined();
|
||||
await expect(service.ensureCategory()).resolves.toBeUndefined();
|
||||
|
||||
configSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('does not create category when it already exists', async () => {
|
||||
it('does not create category when it already exists with correct path', async () => {
|
||||
clientMock.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
config: {
|
||||
version: '1',
|
||||
misc: { complete_dir: '/mnt/usenet/complete' },
|
||||
categories: { readmeabook: { dir: '/downloads' } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const service = new SABnzbdService('http://sab', 'key', 'readmeabook');
|
||||
await service.ensureCategory('/downloads');
|
||||
const service = new SABnzbdService('http://sab', 'key', 'readmeabook', '/downloads');
|
||||
await service.ensureCategory();
|
||||
|
||||
// Only get_config called, no set_config because path already matches
|
||||
expect(clientMock.get).toHaveBeenCalledTimes(1);
|
||||
expect(clientMock.get.mock.calls[0][1].params.mode).toBe('get_config');
|
||||
});
|
||||
it('throws when addNZB reports a failure', async () => {
|
||||
clientMock.get.mockResolvedValueOnce({
|
||||
data: { status: false, error: 'Bad NZB' },
|
||||
});
|
||||
// Mock getConfig for ensureCategory, then the addurl failure
|
||||
clientMock.get
|
||||
.mockResolvedValueOnce({
|
||||
data: { config: { version: '1', misc: { complete_dir: '/downloads' }, categories: { readmeabook: { dir: '' } } } },
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: { status: false, error: 'Bad NZB' },
|
||||
});
|
||||
|
||||
const service = new SABnzbdService('http://sab', 'key');
|
||||
const service = new SABnzbdService('http://sab', 'key', 'readmeabook', '/downloads');
|
||||
|
||||
await expect(service.addNZB('https://example.com/book.nzb')).rejects.toThrow('Bad NZB');
|
||||
});
|
||||
|
||||
it('throws when SABnzbd returns no NZB IDs', async () => {
|
||||
clientMock.get.mockResolvedValueOnce({
|
||||
data: { status: true, nzo_ids: [] },
|
||||
});
|
||||
// Mock getConfig for ensureCategory, then the addurl with empty IDs
|
||||
clientMock.get
|
||||
.mockResolvedValueOnce({
|
||||
data: { config: { version: '1', misc: { complete_dir: '/downloads' }, categories: { readmeabook: { dir: '' } } } },
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: { status: true, nzo_ids: [] },
|
||||
});
|
||||
|
||||
const service = new SABnzbdService('http://sab', 'key');
|
||||
const service = new SABnzbdService('http://sab', 'key', 'readmeabook', '/downloads');
|
||||
|
||||
await expect(service.addNZB('https://example.com/book.nzb')).rejects.toThrow('did not return an NZB ID');
|
||||
});
|
||||
@@ -491,8 +515,281 @@ describe('SABnzbdService', () => {
|
||||
const again = await getSABnzbdService();
|
||||
|
||||
expect(service).toBe(again);
|
||||
expect(ensureSpy).toHaveBeenCalledWith('/downloads');
|
||||
expect(ensureSpy).toHaveBeenCalled();
|
||||
|
||||
ensureSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('creates singleton with path mapping config when enabled', async () => {
|
||||
downloadClientManagerMock.getClientForProtocol.mockResolvedValue({
|
||||
id: 'client-1',
|
||||
type: 'sabnzbd',
|
||||
name: 'SABnzbd',
|
||||
enabled: true,
|
||||
url: 'http://sab',
|
||||
password: 'api-key',
|
||||
disableSSLVerify: false,
|
||||
remotePathMappingEnabled: true,
|
||||
remotePath: '/mnt/usenet/complete',
|
||||
localPath: '/downloads',
|
||||
category: 'readmeabook',
|
||||
});
|
||||
configServiceMock.get.mockResolvedValue('/downloads');
|
||||
|
||||
const ensureSpy = vi.spyOn(SABnzbdService.prototype, 'ensureCategory').mockResolvedValue();
|
||||
|
||||
const service = await getSABnzbdService();
|
||||
|
||||
expect(service).toBeDefined();
|
||||
expect(ensureSpy).toHaveBeenCalled();
|
||||
|
||||
ensureSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('Path Mapping', () => {
|
||||
it('uses empty category path when download_dir matches complete_dir', async () => {
|
||||
clientMock.get
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
config: {
|
||||
version: '1',
|
||||
misc: { complete_dir: '/downloads' },
|
||||
categories: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({ data: { status: true } });
|
||||
|
||||
const service = new SABnzbdService('http://sab', 'key', 'readmeabook', '/downloads');
|
||||
await service.ensureCategory();
|
||||
|
||||
// Should set empty dir when paths match
|
||||
const setCategoryCall = clientMock.get.mock.calls.find(
|
||||
(call) => call[1]?.params?.mode === 'set_config'
|
||||
);
|
||||
expect(setCategoryCall).toBeDefined();
|
||||
expect(setCategoryCall![1].params.dir).toBe('');
|
||||
});
|
||||
|
||||
it('uses relative path when download_dir is under complete_dir', async () => {
|
||||
clientMock.get
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
config: {
|
||||
version: '1',
|
||||
misc: { complete_dir: '/mnt/usenet/complete' },
|
||||
categories: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({ data: { status: true } });
|
||||
|
||||
const service = new SABnzbdService(
|
||||
'http://sab',
|
||||
'key',
|
||||
'readmeabook',
|
||||
'/mnt/usenet/complete/audiobooks'
|
||||
);
|
||||
await service.ensureCategory();
|
||||
|
||||
const setCategoryCall = clientMock.get.mock.calls.find(
|
||||
(call) => call[1]?.params?.mode === 'set_config'
|
||||
);
|
||||
expect(setCategoryCall).toBeDefined();
|
||||
expect(setCategoryCall![1].params.dir).toBe('audiobooks');
|
||||
});
|
||||
|
||||
it('uses absolute path when download_dir differs from complete_dir', async () => {
|
||||
clientMock.get
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
config: {
|
||||
version: '1',
|
||||
misc: { complete_dir: '/mnt/usenet/complete' },
|
||||
categories: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({ data: { status: true } });
|
||||
|
||||
const service = new SABnzbdService(
|
||||
'http://sab',
|
||||
'key',
|
||||
'readmeabook',
|
||||
'/different/path/audiobooks'
|
||||
);
|
||||
await service.ensureCategory();
|
||||
|
||||
const setCategoryCall = clientMock.get.mock.calls.find(
|
||||
(call) => call[1]?.params?.mode === 'set_config'
|
||||
);
|
||||
expect(setCategoryCall).toBeDefined();
|
||||
expect(setCategoryCall![1].params.dir).toBe('/different/path/audiobooks');
|
||||
});
|
||||
|
||||
it('applies reverse path mapping before comparing with complete_dir', async () => {
|
||||
clientMock.get
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
config: {
|
||||
version: '1',
|
||||
misc: { complete_dir: '/mnt/usenet/complete' },
|
||||
categories: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({ data: { status: true } });
|
||||
|
||||
// RMAB sees /downloads but SABnzbd sees /mnt/usenet/complete
|
||||
const pathMappingConfig = {
|
||||
enabled: true,
|
||||
remotePath: '/mnt/usenet/complete',
|
||||
localPath: '/downloads',
|
||||
};
|
||||
|
||||
const service = new SABnzbdService(
|
||||
'http://sab',
|
||||
'key',
|
||||
'readmeabook',
|
||||
'/downloads', // RMAB's local path
|
||||
false,
|
||||
pathMappingConfig
|
||||
);
|
||||
await service.ensureCategory();
|
||||
|
||||
// After reverse transform, /downloads becomes /mnt/usenet/complete
|
||||
// which matches complete_dir, so category dir should be empty
|
||||
const setCategoryCall = clientMock.get.mock.calls.find(
|
||||
(call) => call[1]?.params?.mode === 'set_config'
|
||||
);
|
||||
expect(setCategoryCall).toBeDefined();
|
||||
expect(setCategoryCall![1].params.dir).toBe('');
|
||||
});
|
||||
|
||||
it('updates category path when it differs from calculated path', async () => {
|
||||
clientMock.get
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
config: {
|
||||
version: '1',
|
||||
misc: { complete_dir: '/mnt/usenet/complete' },
|
||||
categories: { readmeabook: { dir: '/old/path' } },
|
||||
},
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({ data: { status: true } });
|
||||
|
||||
const service = new SABnzbdService(
|
||||
'http://sab',
|
||||
'key',
|
||||
'readmeabook',
|
||||
'/mnt/usenet/complete/audiobooks'
|
||||
);
|
||||
await service.ensureCategory();
|
||||
|
||||
// Should update the category with new relative path
|
||||
const setCategoryCall = clientMock.get.mock.calls.find(
|
||||
(call) => call[1]?.params?.mode === 'set_config'
|
||||
);
|
||||
expect(setCategoryCall).toBeDefined();
|
||||
expect(setCategoryCall![1].params.dir).toBe('audiobooks');
|
||||
});
|
||||
|
||||
it('fetches complete_dir from SABnzbd config', async () => {
|
||||
clientMock.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
config: {
|
||||
version: '4.0.0',
|
||||
misc: { complete_dir: '/mnt/usenet/complete' },
|
||||
categories: { test: { dir: 'test-dir' } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const service = new SABnzbdService('http://sab', 'key', 'readmeabook', '/downloads');
|
||||
const config = await service.getConfig();
|
||||
|
||||
expect(config.completeDir).toBe('/mnt/usenet/complete');
|
||||
expect(config.categories).toEqual([{ name: 'test', dir: 'test-dir' }]);
|
||||
});
|
||||
|
||||
it('returns complete_dir via getCompleteDir helper', async () => {
|
||||
clientMock.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
config: {
|
||||
version: '4.0.0',
|
||||
misc: { complete_dir: '/var/usenet/done' },
|
||||
categories: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const service = new SABnzbdService('http://sab', 'key', 'readmeabook', '/downloads');
|
||||
const completeDir = await service.getCompleteDir();
|
||||
|
||||
expect(completeDir).toBe('/var/usenet/done');
|
||||
});
|
||||
|
||||
it('handles missing complete_dir gracefully', async () => {
|
||||
clientMock.get
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
config: {
|
||||
version: '4.0.0',
|
||||
misc: {}, // No complete_dir
|
||||
categories: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({ data: { status: true } });
|
||||
|
||||
const service = new SABnzbdService('http://sab', 'key', 'readmeabook', '/downloads');
|
||||
await service.ensureCategory();
|
||||
|
||||
// Should fallback to using download_dir directly
|
||||
const setCategoryCall = clientMock.get.mock.calls.find(
|
||||
(call) => call[1]?.params?.mode === 'set_config'
|
||||
);
|
||||
expect(setCategoryCall).toBeDefined();
|
||||
expect(setCategoryCall![1].params.dir).toBe('/downloads');
|
||||
});
|
||||
|
||||
it('handles Windows-style paths in path mapping', async () => {
|
||||
clientMock.get
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
config: {
|
||||
version: '1',
|
||||
misc: { complete_dir: 'D:\\Usenet\\Complete' },
|
||||
categories: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({ data: { status: true } });
|
||||
|
||||
const pathMappingConfig = {
|
||||
enabled: true,
|
||||
remotePath: 'D:\\Usenet\\Complete',
|
||||
localPath: '/downloads',
|
||||
};
|
||||
|
||||
const service = new SABnzbdService(
|
||||
'http://sab',
|
||||
'key',
|
||||
'readmeabook',
|
||||
'/downloads',
|
||||
false,
|
||||
pathMappingConfig
|
||||
);
|
||||
await service.ensureCategory();
|
||||
|
||||
// After reverse transform and comparison (normalized), should match
|
||||
const setCategoryCall = clientMock.get.mock.calls.find(
|
||||
(call) => call[1]?.params?.mode === 'set_config'
|
||||
);
|
||||
expect(setCategoryCall).toBeDefined();
|
||||
// Path should be empty since /downloads maps to D:\Usenet\Complete which matches complete_dir
|
||||
expect(setCategoryCall![1].params.dir).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,6 +42,8 @@ describe('processSearchEbook', () => {
|
||||
configServiceMock.get.mockImplementation(async (key: string) => {
|
||||
if (key === 'ebook_sidecar_preferred_format') return 'epub';
|
||||
if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.li';
|
||||
if (key === 'ebook_annas_archive_enabled') return 'true';
|
||||
if (key === 'ebook_indexer_search_enabled') return 'false';
|
||||
return null;
|
||||
});
|
||||
});
|
||||
@@ -71,7 +73,7 @@ describe('processSearchEbook', () => {
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toContain('ASIN');
|
||||
expect(result.message).toContain("Anna's Archive");
|
||||
expect(ebookScraperMock.searchByAsin).toHaveBeenCalledWith(
|
||||
'B001ASIN',
|
||||
'epub',
|
||||
@@ -113,7 +115,7 @@ describe('processSearchEbook', () => {
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toContain('title search');
|
||||
expect(result.message).toContain("Anna's Archive");
|
||||
expect(ebookScraperMock.searchByAsin).toHaveBeenCalled();
|
||||
expect(ebookScraperMock.searchByTitle).toHaveBeenCalledWith(
|
||||
'Another Book',
|
||||
@@ -179,6 +181,7 @@ describe('processSearchEbook', () => {
|
||||
data: expect.objectContaining({
|
||||
status: 'awaiting_search',
|
||||
errorMessage: expect.stringContaining('No ebook found'),
|
||||
lastSearchAt: expect.any(Date),
|
||||
}),
|
||||
});
|
||||
expect(jobQueueMock.addStartDirectDownloadJob).not.toHaveBeenCalled();
|
||||
@@ -209,7 +212,8 @@ describe('processSearchEbook', () => {
|
||||
where: { id: 'req-5' },
|
||||
data: expect.objectContaining({
|
||||
status: 'awaiting_search',
|
||||
errorMessage: expect.stringContaining('no download links'),
|
||||
errorMessage: expect.stringContaining('No ebook found'),
|
||||
lastSearchAt: expect.any(Date),
|
||||
}),
|
||||
});
|
||||
});
|
||||
@@ -223,6 +227,8 @@ describe('processSearchEbook', () => {
|
||||
if (key === 'ebook_sidecar_preferred_format') return 'epub';
|
||||
if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.li';
|
||||
if (key === 'ebook_sidecar_flaresolverr_url') return 'http://flaresolverr:8191';
|
||||
if (key === 'ebook_annas_archive_enabled') return 'true';
|
||||
if (key === 'ebook_indexer_search_enabled') return 'false';
|
||||
return null;
|
||||
});
|
||||
|
||||
|
||||
@@ -110,9 +110,8 @@ describe('deleteRequest', () => {
|
||||
prismaMock.audibleCache.findUnique.mockResolvedValueOnce({
|
||||
releaseDate: '2021-01-01T00:00:00.000Z',
|
||||
});
|
||||
prismaMock.plexLibrary.findMany.mockResolvedValue([
|
||||
{ id: 'lib-1', title: 'Book', author: 'Author' },
|
||||
]);
|
||||
// Mock deleteMany for ASIN-based deletion
|
||||
prismaMock.plexLibrary.deleteMany.mockResolvedValue({ count: 1 });
|
||||
fsMock.access.mockResolvedValue(undefined);
|
||||
fsMock.rm.mockResolvedValue(undefined);
|
||||
prismaMock.request.update.mockResolvedValue({});
|
||||
@@ -124,7 +123,15 @@ describe('deleteRequest', () => {
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.torrentsRemoved).toBe(1);
|
||||
expect(qbtMock.deleteTorrent).toHaveBeenCalledWith('hash-1', true);
|
||||
expect(prismaMock.plexLibrary.delete).toHaveBeenCalledWith({ where: { id: 'lib-1' } });
|
||||
// Code now uses deleteMany with ASIN-based matching
|
||||
expect(prismaMock.plexLibrary.deleteMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
OR: [
|
||||
{ asin: 'ASIN1' },
|
||||
{ plexGuid: { contains: 'ASIN1' } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const expectedPath = path.join('/media', 'Author', 'Book ASIN1');
|
||||
expect(fsMock.rm).toHaveBeenCalledWith(expectedPath, { recursive: true, force: true });
|
||||
|
||||
Reference in New Issue
Block a user