CHANGELOG.md stopped at v0.5.0 and had gaps below it. Every tag from
v0.0.1 to v0.5.5 now has an entry, written from the commit bodies rather
than the subjects. Entries before v0.1.2 are shorter and marked as
reconstructed after the fact -- the commit messages of that era ("many
changes", "Playback fix") do not record causes.
docs/defect-windows.md is new: for each fixed defect, the releases it was
actually present in, with the evidence for the dating recorded per row so
a row can be disputed. Dated with `git log -S` on the defective token, not
by blaming the lines a fix removed -- that reliably lands on whatever last
touched the adjacent lines rather than on the defect's origin, and was
used only to shortlist.
Twelve defects date to the v0.0.1 proof of concept and shipped for seven
to eight weeks. They are not regressions but original assumptions nothing
exercised, four of them outright latent: the videoBitrate casing was
harmless until a quality picker existed to select against, and the
unconditional Range header was inert until that fix made transcoded
downloads actually transcode -- so DR-170's code dates to v0.0.1 while its
corruption window is the single release v0.5.1.
Three others are plumbing built and never connected: get_next_up_episodes
accepted a series_id with no caller until v0.3.0, the sync queue ran with
neither producer wired, and both watched-state backend halves sat unused.
No automated check sees these; the code is present, tested and reachable
in principle.
Also corrects the v0.5.5 entry. fa7cb6e9 and dcf08f30 are the same diff
off the same parent -- a local commit and its Gitea PR-merge twin -- and a
merge chain pulled the local one into master during v0.5.5. git log
v0.5.4..v0.5.5 therefore lists an autoplay fix that changed no file in the
release; nextEpisodeService.ts is byte-identical across the tag boundary.
That fix shipped in v0.0.2 and has not regressed. It is the one case where
reading the changelog off commit subjects would have produced a false
entry.
scripts/build-android.sh and src-tauri/src/repository/online.rs are also
modified in this tree by a concurrent session and are deliberately left
uncommitted.
10 KiB
Defect windows — which bugs were present when
For each fixed defect, the releases it was actually present in. Companion to CHANGELOG.md, which says what changed; this says how long each fault had been shipping before it did.
"Present since" is the first release containing the defective code, not the first release where a user could hit it — those differ, sometimes by months, and the gap is called out where it matters. "How dated" records the evidence, so a row can be re-checked or disputed:
| Method | Meaning |
|---|---|
pickaxe |
git log -S<token> on the defective token — the commit that introduced the exact string, then the earliest tag containing it. Strongest evidence. |
feature |
The defect is inseparable from a feature that landed whole (bad rung in a new algorithm, missing caller in new plumbing), dated to that feature's release. |
absence |
The fix adds something that was never there. Dated to when the surrounding code was built, since there is no introducing commit to find. Weakest — treat as "no later than". |
Present since the first release
Nine defects date to the initial proof of concept (v0.0.1, 2026-06-23) and shipped for between two weeks and seven weeks short of two months before anyone hit them. That is the dominant pattern here: not regressions, but original assumptions that went unexercised until a later feature leaned on them.
| Defect | Present since | Fixed in | Shipped broken for | How dated |
|---|---|---|---|---|
AudioStreamIndex=0 pinned the video stream as the audio track (DR-140) |
v0.0.1 | v0.4.6 | ~7 weeks | pickaxe |
Download URL spelled videoBitrate, which Jellyfin does not bind (DR-123) |
v0.0.1 | v0.5.1 | ~7 weeks | pickaxe |
pause_download / resume_download were no-ops (DR-168) |
v0.0.1 | v0.5.3 | ~7.5 weeks | pickaxe |
.part sidecar named by with_extension, so no cleanup path matched it (DR-169) |
v0.0.1 | v0.5.3 | ~7.5 weeks | pickaxe |
Range sent on every retry regardless of the response (DR-170) |
v0.0.1 | v0.5.3 | ~7.5 weeks | pickaxe |
/Items/Latest requested with the default GroupItems=false |
v0.0.1 | v0.5.1 | ~7 weeks | pickaxe |
SubtitleStreamIndex omitted from PlaybackInfo, letting the server burn in (DR-176) |
v0.0.1 | v0.5.5 | ~8 weeks | pickaxe |
No PlaySessionId, and one hardcoded DeviceId, on every stream URL (DR-177) |
v0.0.1 | v0.5.5 | ~8 weeks | pickaxe |
download_item never recorded media_type; NULL read as 'audio' (DR-135) |
v0.0.1 | v0.4.6 | ~7 weeks | pickaxe |
download_album read its track list from the local cache (DR-173) |
v0.0.1 | v0.5.5 | ~8 weeks | pickaxe |
Device profile carried no MaxAudioChannels (DR-141) |
v0.0.1 | v0.4.6 | ~7 weeks | absence |
| Streaming ceiling fixed at 20 Mbps with no way to lower it (UR-074) | v0.0.1 | v0.5.3 (as a feature) | ~7.5 weeks | pickaxe |
Why they took so long to surface
Four of these were latent until a later feature exercised them, which is why the fix lands so far from the cause:
- The
videoBitratecasing was harmless while every download wasoriginal. It became visible only once a quality picker existed to select against — and then produced no error, just a full-size file, because Jellyfin discards an unbound query key silently. - The unconditional
Rangeheader was inert for the same reason:originalis the one rung served with aContent-Lengthand real byte-range support. It started corrupting files in v0.5.1, the moment the casing fix made transcoded downloads actually transcode. So the code dates to v0.0.1 and the corruption to v0.5.1 — a one-release window for the visible symptom. - The missing
PlaySessionIdonly bites when a stream is re-opened for the same item. Nothing re-opened one until quality switching, transcoded seek and audio-track switching existed. - The omitted
SubtitleStreamIndexonly bites on sources whose own default subtitle track is image-based, since that is what forces the server from sidecar to burn-in.
Two were masked by soft failure: the asset protocol being disabled (DR-134)
was hidden by the thumbnail cache falling back to the server copy whenever the
server was reachable, and AudioStreamIndex=0 was hidden by servers that
silently correct an out-of-range index — which is exactly why it was reported as
"some videos have no audio" rather than as a bug in the client.
Introduced by a feature, fixed later
| Defect | Present since | Fixed in | How dated |
|---|---|---|---|
| Native-path resume position never applied (both layers assumed the other seeked) | v0.0.9/v0.0.10 | v0.5.1 | feature (PlayerAdapter contract) |
get_downloaded_items matched "this library exists" rather than constraining the item to it (DR-167) |
v0.0.17 | v0.5.3 | feature (browsable downloaded library) |
SCOPE_ITEM_TYPES — the frontend/backend boundary leak (DR-063) |
v0.0.17 | v0.2.1 | pickaxe |
check:boundary anchored to the query site, blind to a named const (DR-094) |
v0.0.17 | v0.2.1 | feature (tripwire landed with the leak it missed) |
| Coverage gate divided by hardcoded denominators, reporting 158% (DR-093) | v0.0.1 | v0.2.1 | pickaxe |
| Tap deferral raced the WebView's synthesized click (DR-092 → DR-098) | v0.1.5 | v0.2.7 | feature (the deferral itself) |
Transport for webview media decided from el.paused in the DOM (DR-097) |
v0.0.9/v0.0.10 | v0.2.7 | feature (Html5PlayerAdapter) |
pick_current_episode rung 3 returned the first gap, not the furthest watched |
v0.3.0 | v0.5.1 | feature |
mirror_user_data mirrored is_favorite alone and returned early (DR-155) |
v0.4.0 | v0.5.1 | pickaxe |
| Stop-report path never fed the sync queue that existed for it (DR-154) | v0.4.6 | v0.5.1 | feature (queue + drain landed with no producer) |
| Background-audio base applied in two display-only places (DR-159) | v0.2.9 | v0.5.3 | pickaxe |
| Positions reported as 0 before the first tick, and always 0 for webview media (DR-178/179/180) | v0.5.3 | v0.5.5 | feature (DR-159's tick boundary) |
Three of these are worth separating out, because the defect is not a mistake in the code so much as plumbing that was built and never connected:
repository_get_next_up_episodesaccepted aseries_idfrom the day it was written, and no caller passed one until v0.3.0.- The sync queue and its drain were built, tested and running in v0.4.6 with neither of its two would-be producers ever called.
- Both halves of the watched-state backend existed with no caller before v0.5.3.
An automated check cannot see any of these — the code is present, tested and reachable in principle. Only tracing a requirement to a call site catches it.
Short windows (one release or less)
| Defect | Present since | Fixed in | Note |
|---|---|---|---|
experimentalNativeVideo defaulted on, shipping audio with a blank screen (DR-161 → DR-172) |
v0.5.3 | v0.5.4 | One release. The decode path was fine; the compositing step never ran. |
| Webview-shaped audio profile insufficient — server ignores a profile's audio codec (DR-149) | v0.4.7 | v0.4.8 | The v0.4.7 fix for DR-148 was necessary and not sufficient. |
Android versionCode floor went stale (minor*100 yielding less than the 5002 already in the field) |
v0.5.0 | v0.5.3 | Caught before a broken APK shipped; no released build was un-installable. |
| Subtitle sidecar work reverted by a commit assembled from a stale tree | v0.5.5 | v0.5.5 | Never released broken — both commits are in v0.5.5. |
Fixed twice / never actually broken
-
Autoplay time reset (v0.0.2). Two commit objects carry this identical change:
dcf08f30(merged via Gitea PR #3, tagged v0.0.2) andfa7cb6e9(the local original). Both have the same parent674c8e5cand the same diff. A merge chain pulledfa7cb6e9and its follow-up1e599627into master's history during v0.5.5, sogit log v0.5.4..v0.5.5lists an autoplay fix that changed no file in that release —nextEpisodeService.tsis byte-identical across the tag boundary. The fix shipped in v0.0.2 and has not regressed.This is the one case where reading the changelog off
git logsubjects would have produced a false entry, and it is a good argument for the project's practice of deriving release notes from TRACES rather than commit subjects.
Recurring shapes
Four causes account for most of the table:
- An omitted parameter is not a neutral default.
SubtitleStreamIndex,AudioStreamIndex,GroupItemsandMaxAudioChannelsall had a server-side default that was actively wrong, and in three of the four the server's choice was more expensive than the one intended — burn-in forcing a full re-encode being the extreme case. - Silent binding failures.
videoBitRateproduced no error, no warning and a plausible-looking file. So did an unboundRange, and so did the coverage gate dividing by a stale denominator. - Two layers each assuming the other acts. Native resume (adapter recorded the position, backend never seeked), end-of-playback dispatch (two paths, one unreachable), and the surface/attach split in v0.5.0's native video.
- A guard keyed on state that moves. The tap deferral keyed suppression on a
timer handle the callback had already cleared; the HTML5 toggle keyed
play-vs-pause on
el.paused, which flips while buffering.
Reproducing this
The pickaxe rows can be re-derived directly:
git log --oneline --reverse -S'<defective token>' -- src-tauri/src # introducing commit
git tag --contains <sha> | sort -V | head -1 # first release with it
Blaming the lines a fix removed (git blame at the fix's parent) is faster to run
across many commits but was not used for the rows above: it reliably lands on
whichever commit last touched the adjacent lines, which is usually not the commit
that introduced the defect. It was used only to shortlist candidates.