agentic/code/frameworks/media-curator/skills/tag-collection/SKILL.md
Apply metadata tags, embed artwork, and organize media files with consistent naming
npx skillsauth add jmagly/aiwg tag-collectionInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Apply metadata tags, embed artwork, and organize media files with consistent naming conventions.
Automate the metadata curation workflow for audio and video collections:
<collection_path>: Path to directory containing media files to process--artist <name>: Filter to specific artist (speeds up processing)--dry-run: Show what would be done without making changes--artwork-dir <path>: Path to canonical artwork directory (default: ./artwork)--force: Overwrite existing tags and files without prompting--skip-artwork: Skip artwork embedding (metadata only)--skip-rename: Apply tags but don't rename files--skip-organize: Apply tags and rename but don't move files# Find all supported media files
opus_files=$(find "$collection_path" -type f -name "*.opus")
mp4_files=$(find "$collection_path" -type f -name "*.mp4")
mp3_files=$(find "$collection_path" -type f -name "*.mp3")
Extract artist, title, album from existing filenames using common patterns:
Artist - Title.extArtist - Album - Track# - Title.extTrack# - Title.ext (when artist is provided via --artist)# Example parsing
filename="Joni Mitchell - Both Sides Now.opus"
artist=$(echo "$filename" | sed -E 's/^([^-]+) - .*/\1/' | xargs)
title=$(echo "$filename" | sed -E 's/^[^-]+ - ([^.]+)\..*/\1/' | xargs)
Query MusicBrainz API for canonical metadata:
# URL-encode artist and title
artist_encoded=$(echo "$artist" | jq -sRr @uri)
title_encoded=$(echo "$title" | jq -sRr @uri)
# Query API
mbdata=$(curl -s "https://musicbrainz.org/ws/2/recording/?query=artist:${artist_encoded}%20AND%20recording:${title_encoded}&fmt=json" \
-H "User-Agent: MediaCurator/1.0")
# Extract metadata
album=$(echo "$mbdata" | jq -r '.recordings[0].releases[0].title')
year=$(echo "$mbdata" | jq -r '.recordings[0].releases[0].date[:4]')
tracknumber=$(echo "$mbdata" | jq -r '.recordings[0].releases[0].media[0].track-offset + 1')
genre=$(echo "$mbdata" | jq -r '.recordings[0].releases[0].release-group.primary-type')
# Rate limit: 1 request per second
sleep 1
Download cover artwork from MusicBrainz Cover Art Archive:
# Get release MBID
mbid=$(echo "$mbdata" | jq -r '.recordings[0].releases[0].id')
# Download front cover
curl -s "https://coverartarchive.org/release/${mbid}/front" -o "/tmp/cover-${mbid}.jpg"
# Fallback to fanart.tv if CAA unavailable
if [ ! -f "/tmp/cover-${mbid}.jpg" ]; then
curl -s "https://webservice.fanart.tv/v3/music/${mbid}?api_key=${FANART_API_KEY}" | \
jq -r '.albums."'"$mbid"'".albumcover[0].url' | \
xargs curl -s -o "/tmp/cover-${mbid}.jpg"
fi
Use appropriate tool based on file format:
Opus Files (opustags):
opustags "$file" \
--set "TITLE=$title" \
--set "ARTIST=$artist" \
--set "ALBUM=$album" \
--set "ALBUMARTIST=$artist" \
--set "TRACKNUMBER=$tracknumber" \
--set "DATE=$year" \
--set "GENRE=$genre" \
--set-cover "/tmp/cover-${mbid}.jpg" \
-o "${file}.tagged"
mv "${file}.tagged" "$file"
MP4 Files (ffmpeg):
ffmpeg -i "$file" -i "/tmp/cover-${mbid}.jpg" \
-map 0 -map 1 -c copy \
-disposition:v:1 attached_pic \
-metadata title="$title" \
-metadata artist="$artist" \
-metadata album="$album" \
-metadata date="$year" \
-metadata genre="$genre" \
"${file}.tagged.mp4"
mv "${file}.tagged.mp4" "$file"
MP3 Files (ffmpeg):
ffmpeg -i "$file" -i "/tmp/cover-${mbid}.jpg" \
-map 0 -map 1 -c copy \
-id3v2_version 3 \
-metadata title="$title" \
-metadata artist="$artist" \
-metadata album="$album" \
-metadata date="$year" \
-metadata genre="$genre" \
-metadata:s:v title="Album cover" \
-metadata:s:v comment="Cover (front)" \
"${file}.tagged.mp3"
mv "${file}.tagged.mp3" "$file"
Apply naming convention based on file type:
Audio Files: {Artist}/{Album}/{Track#} - {Title}.{ext}
# Construct new filename
new_filename=$(printf "%02d - %s.opus" "$tracknumber" "$title")
new_path="${artist}/${album}/${new_filename}"
# Show rename operation
echo "RENAME: $file -> $new_path"
# Execute if not dry-run
if [ "$dry_run" != "true" ]; then
mkdir -p "$(dirname "$new_path")"
mv "$file" "$new_path"
fi
Video Files: {Artist}/{Collection}/{Title} [{Quality}].{ext}
# Detect video quality
quality=$(ffprobe -v error -select_streams v:0 \
-show_entries stream=height -of default=noprint_wrappers=1:nokey=1 \
"$file")
quality_label="${quality}p"
# Construct new filename
new_filename="${title} [${quality_label}].mp4"
new_path="${artist}/${collection}/${new_filename}"
echo "RENAME: $file -> $new_path"
if [ "$dry_run" != "true" ]; then
mkdir -p "$(dirname "$new_path")"
mv "$file" "$new_path"
fi
Move files into directory structure if not already done during rename:
# Ensure artist directory exists
mkdir -p "$artist"
# Move album directories
if [ -d "$album" ]; then
mv "$album" "$artist/"
fi
Generate summary of operations:
echo "=== Tagging Summary ==="
echo "Files processed: $file_count"
echo "Tags updated: $tags_updated"
echo "Artwork embedded: $artwork_count"
echo "Files renamed: $renamed_count"
echo "Files moved: $moved_count"
if [ "$dry_run" = "true" ]; then
echo ""
echo "DRY RUN: No changes were made. Run without --dry-run to apply changes."
fi
Use --dry-run to preview operations without making changes:
/tag-collection ~/Music/Joni\ Mitchell --dry-run
Output shows what would be done:
SCAN: Found 47 Opus files, 12 MP4 files
LOOKUP: Joni Mitchell - Both Sides Now
-> Album: Clouds (1969)
-> Track: 12/14
ARTWORK: https://coverartarchive.org/release/a1b2c3d4.../front
TAG: Set TITLE, ARTIST, ALBUM, TRACKNUMBER, DATE, GENRE
EMBED: cover-a1b2c3d4.jpg
RENAME: Joni Mitchell - Both Sides Now.opus -> Joni Mitchell/Clouds/12 - Both Sides Now.opus
DRY RUN: No changes were made. Run without --dry-run to apply changes.
Tag all files in a directory:
/tag-collection ~/Music/Unsorted
Process only files for a specific artist:
/tag-collection ~/Music/Unsorted --artist "Joni Mitchell"
See what would be done without making changes:
/tag-collection ~/Music/Unsorted --dry-run
Apply metadata tags but skip artwork embedding (faster):
/tag-collection ~/Music/Unsorted --skip-artwork
Use artwork from a specific directory:
/tag-collection ~/Music/Unsorted --artwork-dir ~/Media/artwork
Update tags but leave files in place with original names:
/tag-collection ~/Music/Unsorted --skip-rename --skip-organize
Overwrite existing tags without prompting:
/tag-collection ~/Music/Joni\ Mitchell --force
When not using --force, the command prompts before overwriting:
File already has complete metadata:
Title: Both Sides Now
Artist: Joni Mitchell
Album: Clouds
Year: 1969
Overwrite existing tags? [y/N]:
If MusicBrainz API returns no results:
WARNING: No MusicBrainz match for "Unknown Artist - Unknown Track"
SKIP: Will not tag this file
Manual intervention required - add to skip list or provide metadata manually.
If cover art is unavailable:
WARNING: No cover art found for album "Rare Demos"
CONTINUE: Tags will be applied without artwork
File is still tagged, just without embedded artwork.
If MusicBrainz rate limit is exceeded:
ERROR: MusicBrainz rate limit exceeded (503 Service Unavailable)
PAUSE: Waiting 60 seconds before retry...
Command automatically waits and retries.
If file cannot be written:
ERROR: Permission denied writing to "file.opus"
SKIP: File will not be modified
Check file permissions and ownership.
After completion, verify:
opustags file.opus or ffprobe -show_format file.mp4Typical processing speeds:
For large collections (1000+ files), expect 20-30 minutes.
Install opustags:
# macOS
brew install opustags
# Ubuntu/Debian
sudo apt install opustags
# Arch
sudo pacman -S opustags
Install ffmpeg:
# macOS
brew install ffmpeg
# Ubuntu/Debian
sudo apt install ffmpeg
# Arch
sudo pacman -S ffmpeg
Increase timeout in curl command:
curl --max-time 30 -s "https://musicbrainz.org/ws/2/recording/..."
Verify image format and size:
# Check image file
file cover.jpg
# Resize if too large
convert cover.jpg -resize 1200x1200 cover-resized.jpg
/organize-media - Organize files without metadata changes/extract-artwork - Extract artwork from existing filesdata-ai
Report which research-corpus radar sidecars are overdue for refresh. Computes staleness (days since last refresh vs the cadence window) for every radar, sorted most-overdue-first. Runs via `aiwg corpus radar-status`.
data-ai
Aggregate research-corpus radar sidecars into a corpus or per-cluster freshness report — totals, overdue count, per-cluster / per-GRADE / per-trajectory breakdowns, an overdue table, and per-radar rationale snippets. Runs via `aiwg corpus radar-report`.
testing
Scaffold radar/freshness sidecars for research-corpus REFs. Pulls title/authors from the citation sidecar and GRADE from the analysis doc, defaults the refresh cadence from GRADE and the cluster from a corpus-local map, and stamps documentation/radar/REF-XXX-radar.md. Runs via `aiwg corpus radar-init`.
data-ai
Compute an entity's publication trajectory — per-year paper counts, topic drift, hot-streak detection (≥3 consecutive A-grade years), and career phase. Runs via `aiwg corpus profile-temporal`.