Skip to content

Latest commit

 

History

85 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WaxBin

WaxBin is a CGO-free Go library and waxbin CLI that owns the catalog database and is the single source of truth for an audio collection. It indexes, organizes, searches, browses, and tracks per-user state across music, audiobooks, and podcasts. It runs standalone, much like beets, and is a clean dependency for WaxDeck.

Design tenets

  • No CGO, no external binaries. Cataloging is pure Go for every format via [WaxLabel], and so is the analysis pass (decode, loudness, fingerprint, and waveforms) via [WaxFlow]. The two libraries cover the same containers, so WaxBin can decode every format it catalogs, on every host, apart from the rarer WMA profiles (Pro, Lossless, Voice) that WaxFlow does not decode. fpcalc is the sole remaining optional subprocess, used only for AcoustID lookups in enrichment.
  • Hard scan/analyze boundary. Scanning is I/O-bound and never decodes PCM; loudness, fingerprinting, and peaks live only in a resumable analyze pass.
  • Source of truth. Consumers read the catalog through WaxBin's canonical read API instead of rebuilding their own view of the filesystem.
  • Stable identities. Every surfaced entity has an opaque sortable public id (ULID) that survives re-tag, move, and re-encode. Field provenance and locks keep enrichment and organize from ever overwriting curated data.
  • Library + CLI, no network daemon. Storage is SQLite (WAL) on a local filesystem, with a flock-based single-writer ownership seam. The only server is an optional local control socket (waxbin serve), a unix socket that lets a second terminal mutate the catalog while one process holds the write lock. There is no HTTP or network listener.

Quick start

go build ./cmd/waxbin

# create the catalog and register a managed library root
./waxbin init --db ./catalog.db --root /music:managed

# index, analyze (ReplayGain/fingerprint/peaks), and organize
./waxbin scan     --db ./catalog.db
./waxbin analyze  --db ./catalog.db
./waxbin organize --db ./catalog.db --apply

# read it back
./waxbin browse newest --db ./catalog.db
./waxbin search  --db ./catalog.db "midnight"
./waxbin stats   --db ./catalog.db

Roots are declared as path[:mode[:media[:profile]]], for example /music:managed:music, /audiobooks:managed:audiobook, /rips:in-place. Config resolves with flag > env (WAXBIN_*) > JSON > default precedence. Every data command supports --json (with a schemaVersion) and returns stable exit codes (waxbin exit-codes).

CLI reference

Area Commands
Lifecycle init, library add/library list, scan, analyze, watch, serve, doctor, jobs, version, exit-codes
Read / browse query/ls (incl. --library, --tag KEY=VALUE, --tag-contains, --tag-present/--tag-missing, --limit-mode/--seed), browse <list>, facet --group-by (incl. tag.<KEY>, library, podcast, creditArtist, playlist), search (incl. --max-candidates, --library, --state), show, art (incl. --role), art roles, lyrics, stats [--year N], provenance, acquisition/acquisition set/acquisition clear, lock/unlock, entity info/entity list
Curation & editing edit (incl. --batch), entity (incl. entity rename [--write-back]), credit (incl. --batch), tag/tag keys, lyrics set, chapters, art set (incl. --role), art lock/art unlock (incl. --role), detach [--write-back]
Ingest / organize inbox, import, organize, profiles
Deletion / repair trash, rm [--permanent], mark-missing [--force], merge, audit, diagnostics, upgrade
Portability backup, restore, export, manifest, rebuild
Playlists / podcasts playlist, smartplaylist, podcast, opml
Enrichment enrich (MusicBrainz + Cover Art Archive; optional AcoustID; incl. --item/--entity; an injected provider can also fill role-tagged and artist art)
Maintenance db verify [--fix], db vacuum [--integrity], db thumbs [--older-than/--max-bytes], db migrate, db reset --yes, user, state

Watching for changes

  • waxbin scan is incremental: an unchanged file (same size + mtime) is skipped without re-hashing or re-parsing, and a file deleted on disk is reconciled to missing, behind a survival gate that refuses to act on a transiently unavailable root, so a momentary mount loss cannot mark a whole library missing. scan --force (alias --full) re-hashes and re-parses everything. A deliberate large deletion (more than half a library) is held back by the survival gate; run scan --reconcile-deletions to reconcile it once you have confirmed the files are really gone.
  • waxbin watch keeps the catalog in sync on a schedule (and, with --live, on filesystem events). Scheduled rescans are the primary mechanism because filesystem events are unreliable on WSL2, NFS, SMB, and bind mounts; a periodic full-content rescan (--full-interval) catches same-size/mtime-preserved edits the fast-path misses.
  • watch is a foreground mode. A read-write WaxBin holds an exclusive advisory lock on the catalog for its whole lifetime, so while watch runs, every other mutating command in another terminal (organize, analyze, enrich, import, scan --force) is refused with an ownership conflict (read-only queries always work). Stop the watcher (Ctrl-C) to do manual mutation, or run waxbin serve instead when a second terminal needs to mutate concurrently (see Serving). Idle lock release is deliberately post-1.0.

Serving (multi-client)

waxbin serve [--socket <path>] opens the catalog read-write (taking the write lock) and listens on a local unix control socket (default <db>.waxsock, created owner-only 0600). While it runs, other waxbin commands against the same catalog no longer fail with an ownership conflict. They auto-detect the running server (advertised in the lockfile) and dispatch through it: fast mutations (edit, lock, play state, ratings/stars, playlists, user, merge) are proxied to the server, and the heavier mutating commands borrow the lock through a maintenance-mode hand-off. Read commands always run directly. This is a local socket only, with no network or HTTP listener. The server runs until interrupted (Ctrl-C / SIGTERM).

Quality, repair, and maintenance

  • waxbin audit reports quality and integrity problems: duplicate/split entities, inconsistent metadata, missing art/ReplayGain, unportable filenames, orphaned sidecars, case-insensitive path conflicts, invalid feeds, and derived-data drift. --integrity adds an on-disk bitrot (content-hash) and corrupt-audio pass. It reports only; it never deletes.
  • waxbin merge <type> <survivor-pid> <loser-pid>... collapses duplicate artists / release-groups / albums / genres / series onto one survivor, re-pointing children (so play state and provenance ride along) and recomputing rollups.
  • waxbin upgrade groups alt encodings of the same recording (by fingerprint), ranks each group by quality, and marks the keeper.
  • waxbin db verify --fix repairs derived-data drift (FTS, rollups, sort keys) and reclaims orphaned art. waxbin db vacuum GCs and compacts the database.
  • waxbin stats --year 2025 prints a per-user listening year-in-review.

Curation & editing

The catalog is authoritative, and a curation edit changes the catalog first: it auto-locks the field so a later scan or enrichment pass never re-derives over your change, and records provenance: a hand edit is yours, and a caller that supplies its own source (the Go and socket APIs, art set --source, lyrics set --source) has that recorded instead. --keep-lock changes a value without touching its lock.

  • waxbin edit <pid> --set field=value edits scalar metadata such as title, artist, album, year, and track/disc numbers.
  • waxbin entity <pid> ... edits a normalized entity's curated fields: a sort-name override, release identifiers such as barcode and label, and MusicBrainz IDs.
  • waxbin credit <pid> ... curates contributor roles such as composer and performer.
  • waxbin tag <pid> --key KEY --value V sets a custom tag: a non-standard frame a file carries that WaxBin's typed model does not map, or one you add yourself. waxbin tag <pid> lists an item's tags, and waxbin tag keys lists every custom-tag key in the catalog with per-key item counts.
  • waxbin lyrics set, waxbin chapters, and waxbin art set curate lyrics, book chapters, and cover art.

Each of these edits the catalog and offers opt-in --write-back to also mirror the change into the backing file(s) (see below). Write-back is best-effort: a file that cannot be written returns a typed error naming the files, while the catalog edit still stands. A book re-anchors its identity on write-back so a later rescan resolves the same item, and a multi-file book is written across every part.

On-disk tag write-back (opt-in)

The catalog is always authoritative; these opt-in features mirror an edit back into files for external players, always preserving audio essence (an essence-verified write never alters the audio):

  • The curation edits above (edit, entity, credit, tag, lyrics set, chapters, art set) take --write-back to embed the committed change into the item's file(s).
  • acquisition set/acquisition clear take --write-back for the file's own SOURCE_URL/SOURCE_ID/ACQUISITION_DATE.
  • waxbin analyze --write-replaygain (or write_replaygain_tags in config) writes computed track and album ReplayGain into files after album aggregation (REPLAYGAIN_*, or Opus R128_*).
  • waxbin enrich --write-tags (or write_enrichment_tags in config) writes what the pass filled into files: a book's ASIN/ISBN/PUBLISHER and a track's GENRE. Without it those values live only in the catalog, which a rescan rebuilds from the file's tags, so the next retag clears them.
  • An organize profile with tag_write corrects albumArtist (literal Various Artists for compilations) and disc/track numbering on disk as it moves files, skipping locked fields and re-tagging before the move so a failure aborts cleanly.
  • stamp_item_pid additionally stamps a WAXBIN_ITEM_PID tag during organize, so rebuild can restore original item identities from tags (essence-first: adopted only when unambiguous, minted fresh on any conflict). A full DB backup remains the real disaster-recovery artifact.

Custom-tag queries

Custom tags are filterable and facetable, so they can power browse dimensions and allow/deny rules.

  • Query filters: waxbin query --tag MOOD=happy (equality), --tag-contains MOOD=hap (substring), --tag-present MYKEY and --tag-missing MYKEY (presence). A value may itself contain =, since only the first = splits key from value (--tag DISCOGS_RELEASE=id=12345). The same fields are available in smart-playlist rules as tag.<KEY> with the is, isNot, in, notIn, isPresent, isMissing, contains, startsWith, and endsWith operators. Ordered operators are rejected, since tag values are unordered text.
  • Facets and discovery: waxbin facet --group-by tag.MOOD counts items per distinct value, and waxbin tag keys lists the keys that exist to facet on.
  • isNot is deny-list semantics. tag.X isNot V means the item does not carry value V for key X. It does not mean "carries some value other than V". So an item tagged MOOD=[happy, sad] is not matched by tag.MOOD isNot happy, and an item with no MOOD tag at all is matched. That is exactly "deny when the forbidden value is present".
  • Case sensitivity. Equality (is, isNot) is exact-case (BINARY). Substring (contains, startsWith, endsWith) is ASCII-case-insensitive (SQLite LIKE). Facet value buckets are case-sensitive too, so happy and Happy are distinct buckets; only tag keys are canonicalized to uppercase, never values. Folding browse buckets is the consumer's job. A tag key WaxBin already owns through a scalar, credit, or identifier surface (such as TITLE or ISRC) is reserved and rejected as a tag field.

Library API

lib, err := waxbin.Open(ctx, waxbin.Options{
    DBPath: "catalog.db",
    Roots:  []config.Root{{Path: "/music", Mode: model.ModeManaged}},
})
defer lib.Close()

if _, err := lib.Scan(ctx, waxbin.ScanRequest{}); err != nil { /* ... */ }
page, err := lib.Browse(ctx, read.ListNewest, read.BrowseOptions{Limit: 50})

The stable facade lives in the root package; implementation subsystems live under model/, store/sqlite/, identity/, decode/, analyze/, loudness/, peaks/, fingerprint/, envelope/, query/, read/, art/, meta/, scan/, organize/, inbox/, trash/, playback/, playlist/, podcast/, source/, enrich/, audit/, jobs/, watch/, proxy/, port/, config/, pidpath/, waxerr/, and internal/.

License

MIT. See LICENSE.

About

An embeddable audio library engine for Go. Catalogs music, audiobooks, and podcasts in SQLite, with a CLI for standalone use.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Contributors

Languages