Created
February 12, 2023 11:01
-
-
Save upsuper/31161a78a8c4a7ec3b3b2fa5aee9b02b to your computer and use it in GitHub Desktop.
Scripts to import from iTunes
This file contains hidden or 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
#!/usr/bin/env python3 | |
import plistlib | |
import sqlite3 | |
import uuid | |
from collections import defaultdict | |
from datetime import datetime | |
from typing import NamedTuple | |
from urllib.parse import unquote | |
NAVIDROME_DB = "<path>/navidrome.db" | |
NAVIDROME_MUSIC_PATH = "/home/<user>/Music/" | |
NAVIDROME_USER_ID = "<user-id>" | |
ITUNES_LIBRARY = "<path>/iTunes Library.xml" | |
class ItunesTrack(NamedTuple): | |
location: str | |
play_count: int | |
play_date: datetime | None | |
def read_itunes_tracks() -> list[ItunesTrack]: | |
with open(ITUNES_LIBRARY, 'rb') as f: | |
itunes_library = plistlib.load(f) | |
base_path = itunes_library['Music Folder'] + 'Music/' | |
result: list[ItunesTrack] = [] | |
for track in itunes_library['Tracks'].values(): | |
location: str = track['Location'] | |
assert location.startswith(base_path) | |
location = unquote(location[len(base_path):]) | |
if 'Play Count' in track: | |
play_count: int = track['Play Count'] | |
play_date = track['Play Date UTC'] | |
else: | |
play_count = 0 | |
play_date = None | |
result.append(ItunesTrack(location, play_count, play_date)) | |
return result | |
class NavidromMediaFile(NamedTuple): | |
id: str | |
path: str | |
artist_id: str | |
album_id: str | |
def read_navidrom_files(db: sqlite3.Connection) -> list[NavidromMediaFile]: | |
cur = db.cursor() | |
result: list[NavidromMediaFile] = [] | |
for (id, path, artist_id, album_id) in cur.execute('\ | |
SELECT id, path, artist_id, album_id \ | |
FROM media_file'): | |
assert path.startswith(NAVIDROME_MUSIC_PATH) | |
path = path[len(NAVIDROME_MUSIC_PATH):] | |
result.append(NavidromMediaFile(id, path, artist_id, album_id)) | |
return result | |
class NavidromAnnotation(NamedTuple): | |
id: str | |
item_id: str | |
item_type: str | |
play_count: int | |
play_date: datetime | |
def read_navidrom_annotations(db: sqlite3.Connection) -> list[NavidromAnnotation]: | |
cur = db.cursor() | |
result: list[NavidromAnnotation] = [] | |
for (id, item_id, item_type, play_count, play_date) in cur.execute('\ | |
SELECT \ | |
ann_id, item_id, item_type, play_count, \ | |
play_date as "play_date [timestamp]" \ | |
FROM annotation \ | |
WHERE user_id=?', (NAVIDROME_USER_ID,)): | |
result.append(NavidromAnnotation(id, item_id, item_type, play_count, play_date)) | |
return result | |
def update_navidrom_annotations( | |
db: sqlite3.Connection, | |
itunes_tracks: list[ItunesTrack], | |
media_files: list[NavidromMediaFile], | |
annotations: list[NavidromAnnotation]): | |
insert_table: list[tuple[str, str, str, str, int, datetime]] = [] | |
update_table: list[tuple[int, str]] = [] | |
media_file_table = {file.path: file for file in media_files} | |
annotation_table = { | |
f'{ann.item_type}:{ann.item_id}': (ann.id, ann.play_count) | |
for ann in annotations | |
} | |
albums: dict[str, tuple[int, datetime | None]] = defaultdict(lambda: (0, None)) | |
artists: dict[str, tuple[int, datetime | None]] = defaultdict(lambda: (0, None)) | |
def update_collection_with( | |
collection: dict[str, tuple[int, datetime | None]], | |
id: str, | |
track: ItunesTrack, | |
): | |
(play_count, play_date) = collection[id] | |
play_count += track.play_count | |
if track.play_date is not None and \ | |
(play_date is None or play_date < track.play_date): | |
play_date = track.play_date | |
collection[id] = (play_count, play_date) | |
def fill_lists( | |
item_type: str, | |
item_id: str, | |
play_count: int, | |
play_date: datetime | None): | |
if play_date is None: | |
assert play_count == 0 | |
return | |
key = f'{item_type}:{item_id}' | |
if key in annotation_table: | |
(id, play_count) = annotation_table[key] | |
play_count += track.play_count | |
update_table.append((play_count, id)) | |
else: | |
insert_table.append(( | |
str(uuid.uuid4()), | |
NAVIDROME_USER_ID, | |
item_id, | |
item_type, | |
play_count, | |
play_date)) | |
# Fill the command lists for tracks | |
for track in itunes_tracks: | |
file = media_file_table[track.location] | |
fill_lists('media_file', file.id, track.play_count, track.play_date) | |
update_collection_with(albums, file.album_id, track) | |
update_collection_with(artists, file.artist_id, track) | |
# Fill the command lists for albums and artists | |
for (album_id, (play_count, play_date)) in albums.items(): | |
fill_lists('album', album_id, play_count, play_date) | |
for (artist_id, (play_count, play_date)) in artists.items(): | |
fill_lists('artist', artist_id, play_count, play_date) | |
cur = db.cursor() | |
cur.executemany('\ | |
INSERT INTO annotation \ | |
(ann_id, user_id, item_id, item_type, play_count, play_date) \ | |
VALUES (?, ?, ?, ?, ?, ?)', insert_table) | |
cur.executemany('\ | |
UPDATE annotation \ | |
SET play_count=? \ | |
WHERE ann_id=?', update_table) | |
db.commit() | |
def main(): | |
itunes_tracks = read_itunes_tracks() | |
navidrome_db = sqlite3.connect( | |
NAVIDROME_DB, | |
detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES) | |
navidrome_files = read_navidrom_files(navidrome_db) | |
navidrome_anns = read_navidrom_annotations(navidrome_db) | |
update_navidrom_annotations( | |
navidrome_db, | |
itunes_tracks, | |
navidrome_files, | |
navidrome_anns) | |
if __name__ == '__main__': | |
main() |
This file contains hidden or 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
#!/usr/bin/env python3 | |
import plistlib | |
from pathlib import Path | |
from typing import NamedTuple | |
from urllib.parse import unquote | |
ITUNES_LIBRARY = "<path>/iTunes Library.xml" | |
PLAYLIST_OUTPUT = "playlists" | |
class ItunesTrack(NamedTuple): | |
id: int | |
location: str | |
def main(): | |
with open(ITUNES_LIBRARY, 'rb') as f: | |
itunes_library = plistlib.load(f) | |
base_path = itunes_library['Music Folder'] + 'Music/' | |
track_map: dict[int, str] = {} | |
for track in itunes_library['Tracks'].values(): | |
id: int = track['Track ID'] | |
location: str = track['Location'] | |
assert location.startswith(base_path) | |
location = unquote(location[len(base_path):]) | |
track_map[id] = location | |
for playlist in itunes_library['Playlists']: | |
if 'Playlist Items' not in playlist: | |
continue | |
name = playlist['Name'] | |
with Path(PLAYLIST_OUTPUT, f'{name}.m3u').open('w') as f: | |
f.write('#EXTM3U\n') | |
for item in playlist['Playlist Items']: | |
id = item['Track ID'] | |
f.write(f'{track_map[id]}\n') | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment