# Changelog All notable changes to Kima will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] - nightly ## [1.9.0] - 2026-06-15 A reliability release built around a ground-up rewrite of the audio playback engine. After an end-to-end audit of the player turned up 30 issues across the frontend and backend, the tangle of overlapping recovery mechanisms was replaced with a single, predictable engine; audiobooks became proper sessions; and a long-dead podcast auto-refresh was brought back. Several settings that looked functional but did nothing now actually work. ### Changed - **The audio player is now driven by one state machine instead of four competing recovery mechanisms.** The old controller had a 3-second stall watchdog, a 10-second stall grace, a network-retry loop, and a reload-on-error path all running at once with inconsistent thresholds. They are replaced by a single engine with one recovery ladder: every wait has a deadline, every state has a way out, and the play/pause button is never disabled -- so the "endless spinner you can't even pause" state is no longer possible. iOS's noisy `stalled` event (which fired over a thousand times during healthy playback in a real trace) is now ignored entirely. - **Audiobooks are handled as sessions, not loose URLs.** All book-time math goes through one place backed by a verified per-file track map, so a multi-file book can no longer seek to the wrong file or mark itself finished partway through. ### Fixed - **Multi-file audiobooks could wipe your place and mark themselves finished.** Started from the series or list page, a multi-file book had no file map, so it seeked the first file to a book-absolute position, hit the end, and the player treated that as "book complete" -- overwriting saved progress. The track map is now cached server-side and carried on every surface, and a book is never marked finished without a confirmed last file. - **Chapter taps did nothing when the book wasn't already playing.** Tapping a chapter started playback and then seeked against state that hadn't committed yet, so the seek was discarded. The chapter position now rides the load itself. - **Podcasts stopped pulling new episodes.** A corrupted background job left a stuck marker that silently blocked every future refresh; the refresh button appeared to work but couldn't dislodge it. Failed jobs are now cleared so a refresh can recover, and a refresh that fails backs off instead of retrying in a tight loop. The same dedup trap was fixed on the artist, track, and embedding queues. - **Pausing from the lock screen could un-pause itself when you reopened the app.** A lock-screen pause wasn't recorded as a deliberate pause, so the next time the app came to the foreground it resumed on its own (and could play through the speaker if earbuds had been removed). Lock-screen pause is now a real pause and is respected. - **A long-idle session could stop playing after about a day.** The stream token expired and the next request failed silently; the player now refreshes the token before it expires and recovers a stream at its saved position instead of tearing down. A transient network blip no longer logs you out. - **Subsonic "star" could silently fail.** A third-party app could star a track that never saved because every error was swallowed and success was reported regardless. Genuine failures now surface; missing tracks are skipped; partial successes are kept. - **"Clear Caches" did nothing.** The handler used the wrong Redis client API and threw on every call. It now works and clears all rebuildable caches while leaving queue and control-plane state untouched. ### Added - **Settings that used to be inert now work.** The transcode-cache-size slider is honored on restart, and the "Auto sync library" toggle now drives a periodic library scan (every 6 hours) so music added outside the download pipeline gets picked up automatically. ### Backend - Stream lifecycle hardening: removed a per-user stream cap that could truncate the track you were listening to; added server socket timeouts so dead mobile connections can't accumulate; bounded the Audiobookshelf proxy with a real timeout and cached its track lookups so seeks stop paying a round-trip each; ran transcodes through a real concurrency queue with a kill-switch; aligned HTTP range handling (416 vs full-file) across the music, audiobook, podcast, share, and Subsonic routes; and shortened the year-long cache header so a replaced file isn't served stale. ## [1.8.2] - 2026-06-10 A playback-reliability release: faster track starts, audiobook progress that survives backgrounding and updates, and a round of audiobook and podcast fixes. ### Fixed - **iOS audiobooks lost their place after an app update or a long screen-off session**: progress was saved off the `timeupdate` event, which iOS throttles when the PWA is backgrounded, so a screen-off listening session was never checkpointed and reverted to where the screen was locked. Progress now saves on a 15-second wall-clock timer while playing, independent of the throttled event. - **An iOS interruption (call/notification) could leave audio paused, and audio could restart through the speaker after pulling earbuds**: an auto-resume-after-interruption attempt was removed after a device trace proved its safeguard could not work (iOS never emits the route-change event the guard relied on, so it could not tell an earbud unplug from an interruption ending). The audio session is still re-claimed when an interruption ends, but playback is never auto-restarted, so the earbud-to-speaker case cannot occur. - **Slow track starts**: the streaming route did play-history logging and a settings lookup before sending the first byte. Those now run in parallel and in the background, so playback starts sooner. Default quality with no settings row is now `original` (no transcode) to match the schema default. - **Audiobook seek past the end of a file returned a 500**: a seek using a stale file size made Audiobookshelf return 416, which surfaced as a server error. It now returns a clean 416 instead of piping an error body to the player. - **Mis-cataloged Audiobookshelf libraries imported as a single multi-thousand-hour "audiobook"**: sync now skips items with more than 1000 audio files, which are libraries mistaken for one book (they broke seeking and the player). - **Podcast detail page stuck on an infinite loading spinner (#168)**: a slow or unreachable feed hung the preview request with no error. The preview is now time-bounded on both ends, so a slow feed returns partial data and a dead feed surfaces an error instead of spinning forever. ## [1.8.1] - 2026-06-05 ### Added - **Optional slskd backend for Soulseek (#205, by gossip31)**: a `soulseekMode` setting routes Soulseek through an external [slskd](https://github.com/slskd/slskd) instance instead of the built-in client. slskd holds its own Soulseek account, so this mode needs no Kima-side username or password. The built-in client stays the default and is unchanged. ### Fixed - **Audio analyzer crash-loop on corrupt files (#204, by gossip31)**: a handful of malformed files take an uncatchable native crash inside Essentia's decoder, which kills the worker before the failure can be counted, so those files looped forever and each crash left a large core dump behind. An ffmpeg integrity probe now screens files before Essentia decodes them, so a bad one fails cleanly and quarantines after the normal retry limit. As a backstop, the stale-processing recovery counts a worker crash as a retry, so any crash the probe can't predict still quarantines instead of looping. ## [1.8.0] - 2026-06-04 iOS playback reliability rebuilt from a stable baseline, Vibe search hardened against Redis pressure, the first phase of the Discover Weekly correctness rework, and a dependency pin that unbreaks the build. ### Added - **iOS audio diagnostics**: the installed iOS PWA keeps a small rolling log of audio events on your own server to make playback bug reports actionable. Bounded and on-device; groundwork for a forthcoming general support-bundle export. ### Fixed - **iOS earbud / lock-screen resume produced no audio and lost the session to another app**: three successive patches on the iOS AudioContext backgrounding bridge had compounded into a regression -- the worst awaited the context resume before `audio.play()` and returned early on a non-running context, so an earbud/lock-screen resume did nothing and, after a few attempts, iOS handed the audio session to the next app (a sleep-sounds app would start playing). The three patches were reverted to the proven bridge baseline (resume the context in parallel, always attempt `audio.play()`, gesture preserved), and the real gaps were fixed on top: the "playback" audio-session category is re-claimed on every explicit resume (not just the first), so iOS can't leave it with an app that grabbed it during an interruption, and an AudioContext `statechange` listener re-claims it when the OS ends an interruption. No background auto-resume was added, so the v1.7.12 earbud-unplug-through-speaker regression stays fixed. Installed iOS PWA only. - **Vibe text search crashed with "Stream isn't writeable" under load (#197)**: the text-embedding Redis bridge inherited the shared client's fail-fast options (`enableOfflineQueue:false`), so it threw whenever a connection was mid-reconnect, and a rejected promise was cached -- breaking Vibe search until restart. The first fix hardened only the subscriber; this hardens the whole path: the publisher uses its own buffered connection, dead connections are cleaned up instead of leaking or silently dropping responses, subscribe and publish are time-bounded so an unreachable Redis fails fast instead of hanging, analyzer failures are rejected cleanly instead of 500ing, and the analyzer reports internal errors immediately. - **Release build broke on a `transformers` update**: `transformers` was unpinned and resolved to a version that needs a newer `torch` than the pinned `torch==2.5.1` (it references `torch.float8_e8m0fnu`, added in torch 2.7), failing the analyzer image build. Pinned to `transformers==5.8.1`, the version proven against torch 2.5.1. - **Discover Weekly never showed (and silently auto-deleted) the generated playlist -- Phase 1 of the rework**: the Sunday cron tagged records with the *ending* week, so `GET /current` (which looked for the current week with exact equality) found nothing, and the next run's cleanup then deleted the invisible records. This phase fixes the core correctness path (no schema migration): generation now tags the upcoming week via a centralized week helper; the cron moves to Monday 05:00 with a dedup key aligned to the batch week; `/current` resolves from the latest completed batch (bounded, with a `stale` flag) so drifted records are visible; `buildFinalPlaylist` marks the batch failed on a transaction error instead of hanging in `scanning`; retry-unavailable routes through the shared completion flow so retried albums actually enter the playlist; cancelling a batch marks it `failed` (not a false "successful empty week"); album deletes are wrapped in transactions with an atomic claim guard so a concurrent "like" can't lose its album (and `/like` is symmetric); a same-`rgMbid` `LIBRARY` album is never deleted; and the BullMQ job hash is cleared before re-enqueue so a retry after a failure isn't silently dropped. `TZ=UTC` is pinned for deterministic week math. ## [1.7.15] - 2026-06-01 A frontend quality and UX overhaul (accessibility, theming, and UX refinements) plus two playback fixes. ### Added - **Collection "Refine" panel**: filtering, sorting, and items-per-page are consolidated into one popover, with clearer "In your library" vs "Recommended" labels. - **Mobile vibe track operations**: match-vibe, find-similar, and Song Path are now reachable on touch via a panel sheet; the vibe map gained a first-run hint, and "Drift" was renamed "Song Path" with outcome-focused tooltips. - **Discover Weekly clarity**: a clearer generate CTA, disk-usage disclosure before generation, and two-phase progress. - **Safer destructive settings**: cache/enrichment resets now require a confirmation dialog; maintenance actions are grouped and section labels corrected. - **Onboarding**: per-integration test/result state and clearer admin-account vs integration copy. - **Playlist virtualization**: large playlists render a windowed subset of rows (load-tested ~33,700 -> ~1,260 DOM nodes for a 1,000-track playlist) for smooth scrolling. - **Mini-player gesture hint**: a one-time hint explains the swipe gestures (behavior unchanged). ### Fixed - **iOS playback stopping after almost every song when backgrounded**: the silent-playback watchdog (added in v1.7.13) was armed on every automatic track transition and judged "silent" purely from whether a `timeupdate` event arrived within 2.5s. iOS throttles that event when the installed PWA is backgrounded, so after most songs the watchdog wrongly paused healthy playback and surfaced a "Tap play to resume" error. It now judges liveness by `audio.currentTime` advancement, never tears down while the document is hidden, and only the genuine deep-suspension case (suspended/interrupted AudioContext) still prompts -- restored via an explicit foreground check. Installed iOS PWA only. - **Desktop Discover settings / lyrics not opening in the rebuilt sidebar**: the desktop `UnifiedPanel` never read the externally-registered settings content, so the Discover settings gear (and lyrics) opened the panel to the activity feed instead of the settings. The panel now renders the registered content and returns to the feed on collapse. Pre-existing since the sidebar rewrite. - **Keyboard focus squaring off rounded controls**: the global `:focus-visible` rule set `border-radius` on the focused element (not the outline), distorting circular and rounded buttons/modals on keyboard focus; removed (browsers already round the outline via `outline-offset`). - **Library search silently capped at 10 results**; now renders all matches. - **Real-bug batch**: an invisible refresh button (invalid color class), a dead `?tab=` link parameter, a non-functional queue drag handle, and several silently-swallowed errors (radio, podcasts) now surface. - **Playlist scroll correctness**: the virtualizer offset is measured against the real scroll container and re-measured on reflow. - `logout` return type corrected to `Promise`; `aria-expanded` now stays in sync across every panel close path; removed a nested-interactive ARIA role on the mini-player. ### Changed - **Accessibility (WCAG AA)**: muted text raised to 4.81:1 contrast, a global brand focus ring restored on keyboard navigation app-wide, ARIA labels/landmarks across remaining routes, and 44px minimum touch targets. - **Brand color consolidated** to the official amber (`#fca200`); the legacy green accent was removed. - **Internal theming**: hard-coded hex colors migrated to design tokens, and redundant tokens that merely aliased Tailwind values were dropped in favor of Tailwind utilities (no visual change). ## [1.7.14] - 2026-05-27 ### Fixed - **Podcast refresh silently stuck (#81)**: BullMQ retains completed-job hashes, so re-adding a refresh job with the same id was silently deduplicated and dropped. Completed jobs are now cleaned before re-queue, so podcast refresh works repeatedly. ## [1.7.13] - 2026-05-14 iOS audio reliability overhaul plus audiobook, Soulseek, and podcast fixes. ### Fixed - **iOS audio survival and resume**: the audio element is bridged through an AudioContext to survive backgrounding; the next track's source is swapped synchronously on track-end to preserve the iOS autoplay grant; foreground resume uses a `wasPlaying` flag; AudioContext resume is awaited with silent-playback detection; and audio route-change pauses are now observable. Restored the standalone PWA on iOS so Safari chrome stops covering the UI. - **Audiobook chapter ordering (#184)**: chapters/tracks are resolved by logical chapter number and sorted defensively by offset; sync failures now show the backend's actual error message. - **Soulseek download errors (#192)**: errors propagate to the awaiting promise instead of crashing the backend; service-layer error listeners carry file context. - **Podcasts (#168)**: an error UI is rendered instead of an infinite spinner, and state resets on navigation so errors don't stick across podcasts. ### Added - **iOS audio forensics** (opt-in via `?ios_debug=1`): an audio-event ring buffer, MediaSession and lifecycle instrumentation, and a `/debug/ios-log` viewer with a backend archival endpoint. ### Changed - Pinned Next.js to `^16.2.2` to match the container's resolved version. ## [1.7.12] - 2026-04-16 ### Added - **Persist UMAP map positions to the database**: The vibe galaxy map previously cached its UMAP projection in Redis with a 24h TTL; on every expiry (or container restart) the worker recomputed the full projection, taking ~30s on an 8k-track library. Positions are deterministic once the embedding set is fixed, so they now live in `track_embeddings.map_x` / `map_y`. The map loader tries Redis first, then hydrates from the DB when at least 98% of embeddings have persisted positions (uncovered tracks get patched by `appendTrackToProjection` as they analyze), and only falls back to full UMAP recompute when coverage drops below that threshold. Cold-Redis hydrate is a single indexed join (~100ms) instead of a 30s worker run. The migration is a metadata-only `ADD COLUMN` on PG 11+, zero downtime, safe for `docker compose pull` on existing deployments. - **Deezer as fallback source for podcast search**: iTunes Search API had an outage that broke all podcast discovery. The `/podcasts/discover/*` and `/search?type=podcasts` endpoints now fan out to both iTunes and Deezer in parallel via `Promise.allSettled` with title-based dedupe. Deezer results fill gaps when iTunes is down; iTunes results are preferred when available (they carry the `feedUrl` needed for subscription). The preview endpoint can resolve Deezer-only podcasts by looking up the feed URL via iTunes name match. ### Fixed - **iOS earbud disconnect routing audio to the phone speaker**: The 1-second auto-resume in the audio-controller pause handler (added in v1.7.7) could not distinguish a system pause worth resuming (notification) from one that is not (audio route change when earbuds unplug or Bluetooth disconnects) -- iOS fires an identical pause event for both. When the timer fired `play()` after a route change, iOS routed audio to the loudspeaker, startling the user mid-listen. The original Control Center pause-then-play bug that the auto-resume targeted remained reproducible, so the timer was providing no benefit while causing this regression. Removed the timer and all interruption-flag state; the Media Session play action handler in `useMediaSession` already covers the Control Center path with a `reloadAndPlay` fallback gated on a user gesture, which is the only safe way to resume on iOS. Network retry, stall watchdog, `AbortError` -> reload, `NotAllowedError` -> needs-resume prompt, and visibility/pageshow foreground resume are all preserved. - **Vibe galaxy showing a black screen on load**: A single `kima_galaxy_camera` `sessionStorage` key was shared across the 2D (orthographic) and 3D (perspective) viewing modes. Restoring a 3D flight position + `zoom = 1` (the `PerspectiveCamera` default) into the `OrthographicCamera` on the next mount placed the ortho view far from the points with no zoom, producing a black screen. Each mode now has its own storage key and only restores state captured in that same mode. The legacy single-key entry is removed on mount so any user stuck in the bad state unsticks automatically without needing a cache clear. - **Podcast previews when iTunes resolves via Deezer**: The preview handler now accepts Deezer-only results and resolves their feed URL through an iTunes name lookup, so subscriptions still work when the primary source was Deezer. ### Changed - **Removed the PodcastIndex scaffold**: The PodcastIndex service was referencing `SystemSettings` columns that do not exist in the current Prisma schema -- it would have thrown on first call. No production caller invoked it except the now-removed reset hook in `systemSettings`. Dropped the service, its only cache-reset call site, and the `podcast-index-api` dependency (last published 2021). ## [1.7.11] - 2026-04-08 ### Fixed - **Docker build failure on linux/arm64 (v1.7.10 hotfix)**: The torch/torchaudio/torchvision pins added in v1.7.10 (PR #178) used the `+cpu` local version suffix, which exists on the pytorch CPU index for amd64 but not for arm64 at torch < 2.6.0. The arm64 build step 7/41 failed with `ERROR: Could not find a version that satisfies the requirement torch==2.5.1+cpu`. Dropped the `+cpu` suffix -- PEP 440 matches versions without a local tag against any local variant, so the same pin resolves to `2.5.1+cpu` on amd64 and `2.5.1` on arm64. - **CLAP model not loading (#165, properly this time)**: The v1.7.6 fix closed this issue but was actually dead code -- it edited `services/audio-analyzer-clap/requirements.txt`, but the main Dockerfile only copies `analyzer.py` from that directory and never installs from requirements.txt. CLAP was broken in every release from when the feature shipped until v1.7.10's pins landed (which would have fixed it if the arm64 build had not failed). Root cause: unpinned `torch torchaudio torchvision` let pip's resolver install mismatched versions (torch 2.5.1+cpu paired with torchaudio 2.11.0+cpu -- ABI-incompatible), and unpinned scipy/pandas were installed at latest versions needing numpy>=1.26, which tensorflow-cpu 2.13 then downgraded to 1.24.3, breaking scipy's `from numpy.exceptions import AxisError`. Fixed by the torch+cpu suffix correction above plus PR #178's (now-effective) numpy/scipy/pandas pins. Verified by reproducing the full CLAP + transformers + laion_clap + tensorflow + essentia import chain in the published v1.7.9 image and confirming the fix resolves all errors. ## [1.7.10] - 2026-04-07 ### Added - **Disc numbers and subtitles for multi-disc albums (#157, #170)**: The scanner now reads `disk.no` and `discsubtitle` tags from file metadata. Album views group tracks by disc with a header when multiple discs are present, and show compact `1-05` / `2-08` track labels for multi-disc releases. Single-disc albums are unchanged. Subsonic clients receive the `discNumber` attribute on songs and tracks are ordered by `[disc, trackNo]` across every code path (library, offline, share, and all Subsonic endpoints). A new additive migration adds two nullable columns (`Track.discNumber`, `Track.discSubtitle`) -- no rescan is required, but rescanning a library picks up the new fields. Thanks @loskutov. ### Fixed - **Deezer preview CORS/ORB in hardened browsers (#173, #178)**: Previews were failing in LibreWolf, hardened Firefox, and Brave because Deezer's CDN doesn't send cross-origin headers and strict browsers block the direct `