Skip to content

13.14.4

Choose a tag to compare

@github-actions github-actions released this 17 Sep 23:34

Changelog

Changes in v13.14.3..HEAD:

Highlights

  • The Test Button Tests What Actually Plays — it asked the relay, which is a separate, optional credential and is correctly inactive while the engine's own per-source sessions do the work. So with every source verified, playing and downloading, Test answered "Cloudflare verification required" for all of them. It now runs the engine's own path and reports the extension's verdict — verdict=ok detail=online for deezer, amazon, tidal, qobuz — and a genuinely failing check is named instead of being flattened into a generic failure. (Hush)
  • Sessions Keep Themselves Alive — measured on device: the gateway issues a 6-hour session per source and refreshes it from install_id, signed with the record's own secret, with no Turnstile. The background renewer ran every 3 hours while the renewal window was also 3 hours, so a session got exactly one attempt inside the window — and WorkManager defers freely under doze, so one missed run meant a lapsed session and a manual check for a user who did nothing wrong. The renewer is now hourly, uses UPDATE so an existing install actually moves off the old cadence, and playback renews whatever is due before a resolve. (Hush)
  • One Verification, Stored Once, Seen Everywhere — a check solved in the browser, in the overlay, in Audio Sources or through the grant deep link lands in the same per-source record; the source rows, the Authentication card and the app-open check all read that, so a source with 5h53m left never asks again. On a device whose WebView is older than Cloudflare supports — a car head unit — the browser is not a fallback but the only route offered, and the grant returns by itself: no code to copy. (Hush)
  • Downloads Are Named by Their Bytes — the SpotiFLAC download path hardcoded .flac on the assumption that its container "is FLAC by definition". When a source has no lossless match it answers with a lossy container instead, so an amazon fallback arrived as MP4/AAC stamped .flac — a file every player reads as corrupt, and the likely cause of "it says FLAC but there is no audio". The extension now comes from the file's own header. (Hush)
  • A Download Already in Your Folder Is Used — the download index does not survive a reinstall, a data clear or a restore onto another device, but the files do. A track whose file the wired downloads folder already holds is adopted and played from disk instead of being fetched again. (Hush)

SpotiFLAC Sources

  • The engine is what Test consults — SpotiFLACNativeRuntimeBridge.testSource checks the extension package loads, that the source holds no pending challenge of its own, and calls the extension's health probe, whose payload is the source's real verdict. The runtime's isExtensionAuthenticatedByID is deliberately not used: it answers false for sources that are verified, playing and downloading on this device (the runtime marks one authenticated inside its download preflight, not before it), so gating on it reproduced the very bug it was meant to fix. (Hush)
  • A failing check is reported as itself — an unhealthy entry in the probe's checks array is named (<label> is <status>), and an error in a payload that still answered is a failure, because the engine did answer and the answer was no. The row keeps its one line: 160 characters, clipped. (Hush)
  • Renewal is scheduled well inside the window — BACKGROUND_INTERVAL_MINUTES (60) and BACKGROUND_FLEX_MINUTES (15) live beside the window they must beat, and a test asserts the interval stays at or below a third of it, so nobody can quietly set the period back to the window length. The UPDATE policy matters on its own: with KEEP, every existing install would have kept the old three-hour request forever. (Hush)
  • Playback keeps its own sessions alive — SpotiFLACPlaybackResolver.resolve refreshes what is due before it sweeps, because a renewal is only accepted while the session is still valid, and playing is exactly when the app is in use. Nothing due costs a few small file reads and no request. (Hush)
  • Audio Sources no longer re-warms the runtime forever — the screen's effect was keyed on the source rows, and warming the runtime can end in the verifier recording a result, which writes test state back into those same rows: the effect re-keyed itself, warmed again, and repeated every ~1.5s for as long as the screen was open. Measured: prewarm done ×14 in 11s before, ×2 in 22s now. (Hush)
  • The extension load report is summarised — the runtime lists an extension it skipped because that exact version was already loaded in the same errors array it uses for real failures, so every start wrote a blob whose first entries looked like errors, truncated mid-word at 200 characters. It now reads loadExtensionsFromDir: loaded=8 skipped=8 errors=0, with real failures still shown in full and never truncated away. (Hush)

Verification

  • The Authentication card reports the engine, not the relay — it showed the legacy relay session's state while every source was verified, which is what told users to open a browser for a check they had already passed. It now reports the per-source sessions, and the relay is labelled as what it is. (Hush)
  • Verification from any surface is one report — SpotiFLAutoVerifier.notifyVerified is the single entry point that marks the source usable, bumps the ticker that wakes a parked track, clears the notification and mirrors the session into the durable vault. (Hush)
  • The vault write no longer blocks the UI thread — it is reached by every surface that reports a verification, and it listed the extensions directory and read a record before writing. The bookkeeping stays synchronous (a parked track's wake-up must be immediate); the mirroring runs on a process-wide IO scope. (Hush)
  • Validity is read off the main thread — refreshSessionValidity walks the extensions directory and reads one manifest plus one session record per source, and it is called from composition on every verification tick. On a head unit with slow storage that is a stall, not a slow function. (Hush)

Downloads and Storage

  • Removal clears the song, not just the index — removing a download now deletes its SpotiFLAC playback file as well. A cache left behind answered the next resolve with the bytes the user had just deleted — which is why a re-download appeared to finish before it fetched anything — and kept serving a resolve from the old, possibly lower-quality file. Verified on device: step=remove-download verdict=ok record=false cachedCopy=false, with an unrelated cached track untouched. (Hush)
  • One container per file, decided by the header — DownloadNaming.extensionForFile reads fLaC, ftyp, EBML, OggS, RIFF/WAVE, ID3 and ADTS before falling back to a known extension, and never to .flac. Verified on device: re-downloading the same track produced .m4a and deleted the mislabelled .flac instead of leaving both. (Hush)
  • A release build cannot ship a hand-written extension — verifyDownloadNaming, wired into every assemble* the way the NewApi guard is, fails the build when an extension is handed to the naming path as a string literal or as an identifier whose initialiser in the same file is a literal (private const val FLAC_EXTENSION = "flac" — the exact shipped bug). Proven by reinstating the bug and watching the task fail with the file and line, then reverting. The rule is provenance, not vocabulary: a new container needs no change, a guess cannot be added silently. (Hush)
  • The cache holds the song cache, in the wired location — cached songs live under the Storage screen's song-cache folder (exoplayer/spotiflac-playback/), one file per song with the container the source served, and follow a folder change. The Storage screen reports that share separately from the rest of the song cache, and the two no longer count the same bytes twice. (Hush)
  • Downloaded tracks are served straight off disk — before the caches and independent of which engines are switched on, which is what downloading was for; the source label is published on that path too, so the player does not fall back to guessing. (Hush)

Release Engineering

  • A shell-driven seam for the flows that need a device — the debug-only SpotiFLACDebugReceiver gained download, remove-download, source-row, source-test and renew, and hush://route?to=<route> opens a settings screen from a shell, so a head unit nobody is holding can still be tested. Both exist in nothing that ships, and the debug route needs an explicit exported attribute — which lint caught, which is the gate doing its job. (Hush)
  • Bundled bridges rebuilt — waze-shims.zip regenerated from the current shim sources, so the archive Hush installs and repairs from is byte-for-byte the one the build produces. (Hush)

Housekeeping

  • Version bumped to 13.14.4 (versionCode 174). The Waze shims derive their version from the app's, so they move with it. (Hush)
  • README updated: SpotiFLAC now appears in the loot table and the upstream shoutout, with a SpotiFLAC sources (lossless) section covering the registry-provided sources, the one-time verification and background renewal, the browser route for a car head unit, and where downloads and cached songs land. (Hush)
  • Unit tests pass (482 tests, all green), including new suites for download naming across every container Hush handles and for the renewal cadence's relationship to the window it must beat. (Hush)
  • Lint fossMobileUniversalDebug: clean, with NewApi fatal and abortOnError true in app and waze-shim; verifyDownloadNaming green; compileGmsMobileUniversalReleaseKotlin clean. (Hush)
  • Verified on a connected device: a cold start with valid sessions raises zero verification prompts and reports renewed=0 of 4 [all:not due (353m left)]; every source that can work answers verdict=ok detail=online; a forced renewal is answered by the gateway with a new expiry exactly 6 hours from the call while the session id stays put, and that expiry is what the record then holds; a re-download lands as .m4a and the mislabelled .flac is gone; removing it clears record and cached copy together. (Hush)

Upstream credits

Hush is built on ArchiveTune and combines features, fixes, and UI from several open-source YouTube Music clients—including Metrolist, Vivi Music, and Echo Music. Those projects are credited below; their licenses and copyright notices are preserved in source.

Project Repository
ArchiveTune ArchiveTuneApp/ArchiveTune
Metrolist metrolistgroup/metrolist
Vivi Music vivizzz007/vivi-music
Echo Music EchoMusicApp/Echo-Music
SpotiFLAC spotiflacapp/SpotiFLAC-Mobile
Zemer Cipher ZemerTeam/zemer-cipher

Thank you to the maintainers and contributors of every project listed above.