v1.8.0
A reliability and quality release driven by a multi-agent expert audit. Hardens error handling across the service/UI cascade so silent-failure UX is replaced with actionable feedback, fixes several latent runtime bugs, and brings the codebase to zero non-exempted Pyright errors (down from 218).
New
- First-run discoverability toast — on first launch, a 1.5s-delayed toast reads "Press ? for help · vim-style keys" (8s timeout). State persists in
session.jsonso the hint shows once. Legacy session files without the field upgrade cleanly. - Per-cause mutation-failure toasts — like/playlist-add operations now distinguish auth-required, auth-expired, network-down, and server-error failures with specific messages (e.g. "Sign in again — run
ytm setup" vs "Check your connection") instead of a single generic "Couldn't update". The classifier inspects ytmusicapi's exception types and parses HTTP status fromYTMusicServerErrormessages. - Page error-fallback states — Recently Played and Context (album/artist/playlist) pages used to show "Loading…" forever on API/disk failure. Now they replace the loading indicator with a clear error message pointing at
~/.config/ytm-player/logs/ytm.log. - 9 new integration tests in
tests/test_integration/covering the search→queue→play flow, track-change fan-out, cache-bypass behaviour, session round-trip, search cancellation, and the mutation cascade. Coverage floor raised 10 % → 47 %.
Fixed
- Album art crashed on Pillow ≥ 10.
Image.LANCZOSwas removed in Pillow 10. Switched toImage.Resampling.LANCZOS. Pyproject pinsPillow>=10, so this had been shipping broken for every modern install. - Spotify single/multi import would have ImportError. The popup imported
_get_video_idfromservices.spotify_import, but that symbol does not exist (the function isget_video_idinutils/formatting.py). Renamed all call sites. gg/Gin browse and playlist sidebar would crash. Code calledListView.action_first()/action_last()— neither method exists on Textual's ListView. Replaced with the standard cursor-index assignment.- New Releases tab in Browse silently empty.
YTMusicService.get_new_releasescalledclient.get_new_releaseswhich doesn't exist onYTMusic. The wrapping broad-except swallowed the AttributeError. Switched toget_explore()['new_releases']per the actual ytmusicapi surface. - Session-save errors disappeared into the void.
_save_session_statenow narrows its catch to(OSError, TypeError)and surfaces a toast on failure instead of silently dropping the user's volume / queue / playback position. - Mutation methods now return
MutationResult—rate_song,add_playlist_items,remove_playlist_items, plusadd_to_library,remove_album_from_library,unsubscribe_artist, anddelete_playlist. A Literal: success / auth_required / auth_expired / network / server_error. Previously returnedNone(orbool) whether the server accepted or not — UI showed "Liked!" or "Added!" toasts even when the API failed silently. Worst case was the Spotify import "Created with N tracks" toast firing when every batch failed. All UI cascade sites now show a per-cause toast suffix viamutation_failure_suffix. - Spotify multi-import partial-failure track count was off by up to 99. When a 350-track import had only the last batch fail, the toast reported "~300/350 added" because the formula assumed every successful batch was a full 100. Now tracks
added_totalcumulatively by summinglen(batch)on success, so the count is exact. - Read-side
logger.debug→logger.exception(~18 sites inytmusic.py). Search, library-list, get_album/artist/playlist/song/lyrics/history etc. previously caught broad exceptions and logged at debug level, so post-mortem of "library page came up empty" required--debug. Now they land in the log file at default level. YTMusicService._callouter catch narrowed. Programming-error exceptions (TypeError, AttributeError) now propagate instead of being swallowed and mistakenly counted toward the consecutive-failure threshold.- Thread-safety on lazy
YTMusicService.clientinit. Concurrent first-access fromasyncio.to_threadworkers is now guarded by athreading.Lockwith double-checked locking. - Credential file writes use
O_NOFOLLOW.auth.json(services/auth.py) andspotify.json(services/spotify_import.py) now refuse to follow a symlink at the target path — defense-in-depth matching the existing pattern inutils/logging.py.
Internal
- Comprehensive broad-except audit at
docs/broad-except-audit.md— categorizes all 263except Exception:sites in the codebase as KEEP / NARROW / PROMOTE with a cross-cutting cascade map. Referenced fromCLAUDE.mdso future contributors check the audit before adding new broad catches. - Pyright clean-up: 218 → 93 errors, with the remaining 93 entirely in
services/mpris.py(D-Bus magic, exempted in CLAUDE.md ruff rules) andservices/macos_eventtap.py(macOS-only AppKit/Quartz). Fixed real bugs along the way:playback_bar._FooterButton.__init__typedkwargsasobject(rejecting all forwarded params),_RepeatButton.repeat_modetyped asstrwhile assigned anint-typed enum value,track_table._filter_timertyped asobject | Noneso.stop()didn't type-check, and several Optional-access defensive gaps. - Codebase-wide
self.app.Xtyping — UI widgets/pages now castself.apptoYTMHostBaseat access points so Pyright can see the host's services. ~42 sites across 6 files. - README polish — badges, tagline, Contributors section.
- Audit-driven follow-up plans at
docs/superpowers/plans/2026-04-28-audit-driven-error-handling-cleanup.mdanddocs/superpowers/plans/2026-04-28-audit-driven-followup.md— written via the superpowers writing-plans + subagent-driven-development workflow.