style: usun em-dashe z calego repo (zamiana na zwykly mylnik)
All checks were successful
deploy / deploy (push) Successful in 1m3s
All checks were successful
deploy / deploy (push) Successful in 1m3s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Sanity — docker dostępny
|
||||
- name: Sanity - docker dostępny
|
||||
run: docker version && docker compose version
|
||||
|
||||
- name: Build obrazów
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Wdrożenie — videodownloader.naurecki.pl
|
||||
# Wdrożenie - videodownloader.naurecki.pl
|
||||
|
||||
## Pipeline
|
||||
Push do `main` uruchamia `.gitea/workflows/deploy.yml` na runnerze Gitea
|
||||
@@ -6,7 +6,7 @@ Push do `main` uruchamia `.gitea/workflows/deploy.yml` na runnerze Gitea
|
||||
i robi `up -d` na demonie Dockera serwera. Serwis `web` (nginx) słucha na
|
||||
`127.0.0.1:8086`, `api` (NestJS) wewnętrznie + diagnostycznie na `127.0.0.1:3105`.
|
||||
|
||||
## Reverse proxy (Apache) — jednorazowo na serwerze
|
||||
## Reverse proxy (Apache) - jednorazowo na serwerze
|
||||
Plik `/etc/apache2/sites-available/videodownloader.naurecki.pl.conf`:
|
||||
|
||||
```apache
|
||||
@@ -31,5 +31,5 @@ sudo certbot --apache -d videodownloader.naurecki.pl --redirect -n \
|
||||
--agree-tos -m dariusz@naurecki.pl
|
||||
```
|
||||
|
||||
DNS: `*.naurecki.pl` (wildcard) już wskazuje serwer — bez dodatkowej konfiguracji.
|
||||
DNS: `*.naurecki.pl` (wildcard) już wskazuje serwer - bez dodatkowej konfiguracji.
|
||||
|
||||
|
||||
20
PROMPT.md
20
PROMPT.md
@@ -7,33 +7,33 @@ Dokument zawiera (1) gotowy **prompt** do wygenerowania aplikacji od zera oraz
|
||||
|
||||
## 1. Master-prompt (do odtworzenia aplikacji)
|
||||
|
||||
> Zbuduj webową aplikację SPA „Video & Sound Downloader Pro” — webowy odpowiednik
|
||||
> Zbuduj webową aplikację SPA „Video & Sound Downloader Pro” - webowy odpowiednik
|
||||
> desktopowego narzędzia łączącego `yt-dlp` i `FFmpeg`. **Stack: backend NestJS
|
||||
> (TypeScript), frontend React + Vite (TypeScript).** Monorepo `apps/api`, `apps/web`.
|
||||
>
|
||||
> **Backend (NestJS):**
|
||||
> - `POST /api/probe` — przyjmuje `{ url }`, uruchamia `yt-dlp -J` i zwraca tytuł,
|
||||
> - `POST /api/probe` - przyjmuje `{ url }`, uruchamia `yt-dlp -J` i zwraca tytuł,
|
||||
> miniaturę, czas trwania, dostępne jakości oraz listę języków napisów.
|
||||
> - `POST /api/jobs` — `{ url, mode, format, subtitles }`, gdzie `mode ∈
|
||||
> - `POST /api/jobs` - `{ url, mode, format, subtitles }`, gdzie `mode ∈
|
||||
> {mp3, mp4, playlist, facebook}`. Tworzy job w katalogu tymczasowym, uruchamia
|
||||
> `yt-dlp` (+ `ffmpeg`) z argumentami zależnymi od trybu i zwraca `jobId`.
|
||||
> - `GET /api/jobs/:id/events` — strumień **SSE** z liniami logu i procentem postępu
|
||||
> - `GET /api/jobs/:id/events` - strumień **SSE** z liniami logu i procentem postępu
|
||||
> (parsowanie `--newline --progress`); zdarzenia `log`, `progress`, `done`, `error`.
|
||||
> - `GET /api/jobs/:id/file` — strumień gotowego pliku do przeglądarki, po wysłaniu
|
||||
> - `GET /api/jobs/:id/file` - strumień gotowego pliku do przeglądarki, po wysłaniu
|
||||
> sprzątnięcie katalogu joba (TTL-cleanup dla porzuconych).
|
||||
> - `DELETE /api/jobs/:id` — STOP: zabija proces yt-dlp/ffmpeg joba.
|
||||
> - Joby w pamięci (EventEmitter) — bez bazy, lekko i „portable”.
|
||||
> - `DELETE /api/jobs/:id` - STOP: zabija proces yt-dlp/ffmpeg joba.
|
||||
> - Joby w pamięci (EventEmitter) - bez bazy, lekko i „portable”.
|
||||
> - Mapowanie trybów: `mp3` → `-x --audio-format mp3 --audio-quality <kbps>`;
|
||||
> `mp4`/`facebook` → `-f "bv*[height<=H]+ba/b" --merge-output-format mp4`;
|
||||
> `playlist` → `--yes-playlist`, wynik pakowany do ZIP. Napisy:
|
||||
> `--write-subs --sub-langs all --embed-subs` (bez auto-napisów: `--no-write-auto-subs`).
|
||||
>
|
||||
> **Frontend (React) — odwzoruj 1:1 zrzut ekranu desktopu:**
|
||||
> **Frontend (React) - odwzoruj 1:1 zrzut ekranu desktopu:**
|
||||
> ciemny motyw, akcenty niebieskie. Nagłówek z ikoną i wersją; pole **Link** z
|
||||
> przyciskiem wklejenia ze schowka; sekcja **„Wybierz opcję”** z 4 kafelkami
|
||||
> (MP3 / MP4 / PLAYLISTA / FACEBOOK, ikony i kolory jak na zrzucie, aktywny kafel
|
||||
> podświetlony); **„Jakość / Format”** jako dropdown zależny od trybu; w miejscu
|
||||
> „Zapisz do” — informacja o pobraniu przez przeglądarkę; duży przycisk
|
||||
> „Zapisz do” - informacja o pobraniu przez przeglądarkę; duży przycisk
|
||||
> **„POBIERZ …”** + **STOP**; **banner trybu testowego**; panel **Log** na żywo
|
||||
> (z SSE); stopka **Wsparcie** / **O mnie**.
|
||||
>
|
||||
@@ -56,7 +56,7 @@ Dokument zawiera (1) gotowy **prompt** do wygenerowania aplikacji od zera oraz
|
||||
| 6 | **CI/CD** | `.gitea/workflows/deploy.yml`: push `main` → build + `up -d` (docker.sock). |
|
||||
| 7 | **Reverse proxy** | vhost Apache `videodownloader.naurecki.pl` → `127.0.0.1:8086`. |
|
||||
| 8 | **SSL** | `certbot --apache -d videodownloader.naurecki.pl`. |
|
||||
| 9 | **DNS** | Gotowe — `*.naurecki.pl` (wildcard) już wskazuje `212.109.167.206`. |
|
||||
| 9 | **DNS** | Gotowe - `*.naurecki.pl` (wildcard) już wskazuje `212.109.167.206`. |
|
||||
| 10 | **Weryfikacja** | `https://videodownloader.naurecki.pl` + test pobrania. |
|
||||
|
||||
**Ustalenia infrastrukturalne** (zweryfikowane na serwerze):
|
||||
|
||||
18
README.md
18
README.md
@@ -1,8 +1,8 @@
|
||||
# Video & Sound Downloader Pro — wersja web (SPA)
|
||||
# Video & Sound Downloader Pro - wersja web (SPA)
|
||||
|
||||
Webowy odpowiednik desktopowego narzędzia **Video & Sound Downloader Pro v6.5.6**.
|
||||
Łączy `yt-dlp` + `FFmpeg` w jednym panelu: pobieranie audio (MP3), wideo (MP4),
|
||||
całych playlist oraz materiałów z Facebooka — bez wpisywania komend i bez ręcznej
|
||||
całych playlist oraz materiałów z Facebooka - bez wpisywania komend i bez ręcznej
|
||||
konwersji. Backend: **NestJS**, frontend: **React (Vite)**. Obraz portowany jest do
|
||||
Dockera i wdrażany na `https://videodownloader.naurecki.pl`.
|
||||
|
||||
@@ -17,10 +17,10 @@ Dockera i wdrażany na `https://videodownloader.naurecki.pl`.
|
||||
```
|
||||
videodownloader/
|
||||
├── apps/
|
||||
│ ├── api/ # NestJS — joby pobierania, yt-dlp+ffmpeg, postęp SSE
|
||||
│ ├── api/ # NestJS - joby pobierania, yt-dlp+ffmpeg, postęp SSE
|
||||
│ │ ├── src/
|
||||
│ │ └── Dockerfile # node + python3 + yt-dlp + ffmpeg
|
||||
│ └── web/ # React + Vite (TS) — UI odwzorowany ze zrzutu
|
||||
│ └── web/ # React + Vite (TS) - UI odwzorowany ze zrzutu
|
||||
│ ├── src/
|
||||
│ ├── nginx.conf # serwuje statyki + proxy /api (+ SSE bez buforowania)
|
||||
│ └── Dockerfile
|
||||
@@ -36,15 +36,15 @@ Ruch publiczny: `Apache (vhost videodownloader.naurecki.pl, SSL)` → `127.0.0.1
|
||||
- **Pole Link** z przyciskiem „wklej ze schowka”.
|
||||
- **4 tryby**: `MP3` (pobierz dźwięk), `MP4` (pobierz wideo), `PLAYLISTA`
|
||||
(pobierz całą listę → ZIP), `FACEBOOK` (pobierz wideo).
|
||||
- **Jakość / Format** — lista zależna od trybu (MP3 320/256/192/128 kbps;
|
||||
- **Jakość / Format** - lista zależna od trybu (MP3 320/256/192/128 kbps;
|
||||
MP4 2160/1440/1080/720/480p).
|
||||
- **Napisy** — opcja pobrania dostępnych napisów (`--write-subs --embed-subs`);
|
||||
- **Napisy** - opcja pobrania dostępnych napisów (`--write-subs --embed-subs`);
|
||||
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.
|
||||
- **Log na żywo** (SSE) - komunikaty postępu, konwersji i scalania audio/wideo.
|
||||
- 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`)
|
||||
- **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`.
|
||||
|
||||
@@ -67,4 +67,4 @@ Wymaga lokalnie `yt-dlp` i `ffmpeg` w PATH (w produkcji są w obrazie).
|
||||
|
||||
Push do `main` → runner Gitea (`.gitea/workflows/deploy.yml`) buduje obrazy z
|
||||
`docker-compose.prod.yml` i robi `up -d` na demonie Dockera serwera. Konfiguracja
|
||||
reverse proxy + SSL — patrz [DEPLOY.md](DEPLOY.md).
|
||||
reverse proxy + SSL - patrz [DEPLOY.md](DEPLOY.md).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "vsd-api",
|
||||
"version": "6.5.6",
|
||||
"description": "Video & Sound Downloader Pro — API (NestJS)",
|
||||
"description": "Video & Sound Downloader Pro - API (NestJS)",
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
|
||||
@@ -30,7 +30,7 @@ export class JobsController {
|
||||
|
||||
@Sse(':id/events')
|
||||
events(@Param('id') id: string): Observable<MessageEvent> {
|
||||
// Bez pola `type` — inaczej Nest emituje nazwane zdarzenia (event: <kind>),
|
||||
// Bez pola `type` - inaczej Nest emituje nazwane zdarzenia (event: <kind>),
|
||||
// a przeglądarkowy EventSource.onmessage reaguje tylko na domyślne „message”.
|
||||
return this.jobs.events(id).pipe(
|
||||
map((ev) => ({ data: JSON.stringify(ev) }) as MessageEvent),
|
||||
|
||||
@@ -129,7 +129,7 @@ export class JobsService {
|
||||
if (entries.length === 1) {
|
||||
const p = path.join(job.workDir, entries[0]);
|
||||
const st = await fs.stat(p);
|
||||
this.emit(job, { kind: 'log', line: `OK: gotowy plik — ${entries[0]} (${fmtSize(st.size)})` });
|
||||
this.emit(job, { kind: 'log', line: `OK: gotowy plik - ${entries[0]} (${fmtSize(st.size)})` });
|
||||
return { path: p, name: entries[0], size: st.size };
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ export class JobsService {
|
||||
void archive.finalize();
|
||||
});
|
||||
const st = await fs.stat(zipPath);
|
||||
this.emit(job, { kind: 'log', line: `OK: gotowe archiwum — ${zipName} (${fmtSize(st.size)})` });
|
||||
this.emit(job, { kind: 'log', line: `OK: gotowe archiwum - ${zipName} (${fmtSize(st.size)})` });
|
||||
return { path: zipPath, name: zipName, size: st.size };
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ server {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# API + SSE (strumień logów) — bez buforowania, długie timeouty na pobieranie
|
||||
# API + SSE (strumień logów) - bez buforowania, długie timeouty na pobieranie
|
||||
location /api/ {
|
||||
proxy_pass http://api:3000;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
@@ -151,7 +151,7 @@ export default function App() {
|
||||
}
|
||||
setBusy(true);
|
||||
setProgress(0);
|
||||
setLog([`Start: ${def.title} — ${url.trim()}`]);
|
||||
setLog([`Start: ${def.title} - ${url.trim()}`]);
|
||||
try {
|
||||
const res = await fetch('/api/jobs', {
|
||||
method: 'POST',
|
||||
@@ -303,7 +303,7 @@ export default function App() {
|
||||
</div>
|
||||
</div>
|
||||
<p className="hint">
|
||||
Napisy dotyczą tylko faktycznie dostępnych napisów — nie obejmują
|
||||
Napisy dotyczą tylko faktycznie dostępnych napisów - nie obejmują
|
||||
automatycznie generowanych napisów YouTube. Gotowy plik pobiera się przez
|
||||
przeglądarkę (folder „Pobrane”).
|
||||
</p>
|
||||
@@ -358,7 +358,7 @@ export default function App() {
|
||||
<button
|
||||
className="ghost"
|
||||
onClick={() =>
|
||||
alert('Video & Sound Downloader Pro — webowa wersja oparta o yt-dlp + FFmpeg.')
|
||||
alert('Video & Sound Downloader Pro - webowa wersja oparta o yt-dlp + FFmpeg.')
|
||||
}
|
||||
>
|
||||
O mnie
|
||||
|
||||
@@ -16,9 +16,9 @@ services:
|
||||
YTDLP_AUTO_UPDATE: ${YTDLP_AUTO_UPDATE:-1}
|
||||
YTDLP_UPDATE_INTERVAL_H: ${YTDLP_UPDATE_INTERVAL_H:-24}
|
||||
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
|
||||
# tylko loopback hosta — diagnostyka; publiczny ruch idzie przez web (nginx)
|
||||
# tylko loopback hosta - diagnostyka; publiczny ruch idzie przez web (nginx)
|
||||
ports:
|
||||
- "127.0.0.1:3105:3000"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user