From abedca5b8c8033ee14e504b3aec6c460534ecfed Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 4 Jul 2026 06:31:31 +0200 Subject: [PATCH] =?UTF-8?q?chore(release):=20v0.22.0=20=E2=80=94=20Downloa?= =?UTF-8?q?d=20Center=20worker/sidecar=20in=20all=20composes=20+=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prep the Download Center epic (Phase 1 + editor + share) for prod/self-host: - Dockerfile: create /downloads owned by appuser so a named-volume mount is writable (prod Linux). - docker-compose.{home,selfhost,yml}: add the 'worker' (yt-dlp/ffmpeg job loop) + 'bgutil-pot' (PO-token) services + a downloads mount (DOWNLOAD_ROOT, WORKER_ENABLED). Media defaults to a named volume; DOWNLOAD_HOST_PATH points it at a host dir (e.g. a Plex-readable folder). - README / docs/self-hosting.md / .env.example / install.{sh,ps1}: document the Download Center, the two extra containers, and DOWNLOAD_HOST_PATH. - VERSION 0.22.0 + releaseNotes entry. --- .env.example | 8 ++++++ Dockerfile | 8 +++++- README.md | 3 ++ VERSION | 2 +- docker-compose.selfhost.yml | 48 ++++++++++++++++++++++++++++++++ docker-compose.yml | 42 ++++++++++++++++++++++++++++ docs/self-hosting.md | 25 +++++++++++++++-- frontend/src/lib/releaseNotes.ts | 14 ++++++++++ install.ps1 | 4 +++ install.sh | 4 +++ 10 files changed, 154 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 18c3325..06725d1 100644 --- a/.env.example +++ b/.env.example @@ -54,3 +54,11 @@ SMTP_FROM= # more than one instance against the same database, keep SCHEDULER_ENABLED=true on exactly one # of them and false on the rest, to avoid double quota use and write races. SCHEDULER_ENABLED=true + +# --- Download center --- +# The Download Center adds a `worker` container (runs the yt-dlp/ffmpeg job loop) and a small +# `bgutil-pot` sidecar (mints YouTube tokens) — both come up automatically with docker compose. +# Downloaded media defaults to a Docker-managed named volume. Set DOWNLOAD_HOST_PATH to a host +# directory instead — e.g. one your Plex server can read — to keep the Plex-style tree there. +# The path must be writable by the container user (uid of `appuser`, 1000): chown 1000:1000 . +# DOWNLOAD_HOST_PATH=/mnt/media/youtube diff --git a/Dockerfile b/Dockerfile index 08f9839..5775471 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,7 +54,13 @@ COPY backend/ . COPY VERSION ./VERSION COPY --from=frontend /fe/dist ./app/static_spa -RUN adduser --disabled-password --gecos "" appuser && chown -R appuser /app +# Create the download-center mount point owned by the app user, so a fresh named volume mounted +# at /downloads inherits appuser ownership (Docker copies the image dir's ownership into a new +# empty volume) — the worker/API can then write there without a manual chown. A bind mount to a +# host path instead needs that path writable by this uid (see docs/self-hosting.md). +RUN adduser --disabled-password --gecos "" appuser \ + && mkdir -p /downloads \ + && chown -R appuser /app /downloads USER appuser EXPOSE 8000 diff --git a/README.md b/README.md index c9df4e6..cd72694 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,9 @@ blocker and SponsorBlock keep working. tags to slice the feed by. - **Playlists with two-way YouTube sync** — build them locally, keep them in sync in both directions. - **In-app player** with resume, plus keyboard/scroll controls. +- **Download Center** — save videos to the server with yt-dlp in a Plex-friendly layout (format + presets, per-user storage quota), trim / crop / split & join them in a built-in editor, then save + to your device, share with another user, or hand out a public watch link. - **Multi-user** with per-user private state, a shared catalog, and a fair daily API-quota guard. - **Self-hosted & private**, with a first-run web setup wizard and the interface in **English, Hungarian and German**. diff --git a/VERSION b/VERSION index 8854156..2157409 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.21.0 +0.22.0 diff --git a/docker-compose.selfhost.yml b/docker-compose.selfhost.yml index 758e6fc..fe96969 100644 --- a/docker-compose.selfhost.yml +++ b/docker-compose.selfhost.yml @@ -37,6 +37,10 @@ services: DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-siftlode}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-siftlode} # This instance owns the background scheduler (single writer). SCHEDULER_ENABLED: "true" + # Download center: the API serves finished files + builds filmstrips; the worker (below) runs + # the yt-dlp job loop. Read-only root is fine — it only writes to the /downloads mount + tmpfs. + DOWNLOAD_ROOT: /downloads + WORKER_ENABLED: "false" depends_on: db: condition: service_healthy @@ -45,6 +49,10 @@ services: # Reachable on the host's LAN at http://:8080. Put a reverse proxy (Caddy/Nginx) in # front for HTTPS / public exposure — and set OAUTH_REDIRECT_URL to the https URL. - "${HTTP_PORT:-8080}:8000" + volumes: + # Downloaded media. Defaults to a Docker-managed named volume; set DOWNLOAD_HOST_PATH in .env + # to a host directory (e.g. one your Plex server reads) to keep the Plex-style tree there. + - ${DOWNLOAD_HOST_PATH:-siftlode_downloads}:/downloads security_opt: - no-new-privileges:true cap_drop: @@ -60,8 +68,48 @@ services: start_period: 30s restart: unless-stopped + # Download worker: same image, runs the yt-dlp / ffmpeg job loop instead of the API. Shares the + # DB (job queue) and the downloads mount with the API. Not read-only (yt-dlp/deno/ffmpeg write to + # $HOME caches + the staging dir). + worker: + image: forge.b1fr0st.eu/peter/siftlode:${IMAGE_TAG:-latest} + command: ["python", "-m", "app.worker"] + env_file: + - .env + environment: + DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-siftlode}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-siftlode} + SCHEDULER_ENABLED: "false" + DOWNLOAD_ROOT: /downloads + WORKER_ENABLED: "true" + depends_on: + db: + condition: service_healthy + bgutil-pot: + condition: service_started + networks: [internal] + volumes: + - ${DOWNLOAD_HOST_PATH:-siftlode_downloads}:/downloads + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + tmpfs: + - /tmp + restart: unless-stopped + + # PO-token provider sidecar: mints YouTube Proof-of-Origin tokens so the worker beats bot-detection + # and unlocks high-quality formats (reached at http://bgutil-pot:4416, the config default). + bgutil-pot: + image: brainicism/bgutil-ytdlp-pot-provider:1.3.1 + init: true + networks: [internal] + security_opt: + - no-new-privileges:true + restart: unless-stopped + volumes: siftlode_pgdata: + siftlode_downloads: networks: internal: diff --git a/docker-compose.yml b/docker-compose.yml index 70ad302..1c26e30 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,11 +25,19 @@ services: env_file: .env environment: DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-siftlode}:${POSTGRES_PASSWORD:-siftlode}@db:5432/${POSTGRES_DB:-siftlode} + # Download center: the API serves finished files + builds filmstrips; the worker (below) runs + # the yt-dlp job loop. + DOWNLOAD_ROOT: /downloads + WORKER_ENABLED: "false" depends_on: db: condition: service_healthy ports: - "${APP_PORT:-8080}:8000" + volumes: + # Downloaded media. Defaults to a Docker-managed named volume; set DOWNLOAD_HOST_PATH in .env + # to a host directory (e.g. one your Plex server reads) to keep the Plex-style tree there. + - ${DOWNLOAD_HOST_PATH:-downloads}:/downloads healthcheck: test: ["CMD", "python", "-c", "import urllib.request; exit(0 if urllib.request.urlopen('http://localhost:8000/healthz').status == 200 else 1)"] interval: 15s @@ -38,5 +46,39 @@ services: start_period: 25s restart: unless-stopped + # Download worker: same image, runs the yt-dlp / ffmpeg job loop instead of the API. Shares the + # DB (job queue) + the downloads mount with the API. + worker: + build: + context: . + dockerfile: Dockerfile + args: + APP_VERSION: ${APP_VERSION:-dev} + GIT_SHA: ${GIT_SHA:-unknown} + BUILD_DATE: ${BUILD_DATE:-} + command: ["python", "-m", "app.worker"] + env_file: .env + environment: + DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-siftlode}:${POSTGRES_PASSWORD:-siftlode}@db:5432/${POSTGRES_DB:-siftlode} + SCHEDULER_ENABLED: "false" + DOWNLOAD_ROOT: /downloads + WORKER_ENABLED: "true" + depends_on: + db: + condition: service_healthy + bgutil-pot: + condition: service_started + volumes: + - ${DOWNLOAD_HOST_PATH:-downloads}:/downloads + restart: unless-stopped + + # PO-token provider sidecar: mints YouTube Proof-of-Origin tokens so the worker beats bot-detection + # and unlocks high-quality formats (reached at http://bgutil-pot:4416, the config default). + bgutil-pot: + image: brainicism/bgutil-ytdlp-pot-provider:1.3.1 + init: true + restart: unless-stopped + volumes: pgdata: + downloads: diff --git a/docs/self-hosting.md b/docs/self-hosting.md index bfb4cb0..94d68b6 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -8,7 +8,9 @@ configured in a web wizard on first start. There's no editing of config files by - A machine with **Docker** and the **Docker Compose plugin** (Docker Desktop on Windows/macOS, or Docker Engine on Linux). -- A few hundred MB of disk and ~1 GB RAM free. +- A few hundred MB of disk and ~1 GB RAM free for the app itself. The optional Download Center + stores media too — budget disk for whatever you download (it's bounded by per-user quotas you + set as admin). - Optional: a domain name + reverse proxy if you want HTTPS / public access (see below). ## 1. Get the files @@ -77,6 +79,24 @@ public access, put a reverse proxy (Caddy, Nginx, Traefik…) in front to termin secure). If you've already run the installer, edit `OAUTH_REDIRECT_URL` in `.env` to the https callback URL and `docker compose -f docker-compose.selfhost.yml up -d`. +## Download Center (media storage) + +The stack includes a **Download Center**: an admin-enabled feature that saves videos to the server +with yt-dlp (Plex-friendly folders + `.nfo`/poster), lets users trim/crop/join clips, and shares +them. It runs two extra containers that come up automatically — a `worker` (the download/edit job +loop) and a small `bgutil-pot` sidecar (mints YouTube tokens so downloads aren't bot-blocked). No +configuration is required; per-user storage quotas are set on the admin **Downloads → System** page. + +By default the media lives in a Docker-managed volume (`siftlode_downloads`). To keep it somewhere +you can reach from other apps — e.g. a folder your **Plex** server indexes — point it at a host +directory by adding this to `.env` and re-running `up -d`: + +```bash +DOWNLOAD_HOST_PATH=/mnt/media/youtube +``` + +The directory must be writable by the container user (uid `1000`): `sudo chown -R 1000:1000 `. + ## Day-to-day ```bash @@ -84,8 +104,9 @@ callback URL and `docker compose -f docker-compose.selfhost.yml up -d`. docker compose -f docker-compose.selfhost.yml pull docker compose -f docker-compose.selfhost.yml up -d -# Logs / status +# Logs / status (api = web, worker = downloads/edits) docker compose -f docker-compose.selfhost.yml logs -f api +docker compose -f docker-compose.selfhost.yml logs -f worker docker compose -f docker-compose.selfhost.yml ps # Stop diff --git a/frontend/src/lib/releaseNotes.ts b/frontend/src/lib/releaseNotes.ts index 5d5d5eb..abf8efa 100644 --- a/frontend/src/lib/releaseNotes.ts +++ b/frontend/src/lib/releaseNotes.ts @@ -14,6 +14,20 @@ export interface ReleaseEntry { } export const RELEASE_NOTES: ReleaseEntry[] = [ + { + version: "0.22.0", + date: "2026-07-04", + summary: "The Download Center — save videos to the server, edit them into clips, and share them.", + features: [ + "Download Center: save any video (or a search result) to the server with yt-dlp, laid out Plex-style with a poster + info file. Choose a built-in format preset or make your own, keep an eye on your per-user storage quota, and save the finished file to your device.", + "Built-in video editor: trim, crop, and cut a download into segments — keep the parts you want as separate clips or join them into one file. Pick a precise (frame-accurate) or fast cut, with a scrub timeline and a hover preview.", + "Sharing: share a download with another user (it appears in their “Shared with me”, where they can edit their own copy or remove it), or hand out a public watch link that plays on a clean, login-free page — with an optional password, an expiry, and a stream-only vs downloadable toggle.", + "Tidier video titles across the feed, search and downloads — clickbait ALL-CAPS and trailing hashtag clutter are cleaned up for display (the original title is kept underneath).", + ], + fixes: [ + "The browser tab now carries an app icon and shows the section you're on; clicking the Siftlode logo returns you to the feed.", + ], + }, { version: "0.21.0", date: "2026-07-02", diff --git a/install.ps1 b/install.ps1 index bd2f2e9..c3069de 100644 --- a/install.ps1 +++ b/install.ps1 @@ -28,6 +28,10 @@ POSTGRES_PASSWORD=$pgPass SECRET_KEY=$secretKey TOKEN_ENCRYPTION_KEY=$fernet OAUTH_REDIRECT_URL=$url/auth/callback + +# Optional: store Download Center media in a host folder (e.g. one your Plex server reads) instead +# of a Docker volume. Must be writable by uid 1000 (chown -R 1000:1000 ). +# DOWNLOAD_HOST_PATH=/mnt/media/youtube "@ | Set-Content -Path .env -Encoding ascii Write-Host "Wrote .env (secrets generated)." } diff --git a/install.sh b/install.sh index f9e3773..c6ad65b 100755 --- a/install.sh +++ b/install.sh @@ -26,6 +26,10 @@ POSTGRES_PASSWORD=$POSTGRES_PASSWORD SECRET_KEY=$SECRET_KEY TOKEN_ENCRYPTION_KEY=$TOKEN_ENCRYPTION_KEY OAUTH_REDIRECT_URL=$URL/auth/callback + +# Optional: store Download Center media in a host folder (e.g. one your Plex server reads) instead +# of a Docker volume. Must be writable by uid 1000 (sudo chown -R 1000:1000 ). +# DOWNLOAD_HOST_PATH=/mnt/media/youtube EOF chmod 600 .env echo "Wrote .env (secrets generated)."