Skip to content

Releases: maxinjohn/Hush

13.14.6

Choose a tag to compare

@github-actions github-actions released this 24 Sep 08:40

Changelog

Changes in v13.14.5..HEAD:

Highlights

  • Liked Songs Reaches the Car — the account's Liked Songs is not a playlist, but it is the folder a Spotify listener reaches for first, and the car's Spotify group did not have it. It now sits at the top of that group inside Home → Mixes and radios, the way the phone's own library orders it, and it is addressed as a playlist (spotify_playlist/_liked) rather than as a fourth kind of entry — so play, shuffle, browse and the leaf path for one of its tracks are the paths that already existed, with no special case anywhere. The leading underscore is why the id can never collide with a real base-62 playlist id, and a test pins that. The folder is dropped entirely when the account has nothing saved in it, because an entry that opens onto nothing is the same broken promise as a switch that does nothing. (Hush)
  • A Gateway Blip No Longer Hides the Folder for Five Minutes — the count behind that entry is cached, because a car re-asks for the folder every time the user scrolls back to it and one gateway round trip per flick of the dial is not a design. But the cache could not tell "this account has no Liked Songs" (0) from "this account could not be asked" (null), so one failed probe was cached for the whole LIKED_PROBE_TTL_MS and the folder vanished for five minutes — indistinguishable from the feature not existing. An answer the account actually gave is now good for five minutes; a probe that failed is retried after 20 seconds. (Hush)
  • The Equalizer Crash Is the Serializer That Was Never Generated — the reported release stack was Serializer for class 'ig9' is not found. at AxionEqViewModel$applyToService$1.invokeSuspend. SavedEQProfile, ParametricEQ, ParametricEQBand and FilterType are persisted through the reified Json.encodeToString/decodeFromString, which resolve the serializer by reflection at runtime — and without @Serializable there is no generated serializer to find, so the lookup throws on the main thread the first time a profile is saved or read. All four are annotated now, and EQProfileJsonTest asserts serializer<T>() reflectively for each of them plus a full round trip and a decode of JSON written before the fix, so a profile saved by an older build still loads. (Hush)
  • A Whole-Bug-Class Sweep, Not One Annotation — the same shape — a reified encodeToString/decodeFromString whose target has no @Serializable — was searched for across app/src/main and core/src/main. Every reified call target in the app is annotated: SavedAccount, NewsItem, RecognitionHistoryItem, ThemeExportV1, RemoteCipherConfig, ExtensionRegistry, DownloadedFileIndex, DownloadOriginIndex, SpotiFLACCacheIndex, SpotiFLACMissIndex, SpotiFLACExtensionCheckIndex, SpotiFLACSessionVerdictIndex, CanvasCacheEntry, GeneratedAppIcon. core has no reified call site at all. This is a class of bug no compiler and no lint check can see, which is why the guard is a test. (Hush)
  • The Equalizer Stopped Reading the Disk in the Frame That Opened the Menu — EQProfileRepository lists and reads every eq_profiles/*.json in its constructor, and the player menu builds it during composition (PlayerMenu), so opening the menu read a directory and parsed its files on the main thread, in that frame; saveProfile, deleteProfile and setActiveProfile then wrote files from Dispatchers.Main as well. Every file operation now happens on a private Dispatchers.IO scope, mutations wait on a loaded barrier so a profile saved while the initial read is in flight cannot be dropped by the read publishing its older list, and all three mutators are withContext(Dispatchers.IO). (Hush)
  • Waze's Bridge Is Told That Hush Is Here — the reported case is Waze opened first: the Bridge comes up on its own, finds no player, and keeps showing a dead panel — and opening Hush afterwards changed nothing, because the only things that ever told a Bridge to re-attach were Waze's own bind and a Reconnect button buried in Hush's settings, which the user has no way to reach while driving. Both directions of that handshake now close: Hush's process start and its playback service coming up broadcast the same reconnect request the button sends, to the same packages, under the same permission (WazeBridgeAutoReconnect). It is idempotent on the Bridge side (re-bind, re-start the service, ask for a snapshot), fired once per service creation rather than per track, and gated on WAZE_SUPPORTED. The shim detection in MusicService was refactored to call the same installedBridgePackages, so "what is a Bridge" has one definition instead of two that would eventually disagree. (Hush)

Android Auto

  • The Liked Songs folder is a playable, browsable entry — built with queueMediaItem so a head unit both expands it and plays it, titled from the account's own count (n_song), and returned by onGetItem for the folder id alone: a tap on one of its tracks arrives with an action and has to fall through to the leaf branches, which is the one case a folder-shaped item can get wrong. (Hush)
  • Its tracks come from the Liked Songs endpoint, paged like a playlist's — SpotifyLibraryRepository.likedSongs(limit) walks likedSongs in 50-track pages and stops as soon as the caller has enough, skipping local files, exactly as playlistTracks does; likedSongsCount() costs one page of one item, since the only thing a browsable folder needs to know is whether it would be empty. (Hush)
  • _liked parses as a playlist with no action, and no real playlist can be mistaken for it — pinned in AndroidAutoPlaylistsTest: parse with no action, the shuffle id, a leaf spotify_playlist/_liked/track123 yielding selectedTrackId == track123, and that no base-62 id can satisfy isLiked. (Hush)
  • The phone's own Liked Songs is a different entry — the root LIKED item is Hush's local likes; the Spotify one is the account's, and it is the one under Mixes and radios. Two likes, two folders, no ambiguity about which is which. (Hush)

SpotiFLAC

  • A run says which source it is on — a verification run works through several sources in sequence and can take a while over each, so "something is happening" is not an answer a user can act on. SpotiFLAutoVerifier now publishes steps (a StateFlow of Step with SOLVING / WAITING / VERIFIED / NEEDS_CHECK), and SpotiFLACVerificationChecklist turns that into the words and colours — a pure object, because the wording is a decision and a decision buried in Compose can only be checked by looking at a phone. It is also the one place two vocabularies have to agree: the line under the buttons counts a run, the rows name its members, and a reader who sees "1 needs a check" above a row saying "Verified" believes neither. (Hush)
  • Renew and Verify are two buttons again, because they are two jobs — they were merged into one "Verify & renew all" that renewed first and then quietly raised checks for whatever the gateway had turned down, which read as a contradiction: the line above counted how many sources already need a check, and the same button offered to renew them. The gateway rotates a live session; a session it has thrown away can only be replaced by a check, and no amount of renewing produces one. Each button is enabled only when it can do something. (Hush)
  • One source's check can be asked for on its own — verifySource joins the same queue with the same permission to open the browser, because asking for a check is the consent a background prewarm does not have — so a car head unit's challenge opens for the user here exactly as it does from the batch. It names itself in the log (settings-verify-one) so a report can tell a deliberate single check from a batch run. (Hush)
  • A run that cannot open a browser says so — SpotiFLACChallengeRoute.openInBrowser returned Unit while startActivity throws ActivityNotFoundException on a device with no browser at all, so a head unit with no browser was told the check had been opened when nothing had. It returns Boolean now (runCatching/recoverCatching), and both callers report it: SpotiFLACBrowserVerification and the overlay's browserLaunchFailed state. (Hush)
  • A session the gateway threw away stops reading "Verified" — SpotiFLACSessionRenewer now distinguishes a renewal that was answered from one that needs the user, and a gateway-rejected session is dropped rather than kept as a live-looking record (shouldForgetSessionAfterRenewal(result) = result.contactedGateway && result.needsVerification), so the row matches the count the Verify button acts on. (Hush)
  • Two threads updating the same step list no longer lose one — SpotiFLAutoVerifier.recordStep/forgetStep did read-modify-write on _steps.value from the enqueuing caller and from the verifier thread, which is a lost update whenever both touch it (the enqueuing side would erase a step the verifier had just finished). Both use MutableStateFlow.update now. (Hush)

Settings and probes

  • One control per row — the extension source rows showed "Test", a column of ▲/▼, and an "Update" under the description: five tap targets wide, where a tap meant for one of them landed on another. The row's actions are one overflow (MoreVert), applied to the source rows, the priority rows and the card's session rows, and the menu is anchored to the row's own bounds (Box) rather than the screen — which is why a menu used to open on the right of the screen instead of under the button that opened it. (Hush)
  • A build task rejects the shape coming back — a loose control in those rows is a one-word edit no compiler or lint check can see, so verifyRowControls reads the settings rows and fails the build when the batch "Verify" or a per-source "Test" reappears beside the row instead of inside its m...
Read more

13.14.5

Choose a tag to compare

@maxinjohn maxinjohn released this 20 Sep 22:45

Changelog

Changes in v13.14.4..HEAD:

Highlights

  • A Cached Song Now Survives a Restart — Hush kept its SpotiFLAC playback files inside Media3's player-cache directory, and SimpleCache deletes files it does not recognise anywhere under its own directory. So every cold start emptied the folder: measured on the reporting device, a force-stop and relaunch took the cache from 6 files to 1, with cache reconcile: orphans=0 stale=[bcdd50…, 96fd74…, ab23e9…, 3108506…] — Hush's own sweep had nothing to delete, it was told about the loss afterwards — and the engine's own index then reported ISRCIndex … 0 files. The player cache now has a player-cache/ subdirectory to itself, so it can only delete its own bytes. Verified after the fix: a 29,038,603-byte cached file survives a restart and playback serves it directly — url resolve skipped mediaId=vvfBF9bt29g: already on disk (…media), with no provider contacted. (Hush)
  • The Format Row Names Its Evidence — FLAC • 1411 kbps could come from a resolve in this session, a row an earlier resolve saved, a file on this device, or the audio Media3 is decoding right now, and the screen did not say which — so a stale row beside a streamed track was indistinguishable from a lossless file being served. The row now carries its provenance as one value (FormatRow + ServedFormatClaim), and a track whose stored row had to be refused shows what is actually decoding (DecodedAudioRow) instead of an empty codec line. (Hush)
  • Motion That Survives a Device With Animations Off — with the system animator duration scale at 0 — a car head unit, or animator_duration_scale = 0 as measured on the reporting device — Compose silences animateFloatAsState, rememberInfiniteTransition and the indeterminate *ProgressIndicators, so buttons jumped between two sizes, no ripple appeared, and a spinner that means "something is happening" painted one frame and held it. Press motion and the indeterminate indicators are now frame-driven (HushPressMotion, FrameDrivenMotion), which no system setting can silence: a press contracts, springs past its own size and throws a two-wave ring, and the play/pause button animates while a download is in flight instead of looking frozen. (Hush)
  • A Playback Confirmation Is Not a Flaky Stream — when every stream client answered LOGIN_REQUIRED, media3 wrapped it as ERROR_CODE_IO_UNSPECIFIED — the code a dropped connection produces — so the player spent its retry budget clearing caches and re-resolving with rotated clients, each round re-running the whole client sweep, and the panel naming the one action that works arrived late or was replaced by an auto-skip. The demand is now recognised through the wrapper (PlaybackConfirmationRequired) and surfaced immediately, with its "play this one from YouTube Music" action. (Hush)
  • A Blocked Address Heals Itself — a gateway block is the gateway refusing an address, and an address changes far more often than the app does. Measured on the reporting device: the same install that was refused with {"retry_after":75358} was served normally the instant a VPN came up, while the block suppressed every source for the ~21 hours it had left, and the only way out was for the user to find "Try again now". SpotiFLACRouteWatch now watches both halves of a route — the platform's network change, and the proxy preference, which nothing reports at all — and asks the gateway once per change, never on a timer. The way into a refused network is covered too: with no block recorded there was nothing to disprove, so the first play after a switch still walked the whole provider chain before falling back. (Hush)
  • Car Screens Get Spotify Playlists — Android Auto's library only exists once a head unit connects, which is why a missing folder, a switch that does nothing or a track a tap cannot play go unnoticed until someone is sitting in the vehicle. Spotify playlists now appear in "Mixes and radios" beside YouTube's — gated on their own switch and a connected account — the "Visible sections" order is finally consulted by the library service, the quick-add destination is a real session command with a real handler, and resolved online tracks are marked as browsable leaves, which media3's library session requires (mediaMetadata must specify isBrowsable). (Hush)

SpotiFLAC Sources

  • The engine's state is not the switch — spotiflac=true with every session lapsed put SpotiFLAC first in the resolve order, the sweep spent its budget on four sources the runtime refuses to download with, and the track ended in "no source could serve this" while YouTube was enabled and could have played it at once. SpotiFLACAvailability separates the user's intent from the engine's state: an engine with nothing usable and sources waiting on the user is paused rather than consulted, and a source with no session contract (SoundCloud, the YouTube Music provider) is never the reason to pause. (Hush)
  • A stall is attributed to the provider that was actually tried — Hush hands the runtime the whole candidate list and the runtime falls through it by itself, so the provider an abandoned attempt went quiet on is routinely not the one it was requested with. Measured: Native download start: source=amazon → Trying provider: deezer → Provider temporarily unavailable for extension deezer; retrying in 10s → provider stalled id=amazon … abandoned. That demotion moved an innocent provider behind the wedged one and sent the next sweep into deezer again, paying the same timeout. SpotiFLACRuntimeWalk reads the walk out of the runtime's own log, as evidence rather than a verdict — only lines whose whole purpose is to name a provider are matched. (Hush)
  • A failing source says whose service failed — the Test row reported Zarz API is offline, which reads like a verdict on the user's session or verification, when deezer's own gateway check was {"ok": false, "error": "fetch failed"}. It now prefers the runtime's own sentence — Provider's own service - Deezer: fetch failed — scoped so it can never read as a session verdict, and the same verdict is kept (with its reason) and shown on the row without pressing Test, Deezer: tried last for 10m. It ends the moment the source serves again: Native download ok: source=qobuz-web bytes=8690812 codec=flac clears it, and a source that just served is not unavailable. (Hush)
  • An installed extension updates itself — a package file was written once and trusted for the life of the install, because the download only ran when the file was missing. Measured: amazon sat at 2.3.8 while both registries published 2.3.10, with no code path that could replace it. SpotiFLACExtensionUpgrade compares the registry, stored and installed versions (a version it cannot parse is never treated as newer), and Audio Sources shows both versions with a one-tap update that reuses the refresh path. A startup sweep reports what it moved in one line, and each source keeps a check log (when it was last checked, which registry it came from). (Hush)
  • One proxy setting, not two — the relay calls leave through Hush's own Internet proxy (SpotiFLACRelaySettings), built by the same ProxyUtils.createProxyOrNull the rest of the app uses, so "enabled", "host", "port" and "type" can only mean one thing. A SpotiFLAC-only proxy would have been a second answer to the same question. (Hush)
  • The legacy gateway session is gone — Hush's own GET /v2/bootstrap → Turnstile → POST /v2/session/exchange session can serve no source: the gateway binds a session to the app version that minted it, so the same path answered 403 signed as tidal-web@1.2.6 and 428 VERIFY_REQUIRED signed as the client version (measured). The gateway no longer hands one out either — bootstrap returns a fresh challenge on every launch — and the surface that could complete it was gone, so its state (CHALLENGE_PENDING, forever) had no repair. SpotiFLACInstallIdentity keeps what is still true: one install id, and the client version and relay address every call is described by. (Hush)
  • The title a catalogue sees is a song name — providers were handed the video's own metadata, so a title like "Mizhiyil Mizhiyil | Maayabazar | Mammootty | Sheela Koul | Rahul Raj - HD Video Song | Sujatha Mohan" credited to a channel ("Malayalam Hits", "<Artist> - Topic") produced no match, an empty provider track ID and Invalid Tidal/Deezer track ID — which reads as "SpotiFLAC is broken" for any track whose video title is not already a clean song name. SpotiFLACLookupText takes the first |-separated segment, drops format tags while keeping real variants ("Slowed Down Version", "Remix"), and refuses a channel-shaped artist outright. (Hush)
  • One lookahead, asked twice — how many tracks are warmed now comes from SpotiFLACPrefetchPlan, which both engines consult, so the count is the user's "Prefetch upcoming songs" setting rather than a second opinion, and the plan never warms the track being played in its own lookahead and never repeats a position when it wraps. The parallel-fetch switch is wired: with it on, every enabled source is asked at once and the first answer wins. (Hush)

Downloads and Caching

  • One key, one file — the serial walk recorded the runtime's container-suffixed output while the race landed the canonical name, so a track could exist twice: measured on device, a 25,817,207-byte .media beside a 24,542,701-byte .media.m4a of the same song, only one of which was indexed — so nothing counted the other. Every path now lands on the one path a key owns, and leftovers for that key are pruned when the file is recorded. (Hush)
  • The cache's file names are parsed from the first dot — every sweep and in-flight guard read name.substringBeforeLast('.'), which turns K.media.m4a.partial into K.media.m4a — never a track key. So a...
Read more

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 ...
Read more

13.14.3

Choose a tag to compare

@github-actions github-actions released this 17 Sep 06:36

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_key contract 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 — reResolveCurrentTrackForSourceToggle replaces 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; cancelAbandonedPlaybackResolutions drops 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 — HistoryRecoveryQueue and TransportRecoveryPolicy (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, and onPlayerCommandRequest/onPlaybackResumption. pause, stop, seek and the stale sync that 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, CodecInfoRow renders Fetching from <source>… over an indeterminate bar instead of a blank row or a frozen "Downloading 0%". The placeholder flow is remembered unconditionally, because LocalPlayerConnection is a staticCompositionLocalOf that goes from null to bound and a remember reached 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-music and pandora were handed to every sweep on a device that has neither, and ytmusic-spotiflac was 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 caught CancellationException into a Result, 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 like runCatching. ExtensionRepositoryManager had 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_key executes on the host — SpotiFLACMovKeyDecryptor (new) decrypts the ISO-BMFF payload to a repaired container, or extracts a real .flac when 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 — SpotiFLACFileIntegrity now rejects a file whose sample entry is enca/encv or which carries sinf/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 active cannot 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.restoreFromVault writes 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 with verification_required. (Hush)

Waze Bridge

  • 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...
Read more

13.14.2

Choose a tag to compare

@github-actions github-actions released this 16 Sep 13:28

Changelog

Changes in v13.14.1..HEAD:

Highlights

  • Waze Bridge Repair Survives Process Death — the repair target lived only in memory, and a repair spans a system uninstall that puts Hush in the background — where it can be killed. Reproduced on a real device: repair started, bridge removed, process recreated, and the repair was simply forgotten, leaving the bridge gone and nothing installing the bundled one. The target is now persisted (with a 15-minute staleness rule), so the second half runs whichever process performs it. (Hush)
  • Bridge Installs Are Patient Enough for the Prompts They Cause — a Bridge install on a real device is not one dialog: it is the installer, then Play Protect's "Scan app", then the vendor's own "Continue installation". Three prompts took longer than the ~33s the attempt allowed, so a working install was reported as a failure and only the retry rescued it. The wait is now ~46s per pass, counted from measurement rather than guesswork. (Hush)
  • A Verification Completed in Settings Now Wakes Playback — the Audio Sources screen hosted its own challenge and, unlike the overlay and the browser route, never reported success. A track parked waiting for that source was never resumed, the "SpotiFLAC needs verification" card stayed up over a source that was already fine, and the freshly earned session was never mirrored into the vault. All three surfaces now report through one entry point. (Hush)
  • A Release Gate for the Waze Bridge Lifecycle — a shell-driven smoke test that exercises detect → remove → install → verify → update in place → uninstall → repair → restore against a real device, with machine-readable verdicts and its own exit codes, so the lifecycle that breaks users on release day is checked before a release rather than after. (Hush)

Waze Bridge

  • Repair target persisted across process restarts — WazeBridgeRepair writes the Bridge it is repairing to storage and restores it on the next start, bounded by MAX_AGE_MS so a repair from an unrelated session is never resumed. Verified on device by force-stopping the app between the removal and the completion: the new process reported repair complete: com.spotify.music is gone; installing the bundled bridge and installed 1711. (Hush)
  • Install attempt watches long enough for the platform's own prompt chain — WazeBridgeInstallAttempt polls for ~46s per pass instead of ~33s, keeping the single automatic retry. The wait still ends the moment an install lands, so a prompt install is unaffected. (Hush)
  • A platform-aborted install is named, not guessed at — when the device's package verifier gives up on a sideloaded Bridge, or the platform refuses a correctly-signed Bridge because it still holds a record of the one that was removed (INSTALL_FAILED_DUPLICATE_PERMISSION, INSTALL_FAILED_UPDATE_INCOMPATIBLE), the attempt reports that condition with its evidence instead of a bare failure. (Hush)

Verification

  • One entry point for "this source is verified" — SpotiFLAutoVerifier.notifyVerified is now what every surface calls (the automatic overlay, the browser route, the Audio Sources challenge, and the grant deep link), so a verification cannot complete without waking a parked track and clearing the notice that asked for it. Safe for a source the automatic queue never held: it records the success and resumes playback without disturbing a run in flight. (Hush)
  • The automatic queue can no longer be corrupted by its own callers — SpotiFLAutoVerifier kept its queue, cooldowns and attempt map in plain LinkedHashSet/HashMap fields while being finished from five places on five threads (playback, the runtime bridge's preflight, the browser route's process-wide Dispatchers.Default scope, and the two in-app screens on main). A mutation from two of those at once could lose an entry or throw out of queued(), and a source left active forever makes every later source wait behind it — which is exactly what "verification sometimes does nothing" looks like. The collections are guarded by one lock, held only around themselves so a re-entering callback cannot deadlock, and the ticker increment is inside the same critical section because two surfaces can report the same verification at once. Covered by a concurrency regression test. (Hush)
  • "Re-verify" is only offered when there is a check to raise — a source with a healthy session has nothing to solve (the runtime keeps the challenge it registered, and that one is already spent), so the button could only answer "already verified", which reads as a broken button. It now appears for a source that needs a check, or one whose session has lapsed; routine renewal stays with "Renew now". (Hush)
  • Manual and automatic routes unchanged in behaviour — the in-app WebView remains the default wherever the engine can run Cloudflare's check; the browser route stays the one-tap alternative that hands its grant back on its own (no code to copy), and the only route on a device whose embedded WebView is older than Cloudflare supports. (Hush)

Release Engineering

  • scripts/waze-bridge-lifecycle-smoke.sh — drives the debug-only repair receiver and reports one machine-readable verdict per step, with --json for a CI consumer and exit codes 0 (passed) / 1 (failed) / 2 (usage or device) / 3 (the device's own package verifier blocked a step, so nothing was proved either way). It taps only system-owned prompts, never Hush's own UI. (Hush)
  • In-place update is actually testable — the platform refuses a version downgrade over an installed release-signed package, so the leg stages an older Bridge on a clean slate and then requires Hush to replace it, proving the update was in place (the platform's firstInstallTime is unchanged) rather than a remove-and-reinstall. (Hush)
  • Repair can be exercised on demand — --mismatch-apk stages a differently-signed Bridge for the repair leg, moving the other Bridges aside first (all three declare the same signature-level permission, so a foreign-signed one cannot coexist with them) and restoring them afterwards. (Hush)
  • A failing run still restores the Bridge under test — so a release gate can never leave a device without a Bridge. (Hush)

Housekeeping

  • Version bumped to 13.14.2 (versionCode 172, app + waze-shim). (Hush)
  • Unit tests pass (407 tests, all green), including new coverage for the resumable repair target and for a verification completed outside the automatic queue. (Hush)
  • Lint fossMobileUniversalDebug: 0 errors. Debug build installed and exercised on a connected device: all three Bridges BRIDGE_CURRENT at 1711, and four SpotiFLAC gateway sessions self-renewing with no manual check required. (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.

13.14.1

Choose a tag to compare

@github-actions github-actions released this 15 Sep 15:06

Changelog

Changes in v13.14.0..HEAD:

Highlights

  • Android 8–11 Startup Crash Fixed — v13.14.0 could not start at all on Android 8 through 11 (including Android Automotive head units): a Class.getPackageName() call (API 31) sat in the startup preference flow and killed the process before any UI appeared. The namespace is now derived with Class.getName() (API 1). (Hush)
  • Download Crash Fixed on Android 8–12 — Intent.getParcelableExtra(name, Class) (API 33) was called unguarded at the start of every download. Now version-guarded, so downloading no longer throws on older devices. (Hush)
  • API-Level Regressions Are Now Release-Blocking — NewApi is fatal and abortOnError = true in both the app and the Waze shim, so lintVital (part of every release assemble) fails the build on any above-minSdk call instead of letting it reach a phone. (Hush)
  • Blocked Artists Enforced in the Playback Queue — blocking an artist now also removes their tracks from the queues the player actually builds (fresh queue, restored queue, radio/infinite, Together), not just the browse screens. (Hush)
  • Artwork Out-Of-Memory Fixed — notification, player and widget artwork is decoded at a bounded resolution with a bounds-only first pass, so a large upload no longer allocates tens of megabytes (and no longer allocates a second full-size copy). (Hush)

Crash Fixes (Android 8–11 / Automotive)

  • Class.getPackageName() (API 31) removed from the startup path — IconUtils/AppIconRepository now derive the manifest namespace from MainActivity::class.java.name and keep using context.packageName for the applicationId, so the dynamic icon alias resolves in debug (.debug suffix) and on Android 8–11. Bundle/dex verified: no getPackageName()Ljava/lang/String; remains and MainActivity survives R8. (Hush)
  • Typed getParcelableExtra (API 33) guarded — ExoDownloadService reads the Media3 download request with the typed overload on API 33+ and the single-argument overload below it. (Hush)
  • Receiver flags unified through ContextCompat — every registerReceiver call that passed RECEIVER_EXPORTED/RECEIVER_NOT_EXPORTED (API 33 constants, enforced from API 34) now goes through ContextCompat.registerReceiver, which masks the flags below API 33 and forwards them above it. Covers the app's Waze command receiver, the Bluetooth receiver, the canvas-unlock receiver, and the shim's WAZE_METADATA_UPDATE receiver. (Hush)

API-Level Guard (Build / CI)

  • NewApi is fatal — declared in app/lint.xml and the new waze-shim/lint.xml. Verified by injecting the exact defect that shipped (Uri::class.java.packageName): lintVitalGmsMobileUniversalRelease fails with Call requires API level 31, and passes once removed. (Hush)
  • abortOnError = true — with the previous false, even a fatal lint issue was only reported, which is how the API-31 call reached users. warningsAsErrors stays off, so warnings do not fail a build. (Hush)

Playback and Queues

  • Blocked artists filtered where the queue is built — restorePersistentQueue (app restart), startQueue, radio/infinite queue creation and refill, and the Together playback queue all apply the blocked-artist set, read from the database with an empty-set fallback that cannot break queue start. (Hush)
  • Index-correct filtering — Queue.Status.filterBlockedArtists goes through the same filterItems path as explicit/video filtering, so dropping a track before the selected one re-counts mediaItemIndex instead of starting playback on a different song. Covered by QueueBlockedArtistFilterTest (drop before, drop after, drop selection, drop everything, out-of-range index, empty no-op). (Hush)
  • Hide music videos unchanged — the existing filterVideo enforcement is untouched; blocked artists join the same chain rather than replacing it. (Hush)

Performance and Memory

  • Bounded artwork decoding — new BoundedBitmapDecode helper: bounds-only pass → inSampleSize → decode straight to software ARGB_8888. The full-resolution bitmap is never allocated, and the extra full-size copy the old code made is gone. Wired into NotificationArtworkLoader, CoilBitmapLoader (Media3 BitmapLoader) and MusicServiceWidgetUpdater's dominant-colour extraction (now sampled at 256 px and recycled). (Hush)

Equalizer

  • Active-profile recomposition fix — AxionEqScreen read activeProfileId.value inside composition (lint StateFlowValueCalledInComposition), so the selected profile did not update the UI. It now collects the flow as state. (Hush)

Build / CI

  • TV release build fixed — the Waze shim archive was produced into app/src/main/assets, which every variant merges, so assembleGmsTvUniversalRelease failed Gradle's implicit-dependency validation when built alongside the mobile variants. The copy now targets only the mobile flavor's asset directory (the runtime reads the shims from this archive, and WAZE_SUPPORTED is false on TV). Verified with all three CI release variants in one invocation. (Hush)

SpotiFLAC

  • Provider stall watchdog no longer kills healthy downloads — the 25s attempt ceiling was measured from the attempt start, so a provider that was streaming a large lossless file was abandoned mid-transfer and then recorded as stalled and demoted for the full 2-minute cooldown. On device it reported itself as timed out without progress for 29ms — 29 ms after bytes had arrived. The ceiling is now counted from the last real byte transfer (only a growing byte count counts, so a provider re-emitting its size still cannot look alive), the abort reason is captured when it fires, and the reported silence matches the trigger that fired. (Hush)
  • Playback cache identity is stable across call sites and queue restores — MediaMetadata.durationMs reads 0 when an item carries no duration (the norm for a restored queue), and a plain fallback chain never reached the database behind it, so 0 became part of the track's identity. Measured on device for one song: the file was downloaded under Africa|Toto||0 and the next lookup computed Africa|Toto||272000, missed the cache, and re-downloaded a track already on disk. A single bestDurationMs helper now takes the first known duration across the queue item, what playback recorded, and the database. (Hush)
  • Second-chance lookup by media id — when the identity key has no entry, the media-id index still knows which file this queue item resolved to, so a drifted identity serves the existing file instead of re-downloading it. Verified on device: cache miss key=00d73932… followed by cache hit by mediaId index key=df62ab79… and no download. (Hush)
  • Quality bucket guard on that lookup — it has no quality component in its key, so entries now record the bucket they were fetched for: a hi-res request is never answered with the lossless copy on disk, while an entry written before the field existed still satisfies the default lossless request. (Hush)

Housekeeping

  • Version bumped to 13.14.1 (versionCode 171, app + waze-shim). (Hush)
  • SpotiFLAC verified end-to-end on a real device: six provider extensions installed, four gateway sessions self-renewing without a manual check, lossless FLAC files served from deezer, qobuz-web and tidal-web (codec=flac bits=16 rate=44100), correct SpotiFLAC • <source> • cached labels, and cached replay served ahead of the Media3 cache with no re-download. (Hush)
  • Unit tests pass (328 tests, all green), including new suites for the blocked-artist queue filter, the provider stall policy, and the playback cache identity. The device hashes behind the cache drift are pinned in the cache key test, so the evidence stays attached to the code that has to avoid it. (Hush)
  • Lint: 0 errors and 0 NewApi findings in app and waze-shim; all three CI release variants assemble successfully with the configuration cache enabled. (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.

13.14.0

Choose a tag to compare

@github-actions github-actions released this 13 Sep 23:02

Changelog

Changes in v13.13.8..HEAD:

Highlights

  • SpotiFLAC Source Support — Hush can now resolve and play tracks from Spotify-mirror sources through the upstream SpotiFLAC runtime, with lossless (FLAC) downloads across multiple providers, a user-ordered source priority, automatic fallback, and self-renewing per-source verification. (Hush)
  • Native Musixmatch Lyrics Provider — added as a first-class lyrics provider with per-provider priority selection, alongside the existing PAXsenix Musixmatch and other sources. (Hush)
  • Equalizer AutoEq Profile Import — import AutoEq measurement profiles directly into the parametric EQ, with saved-profile apply and management. (Hush)
  • Separate Downloads and Cache Locations — downloads and cache are now distinct, independently relocatable folders. Downloads keep the source's real file extension; cache holds fragments. Custom location choices now persist across restarts. (Hush)
  • Self-Healing Cipher Config — the remote player config now reads the ZemerTeam zemer-cipher synced config schema with per-player entries and aliases, plus manual refresh and a size guard. (Hush)

SpotiFLAC Source

  • Native Go runtime bridge — SpotiFLACNativeRuntimeBridge drives the upstream SpotiFLAC Go runtime (app/libs/gobackend.aar) for extension discovery, signed requests, and downloads. Builds without the AAR degrade gracefully instead of crashing. (Hush)
  • Source priority ordering — SpotiFLACQualityCascade lets you order which source is tried first and fall back through the rest, so a source that lacks a track no longer ends playback. (Hush)
  • Per-source quality cascade — when a source reports no compatible lossless quality, it is retried at a quality token its own manifest declares (e.g. HIGH for tidal-web, best for YT Music) instead of a hardcoded value no source accepts. (Hush)
  • Every enabled source is actually attempted — a hardcoded retry quality that silently re-asked the same question and a sweep budget sized for the old chain both caused sources to be skipped. The budget is now derived from the real work (SpotiFLACQualityCascade.sweepBudgetMs). (Hush)
  • Per-provider stall watchdog — SpotiFLACProviderStall abandons a wedged provider quickly instead of consuming the whole sweep budget. Liveness is now item-scoped, so an unrelated concurrent download can no longer keep a stalled provider looking alive. (Hush)
  • Persisted stall demotion — providers that stall are demoted to the back of the chain, and that memo now survives process restarts, so a cold start no longer leads with a provider that always wedges. (Hush)
  • Sweep verdicts and miss memo — SpotiFLACSweepVerdict distinguishes a genuine no-match from an unfinished sweep, and SpotiFLACMissMemo caches a real miss so it is not re-swept on every replay. (Hush)
  • Self-renewing verification — SpotiFLACSessionRenewWorker renews per-source signed sessions in the background, so a rotated or expired gateway session no longer forces a manual Cloudflare check mid-playback. (Hush)
  • Extension package integrity and signing — SpotiFLACExtensionPackageVerifier, SpotiFLACExtensionKeyStore, and SpotiFLACExtensionPackageStore verify downloaded extension packages before they are used, with dynamic registry updates. (Hush)
  • Playback cache with real integrity checks — SpotiFLACPlaybackCache and SpotiFLACFileIntegrity validate a cached file on its recorded size and container signature (FLAC/Ogg/ID3/ftyp/WebM/MPEG). Truncated downloads and gateway error pages written to the audio path are discarded and re-swept instead of being served as audio. (Hush)
  • Single copy on disk — a resolved SpotiFLAC file is served directly rather than being copied into media3's cache, so a track no longer occupies two copies. (Hush)
  • Corruption recovery — a cached file that verifies but will not decode (ERROR_CODE_PARSING_CONTAINER_MALFORMED, DECODING_FAILED) is discarded and every source is walked again, bounded by the existing retry budget. (Hush)
  • In-flight download guard — a file currently being rewritten is never mistaken for damage or evicted, and user-downloaded (pinned) files are never auto-deleted. (Hush)
  • Download origin tracking — DownloadOriginStore records which source a track came from, so a download is fetched from the same source the track plays from. (Hush)
  • Verification overlay — SpotiFLACVerificationOverlay surfaces per-source verification progress in-app instead of a bare source error. (Hush)

Playback and Source Routing

  • Single resolution path — PlaybackDataSourceRouting, PlaybackEngineOrder, and SchemeRoutingDataSource consolidate the previously duplicated SpotiFLAC-first / YouTube-first resolution paths into one. (Hush)
  • Fallback to YouTube is conditional — the fallback option is only offered when YouTube streaming is enabled, and honours the source toggles on the real stream-resolution path. (Hush)
  • No playable source handling — NoPlayableSourceException plus a one-tap "play this one from YouTube" action, so a genuine miss is actionable rather than a dead error. (Hush)
  • No more silent skipping — TransportSkipPolicy prefers trying every enabled source before advancing, rather than skipping the track. (Hush)
  • Accurate source labelling — PlaybackSourceInfo reports whether a track is served from the SpotiFLAC cache, a live SpotiFLAC download, or a YouTube stream, shown distinctly from media3's own downloaded badge. (Hush)
  • Playback download progress — PlaybackDownloadProgress shows download progress in the player, so it is clear when a track is still downloading. (Hush)
  • Queue persistence — the playback queue and position are committed when the app goes to the background, so any close path preserves them and a cold start resumes correctly. (Hush)
  • Next/previous no longer dead on restore — restoring a persisted queue now yields a usable timeline before a new playlist is chosen. (Hush)

Downloads and Storage

  • Downloads / cache separation — DownloadedFileStore, DownloadedFileLocation, and DownloadNaming keep downloads and cache as separate roots with separate migration and cleanup rules. (Hush)
  • Custom location persistence fix — a custom download or cache directory now survives an app restart instead of silently reverting. (Hush)
  • Folder hygiene — DownloadFolderHygiene avoids leaving stray partial files and duplicates in the downloads root. (Hush)
  • Shared cache limit — the SpotiFLAC cache shares the app's Storage settings limit rather than duplicating its own size and clear controls; usage is shown next to the max song cache size. (Hush)

Lyrics

  • Native Musixmatch provider — MusixmatchLyricsProvider with in-app client, registered in LyricsHelper and selectable in the lyrics provider priority list. (Hush)
  • Priority provider selection — LyricsProviderPriority gained the Musixmatch toggle so ordering between providers is user-controlled. (Hush)

Equalizer

  • AutoEq import — import AutoEq measurement profiles into the parametric EQ (AutoEqImporter + in-screen import dialog). (Hush)
  • Saved profile management — custom profiles are selectable and applyable, with active-profile tracking. (Hush)

Cipher / Player Config

  • Zemer synced config schema — RemoteCipherConfig now parses schemaVersion and a players map (signature function, throttling class, signature timestamp, aliases) with per-player lookup by hash or alias. (Hush)
  • Effective signature timestamp — signature timestamp resolution uses the per-player config when available and falls back to the local value. (Hush)
  • Manual refresh + size guard — CipherConfigFetcher.refreshNow() and a maximum config size, so a bad remote payload cannot break resolution. (Hush)

Waze Bridge

  • Reconnect policy — WazeReconnectPolicy (with unit tests), WazeBootReceiver, and the reconnect receiver make the bridge recover automatically when Waze's SDK service restarts mid-session. (Hush)
  • Reliable package resolution — HushPackageResolver detects the active Hush process instead of relying on installed-package presence, fixing shim↔app signature/connection mismatches. (Hush)
  • Queue reliability — queue revisions and a content fingerprint ensure the Waze queue is republished when it actually changes, including after a player restart or playlist switch. (Hush)
  • Metadata hot-path fix — the queue bundle is no longer rebuilt and re-broadcast on every metadata tick; it is cached behind an allocation-free fingerprint, with a retention cap for very large queues. (Hush)
  • Regenerated shims — waze-shims.zip rebuilt and synced to both main and mobile asset directories. (Hush)

Performance and Memory

  • Lower-RAM search — OnlineSearchSuggestionViewModel, CachePlaylistViewModel, and App/AppMemoryPolicy changes reduce allocation during repeated searches and trim work on low-RAM devices. (Hush)
  • Faster first playback — removed the duplicate first-track resolution requests and the startup gating that delayed initial playback. (Hush)
  • Visualizer fix — the PulseMatrix visualizer now emits real FFT band data (verified: 16 non-zero bands, fftAge 0–76 ms) and correctly drops to zero when paused. (Hush)
  • Source-label marquee — new shared Modifier.hushMarquee() scrolls long codec/source labels indefinitely instead of truncating them, and stays still when the label fits. (Hush)

Build / CI

  • gobackend.aar required and verified — CI asserts app/libs/gobackend.aar is present and contains libgojni.so before building, so a missing SpotiFLAC runtime fails fast instead of shipping a silently degraded app. (Hush)
  • Musixmatch module shipped via overlays — the Hush-only lyrics/musixmatch module ...
Read more

13.13.8

Choose a tag to compare

@github-actions github-actions released this 26 Aug 22:40

Changelog

Changes in v13.13.5..HEAD:

Highlights

  • Android Auto Crash Fix — fixed multiple crash causes when connecting to Android Automotive OS car displays. (Hush)
  • Blank Home Screen Fix — restored MusicBinder fallback in onBind() that was removed during the Auto crash fix, causing the app's own service binding to fail and rendering a blank home page. (Hush)
  • Waze Shim Sync Fix — fixed the build script and Gradle task only syncing waze-shims.zip to mobile/assets, leaving main/assets with a stale copy. FOSS builds now ship up-to-date shim APKs. (Hush)

Android Auto / Automotive

  • onBind binder fallback restored — MediaBrowserServiceCompat.onBind() only returns a non-null binder for the standard "android.media.browse.MediaBrowserService" action. The app's own bindService() uses a plain Intent with no action, so super returned null. Restored ?: binder fallback so the app's ServiceConnection always receives a valid MusicBinder. Android Auto / Waze shim connections still use the standard action and get the correct IMediaBrowserService binder. (Hush)
  • onCreate graceful degradation — replaced stopSelf() with warning logs when AudioManager/ConnectivityManager is unavailable during startup. Prevents the service from killing itself while Android Auto is already bound. (Hush)
  • onGetSession diagnostic logging — added logging when onGetSession returns null (session not yet initialized) for debugging race conditions with Android Auto binding. (Hush)
  • onDestroy ANR fix — reduced runBlocking timeouts from 1-2s to 500ms to stay within Automotive OS ANR budget. (Hush)
  • ProGuard keep rules — added -keep rules for MusicService, MusicBinder, MediaLibrarySessionCallback, and all MediaLibraryService/MediaSessionService subclasses. Prevents R8 from stripping classes that Media3 looks up reflectively when Android Auto connects. (Hush)

Waze Bridge

  • Shim asset sync fix — build-release.sh and copyShimApks Gradle task now copy waze-shims.zip to both app/src/main/assets/ and app/src/mobile/assets/. Previously only mobile/assets was updated, causing FOSS builds to ship stale shim APKs that couldn't be updated on-device. (Hush)

Build / CI

  • CommentsScreen CI fix — rewrote CommentsScreen.kt to be fully self-contained with inline data classes and its own InnerTube fetch logic. Eliminates dependency on core module's CommentsPage which CI's stale Gradle configuration cache never compiled. (Hush)
  • Core module cache bust — added invalidation marker to core/build.gradle.kts to force recompilation when new files are added. (Hush)
  • android.util.Log cleanup — replaced all remaining android.util.Log calls with Timber in Thumbnail.kt and MusicService.kt for consistency and ProGuard safety. (Hush)

Housekeeping

  • Version bumped to 13.13.8 (versionCode 169, app + waze-shim). (Hush)
  • Unit tests pass (all green). (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

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

13.13.5

Choose a tag to compare

@github-actions github-actions released this 24 Aug 08:46

Changelog

Changes in v13.13.4..HEAD:

Highlights

  • SpotiFLAC Integration — experimental alternative audio source with extension-based architecture, session management, and dedicated settings screen. (Hush)
  • Persistent URL Cache — stream URLs survive app reinstalls via MediaStore fallback; background refresh keeps URLs fresh. (Hush)
  • Liked Music Queue Fix — playing from YouTube playlists (including Liked Music with 800+ songs) now builds a full queue instead of a 1-song queue. (Hush)
  • GrowingListQueue — online playlist queues grow dynamically as the user scrolls, loading more songs into the playback timeline. (Hush)

SpotiFLAC (Experimental)

  • Extension-based audio source — SpotiFLAC integrates as a pluggable audio source via an extension framework (ExtensionRuntime, ExtensionManifest, ExtensionSignedSession). (Hush)
  • Session management — SpotiFLACSessionManager handles bootstrap, session persistence, HMAC rolling-key authentication, and automatic session recovery on 401/403 errors. (Hush)
  • Playback resolver — SpotiFLACPlaybackResolver resolves stream URLs from SpotiFLAC with quality cascade (FLAC → AAC → OGG) and relay gateway support. (Hush)
  • Settings screen — dedicated SpotiFLAC settings page with session info, test connection, quality selection, and developer diagnostics. (Hush)
  • Moved to Dev Settings — SpotiFLAC master toggle lives under Developer Options; Audio Sources page shows experimental warning label. (Hush)
  • YouTube toggle grey-out — when SpotiFLAC is disabled, the YouTube source toggle is greyed out (always on). When SpotiFLAC is enabled, both sources can be toggled independently. (Hush)

Player

  • Like button fix — heart/like toggle now correctly sends toggleLike with proper media ID and logs the result via Timber. (Hush)
  • YouTube song skipping fix — improved mid-playback error detection and stream URL recovery; HTTP 403/404/410 errors from expired YouTube stream URLs now trigger automatic URL invalidation and retry instead of skipping to the next song. (Hush)
  • Proactive URL refresh — background coroutine polls every 30 seconds and refreshes stream URLs before they expire, preventing mid-song failures. Configurable refresh interval in Settings → Storage (Off / 6h / 12h / Daily / Weekly). (Hush)
  • Playback startup speed — removed redundant player.prepare() calls that added 10–15s cold-start delay; prepare() is now called once immediately after ExoPlayer.Builder().build(). (Hush)
  • Stream recovery tracker — improved PlaybackStreamRecoveryTracker with better error classification and retry logic for transient network failures. (Hush)
  • Source label display — resolved case where the audio source label in the player bottom bar wasn't showing for all songs. (Hush)

Queue

  • Liked Music / Online Playlist queue — song taps in OnlinePlaylistScreen now build the queue from loaded playlist songs (same pattern as Top 100, Auto, Local, Cache screens) instead of YouTubeQueue.playlist() which returned 1 song for Liked Music. (Hush)
  • GrowingListQueue — new queue implementation that captures all currently-loaded songs at tap time and grows as the playlist screen loads more pages via scroll. Includes unit tests. (Hush)
  • MusicService auto-load-more — improved the auto-load-more coroutine logic; nextPage() call now only deduplicates when the first new item actually matches the last queued item. (Hush)

Waze Bridge

  • Queue display in Waze — Waze audio panel now shows the full playback queue with track names, artists, and artwork. Two-level MediaBrowser hierarchy: root shows "Hush Queue" group, drill-down shows individual tracks. (Hush)
  • Tap-to-play — tapping a song in Waze's queue automatically starts playback (skip + play command). (Hush)
  • Play/pause fix — Waze pause command now correctly pauses playback. Fixed broadcast fallback that was sending to a Service component instead of the dynamically registered WazeCommandReceiver. (Hush)
  • Previous track button — notification now shows Prev/Pause/Play/Next in compact view. (Hush)
  • Broadcast fallback — all Waze→Hush commands (play, pause, next, prev, seek, queue item, search, sync) now fall back to broadcast with action+package (no explicit component) when startForegroundService fails on Android 14+. (Hush)
  • sendCommandToHush unified — extracted sendWazeCommandToHush() helper for seek, queue item, and search commands with consistent broadcast fallback. (Hush)
  • onPlayFromMediaId routing — tapping a specific queue item in Waze sends the correct skip_to_queue_item command instead of generic play. (Hush)
  • CoroutineScope leak fix — replaced ad-hoc CoroutineScope(Dispatchers.Main) launches with a managed serviceScope that is cancelled on service cleanup. (Hush)
  • Debounce map cleanup — lastCommandTimeByType map now prunes stale entries every 50 commands to prevent unbounded growth. (Hush)
  • WazeCommandMapper tests — 14 unit tests for Messenger message → command translation. (Hush)
  • WazeCommandHandling tests — 19 unit tests for MusicService Waze command protocol. (Hush)
  • WazeBroadcastFallback tests — 13 unit tests for broadcast fallback contract. (Hush)
  • All 3 shim APKs updated — Spotify, YouTube Music, and Deezer shims rebuilt with queue display, auto-play, and reliability fixes. (Hush)
  • Updated waze-shims.zip — rebuilt bridge APKs with all fixes. (Hush)

YouTube / InnerTube

  • YouTube Music bridge package name — fixed package name for YouTube Music bridge integration. (Hush)
  • InnerTube page parsing — improvements to NextPage, HomePage, LibraryPage, SearchPage, and SearchSummaryPage parsing for better reliability. (Hush)
  • YouTube.kt refactoring — stream URL resolution, PoToken handling, and search improvements. (Hush)
  • InnerTube.kt — connection and request improvements. (Hush)

Downloads

  • DownloadUtil improvements — better stream URL resolution for downloads, improved error handling, and progress tracking. (Hush)

Sync

  • SyncUtils — improved sync reliability with better error handling and retry logic. (Hush)

Settings

  • SpotiFLAC toggle moved to Dev Settings — master SpotiFLAC ON/OFF switch now lives in Developer Options. (Hush)
  • Audio Sources page reorganized — SpotiFLAC settings section shows experimental warning; YouTube toggle greyed out when SpotiFLAC is off. (Hush)
  • Storage settings — background URL refresh interval configuration added. (Hush)
  • Debug settings — SpotiFLAC toggle added alongside existing developer options. (Hush)
  • Internet settings — minor improvements. (Hush)

Removed

  • JioSaavn module removed — entire jiosaavn/ module deleted (SaavnService, SaavnUrlDecryptor, DeviceRouter). JioSaavn is no longer a bundled audio source. (Hush)
  • SaavnPlaybackResolver deleted — removed JioSaavn playback resolution code. (Hush)
  • JioSaavnSettings deleted — removed JioSaavn settings screen and SaavnBetaWarningDialog. (Hush)
  • SaavnAudioQuality constants — removed deprecated JioSaavn audio quality constants. (Hush)
  • Debug secret dump — removed leftover hush_debug.txt file that was dumping the SpotiFLAC session secret to plaintext. (Hush)

Tests

  • GrowingListQueueTest — unit tests for the new GrowingListQueue implementation. (Hush)
  • AudioSourceQualityRankingTest — tests for audio source quality ranking logic. (Hush)
  • ExtensionRuntimeTest — tests for the extension runtime framework. (Hush)
  • SpotiFLACClientTest — tests for SpotiFLAC client relay error handling. (Hush)
  • SpotiFLACSessionManagerTest — tests for session bootstrap and exchange error paths. (Hush)
  • WazeCommandMapperTest — 14 tests for Messenger message → command translation. (Hush)
  • WazeCommandHandlingTest — 19 tests for MusicService Waze command protocol. (Hush)
  • WazeBroadcastFallbackTest — 13 tests for broadcast fallback contract. (Hush)

Build / CI

  • Waze shim build fixes — waze-shim/build.gradle.kts updated with proper signing configuration and build compatibility. (Hush)
  • ProGuard rules — updated R8/ProGuard rules for new SpotiFLAC and extension classes. (Hush)
  • resign-release-apk.sh — new script for resigning release APKs with the project keystore. (Hush)
  • Gradle libs.versions.toml — dependency version updates. (Hush)

Housekeeping

  • Version bumped to 13.13.5 (versionCode 166, app) / 167 (waze-shim). (Hush)
  • Removed dead code: SaavnAudioQuality, SaavnPlaybackResolver, JioSaavnSettings, SaavnBetaWarningDialog. (Hush)
  • Removed networkMain dispatcher import from InternetSettings. (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

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

Feature attribution

Source Examples integrated into Hush
ArchiveTune Core playback, YT sync, lyrics, Cas...
Read more

13.13.4

Choose a tag to compare

@github-actions github-actions released this 13 Aug 22:38

Changelog

Changes in v13.13.3..HEAD:

Fixes

  • fix: YouTube Music bridge package name fixed — com.google.android.youtubeMusic → com.google.android.apps.youtube.music (with proper dots), ensuring correct app identification and bridge functionality
  • fix: player theme selection persistence + version bump 13.13.4\n\n- Fix: PlayerDesignStyle.selectableValues now only includes V6 (non-deprecated style)\n Previously V9/V8 were marked deprecated and normalized to V6 on save, making selections appear to not work\n- Bump version to 13.13.4 (was 13.13.3)\n- Updated changelog with version bump and selection persistence fix\n- Lint errors already fixed in previous commits

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

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

Feature attribution

See the full table in README — Loot table.

Source Examples integrated into Hush
ArchiveTune Core playback, YT sync, lyrics, Cast, Together, local files
Metrolist Music alarms, loudness levels, playlist export, Android Auto settings
Vivi Music Playlist prefetch, auto-backup before update
Echo Music Settings search, IPv4/IPv6 network mode
ViMusic / OuterTune / BetterLyrics InnerTube base, UI patterns, synced lyrics