Files
qbz/crates/Cargo.toml
T
vicrodh 5d3d51a882 feat(rip): rip a CD to tagged FLAC, bit-exact and seekable
New crate `qbz-rip`: CD audio in, tagged FLAC out. Frontend-agnostic — it
knows discs, samples and files, and reports progress through a callback so a
UI can drive it without this crate depending on one.

`flacenc`, pure Rust: no libFLAC to build, nothing extra to bundle into
Flatpak, Snap or AppImage, and macOS gets it for free. `lofty` (already in the
tree) writes the Vorbis comments.

`RipSource` is the portability seam, and it is the only part that differs
between platforms: Linux reads sectors through ioctls, while macOS MOUNTS an
audio CD and presents each track as an AIFF under /Volumes — so there a "read"
is a file read. Everything after the seam is shared.

Two defects were found by ripping a real disc and looking at the result, and
neither would have failed a test:

  1. The tags were never written. Tagging happened on the `.flac.part` file,
     and lofty identifies a file by its EXTENSION — so it wrote nothing, and
     said nothing. Tagging now happens after the rename, with a test that
     reads the bytes back and asserts the strings are in them.

  2. The files could not be SEEKED. `flac -d --skip=2:00` returned
     FLAC__STREAM_DECODER_SEEK_ERROR and `flac -t` warned 3102 times over 3103
     frames. The cause is precise: a stream's last frame is short (12 709 620
     samples at a 4096 block leaves 3828), flacenc reports that honestly as
     min_blocksize 3828 / max 4096 — but in FLAC `min != max` MEANS
     variable-blocksize, and a decoder reading one expects SAMPLE numbers in
     the frame headers while the frames correctly carry FRAME numbers.
     libFLAC's own encoder writes min == max for exactly this reason. Two
     bytes of STREAMINFO, and now: zero warnings, seek works.

Verified against the drive, not inferred: 176 400 bytes decoded out of the
FLAC are byte-identical to the same sectors read straight off the disc. The
whole album ripped from the UI — seven tracks, 436 MB, every one decoding
clean, tagged with TITLE/ARTIST/ALBUM/TRACKNUMBER/TRACKTOTAL/DATE/ALBUMARTIST,
and no partial file left behind.

Filenames are sanitised for the strictest filesystem the library might be
copied to, including Windows' reserved DEVICE names and its habit of silently
dropping a trailing dot — which would turn two tracks into one collision.

cc
2026-08-21 02:27:24 -06:00

180 lines
7.1 KiB
TOML

[workspace]
resolver = "2"
members = [
"qbz-app",
"qbz-theme",
"qbz-models",
"qbz-audio",
"qbz-player",
"qbz-qobuz",
"qbz-radio",
"qbz-core",
"qbz-integrations",
"qbz-cache",
"qbz-library",
"qbz-cast",
"qbz-cmaf",
"qbz-dsd",
# Optical media (CD-DA now; SACD/ISO next). Frontend-agnostic, and NOT a
# dependency of qbz-dsd's reverse: qbz-dsd will depend on THIS, never the
# other way, or the two close a cycle.
"qbz-disc",
"qbz-rip",
"qbz-secrets",
"qbz-offline-cache",
"qbz-plex",
"qconnect-core",
"qconnect-protocol",
"qconnect-transport-ws",
"qconnect-app",
"qbz-ui",
# The HiFi Wizard's frontend-agnostic core (ADR-006), extracted for the Qt
# port. `qbz-dac-wizard` is the SLINT one and is deliberately NOT wired to
# this crate: the Slint tree is frozen (owner, 2026-08-11 — "ya no hacemos
# cambios para Slint"), and rewiring it would have been a change to a tree
# nobody compiles or tests any more. The two therefore hold the same logic
# twice; that is safe only BECAUSE the Slint side is frozen, and it is the
# reason a fix here never needs porting back.
"qbz-dac-wizard-core",
"qbz-dac-wizard",
"qbz-text-utils",
"qbz-slint-common",
"qbz",
"qbz-credentials",
"qbz-media-controls",
"qbz-mixtape",
"qbz-playlist-import",
"qbz-lyrics",
"qbz-i18n",
"qbz-music-link",
"qbz-reco",
"qbz-external-reco",
"qbz-log",
"qbzd",
"qbz-qt",
# The source-agnostic seam (one trait + one registry over Qobuz / local /
# Plex). Purely additive: nothing in `qbz` or `qbz-mixtape` changes, and it
# deliberately does NOT depend on qbz-audio / qbz-player.
"qbz-source",
# Media-server protocol crates: siblings of `qbz-plex`, same shape. HTTP +
# DTOs only — no cache schema, no settings store, no Qt (ADR-006).
"qbz-jellyfin",
"qbz-subsonic",
# ONE cache for every remote server (see its header for why the Plex one
# is not migrated in the same change).
"qbz-media-cache",
]
# Qt-frontend POC worktree: plain `cargo build`/`cargo check` in this workspace
# produces ONLY the Qt binary (plus its Slint-free dependency closure). The
# Slint crates remain members but are never built by default here.
default-members = ["qbz-qt"]
[workspace.package]
version = "2.0.2"
edition = "2021"
license = "MIT"
repository = "https://github.com/vicrodh/qbz"
[workspace.dependencies]
# Async runtime
tokio = { version = "1", features = ["full"] }
async-trait = "0.1"
futures-util = "0.3"
tokio-stream = { version = "0.1", features = ["sync"] }
# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# HTTP
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-webpki-roots-no-provider", "cookies", "stream"] }
# Logging
log = "0.4"
tracing = "0.1"
# Error handling
thiserror = "2"
anyhow = "1"
# Crypto (Qobuz auth)
md-5 = "0.10"
base64 = "0.22"
# Time
chrono = "0.4"
# Regex
regex = "1"
# Audio
symphonia = { version = "0.5", features = ["all"] }
# Random number generation (shuffle / DJ-mix) — mirrors src-tauri pin
rand = "0.10"
# Fuzzy string similarity (track-mix dedup) — mirrors src-tauri pin
strsim = "0.11"
# Note: Linux-specific dependencies (alsa) go in individual crate Cargo.toml files
# because workspace manifests cannot have [target] sections
[profile.release]
# Strip symbol tables from release binaries. Measured on 2026-07-02:
# 400 MB -> 218 MB (-45%). There are no .debug_* sections in a default
# release build — the weight is .strtab (151 MB) + .symtab (22 MB), inflated
# by Slint's generated generic symbol names. Trade-off: panic backtraces show
# addresses instead of names; the release workflow archives the UNSTRIPPED
# binary as an Actions artifact per tag for offline symbolication.
# Codegen knobs (opt-level / codegen-units / jobs) intentionally stay in the
# workflows' env (CARGO_PROFILE_RELEASE_*) — they are CI-memory tuning, not
# a property of the package.
strip = "symbols"
[patch.crates-io]
# Slint 1.16's femtovg-wgpu renderer leaves the wgpu surface alpha mode at
# `Auto`, which resolves as an opaque compositor surface on KWin/Wayland and
# makes borderless transparent windows show a black rectangular backing. The
# patch keeps Slint pinned but chooses a supported non-opaque composite alpha
# mode when the surface advertises one, so the miniplayer card can have real
# transparent rounded corners. The non-opaque mode is PER-WINDOW opt-in
# (`wgpu::set_surface_prefers_transparent`, flipped from qbz's winit
# window-attributes hook): only the miniplayer pays the compositor-blend cost,
# the main window keeps an Opaque swapchain.
i-slint-renderer-femtovg = { path = "vendor/i-slint-renderer-femtovg-1.16.1" }
# On modern macOS, fontique's CoreText per-script fallback can fail to resolve
# a Han-script family (the suggested PingFang is not always in the scanned
# family map), so Japanese kanji rendered blank while kana worked. The patch
# pre-registers the known system CJK families (Hiragino/PingFang/Apple SD
# Gothic Neo/Arial Unicode MS, Japanese-biased default) in the fallback map at
# collection creation, which takes priority over the CoreText query. macOS-only
# (#[cfg]); Linux keeps its working fontconfig fallback.
i-slint-common = { path = "vendor/i-slint-common-1.16.1" }
# With `overflow: elide`, sharedparley culled EVERY line whose bottom edge
# exceeded max_height — including the FIRST one, eliding the text to nothing.
# CJK fallback fonts (Hiragino on macOS) report line metrics taller than
# snug fixed-height boxes (TrackRow's 20px title line at 14px), so every
# CJK track title rendered blank on macOS while Latin fit (#543; verified
# with a headless repro matrix on the Mac Mini). The patch always draws the
# first line — it still gets the ellipsis treatment as the last visible one.
i-slint-core = { path = "vendor/i-slint-core-1.16.1" }
# Slint compiles femtovg with the swash raster path. Swash fails to scale
# some variable fonts at a non-default variation instance (the CFF2-based
# CJK system fonts on modern macOS), and femtovg cached those glyphs as
# permanently missing — so CJK text at any non-regular weight rendered
# blank (#543; regular weight = empty coords = renders fine). The patch
# retries the glyph at the font's DEFAULT instance before giving up: worst
# case the glyph draws at regular weight instead of not at all. No effect
# on fonts/instances that already render.
femtovg = { path = "vendor/femtovg-0.23.2" }
# macOS Skia renderer: skia bug 310510989 makes new_from_data fail for TTC
# faces at index > 0, and for some system collections (AppleSDGothicNeo.ttc,
# where EVERY weight sits at index > 0) the upstream read/write-fonts rebuild
# workaround ALSO fails — every Korean glyph run at non-regular weights drew
# nothing. The patch adds a second-chance workaround: resolve the same face
# through CoreText by family name + OS/2 weight, guarded by cmap glyph-id
# spot checks so a wrong match is rejected instead of drawing wrong glyphs.
i-slint-renderer-skia = { path = "vendor/i-slint-renderer-skia-1.16.1" }