Update PVideoDl to support custom download strategies and enhance cookie handling. Added new downloader registration system, updated README for clarity on downloaders, and introduced cookies configuration in settings. Frontend now includes a dedicated tab for download strategies and API endpoints for listing them.

This commit is contained in:
Leonid Pershin
2026-06-20 09:08:31 +03:00
parent 3e47e95fc4
commit eae4def0bf
15 changed files with 639 additions and 74 deletions
+5 -1
View File
@@ -1,6 +1,6 @@
// Клиент к бэкенду. Базовый путь относительный — и в деве (через Vite-прокси),
// и в проде (FastAPI отдаёт статику с того же origin) работает одинаково.
import type { Download } from './types';
import type { Download, DownloaderInfo } from './types';
const BASE = '/api';
@@ -36,3 +36,7 @@ export async function addDownloads(urls: string[]): Promise<Download[]> {
export async function deleteDownload(id: string): Promise<void> {
return handle(await fetch(`${BASE}/downloads/${id}`, { method: 'DELETE' }));
}
export async function listDownloaders(): Promise<DownloaderInfo[]> {
return handle(await fetch(`${BASE}/downloaders`));
}
+7
View File
@@ -15,6 +15,13 @@ export interface Download {
finished_at: string | null;
}
export interface DownloaderInfo {
name: string;
kind: 'direct' | 'extractor' | 'fallback' | string;
priority: number;
description: string | null;
}
export type DownloadEventType = 'created' | 'progress' | 'done' | 'failed' | 'deleted';
export interface DownloadEvent {
+5 -4
View File
@@ -2,7 +2,7 @@
import '../app.css';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { DownloadCloud, History, Wifi, WifiOff } from 'lucide-svelte';
import { DownloadCloud, History, Puzzle, Wifi, WifiOff } from 'lucide-svelte';
import * as api from '$lib/api';
import {
setDownloads,
@@ -35,7 +35,8 @@
const tabs = [
{ href: '/', label: 'Активные', icon: DownloadCloud },
{ href: '/history', label: 'История', icon: History }
{ href: '/history', label: 'История', icon: History },
{ href: '/downloaders', label: 'Загрузчики', icon: Puzzle }
];
const path = $derived($page.url.pathname);
</script>
@@ -63,7 +64,7 @@
<nav class="mb-8 flex gap-1 border-b border-zinc-800">
{#each tabs as tab (tab.href)}
{@const active = path === tab.href}
{@const count = tab.href === '/' ? $activeCount : $historyCount}
{@const count = tab.href === '/' ? $activeCount : tab.href === '/history' ? $historyCount : null}
<a
href={tab.href}
class="-mb-px inline-flex items-center gap-2 border-b-2 px-3 py-2.5 text-sm font-medium transition-colors {active
@@ -72,7 +73,7 @@
>
<tab.icon class="size-4" />
{tab.label}
{#if $hydrated && count > 0}
{#if $hydrated && count !== null && count > 0}
<span class="rounded-full bg-zinc-800 px-1.5 py-0.5 text-xs font-normal text-zinc-400"
>{count}</span
>
@@ -0,0 +1,71 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Puzzle } from 'lucide-svelte';
import * as api from '$lib/api';
import { pushToast } from '$lib/stores';
import type { DownloaderInfo } from '$lib/types';
let items = $state<DownloaderInfo[]>([]);
let loading = $state(true);
onMount(() => {
api
.listDownloaders()
.then((d) => (items = d))
.catch((e) => pushToast('error', `Не удалось загрузить загрузчики: ${e.message}`))
.finally(() => (loading = false));
});
const kind: Record<string, { label: string; cls: string }> = {
direct: { label: 'Прямые файлы', cls: 'bg-sky-500/15 text-sky-300' },
extractor: { label: 'Сайт', cls: 'bg-indigo-500/15 text-indigo-300' },
fallback: { label: 'Универсальный', cls: 'bg-zinc-700/60 text-zinc-300' }
};
const kindInfo = (k: string) => kind[k] ?? { label: k, cls: 'bg-zinc-700/60 text-zinc-300' };
</script>
<svelte:head><title>PVideoDl — загрузчики</title></svelte:head>
<p class="mb-5 text-sm text-zinc-500">
Стратегии скачивания в порядке приоритета: ссылку берёт первый подходящий загрузчик.
Если ссылка не прямой файл и ни один загрузчик её не поддерживает — загрузка завершится
ошибкой.
</p>
<section>
{#if loading}
<p class="py-12 text-center text-sm text-zinc-500">Загрузка…</p>
{:else if items.length === 0}
<div class="flex flex-col items-center gap-3 py-16 text-center text-zinc-500">
<Puzzle class="size-10 opacity-40" />
<p class="text-sm">Загрузчиков нет.</p>
</div>
{:else}
<div class="flex flex-col gap-3">
{#each items as d (d.name)}
<div
class="rounded-xl border border-zinc-800 bg-zinc-900/60 p-4 transition-colors hover:border-zinc-700"
>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<p class="font-medium text-zinc-100">{d.name}</p>
{#if d.description}
<p class="mt-0.5 text-sm text-zinc-500">{d.description}</p>
{/if}
</div>
<div class="flex shrink-0 items-center gap-2">
<span
class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {kindInfo(
d.kind
).cls}"
>
{kindInfo(d.kind).label}
</span>
<span class="text-xs text-zinc-600" title="Приоритет">#{d.priority}</span>
</div>
</div>
</div>
{/each}
</div>
{/if}
</section>