- Rust 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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> |
||
| docs/superpowers | ||
| src | ||
| tests | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
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.