diff --git a/README.md b/README.md index c91ade3..a55bea5 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,11 @@ Ruch publiczny: `Apache (vhost videodownloader.naurecki.pl, SSL)` → `127.0.0.1 zgodnie z oryginałem **nie** dotyczy automatycznie generowanych napisów YouTube. - Duży przycisk **POBIERZ** + **STOP** (przerwanie joba). - **Log na żywo** (SSE) — komunikaty postępu, konwersji i scalania audio/wideo. -- Przyciski **Wsparcie** / **O mnie**. +- Przyciski **Wsparcie** / **O mnie** oraz informacja o wersji **yt-dlp** z + przyciskiem „Sprawdź aktualizacje”. +- **Auto-aktualizacja yt-dlp** — API sprawdza i aktualizuje yt-dlp (`yt-dlp -U`) + przy starcie oraz cyklicznie (domyślnie co 24 h; `YTDLP_AUTO_UPDATE`, + `YTDLP_UPDATE_INTERVAL_H`). Ręczne wymuszenie: `POST /api/ytdlp/update`. > Różnica względem wersji desktop: pole „Zapisz do” (ścieżka dyskowa Windows) w > wersji web zostaje zastąpione pobraniem gotowego pliku przez przeglądarkę. Serwer diff --git a/apps/api/src/jobs/jobs.module.ts b/apps/api/src/jobs/jobs.module.ts index b856d01..5f775f3 100644 --- a/apps/api/src/jobs/jobs.module.ts +++ b/apps/api/src/jobs/jobs.module.ts @@ -2,9 +2,11 @@ import { Module } from '@nestjs/common'; import { JobsController, ProbeController } from './jobs.controller'; import { JobsService } from './jobs.service'; import { YtdlpService } from '../ytdlp/ytdlp.service'; +import { UpdaterService } from '../ytdlp/updater.service'; +import { YtdlpController } from '../ytdlp/ytdlp.controller'; @Module({ - controllers: [JobsController, ProbeController], - providers: [JobsService, YtdlpService], + controllers: [JobsController, ProbeController, YtdlpController], + providers: [JobsService, YtdlpService, UpdaterService], }) export class JobsModule {} diff --git a/apps/api/src/ytdlp/updater.service.ts b/apps/api/src/ytdlp/updater.service.ts new file mode 100644 index 0000000..e5c63c9 --- /dev/null +++ b/apps/api/src/ytdlp/updater.service.ts @@ -0,0 +1,89 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { spawn } from 'child_process'; + +const YTDLP = process.env.YTDLP_BIN || 'yt-dlp'; +// Co ile godzin sprawdzać aktualizacje (0 lub YTDLP_AUTO_UPDATE=0 wyłącza auto-update) +const INTERVAL_H = Number(process.env.YTDLP_UPDATE_INTERVAL_H || 24); + +interface UpdateResult { + updated: boolean; + message: string; + version: string; +} + +@Injectable() +export class UpdaterService implements OnModuleInit { + private readonly log = new Logger('Updater'); + version = 'unknown'; + lastCheck: string | null = null; + lastResult = ''; + updating = false; + + async onModuleInit() { + this.version = await this.readVersion(); + this.log.log(`yt-dlp w wersji ${this.version}`); + if (process.env.YTDLP_AUTO_UPDATE !== '0' && INTERVAL_H > 0) { + void this.update(); // sprawdź od razu przy starcie + setInterval(() => void this.update(), INTERVAL_H * 3600 * 1000).unref(); + } + } + + private readVersion(): Promise { + return run([YTDLP, '--version']) + .then((r) => r.out.trim() || 'unknown') + .catch(() => 'unknown'); + } + + /** Uruchamia samoaktualizację yt-dlp (`yt-dlp -U`). */ + async update(): Promise { + if (this.updating) { + return { updated: false, message: 'Aktualizacja już trwa.', version: this.version }; + } + this.updating = true; + try { + const r = await run([YTDLP, '-U']); + const message = `${r.out}${r.err}`.trim(); + const updated = /Updated yt-dlp to|Updating to/i.test(message); + this.version = await this.readVersion(); + this.lastCheck = new Date().toISOString(); + this.lastResult = message; + this.log.log( + updated + ? `yt-dlp zaktualizowany do ${this.version}` + : `yt-dlp aktualny (${this.version})`, + ); + return { updated, message, version: this.version }; + } catch (e: any) { + const message = `Błąd aktualizacji: ${e?.message || e}`; + this.lastCheck = new Date().toISOString(); + this.lastResult = message; + this.log.warn(message); + return { updated: false, message, version: this.version }; + } finally { + this.updating = false; + } + } + + status() { + return { + version: this.version, + autoUpdate: process.env.YTDLP_AUTO_UPDATE !== '0' && INTERVAL_H > 0, + intervalHours: INTERVAL_H, + lastCheck: this.lastCheck, + lastResult: this.lastResult, + updating: this.updating, + }; + } +} + +function run(cmd: string[]): Promise<{ code: number; out: string; err: string }> { + return new Promise((resolve, reject) => { + const c = spawn(cmd[0], cmd.slice(1)); + let out = ''; + let err = ''; + c.stdout!.on('data', (b) => (out += b.toString())); + c.stderr!.on('data', (b) => (err += b.toString())); + c.on('close', (code) => resolve({ code: code ?? 0, out, err })); + c.on('error', reject); + }); +} diff --git a/apps/api/src/ytdlp/ytdlp.controller.ts b/apps/api/src/ytdlp/ytdlp.controller.ts new file mode 100644 index 0000000..ffb2f06 --- /dev/null +++ b/apps/api/src/ytdlp/ytdlp.controller.ts @@ -0,0 +1,19 @@ +import { Controller, Get, Post } from '@nestjs/common'; +import { UpdaterService } from './updater.service'; + +@Controller('ytdlp') +export class YtdlpController { + constructor(private readonly updater: UpdaterService) {} + + /** Bieżąca wersja yt-dlp + stan auto-aktualizacji. */ + @Get() + status() { + return this.updater.status(); + } + + /** Ręczne sprawdzenie/wymuszenie aktualizacji yt-dlp. */ + @Post('update') + update() { + return this.updater.update(); + } +} diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index cff0966..2a4e1d9 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -83,6 +83,7 @@ export default function App() { const [busy, setBusy] = useState(false); const [progress, setProgress] = useState(0); const [log, setLog] = useState(['OK: wszystko gotowe do pobierania.']); + const [ytdlp, setYtdlp] = useState<{ version: string; autoUpdate: boolean; updating?: boolean } | null>(null); const esRef = useRef(null); const jobRef = useRef(null); const logEndRef = useRef(null); @@ -98,6 +99,31 @@ export default function App() { logEndRef.current?.scrollIntoView({ block: 'end' }); }, [log]); + useEffect(() => { + fetch('/api/ytdlp') + .then((r) => r.json()) + .then(setYtdlp) + .catch(() => {}); + }, []); + + async function checkUpdate() { + setYtdlp((y) => (y ? { ...y, updating: true } : y)); + appendLog('Sprawdzam aktualizacje yt-dlp...'); + try { + const r = await fetch('/api/ytdlp/update', { method: 'POST' }); + const res = await r.json(); + appendLog( + res.updated + ? `yt-dlp zaktualizowany do ${res.version}.` + : `yt-dlp jest aktualny (${res.version}).`, + ); + setYtdlp((y) => (y ? { ...y, version: res.version, updating: false } : y)); + } catch { + appendLog('Nie udało się sprawdzić aktualizacji yt-dlp.'); + setYtdlp((y) => (y ? { ...y, updating: false } : y)); + } + } + function appendLog(line: string) { setLog((l) => [...l.slice(-400), line]); } @@ -318,6 +344,17 @@ export default function App() { + {ytdlp && ( +
+ + yt-dlp {ytdlp.version} + {ytdlp.autoUpdate ? ' · auto-update' : ''} + + +
+ )}