Skip to content

Latest commit

History

10 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Sift

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.

CILicense: AGPL v3

Why not beets

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.

What it does

  • 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 .lrc sidecars, shipped and verified with the audio.
  • Looks releases up in MusicBrainz and VocaDB, including curated romanisations — 初音ミクHatsune Miku as a database fact, not a transliteration guess.

Installing

Arch Linux

paru -S sift # builds from the tagged release
paru -S sift-bin # prebuilt, no Rust toolchain needed
paru -S sift-git # tracks main

Everything else

Grab 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.

Setting up a server

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.

Building

Needs Rust (stable), Node 22+, and your platform's webview headers. See CONTRIBUTING.md.

npm install
npm run tauri dev
npm run tauri build

How it works

crates/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.

Transfers stream tar, not SFTP

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_VERIFIED

Verification 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.

Things that are the way they are for a reason

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

packaging/ holds the AUR PKGBUILDs, the desktop entry, AppStream metadata, and the release process. See packaging/README.md.

Known gaps

  • 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

License

AGPL-3.0-only.

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.

About

slop

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages