More than half of the crate files had no module doc, so a contributor had to
read a large file top to bottom to learn what it owns. Every file over 300
lines now states its responsibility in one or two lines.
Six writers repeated the same publication sequence: create a temporary file
in the target directory, write it, sync_all, optionally run a cancellation
check, then NamedTempFile::persist over the target. `core::fs::write_atomic`
runs exactly that order and optionally fsyncs the directory afterwards
(best effort, as extension storage did).
Each site keeps its temporary-file prefix (`.spotiflac-` for extension
storage, `.spotiflac-m3u-` for M3U export, tempfile's `.tmp` default,
exposed as `DEFAULT_TEMP_PREFIX`, for the registry cache, lyrics cache and
both network cover writers), its check placement between fsync and rename,
and its error text: `WriteAtomicError` displays a persist failure with
tempfile's own message, and converts to the underlying io::Error where the
site previously used `PersistError::error`.
Not converted, because their publication differs: playlist staging and
collection snapshots use persist_noclobber (collections also skips fsync),
the backup restore stages into a temporary directory, and the extension
package download maps each step to its own error context.
spotiflac-core now depends on the workspace `tempfile` already used by the
other crates; the lockfile only gains that edge.
Keep one copy of helpers that were implemented several times, each proven
equivalent to the copies it replaces by a test that runs the removed
implementation (kept inline in the test module) against the survivor:
- Go-style lexical path clean: new `core::fs::clean`, used by
`isrc::native_files` and re-exported as `extensions::files::clean`.
- Dart `String.trim` whitespace: `path_keys::dart_whitespace` and
`dart_trim` now back the lyrics parser's explicit character list and the
playlist importer's private `trim`; the test compares every Unicode
scalar value.
- `normalized()` (Dart trim then simple lowercase): the native backup
importer uses the identical `native_library::normalized`.
- Lowercase hex encoding: `spotiflac_core::lower_hex` replaces the per-byte
`format!` collectors in data_jobs and network_tags, the signed-session
`hex` and the `hex` arm of `binary::encode`; checked for all 256 bytes.
- ID3 syncsafe decode: the tag writer's `size` reuses the reader's
`tags::id3::syncsafe`; checked across the 0x7F/0x80 boundaries per byte.
- Bounded body reads: `HttpStream::read_limited` replaces the read-to-limit
loops of the lyrics HTTP client, the resolver HTTP client and the
extension registry reader. Callers keep their buffer sizes (16 KiB,
16 KiB, 64 KiB), their own error mapping and their messages; a loopback
test compares it with the previous loop at and around the limit.
The mobile ID3 `synchsafe` is not merged: it validates the length and the
high bits and returns `None`, unlike the core decoder.
native_lyrics.rs kept only the shared types, has_usable_timing and the
execute entry point. Lexing and timing helpers move to text, the LRC
parser to lrc, the TTML parser and XML normalization to ttml, credit
extraction (with its regex statics) to credits, and the tests to
tests.rs. Function bodies are unchanged; only visibility on moved items
and use lines differ. The fixture include_str path gains one parent
segment for the new file depth.
RustCoreBackend.invokeApplication handled readFileMetadata,
editFileMetadata and cancelNativeDataJob, but MainActivity intercepts
all three first: the metadata methods are in nativeBackendMethods and
dispatchBackendApplication answers cancelNativeDataJob before it calls
invokeApplication. No Kotlin code calls invokeApplication with those
names.
Dart never sends these channel methods, so their handlers go on every
platform:
- shareMultipleContentUris (Android) and getAvailableLyricsProviders
(Android and iOS) have no Dart caller.
- scanLibraryFolderIncrementalFromSnapshot and
scanSafTreeIncrementalFromSnapshot are called only from the Dart host
branch of the library scan, which runs only when
supportsCoreBackend is false, that is never on Android or iOS. The
unreachable Dart wrappers are removed separately.
The allowlist and the when cases in MainActivity stay in sync (70
names). The UniFFI exports get_available_lyrics_providers_json and
scan_library_folder_incremental_from_snapshot were used only by the
removed handlers; the backend methods behind them stay.
Rust is the only backend, so several single-backend switches had become
constants and their other branches could never run:
- CoreBackend.supportsOutputDescriptors was always false. The
descriptor branch in SafDownloadHandler and its pfd/detachFd cleanup
never ran, and the Rust planner rejects output_fd anyway.
- routesApplication was always true on Android and iOS. Drop the member,
the "go" value of getBackendImplementations (the reply keys and the
"rust" value Dart receives are unchanged), the iOS routing guard and
the interface defaults that threw for unrouted methods.
- cleanupExtensions(), waitForDownloadProgressDelta and
scanCueForLibrary had no production caller. Dart reaches cleanup
through the cleanupExtensions channel method, and RustCoreBackend
overrides openDownloadProgress, so the polling default goes too.
The UniFFI exports DownloadState.wait_progress_delta and
ExtensionManager.scan_cue_file_for_library were used only by those
Kotlin overrides. Bindings are generated at build time by
scripts/build_rust_backend.sh; no generated binding is tracked.
with_credits runs for every parse_lyrics data job and compiled up to
seven regexes per call. Hoist them into LazyLock statics next to the
other lyrics patterns, with the same pattern text, so each is compiled
once per process. Every other regex in the workspace is already static.
storage_failure also lowercased the error message once per probe on the
download failure path; lowercase it once and reuse it.
NetworkService::set_network_compatibility_options ignored the
insecure_tls flag by design because TLS verification is never disabled.
Remove the parameter from the network service and the extension
environment so the dead flag no longer travels through internal layers.
The UniFFI method keeps its signature: Kotlin and Swift still pass the
flag, and the mobile binding accepts and ignores it until the binding
and channel cleanup lands separately.
Rust is the only backend and routes the app on Android and iOS. Rewrite
the migration-era crate, script and comment wording that implied Go still
runs, and note in each crate doc that Go mentions describe the retired
backend whose observable behavior remains a compatibility contract.
Remove tokio from spotiflac-extensions and thiserror and the direct hyper
entry from spotiflac-network (hyper stays resolved through hyper-util).
Move roxmltree, rusqlite and libc into workspace.dependencies. libc already
resolves to 0.2.190, so the workspace pin uses that exact version. No
resolved version or enabled feature changes.
Add two keyless providers that serve Apple Music TTML: lrc.red, matched by
recording, and BetterLyrics, which answers songs it has already cached.
TTML is read into the first-party Apple line structure, so vocal sides,
backing groups, word timing, translations and romanization share one eLRC
formatter. The XML depth guard moves to core so both readers use it.
SpotiFLAC-Mobile Lyrics becomes its own provider instead of a fallback
inside Apple Music. New defaults are Apple Music, lrc.red, SpotiFLAC-Mobile
Lyrics, LyricsPlus and LRCLIB. Migration 13 moves untouched defaults to the
new list and keeps the former fallback right after Apple Music in custom
orders. Replace the dead LyricsPlus Workers mirror.
Match decoded path components once in Dart and Rust while rejecting encoded separators and paths outside the selected folder. Add folder scan diagnostics, shared parser fixtures, a synthetic 1806-track traversal and native Android regression coverage.
Export, import, and split legacy history backups in Rust while preserving schema 15, all history columns, path keys, and atomic restore behavior. Keep provider refresh and pending history edits intact.
Move full and incremental ingestion, source snapshots, path keys, and download exclusion to native transactions. Preserve schema 17, FTS, WAL coordination, cancellation, and Android SAF timestamp backfill.
Move playlist parsing and serialization off the UI isolate, preserving field mapping, identifiers, durations, and export entries. Extend the shared parser contract tests with playlist fixtures.
Port LRC, enhanced LRC, and TTML parsing plus ID3v2.3 lyrics writing and picture encoding. Keep legacy contracts and deep XML fallback; guard asynchronous player updates against stale track results.
Move PCM beat analysis and spectrum cutoff estimation to cancellable Rust workers with the existing algorithms and isolated Dart fallbacks. Keep visual rendering and playback behavior unchanged.
Port the pinned Material quantizer and Dart RNG compatibility to Rust while keeping Material Score and final color schemes in Dart. Include exact palette fixtures, licenses, and Kotlin worker ABI coverage.
Introduce Android, iOS, and Dart worker dispatch with queued cancellation, binary inputs, and background JSON codecs. Move APK verification to streamed native SHA256 while retaining the isolated fallback.
Path and SAF scans wait at their next file checkpoint while paused and keep their position in memory. Cancel, backend shutdown and released request leases still end a paused scan.
Keep automatic Hi-Res replacement off by default. Require exact full-duration PCM equivalence across every channel before and after processing. Retain the original through publication and history persistence, restore it on failure or cancellation, and bound WAV format parsing.
Validated with 30 Rust tests, 46 Dart tests, clean analyzer/Clippy checks, Android ARM64 and iOS builds, and iOS simulator startup.
Introduce a per-file Hi-Res authenticity checker and optional auto re-download for flagged fake Hi-Res downloads. Adds Rust checker modules (fft, evidence, FLAC/WAV decoding, tests) and a mobile API (check_hires_authenticity). Wire platform bindings for Android/iOS and PlatformBridge. Add Dart HiResCheckService, UI HiResCheckCard, settings toggle (redownloadFakeHiRes) and integration in the download pipeline to quarantine and replace suspicious files with LOSSLESS when safe. Adds claxon FLAC dependency.
Replace the Go backend with the Rust workspace and route Android/iOS through
UniFFI bindings. Include the migrated extension runtime, network, providers,
media metadata, downloads and library operations, with version 5.0.0+147.
Remove Go sources, adapters, native build selection and CI dependencies.
Build Rust unconditionally and lock the iOS Rust pod using a relative path.
Retain legacy source and migration evidence in a local ignored archive.
Validation: Android Kotlin compile and 53 native tests; Swift Rust-branch
equivalence and syntax; CocoaPods install; workflow, shell, Ruby, plist and
diff checks. Reuse the preceding 100 Rust tests, fmt/Clippy and five-ABI
build checkpoint; no new APK or iOS application build for this cleanup.
Known follow-up: URL/URLSearchParams globals are missing from the Rust JS
runtime. A controlled extension replay confirms a URL-resolution regression;
this commit does not fix that runtime gap.