Commit Graph

43 Commits

Author SHA1 Message Date
c11e1fdaf7 Complete rate-limiter coverage for all background yt-dlp calls
- fetch_playlist_videos now uses _meta_run (background playlist indexing)
- fetch_channel_links now uses _meta_run (dead code, future-proofed)
- _upsert_video_from_yt accepts polite= flag; background enrichment passes polite=True
- Only intentional user-facing one-shot calls (download_subs_only,
  fetch_video_comments, polite=False metadata) bypass the lock

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 01:09:14 +02:00
19dae63385 Route all discovery fetches through global rate limiter
- search_youtube, fetch_trending, fetch_featured_channels now use _meta_run
- Replaced ThreadPoolExecutor(4) parallel searches with sequential loop
- Replaced ThreadPoolExecutor(3) parallel featured-channel fetches with sequential
- _fetch_and_index_channel passes polite=True to fetch_channel/video_metadata

Discovery was firing 4+ simultaneous yt-dlp processes, each with cookies,
which is what invalidated the session.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 01:05:56 +02:00
c26fc3483c Rate limit only background batch fetches, not user requests
fetch_video_metadata and fetch_channel_metadata now take polite=True for
background tasks (enforces 5s+ gap via global lock) while user-facing
calls (watch page, follow channel, download) use polite=False and run
immediately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:34:59 +02:00
c180c293b0 Add global yt-dlp metadata rate limiter (5s + jitter between calls)
All fetch_video_metadata / fetch_channel_metadata / fetch_channel_playlists
/ fetch_available_subs calls now go through _meta_run which enforces a
minimum 5s gap (+ 0.5-2.5s random jitter) across all concurrent tasks.
Per-task sleep loops removed since the global lock serializes everything.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:31:14 +02:00
6cfaca382c Fix playlist thumbnails — extract from yt-dlp thumbnails array
_stable_thumbnail expects a video ID but was being passed a playlist ID
(PLxxx), producing a broken URL. Now picks the best thumbnail from
yt-dlp's thumbnails array, falling back to the singular thumbnail field.

Also backfills playlist.thumbnail_url from the first video when indexing
a playlist that still has no thumbnail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:42:30 +02:00
2f37072187 Fix popular fetch and improve date/view_count coverage
Popular fetch now does a two-phase approach: fast flat-playlist to get
IDs in popularity order, then parallel full metadata fetch (8 workers)
to get real view_count and published_at for each video. Previously
flat-playlist mode returned timestamp/view_count as null.

Enrich task now also backfills published_at and view_count (not just
description). Startup limit 3→50, enrichment sleep 2s→0.5s.

Raise all thread pool sizes to match 8-core machine:
- Discovery search: 5→8 workers
- Graph signal: 4→8 workers
- Popular fetch: 5→8 workers
- Download semaphore default 3→6, cap 10→16

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:36:18 +02:00
5b0cf27f07 Add playlists support and fix explore older videos
- New playlists router: fetch channel playlists from YouTube, index
  playlist videos, browse by playlist with pagination
- Playlist model gets video_ids column to store ordered video list
- Register playlists router in main.py with DB migration
- Add Playlists tab to Channel page: grid of playlist cards, click to
  browse videos, index/re-index per playlist
- Fix explore older videos skipping all entries without published_at;
  flat-playlist entries for older videos rarely include timestamp data

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:28:35 +02:00
aa91156bbc Add older content exploration: channel page + home feed Rediscover mode
Channel page:
- "Explore older videos" button fetches 100 videos at a time further back
  in the channel history using yt-dlp --playlist-start/--playlist-end
- "Fetch entire history" still available for full crawl
- Backend: /channels/{id}/explore?page=N endpoint + playlist offset support
  in fetch_channel_metadata(start_video=N)

Home feed:
- New "Rediscover" mode: older unwatched videos (90+ days old) from
  followed channels, randomly sampled then re-ranked by tag affinity

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:17:20 +02:00
871f668525 Parallelize discovery searches and add graph signal
Run search queries concurrently (5 workers) instead of sequentially —
cuts crawl time dramatically. Add graph signal: fetch featured channels
from followed channels' /channels tab in parallel (4 workers), which
surfaces creator-curated recommendations as a high-signal, diverse pool
that search alone can't reach.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:59:23 +02:00
da765ce76e Fix subtitle positioning and show existing langs without re-downloading
- Strip yt-dlp's align:start position:0% cue settings from VTT files
  after both video download and subtitle-only download so CSS ::cue centers them
- CC chip now shows already-downloaded langs (e.g. 'CC: en') directly
  from disk with a '+' button to add more — no YouTube call needed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:22:33 +02:00
bbf8365c70 Add subtitle-only download for already-downloaded videos
- download_subs_only(): yt-dlp --skip-download to fetch just .vtt sidecar
- POST /by-yt/{ytId}/download-subs endpoint
- CC chip now visible on downloaded videos; clicking checks YouTube,
  shows lang picker with "Add subtitles" button separate from re-download

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:16:30 +02:00
27f17c16ef Fix subtitle playback: vtt format, track elements, fast disk scan
- Convert subs to .vtt (was .srt which browsers don't support in <track>)
- Add GET /subtitle-files endpoint: instant disk scan for .vtt sidecar files,
  no yt-dlp call needed
- Inject <track> elements into the video player for each .vtt on disk;
  browser CC button appears automatically
- Before download: CC chip triggers YouTube availability check (slow, on demand)
- After download with subs: shows "CC ✓" — subtitles live in the player controls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:11:58 +02:00
399c5fcada Add per-video subtitle language picker on Watch page
- fetch_available_subs() queries yt-dlp for manual + auto-generated
  subtitle langs available on YouTube for any given video
- GET /api/videos/by-yt/{ytId}/subs exposes this to the frontend
- DownloadRequest now accepts subtitle_langs to override the global
  setting on a per-download basis
- Watch page fetches available subtitle langs on load (in parallel),
  shows a CC dropdown with manual langs + auto-generated langs labeled
  "(auto)"; selected lang is passed through to the download

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 20:57:57 +02:00
ea99b74ba8 Add scheduled sync, disk space awareness, and subtitle downloads
- auto-sync daemon: background thread checks every hour and syncs followed
  channels for users with sync_interval_hours set (6/12/24h options)
- disk stats: /api/stats now returns total/used/free/download bytes;
  Stats page shows a disk usage bar
- subtitles: subtitle_langs setting (e.g. "en,sv") passed through all
  download paths; yt-dlp writes .srt files alongside the video
- Settings page: sync interval dropdown + subtitle languages input

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 20:36:50 +02:00
3abbd5749e Remove embed-metadata and embed-thumbnail to speed up post-merge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 20:26:47 +02:00
2bb5f35db0 Restore embed-metadata/thumbnail for Jellyfin, drop wasteful faststart pass
The Merger+ffmpeg faststart postprocessor arg was overwritten by the
subsequent embed-metadata and embed-thumbnail passes anyway, making it
a pointless extra ffmpeg remux. Dropped it and restored the embeds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 20:23:19 +02:00
a15123028c Speed up post-download merge by dropping embed-thumbnail and embed-metadata
Both flags trigger extra ffmpeg passes over the entire file after the
stream merge. They're unnecessary — metadata lives in the DB and
thumbnails come from YouTube. Removing them cuts the post-join wait
to just the faststart rewrite.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 20:20:42 +02:00
b41412071a Fix quality: drop unsupported tv_embedded player client override
yt-dlp 2026.03.17 dropped support for tv_embedded — it silently skips it
and falls back to web-only, which only exposes the pre-merged 360p format
(ID 18). The override was added to avoid SABR restrictions but is now the
cause of the low-quality downloads.

Removing --extractor-args restores yt-dlp's default client selection
(android_vr + web fallback) which exposes all formats up to 2160p.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 20:14:23 +02:00
e9140ab6a1 Fix quality format fallbacks and resolution detection above 1080p
The per-quality format strings fell back to best[height<=NNN] which on
YouTube resolves to pre-merged streams capped at ~360p, causing every
quality selector choice to silently download low-res video. Replace with
bestvideo+bestaudio as the intermediate fallback so adaptive streams are
always preferred over pre-merged ones.

Also fix detect_resolution to correctly label 1440p and 2160p files
instead of capping the display at 1080p.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 20:07:41 +02:00
Mattias Tall
8da361b087 Fix quality: use tv_embedded,web player client instead of web-only
YouTube's web client gets SABR format restrictions in 2025-2026 yt-dlp,
limiting available streams and causing fallback to 360p. tv_embedded
bypasses SABR and exposes the full format list including 4K.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 17:48:10 +02:00
Mattias Tall
c24964a1ee Fix quality formats: drop AVC1/MP4 codec restrictions that caused 360p fallback
Most modern YouTube videos use VP9/AV1, so the old bestvideo[ext=mp4][vcodec^=avc1]
filter always failed and fell through to format codes 22/18 (720p/360p).
--merge-output-format mp4 handles the container; no need to restrict codec.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 17:39:55 +02:00
Mattias Tall
83e1b18c5b Fix max_comments format: use full 4-tuple for yt-dlp YouTube extractor
max_comments takes thread_count,total,replies_per_thread,reply_pages.
Passing just one value left the rest unset which caused yt-dlp to fetch
only 1 comment. Now passes 20,20,0,0 to fetch 20 top-level comments
with no replies. Also switch --no-download to --skip-download.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:45:15 +02:00
Mattias Tall
50ce373767 Fix comment fetching: write to temp file instead of parsing stdout
--write-comments writes to .info.json reliably; parsing stdout with
--dump-json was never guaranteed to include comments. Use a TemporaryDirectory,
write the info.json there, read it, then let the context manager clean up.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:38:00 +02:00
Mattias Tall
74e2b4cd73 Fix comments (Python API), add dislike bar
Comments: switch from CLI --write-comments to yt-dlp Python API with
getcomments=True — more reliable, proper extractor_args dict format

Dislikes: add dislike_count column, fetch from returnyoutubedislike.com
after each video metadata upsert (5s timeout, non-fatal)

UI: replace emoji like count with a like/dislike ratio bar — blue fill
showing like proportion, labels on each end; views stay in meta row

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:33:00 +02:00
Mattias Tall
c9e64d2814 Fix comment extractor args separator (, → ;) and use comment_sort=top
yt-dlp separates extractor args with ; not ,. The malformed arg was
causing max_comments to parse as a garbage string, fetching ~1 comment.
Also swap max_comment_depth (not a real YouTube extractor arg) for
comment_sort=top to get highest-engagement comments first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:27:26 +02:00
Mattias Tall
3f225e7647 Add like count to videos
Same pattern as view_count: model column, yt-dlp extraction, SQL select,
VideoDetail field, startup migration, and display in Watch meta row.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:25:44 +02:00
Mattias Tall
8221177615 Add view count to videos
- Video model: view_count column (Integer, nullable)
- ytdlp._normalize_video: extract view_count from yt-dlp info
- _VIDEO_SELECT: include v.view_count in all queries
- VideoDetail schema: view_count field
- Watch page: formatViews() helper, show "X.XM views" in meta row
  alongside date and category

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:18:53 +02:00
Mattias Tall
cdf6520fd8 Add lazy comment fetching to watch page
- VideoComment model (video_id, author, text, likes, is_pinned, published_at)
- fetch_video_comments() in ytdlp.py: top 20 comments, no reply threads,
  sorted pinned-first then by likes
- GET /videos/by-yt/{id}/comments — returns cached comments instantly
- POST /videos/by-yt/{id}/comments/refresh — fetches from YouTube, stores, returns
- Watch page: CommentsSection shows "Load comments" button when uncached,
  renders comments with author/likes once loaded; Refresh link to re-fetch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:15:41 +02:00
Mattias Tall
426e85c2c9 Mobile nav, channel banners, search channel linking
- Add bottom tab bar (Home/Following/Discover/Downloads/Settings) for mobile
- Fetch and display channel banner images on channel pages
- Fix ChannelCard: channels without a local DB id now follow+navigate on click

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 10:55:25 +02:00
Mattias Tall
050caead54 Install Node.js via nodeenv (pip); use web client
nodeenv is a Python package that downloads a pre-built Node.js binary —
no apt repos, no compilation, guaranteed to work in python:3.12-slim.
The 'node' binary is linked into /usr/local/bin so yt-dlp can find it.

With Node.js available the web client works fully (37 formats) and can
solve YouTube's n-challenge that every other approach was failing on.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 10:16:10 +02:00
Mattias Tall
53ea64ee8a Switch to web_embedded player client
web_embedded: supports cookies, no Node.js/JS runtime needed, 23 video
formats available. android_vr was skipped by yt-dlp when cookies are
present since that client doesn't support cookie auth.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 10:12:32 +02:00
Mattias Tall
b58dc26bd4 Switch to android_vr player client — no Node.js required
android_vr provides pre-signed format URLs that bypass YouTube's
n-challenge and signature JS requirements entirely. Tested: 23 video
formats available without any JavaScript runtime installed.

Reverts Node.js Dockerfile addition (which failed to build anyway).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 10:10:58 +02:00
Mattias Tall
299338ff80 Fix double _cookie_args() call in download; fix diagnostic player client
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 10:01:24 +02:00
Mattias Tall
3e439f2d3a Add Node.js to Docker image; use web player client only
Node.js is required by yt-dlp to solve YouTube's n-challenge (format URL
deobfuscation). Without it the web client returns no video formats.

The tv and ios player clients were removed — both require GVS PO tokens
that we don't have, so they only produce warnings and block every request.
The web client with Node.js installed gives 30+ formats and works cleanly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 09:57:02 +02:00
Mattias Tall
98d986cd95 Fix cookie fallback breaking yt-dlp in Docker; add OAuth2 auth flow
- _cookie_args() no longer falls through to --cookies-from-browser when
  cookies_file is configured but missing. Firefox isn't installed in the
  Docker image, so that fallback caused yt-dlp to exit with empty stdout
  and every metadata fetch to return "Video not found on YouTube".
- fetch_video_metadata() now retries without auth args if the first call
  fails, so a broken cookie config can't block public video fetches.
- Add use_oauth2 setting + full device-auth flow (POST /settings/oauth2-init,
  GET /settings/oauth2-status) with OAuth2Section UI in Settings page.
- Add GET /settings/ytdlp-test diagnostics endpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 09:53:02 +02:00
inputnoise
32af6c1c49 Try tv player client first to bypass datacenter IP bot detection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:45:05 +02:00
inputnoise
4ab8245a93 Debug: log fetch_video_metadata cookie args and errors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:42:02 +02:00
inputnoise
a09f8ac5c2 Use print() for cookie debug log so it shows in container logs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:25:39 +02:00
inputnoise
d50ccf399f Log cookie args on download start for debugging 2026-05-25 21:19:26 +02:00
inputnoise
a2a84d2c04 Use iOS player client to bypass YouTube bot detection (fixes 'only images available') 2026-05-25 21:04:24 +02:00
inputnoise
56dd5f8360 Add cookies file support for Docker; auto-detect /data/cookies.txt 2026-05-25 20:57:04 +02:00
inputnoise
bcc425b6fb Fix volume permissions: entrypoint chowns /data to uid 1000, run app as non-root 2026-05-25 20:50:10 +02:00
inputnoise
1827dd6c4e Initial commit — YT Hub
Self-hosted personal YouTube management app.
FastAPI + SQLite backend, React + Vite + Tailwind frontend.
Dockerfiles and compose included for Portainer deployment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 20:09:04 +02:00