v1.6.0
A polish release focused on diagnostics, stability, security, and performance. 51 commits, 65 new tests (491 total), no headline user-facing features — but a lot of friction removed from "what do I do when something breaks?"
New
ytm doctorcommand — prints a one-paste diagnostic report (version, Python, mpv, OS, recent log lines, most recent crash trace). Drop the output into a bug report and you've given me everything I need to triage.- File-based logging — logs now go to
~/.config/ytm-player/logs/ytm.log(rotated, 5×1MB by default). Previously, log output disappeared into Textual's alt-screen and was unrecoverable. New[logging]config section exposes level + rotation knobs. New--debugCLI flag enables verbose tracing. - Crash file capture — unhandled exceptions from any thread now write a full traceback to
~/.config/ytm-player/crashes/so you have something to attach to issues even when the app is dead. - Local audio cache now serves replays — previously the
CacheManagerindexed downloaded tracks butplay_track()never asked it. Cached files (downloads + replayed tracks under your[cache] max_size_gb) now bypass yt-dlp entirely for instant playback with no network round-trip. - Update notifications on startup — a background worker checks PyPI once per 24 hours and surfaces a one-time toast when a newer version is available. Silent on network failure, cache lives at
~/.config/ytm-player/update_check.json. Opt out by settingcheck_for_updates = falsein[general]. - Session.json schema versioning —
schema_versionfield lets future format changes detect-and-discard incompatible state instead of silently misbehaving.
Fixes — security
- IPC socket is now created owner-only via
umask(0o077)aroundbind(), in addition to the existingchmod 0600.
Fixes — concurrency / stability
- Player no longer races on track changes —
_current_trackwrites duringplay()andstop()are now under_skip_lock, preventing torn reads from MPRIS, Discord, Last.fm, and the end-of-track callback during rapid skips (C1). - mpv crash recovery no longer silently breaks subsequent playback — when
_play_synccatchesmpv.ShutdownErrorand_try_recoversucceeds,_current_trackand theTRACK_CHANGEevent payload now stay consistent. (Two issues here: an initial fix went too far and cleared_current_trackmid-play(), breaking MPRIS/Discord/Last.fm and stopping auto-advance after recovery — caught in final review and reverted.) ytmusic.get_playlist(order=...)is now safe under concurrent calls — the function monkey-patches ytmusicapi's internal_send_requestto inject theorderparameter, but two concurrent calls would corrupt the patch state. Now serialized with anasyncio.Lock(C3).- Settings.toml and session.json writes are now atomic — uses
os.replaceafter writing to a.tmpsibling, so power loss /kill -9mid-write can no longer leave you with a half-written file that crashes startup. - Session.json corruption no longer crashes startup — bad JSON or schema mismatch falls back to defaults silently. First launch (file absent) stays silent; format mismatch on existing files logs a warning.
- Play failures no longer block retry within 1 second — the double-click debounce stamp now clears on stream-resolution failure or
player.play()exception, so clicking the same track again after a failure isn't silently swallowed. - Update check no longer pins users to a stale "latest" if the system clock ever ran fast — negative cache age now triggers a re-fetch.
- Bare
except:swallows promoted tologger.exceptionin 6 high-impact spots (player callbacks, ytmusic API failures, MPRIS/Discord/Last.fm errors, history logging) — failures that previously vanished now show up in~/.config/ytm-player/logs/ytm.logwith a stack trace.
Performance
- Theme switches feel instant —
theme.tomlis now mtime-cached inget_css_variablesinstead of being parsed from disk on every CSS variable lookup. Edits totheme.tomlstill pick up automatically (mtime invalidates the cache). - Queue page play-indicator update is O(1) instead of O(n) — switching tracks no longer rewrites every row in queues with hundreds of tracks; only the changed cells are touched.
- Album art rendering moved off the event loop —
_image_to_half_blocks(PIL resize + per-pixel iteration) now runs inasyncio.to_thread, so loading a thumbnail never blocks input handling. - Library page background workers cancel on page removal — the "fetch remaining tracks" worker for large libraries no longer keeps running after you've navigated away.
Project
- Multi-OS CI — workflow now runs on Ubuntu / macOS / Windows × Python 3.12 / 3.13 (was Ubuntu / 3.12 only). Pip cache enabled.
devbranch now triggers CI too. - Dependabot configured for weekly grouped pip + GitHub Actions updates.
- Issue templates (bug + feature) — bug template requires
ytm doctoroutput; feature template asks contributors to bundle related ideas. - PR template, CODEOWNERS, SECURITY.md (private vulnerability reporting), CONTRIBUTING.md (dev setup, ruff order, logging conventions, RTL guard, PR norms).
.gitattributesenforces LF line endings;.gitignoreexpanded for IDE/OS junk,result(Nix),*.log, etc.- Repo metadata: description, homepage, 9 topics (
tui,textual,music-player,youtube-music,terminal,mpv,python,vim-keybindings,cli), GitHub Discussions enabled.
Tests
- 65 new tests, 491 total (was 426). New coverage areas: logging + excepthook setup, doctor diagnostics, cache contract, session restore resilience, schema versioning, update check + clock skew, IPC dispatch + seek parsing, key normalization, navigation back-stack invariants, playback debounce + cache-hit path, atomic writes, settings load.
- Help-page coverage enforcement test — fails CI if any
Actionenum member is missing fromACTION_DESCRIPTIONSorACTION_CATEGORIESinui/pages/help.py. Catches the "added a new keybind, forgot to document it" drift forever.