Skip to content

Instantly share code, notes, and snippets.

@sirn
Last active November 17, 2018 10:48
Show Gist options
  • Save sirn/cdac4dd9107f213a8f7fdab2d1bca1e0 to your computer and use it in GitHub Desktop.
Save sirn/cdac4dd9107f213a8f7fdab2d1bca1e0 to your computer and use it in GitHub Desktop.
Fix iTunes Library.xml

Fix iTunes Library

This is a one-off script to fix broken Location entry in iTunes Library.xml.

iTunes internal database will sometimes have a broken file location when exporting. For example, for a purchased song, it might still have a reference to a temporary file at iTunes Media/Downloads rather than file inside iTunes Media/Music directory. While iTunes itself doesn't seems to use these locations for its playback, it becomes problematic when other music players try to import the iTunes library data (e.g. Swinsian).

This script will parse iTunes Library.xml to detect broken file location and try to find the file. This script was created for personal use, so it may or may not work in your environment. Use it at your own risk and remember to backup the library before trying!

You should not need to use this if you're not syncing the iTunes library with other media players on a Mac.

Usage

usage: fixitunesxml.py [-h] [--prefix PREFIX] library

Fixes broken file location in iTunes Library.xml.

positional arguments:
  library          Path to iTunese Library.xml.

optional arguments:
  -h, --help       show this help message and exit
  --prefix PREFIX  Path to directory containing iTunes library.

Example

$ python3 fixitunesxml.py iTunes\ Library.xml
-> 2/2
   -> File could not be found.
   -> Detecting file path...
      -> File detected, rewriting path.
      -> Done.
-> 2/2 (instrumental)
   -> File could not be found.
   -> Detecting file path...
      -> File detected, rewriting path.
      -> Done.
$ ls -1 iTunes\ Library.xml*
iTunes Library.xml
iTunes Library.xml.new

Requirements

Python 3.5

#!/usr/bin/env python3
import argparse
import os
import plistlib
import re
import sys
import urllib.parse
from enum import Enum
class Issue(Enum):
none = 0
metadata_missing = 1
file_not_found = 2
def process_tracks(plist):
for track_id, track_data in plist["Tracks"].items():
if "Movie" in track_data and track_data["Movie"]:
continue
issue = Issue.none
if "Location" in track_data:
parsed_url = urllib.parse.urlparse(track_data["Location"])
file_path = urllib.parse.unquote(parsed_url.path)
if not os.path.exists(file_path):
issue = Issue.file_not_found
else:
issue = Issue.metadata_missing
yield track_id, track_data, issue
def escape_fragment(path):
return re.sub(r"[:/<>]", "_", path)
def safe_join(prefix, *fragments):
return os.path.join(prefix, *[escape_fragment(f) for f in fragments])
def generate_paths(prefix_path, track_data):
base_name = track_data["Name"]
base_path = os.path.join(prefix_path, "Music")
if "Track Number" in track_data:
base_name = "%02d %s" % (track_data["Track Number"], base_name)
if "Compilation" in track_data and track_data["Compilation"]:
yield safe_join(
base_path, "Compilations", track_data["Album"], "%s.m4a" % base_name
)
elif "Album Artist" in track_data:
yield safe_join(
base_path,
track_data["Album Artist"],
track_data["Album"],
"%s.m4a" % base_name,
)
elif "Artist" in track_data:
yield safe_join(
base_path, track_data["Artist"], track_data["Album"], "%s.m4a" % base_name
)
def detect_path(prefix_path, track_data):
for path in generate_paths(prefix_path, track_data):
if os.path.isfile(path):
return path
def main(library_path, prefix_path):
if not os.path.isfile(library_path):
print("File %s does not exists or is not a file." % (library_path,))
print("Please provide a valid path to iTunes Library.xml file.")
sys.exit(1)
with open(library_path, "rb") as fp:
try:
plist = plistlib.load(fp)
except plistlib.InvalidFileException:
print("File %s is not a valid iTunes library." % (library_path,))
print("Please provide a valid path to iTunes Library.xml file.")
sys.exit(1)
if prefix_path is None:
if not "Music Folder" in plist:
print("Music folder does not exists in the iTunes Library.")
print("Please provide --prefix to override.")
sys.exit(1)
prefix_url = urllib.parse.urlparse(plist["Music Folder"], scheme="file")
prefix_path = urllib.parse.unquote(prefix_url.path)
if not os.path.isdir(prefix_path):
print("Path %s does not exists or is not a directory." % (prefix_path,))
print("Please provide a valid path to iTunes library.")
sys.exit(1)
valid = True
changed = False
for track_id, track_data, issue in process_tracks(plist):
if issue is Issue.none:
continue
print("-> \033[1m%s\033[0m" % (track_data["Name"]))
valid = False
if issue is Issue.file_not_found:
print(" -> \033[0;31mFile could not be found.\033[0m")
elif issue is Issue.metadata_missing:
print(" -> \033[0;31mLocation metadata is missing.\033[0m")
print(" -> Detecting file path...")
new_path = detect_path(prefix_path, track_data)
if new_path is None:
print(" -> \033[0;93mFile could not be detected.\033[0m")
continue
print(" -> \033[0;92mFile detected, rewriting path.\033[0;0m")
escaped_new_path = urllib.parse.quote(new_path.encode("utf-8"))
new_url = urllib.parse.urlparse(escaped_new_path, scheme="file")
track_data["Location"] = new_url.geturl()
print(" -> Done.")
if not changed:
changed = True
if valid:
print("-> The file looks valid. Nothing to be done.")
exit(0)
elif not valid and not changed:
print()
print("-> The file looks invalid, but nothing could be done.")
print(" You may want to manually edit the iTunes Library.xml")
with open("%s.new" % (library_path,), "wb") as fp:
plistlib.dump(plist, fp)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="""
Fixes broken file location in iTunes Library.xml.
"""
)
parser.add_argument(
"library",
help="Path to iTunese Library.xml.",
default=os.path.expanduser("~/Music/iTunes/iTunes Library.xml"),
type=str,
)
parser.add_argument(
"--prefix", help="Path to directory containing iTunes library.", type=str
)
args = parser.parse_args()
library_path = os.path.expanduser(args.library)
prefix_path = None
if args.prefix:
prefix_path = os.path.expanduser(args.prefix)
main(library_path, prefix_path)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment