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>
- 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>
- 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>
- Backend: DELETE /stats/taste/{tag} removes the row from user_tag_affinity
- API: deleteTasteTag(tag) helper
- Stats UI: × button on each tag chip, faint by default, full opacity on hover;
invalidates stats query so the tag disappears immediately
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- manifest.json + icon.svg for installability on mobile (standalone mode)
- index.html: theme-color, apple-mobile-web-app meta tags, manifest link
- Settings: Import CSV section reads Google Takeout subscriptions.csv,
extracts UC... channel IDs, calls follow-bulk to follow them all at once
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Name, subscriber count, and action buttons sit at the bottom of the
banner with a gradient overlay. Falls back to a plain dark header when
no banner is available. Description moves below the header.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
Shows node version, yt-dlp version, cookie args, and raw stderr tail
to diagnose download/metadata failures without needing shell access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A failed login attempt was triggering the global 401 interceptor which
silently redirected back to /login, making the form appear broken.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getQueue was called on the watch page but missing from the import list,
causing a ReferenceError that broke the entire /watch/:id route.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _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>
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>