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
180 lines
7.1 KiB
TOML
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" }
|