Python library for handling audio metadata across multiple formats including MP3, FLAC, MP4, OGG and many others
—
This document covers Mutagen's support for audio container formats including MP4/M4A (iTunes), ASF/WMA (Windows Media), and other formats that embed audio streams with rich metadata capabilities.
# MP4/M4A/M4B/M4P format
from mutagen.mp4 import MP4, MP4Tags, MP4Cover, MP4FreeForm, AtomDataType
from mutagen.mp4 import Open, delete
# ASF/WMA format
from mutagen.asf import ASF, ASFInfo, ASFTags, ASFValue
from mutagen.asf import Open as ASFOpen
# AAC format (Advanced Audio Coding)
from mutagen.aac import AAC, AACInfo
# AC-3 format (Dolby Digital)
from mutagen.ac3 import AC3, AC3Info
# Musepack format
from mutagen.musepack import Musepack, MusepackInfo
# Easy interfaces
from mutagen.easymp4 import EasyMP4, EasyMP4Tagsclass MP4:
"""MPEG-4 audio container (M4A, M4B, M4P, etc.).
Supports iTunes-style metadata atoms with rich data types including
text, numbers, covers, and custom freeform data.
Attributes:
info: MP4 stream information
tags: MP4Tags container with iTunes-compatible metadata
filename: Path to the MP4 file
"""
def __init__(self, filename: str) -> None:
"""Load MP4 file and parse metadata atoms.
Args:
filename: Path to MP4 file (.m4a, .m4b, .m4p, .mp4, etc.)
Raises:
MutagenError: If file is not valid MP4 or corrupted
"""
def add_tags(self) -> None:
"""Add empty MP4 metadata if none exists."""
def save(self) -> None:
"""Save MP4 metadata back to file."""
# Function aliases
Open = MP4
def delete(filename: str) -> None:
"""Remove MP4 metadata while preserving audio.
Args:
filename: Path to MP4 file
"""
# Usage examples
from mutagen.mp4 import MP4
# Load MP4 file
mp4_file = MP4("song.m4a")
# Access stream info
print(f"Duration: {mp4_file.info.length} seconds")
print(f"Bitrate: {mp4_file.info.bitrate} bps")
# Access iTunes metadata
print(mp4_file["©nam"]) # Title
print(mp4_file["©ART"]) # Artist
print(mp4_file["©alb"]) # Album
# Modify metadata
mp4_file["©nam"] = ["New Title"]
mp4_file["©ART"] = ["New Artist"]
mp4_file.save()MP4 uses iTunes-compatible metadata atoms with specific naming conventions:
class MP4Tags:
"""iTunes-style MP4 metadata container.
Uses 4-character atom names (e.g., ©nam, ©ART) for standard metadata
and freeform atoms for custom data.
"""
# Standard iTunes atoms
STANDARD_ATOMS = {
# Text metadata
"©nam": "Title",
"©ART": "Artist",
"©alb": "Album",
"©day": "Date",
"©gen": "Genre",
"©wrt": "Composer",
"©too": "Encoder",
"©cmt": "Comment",
"©lyr": "Lyrics",
"aART": "Album Artist",
"©grp": "Grouping",
# Numeric metadata
"trkn": "Track Number (current/total)",
"disk": "Disc Number (current/total)",
"tmpo": "BPM",
"©day": "Year",
# Flags/ratings
"rtng": "Rating",
"pcst": "Podcast flag",
"cpil": "Compilation flag",
"hdvd": "HD Video flag",
# Cover art
"covr": "Cover Art",
# Sorting
"sonm": "Sort Title",
"soar": "Sort Artist",
"soal": "Sort Album",
"soaa": "Sort Album Artist",
# Purchase info
"apID": "Purchase Account",
"cnID": "Catalog ID",
}
# Usage examples
mp4_file = MP4("song.m4a")
# Text metadata
mp4_file["©nam"] = ["Song Title"]
mp4_file["©ART"] = ["Artist Name"]
mp4_file["©alb"] = ["Album Name"]
mp4_file["©day"] = ["2023"]
mp4_file["©gen"] = ["Rock"]
# Track/disc numbers (as tuples)
mp4_file["trkn"] = [(1, 12)] # Track 1 of 12
mp4_file["disk"] = [(1, 2)] # Disc 1 of 2
# BPM (as list of integers)
mp4_file["tmpo"] = [120]
# Boolean flags
mp4_file["cpil"] = [True] # Compilation
mp4_file["pcst"] = [True] # Podcast
mp4_file.save()class MP4Cover:
"""MP4 cover art container.
Attributes:
data: Image data as bytes
imageformat: Format constant (FORMAT_JPEG or FORMAT_PNG)
"""
FORMAT_JPEG = 0x0D
FORMAT_PNG = 0x0E
def __init__(self, data: bytes, imageformat: int = None) -> None:
"""Create MP4 cover art.
Args:
data: Image data bytes
imageformat: Image format (auto-detected if None)
"""
# Usage examples
from mutagen.mp4 import MP4, MP4Cover
mp4_file = MP4("song.m4a")
# Add cover art
with open("cover.jpg", "rb") as f:
cover_data = f.read()
# Create cover art object
cover = MP4Cover(cover_data, MP4Cover.FORMAT_JPEG)
# Add to file (supports multiple covers)
mp4_file["covr"] = [cover]
# Multiple covers
with open("back.png", "rb") as f:
back_data = f.read()
back_cover = MP4Cover(back_data, MP4Cover.FORMAT_PNG)
mp4_file["covr"] = [cover, back_cover]
mp4_file.save()
# Access existing covers
covers = mp4_file.get("covr", [])
for i, cover in enumerate(covers):
with open(f"extracted_cover_{i}.jpg", "wb") as f:
f.write(cover)class MP4FreeForm:
"""Freeform MP4 metadata atom.
Allows custom metadata with arbitrary names and data types.
Uses reverse DNS naming convention.
Attributes:
data: Raw bytes data
dataformat: AtomDataType enum value
"""
class AtomDataType:
"""MP4 atom data type enumeration."""
IMPLICIT = 0
UTF8 = 1
UTF16 = 2
SJIS = 3
HTML = 6
XML = 7
UUID = 8
ISRC = 9
MI3P = 10
GIF = 12
JPEG = 13
PNG = 14
URL = 15
DURATION = 16
DATETIME = 17
GENRES = 18
INTEGER = 21
RIAA_PA = 24
# Usage examples
from mutagen.mp4 import MP4, MP4FreeForm, AtomDataType
mp4_file = MP4("song.m4a")
# Custom text metadata
custom_text = MP4FreeForm(b"Custom Value", AtomDataType.UTF8)
mp4_file["----:com.apple.iTunes:CUSTOM_FIELD"] = [custom_text]
# Custom binary data
binary_data = MP4FreeForm(b"\x00\x01\x02\x03", AtomDataType.IMPLICIT)
mp4_file["----:com.apple.iTunes:BINARY_DATA"] = [binary_data]
# Custom integers
integer_data = MP4FreeForm(b"\x00\x00\x00\x7B", AtomDataType.INTEGER) # 123
mp4_file["----:com.apple.iTunes:CUSTOM_NUMBER"] = [integer_data]
mp4_file.save()
# Read freeform data
custom_field = mp4_file.get("----:com.apple.iTunes:CUSTOM_FIELD")
if custom_field:
print(f"Custom field: {custom_field[0].decode('utf-8')}")class ASF:
"""Advanced Systems Format / Windows Media Audio file.
Microsoft's container format supporting WMA audio and rich metadata
through ASF attribute objects.
Attributes:
info: ASFInfo with stream information
tags: ASFTags container with metadata attributes
filename: Path to ASF file
"""
def __init__(self, filename: str) -> None:
"""Load ASF file and parse metadata.
Args:
filename: Path to ASF file (.wma, .wmv, .asf)
Raises:
MutagenError: If file is not valid ASF or corrupted
"""
def add_tags(self) -> None:
"""Add empty ASF metadata if none exists."""
def save(self) -> None:
"""Save ASF metadata back to file."""
class ASFInfo:
"""ASF stream information.
Attributes:
length: Duration in seconds
bitrate: Bitrate in bits per second
sample_rate: Audio sample rate in Hz (if audio stream)
channels: Number of audio channels (if audio stream)
codec_name: Codec name string
codec_description: Detailed codec description
"""
# Function alias
Open = ASF
# Usage examples
from mutagen.asf import ASF
# Load ASF file
asf_file = ASF("song.wma")
# Access stream info
info = asf_file.info
print(f"Codec: {info.codec_name}")
print(f"Duration: {info.length} seconds")
# Access metadata
print(asf_file["Title"])
print(asf_file["Author"]) # Artist
print(asf_file["AlbumTitle"])
# Modify metadata
asf_file["Title"] = ["New Title"]
asf_file["Author"] = ["New Artist"]
asf_file.save()class ASFTags:
"""ASF metadata container.
Dictionary-like interface for ASF attributes. Supports multiple
data types including strings, integers, booleans, and binary data.
"""
class ASFValue:
"""ASF attribute value wrapper.
Handles type conversion and encoding for ASF metadata values.
Attributes:
value: The actual value (various types)
type: ASF data type constant
"""
# Standard ASF attributes
STANDARD_ATTRIBUTES = {
"Title": "Title",
"Author": "Artist/Author",
"AlbumTitle": "Album",
"AlbumArtist": "Album Artist",
"Genre": "Genre",
"Year": "Year",
"Track": "Track Number",
"TrackNumber": "Track Number",
"PartOfSet": "Disc Number",
"Copyright": "Copyright",
"Description": "Comment/Description",
"Rating": "Rating",
"IsVBR": "Variable Bitrate Flag",
}
# Usage examples
from mutagen.asf import ASF, ASFValue
asf_file = ASF("song.wma")
# Text metadata
asf_file["Title"] = ["Song Title"]
asf_file["Author"] = ["Artist Name"]
asf_file["AlbumTitle"] = ["Album Name"]
asf_file["Genre"] = ["Rock"]
# Numeric metadata
asf_file["Track"] = [1]
asf_file["Year"] = [2023]
# Boolean flags
asf_file["IsVBR"] = [True]
# Custom attributes
asf_file["CustomField"] = ["Custom Value"]
asf_file.save()
# Access with type information
title_attr = asf_file.get("Title")
if title_attr:
for value in title_attr:
print(f"Title: {value} (type: {type(value)})")class AAC:
"""Advanced Audio Coding file.
Raw AAC stream without container. Limited metadata support.
Attributes:
info: AACInfo with stream details
filename: Path to AAC file
"""
class AACInfo:
"""AAC stream information.
Attributes:
length: Duration in seconds
bitrate: Bitrate in bits per second
sample_rate: Sample rate in Hz
channels: Number of channels
"""
# Usage examples
from mutagen.aac import AAC
aac_file = AAC("song.aac")
print(f"Sample rate: {aac_file.info.sample_rate}")
print(f"Channels: {aac_file.info.channels}")class AC3:
"""Dolby Digital AC-3 audio file.
Surround sound audio format with limited metadata.
Attributes:
info: AC3Info with stream details
filename: Path to AC-3 file
"""
class AC3Info:
"""AC-3 stream information.
Attributes:
length: Duration in seconds
bitrate: Bitrate in bits per second
sample_rate: Sample rate in Hz
channels: Number of channels
frame_size: Frame size in bytes
"""
# Usage examples
from mutagen.ac3 import AC3
ac3_file = AC3("audio.ac3")
print(f"Channels: {ac3_file.info.channels}")
print(f"Bitrate: {ac3_file.info.bitrate}")class Musepack:
"""Musepack audio file.
High-quality lossy compression with APEv2 tags.
Attributes:
info: MusepackInfo with stream details
tags: APEv2 tag container
filename: Path to Musepack file
"""
class MusepackInfo:
"""Musepack stream information.
Attributes:
length: Duration in seconds
bitrate: Bitrate in bits per second
sample_rate: Sample rate in Hz
channels: Number of channels
version: Musepack version
profile: Quality profile name
"""
# Usage examples
from mutagen.musepack import Musepack
mpc_file = Musepack("song.mpc")
print(f"Profile: {mpc_file.info.profile}")
# APEv2 tags
mpc_file["Title"] = "Song Title"
mpc_file["Artist"] = "Artist Name"
mpc_file.save()class EasyMP4(MP4):
"""MP4 with easy dictionary-like interface.
Provides simplified tag names instead of iTunes atom names.
"""
class EasyMP4Tags:
"""Easy MP4 tags container with normalized field names.
Maps simple tag names to iTunes atoms:
- title -> ©nam
- artist -> ©ART
- album -> ©alb
- date -> ©day
- etc.
"""
# Usage examples
from mutagen.easymp4 import EasyMP4
# Load with easy interface
easy_mp4 = EasyMP4("song.m4a")
# Simple tag names
easy_mp4["title"] = ["Song Title"]
easy_mp4["artist"] = ["Artist Name"]
easy_mp4["album"] = ["Album Name"]
easy_mp4["date"] = ["2023"]
easy_mp4["tracknumber"] = ["1/12"]
easy_mp4["discnumber"] = ["1/2"]
# Multi-value support
easy_mp4["genre"] = ["Rock", "Alternative"]
easy_mp4.save()
# Available keys
print("Available keys:", list(EasyMP4.valid_keys.keys()))from mutagen.mp4 import MP4, MP4Cover
def setup_mp4_complete(filename, metadata, cover_path=None):
"""Set up MP4 file with complete iTunes metadata."""
mp4_file = MP4(filename)
# Basic text metadata
mp4_file["©nam"] = [metadata["title"]]
mp4_file["©ART"] = [metadata["artist"]]
mp4_file["©alb"] = [metadata["album"]]
mp4_file["©day"] = [str(metadata["year"])]
# Track/disc numbers
if "track" in metadata:
track_total = metadata.get("track_total")
if track_total:
mp4_file["trkn"] = [(metadata["track"], track_total)]
else:
mp4_file["trkn"] = [(metadata["track"], 0)]
if "disc" in metadata:
disc_total = metadata.get("disc_total", 0)
mp4_file["disk"] = [(metadata["disc"], disc_total)]
# Optional fields
if "albumartist" in metadata:
mp4_file["aART"] = [metadata["albumartist"]]
if "genre" in metadata:
mp4_file["©gen"] = [metadata["genre"]]
if "composer" in metadata:
mp4_file["©wrt"] = [metadata["composer"]]
if "bpm" in metadata:
mp4_file["tmpo"] = [metadata["bpm"]]
# Flags
if metadata.get("compilation"):
mp4_file["cpil"] = [True]
if metadata.get("podcast"):
mp4_file["pcst"] = [True]
# Cover art
if cover_path:
with open(cover_path, "rb") as f:
cover_data = f.read()
# Detect format
if cover_path.lower().endswith('.png'):
format_type = MP4Cover.FORMAT_PNG
else:
format_type = MP4Cover.FORMAT_JPEG
cover = MP4Cover(cover_data, format_type)
mp4_file["covr"] = [cover]
# Custom metadata
if "custom_fields" in metadata:
for key, value in metadata["custom_fields"].items():
atom_name = f"----:com.apple.iTunes:{key}"
mp4_file[atom_name] = [MP4FreeForm(value.encode('utf-8'),
AtomDataType.UTF8)]
mp4_file.save()
print(f"MP4 file tagged: {filename}")
# Usage
metadata = {
"title": "Song Title",
"artist": "Artist Name",
"album": "Album Name",
"year": 2023,
"track": 1,
"track_total": 12,
"disc": 1,
"albumartist": "Album Artist",
"genre": "Rock",
"bpm": 120,
"compilation": False,
"custom_fields": {
"ENCODER": "Custom Encoder",
"MOOD": "Happy"
}
}
setup_mp4_complete("song.m4a", metadata, "cover.jpg")def convert_metadata(source_file, target_file):
"""Convert metadata between container formats."""
import mutagen
source = mutagen.File(source_file)
target = mutagen.File(target_file)
if not source or not target:
return False
source_format = source.__class__.__name__
target_format = target.__class__.__name__
# Common metadata mapping
metadata_map = {}
if source_format == "MP4":
metadata_map = {
"title": source.get("©nam", [""])[0],
"artist": source.get("©ART", [""])[0],
"album": source.get("©alb", [""])[0],
"date": source.get("©day", [""])[0]
}
elif source_format == "ASF":
metadata_map = {
"title": source.get("Title", [""])[0],
"artist": source.get("Author", [""])[0],
"album": source.get("AlbumTitle", [""])[0],
"date": str(source.get("Year", [0])[0])
}
# Apply to target
if target_format == "MP4":
target["©nam"] = [metadata_map["title"]]
target["©ART"] = [metadata_map["artist"]]
target["©alb"] = [metadata_map["album"]]
target["©day"] = [metadata_map["date"]]
elif target_format == "ASF":
target["Title"] = [metadata_map["title"]]
target["Author"] = [metadata_map["artist"]]
target["AlbumTitle"] = [metadata_map["album"]]
try:
target["Year"] = [int(metadata_map["date"])]
except ValueError:
pass
target.save()
return True
# Usage
convert_metadata("song.m4a", "song.wma")Install with Tessl CLI
npx tessl i tessl/pypi-mutagen