Skip to content
Rymembra Docs GitHub

Docs

Network

One blocking HTTP client for every service, per-service pacing with a circuit breaker, seekable HTTP streams, and the OAuth pieces that do not depend on the provider. The LAN transport of «Listen together» is a separate crate, rymembra-insieme: see Listen together.

The HTTP client#

Source core/rymembra-net/src/http.rs core/rymembra-net/src/immagine.rs core/rymembra-net/src/lib.rs

rymembra_net::Rete wraps a ureq 3 agent (cheap to clone; clones share the connection pool). It sees only the error catalog — never the database — which is what makes «a lock held during a request» impossible to write.

  • Status is not an error: http_status_as_error(false) keeps 4xx/5xx bodies, because the body is the information (invalid_grant vs invalid_client from Google). stato_a_errore translates when the caller wants the default mapping (net.http { status }, net.rateLimited, …).
  • TLS uses the operating system's trust store (rustls-platform-verifier), so a corporate proxy with a root installed in Windows works, and revoked roots go away with Windows updates. https_only everywhere.
  • User-Agent: Rymembra/<version> (+https://github.com/federicobaratti/rymembra) — MusicBrainz policy asks for a contact. The update check is the exception and sends no Rymembra user agent (Updates).
  • Body cap: TETTO_CORPO_DI_SERIE = 10 MiB unless a client raises it (con_tetto_corpo). Images are recognised by magic bytes (immagine::tipo), never by Content-Type.
ConstructorTimeoutsRedirectsUsed by
Rete::nuova(service, t)global tLiberi(10)metadata, catalogs, most clients
Rete::per_prelievoDNS 10 s, connect 15 s, headers 30 s, body 30 mindefaultdownloads
Rete::per_flussoas above, body 90 snone until a gate is givenFlussoHttp
Rete::verso(service, t, url)global tnonelanguage models: plain http allowed only to 127.0.0.1/localhost

Redirect policy

ureq's own redirects are disabled; each client declares Rimandi: Nessuno (a 3xx is a response — for every request that carries a secret or a body: OAuth, scrobbling, language models), Liberi(n) (followed by ureq, Authorization stripped), or Sorvegliati { quanti, cancello } — followed by hand, GET only, each hop approved by a Cancello (Arc<dyn Fn(&str) -> bool>) supplied by the caller: Drive allows at most 2 hops to Google hosts, streams 5 hops within the same catalog. Redirect statuses are 301, 302, 303, 307, 308.

Retries

ritenta / ritenta_finche retry only errors whose catalog rule says so (AppError::is_retryable: network down, timeouts, 408, 429, 5xx), backing off from 1 s and doubling up to ATTESA_MASSIMA = 30 s, honouring Retry-After and X-RateLimit-Reset-In. A 400 or 401 is never retried. preleva streams a body to a file in 64 KiB blocks with a cancel check; flusso streams a POST response (SSE); intervallo_finche fetches a byte range with 3 attempts inside a 45 s budget, checking cancellation every 50 ms.

See also: Pacing, circuit breaker and response cache, Seekable HTTP streams, OAuth and the keychain

Pacing, circuit breaker and response cache#

Source core/rymembra-meta/src/cadenza.rs core/rymembra-meta/src/deposito.rs core/rymembra-meta/src/lib.rs core/rymembra-app/src/enrich.rs

Retrying after a 503 is a second request that arrives too early. rymembra_meta::Cadenza is a gate before each request: one per service, it reserves the next slot under a short lock and sleeps outside it.

ServiceMinimum spacing
MusicBrainzRITMO_MUSICBRAINZ = 1,100 ms (their limit is 1 req/s per IP)
Cover Art Archive250 ms
iTunes Search350 ms
Deezer220 ms
LRCLIB250 ms
ListenBrainz Labs1,000 ms

Circuit breaker: after GUASTI_PER_APRIRE = 3 consecutive transport failures the service is considered down for RAFFREDDAMENTO = 5 min and calls fail at once with net.circuitOpen; a 404 counts as success. There is no half-open state: services are queried in bursts, and the next burst after the cooldown is the probe. Without it, an enrichment pass on an offline laptop would wait through 120 twenty-second timeouts.

Cache: Fornitori remembers answers through the Deposito trait, implemented in rymembra-app by DepositoSqlite over the enrich_cache table on a secondary connection (expired rows are deleted on open). Lifetimes: search 7 days, release 30 days, «not found» 3 days, affinity 90 days, affinity «not found» 30 days. json() distinguishes Ok(None) («they don't have it», cacheable) from Err («unknown», not cached).

See also: The HTTP client, Enrichment, Lyrics

Seekable HTTP streams#

Source core/rymembra-net/src/flusso.rs apps/desktop/src-tauri/src/riproduzione/flusso.rs

FlussoHttp is a Read + Seek + Send + Sync over HTTP Range requests, so the decoder can read tags at the end, return to the first frame and seek — without any file on disk (Jamendo's terms forbid caching; TERMS.md § 2). At most two windows are in memory and they are overwritten as playback advances.

  • Window FINESTRA = 256 KiB, doubling on sequential refills up to FINESTRA_MASSIMA = 2 MiB.
  • After ANTICIPO_DOPO = 64 KiB of sequential reading, the next window is fetched on thread flusso-anticipo; a failed prefetch backs off 1 s → 16 s. Without prefetch, each window was a silence mid-song on a slow line.
  • The constructor makes one request immediately to learn the total length (Content-Range) and remembers the node a redirect led to (DaRete), saving ≈ 900 ms per request on the Internet Archive.
  • A seek elsewhere abandons the in-flight request (pezzo_finche) instead of competing for the line.
  • ascolta_annullo takes the engine's cancel flag, so a new command interrupts a read waiting on the network.

Errors must look like the network. Symphonia only sees io::Errors; da_app maps net.offline to OS error 1231, net.timeout to 121, everything else to 64 (ERROR_NETNAME_DELETED) — numbers errors::rete::e_di_rete recognises — so a dropped stream becomes a retryable playback.streamInterrupted instead of «file damaged».

See also: Playing a catalog stream, The HTTP client, Listen together on the LAN

OAuth and the keychain#

Source core/rymembra-oauth/src/lib.rs core/rymembra-oauth/src/pkce.rs core/rymembra-oauth/src/loopback.rs core/rymembra-oauth/src/portachiavi.rs core/rymembra-oauth/src/scadenza.rs core/rymembra-cloud/src/oauth.rs

rymembra-oauth holds the parts of OAuth 2.0 that are the same for every provider, and no HTTP; provider details (endpoints, scopes, form fields) stay with the provider's crate (rymembra-cloud for Google).

  • PKCE (RFC 7636): verifier = base64url of 32 random bytes (43 characters), challenge S256; plus a random state.
  • Loopback (Attesa): binds 127.0.0.1:0 — a port chosen by the OS, never fixed (guessable) and never 0.0.0.0 (the LAN). Non-blocking accept polled every 100 ms; request line ≤ 8 KiB, ≤ 64 headers; a wrong state gets a 400 and the wait continues; cancellable; replies with a small localized page. Consent always happens in the system browser: a webview able to draw accounts.google.com would be a phishing surface.
  • Keychain (Portachiavi trait): DiSistema uses keyring (Windows Credential Manager) under the service io.github.federicobaratti.rymembra; InMemoria for tests; Guasto when the keychain is unreachable. It never falls back to settings. Entries written by the previous version under its own service name are read as a fallback: the first read copies the value under the current name and, only if that succeeded, removes the old entry; disconnecting removes both, so a forgotten account does not come back.
  • Expiry: tokens are refreshed ANTICIPO = 60 s before they expire.

Never written into an error (and therefore never into the log): the code verifier, authorization codes, access and refresh tokens. Keys stored in the keychain include google.refresh_token, google.client_secret, lastfm.api_key, lastfm.segreto, lastfm.sessione, listenbrainz.token, the per-profile language-model keys, and insieme.chiave.<device id>, the private key with which this PC identifies itself to the PCs it is paired with.

See also: Backup on Google Drive, Scrobbling, Language models