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:

MethodMeaning
pickaxegit log -S<token> on the defective token — the commit that introduced the exact string, then the earliest tag containing it. Strongest evidence.
featureThe 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.
absenceThe 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.

DefectPresent sinceFixed inShipped broken forHow dated
AudioStreamIndex=0 pinned the video stream as the audio track (DR-140)v0.0.1v0.4.6~7 weekspickaxe
Download URL spelled videoBitrate, which Jellyfin does not bind (DR-123)v0.0.1v0.5.1~7 weekspickaxe
pause_download / resume_download were no-ops (DR-168)v0.0.1v0.5.3~7.5 weekspickaxe
.part sidecar named by with_extension, so no cleanup path matched it (DR-169)v0.0.1v0.5.3~7.5 weekspickaxe
Range sent on every retry regardless of the response (DR-170)v0.0.1v0.5.3~7.5 weekspickaxe
/Items/Latest requested with the default GroupItems=falsev0.0.1v0.5.1~7 weekspickaxe
SubtitleStreamIndex omitted from PlaybackInfo, letting the server burn in (DR-176)v0.0.1v0.5.5~8 weekspickaxe
No PlaySessionId, and one hardcoded DeviceId, on every stream URL (DR-177)v0.0.1v0.5.5~8 weekspickaxe
download_item never recorded media_type; NULL read as 'audio' (DR-135)v0.0.1v0.4.6~7 weekspickaxe
download_album read its track list from the local cache (DR-173)v0.0.1v0.5.5~8 weekspickaxe
Device profile carried no MaxAudioChannels (DR-141)v0.0.1v0.4.6~7 weeksabsence
Streaming ceiling fixed at 20 Mbps with no way to lower it (UR-074)v0.0.1v0.5.3 (as a feature)~7.5 weekspickaxe
Hero banner auto-rotation never restarted after a manual swipe (DR-038)v0.0.1v0.9.1~8.5 weekspickaxe

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 videoBitrate casing was harmless while every download was original. 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 Range header was inert for the same reason: original is the one rung served with a Content-Length and 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 PlaySessionId only 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 SubtitleStreamIndex only 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

DefectPresent sinceFixed inHow dated
Native-path resume position never applied (both layers assumed the other seeked)v0.0.9/v0.0.10v0.5.1feature (PlayerAdapter contract)
get_downloaded_items matched "this library exists" rather than constraining the item to it (DR-167)v0.0.17v0.5.3feature (browsable downloaded library)
SCOPE_ITEM_TYPES — the frontend/backend boundary leak (DR-063)v0.0.17v0.2.1pickaxe
check:boundary anchored to the query site, blind to a named const (DR-094)v0.0.17v0.2.1feature (tripwire landed with the leak it missed)
Coverage gate divided by hardcoded denominators, reporting 158% (DR-093)v0.0.1v0.2.1pickaxe
Tap deferral raced the WebView's synthesized click (DR-092 → DR-098)v0.1.5v0.2.7feature (the deferral itself)
Transport for webview media decided from el.paused in the DOM (DR-097)v0.0.9/v0.0.10v0.2.7feature (Html5PlayerAdapter)
pick_current_episode rung 3 returned the first gap, not the furthest watchedv0.3.0v0.5.1feature
mirror_user_data mirrored is_favorite alone and returned early (DR-155)v0.4.0v0.5.1pickaxe
Stop-report path never fed the sync queue that existed for it (DR-154)v0.4.6v0.5.1feature (queue + drain landed with no producer)
Background-audio base applied in two display-only places (DR-159)v0.2.9v0.5.3pickaxe
Positions reported as 0 before the first tick, and always 0 for webview media (DR-178/179/180)v0.5.3v0.5.5feature (DR-159's tick boundary)
Length-less handoff transcode left to the player's own load-error retry, which can only restart it (DR-203)v0.0.16v0.8.2feature (the handoff's progressive-mp3 choice)

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_episodes accepted a series_id from 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)

DefectPresent sinceFixed inNote
experimentalNativeVideo defaulted on, shipping audio with a blank screen (DR-161 → DR-172)v0.5.3v0.5.4One 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.7v0.4.8The 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.0v0.5.3Caught before a broken APK shipped; no released build was un-installable.
Subtitle sidecar work reverted by a commit assembled from a stale treev0.5.5v0.5.5Never 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) and fa7cb6e9 (the local original). Both have the same parent 674c8e5c and the same diff. A merge chain pulled fa7cb6e9 and its follow-up 1e599627 into master's history during v0.5.5, so git log v0.5.4..v0.5.5 lists an autoplay fix that changed no file in that release — nextEpisodeService.ts is 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 log subjects 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:

  1. An omitted parameter is not a neutral default. SubtitleStreamIndex, AudioStreamIndex, GroupItems and MaxAudioChannels all 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.
  2. Silent binding failures. videoBitRate produced no error, no warning and a plausible-looking file. So did an unbound Range, and so did the coverage gate dividing by a stale denominator.
  3. 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.
  4. 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.