Skip to content

Instantly share code, notes, and snippets.

@erighetto
Last active December 12, 2024 05:58
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 dbi.ZPERSISTENTID
FROM ZDATABASEITEMINFO dbi
JOIN ZSOURCEINFO si ON si.Z_PK = dbi.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 find_related_tracks(persistent_id):
"""
Find all tracks that belong to the same album as the given persistent ID.
This ensures that all tracks from the same album share the same artwork.
"""
try:
tree = ET.parse(ITUNES_LIBRARY_XML)
root = tree.getroot()
# Convert persistent ID to hexadecimal
hex_persistent_id = decimal_to_hex(persistent_id)
target_album = None
target_artist = None
target_year = None
# First, find the track with the given persistent ID
for track in root.findall(".//dict/dict"):
track_persistent_id = None
album = None
artist = None
year = 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 == "Album":
album = track[i + 1].text
elif key == "Artist":
artist = track[i + 1].text
elif key == "Year":
year = track[i + 1].text
if track_persistent_id and track_persistent_id == hex_persistent_id:
target_album = album
target_artist = artist
target_year = year
break
if not target_album or not target_artist:
print(f"No related tracks found for persistent ID {persistent_id}.")
return []
# Now, find all tracks with the same album, artist, and year
related_tracks = []
for track in root.findall(".//dict/dict"):
album = None
artist = None
year = None
location = None
for i in range(0, len(track), 2):
key = track[i].text
if key == "Album":
album = track[i + 1].text
elif key == "Artist":
artist = track[i + 1].text
elif key == "Year":
year = track[i + 1].text
elif key == "Location":
location = track[i + 1].text
if album == target_album and artist == target_artist and year == target_year:
if location:
mp3_path = location.replace("file://", "")
related_tracks.append(unescape(unquote(mp3_path)))
return related_tracks
except Exception as e:
print(f"Error finding related tracks for persistent ID {persistent_id}: {e}")
return []
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 has_artwork(file_path):
# Check if the file has artwork
try:
probe = ffmpeg.probe(file_path)
return any(stream['codec_type'] == 'video' for stream in probe['streams'])
except ffmpeg.Error as e:
print(f"Error probing {file_path}: {e}")
return 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
def is_mp3(file_path):
return file_path.lower().endswith('.mp3')
# 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:
related_tracks = find_related_tracks(persistent_id)
artwork_file = get_artwork_file(artwork_id)
if not artwork_file:
print(f"Artwork not found for persistent ID {persistent_id} - {decimal_to_hex(persistent_id)}.")
continue
for mp3_file in related_tracks:
if os.path.exists(mp3_file) and is_mp3(mp3_file):
if has_artwork(mp3_file):
print(f"{mp3_file} already has artwork. Skipping.")
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