Skip to content
karldreherPublic

About

The ID3 Knife, CLI Tool for MP3 metadata edits

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

17 Commits

Folders and files

Repository files navigation

idk

The ID3 Knife, CLI Tool for MP3 metadata edits

Install

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 ~/.local

Shell completions

idk 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.fish

Usage

Input files

Every command that takes FILES accepts:

  • Paths to MP3 files. A path that exists is always used literally, so names like Song [Live].mp3 are safe.
  • Glob patterns (*, ?, [...], **), expanded by idk itself so they work the same in Windows cmd/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 .mp3 files (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 (for find -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.

Copy one tag into another

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 only

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

Confirmation

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.

Set tags

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.

Clear tags

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.

Merge genres and artists

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.

Config file

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: albumartist
idk 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 --confirm

The 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).

Schema

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

Reference 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: Rock

schema validate exits 0 for a valid file and 1 otherwise, listing every violation.

Find files by tag

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 - --confirm

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

Show tags

idk show song.mp3
idk show --json *.mp3 | jq '.[].tags.albumartist'
idk show --field artist --field albumartist *.mp3

Known 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 edits from JSON

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 - --confirm

The 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 - --confirm

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

Release

  1. Bump version in Cargo.toml and merge to main.
  2. Tag and push: git tag v0.2.0 && git push origin v0.2.0.
  3. The Release workflow builds macOS (arm64, x86_64), Linux x86_64 and Windows x86_64 archives and publishes a GitHub Release with them attached.

About

The ID3 Knife, CLI Tool for MP3 metadata edits

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages