The cookie login was not missing, it was buried: Downloads tab -> Custom
Options -> the "Login with Cookies" tab, where Downloads is one tab of five.
The three features that depend on it -- the Feed's account mode, Search and
Browse -- are other tabs, with no route to it. show_cookie_login_dialog(),
which opens that dialog on the right tab, had no caller anywhere in the
codebase; it does now.
An account button sits in the corner of the tab bar, visible from every tab,
saying whether cookies are in use and where they came from. The Feed offers
the same thing next to its account mode rather than greying the option out and
leaving it at that, and the disabled entry now says why it is disabled.
Applying cookies used to require a restart before playback saw them:
_build_ytdl_raw_options was read once, when the player widget was built. A
cookiesChanged signal now reaches the account button, the Feed and the live
mpv handle, and the player reloads at its current position -- ytdl_hook
consults the option when it resolves a URL, so anything already playing keeps
the streams it resolved with. YtdlpClient needed no change: it re-reads config
per call.
Two bugs in the same function: the options are joined with commas and were
never escaped, so a cookie path or a proxy URL containing one silently ended
the option and started a bogus one; and geo_proxy_url was honoured by the
downloader but never passed to the player, so a geo-restricted video would
download and then refuse to play.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tab order was Watch, Search, Feed, Browse, Downloads, and the app opened
on Watch -- which is where the other tabs send you, not somewhere you start.
It is now Feed, Search, Browse, Downloads, Watch, with icons. Routing is
unaffected: _tab_index_of resolves by widget identity, not position.
Refresh-on-open needed care rather than a call in showEvent. A local refresh
is one yt-dlp subprocess per subscribed channel, serially, so it is braked
four ways: only channels not seen for feed.auto_refresh_on_open_minutes (30,
0 to disable), at most eight per visit, once per interval per session, and
after a delay so it does not race the tab transition or first-run setup. The
cached feed is already on screen throughout. That is a new config key rather
than the dead auto_refresh_minutes, because existing configs store that as 0
and would have read as opting out of a feature that did not exist yet.
SmoothTabWidget gained the currentChanged signal, icons and a corner slot it
never had. Its set_current_index needed a re-entrancy flag, not just an index
check: setting the tab bar's index emits its currentChanged straight back into
the same method, and at that point the stack has not moved, so every switch
fired the activation hook twice.
The grid was cleared and rebuilt after every channel finished -- flicker, lost
scroll position and every thumbnail re-read from disk each time. merge_entries
keeps existing cards. While there, _relayout was re-adding cards the layout
already owned, so layout items accumulated on every Load more, and the resize
check compared against columnCount(), which never shrinks, so it relaid out on
every resize event.
Smaller things this exposed: feed errors were written into the label the next
success overwrote, so failures were invisible; switching to account mode left
the local videos on screen; cancel() had no callers, so a refresh outlived the
tab and the window; Browse's Subscribe never said Unsubscribe though it
toggles; "Play all" queued one page while claiming otherwise; and the feed
sorted publish times against wall-clock fetch times in one COALESCE, so the
last-refreshed channel floated to the top.
Feed is the first thing seen now, so an empty one says what to do about it
instead of showing a bare grid.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was no icon system at all. Transport buttons called
QStyle.standardIcon(SP_MediaPlay), which returns the platform theme's dark
monochrome glyph -- painted onto the app's saturated red buttons at a fixed
36px with no text, that is the black square. Everything else called an icon
was an emoji baked into en.json, which is tofu wherever the emoji font is
missing. Qt stylesheets cannot recolour a QIcon, so the colour is an argument
to the new helper; that is the whole fix.
The SVGs are drawn here rather than vendored, which keeps a third-party
licence out of the tree, and they live in the module rather than as asset
files, which keeps them out of package-data and safe in a frozen build.
Tooltips were the other half: three widgets in the entire application had one
and nothing set an accessible name. Filling that in at the call sites would
have meant editing well over a hundred of them, most in upstream-owned files.
Instead one application-level event filter handles QEvent.Polish, which Qt
sends to every widget once before it is shown -- so it also reaches dialogs
built by upstream code, and survives the next merge. It maps placeholder
emoji to icons, fills empty tooltips from the button text, and logs icon-only
buttons that still have none so the gaps are findable.
StyleSheet.MAIN styles the window, inputs, buttons and tables and nothing
else, so the main tab bar, combos, sliders, lists, menus, splitters, tooltips
and the horizontal scrollbar fell through to the platform style. EXTRA_QSS
covers them, in a fork-owned module appended at the one application site.
SmoothTabWidget names its frame "tabContent" with the comment "We draw border
on content instead" -- that rule existed only inside two dialogs, and now
exists for the main window too.
Two corrections to rules that were already there: the pressed style changed
the padding, shifting every label two pixels and clipping fixed-width icon
buttons, and checkboxes were fully rounded, which reads as a radio button
rather than an on/off toggle.
Verified by screenshot on the real display: tab bar, buttons, combo carets
and checkboxes all render as intended, and all 35 icons rasterise non-empty.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
New ytsage/utils/ytsage_library_manager.py: sagetube_library.db
(separate from the upstream download-history DB) with WAL from day one,
holding subscriptions, cached feed_items, watch_history with resume
positions, and the persisted play_queue. Watch history and queue tables
are wired up by the next commit.
FeedPage:
- Local mode: FeedRefreshWorker refreshes each subscribed channel
sequentially (feed.per_channel_items, default 15) and the grid fills
incrementally per channel; results are cached so the feed is
populated instantly on startup.
- Account mode: fetches youtube.com/feed/subscriptions with the user's
browser cookies - the real logged-in feed; the option is enabled only
while cookies are active and errors surface as a status banner.
- Sidebar lists subscriptions (double-click opens the channel in
Browse; context menu unsubscribes). Browse's Subscribe button now
toggles subscription state through the Feed page.
Verified live: subscribe -> refresh -> 15 videos cached in SQLite and
rendered as cards.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The single-page downloader layout becomes the Downloads tab of a
SmoothTabWidget with Watch / Search / Feed / Browse / Downloads pages.
The init_ui edit is deliberately small (the old central widget is now
self.download_page); all new behavior lives in new modules:
- ytsage_gui_router.py: AppRouter signal hub (playVideo, queueVideo,
downloadVideo, openChannel, openPlaylist). A card's Download button
deep-links into the Downloads tab with the URL prefilled and analysis
started automatically.
- ytsage_gui_cards.py: VideoCard (thumbnail with disk cache under
APP_THUMBNAILS_DIR, title/channel/duration, Play/Queue/Download
actions, double-click to play) and VideoCardGrid (responsive grid,
Load more pagination).
- ytsage_gui_watch.py: WatchPage hosting the mpv PlayerPanel and a
drag-reorderable play queue with auto-advance on end of file.
- ytsage_gui_search.py: SearchPage running YtdlpClient.search off the
GUI thread with Load more pagination.
- Browse and Feed pages are placeholders, implemented next.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>