Changelog
36 releases. Latest: v1.9.5 (2026-06-28). RSS.
New features
- Unified Tab / Shift+Tab section navigation —
TabandShift+Tabnow move focus between sections (track tables, result panels, Browse tab labels, and any visible sidebar) consistently on every page, withj/k/arrows moving within the focused section andEnteractivating it. Previously only the Search page worked this way; other pages ignoredTabor repurposed it. On Browse,Tabhighlights a tab label andEnteropens it. - Vim-style pane focus navigation — keyboard users can now move focus between the Playlists sidebar, main content, and visible lyrics pane with
Ctrl+w h,Ctrl+w l, andCtrl+w w. The Playlists shortcut auto-shows the sidebar if it is hidden. Thanks
@860windtree (#107, #108).
Changes
- Queue track reorder moved off
Tab— reordering the selected track now usesShift+J/Shift+K(lowercasej/kmove the cursor, as on every other page). It honours a count prefix, so15 Jmoves the selected track down 15 positions in one step. This freesTab/Shift+Tabon the Queue page for section navigation. playerctl/ media keys / now-playing work out of the box on Linux — thedbus-fastlibrary that MPRIS needs now ships by default on Linux instead of behind an optional[mpris]extra, so a standardpip install ytm-player(or AUR / Nix install) exposesplayerctl, hardware media keys, and desktop now-playing controls with no extra steps.ytm doctorreports MPRIS status and a one-time startup notice flags a broken or incomplete install, so the previous silent no-op can't recur. Thanks for the report
@pironha2 (#110).
Diagnostics
- Crash files self-identify, and
ytm doctorflags stale crashes — every crash log now records the app version, time, Python, and platform it was written under, andytm doctorwarns when the most recent crash predates the installed version (or predates version stamping). A stale, already-fixed crash from an older build no longer reads as a live bug.
- Unified Tab / Shift+Tab section navigation —
A broad release: community playlist and context-menu features, a batch of crash fixes across macOS, Windows, and Nix, and Discord Rich Presence working again.
New features
- Create playlist with metadata — the "New Playlist" flow now asks for name, description, and privacy (Private / Public / Unlisted) in a single modal, instead of defaulting to private with no description. New
CreatePlaylistPopupreplaces the minimalInputPopup. Thanks
@Villoh (#79). - Edit playlist metadata — right-click any user playlist in the sidebar and choose "Edit Playlist" to rename it, update its description, or change privacy. The sidebar and the open library header update in place without reloading tracks. Thanks

@Villoh (#79). - Enriched library playlist header — description, privacy status, and year now appear in the header alongside owner and track count. Thanks

@Villoh (#79). - Sidebar count sync on add — adding tracks via the "Add to Playlist" picker bumps the target playlist's track count in the sidebar immediately, and duplicate adds prompt before re-adding. Thanks

@Villoh (#79). - Auto-navigate on delete — deleting the currently open playlist from the sidebar returns you to the plain library view instead of leaving a ghost page. Thanks

@Villoh (#79). - Remove a track from a playlist — the track action menu now offers "Remove from Playlist" when viewing one of your playlists, removing the track in place. Thanks

@Villoh (#79). - Entity action consolidation — context menu actions (Play All, Shuffle Play, Add to Queue, Start Radio, Go to Artist, Subscribe) now work consistently for albums, playlists, and artists across all dispatch sites: sidebar, search results, track table column right-click, context page, library page, and browse page. "Shuffle Play" pre-shuffles for a one-time random order without enabling ongoing shuffle. "Play All" / "Shuffle Play" from sidebar and search start playback immediately and jump to the queue. Thanks

@wgordon17 (#81). - Column-aware context menus — right-clicking the Artist column opens artist actions (Go to Artist, Play Top Songs, Start Radio, Subscribe); the Album column opens album actions (Play All, Shuffle Play, Add to Queue, Go to Artist). Multi-artist tracks show a picker first. Thanks

@wgordon17 (#81). - Corporate SSL proxy support — set
ca_bundleunder[yt_dlp]to a custom CA certificate bundle so stream resolution works behind SSL-inspecting proxies (Zscaler, Netskope, etc.). A warning is logged if the path doesn't exist. Thanks
@glywil (#98). - Richer Discord Rich Presence — the now-playing status shows the track's album art (falling back to the app icon), displays as Listening to YouTube Music, and includes elapsed time. Thanks

@Wiibleyde (#103).
Fixes
- Discord Rich Presence connects again — the bundled application ID had been rejected by Discord, so presence never appeared. Registered a fresh app and made the ID configurable via
[discord] client_idfor anyone who wants to use their own. Reported by
@Villoh (#88). - Crash setting ANSI themes — selecting
ansi-dark/ansi-light(added in Textual 8.2.5) crashed the app because their colour tokens likeansi_cyanaren't parseable by Rich. They're now translated to the bare ANSI names. Thanks
@dmnmsc (#89). - mpv not found with Homebrew installs — on macOS and Linuxbrew, libmpv lives outside the default library search path, so a non-brew Python (uv tool, pipx, distro) couldn't load it even though
mpvwas on PATH. ytm now searches the Homebrew prefixes, andytm doctorreports libmpv loadability on its own line (#90, #101, #104). - Crash on Windows with the
mprisextra —dbus-fastis Linux-only and raised an uncaught error at import; the extra is now marked Linux-only and the import is platform-gated, souv sync --all-extraselsewhere degrades gracefully instead of crashing at startup. Thanks
@Villoh (#106). - Spotify import builds on Nix again — the
spotifyscraperderivation no longer fails on a sandboxedpip installduring the build, so flake builds with thespotifyextra work again. Thanks
@peternaame-boop for both causing and fixing it (#93). - Nix flake: missing
packagingdependency — the update checker crashed on flake builds becausepackagingwasn't declared. Now included, with an import check so it can't regress. Thanks
@szx19970521 (#95, #105).
Infrastructure
- Faster Nix installs — the nixpkgs pin now points at a cached channel revision, so flake users download prebuilt dependencies instead of compiling them (notably Deno, pulled in by yt-dlp) from source.
Docs
- Install and troubleshooting docs now reference
dbus-fastinstead of the obsoletedbus-next(the code migrated in v1.9.3). Thanks
@aaguilar-hub (#99).
- Create playlist with metadata — the "New Playlist" flow now asks for name, description, and privacy (Private / Public / Unlisted) in a single modal, instead of defaulting to private with no description. New
A small follow-up release: two crash fixes caught by manual smoke after v1.9.2, two CLI/utility fixes, plus three community PRs.
New features
- Configurable default theme — set
themeunder[ui]inconfig.tomlto control which Textual theme loads on startup (defaultytm-dark). Changing theme viaCtrl+P→Themeupdates the current session only; the startup default is no longer overwritten by session state. To persist the active theme as the new default, useCtrl+P→Theme: Set Current as Default. Thanks
@Villoh (#85). Theme: Set Current as Defaultcommand palette action — saves the currently active theme toconfig.tomlso it becomes the startup default. Includes rollback on save failure (e.g. read-only filesystem) with an error toast.- Discovery round-robin —
Dnow cycles deterministically through Charts → Trending → For You → Liked → Artist → Recently Played (was random source selection). Charts sub-rotates through its shelves between presses, and the Discovery label shows the active source (e.g.Discovery (US Daily Top 100)). Mood source dropped (the Moods & Genres tab was removed in v1.9.1 due to upstream crashes). Thanks
@wgordon17 (#75). - Radio queues prepend their seed tracks — radio playback now starts with the seed before suggestions, matching YouTube Music's native behaviour. Append-mode background refill is unchanged. Thanks

@wgordon17 (#75). - Persistent queue source header — Queue page shows
Generated from: …beneath Now Playing for radio and discovery queues, with up to three seed titles inline and a tooltip for the full list. Toggle via[ui] show_queue_source(default on). Thanks
@wgordon17 (#75).
Fixes
- Crash on Go to Artist / Album / Playlist —
TrackTableand_ArtistAlbumListset up their columns inon_mount, butcontext._build_artist's nested-mount chain callsload_tracks/load_albumssynchronously beforeon_mountfires, soadd_rowran with 0 columns and raisedValueError: More values provided than there are columns. Both widgets now eager-init columns in__init__, eliminating the mount-order race. - Update check version comparison —
_is_newernow usespackaging.version.Versionfor proper PEP 440 comparison. The hand-rolled tuple parser dropped non-numeric chunks and got post-releases (e.g.1.6.0.post1) wrong. ytm configwith multi-arg$EDITOR—EDITOR="code -w"and similar now work; previously the whole string was passed as a single argv entry, so subprocess looked for an executable literally namedcode -wand failed. Malformed quoting (e.g. unbalanced quotes) now exits cleanly via the existing error path instead of crashing.- Search race conditions — fixed four interacting bugs that caused searches to require a second Enter, the suggestion overlay to re-appear on top of incoming results, and selecting a suggestion to cancel the in-flight search worker. Thanks

@wgordon17 (#84).
Changed
- Theme persistence model —
config.tomlis now the authoritative source for the startup theme;session.jsonstores runtime state only and no longer restores the theme on launch. This separates user-authored configuration from app-managed session state. - Command palette provider architecture — app-specific commands moved from
get_system_commands()to a dedicatedYTMCommandProviderregistered inApp.COMMANDS. Isolates command definitions insrc/ytm_player/app/_commands.pyand keeps_app.pyfocused on app logic.
Infrastructure
- Pre-commit hooks + pyright in CI +
dbus-fastmigration — new.pre-commit-config.yamlruns ruff-format / ruff / pyright on commit and pytest on push (pre-commit installto activate). New CI job runs pyright at standard strictness. The Linux MPRIS service migrates from the unmaintaineddbus-nextto the maintaineddbus-fastfork (identical API). Thanks
@wgordon17 (#83). - Settings save Windows fallback —
Settings.save()now falls back to a directpath.write_text()whenos.replace()raisesPermissionError(e.g. whenconfig.tomlis held open by an external editor on Windows).
- Configurable default theme — set
A focused fix release. The Charts page region selector was effectively non-functional — three structural bugs in how we read YouTube's chart response made a global event playlist appear regardless of region. Also includes a TrackTable migration that retires three hand-rolled
DataTablepages and two related bug fixes.Charts — bug report thanks

@dmnmsc (#73)- Events visually separated from country charts. YouTube injects a global event playlist (currently
"Coachella 2026: Daily Top 100 Songs"— same playlist ID worldwide) at position 0 of every country's response. Previously the Charts page default-loaded that slot, so picking a country still showed the global Coachella playlist. The Charts page now renders two stacked pill rows: aFeatured globally:strip for any shelves whose title carries a brand prefix (": "separator) and a country-charts row for the actual regional shelves. Country charts sort by priority —Top 100 Songs→Weekly Top Songs on Shorts→Trending 20→ rest — and the default-loaded pill is the first country chart, never an event. The event row hides automatically on narrow terminals (< 80 cols) to reclaim a row of vertical space. - Now reads
daily + weekly + videosfrom the API. Previously onlydailywas consulted, which silently dropped Spain (returns its data undervideos) and missed the Top 100 Songs / Top 100 Music Videos shelves underweeklyfor premium-supported regions. All three keys are now concatenated, then split into events vs charts. - Region picker expanded 17 → 68 entries. Global (
ZZ) is the new default. The list now mirrors YouTube's full advertised set (62 codes fromcountries.options) plus six historically-supported codes outside that list (Hong Kong, Malaysia, Singapore, Taiwan, Thailand, Vietnam). Settings default flipsregion = "GB"→region = "ZZ". - Locale-style configs auto-normalise. New
services/regions.normalise_region()helper strips locale tails —"ES-ES","en-GB","es_ES"now resolve to bare two-letter codes (ES,EN,ES) before hitting the API. YouTube's chart endpoint silently falls back to Global for any locale-shaped input; this prevents existing configs from being broken by that quirk. _clean_shelf_titleno longer strips brand prefixes. Coachella keeps its"Coachella 2026:"prefix on the pill so users can tell an event from a country chart at a glance.
New
- TrackTable migration on Queue, Liked Songs, Recently Played. Three pages migrated from raw
DataTabletoTrackTable. Gains right-click context menus, play indicators, column resize, filtering, and sorting — for free, on three pages that previously rolled those manually. Also retires theon_mouse_downright-click workaround we added to QueuePage in v1.9.0 (TrackTable already wires this up). Thanks
@wgordon17 (#74). [▶ Start Radio]button added to Liked Songs and Recently Played page headers — seeds a radio from 5 random tracks in the collection. Thanks
@wgordon17 (#74).- Shuffle-lock integration on Liked Songs and Recently Played — selecting a track applies the per-collection shuffle preference. Thanks

@wgordon17 (#74). - Discovery mix now cycles sources in fixed order (Charts → Trending → For You → Your Liked Songs → Artist → Recently Played) instead of random selection — guarantees variety across consecutive presses of
D - Radio notifications now list all seed track names as a bulleted list instead of showing only the first seed or a generic label
- Playlist radio notification now includes the playlist name (e.g. "Playing: Radio from My Playlist")
Changed
- Mood source removed from discovery mix — upstream removed Moods & Genres tab; the source was failing silently
clean_shelf_titleandget_chart_shelf_tracksadded as shared utilities for reuse in discovery mix
Fixes
- Radio track durations no longer show
--:--— ytmusicapi'sget_watch_playlistreturns duration under alengthkey (e.g."3:07"), notduration.extract_duration()now checksduration_seconds→duration→lengthin priority order. Thanks
@wgordon17 (#74). - Play history no longer stores duration as 0 —
log_playwas reading rawtrack.get("duration_seconds", 0), but normalized tracks store the value underduration. Switched toextract_duration()so the value is always correct regardless of source. Thanks
@wgordon17 (#74). - TrackTable Duration column no longer cut off on first paint. The row-label column (which carries the
▶playing indicator) reserves ~3 cells of width that the original column-fit pass didn't account for, so the rightmost column ("Duratio…") got pushed past the visible viewport on initial render._fill_title_columnnow runs afterload_tracksandappend_tracksso the Title column shrinks to compensate as soon as rows exist. - Sidebar gains a bottom separator under the Playlists panel — the existing top separator (a
Rulewidget between the pinned-nav block and the LibraryPanel) is now mirrored below the LibraryPanel, so thePlaylistspanel sits between two matching$border-coloured horizontal rules instead of just one.
- Events visually separated from country charts. YouTube injects a global event playlist (currently
This is the third and final wave of major updates in a rapid release cycle — quieter cadence ahead.
A community-PR-driven feature release. Six PRs from

@wgordon17 plus user-reported UX work, an AUR auto-publish pipeline, and a per-collection shuffle memory system. Distribution data milestone — crossed 10,000 lifetime PyPI downloads on the day this release was assembled.Supersedes the same-day v1.9.0 tag, which shipped two small regressions: the browser forward-stack got clobbered when clicking the current page's footer entry a second time, and the playback-bar Shuffle-lock dimming didn't refresh when starting a radio from the sidebar. Both are fixed in this release — install v1.9.1 directly.
New features
- "Start Radio" from playlist — context-menu entry in the sidebar plus a
[▶ Start Radio]header button on the Library and Context pages. Backed by ytmusicapi'sRDAMPLwatch-playlist convention. New service methodYTMusicService.get_playlist_radio(playlist_id). Six unit tests covering VL-prefix stripping, normal flow, exceptions, empty results, non-dict responses. Thanks
@wgordon17 (#70). - Proactive radio refill — when ≤3 tracks remain in queue with autoplay on and repeat off, the app silently fetches more tracks in the background, seeded from up to 5 already-played items in the current queue. New methods
_maybe_extend_queue/_extend_queueon PlaybackMixin. Thanks
@wgordon17 (#71). - Multi-seed
get_radio— service method now accepts a list of seeds, fetches in parallel viaasyncio.gather, dedups byvideoId, shuffles, and trims to limit. Backward-compatible at the signature level (single string still accepted). 8 dedicated tests. Thanks
@wgordon17 (#71). - Single-seed shuffle guard —
get_radioonly shuffles the result pool whenlen(video_ids) > 1. Single-seed callers (the existing "Start Radio on track X" flow) keep their seed-first ordering. Two new tests covering single-seed-deterministic and multi-seed-shuffled behavior. - Discovery roulette — random mix from one of seven sources (trending, mood, charts, home, liked songs, library artist, recent history) with last-source-exclusion rotation to avoid repeats. New service method
get_discovery_mix() -> tuple[list[dict], str]. 6 dedicated tests. Bound to theDkeybinding (added by us alongside the merge — PR description had mentioned the binding but it didn't ship). Discovery is also reachable via the new "♫ Discovery Mix" item in the playlist sidebar's pinned-nav block. Help page lists the new action. Thanks
@wgordon17 (#71). - Shuffle lock (per-playlist) — explicit toggle in the Library page playlist header (
Shuffle lock: ON/Shuffle lock: off). When ON: opening that playlist forces shuffle, and the playback-bar shuffle button is dimmed and rejects clicks (with a toast pointing the user back to the lock toggle). When OFF: normal global shuffle behaviour. Persists per-playlist-ID to~/.config/ytm-player/shuffle_prefs.json(LRU-capped at 1000 entries; thread-safe atomic JSON writes). Replaces the originally-planned implicit "remember last shuffle state" behaviour, which was un-discoverable and didn't actually persist via the playback-bar click path. NewQueueManager.current_context_id+set_context()underpin the lookup. Session restore preservesqueue_context_idso the lock identity survives a restart. - Configurable home shelf count — set
home_shelvesunder[ui]inconfig.tomlto control how many recommendation shelves are fetched on the Browse > For You tab (default 3, range 1–25). Shelves now scroll as a single section instead of independently, with subtle separators ($border) and theme-primary section titles for readability. Thanks
@wgordon17 (#69). - Sidebar overflow config (
[ui] sidebar_overflow) —"truncate"(default) guarantees exactly 1 row per playlist with a…ellipsis on overflow, or"wrap"to let names span multiple lines naturally. Implementation via atruncate-itemsCSS class onLibraryPanelplus a_render_texthelper that picks the right strategy per mode. - Selection info bar — a 1-row strip mounted between the page body and the playback bar, displays the full name of the currently-focused item (sidebar playlist or TrackTable row). Toggleable via
[ui] show_selection_info: bool = True. Replaces the old marquee/bouncing-text animation in playlist sidebars. The bar is gated on widget focus so it shows only what the user is actively navigating, not whatever auto-highlighted in a freshly-mounted panel. - Now-playing row indicator — every TrackTable now uses Textual's row-label feature to show a
▶glyph in a dedicated 1-char column to the left of the index, marking the playing row independently of cursor state. Bold (no color) on data cells of the playing row provides a secondary at-a-glance signal. The glyph survives navigation away and back viacursor_foreground_priority="renderable"and anon_mountlookup of the app's current playing track. This pattern follows the canonical approach used by spotify-tui, ncmpcpp, and cmus. - Charts country support —
get_charts(country=)now defaults to"GB"instead of YouTube's empty-data"ZZ"placeholder. New[ui] regionconfig field (ISO 3166-1 alpha-2) lets users pin Charts to their preferred country without code changes. The Charts page now resolves the top daily chart playlist into a track table (ytmusicapi'sget_chartsno longer returns a flat songs list — daily/weekly/genres/artists chart playlists is the new shape).
Fixes
- macOS built-in keyboard media keys (prev/next) — built-in MacBook keyboards send key codes 19/20 (FAST/REWIND) instead of 17/18 (NEXT/PREVIOUS) that external keyboards use. The Quartz event tap now maps both sets. Verified against Apple's IOKit constants and Rogue Amoeba's canonical reference. Thanks

@wgordon17 (#67). - Browse tab bar visibility — was rendering at
height: 1withborder-bottom: solidon the active tab clipping its label. Now usesheight: 4,border-bottom: tall,width: autofor content-sized tabs, hover state,$surfacebackground,$borderseparator, plus 1-row top padding for breathing room from the page edge. Thanks
@wgordon17 (#68). - yt-dlp android client fallback — madeForKids content (e.g., children's music tracks) was failing with "Video unavailable" on the default web client. yt-dlp now also tries the android client which falls through to a non-PoT legacy format that succeeds. Empirically validated: Baby Shark goes from broken to playing format-18 m4a audio. Normal songs unaffected. The line carries a
# WHY:comment explaining the rationale so a future audit doesn't strip it as cargo-cult config. Thanks
@wgordon17 (#72). - Moods & Genres sub-tab removed entirely — clicking a mood was producing a silent process-exit traceable past every Python-level handler in the audit (see
docs/superpowers/specs/2026-04-30-mood-crash-findings.md). The remaining suspects are sub-Python (libmpv segfault, finaliser raise, etc.) and require runtime data we don't have. Pulled the tab rather than ship a feature that crashes the app on click. Tab order is now (For You, Charts, New Releases). The unusedYTMusicService.get_mood_categories/get_mood_playlistswrappers were dropped at the same time — Discovery Mix calls the underlying ytmusicapi client directly, so the wrappers had no remaining callers. KeyErrorfrom ytmusicapi parsers no longer counted as "unexpected" — addedKeyErrorto_EXPECTED_API_EXCEPTIONSso_calltreats parser drift as a recoverable API-side failure instead of letting it propagate as a "programming error." This was masking real upstream issues and surfacing crash files for routine YouTube response-shape changes.- Sidebar playlist track count now updates immediately after add — was reading the cached
countfield from the library payload and never bumping it afteradd_playlist_itemssucceeded, so the(N tracks)suffix stayed stale until the next library reload. NewLibraryPanel.update_item_count(playlist_id, delta)primitive optimistically updates the cached count and rebuilds the affected row's label text. Wired into both_do_addand_do_create_and_addin playlist_picker. Tolerates VL-prefix mismatches in either direction. Leavescount = None(unknown) untouched rather than fabricating. - Queue right-click now opens the actions popup — previously did nothing because the Queue page uses a raw
DataTable(notTrackTable), so theTrackRightClickedhandler never fired. Addedon_mouse_downonQueuePagethat forwards button-3 clicks to_open_actions_for_trackusing the cursor row. - Track actions popup conditionally swaps "Add to Queue" ↔ "Remove from Queue" — when the track being right-clicked is currently in the playback queue, the menu shows "Remove from Queue" (action_id
remove_from_queue) instead of "Add to Queue". Same menu slot, different label and action based on queue state. Detection happens at popup-open time by scanningqueue.tracksfor the track'svideo_id. - Forward stack no longer clobbered by same-page footer clicks (post-v1.9.0) — clicking the current page's footer entry a second time intentionally pops the previous page off the back stack, but the navigation path was treating it as a fresh forward navigation and clearing the forward stack along with it. Added a
same_page_backflag so the forward stack survives the round-trip. - Sidebar Start Radio refreshes the Shuffle-lock visual state (post-v1.9.0) — starting a radio from the sidebar correctly applied the Shuffle-lock force-shuffle behaviour, but the playback-bar shuffle button's dim/non-dim state wasn't repainted to match. Added an explicit
refresh_shuffle_lock_state()call on the playback bar from the sidebar Start-Radio path.
Diagnostic infrastructure
faulthandler.enable(all_threads=True)— fatal-signal capture wired incli.pyaftersetup_logging. SIGSEGV/SIGBUS/SIGFPE/SIGILL/SIGABRT now leave a Python traceback for every thread in~/.config/ytm-player/crashes/faulthandler.logbefore the kernel delivers the signal. Critical for catching libmpv/python-mpv C-side crashes that bypasssys.excepthookentirely.- asyncio loop exception handler —
App.on_mountinstalls_asyncio_exception_handlervialoop.set_exception_handler. Funnels "Task exception was never retrieved" warnings and any orphan-task errors intocrashes/ytm-crash-*.loginstead of the invisible-stderr default that Textual's alt-screen swallows. sys.unraisablehook—install_excepthooksnow also captures__del__/ weakref-callback / generator-close errors, labels themUnraisable in <obj>, and writes a crash file before chaining tosys.__unraisablehook__. Fixes a class of silent finaliser failures that were previously lost.- mpv
log_handler+loglevel="warn"— Player constructsmpv.MPV(log_handler=...)so libmpv's internal warnings/errors hit the Python logger asmpv[<area>]: <message>.ytm doctorgreps these out ofytm.logso libmpv issues are surface-level visible. ytm doctorv2 — eight sections — version, paths, process status, recent ERROR/WARNING (filtered, last 20), recent mpv warnings, latest faulthandler trace, latest crash file, active-hooks summary. All output passes through a redaction layer that scrubsAuthorization,Cookie,Bearer,token,x-goog-pageid,SAPISIDso issue pastes never leak auth.- App-level
_handle_exceptionoverride — the YTMPlayerApp overrides Textual'sApp._handle_exception(documented "Always results in the app exiting") to write a crash file viawrite_crash_file(), surface a toast, and not callsuper(). Keeps the TUI alive on otherwise-fatal worker / render / event errors so the user doesn't lose their queue position to a transient failure. write_crash_fileself-bootstraps — falls back topaths.CRASH_DIRwheninstall_excepthookswas never called, and logsOSErrorinstead of silently returningNone. Fixes the "crashes dir empty after a real crash" diagnostic black hole.
Charts
- Country picker (
c) — presscon Browse → Charts to open a filterable region modal. Type to filter on ISO code or country name; Enter to select; Esc to cancel. Selection persists to[ui] regioninconfig.tomland triggers a chart refetch. NewAction.PICK_COUNTRYenum entry, default bindingc. - Region list trimmed to 17 empirically-verified working countries (Australia, Brazil, Canada, France, Germany, Hong Kong, Japan, Malaysia, Mexico, Singapore, South Korea, Taiwan, Thailand, UAE, UK, US, Vietnam). YouTube's
countries.optionsadvertises 62 but most return no daily-chart data even with auth; trimmed to the subset that actually displays tracks. - Daily-shelf pills — clickable pill row above the track table shows all available daily chart shelves for the selected country (typically
Daily Top 100/Trending 20/Daily Top Videos/Daily Top Songs (Shorts)— labels stripped of brand prefixes likeCoachella 2026:and country suffixes). Click a pill to swap the table to that shelf's playlist.HorizontalScrollcontainer keeps pills usable on narrow terminals. - OLAK5 playlist fallback — Trending shelves use OLAK5-prefixed auto-generated playlist IDs that ytmusicapi's
get_playlistchokes on (tracks[0]['album']isNone→ TypeError).ChartsSection._load_active_dailydetects the prefix and usesget_watch_playlistinstead, which calls a different endpoint and parses correctly. Service-layerYTMusicService.get_watch_playlistextended to accept aplaylist_id-only call shape (was video-only before). - Empty-region UX — a region picked with no chart data now shows
"No chart data available for <country>. YouTube Music coverage varies by region — press 'c' to pick a different one."instead of the previous catch-all"Failed to load charts.".
Navigation
- Browser-style back / forward —
← BackandForward →buttons in the header bar (next to☰ Playlists). Auto-hide on root pages and at the front of history. Keys:Backspace(back),Shift+Backspace(forward). Forward stack invalidates on any non-back/forward navigation, matching every browser/file-explorer. - Click bubbling fix —
Action.PICK_COUNTRYwas missing fromapp/_keys.py:_handle_action's page-delegation allowlist, socsilently no-op'd when first shipped. Added the entry; same_app-headerID-based HeaderBar lookup as the rest of the codebase.
UI / theming
- Marquee removed from sidebars —
_BouncingLabel(the bouncing-text animation that fired on highlighted playlist names that overflowed) deleted entirely (~60 lines). Replaced with static truncate-with-ellipsis. Long names showSome long playlist nam…; the full name surfaces in the new selection info bar when the item is highlighted. - Bottom-stack layout reflow —
SelectionInfoBar,PlaybackBar, andFooterBarare now wrapped in a singleVertical(id="bottom-stack")docked to the bottom. Order top-to-bottom: info bar (1 row) → playback bar (4 rows) → footer (1 row). Replaces the previous three-sibling-dock-bottom approach which produced inconsistent stacking. truncate()uses Unicode…instead of three ASCII dots. Frees 2 chars of visible content space; matches every modern UI's truncation idiom.- CONTRIBUTING.md "Theming & UI" section — documents the rule that all widget colors flow through
theme.pyvariables ($primary,$text,$surface,$border,$accent,$secondary,$text-muted) and that hardcoded hex colors in widget CSS are forbidden because they break customtheme.tomlfiles. Closes a contributor-docs gap surfaced during PR #69 review.
Refactors / internal
type: ignore[attr-defined]removed in PR #70's_start_playlist_radiocall sites incontext.pyandlibrary.py. Replaced with the project-standardcast("YTMHostBase", self.app)._start_playlist_radio(...)pattern. Added stub method to_base.py:YTMHostBase(CLEANUP-1)._fetch_and_play_radio,_start_discovery_mix,_start_playlist_radio,shuffle_prefs,home_shelves,region,show_selection_info,sidebar_overflow— all added as TYPE_CHECKING stubs / config fields with the project's existing patterns._BouncingLabeland timing constants (_BOUNCE_INTERVAL,_BOUNCE_PAUSE) removed.
Infrastructure / release engineering
- AUR auto-publish workflow — new
.github/workflows/aur-publish.ymltriggered after thePublishworkflow succeeds. Clones the AURytm-player-gitrepository via SSH key (stored insecrets.AUR_SSH_PRIVATE_KEY), copiesaur/PKGBUILD, regenerates.SRCINFOvia the newscripts/regenerate_srcinfo.pyhelper, commits, and pushes. Gated onevent.workflow_run.event == 'push'so TestPyPI dry-runs don't trigger a real AUR push. scripts/regenerate_srcinfo.py— pure-Python.SRCINFOgenerator that parsesaur/PKGBUILD(handles scalar fields, array fields, multi-line arrays, single/double quotes, shell-variable expansion via_expand_vars, and emits the canonical key-value format consumed by AUR). Runs on Ubuntu CI runners without requiring Arch Linux base-devel. 6 unit tests covering parser primitives, repo-PKGBUILD round-trip, and emit format.RELEASING.md— repo-root release procedure. Documents the tag-driven flow (bump__version__→ tagvX.Y.Z→ push), what each automated workflow does (publish.yml,aur-publish.yml), the manual fallback for the AUR push if the action fails, the one-time setup for AUR SSH keys + GitHub secrets + PyPI trusted publishing, the dry-run path via TestPyPI, and the distribution-channel matrix (PyPI/AUR/GitHub Release automated; NixOS flake manual; Gentoo GURU community-maintained by
@dsafxP).AUR_SSH_PRIVATE_KEYandAUR_KNOWN_HOSTSrepo secrets configured (one-time setup).
Coming next
- UX polish (4-8 from the in-session UX review) — Library track-count subtitle, Search panel border deduplication + mode-toggle markup cleanup, Browse active-tab visual weight, Queue "Now Playing" header hierarchy.
Session note — release engineering
This release was assembled across one long session that pushed 50+ commits to public master, including ~14 UI-iteration commits that should have lived on a feature branch. Going forward: UI-iteration sagas land on
dev, get squash-merged intomasteras one clean commit per saga.- "Start Radio" from playlist — context-menu entry in the sidebar plus a