The ID3 Knife, CLI Tool for MP3 metadata edits
Download the archive for your platform from Releases, or build from source into ~/.local (make sure ~/.local/bin is on your PATH):
# Installs the idk binary to ~/.local/bin
cargo install --path . --root ~/.localidk completions <SHELL> prints a completion script to stdout for bash, zsh, fish, powershell and elvish.
# bash
idk completions bash > ~/.local/share/bash-completion/completions/idk
# zsh (any directory on $fpath; run compinit afterwards)
idk completions zsh > "${fpath[1]}/_idk"
# fish
idk completions fish > ~/.config/fish/completions/idk.fishEvery command that takes FILES accepts:
- Paths to MP3 files. A path that exists is always used literally, so names like
Song [Live].mp3are safe. - Glob patterns (
*,?,[...],**), expanded by idk itself so they work the same in Windowscmd/PowerShell as in Unix shells. Quote them to let idk expand them:idk show "**/*.mp3". A pattern that matches nothing is an error. - Directories with
-r/--recursive(alias--recurse), walked for.mp3files (any case), sorted by name. Without-r, a directory is an error. - File lists with
--files-from <PATH>(or-for stdin): one input per line, or NUL-separated (forfind -print0/idk find -0). Entries follow the same rules and come after the positional inputs.
Results are de-duplicated, so overlapping inputs process each file once. Input errors are reported and count as failures (exit 1); the remaining files are still processed.
idk copy tags --from artist --to albumartist song.mp3 --confirm
idk copy tags --from albumartist --to artist *.mp3 --confirm
idk copy tags --from artist --to albumartist --dry-run *.mp3 # preview onlyThe destination tag is overwritten. Other metadata is left untouched, and files whose destination already matches are not rewritten.
| Tag | ID3v2 frame | Aliases |
|---|---|---|
artist |
TPE1 |
tpe1 |
albumartist |
TPE2 |
album-artist, tpe2 |
title |
TIT2 |
tit2 |
album |
TALB |
talb |
tracknumber |
TRCK |
track, trck |
discnumber |
TPOS |
disc, tpos |
date |
TDRC (v2.4) / TYER, year only (v2.3) |
year, tdrc, tyer |
genre |
TCON |
tcon |
composer |
TCOM |
tcom |
comment |
COMM (empty description, eng) |
comm |
txxx:<description> |
TXXX with that description |
Tag names are case-insensitive; txxx: descriptions are matched exactly.
| Option | Description |
|---|---|
--confirm |
Write without prompting for each file (see Confirmation) |
-n, --dry-run |
Print each change (song.mp3: albumartist "Old" -> "New") without writing any file |
--fail-on-empty |
Treat files with no source value as failures instead of skipping them |
-j, --jobs <N> |
Maximum files processed concurrently (default: available CPUs) |
-q, --quiet |
Hide the progress bar and summary; errors are still reported |
Exit codes: 0 success, 1 one or more files failed, 2 invalid usage.
Commands that write (copy tags, set tags, clear tags, merge, apply) show each file's changes and ask before writing it:
song.mp3: albumartist "Old" -> "New"
Write song.mp3? [y/N/a/q]
y writes the file, n (or Enter) skips it, a writes this file and all remaining ones, and q stops without writing more. Skipped files are counted as declined in the summary. Prompts follow input order.
Pass --confirm to write without prompting, as scripts and pipelines need. Without --confirm, stdin must be a terminal: otherwise the command exits with code 2 and writes nothing. That includes apply - and --files-from -, which read stdin. --dry-run never prompts and never writes.
idk set tags --field albumartist="Various Artists" --field genre=Rock *.mp3 --confirm
idk set tags --field "txxx:Source=CD" --dry-run *.mp3--field FIELD=VALUE is repeatable and split on the first =, so values may contain =. Fields take the names in the table above. Files without an ID3v2 tag get a new ID3v2.4 tag; files whose fields already match are not rewritten. An empty value (genre=) or naming a field twice is a usage error.
idk clear tags --field comment --field "txxx:Source" *.mp3 --confirm--field FIELD is repeatable. date removes both TDRC and TYER; comment removes only comments with an empty description; txxx:<description> removes only that description. Files without the field (or without a tag) are unchanged and not rewritten.
Replace variant values with one canonical value. Matching ignores case and surrounding whitespace, "" matches a missing or empty value, and genre references such as (9) match by name (Metal).
idk merge genres --from "Heavy Metal","Metal" --to Rock *.mp3 --confirm
idk merge artists --from "Beatles","the beatles" --to "The Beatles" *.mp3 --confirm
idk merge genres --from "" --to Unknown *.mp3 --confirm # fill missing genres
idk merge genres --from ",Metal" --to Rock *.mp3 --confirm # fill missing and merge Metal--from takes comma-separated values: quote each value that contains spaces (--from "Heavy Metal","Metal"), or repeat the flag (--from "Heavy Metal" --from Metal). The shell joins "a","b" into the single argument a,b, so idk splits on the commas. Values that themselves contain a comma belong in the config file's from: list. merge genres changes only TCON; merge artists changes only TPE1. --dry-run, --jobs and --quiet work as for copy tags.
Operations can read their settings from YAML instead of flags. The top-level tags key is required, unknown keys are rejected, and each command reads only its own section.
tags:
merge:
genres:
from:
- "Heavy Metal"
- "Metal"
to: Rock # a single string, not a list
artists:
from:
- "Beatles"
to: The Beatles
copy:
from: artist
to: albumartistidk merge genres --config my-cool-file.yaml *.mp3 --confirm
idk merge artists *.mp3 --config --confirm # bare --config reads ./idk.yaml
idk copy tags *.mp3 --config --confirmThe config file is optional, but whenever one is given it is validated against idk's JSON Schema before any audio file is touched. Every violation is reported with its key path (for example tags.merge.genres.to: ["Rock"] is not of type "string"), and the run exits with 1.
--config can't be combined with --from/--to. A bare --config takes the next word as its file unless that word starts with -, so put it after the files (or before another flag).
idk schema write # writes ./idk.yaml.json
idk schema write --file my-cool-file.yaml.json
idk schema validate # validates ./idk.yaml
idk schema validate --config my-cool-file.yamlReference the written schema from the top of a config file for editor completion and inline errors (YAML language server, used by VS Code's YAML extension and others):
# yaml-language-server: $schema=./idk.yaml.json
tags:
merge:
genres:
from: ["Heavy Metal", "Metal"]
to: Rockschema validate exits 0 for a valid file and 1 otherwise, listing every violation.
idk find -r ~/Music --missing albumartist
idk find -r ~/Music --where albumartist!=@artist --where genre~^metal -i
idk find -r ~/Music --missing genre -0 | idk set tags --field genre=Unknown --files-from - --confirmPrints the path of every file matching all conditions, in input order:
| Condition | Matches when |
|---|---|
--where FIELD=VALUE |
the field equals VALUE (a missing field is "") |
--where FIELD!=VALUE |
the field differs from VALUE |
--where FIELD~REGEX |
the field matches the regex |
--where FIELD=@OTHER |
the field equals another field (!=@ to differ) |
--missing FIELD |
the field is absent or empty |
--present FIELD |
the field has a value |
-i/--ignore-case applies to =, != and ~. -0/--null separates paths with NUL for xargs -0 or --files-from. Invalid conditions exit 2 before any file is read; unreadable files exit 1. Finding no matches is still exit 0.
idk show song.mp3
idk show --json *.mp3 | jq '.[].tags.albumartist'
idk show --field artist --field albumartist *.mp3Known frames are shown by the field names above; other frames by their frame ID (comments with a description as COMM:<description>). Pictures are summarized as image/jpeg, 12345 bytes. PRIV (vendor private data, e.g. Amazon's), TCOP (copyright) and TSSE (encoder settings) are hidden unless -v/--verbose is given; with it, PRIV is summarized as www.amazon.com, 1024 bytes. Output follows input order.
Tags are sorted by name, with field names first and frame IDs (upper case) after. Absent fields are listed too: the fields named by --field, or by default title, artist, albumartist, album, tracknumber, discnumber, date and genre. They print as (no value) (albumartist: (no value)) and as null in JSON; apply treats null on an absent field as no change.
With --json, stdout is one array:
[{ "path": "song.mp3", "version": "2.4", "tags": { "artist": "Artist", "title": "Title", "albumartist": null } }]Files without an ID3v2 tag have "version": null and empty tags (no absent fields are listed). -j/--jobs and -q/--quiet apply as for copy tags.
apply sets different values on different files from one JSON file. Write the edits by hand:
[
{ "path": "a.mp3", "tags": { "artist": "Artist", "albumartist": "Artist" } },
{ "path": "b.mp3", "tags": { "genre": null } }
]idk apply edits.json --dry-run
idk apply edits.json --confirm
cat edits.json | idk apply - --confirmThe input is a JSON array (- reads stdin). Each object needs path and tags; other keys on the object, including version, are ignored. In tags, a string sets the field, null clears it, and fields left out are untouched. Only keys naming a field are written: field names, aliases and the frame ID of a known field (TPE1). Other keys (TENC, COMM:<description>, date#2) are skipped with a warning.
To start from the current tags, show --json emits this format, so you can edit its output and apply it back:
idk show --json *.mp3 | jq '.[].tags.album |= ascii_upcase' | idk apply - --confirmValues equal to what show prints are not rewritten, so applying unedited show output changes nothing.
Malformed JSON, a wrong shape, non-string values or the same file listed twice exit 2 before any file is touched. Missing files are failures (exit 1) while other entries still apply. Values joined with ; by show are written back as a single value if edited.
- Bump
versioninCargo.tomland merge tomain. - Tag and push:
git tag v0.2.0 && git push origin v0.2.0. - The Release workflow builds macOS (arm64, x86_64), Linux x86_64 and Windows x86_64 archives and publishes a GitHub Release with them attached.