# Defect windows — which bugs were present when For each fixed defect, the releases it was actually present in. Companion to [CHANGELOG.md](../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` 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 `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 | 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_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) | 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) 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: ```bash git log --oneline --reverse -S'' -- src-tauri/src # introducing commit git tag --contains | 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.