|
#!/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) |