Instantly share code, notes, and snippets.
CreatedFebruary 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 | |
importplistlib | |
importsqlite3 | |
importuuid | |
fromcollectionsimportdefaultdict | |
fromdatetimeimportdatetime | |
fromtypingimportNamedTuple | |
fromurllib.parseimportunquote | |
NAVIDROME_DB="<path>/navidrome.db" | |
NAVIDROME_MUSIC_PATH="/home/<user>/Music/" | |
NAVIDROME_USER_ID="<user-id>" | |
ITUNES_LIBRARY="<path>/iTunes Library.xml" | |
classItunesTrack(NamedTuple): | |
location:str | |
play_count:int | |
play_date:datetime|None | |
defread_itunes_tracks()->list[ItunesTrack]: | |
withopen(ITUNES_LIBRARY,'rb')asf: | |
itunes_library=plistlib.load(f) | |
base_path=itunes_library['Music Folder']+'Music/' | |
result:list[ItunesTrack]= [] | |
fortrackinitunes_library['Tracks'].values(): | |
location:str=track['Location'] | |
assertlocation.startswith(base_path) | |
location=unquote(location[len(base_path):]) | |
if'Play Count'intrack: | |
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)) | |
returnresult | |
classNavidromMediaFile(NamedTuple): | |
id:str | |
path:str | |
artist_id:str | |
album_id:str | |
defread_navidrom_files(db:sqlite3.Connection)->list[NavidromMediaFile]: | |
cur=db.cursor() | |
result:list[NavidromMediaFile]= [] | |
for (id,path,artist_id,album_id)incur.execute('\ | |
SELECT id, path, artist_id, album_id\ | |
FROM media_file'): | |
assertpath.startswith(NAVIDROME_MUSIC_PATH) | |
path=path[len(NAVIDROME_MUSIC_PATH):] | |
result.append(NavidromMediaFile(id,path,artist_id,album_id)) | |
returnresult | |
classNavidromAnnotation(NamedTuple): | |
id:str | |
item_id:str | |
item_type:str | |
play_count:int | |
play_date:datetime | |
defread_navidrom_annotations(db:sqlite3.Connection)->list[NavidromAnnotation]: | |
cur=db.cursor() | |
result:list[NavidromAnnotation]= [] | |
for (id,item_id,item_type,play_count,play_date)incur.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)) | |
returnresult | |
defupdate_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:fileforfileinmedia_files} | |
annotation_table= { | |
f'{ann.item_type}:{ann.item_id}': (ann.id,ann.play_count) | |
foranninannotations | |
} | |
albums:dict[str,tuple[int,datetime|None]]=defaultdict(lambda: (0,None)) | |
artists:dict[str,tuple[int,datetime|None]]=defaultdict(lambda: (0,None)) | |
defupdate_collection_with( | |
collection:dict[str,tuple[int,datetime|None]], | |
id:str, | |
track:ItunesTrack, | |
): | |
(play_count,play_date)=collection[id] | |
play_count+=track.play_count | |
iftrack.play_dateisnotNoneand \ | |
(play_dateisNoneorplay_date<track.play_date): | |
play_date=track.play_date | |
collection[id]= (play_count,play_date) | |
deffill_lists( | |
item_type:str, | |
item_id:str, | |
play_count:int, | |
play_date:datetime|None): | |
ifplay_dateisNone: | |
assertplay_count==0 | |
return | |
key=f'{item_type}:{item_id}' | |
ifkeyinannotation_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 | |
fortrackinitunes_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))inalbums.items(): | |
fill_lists('album',album_id,play_count,play_date) | |
for (artist_id, (play_count,play_date))inartists.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() | |
defmain(): | |
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 | |
importplistlib | |
frompathlibimportPath | |
fromtypingimportNamedTuple | |
fromurllib.parseimportunquote | |
ITUNES_LIBRARY="<path>/iTunes Library.xml" | |
PLAYLIST_OUTPUT="playlists" | |
classItunesTrack(NamedTuple): | |
id:int | |
location:str | |
defmain(): | |
withopen(ITUNES_LIBRARY,'rb')asf: | |
itunes_library=plistlib.load(f) | |
base_path=itunes_library['Music Folder']+'Music/' | |
track_map:dict[int,str]= {} | |
fortrackinitunes_library['Tracks'].values(): | |
id:int=track['Track ID'] | |
location:str=track['Location'] | |
assertlocation.startswith(base_path) | |
location=unquote(location[len(base_path):]) | |
track_map[id]=location | |
forplaylistinitunes_library['Playlists']: | |
if'Playlist Items'notinplaylist: | |
continue | |
name=playlist['Name'] | |
withPath(PLAYLIST_OUTPUT,f'{name}.m3u').open('w')asf: | |
f.write('#EXTM3U\n') | |
foriteminplaylist['Playlist Items']: | |
id=item['Track ID'] | |
f.write(f'{track_map[id]}\n') | |
if__name__=='__main__': | |
main() |
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment