Skip to content

fix: cover art stuck on the previous album with native streaming - #406

Merged
LargeModGames merged 3 commits into
mainfrom
fix/native-cover-art-desync
Jul 27, 2026
Merged

fix: cover art stuck on the previous album with native streaming#406
LargeModGames merged 3 commits into
mainfrom
fix/native-cover-art-desync

Conversation

@LargeModGames

@LargeModGames LargeModGames commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #402: with native streaming, advancing through the queue left the cover art
(and the image_url behind Discord RPC, MPRIS and now-playing sync) showing the
previous track's album.

current_playback_snapshot() sourced image_url from the polled /v1/me/player
context even while native librespot metadata was authoritative for title, artist
and album. That context lags librespot by seconds after a skip, and
get_current_playback() deliberately refuses to overwrite it while stale, so the
art stayed pinned to the previous album. Because the runner's cover-art loop only
refetches when the resolved URL changes, an unchanged stale URL meant no refetch
at all. A natively queued track makes it worse: it plays via a direct
player.load that Spirc never reports, so the context is not merely late, it can
stay wrong for the whole song.

librespot's TrackChanged payload already carries the answer. audio_item.covers
is a widest-first list of fully resolved https://i.scdn.co/image/<file_id> URLs,
the same CDN form the Web API returns. This stores the widest into a new
NativeTrackInfo.image_url at the event boundary and prefers it in the snapshot,
falling back to the context item when librespot reports no covers. No extra
network calls and no dependence on the poll loop.

The issue reporter kindly included an alternative patch that adds
IoEvent::FetchNativeTrackArt calling GET /v1/tracks/{id} from inside
get_current_playback(). I did not take that route: get_current_playback() runs
on a poll loop, so it would re-dispatch that fetch on every poll for as long as the
context stays stale, which for a queued track is the whole song. It also keys off
app.last_track_id rather than the event that actually knows which track started.

Testing

  • cargo fmt --all
  • cargo clippy -- -D warnings (exit 0)
  • cargo clippy --no-default-features --features telemetry -- -D warnings (exit 0)
  • cargo test -> 756 passed, 0 failed
  • cargo test --no-default-features --features telemetry -> 496 passed, 0 failed

Three new tests in media_metadata.rs:

  • native_track_art_wins_over_a_stale_context_item (the bug)
  • native_track_falls_back_to_context_art_without_librespot_covers (the fallback)
  • context_art_is_used_once_native_info_is_cleared (the handoff boundary)

Manually verified in the TUI on the full cargo run build against a live Spotify
session: the cover art now follows the current track when advancing through the
queue.

Additional notes

Known limits and follow-up work, none introduced by this change:

  • Same-titled consecutive tracks still show the old art.
    api_confirms_native_info_is_current accepts a bare title match before checking
    IDs, asserted intentional by the existing api_confirms_native_info_when_names_match
    test. When two consecutive tracks share a title but differ in album, native info is
    cleared early and the snapshot reverts to the API item, so the new field is never
    read. Left alone deliberately: that shortcut most likely exists for Spotify track
    relinking (same track, different ID per market), so changing it risks a regression
    outside this issue. context_art_is_used_once_native_info_is_cleared pins the
    boundary.
  • "Wrong song on other devices" is not addressed. That is Spirc not being told
    about direct player.loads, i.e. server-side state rather than the local snapshot.
  • Local files played through librespot carry covers: vec![] (their art is
    embedded in the file), so they still fall through to whatever the Spotify context
    holds. Pre-existing gap, worth its own issue.

Summary by CodeRabbit

  • Bug Fixes
    • Album artwork now stays synchronized with the track currently playing, including after skips and during natively queued playback.
    • Native track cover art is now preferred when available, with automatic fallback to the contextual cover when it isn’t.
    • Empty or missing native cover URLs no longer block the fallback artwork from being shown.

`current_playback_snapshot()` sourced `image_url` from the polled
`/v1/me/player` context even while native librespot metadata was
authoritative for title, artist and album. That context lags librespot by
seconds after a skip, and `get_current_playback()` deliberately refuses to
overwrite it while stale, so the art stayed pinned to the previous album.
The runner's cover-art loop only refetches when the resolved URL changes,
so an unchanged stale URL meant no refetch at all.

A natively queued track makes it worse: it plays via a direct
`player.load` that Spirc never reports, so the context is not merely late,
it can stay wrong for the whole song.

librespot's own `TrackChanged` payload already carries the answer.
`audio_item.covers` is a widest-first list of fully resolved
`https://i.scdn.co/image/<file_id>` URLs, the same CDN form the Web API
returns. Store the widest into a new `NativeTrackInfo.image_url` at the
event boundary and prefer it in the snapshot, falling back to the context
item when librespot reports no covers (local files, whose art is embedded
in the file). No extra network calls, and no dependence on the poll loop.

Empty cover URLs are filtered out: librespot substitutes into a
session-supplied url template, so a blank template would yield `Some("")`
and defeat the fallback instead of deferring to it.

Fixes #402
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: de2270d0-a19a-4687-85f2-2f9d8b6cec40

📥 Commits

Reviewing files that changed from the base of the PR and between aa8906a and 3dbca68.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • src/infra/player/events.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/infra/player/events.rs

📝 Walkthrough

Walkthrough

NativeTrackInfo now carries artwork from librespot track events. Playback snapshots prefer that artwork, fall back to context artwork when unavailable, and restore context artwork when native track state is cleared. Tests and fixtures were updated for the new field.

Changes

Native artwork synchronization

Layer / File(s) Summary
Native artwork contract and event population
src/core/app.rs, src/infra/player/events.rs, src/infra/scripting/tests.rs, src/tui/runner.rs
NativeTrackInfo adds an optional image URL, and TrackChanged populates it from the first non-empty cover URL. Related fixtures include the new field.
Playback artwork selection and validation
src/infra/media_metadata.rs
Playback metadata prefers native artwork, falls back to context artwork when missing, and tests cover stale context artwork, missing native artwork, and cleared native state.

Estimated code review effort: 2 (Simple) | ~15 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title uses an existing conventional-commit prefix and matches the native streaming cover-art fix.
Linked Issues check ✅ Passed The changes address issue #402 by keeping native cover art in sync and preferring it over stale context artwork.
Out of Scope Changes check ✅ Passed The added field, playback logic, tests, and changelog entry all support the cover-art desync fix.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/native-cover-art-desync
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/native-cover-art-desync

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/infra/player/events.rs`:
- Around line 688-700: Update the cover selection in the audio item mapping to
choose the largest entry from audio_item.covers rather than assuming .first() is
widest. Preserve the existing URL cloning and empty-string filtering, and use
the cover dimensions or established size metadata to compare candidates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b40cf32-ba85-4c39-9821-569bbbadc4a9

📥 Commits

Reviewing files that changed from the base of the PR and between e65696c and aa8906a.

📒 Files selected for processing (5)
  • src/core/app.rs
  • src/infra/media_metadata.rs
  • src/infra/player/events.rs
  • src/infra/scripting/tests.rs
  • src/tui/runner.rs

Comment thread src/infra/player/events.rs Outdated
CodeRabbit read `.first()` on `audio_item.covers` as assuming an order
that is not guaranteed. It is guaranteed here: the pinned librespot fork
sorts covers descending by width in `get_covers`
(metadata/src/audio/item.rs) and the filter that follows preserves that
order. The comment asserted widest-first without saying where the
ordering comes from, so cite the source instead of changing the
selection. No behavior change.
The stale-context read shipped in v0.40.2, so this is a fix to released
behavior and belongs under Unreleased > Fixed.
@LargeModGames
LargeModGames merged commit b116b0d into main Jul 27, 2026
16 checks passed
@LargeModGames
LargeModGames deleted the fix/native-cover-art-desync branch July 27, 2026 05:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

album cover / song playing desync

1 participant