Skip to content

Instantly share code, notes, and snippets.

@erighetto
Last active November 14, 2024 05:37
Show Gist options
  • Save erighetto/e00a61d1157c6af779cb5ff66948e9d9 to your computer and use it in GitHub Desktop.
Save erighetto/e00a61d1157c6af779cb5ff66948e9d9 to your computer and use it in GitHub Desktop.
a Python script that embeds album cover images directly into each MP3 file
# When transferring an iTunes library to a new Mac, one common issue is that album artwork often fails to display correctly.
# This can happen because iTunes stores cover art separately from the actual MP3 files, so artwork may not carry over smoothly during migration.
# To solve this, a Python script can be used to embed album cover images directly into each MP3 file's metadata.
# By embedding the artwork, each song will retain its cover art no matter where it’s moved or which music player is used.
# This approach ensures a more seamless experience, making the iTunes library look complete and visually organized on the new device.
# This script simplifies the embedding process by automating image retrieval and integration for each MP3 file,
# saving users from the hassle of manually adding artwork.
import os
import xml.etree.ElementTree as ET
import sqlite3
import ffmpeg
from urllib.request import unquote
from html import unescape
import tempfile
import shutil
# Path to iTunes library XML file
ITUNES_LIBRARY_XML = os.path.expanduser("~/Library/CloudStorage/OneDrive-Personale/MusicLibrary/MusicLibrayItems.xml")
# Artwork folder
ARTWORKS_PATH = os.path.expanduser("~/Library/Containers/com.apple.AMPArtworkAgent/Data/Documents/artwork")
# SQLite database path
ARTWORK_DB_PATH = os.path.expanduser("~/Library/Containers/com.apple.AMPArtworkAgent/Data/Documents/artworkd.sqlite")
def get_all_artwork_ids():
try:
conn = sqlite3.connect(ARTWORK_DB_PATH)
cursor = conn.cursor()
# Fetch all distinct artwork hashes
cursor.execute("SELECT DISTINCT ZHASHSTRING FROM ZIMAGEINFO")
artworks = cursor.fetchall()
conn.close()
return [artwork[0] for artwork in artworks]
except sqlite3.Error as e:
print(f"SQLite database error: {e}")
return []
def get_persistent_ids_for_artwork(artwork_id):
try:
conn = sqlite3.connect(ARTWORK_DB_PATH)
cursor = conn.cursor()
# Fetch all persistent IDs associated with a given artwork
cursor.execute("""
SELECT dii.ZPERSISTENTID
FROM ZDATABASEITEMINFO dii
JOIN ZSOURCEINFO si ON si.Z_PK = dii.ZSOURCEINFO
JOIN ZIMAGEINFO ii ON ii.Z_PK = si.ZIMAGEINFO
WHERE ii.ZHASHSTRING = ?
""", (artwork_id,))
persistent_ids = cursor.fetchall()
conn.close()
return [persistent_id[0] for persistent_id in persistent_ids]
except sqlite3.Error as e:
print(f"SQLite database error: {e}")
return []
def get_mp3_file_path(persistent_id):
try:
tree = ET.parse(ITUNES_LIBRARY_XML)
root = tree.getroot()
# Convert persistent ID to hexadecimal
hex_persistent_id = decimal_to_hex(persistent_id)
# Find the element with the matching Persistent ID
for track in root.findall(".//dict/dict"):
track_persistent_id = None
location = None
for i in range(0, len(track), 2):
key = track[i].text
if key == "Persistent ID":
track_persistent_id = track[i + 1].text
elif key == "Location":
location = track[i + 1].text
if track_persistent_id and track_persistent_id == hex_persistent_id:
f = location.replace("file://", "")
return unescape(unquote(f))
return None
except Exception as e:
print(f"Error parsing XML: {e}")
return None
def apply_artwork_to_mp3(mp3_file, artwork_file):
try:
# Create a temporary file
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file:
temp_mp3_path = temp_file.name
# Add artwork and save output to temporary file
ffmpeg.input(mp3_file).output(
temp_mp3_path, **{'c:v': 'copy', 'c:a': 'copy', 'metadata:s:v': f'file={artwork_file}'}
).run(overwrite_output=True)
# Replace original file with the temporary file
shutil.move(temp_mp3_path, mp3_file)
print(f"Successfully added artwork to {mp3_file}")
except Exception as e:
print(f"Error adding artwork to {mp3_file}: {e}")
# Remove temporary file in case of error
if os.path.exists(temp_mp3_path):
os.remove(temp_mp3_path)
def decimal_to_hex(decimal_id):
# Convert integer to unsigned 16-digit hexadecimal string
return format(decimal_id & (2**64 - 1), '016X')
def probe_mp3_file(file_path):
# Check if the file is in MP3 format and has artwork
try:
probe = ffmpeg.probe(file_path)
is_mp3 = any(stream['codec_name'] == 'mp3' for stream in probe['streams'] if stream['codec_type'] == 'audio')
has_artwork = any(stream['codec_type'] == 'video' for stream in probe['streams'])
return is_mp3, has_artwork
except ffmpeg.Error as e:
print(f"Error probing {file_path}: {e}")
return False, False
def get_artwork_file(artwork_id):
# Find the artwork file path
for dirpath, _, filenames in os.walk(ARTWORKS_PATH):
for file in filenames:
if artwork_id in file:
return os.path.join(dirpath, file)
return None
# Execute the algorithm for each artwork
for artwork_id in get_all_artwork_ids():
# Retrieve all persistent IDs associated with this artwork
persistent_ids = get_persistent_ids_for_artwork(artwork_id)
artwork_file = get_artwork_file(artwork_id)
if not artwork_file:
print(f'Artwork not found for artwork ID {artwork_id}.')
continue
# For each persistent ID, get the MP3 file path and apply the artwork
for persistent_id in persistent_ids:
mp3_file = get_mp3_file_path(persistent_id)
if not mp3_file or not os.path.exists(mp3_file):
print(f"File MP3 not found for persistent ID {persistent_id}.")
continue
is_mp3, has_artwork = probe_mp3_file(mp3_file)
if not is_mp3:
continue
elif has_artwork:
continue
apply_artwork_to_mp3(mp3_file, artwork_file)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment