| namespace | aiwg |
| platforms | ["all"] |
| name | metadata-tagging |
| description | opustags and ffmpeg patterns for applying metadata to audio and video files |
| category | media-curator |
Metadata Tagging Skill
Concrete patterns for applying metadata tags to audio and video files using opustags (Opus) and ffmpeg (MP4/MP3/FLAC).
opustags (Opus Files)
Install
brew install opustags
sudo apt install opustags
sudo pacman -S opustags
git clone https://github.com/fmang/opustags.git
cd opustags
cmake -S . -B build
cmake --build build
sudo cmake --install build
Set Basic Tags
opustags input.opus \
--set "TITLE=Both Sides Now" \
--set "ARTIST=Joni Mitchell" \
--set "ALBUM=Clouds" \
--set "ALBUMARTIST=Joni Mitchell" \
--set "TRACKNUMBER=12" \
--set "DATE=1969" \
--set "GENRE=Folk" \
-o output.opus
Set Individual Tags
opustags input.opus --set "TITLE=Song Title" -o output.opus
opustags input.opus --set "ARTIST=Artist Name" -o output.opus
opustags input.opus --set "ALBUM=Album Name" -o output.opus
opustags input.opus --set "ALBUMARTIST=Various Artists" -o output.opus
opustags input.opus --set "TRACKNUMBER=3" -o output.opus
opustags input.opus --set "TRACKNUMBER=3/12" -o output.opus
opustags input.opus --set "DATE=1969" -o output.opus
opustags input.opus --set "GENRE=Folk Rock" -o output.opus
opustags input.opus --set "COMMENT=Live at Carnegie Hall" -o output.opus
In-Place Updates
opustags -i input.opus --set "TITLE=New Title"
opustags -i input.opus \
--set "TITLE=New Title" \
--set "ARTIST=New Artist"
Read Tags
opustags input.opus
opustags input.opus > tags.txt
opustags input.opus | grep "TITLE="
Delete Tags
opustags input.opus --delete "COMMENT" -o output.opus
opustags input.opus --delete-all -o output.opus
Set Cover Art
opustags input.opus --set-cover cover.jpg -o output.opus
opustags -i input.opus --set-cover cover.jpg
opustags -i input.opus --set-cover new-cover.jpg
Batch Operations
for file in *.opus; do
opustags -i "$file" \
--set "ALBUMARTIST=Joni Mitchell" \
--set "ALBUM=Blue" \
--set "DATE=1971"
done
for file in *.opus; do
opustags -i "$file" --set-cover ../cover.jpg
done
track=1
for file in *.opus; do
opustags -i "$file" --set "TRACKNUMBER=$track"
((track++))
done
Extract Cover Art
opustags input.opus | grep -A 1000 "METADATA_BLOCK_PICTURE" > cover-encoded.txt
ffmpeg (MP4/MP3/FLAC)
Set MP4 Metadata
ffmpeg -i input.mp4 -c copy \
-metadata title="Both Sides Now" \
-metadata artist="Joni Mitchell" \
-metadata album="Live at Isle of Wight 1970" \
-metadata date="1970" \
-metadata genre="Folk" \
-metadata comment="Restored from VHS" \
output.mp4
Set MP3 Metadata (ID3v2.3)
ffmpeg -i input.mp3 -c copy \
-id3v2_version 3 \
-metadata title="Both Sides Now" \
-metadata artist="Joni Mitchell" \
-metadata album="Clouds" \
-metadata date="1969" \
-metadata track="12/14" \
-metadata genre="Folk" \
output.mp3
Set FLAC Metadata (Vorbis Comments)
ffmpeg -i input.flac -c copy \
-metadata TITLE="Both Sides Now" \
-metadata ARTIST="Joni Mitchell" \
-metadata ALBUM="Clouds" \
-metadata DATE="1969" \
-metadata TRACKNUMBER="12" \
output.flac
Embed Cover Art into MP4
ffmpeg -i input.mp4 -i cover.jpg \
-map 0 -map 1 \
-c copy \
-disposition:v:1 attached_pic \
output.mp4
Embed Cover Art into MP3
ffmpeg -i input.mp3 -i cover.jpg \
-map 0 -map 1 \
-c copy \
-id3v2_version 3 \
-metadata:s:v title="Album cover" \
-metadata:s:v comment="Cover (front)" \
output.mp3
Embed Cover Art into FLAC
ffmpeg -i input.flac -i cover.jpg \
-map 0 -map 1 \
-c copy \
-disposition:v:0 attached_pic \
output.flac
Extract Cover Art from Any File
ffmpeg -i input.mp3 -an -vcodec copy cover.jpg
ffmpeg -i input.mp4 -map 0:v -map -0:V -c copy cover.jpg
ffmpeg -i input.flac -an -vcodec png cover.png
Read Metadata
ffprobe -v quiet -show_format -show_entries format_tags input.mp4
ffprobe -v quiet -show_entries format_tags=title,artist,album input.mp3
ffprobe -v quiet -print_format json -show_format input.mp4
Batch MP4 Tagging
for file in *.mp4; do
ffmpeg -i "$file" -c copy \
-metadata album="Live Performances 1970" \
-metadata date="1970" \
"${file%.mp4}-tagged.mp4"
mv "${file%.mp4}-tagged.mp4" "$file"
done
MusicBrainz Lookup
Recording Search by Artist and Title
curl -s "https://musicbrainz.org/ws/2/recording/?query=artist:Joni%20Mitchell%20AND%20recording:Both%20Sides%20Now&fmt=json" \
-H "User-Agent: MediaCurator/1.0 (contact@example.com)"
artist="Joni Mitchell"
title="Both Sides Now"
artist_enc=$(echo "$artist" | jq -sRr @uri)
title_enc=$(echo "$title" | jq -sRr @uri)
curl -s "https://musicbrainz.org/ws/2/recording/?query=artist:${artist_enc}%20AND%20recording:${title_enc}&fmt=json" \
-H "User-Agent: MediaCurator/1.0"
Release (Album) Search
curl -s "https://musicbrainz.org/ws/2/release/?query=artist:Joni%20Mitchell%20AND%20release:Blue&fmt=json" \
-H "User-Agent: MediaCurator/1.0"
curl -s "https://musicbrainz.org/ws/2/release/?query=artist:Joni%20Mitchell%20AND%20release:Blue&inc=release-groups&fmt=json" \
-H "User-Agent: MediaCurator/1.0"
Extract Metadata from JSON Response
mbdata=$(curl -s "https://musicbrainz.org/ws/2/recording/?query=artist:Joni%20Mitchell%20AND%20recording:Both%20Sides%20Now&fmt=json" -H "User-Agent: MediaCurator/1.0")
album=$(echo "$mbdata" | jq -r '.recordings[0].releases[0].title')
year=$(echo "$mbdata" | jq -r '.recordings[0].releases[0].date[:4]')
mbid=$(echo "$mbdata" | jq -r '.recordings[0].releases[0].id')
track_num=$(echo "$mbdata" | jq -r '.recordings[0].releases[0].media[0].tracks | map(select(.title == "Both Sides Now"))[0].position')
echo "Album: $album"
echo "Year: $year"
echo "MBID: $mbid"
echo "Track: $track_num"
Rate Limiting
MusicBrainz allows 1 request per second:
for file in *.opus; do
artist=$(echo "$file" | sed -E 's/^([^-]+) - .*/\1/' | xargs)
title=$(echo "$file" | sed -E 's/^[^-]+ - ([^.]+)\..*/\1/' | xargs)
artist_enc=$(echo "$artist" | jq -sRr @uri)
title_enc=$(echo "$title" | jq -sRr @uri)
mbdata=$(curl -s "https://musicbrainz.org/ws/2/recording/?query=artist:${artist_enc}%20AND%20recording:${title_enc}&fmt=json" \
-H "User-Agent: MediaCurator/1.0")
album=$(echo "$mbdata" | jq -r '.recordings[0].releases[0].title')
year=$(echo "$mbdata" | jq -r '.recordings[0].releases[0].date[:4]')
opustags -i "$file" \
--set "ALBUM=$album" \
--set "DATE=$year"
sleep 1
done
Filename Parsing Patterns
Common Patterns
artist=$(echo "$filename" | sed -E 's/^([^-]+) - .*/\1/' | xargs)
title=$(echo "$filename" | sed -E 's/^[^-]+ - ([^.]+)\..*/\1/' | xargs)
artist=$(echo "$filename" | sed -E 's/^([^-]+) - .*/\1/' | xargs)
album=$(echo "$filename" | sed -E 's/^[^-]+ - ([^-]+) - .*/\1/' | xargs)
track=$(echo "$filename" | sed -E 's/^[^-]+ - [^-]+ - ([0-9]+) - .*/\1/' | xargs)
title=$(echo "$filename" | sed -E 's/^[^-]+ - [^-]+ - [^-]+ - ([^.]+)\..*/\1/' | xargs)
track=$(echo "$filename" | sed -E 's/^([0-9]+) - .*/\1/' | xargs)
title=$(echo "$filename" | sed -E 's/^[0-9]+ - ([^.]+)\..*/\1/' | xargs)
title=$(echo "$filename" | sed -E 's/^(.+) \[[^]]+\]\..*/\1/' | xargs)
quality=$(echo "$filename" | sed -E 's/.*\[([^]]+)\]\..*/\1/' | xargs)
Clean Extracted Strings
artist=$(echo "$artist" | xargs)
title=$(echo "$title" | tr '_' ' ')
title=$(echo "$title" | sed 's/\b\(.\)/\u\1/g')
Complete Tagging Workflow
Single File (Opus)
#!/bin/bash
file="$1"
artist=$(echo "$file" | sed -E 's/^([^-]+) - .*/\1/' | xargs)
title=$(echo "$file" | sed -E 's/^[^-]+ - ([^.]+)\..*/\1/' | xargs)
artist_enc=$(echo "$artist" | jq -sRr @uri)
title_enc=$(echo "$title" | jq -sRr @uri)
mbdata=$(curl -s "https://musicbrainz.org/ws/2/recording/?query=artist:${artist_enc}%20AND%20recording:${title_enc}&fmt=json" \
-H "User-Agent: MediaCurator/1.0")
album=$(echo "$mbdata" | jq -r '.recordings[0].releases[0].title')
year=$(echo "$mbdata" | jq -r '.recordings[0].releases[0].date[:4]')
mbid=$(echo "$mbdata" | jq -r '.recordings[0].releases[0].id')
genre=$(echo "$mbdata" | jq -r '.recordings[0].releases[0]["release-group"]["primary-type"]')
curl -s "https://coverartarchive.org/release/${mbid}/front" -o "/tmp/cover-${mbid}.jpg"
opustags -i "$file" \
-- \
-- \
-- \
-- \
-- \
-- \
--set-cover
Batch Directory (MP4)
#!/bin/bash
dir="$1"
artist="$2"
for file in "$dir"/*.mp4; do
title=$(basename "$file" .mp4)
artist_enc=$(echo "$artist" | jq -sRr @uri)
title_enc=$(echo "$title" | jq -sRr @uri)
mbdata=$(curl -s "https://musicbrainz.org/ws/2/recording/?query=artist:${artist_enc}%20AND%20recording:${title_enc}&fmt=json" \
-H "User-Agent: MediaCurator/1.0")
album=$(echo "$mbdata" | jq -r '.recordings[0].releases[0].title')
year=$(echo "$mbdata" | jq -r '.recordings[0].releases[0].date[:4]')
ffmpeg -i "$file" -c copy \
-metadata title="$title" \
-metadata artist="$artist" \
-metadata album="$album" \
-metadata date="$year" \
"${file%.mp4}-tagged.mp4"
1
Troubleshooting
opustags Not Found
which opustags
brew install opustags
sudo apt install opustags
Invalid UTF-8 in Tags
title=$(echo "$title" | iconv -f utf-8 -t utf-8 -c)
opustags -i "$file" --set "TITLE=$title"
MusicBrainz No Results
curl -s "https://musicbrainz.org/ws/2/recording/?query=recording:${title_enc}&fmt=json" \
-H "User-Agent: MediaCurator/1.0"
curl -s "https://musicbrainz.org/ws/2/recording/?query=recording:${title_enc}~&fmt=json" \
-H "User-Agent: MediaCurator/1.0"
Rate Limit Exceeded
if curl -s -I "https://musicbrainz.org/ws/2/recording/?query=..." | grep -q "503"; then
echo "Rate limit exceeded, waiting 60 seconds..."
sleep 60
fi
See Also
References
- @$AIWG_ROOT/agentic/code/addons/aiwg-utils/rules/human-authorization.md — Seek explicit authorization before overwriting existing metadata tags in bulk
- @$AIWG_ROOT/agentic/code/frameworks/media-curator/skills/cover-art-embedding/SKILL.md — Cover art embedding skill used alongside metadata tagging for complete file tagging
- @$AIWG_ROOT/agentic/code/frameworks/media-curator/skills/tag-collection/SKILL.md — High-level tag-collection command that orchestrates metadata tagging
- @$AIWG_ROOT/agentic/code/frameworks/media-curator/skills/provenance-tracking/SKILL.md — Record provenance of metadata sources (MusicBrainz, Discogs)