13.14.3
Changelog
Changes in v13.14.2..HEAD:
Highlights
- A Source Toggle No Longer Costs You the Queue — switching YouTube or SpotiFLAC on or off used to
clearMediaItems()and put only the track that was playing back, so the queue collapsed to one song and the next save wrote that over the persisted copy — which is why it also looked like "the queue isn't restored after a restart". The current item is now replaced in place, keeping everything behind it. (Hush) - Skipping Mid-Download No Longer Wedges the Player — the resolve for the track you skipped away from is owned by the service, so Media3 cancelling that load did not stop it: the abandoned sweep kept holding the extension runtime while the track you skipped to queued behind it. Abandoned resolutions are now cancelled on transition. (Hush)
- Transport Buttons Work on a Fresh Install or a Reinstalled Head Unit — with nothing persisted, next/previous/play were answered with an empty timeline: silently, on all three routes (car bridge command, hardware/media key, playback resumption). A queue is now rebuilt from the recently played history, and only for a press that is actually asking for music. (Hush)
- One Verification Is Enough, Everywhere — a Turnstile grant redeems only for the extension whose challenge raised it. Delivering one grant to every source verified exactly one and had the other three refused with HTTP 403 — the "the browser says OK and Hush still says 403" loop on a car head unit, where the runtime's challenge is the only one on screen. Each surface now delivers to the challenge's owner. (Hush)
- Amazon Lossless Is Decrypted by Hush — Amazon's extension downloads the stream exactly as Amazon serves it (encrypted) and answers with the key instead of decrypting it, which is what made such a track "play" in silence. The
ffmpeg.mov_keycontract is now read and executed host-side. (Hush) - The Player Says What It Is Waiting For — during a source sweep the row that names the source was blank for the whole window, which is what makes a slow source read as a track that never started. It now reads
Fetching from qobuz-web…. (Hush)
Playback and Transport
- Source toggle preserves the queue —
reResolveCurrentTrackForSourceTogglereplaces the current item in place (replaceMediaItem+seekTo) instead of clearing and re-setting the timeline, and the replacement carries the media id as its URI again so Media3's resolving data source follows the new engine order rather than a URL pinned to the engine just switched off. Verified on device: four toggles, queue size unchanged at 100, and the persisted copy intact across a restart. (Hush) - Abandoned resolutions are cancelled —
PlaybackResolutionKeys(new) owns the in-flight key format and the "which of these are abandoned" decision;cancelAbandonedPlaybackResolutionsdrops every resolution whose media id is not the track now playing, and queue replacement is exempt because the items it brings are the ones about to be wanted. Measured on device, a stall of 20.2s before is 14.1s now. (Hush) - Cold-start transport recovery —
HistoryRecoveryQueueandTransportRecoveryPolicy(both new) rebuild a queue from the recently played history, newest first, de-duplicated and blocked-artist filtered, capped at 50. Wired into all three entry points: the Waze/bridge command receiver,onMediaButtonEvent, andonPlayerCommandRequest/onPlaybackResumption.pause,stop,seekand the stalesyncthat arrives on every car connect are deliberately excluded, so pausing a silent device does not start music. Verified on device by deleting the persisted queue and firing each route independently. (Hush) - The waiting state names the provider — while a sweep is running with no bytes behind it,
CodecInfoRowrendersFetching from <source>…over an indeterminate bar instead of a blank row or a frozen "Downloading 0%". The placeholder flow is remembered unconditionally, becauseLocalPlayerConnectionis astaticCompositionLocalOfthat goes from null to bound and arememberreached only through?:is not wrapped in a group by the Compose compiler. (Hush) - A provider that asks for time is given it, once — the pre-transfer idle budget covers exactly one declared retry (qobuz-web answers
retry_after_seconds: 10) plus margin, instead of the provider's whole retry budget. It was 20s of a 45s sweep spent on qobuz-web alone; measured on device the silence is 14.1s now, and every cheap provider still fails fast. (Hush)
SpotiFLAC Sources
- The sweep order matches the device — the registry is a catalogue and the installed extension packages are what the runtime can load, and they disagreed in both directions:
apple-musicandpandorawere handed to every sweep on a device that has neither, andytmusic-spotiflacwas installed but in no sweep that used the registry list.SpotiFLACCandidateOrder(new) reconciles them: sources with no package are demoted, never dropped (they are still tried once everything loadable has failed) and installed sources the registry omits are added, with user order preserved inside each group. Verified on device. (Hush) - A cancelled lookup is not "this source had nothing" —
SpotiFLACClient's four lookup entry points caughtCancellationExceptioninto aResult, so a skipped track left its source and quality loops querying the network for a track nobody was waiting for.resolveResultOf(new) rethrows cancellation and otherwise behaves exactly likerunCatching.ExtensionRepositoryManagerhad the same shape twice around its registry fetch, where a cancelled sync was logged as a failed URL and then let the built-in subset overwrite the live source list. (Hush) - SpotiFLAC moves out of Developer options — the master switch now lives in Audio Sources, where the sources it governs are chosen. (Hush)
- No duplicate cache controls — the SpotiFLAC screen's own cache size limit and clear button are gone; both are the app's Storage settings, which the cache already obeys. (Hush)
Amazon, Dolby and Encrypted Streams
- The decryption contract is read, not assumed —
SpotiFLACDecryptionContract(new) parses what a provider said has to be done to make its download playable, mirroring the runtime's own normaliser so an accepted spelling is understood rather than looking like a contract nothing implements. (Hush) ffmpeg.mov_keyexecutes on the host —SpotiFLACMovKeyDecryptor(new) decrypts the ISO-BMFF payload to a repaired container, or extracts a real.flacwhen the sample entry is Amazon's lossless tier. Both counter conventions are handled deliberately, and subsample encryption is declined rather than spliced into FLAC silently. (Hush)- A still-encrypted stream is not audio —
SpotiFLACFileIntegritynow rejects a file whose sample entry isenca/encvor which carriessinf/schm/tenc, so an entry written before that check existed is dropped and re-swept instead of being served as silence forever. (Hush) - A Dolby answer is retried, not remembered as a miss — a source can answer a music request with Dolby Digital Plus (
ec-3) or Atmos (ac-4), which is silent on a device with no AC-3/AC-4 output path. That is not a catalogue verdict, so it is treated like "I cannot deliver that quality" — the source is asked for the lossy option it does declare — rather than memoised for hours. Detected from the bytes and from the runtime's reported codec name. (Hush)
Verification
- One grant, one owner — the player overlay, the browser route, the Audio Sources screen and the grant deep link all resolve the extension that actually raised the pending challenge before delivering a grant, and release the requested source's attempt either way so a source left
activecannot make every later one wait behind it. (Hush) - A foreign grant's 403 is not a dead session — the relay exchange request carries no session credentials, so a 401/403 on it cannot be evidence that the stored session is invalid. Clearing it on a grant that belonged to another client wiped a working session and re-raised every source's challenge, which is what kept the card showing a 403 however many times the check was solved. The exchange now only runs while its own challenge is outstanding. (Hush)
- Verification is a runtime fact, not a screen's memory — the Audio Sources screen re-reads the runtime's own signed-session state (and watches the verifier's ticker), so a check completed by the automatic run, the overlay, the notification or a deep link shows up without leaving and returning. (Hush)
- A session hidden by a version bump is restored — a session record's file name is derived from the extension's app version, so a registry update left a verified session sitting under the old name and the source asked for a challenge it had already passed.
SpotiFLACSessionRenewer.restoreFromVaultwrites it back under the name in use now, keyed by extension rather than by version. (Hush) - Stopping the asking — switching SpotiFLAC off drops the queued run, the passive notice and every manual offer, including the notification a car user would otherwise be prompted to tap for playback that can no longer route through SpotiFLAC. (Hush)
- An unreadable manifest is "unknown", not "nothing to verify" —
SpotiFLACSourceAuthState.UNKNOWN(new) separates a manifest that could not be read from one that positively declares no signed session. On a fresh install the packages have not been extracted yet, so every source was previously classified as needing nothing: the automatic queue stayed empty and a download later failed withverification_required. (Hush)
Waze Bridge
- Bundled bridges rebuilt —
waze-shims.zipregenerated from the current shim sources, so the archive Hush installs and repairs from is byte-for-byte the one the build produces. (Hush)
Release Engineering
scripts/spotiflac-verify-smoke.sh— drives the debug-onlySpotiFLACDebugReceiveroveradband asserts the routing from the app's own log lines, including the automatic route, with exit codes0(passed) /1(failed) /2(undecidable — never reported as a pass). It cannot solve Cloudflare for you:--openputs the challenge in the device's browser, and a solved challenge still publishes its unspent grant. (Hush)- A shell-driven seam for verification — the debug receiver exposes each step (
state,challenge,recover,browser,resolve,bytes,verify,toggle,play) and reports the challenge's owner as well as the source asked about, which is the property the routing fix exists to prove. Registered insrc/debug/AndroidManifest.xml, so it exists in nothing that ships. (Hush)
Housekeeping
- Version bumped to 13.14.3 (versionCode 173, app + waze-shim). (Hush)
- Unit tests pass (466 tests, all green), including new suites for the candidate order, cancellation-preserving results, playback resolution keys, cold-start history recovery, the resumption planner's fallback, the fetching-state decision,
mov_keydecryption, and the container/integrity probe. (Hush) - Lint
fossMobileUniversalDebug: 0 errors, withNewApistill fatal inappandwaze-shimso an above-minSdk call cannot reach a release again. (Hush) - Verified on a connected device: queue unchanged across four source toggles (100 items, persisted copy intact) and across a restart; a sweep's silence down from 20.2s to 14.1s with
Fetching from qobuz-web…on screen; cold-start recovery answering the bridge, the media key and the media-session command route with real songs after the persisted queue was deleted. (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.
Thank you to the maintainers and contributors of every project listed above.