Skip to content

Skip past tracks Spotify is playing that are already in the library - #40

Merged
revtex merged 1 commit into
mainfrom
skip-recorded-tracks
Aug 29, 2026
Merged

Skip past tracks Spotify is playing that are already in the library#40
revtex merged 1 commit into
mainfrom
skip-recorded-tracks

Conversation

@revtex

@revtex revtex commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Keeping the file on disk was only half the answer. Offstream declined to record the song and Spotify played it to nobody for three minutes, so working through a playlist that is mostly recorded meant sitting through the parts that already are.

A new switch beside the existing-file setting on the Advanced page — "Also tell Spotify to move on" — sends a skip through the Windows media transport controls when the track playing is one already in the library. Off by default, greyed out under the two policies that write the file again.

Why SMTC and not the Web API

POST /me/player/next would work, but it wants the user-modify-playback-state scope, a signed-in account and a Premium subscription for a convenience feature — and a third scope on a consent screen CLAUDE.md holds to two. SMTC is the same command the keyboard's next-track key sends: no account, no scope, no network, and it reaches Spotify while it is minimised to the tray.

IPlaybackControl is deliberately a second interface rather than a method on ISmtcSessions. Reading what is playing and changing what plays are different privileges, and a session handed no implementation simply never skips.

Four things this had to get right

Each is a distinct failure, and each has a test.

Armed from the second track a session admits, never the first. Pressing record is not an instruction to start rearranging what is playing: the song already under way is one the user chose and is part-way through, and was never going to be recorded whole anyway. It is also the only defence against the media session's opening report, which is the previous track with the play state already true for a few hundred milliseconds — so a session acting on the first thing it sees fires the command at the song the user has just started.

Asked once per track, marked before the call rather than compared after it. Spotify goes on reporting the outgoing track for a moment after it takes the command — the same staleness — so a session deciding per observation asks twice, and the second skip lands on a song the user has not recorded. Marking the track and clearing it only when a different one is considered covers both that window and repeat-one.

Asked at two checkpoints, not one. The existing-file check in Consider runs before the metadata lookup, so a template built on {album}, {year} or {track} renders a path nothing will ever be written to and every track looks new. Wired only there, skipping would silently never fire for exactly the libraries organised well enough to want it. The second checkpoint runs when the lookup lands, gated on the recorder still being the current one — a lookup outlives its track on purpose, and by the time it returns the user may be two songs on.

Stopped after fifty in a row, and said so. A queue Offstream already has, left on repeat, has no other terminating condition: every skip lands on another track that is also on disk. The budget is restored by a recording actually reaching the library — not by one merely starting, or the enrichment checkpoint above would reset the cap on every track and never reach it.

The core reads SkipAlreadyRecordedTracks and the existing-file policy together rather than trusting the page, since a hand-edited settings.json can disagree with a greyed-out switch.

Layout

The switch sits beside the dropdown rather than under it, which meant auto-sizing that grid's right-hand column. It holds the counter field, hidden for every template without {count} — and a star column reserves its half of the card whether the thing in it is visible or not, so the row had 450 units for a dropdown, a switch and its label, and the label was cut off mid-word.

Testing

920 Core + 201 UI tests green; solution builds with 0 warnings under analyzers-as-errors.

Not covered: WindowsSmtcSessions.TrySkipNextAsync and its IsNextEnabled guard are the untestable seam by design, so every test runs against a fake transport. What is proven is when Offstream decides to skip, never that the command reaches Spotify. Same shape as the routing gap the plan already records — wants a live try before this is relied on.

🤖 Generated with Claude Code

Keeping the file on disk was only half the answer. Offstream declined to
record the song and Spotify played it to nobody for three minutes, so
working through a playlist that is mostly recorded meant sitting through
the parts that already are.

The command goes through the Windows media transport controls — the same
one the keyboard's next-track key sends. The Web API has POST
/me/player/next, but it wants the user-modify-playback-state scope, a
signed-in account and a Premium subscription for a convenience feature,
and a third scope on a consent screen CLAUDE.md holds to two. SMTC needs
none of those and reaches Spotify while it is minimised to the tray.

Four things this had to get right, each a distinct failure:

Armed from the second track a session admits, never the first. Pressing
record is not an instruction to start rearranging what is playing: the
song already under way is one the user chose and is part-way through,
and it was never going to be recorded whole anyway. It is also the only
defence against the media session's opening report, which is the
previous track with the play state already true for a few hundred
milliseconds — so a session acting on the first thing it sees fires the
command at the song the user has just started.

Asked once per track, marked before the call rather than compared after
it. Spotify goes on reporting the outgoing track for a moment after it
takes the command — the same staleness as above — so a session deciding
per observation would ask twice, and the second skip lands on a song the
user has not recorded. Marking the track and clearing it only when a
different one is considered covers both that window and repeat-one.

Asked at two checkpoints, not one. The existing-file check in Consider
runs before the metadata lookup, so a template built on {album}, {year}
or {track} renders a path nothing will ever be written to and every
track looks new. Wired only there, skipping would silently never fire
for exactly the libraries organised well enough to want it. The second
checkpoint runs when the lookup lands, gated on the recorder still being
the current one — a lookup outlives its track on purpose, and by the
time it returns the user may be two songs on.

Stopped after fifty in a row, and said so. A queue Offstream already has,
left on repeat, has no other terminating condition: every skip lands on
another track that is also on disk and the session drives Spotify round
it forever, at the speed of a media command rather than of a song. The
budget is restored by a recording actually reaching the library — not by
one merely starting, or the enrichment checkpoint above would reset the
cap on every track and never reach it.

Off by default: it reaches out and changes what the user is listening
to. Greyed out under Overwrite and Duplicate, which write the file again
and leave nothing to skip past — and the core reads the two settings
together rather than trusting the page, since a hand-edited
settings.json can disagree.

The switch sits beside the existing-file dropdown rather than under it,
which meant making that grid's right-hand column auto-size: it held the
counter field, hidden for every template without {count}, and a star
column reserves its half of the card whether the thing in it is visible
or not. The row had 450 units for a dropdown, a switch and its label,
and the label was cut off mid-word.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@revtex
revtex merged commit 03b4e32 into main Aug 29, 2026
6 checks passed
@revtex
revtex deleted the skip-recorded-tracks branch August 29, 2026 00:40
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.

1 participant