diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index e365329..cdc4f1a 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -26,9 +26,6 @@ jobs: - name: Start stacka env: PUBLIC_BASE_URL: https://videodownloader.naurecki.pl - TRIAL_ENABLED: ${{ vars.TRIAL_ENABLED }} - TRIAL_DAYS_LEFT: ${{ vars.TRIAL_DAYS_LEFT }} - TRIAL_EXPIRES: ${{ vars.TRIAL_EXPIRES }} run: docker compose -f docker-compose.prod.yml up -d - name: Czekaj na gotowość API diff --git a/DEPLOY.md b/DEPLOY.md index 709227d..2c5138d 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -33,6 +33,3 @@ sudo certbot --apache -d videodownloader.naurecki.pl --redirect -n \ DNS: `*.naurecki.pl` (wildcard) już wskazuje serwer — bez dodatkowej konfiguracji. -## Tryb testowy (opcjonalny banner) -Ustaw zmienne repozytorium w Gitei (Settings → Actions → Variables): -`TRIAL_ENABLED=1`, `TRIAL_DAYS_LEFT=60`, `TRIAL_EXPIRES=2026-08-27`. diff --git a/README.md b/README.md index 0b2625a..c91ade3 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,6 @@ 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. -- Banner **trybu testowego** (kosmetyczny, sterowany zmienną `TRIAL_*`). - Przyciski **Wsparcie** / **O mnie**. > Różnica względem wersji desktop: pole „Zapisz do” (ścieżka dyskowa Windows) w diff --git a/apps/api/src/health/health.controller.ts b/apps/api/src/health/health.controller.ts index b86fc16..a59ba58 100644 --- a/apps/api/src/health/health.controller.ts +++ b/apps/api/src/health/health.controller.ts @@ -8,11 +8,6 @@ export class HealthController { ok: true, service: 'vsd-api', version: process.env.APP_VERSION || '6.5.6', - trial: { - enabled: process.env.TRIAL_ENABLED === '1', - expires: process.env.TRIAL_EXPIRES || null, - daysLeft: process.env.TRIAL_DAYS_LEFT ? Number(process.env.TRIAL_DAYS_LEFT) : null, - }, }; } } diff --git a/apps/api/src/jobs/dto.ts b/apps/api/src/jobs/dto.ts index 9828211..b9ae0d7 100644 --- a/apps/api/src/jobs/dto.ts +++ b/apps/api/src/jobs/dto.ts @@ -28,4 +28,9 @@ export class CreateJobDto { @IsOptional() @IsBoolean() subtitles?: boolean; + + // 'embed' = osadź napisy w pliku wideo; 'file' = zapisz jako osobny plik .srt + @IsOptional() + @IsIn(['embed', 'file']) + subsMode?: 'embed' | 'file'; } diff --git a/apps/api/src/jobs/jobs.controller.ts b/apps/api/src/jobs/jobs.controller.ts index 1e24dd0..370e47c 100644 --- a/apps/api/src/jobs/jobs.controller.ts +++ b/apps/api/src/jobs/jobs.controller.ts @@ -30,8 +30,10 @@ export class JobsController { @Sse(':id/events') events(@Param('id') id: string): Observable { + // Bez pola `type` — inaczej Nest emituje nazwane zdarzenia (event: ), + // a przeglądarkowy EventSource.onmessage reaguje tylko na domyślne „message”. return this.jobs.events(id).pipe( - map((ev) => ({ data: JSON.stringify(ev), type: ev.kind }) as MessageEvent), + map((ev) => ({ data: JSON.stringify(ev) }) as MessageEvent), ); } diff --git a/apps/api/src/jobs/jobs.service.ts b/apps/api/src/jobs/jobs.service.ts index ce37cd6..9dd0139 100644 --- a/apps/api/src/jobs/jobs.service.ts +++ b/apps/api/src/jobs/jobs.service.ts @@ -67,7 +67,14 @@ export class JobsService { private async run(job: Job, dto: CreateJobDto) { const outTemplate = path.join(job.workDir, '%(playlist_index&{} - |)s%(title)s.%(ext)s'); - const args = buildArgs(dto.mode, dto.url, dto.format, !!dto.subtitles, outTemplate); + const args = buildArgs( + dto.mode, + dto.url, + dto.format, + !!dto.subtitles, + dto.subsMode || 'embed', + outTemplate, + ); this.emit(job, { kind: 'status', status: 'running' }); this.emit(job, { kind: 'log', line: `$ yt-dlp ${args.join(' ')}` }); diff --git a/apps/api/src/ytdlp/ytdlp.service.ts b/apps/api/src/ytdlp/ytdlp.service.ts index ba555a7..bb5e22b 100644 --- a/apps/api/src/ytdlp/ytdlp.service.ts +++ b/apps/api/src/ytdlp/ytdlp.service.ts @@ -12,6 +12,7 @@ export function buildArgs( url: string, format: string | undefined, subtitles: boolean, + subsMode: 'embed' | 'file', outTemplate: string, ): string[] { const base = [ @@ -25,8 +26,11 @@ export function buildArgs( ]; // Napisy: tylko realne (bez automatycznie generowanych przez YouTube). + // embed -> wtopione w plik wideo; file -> osobny plik .srt obok wideo (do ZIP). const subs = subtitles - ? ['--write-subs', '--no-write-auto-subs', '--sub-langs', 'all', '--embed-subs'] + ? subsMode === 'file' + ? ['--write-subs', '--no-write-auto-subs', '--sub-langs', 'all', '--convert-subs', 'srt'] + : ['--write-subs', '--no-write-auto-subs', '--sub-langs', 'all', '--embed-subs'] : []; switch (mode) { diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 5f5ed04..cff0966 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -79,10 +79,10 @@ export default function App() { const [mode, setMode] = useState('mp3'); const [format, setFormat] = useState('320'); const [subtitles, setSubtitles] = useState(false); + const [subsMode, setSubsMode] = useState<'embed' | 'file'>('embed'); const [busy, setBusy] = useState(false); const [progress, setProgress] = useState(0); const [log, setLog] = useState(['OK: wszystko gotowe do pobierania.']); - const [trial, setTrial] = useState<{ daysLeft: number | null; expires: string | null } | null>(null); const esRef = useRef(null); const jobRef = useRef(null); const logEndRef = useRef(null); @@ -94,15 +94,6 @@ export default function App() { setFormat(MODES.find((m) => m.key === mode)!.formats[0].value); }, [mode]); - useEffect(() => { - fetch('/api/health') - .then((r) => r.json()) - .then((h) => { - if (h?.trial?.enabled) setTrial({ daysLeft: h.trial.daysLeft, expires: h.trial.expires }); - }) - .catch(() => {}); - }, []); - useEffect(() => { logEndRef.current?.scrollIntoView({ block: 'end' }); }, [log]); @@ -139,7 +130,7 @@ export default function App() { const res = await fetch('/api/jobs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ url: url.trim(), mode, format, subtitles }), + body: JSON.stringify({ url: url.trim(), mode, format, subtitles, subsMode }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const { id } = await res.json(); @@ -262,21 +253,34 @@ export default function App() {
- +
- Pobranie przez przeglądarkę (folder „Pobrane”) +
+

+ Napisy dotyczą tylko faktycznie dostępnych napisów — nie obejmują + automatycznie generowanych napisów YouTube. Gotowy plik pobiera się przez + przeglądarkę (folder „Pobrane”). +