Repository navigation
Conversation
Three interlocking causes made each track start refetch the 2.9 MB player.js and spawn a fresh hidden WebKit process: - the cipher and PoToken webviews were torn down 15 s after use, which any song outlives; keep them while media is loaded and release after 5 min idle - the player hash was re-read from iframe_api on every resolve because current_hash.txt was written but never read; pin it for the cache TTL - a WEB_REMIX HEAD 403, routine on capped videos, ran the full self-heal (delete player.js, drop the bridge and the PoToken session) each time; allow one heal per ten minutes Measured on a Ryoku desktop: six consecutive track changes with no fetch and no webview build, where before each one paid ~1.2 s and a WebProcess.
mpv titled the stream by its URL, so the PipeWire node carried the full signed googlevideo link. Pass a per-file force-media-title (Artist - Title) through loadfile's options, quoted for the command parser and length-prefixed for the option parser so any title round-trips verbatim. Includes a probe example that asserts the round trip.
The reveal was gated on the Ryoku token IPC and retried for ~1.85 s against a 1.5 s native failsafe, so a cold WebKit mount was routinely revealed natively and unstyled. Race the palette against 250 ms; the native failsafe moved to 4 s in the previous commit's lib.rs change.
Tauri sets cfg(dev) whenever custom-protocol is absent, so the source PKGBUILD's plain cargo build produced a binary that opened build.devUrl. Default the feature on; cargo tauri dev still passes --no-default-features.
Both seek sliders hold the dragged value locally and commit it on the input's change event. Release the thumb outside the window (the bar sits on the bottom edge) and WebKitGTK delivers neither change nor pointerup; the local value then shadowed every later position tick and the timeline sat frozen for the rest of the session while playback carried on. Reproduced by dragging past the window edge: MPRIS kept ticking, the bar did not. A shared guard now commits the pending drag on pointerup, pointercancel, lostpointercapture, window blur, or the first pointer movement with no button held, and a track change ends a drag outright.
Home toggled ryo-is-scrolling on <main> on each scroll event and again 110 ms after the last one, and the queue/lyrics wheel helper did the same on its scroller. Rules keyed on that class matched every <img> and every promoted .ryo-art-wash layer, so each toggle was a full style recalc plus compositing-layer churn, twice per wheel notch. Scrolling is otherwise paint-bound in WebKitGTK (measured the same 60% of a core with and without the toggle), so the class and its rules go rather than get cheaper.
WebKitGTK 2.42's feature API can flip compositing borders and repaint counters on, which is how to see what a scroll actually repaints; the webkit2gtk crate does not bind it yet, so the five calls are declared here and gated on an env var that is normally unset.
The cold-start reveal failsafe grew to 4000 ms in 531c2b5 but the invariant still asked for 1500; the private-path scan matched the .git file of a git worktree; two files had drifted from rustfmt.
The host calls lastfm::spawn from Tauri's synchronous setup closure, on the main thread with no ambient Tokio context; the tokio::spawn that replaced tauri::async_runtime::spawn panicked there at startup. Take the runtime handle explicitly (the daemon will pass its own) and format the files the extraction left unformatted.
The page-stack enter animation is a ParallelAnimation of OpacityAnimator and YAnimator, both timed by Tokens.durFastEffects. That token zeroes under Ryoku's Reduce motion, and a render-thread Animator with duration 0 never applies its `to` value -- so pageStack was left at its `from` opacity 0 on every route change and on first load, blanking every page while the sidebar, player and daemon kept working. Route the three enter call sites through a reenter() that sets the end state directly when the duration is 0, and restarts the animation otherwise. Closes #193
The art column sized its cover with `artPx: min(width, body.height - 30)`, reading the column's own assigned `width` and the body RowLayout's assigned `height` -- both layout *outputs* -- and fed them into the Artwork's Layout.preferredWidth/Height and the title block's Layout.maximumWidth, which are layout *inputs*. Every layout pass therefore re-polished the child inside its own updatePolish, the `ColumnLayout called polish() inside updatePolish()` loop that stuttered window moves and resizes. Size the cover from layout inputs instead: the root's width/height and the header row's implicit height. The expression expands body.height to exactly what it resolved to before, so the cover is pixel-for-pixel the same at any size -- it just no longer reads the layout's own output back into it. Closes #196
…loss; feat: batch collection downloads with smart dedup
YouTube: the anonymous fallback clients now require a valid visitorData,
and the daemon's single non-retrying startup bootstrap left users whose
first fetch failed permanently bot-gated ('Skipped (unavailable)' on
every track). The bootstrap now retries with backoff, a gated resolve
surfaces as ResolveError::BotGated, and AppState re-bootstraps a fresh
visitorData on the spot (5-min cooldown, persisted) and retries once.
Spotify (Premium users saw a sign-in gate every launch and every track
skipped as unavailable): the startup restore is now announced via
spotify-auth restored/restore_failed instead of finishing silently
after the client snapshot; an invalidated librespot session is detected
(alive() was dead code) and recovered once per credential lifetime
under a lock that serializes it against the startup restore; a
sign-in-required failure stops the queue with the real reason instead
of toast-storming skips; and the Spotify device id is persisted per
installation instead of re-randomising every launch.
Downloads: playlists get a Download playlist menu item (parity with
albums); albums and playlists admit through one enqueue_collection RPC
that gathers every continuation page and dedups the whole collection in
a single folder pass. Smart dedup (FolderIndex) skips audio already on
disk under the exact stem, a collision suffix, or the same normalized
'artist title' from a different upload id, for single tracks too.
SoundCloud sign-in (opt-in; guest browsing/playback is unchanged): - crates/soundcloud gains an auth module: the captured OAuth bearer's lifetime is read from its JWT claims, and near-expiry tokens are rotated through the site's own refresh exchange under a single-flight lock (the refresh token is single-use; a double spend would log the session out). send() carries Authorization: OAuth <token>. - ryotunesd's soundcloud_login opens the site's sign-in in a WebKitGTK window (allow-listed to SoundCloud + its IdPs) and harvests the oauth_token/oauth_refresh_token/connect_session triple from the cookie jar, watching both load-finished and cookie-changed since the SPA lands the token without navigating. - The daemon proves the bearer against /me before persisting it under the daemon-only soundcloud_auth settings key (never UI_SETTINGS), re-installs it at startup, announces signed_in/signed_out/ restore_failed via a soundcloud-auth event, and clears + explains an expired session on first use. New RPCs: soundcloud_sign_in/out/status; the subscribe snapshot carries the soundcloud block. - Settings' SoundCloud row becomes a real sign-in control. Merged home: get_home (unfiltered) now composes one HomePage — the YouTube Music spine (works signed out, the default source) plus every signed-in provider's shelves: SoundCloud's own playlists/likes/ following ahead of its discover rows, Spotify's made-for-you rows whenever a Premium session exists (with on-demand recovery). A dead spine or provider degrades to the rest instead of blanking the page; continuation stays the YouTube feed's. Cards already navigate/play across providers via the existing id-prefix dispatch, so the QML renderer needed no change beyond dropping the now-wrong Spotify sign-in gate from Home.
- Collection downloads (albums/playlists) were broken from the start: CollectionEntry lacked #[serde(rename_all = "camelCase")] while the client posts camelCase, so enqueue_collection always died at deserialization with "missing field video_id". Single-track downloads were unaffected. The unit test built the struct in Rust and never crossed the JSON boundary, which is how it shipped green. - Batch jobs now carry their collection label/kind; the downloads queue and history collapse every batch into one card (cover, kind, aggregate progress, expandable track rows) instead of a wall of anonymous rows. Collection tracks land in a folder named after the collection on disk; FolderIndex's recursive stem walk keeps dedup working across layouts. - New crates/ryotunesd/src/diagnostics.rs: a ring buffer + rotating file ($XDG_DATA_HOME/dev.ryoku.ryotunes/logs/ryotunesd.log) fed by a tracing layer (warn+), the server's RPC-error chokepoint, the panic hook, download worker failures, and the client's log_client_error RPC. The ring seeds from the file tail at startup, so evidence survives a crash or upgrade. The client buffers its own failures (not-connected rejects, socket drops, error toasts) while the daemon is down and flushes them on reconnect. - Settings > Diagnostics page: filters by level/source/text, copy-all to clipboard, open-folder, manual refresh (no polling). - Fixed a pre-existing flake: Temp::new() keyed test dirs on pid+nanos, which collides across parallel test threads sharing a coarse clock; a process-local counter makes them unique (0/25 runs after, 1/12 before). - client/Logs.qml needed the Quickshell import for the Singleton type; DiagnosticsPage renamed its snapshot property from 'data' because Item.data is the default children property and shadowing it silently swallowed every child item.
enqueueCollectionFromPage already builds camelCase entries with toEntry; enqueueCollection then re-mapped them, reading video_id off objects that carry videoId. JSON.stringify dropped the undefined keys, so the client posted rows without any videoId at all — the second, stacked half of the broken batch download (the serde snake_case wire format was the first). Entries now map once and post verbatim.
clear_caches now wipes every layer that can wedge stream resolution —
cached stream URLs + lyrics, mpv's on-disk audio bytes, the stored
PoToken, and the per-video WEB_REMIX failure blacklist — and rotates
the anonymous YouTube playback identity (visitorData) on the spot, the
manual fix for "Couldn't load this track — YouTube rejected the stream
link". The RPC gained a {visitorDataRefreshed} result and a rotate
flag: the settings button rotates (default), a quality change clears
only the URL cache and leaves the identity alone. An offline re-fetch
still drops the token and reports it, so the next launch or a
bot-gated resolve re-bootstraps. Wired through the daemon, the Tauri
host, the Svelte dialog and the native QML settings page.
Verified live: rotate:false keeps the stored identity, rotate:true
replaces it, and the daemon answers the new shape on all three calls.
The embedded webview capture could not survive captchas or IdP popups, so users reported never getting signed in. Connect now imports the session straight from the browser's cookie stores (Gecko plaintext, Chromium v10 AES-CBC), default profile first, proving the bearer against /me before persisting it; when no session exists it opens soundcloud.com and polls the stores (mtime-guarded) until the sign-in lands or five minutes pass. The client mirrors the new waiting/expired states.
install_update left 'quit and reopen' as the user's chore. The client now spawns a detached helper that flock-waits for the old daemon's socket lock to release, then execs the launcher (socket-activating the new daemon and opening the new client), and asks the daemon to quit. The manual guidance stays as the fallback text if the helper fails.
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.
your design was good but it really was causing a lot of ram spikes and issues with audio.. if you want deeper description of what i did let me know.