Skip to content

fix: the four defects the client campaigns left open - #228

Merged
InstaZDLL merged 1 commit into
mainfrom
fix/four-defects-before-the-tag
Sep 16, 2026
Merged

InstaZDLL merged 1 commit into
mainfrom
fix/four-defects-before-the-tag

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Sep 16, 2026 •

Copy link
Copy Markdown
Owner

The four defects the client campaigns left open, cleared before the tag. None is a regression — all four ship in 2.0.0-beta.0 — and none is carried into 2.0.0-beta.1.

Closes #219. Closes #224. Closes #226. Closes #225.

#224 — a role separator cutting inside parentheses

split_on applied its separators in passes over the whole value, with no idea where in the string it was. A COMPOSER reading

Kobee (Melange / INHOUSE), Holy M (Melange / INHOUSE), EZIT (INHOUSE), …

names two people. The two slashes made three entities, each carrying a parenthesis it never opened, and they reached the catalogue as artists that search3 answered with — 43 names appeared in search that the browse index never held, four of them these fragments.

It is one pass now, counting parenthesis depth and cutting only at zero. The reference's rule is untouched: Bach/Gounod is still two people, AC/DC is still one band. Depth is clamped at zero so a stray ) cannot make the rest of a value uncuttable, and a ( that never closes protects only the tail it opened.

#225 — song where the schema says entry

getPlayQueue and getNowPlaying named their children as though the rename that applies inside a playlist did not apply to them. Three client campaigns passed over it because every client that met it was lenient; one that decodes against the schema reads an empty queue.

playQueue also gains the username the schema requires beside changed and changedBy.

This is an observable wire change, made deliberately now. The freeze protects the clients that were validated, not a divergence from the specification, and a beta is what it is for. Both names are not emitted together: a response carrying entry and song would be a third contract, conforming to neither, and would need a second wire change later to remove.

#226 — a 416 announcing a length of zero

Two refusals wear that status and they do not mean the same thing.

Content-Range Accept-Ranges
a complete file, range out of bounds bytes */<real length> bytes — only the range was wrong
a transcode not yet whole omitted none

bytes */0 did not say "length unknown". It said the resource is empty, which a client may believe and never ask about again. And the first case answered none, contradicting every other answer the same file gives.

#219 — a refusal that outlived its session

A sign-out and a sign-in fit inside one round trip, so a 401 raised for account A could arrive after account B signed in: renewing spent B's rotating refresh token for a request A made, and the replay went out under B's access token.

Five routes did this, not the four the issue names — the scan event stream renews the same way and was missed. One renewFor now holds the shape, checked before the renewal and again after it, because the renewal is itself a round trip a sign-out can happen inside.

Every test was run against the defect, not only against the fix

what was inverted what fell
the parenthesis guard removed 2 of the 3 tag tests
the parenthesis rule made too broad the other 1, and 1 again
the rename reverted the contract test
username removed alone the contract test
bytes */0 restored the range test
Accept-Ranges: none on a known length the range test
the generation check neutralised the session test — and only it, 1 of 19

That last line is the point: it is new coverage, not a second opinion on something already covered.

Two review findings not taken

Rechecking the generation between renewFor resolving and the replay. It closes a window one microtask wide that nothing in the module can write to — every writer of the session sits behind an await, and a user gesture is a task, which cannot preempt a microtask drain. No scheduling distinguishes the guard from its absence, which is this repository's own bar for keeping one. Say the word and I will add it anyway; it costs one &&.

Making the empty getPlayQueue schema-conforming. The schema has no way to say "empty queue" — the reference omits the element — so conforming means removing playQueue, a third wire change on the one call every client makes at startup, with no observed harm and none of it asked for. That shape is pinned by a test instead, and left for #225 to settle.

After this merges

CHANGELOG.md lives on the chore/release-2-0-0-beta-1 branch (#227) and lists all four of these under Known issues. Whichever merges second needs that section replaced by the fixes, and #225 announced as a wire change beside the two /api/v2 shape changes already there.

Checks

  • cargo fmt --all --check, cargo clippy --all-targets --all-features -- -D warnings — clean
  • cargo test --all-features — 289 passed (was 285)
  • bun --cwd=webapp run test — 135 passed (was 134)
  • CodeRabbit — two rounds, five findings then two; three taken, two answered above

Summary by CodeRabbit

  • Nouvelles fonctionnalités

    • Amélioration de la compatibilité Subsonic : les réponses getNowPlaying et getPlayQueue utilisent désormais le format entry et incluent l’identifiant utilisateur approprié.
    • Les réponses de streaming indiquent plus précisément les plages disponibles, y compris pour les transcodages dont la taille est inconnue.
    • Les crédits contenant des parenthèses sont découpés plus correctement, notamment avec des parenthèses imbriquées.
  • Corrections

    • Une ancienne session ne peut plus renouveler ou rejouer une requête après un changement de connexion.

Nothing here is a regression — all four shipped in `2.0.0-beta.0` — and all four
are cleared before the tag rather than carried into it.

**#224, a role separator cutting inside parentheses.** `split_on` applied its
separators in passes over the whole value, with no idea where it was. A
`COMPOSER` reading `Kobee (Melange / INHOUSE), Holy M (Melange / INHOUSE)` names
two people; the two slashes made three entities, each carrying a parenthesis it
never opened, and they reached the catalogue as artists that search answered
with. It is one pass now, counting parenthesis depth and cutting only at zero.
The rule the reference asks for is untouched: `Bach/Gounod` is still two people
and `AC/DC` is still one band. Depth is clamped at zero so a stray `)` cannot
make the rest of a value uncuttable, and a `(` that never closes protects only
the tail it opened.

**#225, `song` where the schema says `entry`.** `getPlayQueue` and
`getNowPlaying` named their children as if the rename that applies inside a
playlist did not apply to them. Three campaigns passed over it because every
client that met it was lenient; one that decodes against the schema reads an
empty queue. `playQueue` also gains the `username` the schema requires beside
`changed` and `changedBy`. This is an observable wire change, made deliberately
now: the freeze protects the clients that were validated, not a divergence from
the specification, and a beta is what it is for.

**#226, a 416 announcing a length of zero.** Two refusals wear that status and
they do not mean the same thing. A complete file knows its length, so it says
which range would have been satisfiable — and it accepts ranges, which every
other answer for that same file already says, where this one claimed `none`. A
transcode still being produced knows how many bytes it has written, never how
many it will; `bytes */0` did not say "unknown", it said "empty", which a client
may believe and never ask again. That header is now left out rather than filled
with a falsehood.

**#219, a refusal that outlived its session.** A sign-out and a sign-in fit
inside one round trip, so a 401 raised for one account could arrive after
another had signed in — renewing spent the new account's rotating refresh token
for a request the old one made, and the replay went out under the new account's
token. Five routes did this, not the four the issue names: the scan event stream
renews the same way and was missed. One `renewFor` now holds the shape, checked
before the renewal and again after it, because the renewal is itself a round
trip a sign-out can happen inside.

Every test here was run against the defect as well as against the fix. Three of
them fail with the parenthesis guard removed or made too broad; the contract
test fails on the rename and, separately, on `username` alone; the range
assertions fail on `bytes */0` and on the wrong `Accept-Ranges`; the session
test fails with the generation check neutralised, and it is the only one of the
nineteen that does, which is what makes it new coverage rather than a second
opinion.

Two review findings were not taken. Rechecking the generation a second time
between `renewFor` resolving and the replay closes a window one microtask wide
that nothing in the module can write to — every writer sits behind an await, and
a user gesture is a task, which cannot preempt a microtask drain; no scheduling
distinguishes it, which is this repository's own bar for keeping a guard. And
making the empty `getPlayQueue` schema-conforming would mean omitting the
element, a third wire change on the one call every client makes at startup, with
no observed harm and none of it asked for. That shape is pinned by a test
instead, and left for #225 to settle.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@github-actions github-actions Bot added scope: server Server core (Rust) scope: web Embedded web player (React) scope: docs Docs, README, assets scope: streaming Streaming, transcoding, FFmpeg scope: subsonic Subsonic / OpenSubsonic compatibility type: fix Bug fix size: m 50-200 lines labels Sep 16, 2026
@coderabbitai

coderabbitai Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 2f2e10ab-be4e-4856-8d0e-c071c4af8877

📥 Commits

Reviewing files that changed from the base of the PR and between 6884241 and ac2eaf5.

📒 Files selected for processing (9)
  • docs/subsonic-compatibility.md
  • src/media.rs
  • src/subsonic/protocol.rs
  • src/subsonic/userdata.rs
  • src/tags.rs
  • tests/media.rs
  • tests/subsonic_contract.rs
  • webapp/src/api.ts
  • webapp/src/session.test.ts

Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour.


📝 Walkthrough

Walkthrough

La modification corrige les réponses 416 des transcodages, les noms d’éléments Subsonic, le découpage des crédits entre parenthèses et les renouvellements de session après changement de compte. Des tests couvrent chaque comportement.

Changes

Réponses de plages média

Layer / File(s) Summary
Longueur des représentations et réponses 416
src/media.rs, tests/media.rs
MediaError::RangeNotSatisfiable accepte une longueur optionnelle. Les fichiers complets exposent Content-Range, tandis que les transcodages de longueur inconnue utilisent Accept-Ranges: none sans Content-Range.

Contrat Subsonic

Layer / File(s) Summary
Sérialisation des files et validation du contrat
src/subsonic/protocol.rs, src/subsonic/userdata.rs, tests/subsonic_contract.rs, docs/subsonic-compatibility.md
playQueue et nowPlaying utilisent entry au lieu de song. playQueue ajoute username lorsqu’une file existe. Les tests JSON et XML vérifient les files vides et alimentées.

Découpage des crédits

Layer / File(s) Summary
Analyse contextuelle des séparateurs
src/tags.rs
split_on ignore les séparateurs dans les parenthèses, gère les parenthèses imbriquées et conserve le découpage hors parenthèses. Les tests couvrent les entrées déséquilibrées.

Protection du renouvellement de session

Layer / File(s) Summary
Renouvellement conditionné par la génération
webapp/src/api.ts, webapp/src/session.test.ts
renewFor vérifie sessionGeneration avant et après le renouvellement. Les chemins API concernés n’effectuent plus de renouvellement ou de rejeu après un changement de session. Le test couvre une réponse 401 reçue après une reconnexion.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant API as api.ts
  participant Generation as sessionGeneration
  participant Refresh as renewFor
  participant Request as Requête 401
  API->>Generation: Capture la génération courante
  Request-->>API: Retourne 401
  API->>Generation: Vérifie la génération
  alt Génération inchangée
    API->>Refresh: renewFor(generation)
    Refresh->>Generation: Vérifie la génération après renouvellement
    Refresh-->>API: Session renouvelée
  else Génération modifiée
    API-->>Request: Rejette sans renouvellement ni rejeu
  end
Loading

Merge Risk: ⚪ Minimal · up to ac2ea

The protocol, streaming, credit parsing, and session-retry fixes are covered by aligned implementation and regression tests. No current merge-blocking risk is identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 25 functions across 8 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Le titre décrit clairement la correction des quatre défauts principaux traités par la pull request. Il est concis et directement lié aux changements.
Description check ✅ Passed La description est complète et couvre le résumé, les changements, les problèmes liés, les tests exécutés et les points de revue. Elle ne reprend pas exactement les rubriques et cases à cocher du modèl…
Linked Issues check ✅ Passed Les exigences de codage des quatre issues sont couvertes. Pour #224, split_on suit la profondeur des parenthèses et conserve le découpage hors parenthèses, avec des tests pour les cas équilibrés et …
Out of Scope Changes check ✅ Passed Les changements restent dans le périmètre des issues liées. Le changement de documentation décrit le nouvel état du contrat #225. Les modifications de code et les tests prennent en charge les objectif…
Full details: Docstring Coverage

Explanation

Docstring coverage is 56.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 25 functions across 8 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/four-defects-before-the-tag

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

@InstaZDLL InstaZDLL self-assigned this Sep 16, 2026
@github-actions github-actions Bot added type: fix Bug fix and removed type: fix Bug fix labels Sep 16, 2026
@InstaZDLL
InstaZDLL merged commit 9df1197 into main Sep 16, 2026
16 checks passed
@InstaZDLL
InstaZDLL deleted the fix/four-defects-before-the-tag branch September 16, 2026 16:57
InstaZDLL added a commit that referenced this pull request Sep 16, 2026
…nged the wire

#228 merged, so this branch was saying the opposite of the truth: it listed
#219, #224, #225 and #226 as known issues of the release that fixes them.

They move into **Fixed**, and #225 moves further than that. Renaming
`getPlayQueue` and `getNowPlaying` children from `song` to `entry`, and giving
`playQueue` the `username` the schema requires, is an observable change for
anyone already reading `song` — so it belongs beside the two `/api/v2` shape
changes, not buried in a list of corrections. The section is no longer about
`/api/v2` alone.

It also records what was deliberately not done: both names are not emitted
together, because a response carrying `entry` and `song` conforms to neither
contract and would need a second wire change later to undo. And `getPlayQueue`
with nothing saved still answers a bare `playQueue`, which is not
schema-conforming either — making it so means omitting the element, on the one
call every client makes at startup, and that is a decision for after the beta.

The client campaign is recorded as what it was: the whole replayed set, four
clients on three platforms, not the two this file named. Substreamer is said to
be outside the set rather than left to look forgotten. And the commit count is
361, not 354.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: docs Docs, README, assets scope: server Server core (Rust) scope: streaming Streaming, transcoding, FFmpeg scope: subsonic Subsonic / OpenSubsonic compatibility scope: web Embedded web player (React) size: m 50-200 lines type: fix Bug fix

Projects

None yet

1 participant