Last active
November 14, 2024 05:37
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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