Changelog
38 releases. Latest: v2.1.0 (2026-09-08). RSS.
Upgrade notes
- Existing sign-ins keep working. When an older session expires, run
ytm setuponce to enable account-checked automatic renewal. - Authenticated streaming is optional.
[yt_dlp] use_session_cookiesremains off by default. Enabling it requires a working JavaScript solver and runtime; anonymous streaming is unaffected. See configuration. - Downloads share the audio cache limit. Older cache entries may be evicted, oversized downloads are reported as not retained, and interrupted downloads restart from the beginning.
New features
- Multi-select tracks: click a track, then Shift-click another to select the range; Ctrl-click toggles individual tracks. Keyboard alternatives are
vto mark andVto extend a range. PressAto add the selection to a playlist. Marks survive sorting, filtering and background refreshes, including duplicate song occurrences. Mouse modifiers depend on terminal support. See keybindings. (#159) - Browse Playlists and Subscriptions: discover playlists from your home feed and open your subscribed artists. Five tabs fit alongside the sidebar in an 80-column terminal. Thanks

@ThePhatLeee. (#125) - Combined listening history: Recently Played now opens on All, showing local history first, followed by additional account history—not one combined timeline. Separate Local and YT Music tabs remain available; the account tab no longer excludes songs played in this app. Thanks

@Villoh. (#115)
Playback and queue
- Playlist shuffle now includes the first track played and respects saved shuffle preferences. Selecting a specific song still plays that song. For playlists over 300 tracks, the first song is chosen from the first 300; the remaining tracks shuffle in as they load.
- Double-clicking a sidebar playlist starts playback without leaving its Library view.
- Failed streams retry once with a fresh resolver after network or VPN changes. Exhausted recovery reports the failure rather than silently stopping or looping in repeat-one. (#135)
- Stop and newer playback requests supersede stale loads, radio requests and delayed queue updates. Old playback events no longer advance a newer selection.
- Queue actions target the selected occurrence, including duplicate songs. Removing the playing row lets that song finish before continuing with its surviving successor.
- Shuffle toggles and background queue changes keep the displayed rows synchronized. Turning shuffle off preserves the current track; reordering clears temporary sorting while retaining filters.
- Opening and quitting without playback preserves the saved resume point. Failed startup, failed loads and superseded seeks no longer overwrite it.
Playlists, Browse and search
- Playlist changes report rejected or unconfirmed results accurately. After a timeout, check the playlist before retrying—the change may already have succeeded.
- The playlist picker prevents duplicate submissions, retains filter focus, handles duplicate confirmation consistently, and displays newly created playlists even when adding tracks is declined.
- Recently Played preserves marks and cursor position across repeated account-history rows and background updates.
- Browse and Charts recover from failed or interrupted loads. Country changes show matching data, and locale values select the correct country.
- Large playlist libraries and full artist album/single lists are no longer truncated. (#134, #138)
- Radio, discovery, search and background playlist loading no longer cancel unrelated work. Sidebar double-clicks no longer trigger redundant playlist loads.
Sign-in and reliability
- Automatic renewal stays with the recorded account, works for sorted playlists, and backs off for 60 seconds after failure.
- Sign-in updates protect newer setup sessions from concurrent renewal and bind stream cookies to the corresponding sign-in.
- Manual setup accepts Chrome’s pasted headers more reliably, preserves the existing session until replacement, and reports when a running app needs restarting. Zen Browser detection is supported. (#119)
- Unreachable YouTube Music, an unreadable history database, or malformed session/update-cache files no longer prevent the supported startup paths from continuing.
- Shutdown attempts remaining cleanup even when a service fails. Settings containing multiline or control characters round-trip correctly.
- Downloads are indexed correctly, publish only completed files, and report directory failures without closing the app.
Interface, diagnostics and dependencies
- Custom themes correctly control selection highlights and album-art placeholders; switching themes preserves terminal colours and other base settings. (#122, #158)
- Fixed punctuation seek keys, missing radio artwork, markup-like titles and duplicate artist-album rows. (#143)
ytm doctorredacts HTTP(S)/TCP URLs, including embedded stream IP/signature parameters. Review diagnostics before sharing; arbitrary secrets are not guaranteed to be removed.- yt-dlp warnings reach the app log, resolver resets handle concurrent work safely, and
pycryptodomexsupports faster browser-cookie extraction. Thanks
@wgordon17. (#136) - Updated yt-dlp requirements address widespread 403 failures; Spotify scraper compatibility and full-playlist fetching are restored. (#140, #142)
- Packaging excludes known local-note files and checks for unintended untracked inclusions in locally built distributions.
- Existing sign-ins keep working. When an older session expires, run
New features
- Recently Played — YT Music account history tab — the Recently Played page now has two tabs: Local (the existing SQLite play history recorded inside this app) and YT Music (your account-wide play history fetched from the server via ytmusicapi
get_history()). Switch tabs by clicking the labels or with the keyboard (Tab/Shift+Tabto focus a tab label,Enterto activate) — consistent with the Browse page. Each tab is cached so switching back doesn't refetch; re-clicking the active tab does a hard refresh (Local re-reads SQLite, YT Music refetches the server). Both tabs are capped at 100 rows to keep the TUI responsive. The two views are disjoint: Local shows what you played in this app (a just-played track appears at the top immediately), YT Music shows only plays made elsewhere (phone, browser) — plays this app syncs to your account are filtered out of the server feed, and a track you play locally drops off the YT Music tab live.[▶ Start Radio]and filtering work on both. No new keybinding — both views live underg r. Thanks
@Villoh (#116). - Plays sync to your YouTube Music history — tracks played in the TUI are now reported back to your YT Music account (via
add_history_item()), so they show up in your history and feed recommendations like any other client. Fired best-effort in the background so it never blocks playback. Opt-out viaplayback.sync_history_to_ytmusic = false. Thanks
@Villoh (#116). - Configurable listen threshold —
playback.history_min_listen_seconds(default 5) sets how long a track must play before it counts as a play instead of a skip, for both local history and the YT Music account sync. Clamped to 0–3600;0still requires at least one full second of playback. Thanks
@Villoh (#116). - Play Next, everywhere — press
X(orCtrl+X) on any page to slot the focused track in right after the current one. Albums and playlists get a "Play Next" entry in their context menus too, inserting the whole set in order — shuffle-aware, so they still play next even with shuffle on.
Changes
- Only one TUI instance at a time — launching
ytmwhile another instance is running now exits with "ytm is already running (PID …)" instead of letting two instances fight over the same session and IPC socket. - Add to Queue works on every page —
Z/Ctrl+Zsilently did nothing on five of the seven pages; it now works everywhere through the same track resolution the actions menu uses, and queue toasts name the track ("Added to queue: …", "Playing next: …"). - Retired keybindings that promised nothing —
g A/g a("context/selected actions") were bound, routed, and listed in Help, but no page ever implemented them (theaactions menu already does that job) — removed, along with the dead Escape close-popup action. The Help page now matches reality. - Every color now follows your theme — the last hard-coded widget colors (the search-mode
▶indicator and the Spotify-import status texts and result symbols) now use the theme'sprimary/success/warning/errorvariables, so custom themes recolor them too. toggle_search_modeships unbound — theM-vdefault was unreliable because most terminals intercept Alt+letter for their own menus; bind your own key inkeymap.tomlif you want one.
Fixes
- Video rows stop crediting "1.2M views" as the artist — YT Music returns the view count as a pseudo-artist on video results; it's now stripped during normalization, so history and video rows show the real artist and artist-based actions (go to artist, radio) built from those rows target the right one. Thanks

@Villoh (#116). - MPRIS survives broken dbus-fast builds — a dbus-fast that raises
TypeErrorat import (dbus-fast 4.x on Python 3.14) now disables MPRIS with a notice instead of crashing at startup. Thanks
@dsafxP (#113). - Discord presence can no longer wedge on one bad call — a single failed pypresence call (closed pipe, timeout) silently disabled Rich Presence for the rest of the session, leaving the last song stuck on Discord until app exit. RPC calls are now serialized, reconnect on demand, and retry once after a failure. Thanks for the report

@SUPER-MAGIX (#117). - Queue:
d/J/Kafter sort or filter — deleting or reordering on a sorted/filtered Queue page acted on the wrong underlying track; the row→track mapping is now correct. - Playing a selected track works the same on every page — Search and Browse now rebuild the queue the way Library and Context always did, fixing stale Queue-page contents and duplicate plays from double-fired events.
- Removing the playing track under shuffle — the queue now advances to the next shuffled track, matching non-shuffle behaviour, instead of landing somewhere arbitrary.
playerctlvolume and position — MPRISVolumeis wired in both directions andSeekedis emitted on jumps, so desktop applets can set the volume and track the playhead.- Rapid track-switch race — starting a track while another was still resolving could double-play or land on the wrong track; play requests are now serialized and the newest wins.
- IPC hardening — CLI↔TUI messages are newline-framed, and the Windows TCP fallback now requires an auth token instead of trusting anything on localhost.
- Config resilience — a corrupt
keymap.tomlis backed up and replaced with defaults instead of aborting startup, settings values are type-validated at load, andcache_diraccepts~paths. - Navigation state stays fresh — pages no longer restore stale cursor/content state, and deleting a playlist purges its cached pages instead of leaving ghost entries reachable via Back.
- Popup backdrop clicks — clicking outside any popup now dismisses it consistently, across all popups.
- Database errors degrade gracefully — SQLite errors at history/session write sites are caught alongside OS errors instead of crashing the operation.
- Player teardown races — transport actions during mpv shutdown no longer raise, and a failed stream start no longer disturbs end-of-track handling for the track after it.
- Media-key presses can no longer vanish — Windows and macOS key callbacks were scheduled without holding a reference, so the garbage collector could occasionally reclaim one mid-flight and the press did nothing. A shared dispatcher now keeps every in-flight callback alive.
- Radio respects shuffle from a cold start — starting radio into an empty queue with shuffle on skipped the shuffle-order build, so tracks played in linear order until shuffle was toggled off and on.
- Fast track-switching can't wedge stream resolution — cancelling a track that was still resolving could leave the next request for the same track waiting forever, or turn a successful resolve into an error. Both cancellation paths now settle cleanly.
- Playlist-creation failures say why — creating a playlist (sidebar, picker, or Spotify import) now reports whether it failed from an expired session, a network problem, or a server error, instead of a generic "Failed to create playlist".
- A corrupt saved volume can't derail startup — a garbage
volumein session.json falls back to the default and out-of-range values clamp to 0–100, instead of aborting the session restore midway. - Liked Songs distinguishes "empty" from "failed" — a fetch failure now shows a check-the-log message instead of the misleading "No liked songs found."
- Browse outages say so — when For You or Charts can't be fetched (network, expired session, server error), the sections now show the retryable error copy instead of pretending the account has no recommendations or the region has no charts.
- Two swallowed-input quirks — a right-click that didn't open a popup no longer eats your next Enter/click, and picking a search suggestion identical to the current input no longer eats the next keystroke.
ytm doctorstops counting bystanders — any process whose command line contained a/ytm…path (an editor open on the repo, say) counted as a running instance; only the realytm/python -m ytm_playerentry points match now, including console-script launches (where the interpreter sits in argv[0] and theytmscript path in argv[1]).- A crashed session can't lock you out — if the OS handed a crashed instance's PID to an unrelated process,
ytmrefused to launch untilytm.pidwas deleted by hand; the single-instance guard now checks that the recorded process is actually ytm-player and cleans the stale file itself. - Parser drift isn't blamed on your connection — when YouTube changes a response shape mid-operation, the error now reads as a server problem instead of "check your internet connection".
- No phantom crash file from the yt-dlp pre-warm — a failure in the background yt-dlp import was written out as a crash file; it now logs a warning, and the only real cost is a slower first play.
Internal
- The five per-page track-filter stacks are unified into one shared mixin, all popups sit on a shared base shell, and the ytmusicapi sort-parameter patch window no longer leaks into unrelated concurrent API calls.
- First dedicated test coverage for the Spotify import service and the mpv player wrapper.
- Services dedup sweep: the Python 3.10 StrEnum shim has one home, the three platform media-key services share one thread→loop dispatcher, stream-URL expiry follows a single policy, the lyrics and update-check fetchers use
requests, and duplicated CLI/IPC boilerplate, path normalizers, listen-logging, and go-to-artist/album id extraction each collapsed into single implementations. Page-load failures now log their real tracebacks. - Packaging hardening: the AUR package now declares
python-packaging, requiresdbus-fast5.x (4.x is the #113 crash combo), and caps textual below 9.0; a mistagged release can no longer upload the wrong version to PyPI (the tag is checked against__version__before anything builds); CI now also runs on pull requests targetingdev.
- Recently Played — YT Music account history tab — the Recently Played page now has two tabs: Local (the existing SQLite play history recorded inside this app) and YT Music (your account-wide play history fetched from the server via ytmusicapi
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