The rules the whole codebase follows, the crates and what each may depend on, the threads of a running process, the error model, and a glossary of the Italian identifiers.
A handful of rules recur in every crate. Most of them are enforced by the type system or by a test, not by convention.
The domain decides, the application executes
rymembra-domain has no I/O, no clock and no global state: every function is a pure function. The decisions that can lose data live there — which rows a scan removes (plan_scan), whether two files are the same track (TrackKey, ContentKey), how two copies of a play count merge (merge_stats), what a restore would change (plan_restore). They are tested as plain calls, and several are checked against golden outputs recorded from the previous TypeScript implementation (core/rymembra-domain/tests/parita_*.rs). rymembra-app owns the database, the filesystem and the clock, and only executes those plans.
No lock is held while waiting for the network or a slow disk
Every crate that talks to the outside world — rymembra-net, rymembra-meta, rymembra-catalogo, rymembra-cloud, rymembra-scrobble, rymembra-ia, rymembra-archivio, rymembra-sync, rymembra-insieme — has no access to rusqlite. Holding the library lock during a request therefore cannot be written: the type that would be needed never crosses the crate boundary. Long jobs are split into phases that take and release the lock:
candidates() → decide() → apply() → record()
(lock held) (no lock, (no lock, (lock held,
network) disk) one transaction)
Your music files are not written
Metadata found online, corrections, covers, lyrics, plays and ratings live in the library database (rymembra.db). The only writes into a music folder are: the file a download produces (tagged by tag_scrittura::scrivi_tag), an optional .lrc beside a track (testi.accanto, off by default), and the temporary «write tags back into the files» action that undoes what versions up to 2.3.0 wrote — see Enrichment.
The network only answers a feature you use
There is no telemetry and no Rymembra server. Every outbound request belongs to a feature: the free catalogs, MusicBrainz and the cover archives, LRCLIB, GitHub for updates (not from a Microsoft Store copy, which the Store updates), and — only once connected — Google Drive, ListenBrainz, Last.fm or a language model. «Listen together», when switched on, talks only to other PCs on the local network: discovery datagrams, and encrypted connections to PCs that have been paired by comparing a four-digit code (Listen together). PRIVACY.md lists each request and what it carries. Secrets (OAuth refresh tokens, API keys, client secrets) live in the OS keychain under the service io.github.federicobaratti.rymembra and are read by the core just before a request; they never travel through the window.
Blocking code, no async runtime of our own
HTTP is ureq (blocking), not reqwest; background work runs on named std::threads; cancellation is a Fn() -> bool closure or an AtomicBool polled between blocks; deadlines come from rymembra_app::scadenza. Tauri's own runtime is used only to dispatch IPC commands.
Failures are data, and they are visible
Every error is a record from one catalog (Errors). Background loops run each pass inside catch_unwind and record panics in a health registry (Runtime); the release profile keeps panic = "unwind" so that Drop runs and the SQLite WAL is checkpointed even after a panic.
Skins are data, not CSS
A skin is a typed JSON document; the compiler in rymembra-skin is the only author of CSS, and no author-written string reaches the stylesheet. See Skins.
One Cargo workspace at the repository root (resolver = "3", edition 2024, rust-version = "1.90", version 3.1.0) with 16 members, plus the React frontend in apps/desktop/src. A root-level cargo test builds and tests the desktop app too, not only the libraries.
Crate
Role
Internal dependencies
rymembra-domain
Pure decisions: keys, scan plan, album grouping, matching, queue, listen rule, merge, restore plan, lyrics parser, error catalog
—
rymembra-play
Audio engine: decode (symphonia + Opus), resample, EQ, gain, spectrum, cpal output; fingerprint, loudness and onset analysis
domain
rymembra-app
The library on SQLite: migrations, scan, enrichment, playlists, lyrics, backup/sync bridges, profile
The direction is binding: nothing below rymembra-app sees the database, and rymembra-cloud is never a dependency of another core crate. rymembra-app is kept platform-neutral (bundled SQLite, a MusicFiles trait instead of std::fs) because it is meant to be shared with an Android build; only Windows ships today, and rust-toolchain.toml targets x86_64-pc-windows-msvc.
Shared dependencies
Declared once in [workspace.dependencies], each with a written reason: ureq 3.4 with platform-verifier (TLS roots from the OS store), zip 8 with only deflate, blake3, sha2, keyring, encoding_rs, unicode-normalization, serde, flate2, getrandom. The frontend's runtime npm dependencies are React 19, @tauri-apps/api, @tauri-apps/plugin-dialog and CodeMirror 6.
Rymembra is one process: a WebView2 window and the Rust core. The tauri-plugin-single-instance guard brings an existing window to the front instead of starting a second process on the same database (it is skipped when RYMEMBRA_DATI points at another data folder).
The two locks
Library — Stato.libreria: Mutex<Result<Libreria, AppError>>: one SQLite connection plus the cover store. Not a pool: SQLite has one writer anyway and reads take microseconds. If the database failed to open, the error is kept and returned to every command, so the window can still start and explain.
Player — StatoLettore.lettore: the engine handle, the queue, the listen tracker. A scan may hold the library lock for a batch; pause must not wait for it.
Order rule: player first, then library, never the reverse. Next to the library mutex, never behind it, sit values that never change or are single bits: the cover-store path, the data folder, scansione_da_fermare (cancel) and scansione_in_corso (one scan at a time).
Helper
Behaviour
con_libreria
Waits as long as needed; logs the waiting thread after ATTESA_DA_NOTARE = 2 s. On the UI thread it gives up after ATTESA_DELLA_FINESTRA = 4 s with db.locked (Windows marks a window «not responding» after 5 s).
con_libreria_entro
Bounded wait; None means «not executed». Used on exit (250 ms for the last PRAGMA optimize).
lucchetto_entro
try_lock every PASSO_ATTESA = 5 ms. A poisoned mutex is recovered, not treated as busy.
Turno::prendi / adotta
An AtomicBool guard released in Drop, even while unwinding. TurnoDiScansione also wakes the maintenance thread when a scan ends.
Where commands run
A plain #[tauri::command] runs on the UI thread; (async) commands run on Tauri's worker pool (one thread per core). Anything that may wait on a disk — scans, folder probes, profile export/import, lyrics lookup — and the heavy library reads go through disparte::in_disparte / leggi_la_libreria, i.e. spawn_blocking, so a dead network share cannot exhaust the workers.
Long-lived threads
Thread
Module
Cadence and job
cpal callback
rymembra-play::uscita
Real time: pops samples, EQ, spectrum tap, gain, limiter
rymembra-uscita
rymembra-play::uscita
Owns the stream; sleep/wake policy (1 s awake, 250 ms asleep)
rymembra-decodifica
rymembra-play::motore
Decodes into the ring; idle sleep 4 ms; runs under catch_unwind
Ordered slow work: state events, SMTC, tray, listen writes
rymembra-preparatore
riproduzione::fili
Opens the next track for gapless; 15 ms debounce
rymembra-orologio
riproduzione::fili
Every 250 ms: position event, saves, device loss, sleep timer
rymembra-spettro
riproduzione::fili
33 ms while the spectrum is visible, 250 ms otherwise
rymembra-dispositivi
riproduzione::uscite
Polls output devices every 2 s; reopens; backoff to 30 s
rymembra-sorveglia
sorveglia
Watched folders: 2 s debounce, 60 s cap, 5 min network polling
rymembra-nuvola
nuvola
Drive backup: first pass after 30 s, then every 15 min
rymembra-sincronia
sincronia
Device sync: after 20 s, then every 5 min
rymembra-arricchimento
arricchimento
Enrichment: after 90 s, then every 30 min
rymembra-manutenzione
manutenzione
After 60 s: quick check; optimize every 6 h; cover cleanup
rymembra-analisi
analisi
After 2 min, every 20 min; yields while music plays
rymembra-scrobble
scrobble
Drains the queue on demand or every 300 s
rymembra-aggiornamenti
aggiornamenti
10 s after the window is ready, then every 30 min
rymembra-insieme
insieme::filo
Listen together, when on: the session's single thread (network events, commands, state and anchors); helpers -rete, -esecutore (members' gestures on the host), -preparazione, -descrittori, -copertine, -campanello (turns the player's wake-ups into the session's mail, a burst counting as one) and -sincronia (a member's regulator, every 100 ms)
Other threads start on demand: rymembra-scadenza-* (one open with a deadline), rymembra-operaio-* and rymembra-a-rate-* (scan reads and walks), rymembra-volumi-*, flusso-anticipo (HTTP prefetch), rymembra-procura (downloads), rymembra-testi and rymembra-testo-dopo (lyrics), rymembra-scrobble-adesso, rymembra-ia, rymembra-aggiornamento (an install), rymembra-tasti (media keys, from the first press), cartelle-sfratto (while the Folders tree is in memory), rymembra-risveglio (wakes network roots at start).
The periodic-thread template
stato::avvia_filo_periodico is shared by backup, sync, enrichment and maintenance: a start delay, then recv_timeout(intervallo) as both the period and the wake-up channel. A wake-up that marks data dirty waits for its raffica (burst) to go quiet before the pass; aspetta_la_raffica tells «quiet» from «channel closed» so that closing the app never starts a pass. Each pass runs inside sorvegliato (catch_unwind); a panic is logged, counted in the registry read by the fili_salute command, and retried after 5 s, doubling to 10 min.
Every failure in Rymembra is one of 153 codes declared in a single catalogo! macro invocation. One line per error generates ErrorCode (with typed parameters, #[non_exhaustive]), ErrorCodeKind (without them, with ALL), and the metadata: code() ("net.offline"), i18n_key() — always concat!("errors.", code), never written by hand —, domain(), severity(), retry_rule() and the legacy_code() of the previous app.
Domain
Codes
Domain
Codes
net
7
download
15
db
10
spotify
3
fs
11
catalogo
4
playback
8
skin
12
library
11
transfer (reserved for phone transfer)
9
metadata
11
sync
7
settings
7
ia
8
ipc
3
internal
5
insieme (Listen together)
22
—
—
Severity is Info (an expected outcome: «no match», «cancelled»), Warning, Error or Fatal. RetryRule is Always, Never or HttpStatus, which retries only 408, 429 and ≥ 500. Retryability drives automatic retries in rymembra-net and the «Retry» buttons in the window.
AppError
AppError { code, message, cause }. message is for developers and never shown; the user-facing sentence is the translation of the i18n key with the code's parameters. with_cause flattens the cause to one line and truncates it at CAUSA_MASSIMA = 500 characters (an HTML error page from a catalog would otherwise flood the log). Display prints [code] message (causa: …).
errors::rete::e_di_rete classifies an io::Error as «network» from the raw Windows error number (51, 53, 64 ERROR_NETNAME_DELETED, 121, 1231, 2250 ERROR_NOT_CONNECTED, …). It is in the domain crate because both rymembra-app (opening a file) and rymembra-play (reading it mid-stream) must answer the same way.
Across IPC
errore::ErroreIpc { code, domain, severity, retryable, i18nKey, message, cause } is the serialized form; commands return Esito<T> = Result<T, Box<ErroreIpc>> (boxed so successful responses stay small). errore::errore() is the single bridge and writes code, severity, retryability and cause to the log.
In the window, eErroreIpc is the type guard, testoErrore returns tSe(i18nKey, message ?? cause ?? code), dettaglioErrore adds the service's own wording when it differs, and Avvisi.tsx maps info → note, retryable warning → warning with Retry, anything else → blocking alert. strumenti/lingue.js fails if a catalog code has no errors.<code> sentence in it.json.
The Tauri shell (rymembra-desktop), tauri.conf.json, capabilities/, and msix/ — the Microsoft Store package's manifest template, identity (identita.json), logos and listing texts (scheda-store.md)
apps/desktop/rymembra-barra/
Taskbar buttons crate
apps/desktop/src/
React frontend; test/ holds its node --test suites
strumenti/
Repository checks and release tools (Node, no dependencies)
site/
The sources of the website — the landing page (one template, testi/ in four languages) and these docs — built into _site/ by strumenti/sito.js and published by .github/workflows/sito.yml
.github/workflows/
verify.yml, release.yml, sito.yml, msix.yml
Lints
Workspace lints apply to every crate: unsafe_code = "forbid" (only rymembra-barra relaxes it to deny, with unsafe confined to one module), missing_docs = "warn", and Clippy deny on unwrap_used, expect_used, panic and indexing_slicing — a panic in a domain function would stop the music. clippy.toml allows all four in tests. CI builds with RUSTFLAGS=-D warnings, so every warning is an error there.
Release profile
opt-level = 3, lto = "thin", codegen-units = 1, strip = "symbols", and panic = "unwind" for three documented reasons: Drop must run so the SQLite WAL is checkpointed; analisi relies on join().ok() of scoped threads; a malformed file that panics symphonia must cost one track, not the process.
Conventions
Files open with a doc block whose # Perché … sections record the reason, the rejected alternative and, often, the bug that led to the current shape. Read them before changing behaviour.
Constants carry names and units in the identifier (ATTESA_AVVIO, RIPOSO_MS) and a comment with the reasoning behind the number.
Pure rules are extracted into DOM-free modules on the frontend (selezione.ts, rada.ts, modale.ts…) so node --test can load them.
Line endings are LF everywhere (.gitattributes: * text=auto eol=lf).
The toolchain is stable locally (rust-toolchain.toml) and pinned to 1.97.1 in CI through RUSTUP_TOOLCHAIN.