Point it at a folder of music. It reads the tags that are already there, shows you the library it would build on your server, lets you fix anything it got wrong, and only then moves a single byte.
It doesn't retag your files. It doesn't guess silently. Every derived value in the interface says where it came from — the album artist tag, a fallback, the folder name, or your own edit — so correcting a wrong one takes a click instead of a forensic investigation.
Built for Jellyfin libraries, but the output is just
Artist/Album (Year)/01 Track.flac, which everything reads.
beets already does this and does it well. If its matcher works for your library, use beets.
Sift exists for the case where it doesn't: it treats your tags as the source of truth rather than something to be corrected against a database, it shows you the whole plan before acting, and it pushes to a remote server rather than assuming it runs where the files live. Databases are consulted to confirm, never to decide.
- Shows the plan first. A tree of the folders it would create, with the tag each path segment came from. Nothing moves until you press the button.
- Skips what's already there. The server's library is indexed on connect; running Sift twice on the same folder is a no-op, and a partially-copied album sends only the difference.
- Verifies on the server. Hashes are computed while the upload stream is built and checked by the server against its own disk — no second transfer.
- Places atomically. Files stage under the library root and are renamed in only after every checksum passes. A half-transferred album is never visible.
- Resumes and cancels. Stopping leaves verified work in staging; the next run picks up where it left off.
- Undoes. A log is written before any source file is deleted.
- Optionally re-encodes. FLAC -8, Opus 160k, or Opus 96k, with real measured ratios shown in the settings rather than marketing numbers.
- Fetches lyrics from LRCLIB as
.lrcsidecars, shipped and verified with the audio. - Looks releases up in MusicBrainz and VocaDB, including curated
romanisations —
初音ミク→Hatsune Mikuas a database fact, not a transliteration guess.
paru -S sift # builds from the tagged release
paru -S sift-bin # prebuilt, no Rust toolchain needed
paru -S sift-git # tracks mainGrab a build from Releases. Binaries are unsigned, so:
- Windows — SmartScreen will warn. "More info" → "Run anyway".
- macOS — Gatekeeper will block. Right-click → Open, or
xattr -cr /Applications/Sift.app.
Optional: install ffmpeg and put it on PATH to enable re-encoding.
sift-server-setup.sh prepares a Debian/Ubuntu box or LXC: installs Tailscale,
creates a key-only SSH user, and — the part that matters — sets the setgid bit
on the library root so uploaded files inherit Jellyfin's group without ever
needing chown.
bash sift-server-setup.sh --pubkey "ssh-ed25519 AAAA... you@desktop"Getting someone else set up? Settings → Copy setup code for someone else
produces one pasteable line, and SETUP-FOR-HUMANS.md walks them through the
rest with no terminal involved.
Needs Rust (stable), Node 22+, and your platform's webview headers. See CONTRIBUTING.md.
npm install
npm run tauri dev
npm run tauri buildcrates/sift-core pure: scan, group, plan. no network, no writes
crates/sift-meta MusicBrainz / VocaDB / LRCLIB, rate limited
crates/sift-transfer everything that connects, encodes, or deletes
src-tauri command surface — glue, no logic
src Svelte 5. renders the plan, computes nothing
sift-core produces a Plan — a list of source, destination, and issues — and
that one struct is simultaneously what the tree renders and what the transfer
layer consumes. The preview and the actual move cannot disagree, because they
are the same object.
SFTP is request/response: russh-sftp's writer sends a chunk and waits for the
acknowledgement before sending the next. Throughput is then bounded by round-trip
time rather than bandwidth — and Tailscale adds RTT even on a LAN, since every
packet goes through WireGuard. At 256 KB per 5 ms round trip you cap out around
50 MB/s on a gigabit link no matter how fast the disks are.
So each album streams as a tar into an exec channel instead. SSH's own flow
control window paces it, and there are zero application-level round trips while
data moves. The remote side is one command:
mkdir -p STAGING && tar -xf - -C STAGING && cd STAGING \
&& sha256sum -c .sift-manifest && echo SIFT_VERIFIEDVerification rides along inside the same stream. Hashes are computed locally while the tar is built, shipped as a manifest, and checked by the server against what actually landed. Reading everything back to compare would have doubled the transfer.
Destination paths are String, not PathBuf. They name a location on a
remote Linux server, and PathBuf::join uses the host separator — building a
plan on Windows produced Artist\Album, which Linux created as one directory
with a backslash in its name. Every test passed because they all ran on Linux.
Duplicate detection consults a ledger first. Re-encoding means the file on
the server isn't the size of the file on disk, so size alone stops working.
.sift-ledger at the library root maps destination path to source size.
"Not in MusicBrainz" is never inferred from a missing release ID. Most files don't carry one even when the release exists; inferring absence painted nearly every album purple. That verdict only follows a real lookup, and a network failure leaves an album unchecked rather than mislabelled.
Lossy compression and "delete source after verify" are mutually exclusive, enforced in the UI. Together they replace your only lossless copy with a lossy one and then delete the original — and undo would restore the lossy file.
Compression numbers are measured, on realistic audio starting from FLAC at its default level: FLAC -8 gives 99%, Opus 160k gives 15%, Opus 96k gives 9%. The "FLAC halves your files" figure everyone quotes is versus WAV.
packaging/ holds the AUR PKGBUILDs, the desktop entry, AppStream metadata,
and the release process. See packaging/README.md.
- Never run against a large real library; scale is untested
- Host key verification is trust-on-first-use and doesn't persist
- Passphrase-protected SSH keys fail as a generic auth error
- The asset protocol scope is
**, wider than it should be - Server-to-server sync ("Tether") is designed but not built
- Binaries are unsigned
Note that AGPL's network clause does nothing for a desktop application — GPL-3.0 would have the same practical effect here. It's AGPL because that's the default for this codebase's author, and changing it later requires every contributor's agreement, so it's set deliberately now.