Conversation
- Both APIs could only hand back a whole list, which is what made the library screens wait through every page before showing anything; a page and its cursor are now the primitive, and the list versions are a walk over it. - A failed page stays fatal when nothing has arrived and is logged once something has, so a short list cannot be mistaken for a complete one. - The cursor is opaque on purpose: YouTube hands back a `nextPageToken` and SoundCloud a `next_href` URL, neither of which the caller should interpret.
- Every entry into the Library re-fetched everything: the list walks page by page (`ceil(n/50)` serial requests) and then enriches it in chunks, so a 1.000-track library is ~8 s of the same work on every visit — and the screens are rebuilt on every tab change. - The lists are now cached per account for ten minutes, playlists included, with the load under the lock so the warm-up racing a screen shares one walk instead of starting a second. - `LibraryWarmup` builds the YouTube library in the background on start-up and on sign-in (the moments the fetch would otherwise happen in front of the user), which is what makes the first open instant too.
- The screen now receives the library progressively — the first page as soon as it is enriched, the rest while the user is already reading — instead of one list at the end of the walk. - The finished list lands in the existing cache, so the warm-up and the next visit are still one emission; stopping mid-walk stores nothing, which is what keeps a cancelled fetch from becoming the cached answer. - `musicArtists` is shared by the one-shot list and the flow so both narrow the subscriptions to music the same way.
- The screens waited for the whole walk before rendering anything; they now collect the pages and show the first one, enriched, as soon as it exists — the rest appends underneath while the user is already reading, playlists included. - `Flow.catch`, not `runCatching`: it does not catch the collector's cancellation, so leaving the tab mid-walk can neither show a message nor leave a half list cached. - Opening a second playlist cancels the first walk instead of racing it.
- `null` means "not fetched yet" and the empty hint meant "fetched, and there is nothing": treated the same, opening Library showed "no liked songs yet" for a beat before the first page arrived, whatever the library actually held. - The open playlist had the same problem from the other side — its tracks started as an empty list, not as nothing — so it flashed "this playlist is empty" on the way in.
- Emitting per page means a page can legitimately arrive empty — the most recent fifty likes may hold no music at all once filtered — and the screen read that as "no liked songs yet" before the next page brought the list. - `loading` now stays until something arrives, or until the walk ends, and the per-page log says how far it got, so a partial answer is never mistaken for the finished one.
- YouTube Music's own `FEmusic_home` and `FEmusic_explore` browse pages replace the "discover" tab, which only existed because the Data API has no feed. - `discover` becomes `explore` and loses its per-platform label: now that both platforms have a real feed there is nothing left for a label to disclaim. - Home keeps the radios seeded by local plays, because InnerTube rejects the Google token and the editorial shelves therefore come back geographic rather than personal. - A collection walk follows continuations, so a long playlist is not silently truncated.
- the bar is home/search/explore/library on both platforms, which is what makes room for the Home tab; Now Playing stops being a destination because tapping the item that is playing already opens the player, and the bar opens it from anywhere else. - the media-widget tap goes to the player screen, and back only closes it when something is behind it, so a widget tap at a cold start still leaves the app as it did before. - a card can now open another feed, so a mood or genre card does something instead of silently nothing; the card says which of the two it is, because asking the network to find out would be a second round trip and a guess.
- SoundCloud lists the same system playlist twice inside one selection, which crashed Explore at measure time with "Key ... was already used". Four other lists keyed items by id alone and had simply not been handed a repeat yet: search results, library playlists, library artists and a collection's tracks. - the repeated card is dropped as well, because rendering the same playlist twice tells the user nothing, and a list that repeats an item should not be a way to take the app down.
Home and Explore feeds, the reordered tab bar and the duplicate-key fix.
- the interface defaults each `…Flow` to an empty list, so the Library read as empty while the warm-up behind it had already fetched 506 tracks; only SoundCloud implemented them, and the four page imports sitting unused in this file were the shape of the work that was never done. - a playlist's cache key never interpolated the account, so one account's tracks could be served to the next, against the rule the rest of the class keeps.
- the status was never checked and nothing was logged, so a 403, a 429 and a bot check all parsed as "no results": a throttled request and an empty catalogue were the same outcome, and neither left a trace to diagnose from. - a body that does not parse is reported too, with the first of it, which is what named the resolver failure in the end.
- lrclib answers `okhttp/4.12.0` with a Cloudflare 520, deterministically, on both of its endpoints, while the same request from curl or a browser is served — so every lyrics lookup failed and the log blamed the network. - a default only: InnerTube identifies itself by user agent, so callers that set one keep it. Four call sites were relying on the default, this one was simply the first to hit a service that cares.
- `/api/get` compares track name, artist and duration exactly, and a YouTube title carries "(Official Video)" and "(Remastered)" and friends, so it missed lyrics that exist; `/api/search` finds them, and the duration is checked before a hit is used so a live take is not pasted onto the studio recording. - the title and artist are cleaned of upload noise first, the takes that name a different recording (Live, Acoustic, Demo, feat.) are kept, and the answer is cached so a track with no lyrics does not ask again on every play. - a request that failed now throws instead of answering null: "there are no lyrics" and "we could not ask" are different answers, and lrclib hands out 503s under load.
…player - the sheet holds the queue and the lyrics, so reaching them is the same gesture that opens it rather than a second surface with its own way back; the player stays behind them. - the line being sung is centred and larger and the neighbours shrink, because scaling the active line up pushed a long line past the edge of the screen, and that is the one line you have to be able to read. Scaled through graphicsLayer, so the item heights stay fixed under the auto-scroll. - a synced line seeks to its own time on tap, on desktop as well as the phone. - the mini player is hidden while the player itself is open: the collapsed sheet already carries the same track and expands on a tap, so a second copy only repeated it in less detail. - the player draws over the tab instead of replacing it, so closing it returns to the playlist, the search or the scroll position that tab had open rather than rebuilding it at its root.
…ndpoint - the app used to build the authorize URL itself, with the web player's client id and a redirect to a URI SoundCloud owns; that page is the one DataDome guards, and it never got past it. Loading the ordinary sign-in and letting SoundCloud's page run its own flow with its own client id and redirect signs in cleanly — measured on the device: sign-in, code, post-signin redirect, session, no challenge anywhere. - the session is read from the page: `oauth_token` out of the cookie jar, with the page's own storage as the fallback. It is validated against /me before it is stored, the same call the pasted-token path uses, so a token that does not work is refused rather than kept. - no redirect interception is needed any more, which was the only reason the embedded browser had to be a WebView we drive by hand. - the button said `sign_in_with_browser`, which is no longer true of anything.
- `buildAuthRequest`, `parseCallback` and `exchangeCode` had no caller once the sign-in page started minting the session itself, and the nonce/state dance and its PKCE tests existed only to serve them. `computeCodeChallenge` and `generateCodeVerifier` stay: they live in OAuthFlow and are still Google's. - what is left is the token shape and the refresh. The refresh is unreachable while nothing stores a refresh token — the page keeps the session in a cookie — which is a known gap rather than a finished path, and the reason the access token expiring means signing in again.
…-login - the app meant to keep the session alive: `refreshToken` and `expiresAt` are persisted and `ensureValidToken` was called before every api-v2 request. But nothing ever populated either field, so it returned early on every single call — dead code wearing the shape of a safeguard, which reads worse than nothing. An hour in, the session simply stopped working. - the page keeps both halves in cookies. `oauth_token` is an 861-character JWE; the refresh token is 32 characters under `oauth_refresh_token`, which is why looking for it in the first place was worth doing before designing around its absence. - a refresh has to be presented by the client that got the grant, and the mobile flow's id is not the one scraped for api-v2 — presenting the wrong one is what `invalid_grant` was saying. The id is read off the authorize URL on the way past and carried on the session. - renewal is triggered by the API refusing the token, not by a clock: the page never exposes a lifetime, and inventing one would have been a guess whose failure mode is silent.
- the resolution for a download is anonymous — no token, just the scraped public client id — so this is not really about the account. It is about the client id: a client fetching whole files is the behaviour those services police, and it is what gets an id rotated, which breaks the app for everyone using it including this one. - off unless turned on, with the toggle and its reasoning in the accounts dialog. Saving your own upload stays possible without archiving everything you play being the default.
…it works - it talks to undocumented internal endpoints, scrapes the public client id those web players use, and reads the SoundCloud session out of its own WebView after sign-in. All of it is against the terms of use, and none of it is visible from a feature list. - published so it can be read and run by whoever chooses to, each on their own account and at their own risk. No credentials ship; a fork keeps the warning.
- the scraped client id is cached for six hours and is the only thing authenticating these calls. When SoundCloud rotates it, every stream failed for the rest of that window, while the library path — which has re-scraped on a 401 for a while — healed itself. Same signal, now the same response: invalidate and retry once. - a 5xx, or a connection that never got there, returned null and cost the listener the track. One retry with a short pause now. - `runCatching` there also swallowed CancellationException, so changing track mid-resolve was recorded as "no stream" and the player skipped a second time on top of the skip that had just cancelled it. - all four ways the resolution can fail now say which one it was. Before they returned null in silence and the only trace was the engine's "No stream URL …; skipping", which named the symptom and never the cause.
SoundCloud sign-in through its own page, session renewal, lyrics read in the sheet, and the risk work around them: SoundCloud downloads off unless asked for, and a README that says what the app does to those accounts instead of only what it does for you.
- the InnerTube home feed rejects the Google token, so its shelves were geographic rather than personal; a signed-in Home is now built from what only the account knows, leaving the editorial feed to guests - share the subscriptions walk between the library and Home's release shelf
- now playing with previous / play-pause / next, driven by the same playback service so the controls survive the app being backgrounded - the widget has no seek bar, so position stays out of its state and a repaint only follows track, artwork and transport changes
- Glance runs provideGlance once per session and answers later updates by recomposing the same lambda, so title, transport and artwork captured outside it stayed frozen: play never turned into pause and the cover lagged a track - SizeMode.Exact and LocalSize replace the launcher options, which reported a taller card than drawn and let the artwork spill out of the frame - YouTube's 4:3 thumbnails are cropped to their 16:9 frame instead of the bars - restyle after the YouTube Music widget: flush square cover, title with artist, plain controls spread evenly, neutral tint from the cover
- a SoundCloud track gets a like, a YouTube track a like through the Data API, which is what puts it in the library's collection; hidden when signed out - the widget observes both like sets and looks the YouTube rating up per video - the action reaches the engine through the playback service like the transport
- a network error used to skip to the next track at once; the engine now waits with backoff, drops the cached URL and reloads the same track where it stopped - buffer minutes of audio ahead and give each chunk more in-loader retries - an expired or IP-bound URL after a network switch takes the same path
- newer uploads offer HLS only; fall back to it when there is no progressive transcoding, MP3 ahead of Opus - SoundCloud playlist URLs carry no .m3u8, so the media item states the type
- monetised tracks still list plain MP3 renditions but answer 404 for them; only the encrypted ones are served, and Wren does not touch those - a track listing any encrypted rendition is marked as it is parsed, so song rows dim it, show a lock and refuse the tap before any playback attempt - the engine keeps a readable notice on the bar when one is skipped anyway
- the player is not a browse surface, so the YouTube/SoundCloud switch above it changed nothing and read as part of the screen
- the set only filled as listings were parsed, so a restored queue showed a protected track unlocked until its list loaded again - stored in the state dir with the same temp-file-and-rename write as the playback state; loaded right after AppDirs on both platforms
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Wren 0.6.0
A Home tab with real feeds, a home-screen widget on Android, and a player that survives bad coverage.
Android
Browse
SoundCloud
Fixes
Full log:
git log v0.5.1..v0.6.0