feat: auto-aktualizacja yt-dlp (start + cyklicznie) + endpointy /api/ytdlp i przycisk w UI
All checks were successful
deploy / deploy (push) Successful in 2m21s
All checks were successful
deploy / deploy (push) Successful in 2m21s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
zgodnie z oryginałem **nie** dotyczy automatycznie generowanych napisów YouTube.
|
||||||
- Duży przycisk **POBIERZ** + **STOP** (przerwanie joba).
|
- Duży przycisk **POBIERZ** + **STOP** (przerwanie joba).
|
||||||
- **Log na żywo** (SSE) — komunikaty postępu, konwersji i scalania audio/wideo.
|
- **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
|
> 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
|
> wersji web zostaje zastąpione pobraniem gotowego pliku przez przeglądarkę. Serwer
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { Module } from '@nestjs/common';
|
|||||||
import { JobsController, ProbeController } from './jobs.controller';
|
import { JobsController, ProbeController } from './jobs.controller';
|
||||||
import { JobsService } from './jobs.service';
|
import { JobsService } from './jobs.service';
|
||||||
import { YtdlpService } from '../ytdlp/ytdlp.service';
|
import { YtdlpService } from '../ytdlp/ytdlp.service';
|
||||||
|
import { UpdaterService } from '../ytdlp/updater.service';
|
||||||
|
import { YtdlpController } from '../ytdlp/ytdlp.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [JobsController, ProbeController],
|
controllers: [JobsController, ProbeController, YtdlpController],
|
||||||
providers: [JobsService, YtdlpService],
|
providers: [JobsService, YtdlpService, UpdaterService],
|
||||||
})
|
})
|
||||||
export class JobsModule {}
|
export class JobsModule {}
|
||||||
|
|||||||
89
apps/api/src/ytdlp/updater.service.ts
Normal file
89
apps/api/src/ytdlp/updater.service.ts
Normal file
@@ -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<string> {
|
||||||
|
return run([YTDLP, '--version'])
|
||||||
|
.then((r) => r.out.trim() || 'unknown')
|
||||||
|
.catch(() => 'unknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Uruchamia samoaktualizację yt-dlp (`yt-dlp -U`). */
|
||||||
|
async update(): Promise<UpdateResult> {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
19
apps/api/src/ytdlp/ytdlp.controller.ts
Normal file
19
apps/api/src/ytdlp/ytdlp.controller.ts
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,6 +83,7 @@ export default function App() {
|
|||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [log, setLog] = useState<string[]>(['OK: wszystko gotowe do pobierania.']);
|
const [log, setLog] = useState<string[]>(['OK: wszystko gotowe do pobierania.']);
|
||||||
|
const [ytdlp, setYtdlp] = useState<{ version: string; autoUpdate: boolean; updating?: boolean } | null>(null);
|
||||||
const esRef = useRef<EventSource | null>(null);
|
const esRef = useRef<EventSource | null>(null);
|
||||||
const jobRef = useRef<string | null>(null);
|
const jobRef = useRef<string | null>(null);
|
||||||
const logEndRef = useRef<HTMLDivElement | null>(null);
|
const logEndRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -98,6 +99,31 @@ export default function App() {
|
|||||||
logEndRef.current?.scrollIntoView({ block: 'end' });
|
logEndRef.current?.scrollIntoView({ block: 'end' });
|
||||||
}, [log]);
|
}, [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) {
|
function appendLog(line: string) {
|
||||||
setLog((l) => [...l.slice(-400), line]);
|
setLog((l) => [...l.slice(-400), line]);
|
||||||
}
|
}
|
||||||
@@ -318,6 +344,17 @@ export default function App() {
|
|||||||
<button className="ghost" onClick={() => alert('Wsparcie: napisz na support@naurecki.pl')}>
|
<button className="ghost" onClick={() => alert('Wsparcie: napisz na support@naurecki.pl')}>
|
||||||
Wsparcie
|
Wsparcie
|
||||||
</button>
|
</button>
|
||||||
|
{ytdlp && (
|
||||||
|
<div className="ytdlp-info">
|
||||||
|
<span title={ytdlp.autoUpdate ? 'Auto-aktualizacja włączona' : 'Auto-aktualizacja wyłączona'}>
|
||||||
|
yt-dlp {ytdlp.version}
|
||||||
|
{ytdlp.autoUpdate ? ' · auto-update' : ''}
|
||||||
|
</span>
|
||||||
|
<button className="ghost small" onClick={checkUpdate} disabled={ytdlp.updating}>
|
||||||
|
{ytdlp.updating ? 'Sprawdzam…' : 'Sprawdź aktualizacje'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
className="ghost"
|
className="ghost"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|||||||
@@ -121,7 +121,9 @@ body {
|
|||||||
}
|
}
|
||||||
.logline { white-space: pre-wrap; word-break: break-all; line-height: 1.5; }
|
.logline { white-space: pre-wrap; word-break: break-all; line-height: 1.5; }
|
||||||
|
|
||||||
.foot { display: flex; justify-content: space-between; margin-top: 16px; }
|
.foot { display: flex; justify-content: space-between; align-items: center; margin-top: 16px; gap: 12px; }
|
||||||
|
.ytdlp-info { display: flex; align-items: center; gap: 10px; color: var(--muted); font-size: 12px; }
|
||||||
|
.ghost.small { padding: 6px 12px; font-size: 12px; }
|
||||||
.ghost {
|
.ghost {
|
||||||
background: transparent; border: 1px solid var(--border); border-radius: 8px;
|
background: transparent; border: 1px solid var(--border); border-radius: 8px;
|
||||||
color: var(--text); padding: 9px 18px; font-size: 13px; cursor: pointer;
|
color: var(--text); padding: 9px 18px; font-size: 13px; cursor: pointer;
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ services:
|
|||||||
APP_VERSION: "6.5.6"
|
APP_VERSION: "6.5.6"
|
||||||
CORS_ORIGIN: ${PUBLIC_BASE_URL:-https://videodownloader.naurecki.pl}
|
CORS_ORIGIN: ${PUBLIC_BASE_URL:-https://videodownloader.naurecki.pl}
|
||||||
WORK_DIR: /app/work
|
WORK_DIR: /app/work
|
||||||
|
# Auto-aktualizacja yt-dlp: sprawdzanie przy starcie i co N godzin (0 wyłącza)
|
||||||
|
YTDLP_AUTO_UPDATE: ${YTDLP_AUTO_UPDATE:-1}
|
||||||
|
YTDLP_UPDATE_INTERVAL_H: ${YTDLP_UPDATE_INTERVAL_H:-24}
|
||||||
tmpfs:
|
tmpfs:
|
||||||
# katalog roboczy pobierań w pamięci/efemeryczny — sprzątany po jobie
|
# katalog roboczy pobierań w pamięci/efemeryczny — sprzątany po jobie
|
||||||
- /app/work
|
- /app/work
|
||||||
|
|||||||
Reference in New Issue
Block a user