[READ-ONLY] Mirror of https://github.com/probablykasper/gpm-to-itunes. Imports Google Play Music library to iTunes
cli
google-play-music
itunes
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