Python library for handling audio metadata across multiple formats including MP3, FLAC, MP4, OGG and many others
—
This document covers Mutagen's easy interfaces that provide simplified, normalized tag access across different audio formats. Easy interfaces abstract away format-specific tag names and provide dictionary-like access with common field names.
# Easy ID3 interface for MP3 files
from mutagen.easyid3 import EasyID3
# Easy MP4 interface for M4A files
from mutagen.easymp4 import EasyMP4, EasyMP4Tags
# Easy TrueAudio interface
from mutagen.trueaudio import EasyTrueAudio
# Easy MP3 class (combines MP3 with EasyID3)
from mutagen.mp3 import EasyMP3
# Base easy functionality
from mutagen._util import DictMixinEasy interfaces solve the problem of format-specific tag names by providing normalized field names that work consistently across different audio formats:
Problem: Different formats use different tag names
TIT2 for title, TPE1 for artistTITLE for title, ARTIST for artist©nam for title, ©ART for artistSolution: Easy interfaces use common names
title for title across all formatsartist for artist across all formatsclass EasyID3:
"""Easy dictionary-like interface for ID3 tags.
Provides simplified tag names instead of ID3 frame IDs.
Automatically handles encoding and multi-value conversion.
Attributes:
valid_keys: Dict mapping easy names to ID3 frame names
filename: Path to audio file
"""
def __init__(self, filename: str = None) -> None:
"""Create EasyID3 instance.
Args:
filename: Path to MP3 file (optional)
Raises:
MutagenError: If file is not valid MP3 or has no ID3 tags
"""
def save(self, **kwargs) -> None:
"""Save ID3 tags back to file."""
def delete(self) -> None:
"""Remove all ID3 tags from file."""
def add_tags(self) -> None:
"""Add empty ID3 tags if none exist."""
# Standard easy field mappings for ID3 (alphabetical order)
EASY_ID3_FIELDS = {
'album': 'TALB', # Album title
'albumartist': 'TPE2', # Album artist
'albumartistsort': 'TSO2', # Album artist sort order
'albumsort': 'TSOA', # Album sort order
'arranger': 'TPE4', # Arranger
'artist': 'TPE1', # Lead artist
'artistsort': 'TSOP', # Artist sort order
'author': 'TOLY', # Original lyricist
'bpm': 'TBPM', # Beats per minute
'compilation': 'TCMP', # iTunes compilation flag
'composer': 'TCOM', # Composer
'composersort': 'TSOC', # Composer sort order
'conductor': 'TPE3', # Conductor
'copyright': 'TCOP', # Copyright message
'date': 'TDRC', # Recording date (special handling)
'discnumber': 'TPOS', # Disc number
'discsubtitle': 'TSST', # Set subtitle
'encodedby': 'TENC', # Encoded by
'genre': 'TCON', # Genre (special handling)
'grouping': 'TIT1', # Content group description
'isrc': 'TSRC', # ISRC code
'language': 'TLAN', # Language
'length': 'TLEN', # Length in milliseconds
'lyricist': 'TEXT', # Lyricist
'media': 'TMED', # Media type
'mood': 'TMOO', # Mood
'organization': 'TPUB', # Publisher
'originaldate': 'TDOR', # Original release date (special handling)
'title': 'TIT2', # Title
'titlesort': 'TSOT', # Title sort order
'tracknumber': 'TRCK', # Track number
'version': 'TIT3', # Subtitle/description refinement
}
# Usage examples
from mutagen.easyid3 import EasyID3
# Load MP3 with easy interface
easy_tags = EasyID3("song.mp3")
# Simple tag access with normalized names
easy_tags["title"] = ["Song Title"]
easy_tags["artist"] = ["Artist Name"]
easy_tags["album"] = ["Album Name"]
easy_tags["date"] = ["2023"]
easy_tags["tracknumber"] = ["1/12"] # Track 1 of 12
easy_tags["discnumber"] = ["1/2"] # Disc 1 of 2
# Multi-value fields
easy_tags["genre"] = ["Rock", "Alternative"]
easy_tags["artist"] = ["Primary Artist", "Featured Artist"]
# Additional metadata
easy_tags["albumartist"] = ["Album Artist"]
easy_tags["composer"] = ["Composer Name"]
easy_tags["bpm"] = ["120"]
easy_tags["comment"] = ["This is a comment"]
# Save changes
easy_tags.save()
# Read tags
print(f"Title: {easy_tags['title'][0]}")
print(f"Artists: {', '.join(easy_tags['artist'])}")
# List available keys
print("Available keys:", list(EasyID3.valid_keys.keys()))
# Check if key exists
if "title" in easy_tags:
print(f"Title: {easy_tags['title'][0]}")class EasyMP3:
"""MP3 file with EasyID3 interface built-in.
Combines MP3 format support with EasyID3 tag interface.
Acts like MP3 class but uses easy tag names.
"""
def __init__(self, filename: str) -> None:
"""Load MP3 file with easy tag interface."""
# Usage examples
from mutagen.mp3 import EasyMP3
# Load MP3 with built-in easy interface
mp3_file = EasyMP3("song.mp3")
# Access stream info like regular MP3
print(f"Duration: {mp3_file.info.length} seconds")
print(f"Bitrate: {mp3_file.info.bitrate} bps")
# Use easy tag names
mp3_file["title"] = ["Song Title"]
mp3_file["artist"] = ["Artist Name"]
mp3_file.save()
# Auto-detection with easy interface
import mutagen
easy_mp3 = mutagen.File("song.mp3", easy=True) # Returns EasyMP3class EasyMP4:
"""Easy dictionary-like interface for MP4 metadata.
Provides simplified tag names instead of iTunes atom names.
Automatically handles data type conversion and encoding.
"""
def __init__(self, filename: str) -> None:
"""Create EasyMP4 instance.
Args:
filename: Path to MP4 file (.m4a, .m4b, .m4p, etc.)
"""
class EasyMP4Tags:
"""Easy MP4 tags container with normalized field names."""
# Standard easy field mappings for MP4
EASY_MP4_FIELDS = {
# Basic metadata
'title': '©nam', # Title
'artist': '©ART', # Artist
'album': '©alb', # Album
'albumartist': 'aART', # Album artist
'date': '©day', # Date
# Track/disc information
'tracknumber': 'trkn', # Track number (special handling)
'discnumber': 'disk', # Disc number (special handling)
# Classification
'genre': '©gen', # Genre
'grouping': '©grp', # Grouping
# Credits
'composer': '©wrt', # Writer/Composer
'comment': '©cmt', # Comment
'lyrics': '©lyr', # Lyrics
# Technical
'encodedby': '©too', # Encoded by
'copyright': '©cpy', # Copyright
'bpm': 'tmpo', # Beats per minute (special handling)
# Sorting (iTunes)
'titlesort': 'sonm', # Title sort order
'artistsort': 'soar', # Artist sort order
'albumsort': 'soal', # Album sort order
'albumartistsort': 'soaa', # Album artist sort order
}
# Usage examples
from mutagen.easymp4 import EasyMP4
# Load MP4 with easy interface
easy_mp4 = EasyMP4("song.m4a")
# Simple tag access
easy_mp4["title"] = ["Song Title"]
easy_mp4["artist"] = ["Artist Name"]
easy_mp4["album"] = ["Album Name"]
easy_mp4["date"] = ["2023"]
# Track/disc numbers (simplified)
easy_mp4["tracknumber"] = ["1/12"] # Automatically converts to tuple
easy_mp4["discnumber"] = ["1/2"] # Automatically converts to tuple
# Numeric fields
easy_mp4["bpm"] = ["120"] # Automatically converts to integer
# Multi-value support
easy_mp4["genre"] = ["Rock", "Alternative"]
# Sorting fields for iTunes
easy_mp4["titlesort"] = ["Title, The"] # Sort ignoring "The"
easy_mp4["artistsort"] = ["Smith, John"] # Last, First format
easy_mp4.save()
# Read tags
print(f"Title: {easy_mp4['title'][0]}")
print(f"Track: {easy_mp4['tracknumber'][0]}") # Returns "1/12" formatclass EasyTrueAudio:
"""TrueAudio file with EasyID3 interface.
Since TrueAudio uses ID3v2 tags, it can use the same easy
interface as MP3 files.
"""
# Usage examples
from mutagen.trueaudio import EasyTrueAudio
# Load TrueAudio with easy interface
tta_file = EasyTrueAudio("song.tta")
# Same easy field names as EasyID3
tta_file["title"] = ["Song Title"]
tta_file["artist"] = ["Artist Name"]
tta_file.save()def get_easy_tags(filename):
"""Get tags using easy interface regardless of format."""
import mutagen
from mutagen.easyid3 import EasyID3
from mutagen.easymp4 import EasyMP4
from mutagen.trueaudio import EasyTrueAudio
# Try auto-detection with easy interface
audio_file = mutagen.File(filename, easy=True)
if audio_file:
return audio_file
# Fallback to format-specific easy interfaces
try:
if filename.lower().endswith('.mp3'):
return EasyID3(filename)
elif filename.lower().endswith(('.m4a', '.m4b', '.m4p', '.mp4')):
return EasyMP4(filename)
elif filename.lower().endswith('.tta'):
return EasyTrueAudio(filename)
except:
pass
return None
def set_basic_tags(filename, metadata):
"""Set basic tags using easy interface."""
easy_file = get_easy_tags(filename)
if not easy_file:
print(f"No easy interface available for {filename}")
return False
# Standard fields that work across easy interfaces
field_mapping = {
'title': 'title',
'artist': 'artist',
'album': 'album',
'date': 'date',
'genre': 'genre',
'tracknumber': 'tracknumber',
'albumartist': 'albumartist'
}
for source_key, easy_key in field_mapping.items():
if source_key in metadata and easy_key in easy_file.valid_keys:
value = metadata[source_key]
easy_file[easy_key] = [str(value)] if not isinstance(value, list) else value
easy_file.save()
return True
# Usage
metadata = {
'title': 'Song Title',
'artist': 'Artist Name',
'album': 'Album Name',
'date': '2023',
'genre': 'Rock',
'tracknumber': '1'
}
# Works with MP3, MP4, TrueAudio
set_basic_tags("song.mp3", metadata)
set_basic_tags("song.m4a", metadata)
set_basic_tags("song.tta", metadata)def migrate_tags_easy(source_file, target_file):
"""Migrate tags between formats using easy interfaces."""
source_easy = get_easy_tags(source_file)
target_easy = get_easy_tags(target_file)
if not source_easy or not target_easy:
return False
# Get intersection of supported fields
source_keys = set(source_easy.valid_keys.keys())
target_keys = set(target_easy.valid_keys.keys())
common_keys = source_keys & target_keys
# Copy common fields
for key in common_keys:
if key in source_easy:
target_easy[key] = source_easy[key]
target_easy.save()
print(f"Migrated {len(common_keys)} tag fields")
return True
# Example: Copy tags from MP3 to M4A
migrate_tags_easy("song.mp3", "song.m4a")# Extend EasyID3 with custom mappings
from mutagen.easyid3 import EasyID3
def add_custom_easy_mapping(easy_key, frame_id, getter=None, setter=None):
"""Add custom field mapping to EasyID3."""
if getter is None:
def getter(id3, key):
return [frame.text[0] for frame in id3.getall(frame_id)]
if setter is None:
def setter(id3, key, value):
from mutagen.id3 import TextFrame
frame_class = getattr(mutagen.id3, frame_id)
id3[frame_id] = frame_class(encoding=3, text=value)
EasyID3.RegisterKey(easy_key, getter, setter)
# Add custom field for mood
def mood_get(id3, key):
"""Get mood from TMOO frame."""
frames = id3.getall('TMOO')
return [frame.text[0] for frame in frames] if frames else []
def mood_set(id3, key, value):
"""Set mood in TMOO frame."""
from mutagen.id3 import TMOO
id3['TMOO'] = TMOO(encoding=3, text=value)
add_custom_easy_mapping('mood', 'TMOO', mood_get, mood_set)
# Now use custom field
easy_tags = EasyID3("song.mp3")
easy_tags['mood'] = ['Happy']
easy_tags.save()def validate_easy_tags(easy_file, required_fields=None):
"""Validate easy tags and normalize values."""
if required_fields is None:
required_fields = ['title', 'artist', 'album']
issues = []
# Check required fields
for field in required_fields:
if field not in easy_file or not easy_file[field]:
issues.append(f"Missing required field: {field}")
# Normalize track numbers
if 'tracknumber' in easy_file:
track_values = easy_file['tracknumber']
normalized_tracks = []
for track in track_values:
# Ensure track number format
if '/' not in track:
normalized_tracks.append(track)
else:
# Validate track/total format
try:
current, total = track.split('/', 1)
int(current) # Validate numeric
int(total) # Validate numeric
normalized_tracks.append(track)
except ValueError:
issues.append(f"Invalid track number format: {track}")
if normalized_tracks != track_values:
easy_file['tracknumber'] = normalized_tracks
# Normalize dates to YYYY format
if 'date' in easy_file:
date_values = easy_file['date']
normalized_dates = []
for date in date_values:
# Extract year from various date formats
import re
year_match = re.search(r'(\d{4})', date)
if year_match:
normalized_dates.append(year_match.group(1))
else:
issues.append(f"Invalid date format: {date}")
if normalized_dates != date_values:
easy_file['date'] = normalized_dates
return issues
# Usage
easy_tags = EasyID3("song.mp3")
issues = validate_easy_tags(easy_tags)
if issues:
print("Tag validation issues:")
for issue in issues:
print(f" - {issue}")
else:
print("All tags valid")
easy_tags.save()import os
import mutagen
def batch_easy_tag(directory, tag_updates):
"""Batch update tags using easy interfaces."""
results = {'success': [], 'failed': [], 'skipped': []}
for filename in os.listdir(directory):
filepath = os.path.join(directory, filename)
if not os.path.isfile(filepath):
continue
try:
# Try easy interface first
audio_file = mutagen.File(filepath, easy=True)
if audio_file and hasattr(audio_file, 'valid_keys'):
# Apply updates
for key, value in tag_updates.items():
if key in audio_file.valid_keys:
audio_file[key] = [value] if not isinstance(value, list) else value
audio_file.save()
results['success'].append(filename)
else:
results['skipped'].append(f"{filename} (no easy interface)")
except Exception as e:
results['failed'].append(f"{filename} ({str(e)})")
return results
# Usage
tag_updates = {
'albumartist': 'Various Artists',
'date': '2023',
'genre': 'Compilation'
}
results = batch_easy_tag("/path/to/music", tag_updates)
print(f"Successfully updated: {len(results['success'])} files")
print(f"Failed: {len(results['failed'])} files")
print(f"Skipped: {len(results['skipped'])} files")COMMON_EASY_FIELDS = [
'title', # Track title
'artist', # Primary artist
'album', # Album name
'albumartist', # Album artist
'date', # Release date
'genre', # Musical genre
'tracknumber', # Track number (format: "1" or "1/12")
'discnumber', # Disc number (format: "1" or "1/2")
]EXTENDED_EASY_FIELDS = [
'composer', # Composer name
'conductor', # Conductor name
'lyricist', # Lyricist name
'performer', # Performer credits
'grouping', # Content grouping
'comment', # Comment text
'lyrics', # Song lyrics
'bpm', # Beats per minute
'encodedby', # Encoded by
'copyright', # Copyright notice
'website', # Official website
'isrc', # International Standard Recording Code
'language', # Language code
'originaldate', # Original release date
'titlesort', # Title sort order
'artistsort', # Artist sort order
'albumsort', # Album sort order
]Install with Tessl CLI
npx tessl i tessl/pypi-mutagen