Commit Graph

79 Commits

Author SHA1 Message Date
8a5108425b fix: top-bar indicator links to downloads when download/task is active
Previously it always linked to /discovery. Now it links to /downloads
when there are active downloads or tasks, and /discovery only when
discovery is the sole thing running.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 03:35:01 +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
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
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
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
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
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
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
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
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
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
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
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
d31fc1ef7f Add Popular tab to channel page
- YouTube sort=p fetch: indexes top 100 most-viewed videos from a channel,
  storing view_count in the DB
- Popular tab on channel page shows videos sorted by view_count DESC
- Videos/Popular tab switcher with context-appropriate fetch buttons
- Expose view_count in VideoOut; add 'popular' sort to channel videos endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:22:10 +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
0b482b5d49 Overhaul channel page: search, pagination, fetch all history
- Search bar filters indexed videos server-side; "Search YouTube" button
  triggers a deep channel search and indexes matching results
- Server-side sort (newest/oldest/A-Z/unwatched) + infinite scroll (60/page)
- "Fetch recent" indexes last 30, "Fetch all" indexes full history
- Auto-reindex on page visit if stale (>1h), refetches at 8s
- Add /channels/{id}/index-full endpoint (max_videos=0)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:15:09 +02:00
d740fd5224 Auto-reindex channel on page visit if stale
GET /channels/{id} now fires a background _index_channel_task if the
channel hasn't been crawled in the last hour. The frontend refetches
channel + videos 8s after page load to pick up the updated data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:02:59 +02:00
62c2c73906 Expand discovery pool and remove header logo
Double search results per query (20→40), increase query budget (15→25),
use more tags per signal (6→10-12), index more new channels per refresh
(5→10). Remove the YT logo from the header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:55:52 +02:00
52279752e4 Minimal flat redesign: white accent, remove card backgrounds
Replace yellow accent (#f5a623) with white (#ffffff) across the entire
app. Flatten VideoCard grid variant by removing zinc-900 card background
so content sits directly on the page. Simplify active states, badges,
progress bars, and hover effects throughout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:50:44 +02:00
366a2ff183 Limit auto-generated subtitle langs to en/sv/ja in CC dropdown
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:40:47 +02:00
8f0ce0756f Activate subtitle track when lang selected from dropdown
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:35:34 +02:00
33e4691619 Fix subtitle size and don't auto-select any track on load
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:33:11 +02:00
6311b90b21 Simplify CC dropdown: downloaded langs at top, YouTube langs below
Single CC chip shows downloaded langs inline. One click opens a dropdown
with optgroups — already-on-disk at top, YouTube-available below loading
async. No re-download needed to select an already-downloaded lang.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:25:41 +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
75493ed80e Center and style video subtitles via ::cue
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:18:24 +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
97ebcd6c1d Make subtitle availability check lazy (user-triggered, not on page load)
Auto-fetching on every watch page load spawned a yt-dlp process per visit
which could hang and pile up. CC button now triggers the check on demand.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:04:41 +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
Mattias Tall
83e2685c6a Quality selector auto-triggers re-download on change when video is saved
Changing the quality dropdown while a video is already downloaded now
immediately deletes the old file and starts a fresh download at the new
quality — no separate Re-download button needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 17:41:06 +02:00
Mattias Tall
744af7337b Add quality indicator and re-download at quality to Watch page
- Quality selector now always visible when idle (not just pre-download)
- Saved chip shows actual downloaded resolution (e.g. "Saved · 1080p")
- Re-download chip deletes existing file and starts new download at selected quality

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 17:37:03 +02:00
Mattias Tall
623e82fb16 Fix player crash: remove premature pollForFile in handleDownloadAndPlay
The unified effect already gates polling on dlStatus===complete, but
handleDownloadAndPlay was still calling pollForFile immediately on click,
before the download even started.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 17:30:58 +02:00
Mattias Tall
fe8028c1f9 Fix player crash: gate file polling on confirmed download completion
Never start polling until backend status==="complete" or video.is_downloaded
is true, preventing the player from loading a partially-written file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 17:29:07 +02:00
Mattias Tall
94f74215e2 Consistency sweep: fix switcher layout, gaps, buttons, empty states
- Home: mode switcher moved to its own row (no longer crammed next to
  title on mobile), hide-watched simplified to text-only toggle
- Home/History/Discovery: pagination buttons text-sm → text-xs, page
  counter text-sm → text-xs
- Liked/Downloads/SearchResults: top-level gap-8 → gap-6
- Liked: refresh button px-4 py-2 text-sm → px-3 py-1.5 text-xs
- Empty states: standardize to text-zinc-500 text-sm across Queue,
  ContinueWatching, History, Following, Discovery, Liked
- Following: "Latest uploads" tab label → "Feed"
- Home: remove -mt-3 hacks from mode description rows

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 17:21:02 +02:00
Mattias Tall
f422d754b9 Compact all mode/tab switchers site-wide
Home feed mode: px-3 py-1.5 text-sm → px-2 py-1 text-xs, tighter container
Hide-watched toggle: same reduction
Following tabs: px-4 py-2 text-sm → px-3 py-1.5 text-xs, "Latest uploads" → "Feed"
Discovery tabs: same reduction

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 17:12:18 +02:00
Mattias Tall
ebd8ddee6e Fix channel page mobile layout
On mobile: move action buttons below the banner into their own row
(flex-1 Follow + Download, compact Re-index) instead of cramming
three full-size buttons inside the banner overlay alongside the avatar
and name. Desktop keeps the original inline layout. Also reduced
banner height, avatar size, and description to 2-line clamp on mobile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 17:09:17 +02:00
Mattias Tall
ca5196d9f1 Restore dislike signal as icon-only chip next to Like
Removed the label ('Not for me') and kept it icon-only to match the
minimal direction, but the -3.0 affinity signal still fires so the
feed learns to show less of that content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 17:04:16 +02:00