Commit Graph

146 Commits

Author SHA1 Message Date
0c5b236b77 fix: close remaining concurrent yt-dlp session sources
Three more code paths were bypassing the _meta_lock guard and firing
raw yt-dlp processes concurrently with active downloads:

- Popular fetch Phase 1 (flat-playlist channel crawl): changed from
  ytdlp._run to ytdlp._meta_run so it waits for active downloads
- download_subs_only: changed from _run to _meta_run
- fetch_video_comments: returns empty list immediately if a download
  is active (avoids blocking a 90s call indefinitely)
- Diagnostic test endpoint (settings): switched to _meta_run

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 03:12:12 +02:00
c223e57463 fix: prevent concurrent yt-dlp sessions that invalidate cookies
Three code paths could fire yt-dlp immediately (polite=False) while a
download was already running, causing YouTube to see two simultaneous
authenticated sessions and invalidate the cookie:

- search.py: live yt-dlp fallback now skipped while any download is active
- downloads.py: _ensure_video uses polite=True so it waits for active
  downloads to finish before fetching metadata for an unknown video
- channels.py: follow_by_url uses polite=True when fetching metadata
  for a brand-new channel

Added is_download_active() helper to ytdlp.py to expose the active
download state without importing private globals.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 03:07:19 +02:00
a0384b2277 Schedule auto-discovery at 4 AM daily instead of every 23 hours
Replaced the rolling 23-hour check with a fixed-time scheduler that sleeps
until the next 4:00 AM, runs discovery for all users, then sleeps until the
following 4 AM. No longer reads last_discovery_run — just runs at the same
time every day.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 02:59:41 +02:00
bcbd552eab Show discovery progress in the top-bar indicator alongside downloads/tasks
The DownloadIndicator in Layout.jsx now queries discovery-status and shows
discovery progress the same way it shows active downloads and channel tasks:
- Spinning icon appears in the navbar when discovery is running
- Hover popover shows "Discovering channels" with X/Y count and a progress bar
- Polls every 10 s while running, 60 s when idle
- Primary label priority: download % > task phase > discovery X/Y

Discovery page header simplified: progress bar and verbose status removed
since the top-bar indicator now handles it. "Find more" button still
disables while running.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 02:58:13 +02:00
1179b53f2e Fix hidden yt-dlp calls on video page causing cookie invalidation
Two background yt-dlp processes were firing every time a video page loaded:

1. importChapters (called unconditionally via useEffect on mount) was calling
   _upsert_video_from_yt with polite=False when chapters=NULL — no rate
   limiter, no download-pause check, runs concurrently with active downloads.
   Fix: return [] immediately when chapters=NULL and let the normal enrichment
   pipeline (already polite=True) fill them in.

2. get_video_by_yt_id schedules a background _enrich task whenever description
   or chapters are NULL. The frontend polls every 3 s while description is
   null, so dozens of enrichment tasks would pile up for the same video.
   Fix: deduplicate with _enriching set — only one background fetch per
   video_id at a time; the set entry is cleared when the task finishes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 02:43:45 +02:00
146a044e69 Fix For You feed: replace broken jitter with proper tier-based sampling
The old approach added ±12 noise to scores that span -365..+100 (recency
uses raw Julian days), so the perturbation could never reorder videos that
differed by more than 24 points — which is almost all of them. Every
reshuffle returned the same ranking.

Fixes:
- Per-channel candidate window: rn <= 15 (was rn <= 5) for a much wider pool
- Candidate pool: up to 600 per page (was limit * 4 = 100)
- Non-overlapping page offsets: page N pulls SQL rows N*600 .. (N+1)*600 so
  pagination actually moves through new material instead of re-reading the same top-100
- Replaced ±12 perturbation with proper tier-based random sampling:
  top 40% → 60% of page, mid 40% → 30%, bottom 20% → 10% wildcards
  Each reshuffle picks a genuinely different mix from the score-ranked pool

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 02:34:05 +02:00
a535e9f22a Add queue-based gradual discovery with shuffled call ordering and progress UI
Each yt-dlp call is now an independent task (one search query, one trending
fetch, one graph channel fetch). Tasks are shuffled together so we don't fire
10 searches in a row, then enqueued with 30-90s random gaps between them —
a full sweep of ~17 tasks completes in roughly 10-25 minutes instead of
hammering YouTube with 21 calls back-to-back.

Fast signals (community, category clusters) still run synchronously at
schedule time since they're pure SQL.

Progress is tracked per-user (total/done/running) and exposed on
GET /api/discovery/status. The Discovery page polls every 10s while
running and shows a progress bar + "Finding channels… X / Y" in the header.
The auto-discovery daemon skips scheduling if a manual sweep is already running.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 02:28:35 +02:00
e6faf8e08e Drastically reduce discovery yt-dlp call count: 64 → ~21
Each yt-dlp call is a separate subprocess that opens a new HTTP session with
YouTube. 64 sessions in a row looks like a bot regardless of rate limiting.

Changes:
- crawl_by_search: 30 queries → 10 (top 5 tags, 4 channel names, 1 serendipity)
- update_liked_signal: 10 queries → 4
- update_watch_signal: removed (tags already included in crawl_by_search)
- update_trending_signal: 2 regions → 1 (first region only)
- update_graph_signal: 12 sampled channels → 6

New total: ~21 yt-dlp calls per run (~105s with 5s gaps) vs ~320s before.
Signal quality is preserved — the removed queries were low-marginal-value
duplicates of content already covered by the remaining ones.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 02:14:25 +02:00
0a4dfb845e Pause discovery metadata calls while a download is active
_meta_run now checks _active_downloads before each background yt-dlp call.
If a download is running it waits (3s poll loop) until the download finishes
before making the next metadata request.

This prevents YouTube from seeing the same session used simultaneously by
a download and a discovery/metadata call, which was causing cookie invalidation
even with private cookie copies.

Downloads still run immediately without waiting for metadata. Background
discovery is the one that yields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 02:12:43 +02:00
c3191aa000 Fix SyntaxError in ytdlp.py start_download: duplicate try block
Two back-to-back try: blocks with only one finally: caused
"expected 'except' or 'finally' block" at startup. Merged into one.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 02:04:11 +02:00
395b987644 Add last_discovery_run to UserSettings model
Column was added via SQL migration but missing from the SQLAlchemy model
definition, causing AttributeError when the discovery status endpoint
accesses s.last_discovery_run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 02:03:39 +02:00
12f54ac5b0 Auto-schedule daily discovery + fix Find More UX + expand query diversity
Auto-discovery daemon:
- Runs every hour, triggers full discovery for any user whose last run
  was >23 hours ago. First check is 5 minutes after startup.
- Tracks run time in user_settings.last_discovery_run (new column).
- Manual Find More also stamps last_discovery_run.

Discovery status endpoint (GET /api/discovery/status):
- Returns pending_count (unseen queue size) and last_run timestamp.
- Shown in the Discover page header so users know queue state at a glance.

Find More UX fix:
- Was: kick background task, wait 8 seconds, refetch (task takes minutes).
- Now: button shows "Queued ✓" on success with an explanatory banner
  telling the user it takes a few minutes and also runs daily automatically.

Query diversity:
- Added "best [category] channels" serendipity queries to crawl_by_search.
- Limit raised from 25 to 30 queries per run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 01:58:39 +02:00
4d255647a1 Fix cookie invalidation: give each yt-dlp process a private cookie file copy
Downloads run for minutes via Popen while metadata calls continue in parallel.
Both processes read from AND write back to the same --cookies file, causing
concurrent writes that corrupt the session cookie state.

Fix: _make_private_cookie_copy() intercepts --cookies <file> in any arg list
and swaps it for a NamedTemporaryFile copy. Each yt-dlp process gets its own
snapshot; write-backs go to the throwaway copy and are discarded on cleanup.

- _run() uses this for all subprocess.run calls (metadata, subtitles, comments)
- start_download() uses it for the long-lived Popen download process
- _meta_run() benefits automatically since it calls _run()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 01:48:29 +02:00
592194f2ca Fix discovery scoring: cap trending, prevent score inflation, add freshness
- Cap trending base_score at 18.0 (was unbounded — a viral channel could
  score 240+ vs search's 15, making everything else invisible)
- Cap all discovery scores at 50.0 globally so no single signal dominates
- Fix score accumulation: cap accumulated total at 50.0 (was unbounded
  across repeated runs, cementing high-score channels in top positions forever)
- Expire unseen queue entries older than 14 days at start of each run
- Add ±8 score perturbation to discovery list endpoint (was pure score DESC,
  identical every visit until dismissed)
- Add score perturbation to discovery_videos ORDER BY too
- Fix SQL injection in update_category_clusters (category strings were
  interpolated directly into query; now use parameterized queries per category)
- Raise category signal score from 3.0 → 5.0 to compensate for trending cap

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 01:37:09 +02:00
b6a47249d0 Fix search latency: bypass rate limiter for user-triggered searches
search_youtube now takes polite=False (default) for instant user
searches and polite=True for background discovery crawls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 01:27:36 +02:00
86e7648075 Fix _meta_run: hold lock for entire yt-dlp execution, not just scheduling
Previously the lock was released before _run(), so multiple threads could
fire yt-dlp processes simultaneously — completely defeating the rate limiter.
Now the lock is held through the subprocess call and released in finally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 01:21:36 +02:00
1b010d4081 Track video clicks as engagement signals
- stats: started_count now includes any video opened (last_watched_at set)
  not just ones with saved progress seconds
- VideoPlayer: fires updateProgress immediately on open so even a
  click-and-back sets last_watched_at and counts as a started video

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 01:19:47 +02:00
fadb0fffcd Add started_count to stats (videos in progress, not yet finished)
Tracks watch_progress_seconds > 0 AND watched = 0. Shown as
"In progress" card in the engagement row alongside finished/bailed/rewatched.
Total liked moved to engagement row, top row condensed to 3 cards.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 01:18:49 +02:00
9d35cc7c68 Increase recommendation density in For You feed to ~25%
1 discovery card per 3 followed videos (was 1 per 5).
Lower-ranked discovery cards also get shuffled so the same
channels don't always appear at fixed positions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 01:17:04 +02:00
bbf7cc939b Overhaul For You feed ranking and freshness
Ranking improvements:
- Wider candidate pool (4x limit) with ±12pt score perturbation so
  same-score videos shuffle differently each load
- Recent channel engagement signal: channels watched in past 30 days
  get a +4pts/watch boost
- Bail penalty: -25pts for videos started but abandoned before 20%
- Impression penalty: -3pts per prior feed appearance (capped at 10),
  so repeatedly-skipped videos sink naturally
- rn cap raised to 5 for more candidates; Python-side sampling picks top limit

Feed UX:
- Reshuffle button now available on For You (ranked) mode, not just Explore
- shuffleKey now always included in query key (not just random mode)
- Ranked mode staleTime reduced from 10min to 90s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 01:14:10 +02:00
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
4a7f1f06ac Fix discovery refresh: open fresh DB session in background task
The refresh endpoint was passing the request's db session to the
background task, which is closed before the task runs — silently
doing nothing on every refresh.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 01:01:18 +02:00
1ee6edcb17 Lower finished threshold from 90% to 75%
Applies to frontend player, backend safety net, and stats display.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:37:57 +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
15e6b94cbf Expand taste profile: show up to 60 tags with score bars
Top 10 shown as variable-size tag cloud, all tags below as a
two-column bar chart. Backend limit raised from 20 to 60.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:15:57 +02:00
32e55b9042 Hard-cap list view description snippet at 180 chars
line-clamp wasn't clamping reliably; truncate in JS instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:13:24 +02:00
77aebffa49 Set discovery card descriptions to 3 lines
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:10:50 +02:00
ba6dfe321d Trim discovery card descriptions to 1 line
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:08:48 +02:00
31cef555a9 Cap list view description to 3 lines
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:07:55 +02:00
001d2ddcf0 Add Jellyfin NFO sidecar generation for downloaded videos
Writes a Kodi/Jellyfin-compatible .nfo XML file next to each .mp4 on
download completion, deletes it when the download record is removed, and
exposes POST /api/downloads/nfo/generate to backfill NFOs for existing
downloads. Frontend adds a "Generate NFO" button in the Downloads header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:03:58 +02:00
65bc199366 Add browser notification support for new videos
- Settings: Notifications section with toggle that requests browser permission
  and stores preference in localStorage
- Layout: fires a Notification when new_count increases and user isn't on /following
- Works whenever the tab is open (foreground or background)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:56:22 +02:00
8029b2517f Slow down popular fetch and enrich to protect cookies
- Popular fetch phase 2: sequential with 2s delay between requests (was 3 parallel workers)
- Reduced from 200 to 100 videos per popular fetch run
- DB writes happen after each video instead of all at end (no data loss on interrupt)
- _enrich_missing_task: delay increased 0.5s → 2s between requests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:53:37 +02:00
ff601d3585 Add stats peak hours, RSS feed, channel health view, bulk video download
Stats:
- Peak watching hours chart (24-bar) from last_watched_at timestamps

RSS:
- GET /api/channels/rss — last 100 videos from followed channels as RSS 2.0
- RSS link in Following > Health tab

Channel health:
- New Health tab in Following groups channels into Active / Slow / Dormant / Dead
  based on days since last upload

Bulk video download:
- Select mode on Channel page (Videos tab) with checkboxes
- Sticky bottom bar shows count + Download button
- Queues a download for each selected video

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:50:55 +02:00
3652038cf5 Add offline indicator, surprise me button, and continue watching on channel
- Offline banner in nav when backend is unreachable (network error, not 4xx)
- GET /channels/{id}/random — picks random unwatched video, navigates to watch
- GET /channels/{id}/in-progress — videos with >30s progress, not yet watched
- Channel page: 'Surprise me' button (desktop + mobile) navigates to random video
- Channel page: 'Continue watching' row above video list when in-progress videos exist

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:46:58 +02:00
e02ea12494 Add hover popover to nav download indicator
Shows each active download (title + progress bar) and background task
(label, phase, done/total + bar) on hover. Pure CSS group-hover, no JS state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:33:26 +02:00
3a557a1d24 Show popular fetch phases in nav indicator and Downloads page
- Phase 1 (crawling) now creates the task immediately so Downloads shows it
- Phase label updates to 'Enriching view counts' when phase 2 starts
- Nav bar DownloadIndicator also polls /channels/tasks and shows spinning
  indicator + progress % for background tasks (not just file downloads)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:32:39 +02:00
be84660e2d Add popular fetch progress to Downloads page
- Track active background tasks in an in-memory dict with a lock
- Expose GET /api/channels/tasks returning running task list
- _fetch_popular_task updates done count as each video fetch completes
- Downloads page polls /tasks every 2s and shows progress bars

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:17:13 +02:00
c3290d33a7 Reduce parallel YouTube request workers to avoid cookie invalidation
8 simultaneous yt-dlp processes hitting video pages looks like a bot
attack and causes YouTube to nuke the session cookies. Drop to:
- Popular fetch view_count enrichment: 8→3 workers
- Discovery search: 8→4 workers
- Graph signal (featured channels): 8→3 workers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:11:07 +02:00
be7319e96c Sample videos randomly for view_count enrichment, not newest-first
Previously ORDER BY published_at DESC meant only the newest 200 videos
ever got view counts. Now ORDER BY RANDOM() spreads the 200 slots across
the full channel history — videos without a count are still prioritised,
but among those they're drawn randomly. Each run of Fetch Popular covers
a different slice, converging toward full coverage over time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:06:32 +02:00
6e455ed8ce Fetch popular: flat-playlist crawl then parallel view_count enrichment
Phase 1: crawl the full channel with flat-playlist to store any videos
not yet in DB (fast, no individual requests).
Phase 2: fetch real view_count for up to 200 channel videos in parallel
(8 workers), prioritising those missing a count.
Popular tab sorts all channel videos by view_count DESC NULLS LAST.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:05:21 +02:00
ff4d8e4ab4 Popular tab: rank by real view_count, drop broken ?sort=p URL
yt-dlp's own test suite marks channel sort as 'Query for sorting no
longer works' — YouTube blocked it. New approach: fetch view_count for
up to 200 indexed videos in parallel (8 workers, prioritising those
missing counts), then Popular tab sorts by view_count DESC WHERE
view_count IS NOT NULL. Accurate for any channel once enrichment runs.
Frontend refetch wait raised to 60s to cover ~200 parallel fetches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:02:03 +02:00
3e699d61b6 Fix popular task failing silently when table doesn't exist
The outer try had no except — any exception (e.g. table missing) killed
the whole background task with no error visible to the user. Now:
- CREATE TABLE IF NOT EXISTS inline so the task works even if the
  startup migration hasn't run (no server restart required)
- Wrap DELETE in its own try/except
- Catch and print outer exceptions so failures appear in server logs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:52:30 +02:00
c3b83ba1d3 Enrich playlist video dates after indexing
flat-playlist mode returns timestamp=null for most playlist entries so
published_at is missing after the initial index. Now kicks off
_enrich_missing_task (scoped to the playlist size) as a daemon thread
immediately after indexing commits, filling in dates and view counts
in the background via individual video fetches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:50:56 +02:00
77cba81ef4 Popular: write Phase 1 immediately, enrich view_count in background
Previously the task waited for all 30 parallel metadata fetches before
writing anything to the DB (~30s). Now Phase 1 (flat-playlist IDs +
basic info) commits to channel_popular_videos immediately (~5s), so the
tab populates fast. Phase 2 (view_count + dates) runs in a daemon thread
while the user is already browsing.

Also: catch table-not-found errors in the sort=popular query so a cold
server returns [] instead of 500. Frontend refetch wait 35s→8s to match
the faster Phase 1 commit time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:47:42 +02:00
be88d70935 Fetch first video thumbnail for playlists with no thumbnail
When yt-dlp returns no thumbnail for a playlist entry, fetch the
playlist's first video (max_videos=1) and derive a stable thumbnail
URL from its video ID. Applied during both the initial fetch and
on index (already done on index in previous commit).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:42:58 +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
abd7ed7c5a Redesign playlist cards to match VideoCard list style
Replace 2-col grid of small cards with a full-width list layout:
thumbnail on the left, title + status on the right — same proportions
and hover behaviour as VideoCard variant="list". Index/Re-index button
appears on hover, video count shown as a pill overlay on the thumbnail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:41:04 +02:00
112f87e764 Popular tab now shows only flagged popular videos in rank order
Add channel_popular_videos table (channel_id, video_id, rank).
_fetch_popular_task clears and rewrites this table after each fetch.
GET /channels/{id}/videos?sort=popular now JOINs this table and orders
by rank instead of view_count, so the tab shows exactly the videos
YouTube returned in popularity order — nothing more.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:38:53 +02:00