mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
Add SABnzbd Usenet/NZB integration and documentation
Introduces SABnzbd as a supported download client for Usenet/NZB alongside qBittorrent, including service implementation, setup wizard and admin settings UI updates, and protocol-specific job processor logic. Updates documentation, PRD, and database schema to support NZB downloads, adds comprehensive technical details and testing strategies, and fixes Audible integration issues related to search and ASIN extraction.
This commit is contained in:
@@ -1521,7 +1521,7 @@ export default function AdminSettings() {
|
||||
Download Client
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
||||
Configure your torrent download client (qBittorrent/Transmission).
|
||||
Configure your download client: qBittorrent for torrents or SABnzbd for Usenet/NZB downloads.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1541,7 +1541,7 @@ export default function AdminSettings() {
|
||||
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"
|
||||
>
|
||||
<option value="qbittorrent">qBittorrent</option>
|
||||
<option value="transmission">Transmission</option>
|
||||
<option value="sabnzbd">SABnzbd</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -1563,47 +1563,79 @@ export default function AdminSettings() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Username
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={settings.downloadClient.username}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
...settings,
|
||||
downloadClient: {
|
||||
...settings.downloadClient,
|
||||
username: e.target.value,
|
||||
},
|
||||
});
|
||||
setValidated({ ...validated, download: false });
|
||||
}}
|
||||
placeholder="admin"
|
||||
/>
|
||||
</div>
|
||||
{/* qBittorrent: Username + Password */}
|
||||
{settings.downloadClient.type === 'qbittorrent' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Username
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={settings.downloadClient.username}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
...settings,
|
||||
downloadClient: {
|
||||
...settings.downloadClient,
|
||||
username: e.target.value,
|
||||
},
|
||||
});
|
||||
setValidated({ ...validated, download: false });
|
||||
}}
|
||||
placeholder="admin"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Password
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={settings.downloadClient.password}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
...settings,
|
||||
downloadClient: {
|
||||
...settings.downloadClient,
|
||||
password: e.target.value,
|
||||
},
|
||||
});
|
||||
setValidated({ ...validated, download: false });
|
||||
}}
|
||||
placeholder="Enter password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Password
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={settings.downloadClient.password}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
...settings,
|
||||
downloadClient: {
|
||||
...settings.downloadClient,
|
||||
password: e.target.value,
|
||||
},
|
||||
});
|
||||
setValidated({ ...validated, download: false });
|
||||
}}
|
||||
placeholder="Enter password"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* SABnzbd: API Key only */}
|
||||
{settings.downloadClient.type === 'sabnzbd' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
API Key
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={settings.downloadClient.password}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
...settings,
|
||||
downloadClient: {
|
||||
...settings.downloadClient,
|
||||
password: e.target.value,
|
||||
},
|
||||
});
|
||||
setValidated({ ...validated, download: false });
|
||||
}}
|
||||
placeholder="Enter SABnzbd API key"
|
||||
/>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Find this in SABnzbd under Config → General → API Key
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SSL Verification Toggle */}
|
||||
{settings.downloadClient.url.startsWith('https') && (
|
||||
|
||||
@@ -23,19 +23,29 @@ export async function PUT(request: NextRequest) {
|
||||
localPath,
|
||||
} = await request.json();
|
||||
|
||||
if (!type || !url || !username || !password) {
|
||||
// Validate type
|
||||
if (type !== 'qbittorrent' && type !== 'sabnzbd') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Type, URL, username, and password are required' },
|
||||
{ error: 'Invalid client type. Must be qbittorrent or sabnzbd' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate type
|
||||
if (type !== 'qbittorrent' && type !== 'transmission') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid client type. Must be qbittorrent or transmission' },
|
||||
{ status: 400 }
|
||||
);
|
||||
// Validate required fields (SABnzbd only needs URL and API key)
|
||||
if (type === 'sabnzbd') {
|
||||
if (!url || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: 'URL and API key (password) are required for SABnzbd' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
} else if (type === 'qbittorrent') {
|
||||
if (!url || !username || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: 'URL, username, and password are required for qBittorrent' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate path mapping if enabled
|
||||
@@ -127,9 +137,14 @@ export async function PUT(request: NextRequest) {
|
||||
|
||||
console.log('[Admin] Download client settings updated');
|
||||
|
||||
// Invalidate qBittorrent service singleton to force reload of credentials and URL
|
||||
const { invalidateQBittorrentService } = await import('@/lib/integrations/qbittorrent.service');
|
||||
invalidateQBittorrentService();
|
||||
// Invalidate download client service singleton to force reload of credentials and URL
|
||||
if (type === 'qbittorrent') {
|
||||
const { invalidateQBittorrentService } = await import('@/lib/integrations/qbittorrent.service');
|
||||
invalidateQBittorrentService();
|
||||
} else if (type === 'sabnzbd') {
|
||||
const { invalidateSABnzbdService } = await import('@/lib/integrations/sabnzbd.service');
|
||||
invalidateSABnzbdService();
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { QBittorrentService } from '@/lib/integrations/qbittorrent.service';
|
||||
import { SABnzbdService } from '@/lib/integrations/sabnzbd.service';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return requireAuth(request, async (req: AuthenticatedRequest) => {
|
||||
@@ -23,30 +24,30 @@ export async function POST(request: NextRequest) {
|
||||
localPath,
|
||||
} = await request.json();
|
||||
|
||||
if (!type || !url || !username || !password) {
|
||||
if (!type || !url) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'All fields are required' },
|
||||
{ success: false, error: 'Type and URL are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (type !== 'qbittorrent') {
|
||||
if (type !== 'qbittorrent' && type !== 'sabnzbd') {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Only qBittorrent is currently supported' },
|
||||
{ success: false, error: 'Invalid client type. Must be qbittorrent or sabnzbd' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// If password is masked, fetch the actual value from database
|
||||
let actualPassword = password;
|
||||
if (password.startsWith('••••')) {
|
||||
if (password && password.startsWith('••••')) {
|
||||
const storedPassword = await prisma.configuration.findUnique({
|
||||
where: { key: 'download_client_password' },
|
||||
});
|
||||
|
||||
if (!storedPassword?.value) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'No stored password found. Please re-enter your download client password.' },
|
||||
{ success: false, error: 'No stored password/API key found. Please re-enter it.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
@@ -54,13 +55,48 @@ export async function POST(request: NextRequest) {
|
||||
actualPassword = storedPassword.value;
|
||||
}
|
||||
|
||||
// Test connection with credentials
|
||||
const version = await QBittorrentService.testConnectionWithCredentials(
|
||||
url,
|
||||
username,
|
||||
actualPassword,
|
||||
disableSSLVerify || false
|
||||
);
|
||||
// Validate required fields per client type and test connection
|
||||
let version: string | undefined;
|
||||
|
||||
if (type === 'qbittorrent') {
|
||||
if (!username || !actualPassword) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Username and password are required for qBittorrent' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Test qBittorrent connection
|
||||
version = await QBittorrentService.testConnectionWithCredentials(
|
||||
url,
|
||||
username,
|
||||
actualPassword,
|
||||
disableSSLVerify || false
|
||||
);
|
||||
} else if (type === 'sabnzbd') {
|
||||
if (!actualPassword) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'API key (password) is required for SABnzbd' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Test SABnzbd connection
|
||||
const sabnzbd = new SABnzbdService(url, actualPassword, 'readmeabook', disableSSLVerify || false);
|
||||
const result = await sabnzbd.testConnection();
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: result.error || 'Failed to connect to SABnzbd',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
version = result.version;
|
||||
}
|
||||
|
||||
// If path mapping enabled, validate local path exists
|
||||
if (remotePathMappingEnabled) {
|
||||
|
||||
@@ -5,37 +5,80 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { QBittorrentService } from '@/lib/integrations/qbittorrent.service';
|
||||
import { SABnzbdService } from '@/lib/integrations/sabnzbd.service';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { type, url, username, password, disableSSLVerify } = await request.json();
|
||||
|
||||
if (!type || !url || !username || !password) {
|
||||
if (!type || !url) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'All fields are required' },
|
||||
{ success: false, error: 'Type and URL are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (type !== 'qbittorrent') {
|
||||
if (type !== 'qbittorrent' && type !== 'sabnzbd') {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Only qBittorrent is currently supported' },
|
||||
{ success: false, error: 'Invalid client type. Must be qbittorrent or sabnzbd' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Test connection with custom credentials
|
||||
const version = await QBittorrentService.testConnectionWithCredentials(
|
||||
url,
|
||||
username,
|
||||
password,
|
||||
disableSSLVerify || false
|
||||
// Validate required fields per client type
|
||||
if (type === 'qbittorrent') {
|
||||
if (!username || !password) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Username and password are required for qBittorrent' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Test qBittorrent connection
|
||||
const version = await QBittorrentService.testConnectionWithCredentials(
|
||||
url,
|
||||
username,
|
||||
password,
|
||||
disableSSLVerify || false
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
version,
|
||||
});
|
||||
} else if (type === 'sabnzbd') {
|
||||
if (!password) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'API key (password) is required for SABnzbd' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Test SABnzbd connection
|
||||
const sabnzbd = new SABnzbdService(url, password, 'readmeabook', disableSSLVerify || false);
|
||||
const result = await sabnzbd.testConnection();
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: result.error || 'Failed to connect to SABnzbd',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
version: result.version,
|
||||
});
|
||||
}
|
||||
|
||||
// Should never reach here
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Invalid client type' },
|
||||
{ status: 400 }
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
version,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Setup] Download client test failed:', error);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -73,7 +73,7 @@ interface SetupState {
|
||||
prowlarrUrl: string;
|
||||
prowlarrApiKey: string;
|
||||
prowlarrIndexers: SelectedIndexer[];
|
||||
downloadClient: 'qbittorrent' | 'transmission';
|
||||
downloadClient: 'qbittorrent' | 'sabnzbd';
|
||||
downloadClientUrl: string;
|
||||
downloadClientUsername: string;
|
||||
downloadClientPassword: string;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
|
||||
interface DownloadClientStepProps {
|
||||
downloadClient: 'qbittorrent' | 'transmission';
|
||||
downloadClient: 'qbittorrent' | 'sabnzbd';
|
||||
downloadClientUrl: string;
|
||||
downloadClientUsername: string;
|
||||
downloadClientPassword: string;
|
||||
@@ -99,6 +99,11 @@ export function DownloadClientStep({
|
||||
onNext();
|
||||
};
|
||||
|
||||
// SABnzbd only requires URL and API key (no username)
|
||||
const isFormValid = downloadClient === 'sabnzbd'
|
||||
? downloadClientUrl && downloadClientPassword // Password field stores API key for SABnzbd
|
||||
: downloadClientUrl && downloadClientUsername && downloadClientPassword;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
@@ -106,7 +111,7 @@ export function DownloadClientStep({
|
||||
Configure Download Client
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-2">
|
||||
Choose and configure your torrent download client.
|
||||
Choose your download client: qBittorrent for torrents or SABnzbd for Usenet/NZB downloads.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -127,21 +132,21 @@ export function DownloadClientStep({
|
||||
>
|
||||
<div className="font-semibold text-gray-900 dark:text-gray-100">qBittorrent</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Recommended - Full feature support
|
||||
Torrent downloads
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onUpdate('downloadClient', 'transmission')}
|
||||
onClick={() => onUpdate('downloadClient', 'sabnzbd')}
|
||||
className={`p-4 border-2 rounded-lg text-left transition-colors ${
|
||||
downloadClient === 'transmission'
|
||||
downloadClient === 'sabnzbd'
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400'
|
||||
}`}
|
||||
>
|
||||
<div className="font-semibold text-gray-900 dark:text-gray-100">Transmission</div>
|
||||
<div className="font-semibold text-gray-900 dark:text-gray-100">SABnzbd</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Coming soon
|
||||
Usenet/NZB downloads
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
@@ -149,11 +154,11 @@ export function DownloadClientStep({
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{downloadClient === 'qbittorrent' ? 'qBittorrent' : 'Transmission'} URL
|
||||
{downloadClient === 'qbittorrent' ? 'qBittorrent' : 'SABnzbd'} URL
|
||||
</label>
|
||||
<Input
|
||||
type="url"
|
||||
placeholder={downloadClient === 'qbittorrent' ? 'http://localhost:8080' : 'http://localhost:9091'}
|
||||
placeholder={downloadClient === 'qbittorrent' ? 'http://localhost:8080' : 'http://localhost:8080/sabnzbd'}
|
||||
value={downloadClientUrl}
|
||||
onChange={(e) => onUpdate('downloadClientUrl', e.target.value)}
|
||||
/>
|
||||
@@ -162,31 +167,53 @@ export function DownloadClientStep({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Username
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="admin"
|
||||
value={downloadClientUsername}
|
||||
onChange={(e) => onUpdate('downloadClientUsername', e.target.value)}
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
{downloadClient === 'qbittorrent' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Username
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="admin"
|
||||
value={downloadClientUsername}
|
||||
onChange={(e) => onUpdate('downloadClientUsername', e.target.value)}
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Password
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter password"
|
||||
value={downloadClientPassword}
|
||||
onChange={(e) => onUpdate('downloadClientPassword', e.target.value)}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Password
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter password"
|
||||
value={downloadClientPassword}
|
||||
onChange={(e) => onUpdate('downloadClientPassword', e.target.value)}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{downloadClient === 'sabnzbd' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
API Key
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter SABnzbd API key"
|
||||
value={downloadClientPassword}
|
||||
onChange={(e) => onUpdate('downloadClientPassword', e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Find this in SABnzbd under Config → General → API Key
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SSL Verification Toggle */}
|
||||
{downloadClientUrl.startsWith('https') && (
|
||||
@@ -215,7 +242,7 @@ export function DownloadClientStep({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Remote Path Mapping */}
|
||||
{/* Remote Path Mapping (only for clients that download to filesystem) */}
|
||||
<div className="mt-4 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
|
||||
@@ -233,7 +260,7 @@ export function DownloadClientStep({
|
||||
Enable Remote Path Mapping
|
||||
</label>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Use this when qBittorrent runs on a different machine or uses different mount points (e.g., remote seedbox, Docker containers)
|
||||
Use this when {downloadClient === 'qbittorrent' ? 'qBittorrent' : 'SABnzbd'} runs on a different machine or uses different mount points (e.g., remote seedbox, Docker containers)
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2 font-mono">
|
||||
Example: Remote <span className="text-blue-600 dark:text-blue-400">/remote/mnt/d/done</span> → Local <span className="text-green-600 dark:text-green-400">/downloads</span>
|
||||
@@ -244,7 +271,7 @@ export function DownloadClientStep({
|
||||
<div className="mt-4 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Remote Path (from qBittorrent)
|
||||
Remote Path (from {downloadClient === 'qbittorrent' ? 'qBittorrent' : 'SABnzbd'})
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
@@ -253,7 +280,7 @@ export function DownloadClientStep({
|
||||
onChange={(e) => onUpdate('remotePath', e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
The path prefix as reported by qBittorrent
|
||||
The path prefix as reported by {downloadClient === 'qbittorrent' ? 'qBittorrent' : 'SABnzbd'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -280,7 +307,7 @@ export function DownloadClientStep({
|
||||
<Button
|
||||
onClick={testConnection}
|
||||
loading={testing}
|
||||
disabled={!downloadClientUrl || !downloadClientUsername || !downloadClientPassword}
|
||||
disabled={!isFormValid}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
@@ -359,12 +386,12 @@ export function DownloadClientStep({
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-blue-900 dark:text-blue-100">
|
||||
{downloadClient === 'qbittorrent' ? 'qBittorrent Setup' : 'Transmission Setup'}
|
||||
{downloadClient === 'qbittorrent' ? 'qBittorrent Setup' : 'SABnzbd Setup'}
|
||||
</p>
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300 mt-1">
|
||||
{downloadClient === 'qbittorrent'
|
||||
? 'Make sure Web UI is enabled in qBittorrent settings (Tools → Options → Web UI)'
|
||||
: 'Transmission support is coming soon. Please use qBittorrent for now.'}
|
||||
: 'Make sure SABnzbd is running and the API key is configured (Config → General → API Key)'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ interface ReviewStepProps {
|
||||
|
||||
// Common config
|
||||
prowlarrUrl: string;
|
||||
downloadClient: 'qbittorrent' | 'transmission';
|
||||
downloadClient: 'qbittorrent' | 'sabnzbd';
|
||||
downloadClientUrl: string;
|
||||
downloadDir: string;
|
||||
mediaDir: string;
|
||||
|
||||
@@ -98,10 +98,10 @@ export function WelcomeStep({ onNext }: WelcomeStepProps) {
|
||||
</svg>
|
||||
<div>
|
||||
<strong className="text-gray-900 dark:text-gray-100">
|
||||
qBittorrent or Transmission
|
||||
qBittorrent or SABnzbd
|
||||
</strong>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Download client for managing torrent downloads (URL and credentials)
|
||||
Download client for torrents (qBittorrent) or Usenet/NZB (SABnzbd)
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user