Metadata-Version: 2.4
Name: platterpus
Version: 0.6.23
Summary: A secure, EAC-style CD ripper for Linux (FLAC, WAV, WavPack, MP3)
Author: Platterpus contributors
License-Expression: GPL-3.0-only
Project-URL: Repository, https://github.com/rmccann-hub/Platterpus
Keywords: audio,cd,rip,flac,wavpack,mp3,musicbrainz,accuraterip,whipper,cyanrip,qt
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: X11 Applications :: Qt
Classifier: Intended Audience :: End Users/Desktop
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Multimedia :: Sound/Audio :: CD Audio :: CD Ripping
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: PySide6<6.12,>=6.11.1
Requires-Dist: musicbrainzngs==0.7.1
Requires-Dist: tomli-w<2,>=1.0
Requires-Dist: cryptography<51,>=50.0.0
Provides-Extra: dev
Requires-Dist: pytest<10,>=8; extra == "dev"
Requires-Dist: pytest-cov>=5; extra == "dev"
Requires-Dist: hypothesis>=6; extra == "dev"
Requires-Dist: ruff<0.16,>=0.15.22; extra == "dev"
Requires-Dist: mypy<2.4,>=2.3; extra == "dev"
Dynamic: license-file

<p align="center">
  <img src="assets/icons/io.github.rmccann_hub.Platterpus-256.png" alt="Platterpus" width="120">
</p>

# Platterpus

**A secure, EAC-style CD ripper for Linux (FLAC, WAV, WavPack, MP3).** Aims for EAC-equivalent (Exact Audio Copy) archival quality on Linux, packaged as a single-file AppImage. It drives the [`cyanrip`](https://github.com/cyanreg/cyanrip) ripping engine and verifies every rip against AccurateRip and CTDB.

> **Status: v0.6.23 — out of beta.** The ripper pairing is **jointly verified**: handshake rounds 8 through 11 are all closed with `GO` from both projects, and every number either side declared reproduces on the other's tree. Round 12 is **open** — the fork merged upstream `0.9.4-rc2` and proposes `64ae7bc`; our verification declares `GO` and theirs is pre-committed, so this ships as a **pre-release** rather than a stable one, which is what that distinction is for. This build still installs cyanrip `0.9.4-rc1+platterpus.5` (`ddf7ac3`), the build round 8 approved and rig-tested on real hardware — `Ripping errors: 0`, banner verified before and after the rip; a reviewed pin is not an installed one.
>
> **The last of the cancel-path questions is now answered on hardware.** A *completed* second rip after a mid-rip cancel ran on 2026-08-21: 7m39s, AccurateRip-verified, *"all 1 tracks ripped cleanly"*, and every FLAC decoding cleanly. The drive-open fix was already verified the day before. Two hardware-gated items remain: the fork's `-x` **cache probe** measured (`32 sectors, 73.5 KiB, uncached read 362.6 ms`) and then went on to rip the whole disc, which is a fix owed by the fork rather than a gap here; and **overread is `-O`, it has run on the BDR-209D, and it hung the drive ~23 minutes** — do not reach for that toggle on this drive (`docs/dependency-contracts.md`). Those are what a v1.0 would need; this is not that release and does not pretend to be. The next minor is **0.7.100**, and it is gated on a full hardware pass — every test green in one run.
>
> **v0.6.23 is a correctness release, and most of it was found by *checking* rather than by reasoning.** A finished rip could be announced as one that *"never finished"*, because the re-rip comparison was racing the report writer and losing. An unreadable ripper log was reported as evidence that the file had been **altered after the ripper signed it** — an accusation of tampering from a state where nothing had been read. Two real ripper failures reached the user as a bare *"Rip failed"* with none of cyanrip's own sentence, because the message inventory had been five handshake rounds stale and its test compared it against a fixture generated from its own round — a list checked against itself. Whole-disc loudness and peak figures were read from wording the ripper explicitly disclaims while the four rows it guarantees were dropped without a trace, and the sweep that should have caught it passed the entire time because no log in `output_reference/` contains those rows. And our own published half of the cyanrip contract, headed *"Flags we pass you"*, omitted one we really send.
>
> **What is in it.** Implemented end-to-end with 2,000+ tests (including a full-pipeline end-to-end test) at ~93% branch coverage (91% enforced in CI), and validated on real Bazzite hardware (Pioneer BDR-209D): a full 16-track rip *through the published AppImage* with every Test CRC matching its Copy CRC, plus AccurateRip-verified archival results on a pressed disc (12 of 14 tracks exact at confidence 200, the other 2 offset-variant matches). Highlights: **no-terminal first-run setup** (the AppImage adds itself to your menu; a guided wizard installs the ripping stack), **read-offset auto-fill** from the bundled AccurateRip drive list (no disc needed), **cyanrip as the single ripping backend** (actively maintained, no >587 read-offset bug — whipper was retired, see KDD-18), **multiple output formats** (FLAC is always the lossless master; WavPack/MP3/WAV are derived from it), **goal presets** (Fast Verified / Archival Exact / Portable), an at-a-glance **verification verdict** (AccurateRip + CTDB) with a machine-readable JSON rip report written beside the log, a per-drive **read-offset trust line**, **true in-app updates**, **cover art** from the Cover Art Archive, **auto-filing finished rips into your library folder**, an **EAC-compatible companion log** with a per-track **EAC CRC32 column**, and **software-version provenance** recorded in the log header and the window title. **New in v0.6.3 — use your own fork, and be told which one you're on:** the one-time setup wizard now **builds and installs the pinned Platterpus fork of cyanrip** for you (no terminal), verifies the installed binary really is that build, and keeps the working upstream ripper if the build fails; the dependency check **names which build is installed** rather than only its version — which matters because the fork deliberately keeps upstream's version string, so the number alone cannot tell them apart; a multi-disc rip now writes Vorbis-correct `DISCNUMBER`/`totaldiscs` instead of cramming `2/3` into one tag; and every self-check that runs now **says something**, because a check that stays silent reads exactly like a check that found nothing wrong. **New in v0.6.1 — the provenance-and-honesty release:** every rip now records **which cyanrip binary produced it** (the Platterpus fork, unmodified upstream, or — honestly — *not determined*), captures the ripper's **exit code, exact command line and complete output** so a failure is reproducible, surfaces **the ripper's own words** instead of a bare "Rip failed", picks **the right disc of a multi-disc set** (or says it could not tell rather than guessing), and **audits itself**: a `self_check` block lands in every rip's JSON, and `--audit-rips` runs the same checks over your whole library in one command. This is an early release for wider testing — expect rough edges, and please [open an issue](https://github.com/rmccann-hub/Platterpus/issues) for anything you hit.

## At a glance

- **Linux only.** Primary target is Bazzite KDE Plasma 6; should work on any modern desktop Linux running Qt 6 (Fedora, Arch, Ubuntu, Tumbleweed).
- **Runs cyanrip inside Distrobox.** The GUI calls the host-exported `cyanrip` binary; it never bundles cyanrip or tries to install it itself (the guided wizard provisions the container). This is intentional — see [PLANNING.md §8 KDD-07](PLANNING.md).
- **Single-file AppImage** for the GUI itself; no system-level installs required.
- **No terminal prompts from the ripper** — the GUI queries MusicBrainz directly, then runs cyanrip offline with the chosen release's tags, so its interactive prompt never surfaces.
- **Choose your output format** — FLAC (default), WavPack, MP3, or WAV. FLAC is always produced as the lossless master; other formats are derived from it, so you never lose the archival copy. See [Audio output](#audio-output-what-you-get-what-you-dont).
- **Distribution model:** AppImage primary, `pipx` secondary.

---

## Capability & EAC-parity matrix

Where Platterpus stands against EAC-equivalent archival quality: what it has, what's missing, whether each gap is closeable, and — if closing it needs an upstream pull request — from which project's maintainer. **✅ have it · ⚠️ partial · ❌ not yet.** Maintainers: **cyanreg** = [cyanrip](https://github.com/cyanreg/cyanrip), **rocky** = [libcdio-paranoia](https://github.com/libcdio/libcdio-paranoia), **itismadness** = [OPS/Orpheus Logchecker](https://github.com/OPSnet/Logchecker).

| Capability | Status | Reachable? — how / who |
|---|---|---|
| Bit-perfect audio, CRC-provable | ✅ | Have it — AccurateRip + CTDB CRCs |
| AccurateRip verify (v1 + v2) | ✅ | Have it |
| CTDB audio-CRC verify | ✅ | Have it — CRC hardware-validated (KDD-16) |
| EAC-style log + per-track EAC CRC32 column + software-version provenance | ✅ | Have it |
| MusicBrainz tags · front/back/booklet art · UPC/catalog/label · ReplayGain | ✅ | Have it |
| AppImage · zero-terminal setup · in-app update · FLAC master + WavPack/MP3/WAV | ✅ | Have it |
| Gap / `INDEX 00` pre-gap detection + HTOA (hidden track) | ⚠️ | **Decided (KDD-32):** build cyanrip from our soft-fork integration branch — upstream `master` already emits `INDEX 00` (merged via PRs **#104 / #118 / #122**), and the soft-fork carries **PR #115** (UltraFuzzy, open) for *exact* pre-gap detection + HTOA. `cdrdao read-toc` stays the no-upstream-dependency fallback |
| Test & Copy (two full passes) | ✅ | Have it (KDD-30) — cyanrip `-Z` re-read consensus *is* the two-reads-agree guarantee; a confirmed track renders as a matching **Test CRC / Copy CRC** pair, and *Verify every track* runs it disc-wide |
| Cache-defeat *verdict* | ✅ | Have it (KDD-29) — **measured** with `cd-paranoia -A` (libcdio's copy of cyanrip's own read engine) via *Set up drive → Analyse cache*; still honestly "(unknown)" when inconclusive, never faked |
| Log integrity checksum (ours, openly verifiable) | ✅ | Have it (KDD-28) — a plain SHA-256 of the log text, at least as strong as EAC's and checkable with `sha256sum` (no secret key). Clearly labelled *not* an EAC checksum |
| C2 error pointers | ✅\* | **Aligned with EAC archival best practice — which *disables* C2.** The perfect-rip guide leaves C2 unchecked *even when the drive supports it* (drives falsely report clean reads while dropping C2 internally); the archival path relies on re-reads + AccurateRip/CTDB. So "no C2" is correct, not a gap |
| Signed EAC log checksum | ❌ | **Never** — signing our log as EAC forges provenance (bannable fake log). No PR, ever |
| Elite-tracker (RED/OPS/Orpheus) log acceptance | ❌ | Out of scope — *identity-walled* (checkers score cyanrip 0 regardless of audio). Honest path: re-add whipper, or a 2-PR chain **cyanreg → itismadness** (low odds) |

**In short:** everything that *proves* a good archival rip — bit-perfect audio, AccurateRip + CTDB verification, a measured cache-defeat verdict, Test & Copy, an openly-verifiable log checksum, tags, art, provenance — is in place. Each of those was closed with **equal-or-stronger rigor than EAC, honestly labelled as ours** — we never forge EAC's output. The one remaining gap is **gap/INDEX-00 + HTOA**, and its mechanism is now decided (build cyanrip from our soft fork — KDD-32). The rest is either *never* (signed checksum = forgery), *aligned with best practice* (C2 stays off), or *identity-walled* (elite-tracker acceptance). Contributor detail: [`docs/cyanrip-upstream.md`](docs/cyanrip-upstream.md) and [`docs/cyanrip-fork.md` Part A §10](docs/cyanrip-fork.md).

### Point-by-point vs. the EAC "perfect rip" checklist

Mapped directly to the settings the *Archival-Grade Extraction* master guide calls out for EAC 1.8 (`docs/archive/archival-extraction-guide-2026-06.md`). ✅ matches · ⚠️ partial/in-progress · ➖ deliberately N/A.

| EAC "perfect rip" setting | Platterpus / cyanrip equivalent | Match |
|---|---|---|
| **Secure Mode** — re-read sectors until statistical parity | cyanrip paranoia = **max** + `-Z N` consensus re-read (re-rips a track until N reads agree) | ✅ |
| **Accurate Stream** drive feature | assumed by the paranoia read path | ✅ |
| **Drive caches audio data** → flush cache between re-reads (cache-defeat) | libcdio-paranoia attempts cache-defeat every rip, and *Set up drive → **Analyse cache*** now **measures** the verdict with `cd-paranoia -A` — libcdio's own copy of that same read engine — recording a real Yes/No per drive into the EAC-compatible log (KDD-29). Inconclusive stays honestly "(unknown)", never a faked "Yes" (KDD-25) | ✅ |
| **C2 error info — leave UNCHECKED** (disable, even if supported) | We don't use C2 → **exactly what the guide prescribes** | ✅ |
| **Read sample offset correction** | applied via cyanrip `-s`, value from the bundled AccurateRip drive DB (by model, e.g. `+667`) or manual entry. Instead of EAC's one-shot "Key Disc" probe, **every rip that matches the AccurateRip consensus re-confirms the offset on your own drive** and promotes its trust line to *confirmed* (KDD-31) — the same corrected result, continuously re-proven | ✅ |
| **Overread into Lead-In/Lead-Out** — off unless firmware-verified | default: cyanrip overreads **+2 frames, filled with silence** (conservative — matches the guide's *off* baseline); opt-in **Settings → Overread** reads the real outermost samples (cyanrip `-O`) for drives verified to support it | ✅ |
| **Allow speed reduction** on scratches | cyanrip adaptive read-speed ladder | ✅ |
| **Gap/Index — Detection Method A, Secure** | cyanrip `master` emits `INDEX 00`/pregaps from the TOC; exact subchannel detection + HTOA is cyanrip **PR #115** (UltraFuzzy, open — a different, lower layer than the already-merged cue reporting). Mechanism decided (KDD-32): build cyanrip from our soft-fork integration branch, which picks up `master`'s markers and carries #115 | ⚠️ |
| **AccurateRip** verify | v1 + v2 (+ offset-variant) | ✅ |
| **CTDB** verify | present — CRC hardware-validated (KDD-16) | ✅ |
| **FLAC** `-8 -V -j` (max compression + decode-verify + threads) | cyanrip FLAC → post-rip **FLAC verify (decodes clean)** + optional max-compression recompress | ✅ |
| **WAV** uncompressed baseline | WAV output (no tags — the UI warns) | ✅ |
| **WavPack** hybrid `-c` + `-m -v` | WavPack **lossless** (not the lossy+`.wvc` hybrid) | ⚠️ |
| **LAME** `-V 0 -q 4` (dodge the r6147 `noise_shaping_amp` bug) | MP3 is encoded by **ffmpeg** VBR, not `lame.exe -q 0..3` — so that LAME-specific footgun **isn't in our path** | ➖ |
| **Vorbis / APEv2 / ID3** tags per format | FLAC→Vorbis (cyanrip), MP3/WavPack tags via ffmpeg | ✅ |
| **Signed EAC log checksum** | **never** — signing our log as EAC forges provenance | ➖ (refused) |

---

## Installation

### Easiest — download one file, no terminal (recommended)

You don't need the command line. Download the GUI, double-click, and it sets
itself up by asking a couple of questions.

1. **Download** `platterpus-x86_64.AppImage` from the **[Releases page](https://github.com/rmccann-hub/Platterpus/releases/latest)** (one file).
2. **Allow it to run** (a one-time Linux step — a downloaded program isn't
   runnable until you say so):
   - **KDE (Dolphin):** right-click the file → **Properties** → **Permissions** → tick **Is executable** → OK.
   - **GNOME (Files):** right-click → **Properties** → **Permissions** → enable **Allow executing file as program**.
3. **Double-click it.** On first launch it will offer to:
   - **add Platterpus to your applications menu** (so next time you just click it in the menu), and
   - **set up the ripping tool** — a guided wizard installs everything ripping needs (it may ask for your password once; on Bazzite/Silverblue it's instant). No terminal.
4. Then in the app: **Tools → Set up drive…** — your drive's read offset is
   filled in automatically; click **Save offset**. Insert a CD and **Start**.

That's the whole thing: one download, a couple of clicks, answer the prompts.
(Updating later = download the new AppImage and replace the old one.)

### Easy second option — one command with pipx

Comfortable with a terminal? A single copy-paste installs Platterpus from PyPI
and puts it on your `PATH` (the GUI still runs the first-run wizard to set up the
ripping stack):

```bash
pipx install platterpus    # then run:  platterpus
```

Don't have pipx? `sudo dnf install pipx` (Fedora/Bazzite) or `sudo apt install
pipx` (Ubuntu/Debian). Upgrade later with `pipx upgrade platterpus`. (To run
unreleased/dev code from a checkout instead, see
[Method B](#method-b--pipx-recommended-for-technical-users).)

> **Why a wizard?** Ripping runs through `cyanrip` inside a small container so
> it never touches your system ([why](PLANNING.md)). The first-run wizard sets
> that container up for you — the same work the scripts below do by hand.

### Quickstart for testers / scripted install

Prefer one command? This installs the *host stack* (Distrobox + cyanrip) **and**
the GUI, plus shortcuts:

```bash
curl -fsSL https://raw.githubusercontent.com/rmccann-hub/Platterpus/main/install.sh | bash
```

Prefer to download and run it yourself? Grab `install.sh` from the [Releases page](https://github.com/rmccann-hub/Platterpus/releases/latest), then `bash install.sh`. Useful flags: `--dry-run` (preview), `--no-host` (GUI only, host stack already set up), `--appimage PATH` (use a local AppImage). First run takes ~20–40 min because it builds the container.

Then, inside the GUI: **Tools → Set up drive…** to calibrate your drive's read offset (one time), insert a CD, and rip. To remove everything later, use the **Uninstall Platterpus** shortcut (or see [Uninstalling](#uninstalling)).

> **Already have cyanrip + Distrobox set up** (e.g. re-installing on the same machine, or installing the GUI on a second box that shares the stack)? Skip the host build and just add the GUI: `curl -fsSL …/install.sh | bash -s -- --no-host` (or `bash install.sh --no-host`).

> Why two pieces under the hood? The GUI can't rip without the host stack — that's by design ([why](PLANNING.md)). `install.sh` just sets up both for you; you can still do each step by hand (below).


#### Supported distributions

The one-line installer works on any modern desktop Linux. It auto-detects your package manager to install Distrobox + podman; everything ripping-related runs in a Fedora container, so your host distro only needs Distrobox and a container backend.

| Distro family | Auto-handled by the installer? | Notes |
|---|---|---|
| **Fedora / Bazzite / Silverblue / RHEL / CentOS** | ✅ Fully | Bazzite & Silverblue ship Distrobox + podman already; nothing extra. |
| **Ubuntu / Debian (24.04+)** | ✅ Fully | Installs `podman` too (the `distrobox` package only *recommends* it). |
| **Linux Mint / Pop!_OS / elementary** | ✅ Fully | Ubuntu-based — same path as Ubuntu/Debian. |
| **Arch / Manjaro / EndeavourOS** | ✅ Fully | Installs `distrobox` + `podman` via `pacman`. |
| **openSUSE Leap / Tumbleweed** | ✅ Fully | Installs `distrobox` + `podman` via `zypper`. |
| **Other / older distros** | ⚠️ Fallback | Uses Distrobox's official installer. Make sure `podman` (or `docker`) is present first. |

If the installer can't set up the host stack on your distro, do [the manual steps](#manual-steps) once — they work everywhere and are the source of truth.

The rest of this section is the long form — read it if the quickstart hits a snag or you'd rather do each step by hand.

### Fast path — one command (Steps 1-4 + 7)

[`setup-host.sh`](setup-host.sh) automates the host setup: it installs Distrobox (if needed), creates the `ripping` container, installs cyanrip + flac inside it, exports the binaries to your host, then clones this repo and runs `dev-setup.sh` (venv + editable install + app-menu shortcut).

```bash
# From a fresh clone:
bash setup-host.sh

# …or straight from the web (no clone needed first):
curl -fsSL https://raw.githubusercontent.com/rmccann-hub/Platterpus/main/setup-host.sh | bash
```

Useful flags: `--dry-run` (print every command, change nothing), `--yes` (skip confirmations), `--no-gui` (host stack only). It's idempotent — safe to re-run. It does **not** calibrate your drive (do that in the GUI: **Tools → Set up drive…**) or install Picard (the GUI offers that on first run).

Prefer to do it by hand, or the script hit a snag? The manual steps below are the source of truth.

### Manual steps

There are five things to set up. Plan on **20-40 minutes** the first time. Once it's done, you don't touch most of it again.

| Step | What | Why |
|------|------|-----|
| 1 | Install Distrobox | Provides an isolated Fedora environment for cyanrip |
| 2 | Create a `ripping` container | Where cyanrip actually lives |
| 3 | Install cyanrip + flac in the container | The tools that do the ripping |
| 4 | Export them to the host | So Platterpus can find them |
| 5 | Set your drive's read offset | One-time calibration for accurate rips |
| 6 | Install MusicBrainz Picard *(optional)* | Manual tag editing for unknown discs |
| 7 | Install Platterpus | This project |

> **If a step doesn't behave as written:** skip to the [Troubleshooting](#troubleshooting) section near the end of this README. The common surprises — "no drives found", `cyanrip: command not found`, HTTPS clone authentication failure — all have entries there.

### Step 1 — Install Distrobox

Distrobox lets you run a different Linux distribution's tools alongside your host system. It's the recommended way to run cyanrip on immutable distros like Bazzite.

> **Distrobox needs a container backend** — `podman` (recommended) or `docker`. Bazzite, Fedora Silverblue, and most atomic distros ship podman already. On **Ubuntu/Debian** it isn't guaranteed, so install it alongside Distrobox (the commands below do this). If `distrobox create` later fails with *"Cannot find a container manager"*, a missing backend is why — `sudo apt install podman` fixes it.

**On Bazzite (already pre-installed):**

```bash
distrobox --version
```

If you see a version, skip to Step 2.

**On Fedora / Fedora Silverblue:**

```bash
sudo dnf install distrobox
```

**On Arch / Manjaro:**

```bash
sudo pacman -S distrobox
```

**On Ubuntu / Debian (24.04+):**

```bash
sudo apt install distrobox podman
```

(Installing `podman` explicitly here is the Ubuntu-specific gotcha — the `distrobox` package only *recommends* it, so on minimal installs it can be absent and `distrobox create` then fails.)

**On Linux Mint / Pop!_OS / elementary OS:**

These are Ubuntu-based, so the Ubuntu command works:

```bash
sudo apt install distrobox podman
```

**On openSUSE Leap / Tumbleweed:**

```bash
sudo zypper install distrobox podman
```

(If your openSUSE version doesn't package `distrobox` yet, use the one-line installer under "older systems" below — but install `podman` with `zypper` first, since the installer doesn't pull a backend.)

**On older systems:**

Distrobox has a one-line installer:

```bash
curl -s https://raw.githubusercontent.com/89luca89/distrobox/main/install | sudo sh
```

Verify with `distrobox --version`.

### Step 2 — Create the `ripping` container

Create a Fedora-based container named `ripping`. The brief specifies Fedora 40; later Fedora versions also work — substitute `:41` or `:latest` if you prefer.

```bash
distrobox create --name ripping --image registry.fedoraproject.org/fedora-toolbox:latest
```

Distrobox will prompt to pull the image — type **Y** and press Enter. The download is about 600 MB the first time. Once it finishes:

```bash
distrobox enter ripping
```

You're now inside the container. The prompt should change to show you're in the `ripping` environment. To leave at any time, type `exit`.

> **Why `:latest` and not `:40`?** The brief specifies Fedora 40; newer Fedora releases (42, 43, 44…) also work and ship newer security fixes. `:latest` resolves to whatever's current. Don't pin *below* Fedora 42, though: the cyanrip COPR (`barsnick/non-fed`) only builds for Fedora 42–44 + rawhide, so an older container would fail the Step 3 `dnf install cyanrip`.

### Step 3 — Install cyanrip and flac

> **Easiest path:** run [`setup-host.sh`](setup-host.sh) (or the one-line installer above) — it adds cyanrip's COPR repo and installs everything for you. The manual steps below are only if you're doing it by hand.

Inside the container (your prompt should still show you're in `ripping`):

```bash
# flac provides both the `flac` decoder and `metaflac` (the tag editor).
sudo dnf install flac

# cyanrip isn't packaged by Fedora — add the barsnick COPR (GPG-checked), then install it:
sudo dnf copr enable barsnick/non-fed
sudo dnf install cyanrip
```

Verify the tools are installed:

```bash
cyanrip --version
metaflac --version
```

`cyanrip --version` should report something like
`cyanrip 0.9.4-rc1+platterpus.5 (platterpus-fork-gddf7ac3)`. The parenthetical is
the part that matters: it names the **fork**, which is the build Platterpus is
verified against.

**On the version flag, because the obvious advice is wrong in both directions.**
There is no single spelling that works on every cyanrip, and this README used to
say the opposite of the truth — *"note the capital `-V` — cyanrip has no
`--version`"*. Measured, and published in the fork's own provider contract:

| build | `--version` | `-V` | `-v` |
|---|---|---|---|
| stock, before the genopt rewrite (0.9.3) | fails | **works** | fails |
| stock, genopt onward (0.9.4-rc1) | **works** | fails | **works** |
| the Platterpus fork | **works** | **works** | **works** |

`-V` and `--version` are exactly complementary across the stock line, so a probe
that has to cover both needs two attempts by construction. On the fork you
install here, all three work — use `--version`. `metaflac` is part of the `flac`
package.

### Step 4 — Export the binaries to your host

Still inside the container, export the binaries. **Export all four** — the
in-app wizard and `setup-host.sh` do, and each one is load-bearing:

```bash
distrobox-export --bin /usr/bin/cyanrip       # the ripper
distrobox-export --bin /usr/bin/metaflac      # tag + cover-art writing
distrobox-export --bin /usr/bin/flac          # decodes audio for the CTDB check
distrobox-export --bin /usr/bin/cd-paranoia   # measures your drive's cache defeat
```

This creates wrapper scripts at `~/.local/bin/<name>` on the **host** (not in the container). Those wrappers transparently enter the container when called, so from the host's perspective cyanrip looks like a regular installed program.

Skipping `flac` is the easy mistake: CTDB verification is **on by default** and decodes your audio with the host `flac`, so without the export that check silently degrades. `cd-paranoia` is optional — without it the cache-defeat verdict stays honestly "(unknown)".

Now leave the container:

```bash
exit
```

You're back on the host. Verify the wrappers work:

```bash
which cyanrip
# → /home/<you>/.local/bin/cyanrip

cyanrip -V
# → cyanrip 0.9.3.1
```

If `which` returns nothing, your `~/.local/bin` isn't on `$PATH`. Most desktop Linux setups put it there automatically; if yours doesn't, add this to `~/.bashrc` or `~/.zshrc`:

```bash
export PATH="$HOME/.local/bin:$PATH"
```

Then open a new terminal.

### Step 5 — Set your drive's read offset

Every optical drive reads audio slightly off from where it "should" — by a positive or negative number of samples. For bit-perfect archival rips that match AccurateRip's database, the offset for your drive has to be known so cyanrip can correct for it.

**This is a one-time, in-app step — there's no terminal command for it.** cyanrip reads no config file of its own; Platterpus stores the offset in its own config at `~/.config/platterpus/config.toml` and passes it to cyanrip at rip time via the `-s` flag. You set it through the **drive-setup wizard**, offered on first launch (or anytime from **Tools → Set up drive…**), which gives you two ways to get the value:

- **Automatic, no disc needed** — if Platterpus recognises your drive model, it fills the offset in from the bundled AccurateRip drive-offset list (e.g. `+667` for the Pioneer BDR-209D). Just click **Save offset**.
- **Enter it by hand** — look your drive up in the [AccurateRip offset list](https://www.accuraterip.com/driveoffsets.htm) and type the value into the wizard's manual-entry field. Handy if your model isn't recognised, or if you only have CD-Rs.

> **Why no "detect from a disc" button?** cyanrip (the ripping backend) has no AccurateRip offset-*finder*, so Platterpus doesn't offer on-disc auto-detection — a button that could only guess or fail would be worse than none. The drive-offset list is the reliable, disc-free source; a rip that then verifies against AccurateRip is what confirms the offset is right for your unit.

> **What about `~/.config/whipper/whipper.conf`?** If you ran an older whipper-based install, that file may still exist. It's **legacy, read-only reference only** — kept so an upgrading user can see their previous offset. cyanrip neither reads nor writes it; the live offset lives in Platterpus's own config. There's nothing to create or edit there.

### Step 6 — Install MusicBrainz Picard *(optional)*

Picard is what you'll use to manually fix tags for discs MusicBrainz doesn't recognize. The GUI installs it as a Flatpak (it auto-launches it via `flatpak run`), and offers to do so automatically when you first need it.

> **Ubuntu/Debian prerequisite:** the GUI installs Picard through **Flatpak**, which isn't installed on Ubuntu by default. Install it once and the GUI's auto-install works as-is afterwards (the GUI's install command points at a `.flatpakref` that adds the Flathub remote for you, so you don't need a separate `flatpak remote-add` step):
>
> ```bash
> sudo apt install flatpak
> ```
>
> Bazzite, Fedora Silverblue, and most KDE/GNOME spins already ship Flatpak. Picard is optional — if you skip it, the GUI simply lists it as "Optional (not installed)" and never nags; you only need it for hand-editing tags on unrecognized discs.

To pre-install Picard yourself rather than letting the GUI do it:

```bash
flatpak install --user flathub org.musicbrainz.Picard
```

Verify:

```bash
flatpak run org.musicbrainz.Picard --version
```

Platterpus will auto-launch Picard with the rip folder when you mark a disc as Unknown Album, *if* you enable the toggle in Settings.

### Step 7 — Install Platterpus

> **Recommended: Method A (AppImage).** As of v0.1.0 it's published as a downloadable release asset — this is the simplest path for most people. Method B (`pipx install platterpus`) installs from **PyPI**, where the wheel is published automatically on each tagged release via Trusted Publishing (live through the current release). Method C runs the GUI from a source clone and is aimed at developers.

Pick **one** of the methods below.

#### Method A — AppImage (recommended for end users)

Download the latest `platterpus-x86_64.AppImage` from the **[Releases page](https://github.com/rmccann-hub/Platterpus/releases/latest)**, then:

```bash
chmod +x platterpus-x86_64.AppImage
./platterpus-x86_64.AppImage
```

That's it — the AppImage bundles Python, Qt, and the GUI's dependencies, so there's nothing else to install on the GUI side. (You still need the host stack for ripping to work — the first-run wizard sets it up.)

**Menu entry / desktop icon:** you don't need to do anything — on its **first run the AppImage offers to add itself to your applications menu** (and copies its icon), **moving itself to `~/Applications`** so it lives with your other apps instead of staying in Downloads. Just say yes. (The old `install-appimage.sh` helper still exists for scripted setups and offers an `--uninstall`, but it's no longer required. [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) also works if you prefer.)

**Updates:** use **Help → Check for updates…** — if a newer release exists the app downloads it in the background, verifies it against the release's published checksum, installs it to `~/Applications`, and restarts itself. (Releases also ship a `.zsync` file and the AppImage embeds standard update-information, so [AppImageUpdate](https://github.com/AppImageCommunity/AppImageUpdate) delta updates work too, for those who use it.)

> **On a FUSE-less host** (rare on desktop Linux, but some minimal setups): run with `APPIMAGE_EXTRACT_AND_RUN=1 ./platterpus-x86_64.AppImage`, or see [AppImage won't launch](#appimage-wont-launch) in Troubleshooting.

#### Method B — pipx (recommended for technical users)

`pipx` installs Python applications in isolated environments and adds them to your `PATH`.

Install pipx if you don't have it (Bazzite ships with it):

```bash
sudo dnf install pipx     # Fedora / Bazzite
# or
sudo apt install pipx     # Ubuntu / Debian
```

Then install Platterpus:

```bash
pipx install platterpus
```

> The wheel is published to PyPI automatically on each tagged release (via Trusted Publishing), so `pipx install platterpus` is live. To pin a version, `pipx install platterpus==X.Y.Z`. (Install from a local checkout — `git clone …` then `pipx install .` — only when you want unreleased/dev code.)

Run with `platterpus` from any terminal.

#### Method C — From source (for developers)

> The repository is **public**, so no authentication is needed to clone over HTTPS. (If you plan to push changes, set up SSH or `gh auth login` — but for just running from source, a plain clone works.)

Clone and install:

```bash
git clone https://github.com/rmccann-hub/Platterpus.git
cd Platterpus
```

The default `main` branch contains the full source — no branch switch needed.

From here you have two options.

**Option 1 — one-shot setup script (recommended):**

```bash
bash dev-setup.sh
source .venv/bin/activate
platterpus
```

`dev-setup.sh` creates a venv, upgrades pip, and runs `pip install -e .` for you. Run it again later (after `git pull`) to refresh dependencies if anything's been added.

**Option 2 — manual steps (same effect, if you want to see each one):**

```bash
# Create a virtual environment. On Bazzite, Fedora 38+, Ubuntu 24.04+,
# and other distros with PEP 668 enforcement, this is required — a
# plain `pip install` against the system Python will refuse with
# "error: externally-managed-environment".
python3 -m venv .venv
source .venv/bin/activate

# pip in a fresh venv is usually outdated; upgrade before installing
# anything else. Avoids "WARNING: ... newer version of pip available."
pip install --upgrade pip

# Install the package in editable mode. From now on, anything you
# edit in src/platterpus/ is picked up the next time you run the GUI.
pip install -e .

# Run the GUI. The console-script entry point lives in .venv/bin
# (added to PATH by the `activate` line above).
platterpus
```

To re-enter the same environment in a future terminal session:

```bash
cd ~/Platterpus
source .venv/bin/activate
platterpus
```

To leave the venv: `deactivate`.

To build an AppImage from your local checkout:

```bash
pip install --user build "python-appimage>=1.4,<2"
bash build/build_appimage.sh
```

The resulting `platterpus-x86_64.AppImage` appears at the repo root. See [`build/python-appimage/README.md`](build/python-appimage/README.md) for details.

---

## Ripping backend: cyanrip

The GUI drives a single ripping engine: [**cyanrip**](https://github.com/cyanreg/cyanrip) — actively maintained, EAC-equivalent archival quality. There's no backend setting or toggle; cyanrip is it. (The project originally used whipper, but its cd-paranoia has a known bug at read offsets **over 587 samples** that can fail tracks — e.g. the Pioneer BDR-209D's +667; cyanrip applies the offset correctly with its own paranoia even past that threshold, which is why we switched. See [KDD-18](PLANNING.md).)

The GUI does the MusicBrainz lookup itself and then runs cyanrip **offline** — with `-N` (no network metadata lookup) and the chosen release's tags fed in via `-a`/`-t` — so cyanrip's own interactive prompt never surfaces and the rip needs no in-container network. Cover art is fetched separately by the GUI from the Cover Art Archive.

## Audio output: what you get, what you don't

**Output format** is chosen in **Settings → Output format**: **FLAC** (default),
**WavPack** (`.wv`), **MP3**, or **WAV**. Every rip produces FLAC first — the lossless
archival *master* — and for any other choice the GUI **keeps that FLAC** and creates
the selected format alongside it (a quick post-rip transcode via ffmpeg). So you never
lose the lossless master, whatever you pick.

| Format | Lossless? | Tags | Cover art | Use it for |
|--------|-----------|------|-----------|------------|
| **FLAC** | ✅ (verified bit-perfect) | ✅ | ✅ embedded | The archive. The master copy. |
| **WavPack** (`.wv`) | ✅ | ✅ | folder `cover.jpg`¹ | A lossless library in a different container |
| **MP3** | ❌ best-quality VBR (~245 kbps) | ✅ | ✅ embedded | Phones, cars, portability |
| **WAV** | ✅ | ❌ | ❌ | Raw PCM interchange only (the GUI warns) |

¹ The front cover always lands in the album folder as an image file; ffmpeg can't embed
art *inside* a `.wv` (a known limitation — see `docs/archive/mp3-wav-support-2026-06.md`). For embedded
lossless art, FLAC is the choice.

The flag-by-flag detail below is about how each format is encoded.

*(cyanrip encodes FLAC through FFmpeg at maximum compression, and self-verifies the read with its own paranoia.)*

### FLAC (default — the lossless archival master)

cyanrip encodes each track to FLAC through FFmpeg at **maximum compression**, and verifies the read itself via its own paranoia engine — so every track is provably bit-perfect (and confirmed afterwards against AccurateRip and CTDB). There's no compression-level knob to set: it's already at the top.

**Historical context — the "Re-compress FLACs" setting.** Settings still lists a **"Re-compress FLACs"** toggle, but it is **inert and disabled** with cyanrip and does nothing: cyanrip already produces maximum-compression FLAC, so there's nothing to re-compress. (The control is kept only as a seam for a hypothetical future backend that *didn't* encode at max — for example, the old whipper backend relied on flac's default level 5, where a post-rip re-encode to `-8` would have shaved ~5% off file size. That no longer applies.) Don't expect flipping it to change anything.

For background: **all FLAC compression levels are lossless** — `-0` and `-8` decode to identical audio; only file size (and a little decode CPU) differ. cyanrip's max-compression output and its self-verification give you the smallest standard FLAC with the bit-perfect property already proven.

The full FLAC encoder reference is at [xiph.org/flac/documentation_tools_flac.html](https://xiph.org/flac/documentation_tools_flac.html).

### WavPack, MP3, and WAV (derived from the FLAC master)

When you pick a non-FLAC format, after each rip the GUI transcodes the FLAC master to
your choice with **ffmpeg**, keeping the FLAC. It runs in the background (never freezes
the window), writes each file atomically, and **never costs you the lossless master** —
a transcode failure just means you still have the FLAC to retry from. Per file:

```bash
# WavPack — lossless, tags carried over (APEv2)
ffmpeg -i <file>.flac -map_metadata 0 -map 0:a -c:a wavpack <file>.wv

# MP3 — best-quality VBR (== lame -V0, ~245 kbps), tags + embedded cover
ffmpeg -i <file>.flac -map_metadata 0 -id3v2_version 3 -c:v copy \
       -c:a libmp3lame -q:a 0 <file>.mp3

# WAV — raw 16-bit PCM (no tags or cover art — RIFF can't hold them)
ffmpeg -i <file>.flac -map 0:a -c:a pcm_s16le <file>.wav
```

`ffmpeg` is the single encoder dependency for all three (it's already present wherever
the cyanrip backend is — cyanrip is built on FFmpeg), detected through the same
dependency self-management subsystem as everything else — no bespoke install code. The
MP3 setting follows [HydrogenAudio's Recommended LAME](https://wiki.hydrogenaudio.org/index.php/LAME):
VBR `-V0` (joint-stereo on), the highest-quality VBR. Design + the full encoder-argument
rationale: [docs/archive/mp3-wav-support-2026-06.md](docs/archive/mp3-wav-support-2026-06.md).

### Compared to EAC's bit-perfect settings

The widely-cited [Perfect CD Ripping to FLAC with Exact Audio Copy guide](https://flemmingss.com/perfect-cd-ripping-to-flac-with-exact-audio-copy/) is the gold standard for archival rips on Windows. The full point-by-point mapping lives **once**, in the [capability & EAC-parity matrix](#capability--eac-parity-matrix) at the top of this README; the per-setting audit is [PLANNING.md KDD-13](PLANNING.md) and the deep-dive is [docs/eac-parity.md](docs/eac-parity.md).

Bit-perfection here is proven the open way — AccurateRip and CTDB CRCs, checkable by anyone against public databases — not by chasing acceptance from private trackers (RED/OPS/Orpheus). That acceptance is a deliberate **non-goal**: it's gated on ripper identity, not audio quality, so no honest partial score exists to chase. See [PLANNING.md KDD-24](PLANNING.md) and [docs/eac-parity.md](docs/eac-parity.md).

### Rip settings at a glance

**Now in Settings** (surfaced in the Settings dialog):

- **Goal** preset — *Fast Verified* / *Archival Exact* / *Portable* snaps the format/verification/quality controls to your intent; editing any of them switches the goal to *Custom*
- **Output format** — FLAC (the lossless master, always produced), WavPack, MP3, or WAV
- Cover art — fetch + embed in FLAC, save next to it, or both (defaults to *embed*)
- Max retries per track (default 5)
- **Max reads to confirm a shaky track** — rip once at full speed, then re-read *only* the tracks that didn't match AccurateRip until N reads agree on the checksum (cyanrip's `-Z`; **on by default**, 2). See "How ripping works" below.
- Verify with CTDB after a rip (a second, whole-disc verification path alongside AccurateRip; the CRC is hardware-validated — a match means verified)
- Verify FLACs after a rip (decode-test each output against its stored MD5)
- **Overread the lead-in/lead-out** (off by default) — ask the drive to read the disc's real outermost samples instead of writing them as silence (cyanrip `-O`). Leave it off unless your drive is known to support overreading — some firmware can freeze on it.
- **Move finished rips to** a library folder (empty = off) — once every post-rip check has settled (verification, transcode, checksums, the rip report), the album folder is filed into your library; a name collision lands in a "(2)" sibling, never an overwrite
- Auto-eject after a successful rip, plus read-offset calibration via the drive-setup wizard

*(Of the three whipper-only toggles removed with whipper (KDD-18) — force overread, keep-going-on-track-failure, and continue-ripping-CD-Rs — the last two stay gone: cyanrip handles track failures and CD-Rs natively with no equivalent flag. Force overread returned in v0.5.0, rebuilt cyanrip-native as the Overread toggle above.)*

After a rip, the results pane shows an at-a-glance **verification verdict** (green = every track verified against AccurateRip, amber = partial, grey = not in the database) above the per-track table, plus the CTDB result.

### How ripping works

Platterpus rips the disc **once at full speed** and checks every track against AccurateRip. A track that matches the database on that first read is already proven bit-perfect, so it's left alone. Only the tracks that *didn't* match are then **secure-re-ripped** — re-read until "Max reads to confirm a shaky track" reads agree on the checksum (cyanrip's `-Z`, on by default) — and the better read is kept. So a clean disc is a single fast pass (roughly real-time), and the careful, slow work happens only where it's actually needed. If read errors appear, an adaptive read-speed ladder also re-reads the disc more slowly. The FLAC master is always kept and, unless disabled, decode-verified against its stored MD5; any non-FLAC output is derived from that verified master.

See [TASKS.md](TASKS.md) under "EAC bit-perfect parity gaps" for the history.

---

## First run

When you launch Platterpus for the first time:

1. **Dependency check.** The GUI verifies all seven of its dependencies are reachable — cyanrip, metaflac, flac, ffmpeg, cd-paranoia, Picard, and the `musicbrainzngs` library. If anything's missing, it pops a dialog with one of three resolutions:
   - **Auto-install** (Picard): one OK and it runs `flatpak install --user`.
   - **Pending installs:** a checklist for items that need batching or confirmation.
   - **Manual install:** a copyable search string for anything that needs root (or a reboot) to install.

2. **Drive offset (first launch only).** Rips can't be made bit-perfect until your drive's read offset is set. If none is configured yet, the GUI offers the drive-setup wizard once. It can fill the offset in **automatically** from the bundled AccurateRip drive list (no disc needed), or take a value you **enter by hand** (look your drive up at [accuraterip.com/driveoffsets.htm](https://www.accuraterip.com/driveoffsets.htm) — handy if your model isn't recognised or you only have CD-Rs). It's a one-time, dismissible prompt; afterwards re-run it anytime from **Tools → Set up drive…**.

3. **Pick a drive.** The dropdown at the top of the window lists the optical drives detected on your system. Click Refresh if you plug in a drive after launch.

4. **Insert a CD.** The GUI fetches the disc's MusicBrainz ID, looks it up, and shows the match status. If multiple releases match, a picker dialog appears. (The GUI does the MusicBrainz lookup itself and runs cyanrip offline, so the ripper never prompts you for anything — this picker is where any disambiguation happens.)

5. **Edit metadata.** The track table is editable. Fix any tags that look wrong before you rip.

6. **Click "Start rip."** Overall progress, a live progress bar on the track currently ripping, and per-track AccurateRip confidence appear as the rip runs. You can cancel mid-rip.

7. **View the log.** When the rip finishes, the "View log" button opens the rip log in your default text editor. If **Move finished rips to** is set in Settings, the album folder is filed into your library once every post-rip check has settled — the View log / report / folder buttons follow it to its new home.

For discs MusicBrainz doesn't recognize, use the Unknown Album flow from the menu — the GUI rips with placeholder `Track NN` tags and optionally launches Picard for you to fix them up.

---

## Command-line usage (advanced)

Platterpus is a GUI first, but it has a few command-line flags for diagnostics.
**There is no `platterpus` command on your `PATH` unless you installed via `pipx`
(Method B).** If you run the downloaded **AppImage** (the recommended install),
the AppImage *is* the executable — pass the flags to it directly (its launcher
forwards every argument straight to the app):

> **First, check where the AppImage actually is.** If you accepted the first-run
> offer to add Platterpus to your application menu, the app **moved itself** to
> `~/Applications/platterpus-x86_64.AppImage` — it names the new path in the
> dialog that follows, and there is nothing left in your Downloads folder. Every
> `./platterpus-x86_64.AppImage …` command below then becomes
> `~/Applications/platterpus-x86_64.AppImage …`; a `./`-relative command run from
> where you downloaded it will just say *No such file or directory*. If you
> declined the offer, the file is still wherever you put it and `./` works from
> that folder. When in doubt: `ls ~/Applications/platterpus-x86_64.AppImage`.

```bash
# Show the version and build fingerprint, then exit
./platterpus-x86_64.AppImage --version

# "Doctor" — a no-CD first-pass check of the ripping environment, then exit
./platterpus-x86_64.AppImage --doctor

# Install or update the ripping stack from the terminal, then exit: the
# Distrobox container, cyanrip, and the pinned Platterpus fork of cyanrip built
# over it. Same steps the GUI's "Set up ripping" wizard runs, and idempotent —
# anything already in place reports "already present" and is left alone.
./platterpus-x86_64.AppImage --install-ripper

# Verify an already-ripped album against the CUETools Database (CTDB) and sweep
# the CRC offset to confirm the read offset aligns with the pressing. No CD or
# re-rip needed — it reads the FLACs already on disk.
./platterpus-x86_64.AppImage --ctdb-calibrate "/path/to/Artist/Album/"

# Audit every already-ripped album under a folder and exit. Read-only: no CD,
# no re-rip, nothing modified. Reports, per album, which cyanrip built it,
# whether the ripper said it finished, which disc of a multi-disc set the tags
# came from, what pre-gap provenance was seen, and whether the audio files the
# log claims actually have bytes in them. Exits non-zero if anything needs
# attention, so it is usable from a script.
./platterpus-x86_64.AppImage --audit-rips ~/Music/rips/

# Run a batch of UI tests without a person in front of the machine. FILE holds
# one step per line (open a dialog, check what is on screen, take a screenshot,
# run the ripper and assert its exit code); the app opens its test console with
# the file loaded and starts it immediately. The window is real and on screen —
# this is "no person needed", not "no display needed". A failing step is recorded
# and the batch keeps going, so you always come back to a complete transcript.
# Settings has the same thing as a saved setting, with an option to run it on
# every launch.
./platterpus-x86_64.AppImage --run-script ~/my-tests.txt

# Run the unattended hardware-session harness into a folder, then exit. One
# artifact per step, and it never stops on a failure — a failing step is data.
# Covers: both versions, --doctor, the ripper's own -x and -j probes (which a rip
# never sends), pre-gap screening, --audit-rips, an ETA sweep, log sizes, a fresh
# ripper clone + build, handshake status and preflight. Send the whole folder.
./platterpus-x86_64.AppImage --rig-session ~/rig-session-output

# The cyanrip seam check on its own. `--rig-session` already runs this, so you
# rarely need it directly — it exists because the cyanrip fork's own script calls
# it, and both write into the same MANIFEST.txt so the two projects' evidence is
# one upload rather than two piles. Read-only: nothing rips or re-encodes.
# Inside a test script the same check is the `rig-check` verb.
./platterpus-x86_64.AppImage --rig-check ~/seam-out \
    --rig-check-album "/path/to/Album" --rig-check-device /dev/sr0

# Compare two rips of the SAME disc track-by-track (which tracks are byte-for-
# byte identical, which differ, and which rip is the better master). Points at
# the .platterpus.json report each rip writes beside the FLACs.
./platterpus-x86_64.AppImage --compare "/path/old/Album.platterpus.json" \
                                        "/path/new/Album.platterpus.json"

# Assemble the best of two rips of the same disc into a new folder — copies, per
# track, whichever rip is the better master. Non-destructive: your two source
# folders are never touched.
./platterpus-x86_64.AppImage --assemble-best-of "/path/BestOf/" \
    "/path/old/Album.platterpus.json" "/path/new/Album.platterpus.json"
```

**When to use `--compare`:** re-ripping a disc you already ripped? Compare the
new report against the old one. Tracks that come back *identical* are rock-solid;
a track whose result *changed* (e.g. an exact AccurateRip match last time,
offset-variant this time) points to a read-stability problem on that track worth
a closer look. The GUI does this automatically after a rip when it finds a prior
rip of the same disc in your library, and shows a one-line summary in the
results pane.

**Path tip for `--ctdb-calibrate`:** a rip folder can contain a look-alike colon
(`∶`, U+2236) where the album title had a `:` — cyanrip substitutes it so the
folder name is filesystem-safe. Don't type a normal `:` (the path won't be
found); let the shell supply the real character — `cd` into the folder and pass
`"$PWD"`, or use tab-completion / a glob (`…/Every\ Breath\ You\ Take*`).

**Keeping cyanrip up to date: the app does it, and you never type a commit.**
Platterpus checks the fork's published releases a few seconds after launch and
again from **Help → Check for cyanrip updates…**. When the build it finds is one
the handshake record in this repository has approved — which includes the common
case of *your ripper isn't the build this Platterpus was verified against* — it
offers **Install it now**, and one click builds and installs it. There is no SHA
to copy and nothing to read first, because taking that build is what makes your
rips report `approved`.

The launch-time check is **silent unless it has something to offer**. Which
channel it looks at is **Settings → Updates**: stable by default, or pre-release
cyanrip builds if you tick the beta box.

When the newest published build is one **no round here has verified yet**, the
app tells you, states plainly that every rip made with it would report its ripper
as `unapproved`, and does *not* offer to install it — that one is deliberate, and
it is what `--install-ripper` below is for.

**When to use `--install-ripper`:** normally you never need it, now that the
above exists. It is the manual route — the one a rig script calls — for
installing **a specific commit**: a mid-round test pin, a build under review, or
going back to an older one. It prints the pin it is building and the build tag the
finished binary must report, so you can see which ripper you ended up with.

**And it takes a commit**, which is what makes the sentence above actually true:

```sh
# ~/Applications/… if you let the app add itself to your menu (see the note at
# the top of this section); ./platterpus-x86_64.AppImage if you declined.
~/Applications/platterpus-x86_64.AppImage --install-ripper 9048082
```

Without an argument it builds the pin baked into *this* Platterpus build — so
before this option existed, "get it without waiting" still meant waiting for a
release, which is the granularity the whole point was to avoid. The fork's pin
moved **five times inside one handshake round**, twice in a single day. With a
commit it builds that commit instead, through the same steps, and verifies the
binary reports `platterpus-fork-g<commit>`.

It deliberately does **not** predict the version string for a commit we don't
pin — we can't read that tree's `meson.build`, and printing a guess next to the
word "expects" would invite a comparison against a number nobody measured. You
get the build tag, which is checked, and an explicit note that the version is
not predictable. Every rip with a non-pinned build reports
`ripper handshake approval: unapproved`, and the install says so up front rather
than letting the rip report be the first place you see it.

If you installed with **`pipx`** (Method B), the same flags work on the
`platterpus` command instead — e.g. `platterpus --doctor`.

---

## Troubleshooting

### `pip install` fails with "does not appear to be a Python project"

Make sure you're in the cloned repository directory (where `pyproject.toml` lives) and that the clone completed:

```bash
ls pyproject.toml    # should exist
```

Then re-run `pip install -e .` (or `bash dev-setup.sh`).

### `sudo dnf install gh` fails on Bazzite

Bazzite is an immutable distro — the host filesystem is read-only and `dnf` only works inside containers. Two paths:

- **Use SSH instead** (recommended for one-time auth setup). See Method C in the install instructions above.
- **Or install `gh` system-wide via rpm-ostree:** `rpm-ostree install gh && systemctl reboot`. Requires a reboot. After the reboot, `gh auth login` will work.

### `pip install` fails with "error: externally-managed-environment"

Bazzite, Fedora 38+, Ubuntu 24.04+, and other distros now ship a PEP 668 marker that blocks `pip install` against the system Python. The fix is to install into a virtual environment, which Method C already does for you:

```bash
cd Platterpus
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
```

If you tried `pip install -e .` without activating a venv first, no harm done — re-run with the venv active and it'll work.

### `git clone` fails with "Password authentication is not supported"

The repository is **public**, so a plain `git clone https://github.com/rmccann-hub/Platterpus.git` needs no authentication. If you only want to *run* the GUI, you don't need to clone at all — use the AppImage from the [Releases page](https://github.com/rmccann-hub/Platterpus/releases/latest) (Method A).

If you plan to **push changes**, GitHub deprecated HTTPS password auth in 2021, so set up auth first — either an SSH key on your account (clone via `git@github.com:…`) or `gh auth login` (web-browser login; stores a token in your git credential helper).

### Where is my drive's read offset stored?

cyanrip uses **no config file** of its own. Platterpus stores your drive's read offset in its own config at `~/.config/platterpus/config.toml` and passes it to cyanrip at rip time. Set or change it in the GUI via **Tools → Set up drive…**. (A `~/.config/whipper/whipper.conf` left over from an older whipper install is legacy reference only — cyanrip doesn't read it.)

### `cyanrip: command not found`

Your `~/.local/bin` isn't on `$PATH`. Add this to `~/.bashrc` or `~/.zshrc`:

```bash
export PATH="$HOME/.local/bin:$PATH"
```

Open a new terminal. Verify with `which cyanrip`.

### `platterpus: command not found`

There's no `platterpus` command unless you installed via `pipx` (Method B). If
you run the **AppImage**, the AppImage is the executable — invoke it directly,
e.g. `./platterpus-x86_64.AppImage --doctor`. See [Command-line usage](#command-line-usage-advanced).

### "no drives found" when launching the GUI

The Distrobox container can't see `/dev/sr0`. Bazzite, Fedora Silverblue, and most modern distros pass optical drives through automatically. If yours doesn't:

```bash
distrobox stop ripping
distrobox enter ripping
# inside the container:
sudo dnf install eudev
```

(Some minimal container bases don't include udev; this restores device-node passthrough.)

You can also confirm the container can see the drive device node from inside it:

```bash
distrobox enter ripping
ls -l /dev/sr*
```

If the device shows up inside the container but the GUI still finds no drives from the host, the export wrapper isn't passing through device access. Re-run `distrobox-export --bin /usr/bin/cyanrip` from inside the container.

### "MusicBrainz error: rate limited"

MusicBrainz throttles unidentified queries. The GUI already sets a User-Agent at launch; if you're still hitting limits, you're sharing an IP with a busy network. Wait a minute and try again.

### AppImage won't launch

Most modern Linux distros have FUSE installed and AppImages just work. On Bazzite, no extra steps. If you see "AppImages require FUSE", either install FUSE or extract the AppImage:

```bash
./platterpus-x86_64.AppImage --appimage-extract
./squashfs-root/AppRun
```

### The GUI launches but freezes

Check the log at `~/.local/share/platterpus/log.txt`. The most common cause is the ripper hanging on a defective disc — cancel from the GUI, eject, try a clean disc.

### The drive-setup wizard says my disc isn't in AccurateRip

If Platterpus doesn't recognise your drive model (so the offset isn't filled in automatically), look your drive up at [accuraterip.com/driveoffsets.htm](https://www.accuraterip.com/driveoffsets.htm) and enter the value by hand in the wizard's manual-entry field. After a rip, the AccurateRip verdict tells you whether the offset was right — matching tracks confirm it; zero matches (on a disc that's in the database) suggest the offset is wrong.

### "metaflac: command not found" only when ripping

You exported cyanrip but not metaflac. Re-enter the container:

```bash
distrobox enter ripping
distrobox-export --bin /usr/bin/metaflac
exit
```

---

## Updating

### Update Platterpus

- **AppImage:** download the new release, replace the old file.
- **pipx:** `pipx upgrade platterpus`
- **From source:** `git pull && pip install -e .`

### Update cyanrip or metaflac

```bash
distrobox enter ripping
sudo dnf upgrade cyanrip flac
exit
```

The host-exported wrappers don't change; they always run whatever is currently inside the container.

### Update the container's base Fedora version

```bash
distrobox enter ripping
sudo dnf system-upgrade download --refresh --releasever=44   # stay >= 42: the cyanrip COPR only builds for Fedora 42+
sudo dnf system-upgrade reboot   # inside the container only
```

---

## Uninstalling

**Easiest — no terminal:** open the app and use **Tools → Uninstall Platterpus…**, or click the **Uninstall Platterpus** entry the AppImage adds to your application menu (under System). It removes everything the app installed — shortcuts, the cyanrip/metaflac/flac/cd-paranoia commands (and any leftover whipper export from an older install), the `ripping` container, optionally the AppImage file itself, and the app's own settings and logs (including the stored read offset) — with a confirmation first and per-item checkboxes. **Never touched:** your music, and Distrobox/podman themselves (any other containers you have keep working). The same uninstaller can be launched from a terminal with `./platterpus-x86_64.AppImage --uninstall` (or `platterpus --uninstall` on a pipx install).

**Script alternative** (source checkouts, or if you prefer the terminal): the [`uninstall.sh`](uninstall.sh) script tears everything down in layers, safest-first — it also covers the dev `.venv/`, which the in-app uninstaller doesn't (a packaged app doesn't know your checkout's location). It **never** removes your ripped music or a source checkout without an explicit flag.

```bash
# Interactive — removes the GUI's venv/config/logs by default, then prompts
# you about the broader stack (Picard, the ripping container,
# the host-exported binaries) one at a time.
bash uninstall.sh

# Preview only — print what would be removed, change nothing.
bash uninstall.sh --dry-run

# Everything except your music files and the cloned repo, no prompts.
bash uninstall.sh --full --yes

bash uninstall.sh --help   # full option list
```

To remove the host stack fully by hand instead:

```bash
distrobox rm ripping            # remove the container
rm -f ~/.local/bin/cyanrip ~/.local/bin/metaflac \
      ~/.local/bin/flac ~/.local/bin/cd-paranoia   # host exports (all four)
rm -f ~/.local/bin/whipper      # leftover from an older whipper install, if present
rm -rf ~/.config/platterpus ~/.local/share/platterpus
rm -rf ~/.config/whipper        # legacy whipper config, if present
```

Your music at `~/Music/rips/` (or wherever Settings points) is never touched by any of this.

## Where things live

| Path | Contents |
|------|----------|
| `~/.local/bin/cyanrip` | The Distrobox-exported ripper wrapper. Always present. **Don't edit.** |
| `~/.local/bin/metaflac` | The Distrobox-exported tag-editor wrapper. **Don't edit.** |
| `~/.local/bin/whipper` | Legacy leftover from an older whipper install, if present — no longer used; safe to remove. |
| `~/Applications/platterpus-x86_64.AppImage` | The app itself, after menu integration moves it out of Downloads. |
| `~/.config/platterpus/config.toml` | The GUI's own settings (output dir, templates, toggles) **and your drive's read offset**. The real settings file. |
| `~/.config/whipper/whipper.conf` | Legacy offset reference only — cyanrip does not use it. Kept so an upgrading user can see their old offset. |
| `~/.local/share/platterpus/log.txt` | GUI log file. Check here when something goes sideways. |
| `~/Music/rips/` *(default)* | Where rips land, under `Artist/Album/`. Configurable in Settings. |
| `…/Artist/Album/` | The rip itself: the FLAC tracks **plus** the sidecars — cyanrip's own `.log` and `.cue`, Platterpus's `<Album>.platterpus.json` report, the optional `<Album> (EAC-compatible).log`, and any saved `cover.<ext>` / `back.<ext>` / `booklet-NN.<ext>` artwork. |
| `~/.config/platterpus/drive_profiles.json` | The per-drive trust ledger (KDD-23): your drive's fingerprint, its read offset and where that value came from, and the measured cache-defeat verdict. |
| `~/.local/bin/` | The four host-exported wrappers — `cyanrip`, `metaflac`, `flac`, `cd-paranoia` — each of which transparently enters the `ripping` container. |

---

## Documentation for contributors

Core project documents (in this directory):

- [`CLAUDE.md`](CLAUDE.md) — project rules and conventions (read before contributing); Project operations section has current build/run/test/uninstall commands
- [`PLANNING.md`](PLANNING.md) — architecture, directory tree, per-module responsibilities, keyed design decisions (KDD-01 through KDD-35)
- [`TASKS.md`](TASKS.md) — active task checklist. P0 (T01-T32, complete), P1.1 (install/uninstall ease), P1 (broader backlog), P2 (future), Out of scope.
- [`DEPENDENCIES.md`](DEPENDENCIES.md) — pinned versions, last upstream release dates, replacement plans, retirement-review log

Source documents and reference material (in `docs/`):

- [`docs/README.md`](docs/README.md) — index of `docs/` contents, the single-source-of-truth map + rebuild-from-scratch checklist
- [`docs/architecture.md`](docs/architecture.md) — architecture & contributor guide: layered design, patterns & lessons, extension recipes, packaging/release/security (**read before contributing code**)
- [`docs/testing.md`](docs/testing.md) — testing strategy & standards; [`docs/test-plan.md`](docs/test-plan.md) — manual & release testing procedure
- [`docs/platterpus-research-brief-v2.1.md`](docs/platterpus-research-brief-v2.1.md) — the canonical project brief
- [`docs/platterpus-session-start.md`](docs/platterpus-session-start.md) — bootstrap instructions for a fresh Claude Code session (Step 0 = optional research-rerun prompt)
- [`docs/eac-parity.md`](docs/eac-parity.md) — cyanrip-log vs EAC-log field comparison

Build / dev tooling:

- [`setup-host.sh`](setup-host.sh) — one-command full bootstrap (Distrobox + container + cyanrip + export + clone + dev-setup)
- [`dev-setup.sh`](dev-setup.sh) — one-command post-clone setup (venv + pip + editable install + app-menu shortcut)
- [`uninstall.sh`](uninstall.sh) — tear-down counterpart (use `--help` for options)
- [`build/build_appimage.sh`](build/build_appimage.sh) — produce the AppImage locally
- [`build/make_icon.py`](build/make_icon.py) — regenerate the app icon
- [`build/python-appimage/README.md`](build/python-appimage/README.md) — AppImage recipe details
- **CI / releases:** `.github/workflows/ci.yml` runs the tests on every push/PR; `.github/workflows/release.yml` builds the AppImage and publishes it to a GitHub Release when a `vX.Y.Z` tag is pushed or the workflow is dispatched with the tag as input — after bumping `__version__` and rolling the `CHANGELOG.md` `[Unreleased]` section (see `CLAUDE.md` → *CI / release*; the build fails if the version and tag disagree). No local build or manual upload.

---

## License

[**GPL-3.0-only**](LICENSE). Chosen to align with the free-software CD-ripping ecosystem this builds on (whipper, cdparanoia, CUETools) and to keep the tool and any forks open. cyanrip and other GPL tools are invoked as separate processes (not linked), and PySide6 is used under its LGPL-3 option — so the combined work is cleanly GPL-3.0.

See [PLANNING.md KDD-10](PLANNING.md) for the rationale.

---

*Last updated for Platterpus v0.6.23.*
