[READ-ONLY] Mirror of https://github.com/probablykasper/gpm-to-itunes. Imports Google Play Music library to iTunes
cli google-play-music itunes
0

Configure Feed

Select the types of activity you want to include in your feed.

gpm-to-itunes / main.py
18 kB 404 lines
1import gmusicapi 2import json, datetime, pprint, sys, os, subprocess 3from pathlib import Path 4import appscript 5from tqdm import tqdm 6from mp3_tagger import MP3File, VERSION_1, VERSION_2, VERSION_BOTH 7pprint = pprint.PrettyPrinter(indent=4).pprint 8 9if len(sys.argv) > 1: 10 action = sys.argv[1] 11else: 12 sys.exit('Error: Must have an "action" argument.') 13 14if action == 'match_files': 15 if len(sys.argv) > 2: 16 songs_path = sys.argv[2] 17 else: 18 sys.exit('Error: Must have a "songs_path" argument.') 19elif action == 'add_to_itunes': 20 if len(sys.argv) > 3: 21 sudo_password = sys.argv[2] 22 itunes_media_folder = sys.argv[3] 23 else: 24 sys.exit('Error: Must have "sudo_password" and "itunes_media_folder" argument.') 25 26def load_library(path = "user_files/library.json"): 27 print('Loading GPM library...') 28 gpm_library = json.loads(open(path).read()) 29 print(" Track count:", len(gpm_library["tracks"])) 30 print(" Playlist count:", len(gpm_library["playlists"])) 31 return gpm_library 32def save_library(content, path = "user_files/library.json"): 33 file = open(path, "w+") 34 file.write(json.dumps(content, indent=2)) 35 file.close() 36 37def get_key(track, id3_tags = False): 38 # format: 'artist - title - album' 39 if 'title' in track: 40 return '%s - %s - %s' % (track.get('artist', ''), track.get('title', ''), track.get('album', '')) 41 elif 'song' in track: 42 return '%s - %s - %s' % (track.get('artist', ''), track.get('song', ''), (track.get('album', '') or '')) 43 44def gpm_timestamp_to_date(gpm_timestamp): 45 timestamp = int(gpm_timestamp)/1e6 46 date = datetime.datetime.fromtimestamp(timestamp) 47 return date 48 49def get_posix_path(file_path): 50 from shlex import quote 51 escaped_file_path = quote(file_path).strip("'") 52 posix_path_cmd = """ osascript -e 'POSIX file "%s" as string' """ % (escaped_file_path) 53 posix_path = subprocess.check_output(posix_path_cmd, shell=True).decode("utf-8").rstrip() 54 return posix_path 55 56def init_gpm_api(new_login): 57 gpm_oauth_credentials_path = 'gpm_oauth_credentials.cred' 58 Mobileclient = gmusicapi.Mobileclient() 59 if new_login: Mobileclient.perform_oauth(storage_filepath=gpm_oauth_credentials_path) 60 if not Path(gpm_oauth_credentials_path).is_file(): sys.exit('Not logged in.') 61 logged_in = Mobileclient.oauth_login(Mobileclient.FROM_MAC_ADDRESS, oauth_credentials=gpm_oauth_credentials_path) 62 if logged_in == False or not Mobileclient.is_authenticated: 63 sys.exit("Not logged in.") 64 return Mobileclient 65 66def download_library(): 67 Mobileclient = init_gpm_api(new_login=False) 68 print('Downloading GPM library metadata...') 69 library = { 70 'tracks': Mobileclient.get_all_songs(), 71 'playlists': Mobileclient.get_all_user_playlist_contents(), 72 } 73 return library 74 75def restructure_library(original_library): 76 print('Restructuring GPM library...') 77 library = { 78 'tracks': {}, 79 'track_keys': {}, 80 'playlists': original_library['playlists'], 81 } 82 duplicates = 0 83 for gpm_track in original_library['tracks']: 84 key = get_key(gpm_track) 85 if key in original_library['tracks']: 86 print(' Error: Duplicate track "' + key + '"') 87 duplicates += 1 88 else: 89 library['tracks'][key] = gpm_track 90 library['track_keys'][gpm_track['id']] = key 91 if duplicates != 0: 92 print( 93 "Found ", str(duplicates), " duplicates." 94 " If you proceed, only one of the duplicates will be kept." 95 " To fix this, delete or rename tracks from your GPM library" 96 " that have the same artist name, title and album. Note that this means" 97 " you'll also have to re-download your music if you've already downloaded it" 98 ) 99 return library 100 101def match_files(gpm_library): 102 # scan how many files there are 103 fileCount = 0 104 for subdir, dirs, files in os.walk(songs_path): 105 fileCount += len(files) 106 with tqdm(total=fileCount) as progressbar: 107 for subdir, dirs, files in os.walk(songs_path): 108 for file in files: 109 file_path = os.path.join(subdir, file) 110 filename, extension = os.path.splitext(file_path) 111 if extension != ".mp3": continue 112 mp3 = MP3File(file_path) 113 mp3.set_version(VERSION_2) 114 key = get_key(mp3.get_tags(), True) 115 116 gpm_track = gpm_library['tracks'][key] 117 track_md = { 118 'played_count': gpm_track['playCount'], 119 'loved': True if 'rating' in gpm_track and gpm_track['rating'] == '5' else False, 120 'disliked': True if 'rating' in gpm_track and gpm_track['rating'] == '1' else False, 121 'date_added': gpm_track['creationTimestamp'], 122 'played_date': gpm_track['recentTimestamp'], 123 'year': gpm_track['year'], 124 'album_artist': gpm_track['albumArtist'], 125 'composer': gpm_track['composer'], 126 'comment': gpm_track['comment'] if "comment" in gpm_track else "", 127 'track_number': gpm_track['trackNumber'], 128 'track_count': gpm_track['totalTrackCount'], 129 'genre': gpm_track['genre'], 130 'disc_number': gpm_track['discNumber'], 131 'disc_count': gpm_track['totalDiscCount'], 132 } 133 track_md['file_path'] = file_path 134 if key in gpm_library['tracks']: 135 gpm_library['tracks'][key]['track_md'] = track_md 136 else: 137 tqdm.write(' Error: Track "%s" could not be matched to GPM library.' % (key)) 138 progressbar.update(1) 139 for key, track in gpm_library['tracks'].items(): 140 if 'track_md' not in track: 141 if 'file_path' not in track['track_md']: 142 print('Error: Track "%s" could not be associated with a song file' % (key)) 143 return gpm_library 144 145def add_to_itunes(gpm_library, only_scan): 146 147 iTunes = appscript.app('iTunes') 148 itunes_library = iTunes.library_playlists['Library'] 149 if Path('user_files/md_map.json').is_file(): 150 md_map = json.loads(open('user_files/md_map.json').read()) 151 else: 152 md_map = {} 153 154 # find unmatched itunes tracks 155 itunes_tracks = itunes_library.tracks.get() 156 unmatched_itunes_tracks = {} 157 for itunes_track in itunes_tracks: 158 title = getattr(itunes_track, "name").get() 159 artist = getattr(itunes_track, "artist").get() 160 album = getattr(itunes_track, "album").get() 161 key = title +" - "+ artist +" - "+ album 162 unmatched_itunes_tracks[key] = { 163 "title": title, 164 "artist": artist, 165 "album": album, 166 } 167 168 # scan itunes 169 if True: 170 print('Scanning iTunes library for already existing and duplicate tracks...') 171 already_existing = 0 172 duplicates = 0 173 for key, gpm_track in tqdm(gpm_library['tracks'].items()): 174 title = gpm_track['title'] 175 artist = gpm_track['artist'] 176 album = gpm_track['album'] 177 gpm_track['md_map'] = False 178 md_map_key = title+" - "+artist+" - "+album 179 if md_map_key in md_map: 180 gpm_track['md_map'] = True 181 title = md_map[md_map_key]['title'] 182 artist = md_map[md_map_key]['artist'] 183 album = md_map[md_map_key]['album'] 184 search_criteria = appscript.its.name == title 185 search_criteria = search_criteria.AND(appscript.its.artist == artist) 186 search_criteria = search_criteria.AND(appscript.its.album == album) 187 search_result = itunes_library.tracks[search_criteria].get() 188 if len(search_result) == 0: 189 gpm_track['already_exists'] = False 190 else: 191 gpm_track['already_exists'] = True 192 gpm_track['itunes_track'] = search_result[0] 193 unmatched_itunes_tracks.pop(title+" - "+artist+" - "+album, None) 194 if len(search_result) == 1: 195 tqdm.write(' Track already exists in iTunes library "%s"' % key) 196 already_existing += 1 197 else: 198 tqdm.write(' Error: Duplicate(s) in iTunes library of track "%s' % key) 199 duplicates += 1 200 if already_existing != 0: 201 print( 202 "Found", str(already_existing), "already existing track(s) in the itunes library." 203 " If you proceed, the script will use the track already in iTunes." 204 " The track will keep the itunes track's 'date added'," 205 " use the newst 'last played date'" 206 " and combine the play counts from both tracks." 207 ) 208 if duplicates != 0: 209 print( 210 'Error: Found', str(duplicates), 'duplicate track(s) in the itunes library.' 211 ' If you proceed, one of the duplicates will be considered an already existing track.' 212 ) 213 print() 214 print() 215 print() 216 for key, value in unmatched_itunes_tracks.items(): 217 print(key) 218 print("Unmatched iTunes tracks:") 219 print(json.dumps(unmatched_itunes_tracks, indent=4)) 220 221 # add tracks to itunes 222 if only_scan == False: 223 print('Setting system clock, adding tracks to iTunes and updating metadata...') 224 225 # stop using network time (so that we can edit time) 226 usingnetworktime_cmd = 'echo 123 | sudo -S systemsetup -getusingnetworktime'.split() 227 usingnetworktime_output = subprocess.check_output(usingnetworktime_cmd).decode("utf-8") 228 original_usingnetworktime = 'off' if 'Network Time: Off' in usingnetworktime_output else 'on' 229 subprocess.call( 230 ('echo %s | sudo -S systemsetup -setusingnetworktime %s' 231 % (sudo_password, 'off')), shell=True, stdout=subprocess.PIPE 232 ) 233 print() 234 235 # folder playlist 236 itunes_folder_playlist = iTunes.make( 237 new=appscript.k.folder_playlist, 238 with_properties={ 239 appscript.k.name: 'GPM PLAYLISTS', 240 appscript.k.description: 'Playlists imported from GPM using the gpm-to-itunes script', 241 } 242 ) 243 # make playlists for tracks that were liked/disliked in GPM 244 thumbs_up_playlist = iTunes.make( 245 new=appscript.k.user_playlist, 246 with_properties={ 247 appscript.k.name: 'GPM Thumbs Up' 248 } 249 ) 250 thumbs_down_playlist = iTunes.make( 251 new=appscript.k.user_playlist, 252 with_properties={ 253 appscript.k.name: 'GPM Thumbs Down' 254 } 255 ) 256 # add thumbs up/down playlists to folder playlist 257 thumbs_up_playlist.move(to=itunes_folder_playlist) 258 thumbs_down_playlist.move(to=itunes_folder_playlist) 259 260 FNULL = open(os.devnull, 'w') 261 i = 0 262 for key, gpm_track in gpm_library['tracks'].items(): 263 i += 1 264 # if i < 6900: continue 265 print(i, " ", key) 266 track_md = gpm_track['track_md'] 267 # tqdm.write(track_md['date_added']) 268 269 date_added = gpm_timestamp_to_date(track_md['date_added']) 270 date = date_added.strftime('%m:%d:%y') # mm:dd:yy 271 time = date_added.strftime('%H:%M:%S') # hh:mm:ss 272 subprocess.call( 273 ('echo "%s" | sudo -S systemsetup -setdate %s -settime %s' 274 % (sudo_password, date, time)), shell=True, stdout=FNULL, stderr=subprocess.STDOUT 275 ) 276 277 # add to itunes and update metadata 278 gpm_track_md = gpm_track['track_md'] 279 if gpm_track['already_exists'] == True: # track already exists in itunes 280 itunes_track = gpm_track['itunes_track'] 281 282 itunes_track_location = getattr(itunes_track, 'location').get() 283 if itunes_track_location == appscript.k.missing_value and itunes_media_folder: 284 import shutil, ntpath 285 filename = ntpath.basename(track_md['file_path']) 286 new_itunes_track_location = os.path.join(options['itunes_media_folder'], filename) 287 shutil.copyfile(track_md['file_path'], new_itunes_track_location) 288 new_posix_path = get_posix_path(new_itunes_track_location) 289 getattr(itunes_track, 'location').set(new_posix_path) 290 291 itunes_played_count = getattr(itunes_track, 'played_count').get() 292 gpm_played_count = gpm_track_md['played_count'] 293 new_played_count = itunes_played_count + gpm_played_count 294 getattr(itunes_track, 'played_count').set(new_played_count) 295 296 itunes_played_date = getattr(itunes_track, 'played_date').get() 297 gpm_played_date = gpm_timestamp_to_date(gpm_track_md['played_date']) 298 if itunes_played_date != appscript.k.missing_value and itunes_played_date > gpm_played_date: 299 new_played_date = itunes_played_date 300 else: 301 new_played_date = gpm_played_date 302 getattr(itunes_track, 'played_date').set(new_played_date) 303 304 getattr(itunes_track, 'year').set(gpm_track_md['year']) 305 getattr(itunes_track, 'album_artist').set(gpm_track_md['album_artist']) 306 getattr(itunes_track, 'composer').set(gpm_track_md['composer']) 307 if gpm_track["comment"] != "": 308 getattr(itunes_track, 'comment').set(gpm_track_md['comment']) 309 getattr(itunes_track, 'track_number').set(gpm_track_md['track_number']) 310 getattr(itunes_track, 'track_count').set(gpm_track_md['track_count']) 311 getattr(itunes_track, 'genre').set(gpm_track_md['genre']) 312 getattr(itunes_track, 'disc_number').set(gpm_track_md['disc_number']) 313 getattr(itunes_track, 'disc_count').set(gpm_track_md['disc_count']) 314 315 if gpm_track['md_map']: 316 getattr(itunes_track, 'name').set(gpm_track['title']) 317 getattr(itunes_track, 'artist').set(gpm_track['artist']) 318 getattr(itunes_track, 'album').set(gpm_track['album']) 319 else: # track doesn't exist in itunes 320 posix_path = get_posix_path(track_md['file_path']) 321 gpm_track['itunes_track'] = iTunes.add(posix_path) 322 itunes_track = gpm_track['itunes_track'] 323 324 getattr(itunes_track, 'played_count').set(gpm_track_md['played_count']) 325 getattr(itunes_track, 'played_date').set(gpm_timestamp_to_date(gpm_track_md['played_date'])) 326 327 gpm_loved = gpm_track_md['loved'] 328 gpm_disliked = gpm_track_md['disliked'] 329 if (gpm_loved): 330 itunes_track.duplicate(to=thumbs_up_playlist) 331 elif (gpm_disliked): 332 itunes_track.duplicate(to=thumbs_down_playlist) 333 334 # set network time to what it was before 335 subprocess.call( 336 ('echo %s | sudo -S systemsetup -setusingnetworktime %s' 337 % (sudo_password, original_usingnetworktime)).split() 338 ) 339 340 # add playlists to itunes 341 if only_scan == False: 342 print('Adding playlists to iTunes...') 343 for gpm_playlist in gpm_library['playlists']: 344 # make playlist 345 if 'description' in gpm_playlist: description = gpm_playlist['description'] 346 else: description = '' 347 itunes_playlist = iTunes.make( 348 new=appscript.k.user_playlist, 349 with_properties={ 350 appscript.k.name: gpm_playlist['name'], 351 appscript.k.description: description, 352 } 353 ) 354 itunes_playlist.move(to=itunes_folder_playlist) 355 # add songs to playlist 356 for gpm_playlist_track in gpm_playlist['tracks']: 357 key = gpm_library['track_keys'][gpm_playlist_track['trackId']] 358 gpm_library['tracks'][key]['itunes_track'].duplicate(to=itunes_playlist) 359 if only_scan == False: 360 print('All done. Now make sure your clock is correct.') 361 362if action == 'login': 363 364 init_gpm_api(new_login=True) 365 366elif action == 'fetch': 367 368 gpm_library = download_library() 369 save_library(gpm_library, 'user_files/library_downloaded.json') 370 print(" Track count:", len(gpm_library["tracks"])) 371 print(" Playlist count:", len(gpm_library["playlists"])) 372 373 gpm_library = restructure_library(gpm_library) 374 save_library(gpm_library, 'user_files/library_restructured.json') 375 376elif action == 'match_files': 377 378 gpm_library = load_library('user_files/library_restructured.json') 379 print('Matching GPM library with song files...') 380 gpm_library = match_files(gpm_library) 381 save_library(gpm_library, 'user_files/library_matched_files.json') 382 383elif action == 'scan_itunes': 384 385 gpm_library = load_library('user_files/library_matched_files.json') 386 add_to_itunes(gpm_library, only_scan=True) 387 388elif action == 'add_to_itunes': 389 390 gpm_library = load_library('user_files/library_matched_files.json') 391 add_to_itunes(gpm_library, only_scan=False) 392 393else: 394 395 sys.exit('Unknown action "'+action+'"') 396 397# loop through audio files 398# -get the audio file's artist/title 399# -loop through json library for a match, append the file path to the json library track 400# -or maybe use the python get() thing to look for the right artist/title match 401# add iTunes metadata (plays, loved/disliked, date added/played/modified) 402# loop through json library 403# set clock 404# add track to iTunes