Skip to content

Instantly share code, notes, and snippets.

@upsuper
Created February 12, 2023 11:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save upsuper/31161a78a8c4a7ec3b3b2fa5aee9b02b to your computer and use it in GitHub Desktop.
Save upsuper/31161a78a8c4a7ec3b3b2fa5aee9b02b to your computer and use it in GitHub Desktop.
Scripts to import from iTunes
#!/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()
#!/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