Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions client/src/components/settings/FlareSolverrSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import customSonner from '@/components/CustomSonner';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { rpc } from '@/lib/rpc';
import type { DbUserSettings } from '@server/db/app/app-schema';
import { Loader2 } from 'lucide-react';
import { useState } from 'react';
import type { Dispatch, SetStateAction } from 'react';

type FlareSolverrSettingsProps = {
enabled: boolean;
serverUrl: string;
timeoutSeconds: number;
setData: Dispatch<SetStateAction<DbUserSettings | null | undefined>>;
};

export default function FlareSolverrSettings({
enabled,
serverUrl,
timeoutSeconds,
setData,
}: FlareSolverrSettingsProps) {
const [isTestingConnection, setIsTestingConnection] = useState(false);

const handleTestConnection = async () => {
if (!enabled) {
customSonner({ variant: 'error', text: 'FlareSolverr is disabled' });
return;
}

if (!serverUrl) {
customSonner({ variant: 'error', text: 'FlareSolverr URL is required' });
return;
}

if (!isValidUrl(serverUrl)) {
customSonner({
variant: 'error',
text: 'Invalid FlareSolverr URL. It should include the protocol.',
});
return;
}

try {
setIsTestingConnection(true);
const response = await rpc.api.flaresolverr.verify.$post({
json: {
flaresolverrUrl: serverUrl,
timeoutSeconds,
},
});
const payload = await response.json();

if (!response.ok || !payload?.success) {
customSonner({
variant: 'error',
text: payload?.message ?? 'Failed to test FlareSolverr connection',
});
return;
}

customSonner({ text: 'FlareSolverr connection successful' });
} catch (error) {
const description =
error instanceof Error ? error.message : String(error);
customSonner({
variant: 'error',
text: 'Failed to test FlareSolverr connection',
description,
});
} finally {
setIsTestingConnection(false);
}
};

return (
<div className="flex flex-row gap-4">
<div className="flex flex-col gap-2 w-1/3">
<h2 className="text-xl font-black text-zinc-300">
FlareSolverr Settings
</h2>
</div>

<div className="flex flex-col gap-10 w-2/3">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-3">
<Checkbox
id="flaresolverr-enabled"
checked={enabled}
onCheckedChange={(checked) =>
setData((data) => {
if (!data) return data;
return {
...data,
flaresolverrEnabled: checked === true,
};
})
}
/>
<Label htmlFor="flaresolverr-enabled">Enable FlareSolverr</Label>
</div>
</div>

<div className="flex flex-row w-full gap-6 border-t border-zinc-800 pt-6">
<div className="flex flex-col w-1/2 gap-4">
<h3 className="text-lg font-extrabold">Server URL</h3>
<Input
className="font-mono text-base"
placeholder="http://localhost:8191"
value={serverUrl}
disabled={!enabled}
onChange={(event) =>
setData((data) => {
if (!data) return data;
return {
...data,
flaresolverrUrl: event.target.value,
};
})
}
/>
</div>
<div className="flex flex-col w-1/2 gap-2 justify-end">
<Button
variant="secondary"
className="font-bold"
onClick={handleTestConnection}
disabled={!enabled || isTestingConnection}
>
{isTestingConnection ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
'Test Connection'
)}
</Button>
</div>
</div>

<div className="flex flex-col gap-4 border-t border-zinc-800 pt-6">
<h3 className="text-lg font-extrabold">Timeout</h3>
<Input
className="w-40 font-mono text-base"
type="number"
min={1}
max={300}
value={timeoutSeconds}
disabled={!enabled}
onChange={(event) =>
setData((data) => {
if (!data) return data;
return {
...data,
flaresolverrTimeoutSeconds: parseTimeoutSeconds(
event.target.value,
),
};
})
}
/>
</div>
</div>
</div>
);
}

// Helpers

function isValidUrl(value: string): boolean {
try {
return Boolean(new URL(value));
} catch {
return false;
}
}

function parseTimeoutSeconds(value: string): number {
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed)) return 60;
return Math.min(Math.max(parsed, 1), 300);
}
35 changes: 22 additions & 13 deletions client/src/routes/dashboard/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import customSonner from '@/components/CustomSonner';
import GeneralSettings from '@/components/settings/GeneralSettings';
import FlareSolverrSettings from '@/components/settings/FlareSolverrSettings';
import JackettSettings from '@/components/settings/JackettSettings';
import NotificationSettings from '@/components/settings/NotificationSettings';
import SettingsMenu from '@/components/settings/SettingsMenu';
Expand Down Expand Up @@ -36,7 +37,7 @@ export default function Settings() {

if (typeof next === 'function') {
const updater = next as (
prevState: DbUserSettings | null | undefined
prevState: DbUserSettings | null | undefined,
) => DbUserSettings | null | undefined;
const updated = updater(resolvedCurrentData);
return updated ?? undefined;
Expand All @@ -45,7 +46,7 @@ export default function Settings() {
return next ?? undefined;
});
},
[settingsData]
[settingsData],
);

const handleSave = async () => {
Expand All @@ -66,11 +67,11 @@ export default function Settings() {
if (isLoadingSettings) {
return (
<>
<div className='flex flex-col w-full'>
<div className="flex flex-col w-full">
<SettingsMenu />
</div>
<div className='flex flex-col gap-4 mx-auto mt-[30vh]'>
<Loader2 className='w-10 h-10 animate-spin' />
<div className="flex flex-col gap-4 mx-auto mt-[30vh]">
<Loader2 className="w-10 h-10 animate-spin" />
</div>
</>
);
Expand All @@ -91,32 +92,40 @@ export default function Settings() {
syncInterval={data?.syncInterval ?? 0}
setData={setData}
/>
<Separator className='my-12' />
<Separator className="my-12" />
<TorrentClientSettings
downloadDir={data?.downloadDir ?? ''}
mediaDir={data?.mediaDir ?? ''}
deleteAfterDownload={data?.deleteAfterDownload ?? false}
setData={setData}
/>
<Separator className='my-12' />
<Separator className="my-12" />
<NotificationSettings
telegramId={data?.telegramId ?? 0}
botToken={data?.botToken ?? ''}
setData={setData}
/>
<Separator className='my-12' />
<Separator className="my-12" />
<JackettSettings
jackettUrl={data?.jackettUrl ?? ''}
jackettApiKey={data?.jackettApiKey ?? ''}
setData={setData}
/>
<Separator className='my-12' />
<Separator className="my-12" />
<FlareSolverrSettings
enabled={data?.flaresolverrEnabled ?? false}
serverUrl={data?.flaresolverrUrl ?? ''}
timeoutSeconds={data?.flaresolverrTimeoutSeconds ?? 60}
setData={setData}
/>
<Separator className="my-12" />
<Button
className='w-fit my-10 flex font-extrabold ml-auto'
size='lg'
onClick={handleSave}>
className="w-fit my-10 flex font-extrabold ml-auto"
size="lg"
onClick={handleSave}
>
{isSavingSettings ? (
<Loader2 className='w-4 h-4 animate-spin' />
<Loader2 className="w-4 h-4 animate-spin" />
) : (
'Save Settings'
)}
Expand Down
9 changes: 8 additions & 1 deletion server/src/db/app/app-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const torrentItems = sqliteTable(
}).notNull(),
errorMessage: text('error_message'),
},
(t) => [index('tracker_index').on(t.tracker)]
(t) => [index('tracker_index').on(t.tracker)],
);

export const userSettings = sqliteTable('user_settings', {
Expand All @@ -61,6 +61,13 @@ export const userSettings = sqliteTable('user_settings', {
jackettUrl: text('jackett_url'),
kinozalUsername: text('kinozal_username'),
kinozalPassword: text('kinozal_password'),
flaresolverrEnabled: int('flaresolverr_enabled', {
mode: 'boolean',
}).default(false),
flaresolverrUrl: text('flaresolverr_url'),
flaresolverrTimeoutSeconds: int('flaresolverr_timeout_seconds')
.default(60)
.notNull(),
});

export type DbTorrentItem = typeof torrentItems.$inferSelect;
Expand Down
2 changes: 2 additions & 0 deletions server/src/db/migrations/0001_add_flaresolverr_settings.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE `user_settings` ADD `flaresolverr_enabled` integer DEFAULT false;--> statement-breakpoint
ALTER TABLE `user_settings` ADD `flaresolverr_url` text;
1 change: 1 addition & 0 deletions server/src/db/migrations/0002_add_flaresolverr_timeout.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `user_settings` ADD `flaresolverr_timeout_seconds` integer DEFAULT 60 NOT NULL;
Loading
Loading