metaedit

Operations

Running metaedit day to day: what the archive stores, the CLI that tends it, and what was learned by pointing it at real servers. Every environment variable the stack reads — including the archive and rate-limit knobs summarised here — is listed with its default in the configuration reference.

The archive

Every Last.fm request is stored, in three layers:

  1. Raw — lastfm_request logs one row per HTTP attempt (success or failure) in a monthly partition; lastfm_response stores response bodies content-addressed by body hash, so an unchanged body is stored once while every observation of it is still recorded.
  2. Derived current state — lastfm_artist / lastfm_album / lastfm_track, one row per entity, each pointing at the latest_response_id it came from.
  3. Derived graph — lastfm_tag_edge, lastfm_similarity, lastfm_artist_alias, lastfm_entity_tag.

Layers 2 and 3 are reproducible from layer 1 alone, with no network access.

lastfm_request logs HTTP attempts to Last.fm only. A lookup answered from the archive is the case where no attempt was made, so it is not written there; those reads are counted in process memory and reported by GET /api/archive/stats under reads, alongside stray_archive_reads, which counts any historical rows written before that distinction was enforced.

CLI

uv run metaedit archive-stats                    # bytes stored vs. the cap
uv run metaedit partitions --months 6            # extend the partition window
uv run metaedit prune-raw --keep-days 365        # report; add --yes to actually drop
uv run metaedit reindex                          # rebuild the derived layer
uv run metaedit reindex --dry-run                # what a rebuild would change
uv run metaedit reindex --only artist            # one table family
uv run metaedit archive-entities                 # what is archived, and what is not understood

reindex derives the structured layer from the raw archive: entity rows for artists, albums and tracks, tag edges with real popularity counts, similar artists, autocorrect aliases and an archive-wide tag aggregate. It is reproducible by construction — every derived primary key is assigned deterministically, never by a sequence — and --dry-run reports the delta without writing.

reindex needs no network access and never writes to the raw layer, so it is safe to run at any time, and safe to interrupt: the swap is one transaction, so a failure leaves the previous derived tables untouched. Its --dry-run is the reviewer’s tool for a parsing or policy change.

Because the failure mode of a parsing mismatch is absence — fewer entities, with every count downstream still looking plausible — the report carries a unexpected_shapes count. Zero is healthy. A rise means archived bodies are no longer understood, and metaedit archive-entities names the offending method and payload keys.

The contract it implements is fixed in docs/design/0003-derivation-and-reindex.md.

Known deviation: reindex --since parses and validates its timestamp but performs a full rebuild rather than a partial derivation. Recorded in the same design doc §7 with the reasoning; it cannot produce wrong data, only cost time.

Retention and the ToS cap

Nothing is deleted automatically. The Last.fm API Terms of Service cap stored Last.fm Data at 100 MB; that number is measured and displayed, and pruning is always an explicit operator decision (prune-raw reports what it would drop and refuses to act without --yes).

Retention is monthly, because partitions are: prune-raw --keep-days N lists only whole months that are entirely older than the window, and deliberately keeps the cutoff month and the month before it too. A shallow window therefore reports nothing rather than risking the loss of recent observations.

Live-verified behaviour

Both adapter suites have been run against real services (a self-hosted Jellyfin 12.2.0, and the live Last.fm API). Findings that contradicted the code, kept because each one is a trap that a future change could walk back into:

Jellyfin

Last.fm

Operations notes