No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Runa the Witch f0a8712952
Drop tracks whose file already exists instead of numbering them
Deduplication previously only worked within a single process: restarting
locrec over an existing library rewrote every track with a (2) suffix. The
output path is now the deduplication key, so a restart adds only what is
missing.

The existence check runs before art is fetched, so a skipped track costs no
network. Collision numbering still applies to the rescue .pcm, where losing
unrepeatable audio is the worse outcome.

Moves write_track into the library crate so its three constraints (never
overwrite, never duplicate, never lose PCM) are covered by tests rather than
by inspection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 15:48:02 +02:00
docs/superpowers Drop tracks whose file already exists instead of numbering them 2026-08-23 15:48:02 +02:00
src Drop tracks whose file already exists instead of numbering them 2026-08-23 15:48:02 +02:00
tests Apply rustfmt across modules from earlier tasks 2026-08-22 20:02:44 +02:00
.gitignore Ignore SDD scratch directory 2026-08-22 18:12:44 +02:00
Cargo.lock Add track metadata type and playerctl line parsing 2026-08-22 18:15:04 +02:00
Cargo.toml Add track metadata type and playerctl line parsing 2026-08-22 18:15:04 +02:00
README.md Drop tracks whose file already exists instead of numbering them 2026-08-23 15:48:02 +02:00

locrec

Records whatever is playing on your machine and saves it as one tagged FLAC per track. Audio comes from PipeWire, track metadata from playerctl. Start it, let an album play, and you get files named and tagged without editing anything by hand.

Requirements

pw-record, playerctl, ffmpeg, and stdbuf. On Arch these come from pipewire, playerctl, ffmpeg, and coreutils.

You also need an MPRIS-capable player. Spotify, most browsers, mpv with the MPRIS plugin, and anything else that shows up in playerctl -l will work.

Building

cargo build --release

The binary lands at target/release/locrec.

Usage

Start playback, then run:

locrec --output-dir ~/Music/rips

It prints one line per track and stops on Ctrl-C:

recording Spotify (serial 9774) following player "spotify"
capturing from spotify
dropped Elite Enemy - Enemy Of The State (recorded 24.7s of 159.1s)
kept    Coone - Chaos & Crime -> /home/you/Music/rips/Coone/Chaos & Crime/01 - Chaos & Crime.flac
interrupted; stopping
dropped The Prophet - Wake Up (recorded 41.2s of 204.0s)
done: 1 kept, 0 skipped, 2 dropped, 0 failed

The two dropped tracks are the point rather than a problem: the first was already playing when locrec started, and the second was still playing when it stopped. Neither recorded in full, so neither is kept. A track that had finished when you hit Ctrl-C is kept, so playing an album to the end and then stopping does not cost you the last track. It may carry a little silence on the end, because recording continues after playback stops.

The failed count is tracks that recorded fine but could not be encoded. Those are not lost: see below.

Options

Flag Default Meaning
-o, --output-dir <DIR> . Where tracks are written
-p, --player <NAME> first available Which playerctl player to follow
--target <SPEC> auto What to capture: auto, sink, a node serial, or a node name
--sync-offset-ms <MS> 100 How long after a track change to make the cut
--min-complete <PCT> 98 How much of a track must record for it to be kept
--no-art off Skip downloading and embedding cover art
-v, --verbose off Log every metadata event

What gets kept

locrec compares how much audio it recorded against the length the player reports, and keeps a track when it got at least 98 percent of it. Start mid-song and that song is discarded. Skip halfway through and that one goes too. The result is a directory of complete tracks with no half-songs to weed out later.

When a player reports no length at all, locrec cannot prove the track was cut short, so it keeps the file and marks the line:

kept    Some Artist - Some Title -> /path/to/file.flac (length unknown, not verified)

Everything without that marker was actually checked against the player's reported length.

Output layout

<output-dir>/<album artist>/<album>/<NN> - <title>.flac

Missing metadata falls back to Unknown Artist and Unknown Album. Existing files are never overwritten or duplicated: see below.

Running it again

A track whose file is already on disk is dropped rather than recorded a second time:

skipped Coone - Chaos & Crime (already recorded at /home/you/Music/rips/Coone/Chaos & Crime/01 - Chaos & Crime.flac)

So leaving locrec running across sessions, or restarting it partway through an album, builds the library up rather than filling it with duplicates. Nothing is overwritten and no (2) copies appear.

The check is on the output path, which means it is really a check on artist, album, track number and title. Two genuinely different recordings that share all four look identical to locrec, and the second is dropped. A live version tagged exactly like the studio one is the realistic case. Record it to a different --output-dir if you need both.

Reading the tags back

locrec writes TITLE, ARTIST, ALBUM, ALBUMARTIST, TRACKNUMBER and DISCNUMBER, plus cover art as an attached picture.

One thing will confuse you if you check with ffprobe: it renames three of those when reading FLAC back. ALBUMARTIST comes back as album_artist, TRACKNUMBER as track, and DISCNUMBER as disc. Querying the original names returns nothing at all rather than an error, which looks exactly like the tags are missing. They are not. Ask for everything instead:

ffprobe -v error -show_entries format_tags -of json yourfile.flac

When encoding fails

If ffmpeg fails on a track, the audio is not thrown away. locrec removes the half-written .flac and writes the raw samples next to where the track should have gone:

note: raw audio preserved at .../01 - Title.pcm (s16le, 48000 Hz, stereo)

You can encode that yourself once the underlying problem is fixed:

ffmpeg -f s16le -ar 48000 -ac 2 -i "01 - Title.pcm" "01 - Title.flac"

Choosing what to capture

By default locrec finds the PipeWire node belonging to your player and records only that, so notifications and other applications stay out of the recording.

When it cannot tell which node is yours it falls back to the output sink and warns. This is common with browsers: several tabs can each produce their own audio node with the same name, and nothing distinguishes them. Pass an explicit node when that happens:

pw-dump | jq -r '.[] | select(.info.props."media.class"=="Stream/Output/Audio")
  | "\(.info.props."object.serial")  \(.info.props."application.name")"'

locrec --target 9774

Note that --target wants a node's object.serial, not the object id that pw-cli ls Node prints. They are different numbers.

Tuning the cut point

A player announces a track change over D-Bus at a slightly different moment than the audio actually crosses the boundary, and PipeWire adds its own buffer delay on top. --sync-offset-ms closes that gap. The default of 100 matches pw-record's default latency.

Listen to the end of one file and the start of the next:

  • Hearing the tail of the previous track at the start of a file means the offset is too small.
  • Missing the first moment of a track means it is too large.

Adjust once for your setup and leave it alone.

Troubleshooting

"pw-record connected to the capture device ... rather than to playback"

locrec refuses to run in this situation, and the refusal is deliberate. Under some conditions PipeWire resolves a sink's name to the input device of the same name, which means recording your microphone into files labelled as music. Pass an explicit --target <serial> for the node you meant.

"the opening seconds of capture are near silent"

Either nothing was playing yet, or the capture is attached to something that is not producing audio. If the resulting files really are silent, pass an explicit --target.

"N running PipeWire streams match player ..."

Several nodes share your player's name, which browsers do routinely. locrec records the output sink instead, so other applications will bleed in. Pass --target <serial> to record just the one you want.

Limitations

Restarting a dead pw-record or playerctl is not handled: if either exits, locrec stops. It follows one player at a time, treats every track boundary as a cut regardless of whether an album is gapless, and only writes FLAC.