Add Transmission/NZBGet and per-client paths and much more

Extend multi-download-client support to include Transmission and NZBGet and introduce per-client custom download paths. Adds protocol mapping and new client types, Transmission/NZBGet integration services, API CRUD and validation changes, UI components/modal updates and live path previews, and manager routing by protocol. Includes DB migrations (download_path on download_history, interactive_search_access on users), schema updates, and related processor/service fixes and tests to ensure backward compatibility and proper path resolution.
This commit is contained in:
kikootwo
2026-02-09 19:45:43 -05:00
parent d7acd67aa4
commit 4b90b35748
117 changed files with 9346 additions and 1488 deletions
@@ -13,7 +13,7 @@ import { DownloadClientStep } from '@/app/setup/steps/DownloadClientStep';
interface DownloadClient {
id: string;
type: 'qbittorrent' | 'sabnzbd';
type: 'qbittorrent' | 'sabnzbd' | 'transmission';
name: string;
enabled: boolean;
url: string;
@@ -469,34 +469,39 @@ describe('DownloadClientStep', () => {
});
describe('Client Type Restrictions', () => {
it('shows "Already configured" when qBittorrent is already added', () => {
it('shows "Protocol already configured" when a torrent client is already added', () => {
const mockClient = createMockClient({ type: 'qbittorrent' });
render(<DownloadClientHarness onNext={vi.fn()} onBack={vi.fn()} initialClients={[mockClient]} />);
// "Already configured" text should appear for qBittorrent
expect(screen.getByText('Already configured')).toBeInTheDocument();
// "Protocol already configured" text should appear for torrent clients
const configuredMessages = screen.getAllByText('Protocol already configured');
expect(configuredMessages.length).toBeGreaterThanOrEqual(1);
// Add qBittorrent button should not exist
// Add qBittorrent and Add Transmission buttons should not exist (torrent protocol taken)
expect(screen.queryByRole('button', { name: /Add qBittorrent/i })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /Add Transmission/i })).not.toBeInTheDocument();
// SABnzbd should still have Add button
// SABnzbd should still have Add button (different protocol)
expect(screen.getByRole('button', { name: /Add SABnzbd/i })).toBeInTheDocument();
});
it('shows "Already configured" when SABnzbd is already added', () => {
it('shows "Protocol already configured" when SABnzbd is already added', () => {
const mockClient = createMockClient({ type: 'sabnzbd', name: 'My SABnzbd' });
render(<DownloadClientHarness onNext={vi.fn()} onBack={vi.fn()} initialClients={[mockClient]} />);
// "Already configured" text should appear for SABnzbd
expect(screen.getByText('Already configured')).toBeInTheDocument();
// "Protocol already configured" text should appear for both usenet client cards (SABnzbd + NZBGet)
const configuredMessages = screen.getAllByText('Protocol already configured');
expect(configuredMessages.length).toBe(2);
// Add SABnzbd button should not exist
// Add SABnzbd and NZBGet buttons should not exist (usenet protocol taken)
expect(screen.queryByRole('button', { name: /Add SABnzbd/i })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /Add NZBGet/i })).not.toBeInTheDocument();
// qBittorrent should still have Add button
// Torrent clients should still have Add buttons
expect(screen.getByRole('button', { name: /Add qBittorrent/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Add Transmission/i })).toBeInTheDocument();
});
});
@@ -635,9 +640,9 @@ describe('DownloadClientStep', () => {
expect(editButtons).toHaveLength(2);
});
// Both "Already configured" messages should appear
const alreadyConfiguredMessages = screen.getAllByText('Already configured');
expect(alreadyConfiguredMessages).toHaveLength(2);
// Both "Protocol already configured" messages should appear (torrent + usenet)
const alreadyConfiguredMessages = screen.getAllByText('Protocol already configured');
expect(alreadyConfiguredMessages.length).toBeGreaterThanOrEqual(2);
});
});