v2.0.0
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.