def main(): cli = dict((key.lstrip("-<").rstrip(">"), value) for key, value in docopt(__doc__).items()) print_ = safe_print if not cli['quiet'] else lambda *args, **kwargs: None mcw = MobileClientWrapper() mcw.login(cli['user'], cli['pass']) print_("Scanning for songs...\n") search_songs = mcw.get_google_songs(filters=cli['filter'], filter_all=cli['all']) search_songs.sort(key=lambda song: (song['artist'], song['album'], song['trackNumber'])) if search_songs: if cli['yes'] or raw_input("Display results? (y/n) ").lower() == "y": print_() for song in search_songs: title = song.get('title', "<empty>") artist = song.get('artist', "<empty>") album = song.get('album', "<empty>") print("{0} _by_ {1} _from_ {2} ({3})".format(title, artist, album, song['id'])) print_() else: safe_print("\nNo songs found matching query") mcw.logout() print_("\nAll done!")
def main(): cli = dict((key.lstrip("-<").rstrip(">"), value) for key, value in docopt(__doc__).items()) print_ = safe_print if not cli['quiet'] else lambda *args, **kwargs: None mcw = MobileClientWrapper() mcw.login(cli['user'], cli['pass']) print_("Scanning for songs...\n") search_songs = mcw.get_google_songs(filters=cli['filter'], filter_all=cli['all']) search_songs.sort( key=lambda song: (song['artist'], song['album'], song['trackNumber'])) if search_songs: if cli['yes'] or raw_input("Display results? (y/n) ").lower() == "y": print_() for song in search_songs: title = song.get('title', "<empty>") artist = song.get('artist', "<empty>") album = song.get('album', "<empty>") print("{0} _by_ {1} _from_ {2} ({3})".format( title, artist, album, song['id'])) print_() else: safe_print("\nNo songs found matching query") mcw.logout() print_("\nAll done!")
def unsubscribe_from_playlists(self, playlist_ids: list = [], contains: list = []): removed_subscriptions = False for playlist_id in playlist_ids: if playlist_id not in self.subscribed_playlists.keys(): raise Exception( "Cannot unsubscribe from playlist with id {}, because the user is not subscripted to it." .format(playlist_id)) playlist = self.subscribed_playlists[playlist_id] removed_subscriptions = True safe_print("Unsubscribed from playlist {} by {}".format( playlist.name, playlist.owner_id)) del self.subscribed_playlists[playlist_id] # Lowercase pattern matching with the playlist name subscribed_playlists = list(self.subscribed_playlists.values()) for pattern in contains: pattern = pattern.lower() for playlist in subscribed_playlists: if pattern in playlist.name.lower(): removed_subscriptions = True safe_print("Unsubscribed from playlist {} by {}".format( playlist.name, playlist.owner_id)) del self.subscribed_playlists[playlist.id] # Only save if we actually changed something. TODO: Save these in a separate file. if removed_subscriptions: self._save()
def main(): cli = dict((key.lstrip("-<").rstrip(">"), value) for key, value in docopt(__doc__).items()) print_ = safe_print if not cli['quiet'] else lambda *args, **kwargs: None if not cli['output']: cli['output'] = os.getcwd() mmw = MusicManagerWrapper(log=cli['log']) mmw.login(oauth_file=cli['cred'], uploader_id=cli['uploader-id']) download_songs = mmw.get_google_songs(filters=cli['filter'], filter_all=cli['all']) download_songs.sort(key=lambda song: (song['artist'], song['album'], song['track_number'])) if cli['dry-run']: print_("Found {0} songs to download".format(len(download_songs))) if download_songs: safe_print("\nSongs to download:\n") for song in download_songs: safe_print("{0} by {1}".format(song['title'], song['artist'])) else: safe_print("\nNo songs to download") else: if download_songs: print_("Downloading {0} songs from Google Music\n".format(len(download_songs))) mmw.download(download_songs, cli['output']) else: safe_print("\nNo songs to download") mmw.logout() print_("\nAll done!")
def __init__( self, spotify: Spotify, user_id: str, name: str = default_name, description: str = default_description, ): safe_print( "Creating playlist with name {} and the following description:\n\t'{}'" .format(name, description)) feed = spotify.user_playlist_create(user_id, name=name, public=False, description=description) self.id = feed["id"] self.name = feed["name"] self.last_update = datetime.utcnow()
def main(): cli = dict((key.lstrip("-<").rstrip(">"), value) for key, value in docopt(__doc__).items()) print_ = safe_print if not cli['quiet'] else lambda *args, **kwargs: None mcw = MobileClientWrapper() mcw.login(cli['user'], cli['pass']) delete_songs = mcw.get_google_songs(filters=cli['filter'], filter_all=cli['all']) if cli['dry-run']: print_("Found {0} songs to delete".format(len(delete_songs))) if delete_songs: safe_print("\nSongs to delete:\n") for song in delete_songs: safe_print("{0} by {1}".format(song['title'], song['artist'])) else: safe_print("\nNo songs to delete") else: if delete_songs: if cli['yes'] or raw_input( "Are you sure you want to delete {0} songs from Google Music? (y/n) " .format(len(delete_songs))).lower() == "y": print_("\nDeleting {0} songs from Google Music\n".format( len(delete_songs))) songnum = 0 total = len(delete_songs) pad = len(str(total)) for song in delete_songs: mcw.api.delete_songs(song['id']) songnum += 1 print_( "Deleted {num:>{pad}}/{total} songs from Google Music". format(num=songnum, pad=pad, total=total), end="\r") sys.stdout.flush() print_() else: print_("No songs deleted.") else: safe_print("\nNo songs to delete") mcw.logout() print_("\nAll done!")
def subscribe_to_playlists(self, playlist_ids: list = [], contains: list = []): new_subscriptions = False for playlist_id in playlist_ids: if playlist_id not in self.followed_playlists.keys(): raise Exception( "Cannot subscribe to playlist with id {}. Either the user owns it or does not follow it." .format(playlist_id)) if playlist_id not in self.subscribed_playlists.keys(): playlist = self.followed_playlists[playlist_id] tracks = self._get_playlist_tracks(playlist["owner"]["id"], playlist_id) self.subscribed_playlists[playlist_id] = SubscribedPlaylist( playlist, tracks) new_subscriptions = True safe_print("Subscribed to playlist {} by {}".format( playlist["name"], playlist["owner"]["id"])) # Lowercase pattern matching with the playlist name for pattern in contains: pattern = pattern.lower() for playlist in self.followed_playlists.values(): if (pattern in playlist["name"].lower() and playlist["id"] not in self.subscribed_playlists.keys()): tracks = self._get_playlist_tracks(playlist["owner"]["id"], playlist["id"]) self.subscribed_playlists[ playlist["id"]] = SubscribedPlaylist(playlist, tracks) new_subscriptions = True safe_print("Subscribed to playlist {} by {}".format( playlist["name"], playlist["owner"]["id"])) # Only save if we actually changed something. TODO: Save these in a separate file. if new_subscriptions: self._save()
def print_feed_log(self): if not os.path.exists(self._feed_log_path): print("No feed log exists yet!") return feed_log = pickle.load(open(self._feed_log_path, "rb")) num_tracks = len(feed_log["track_ids"]) print("Found {} tracks in log.".format(num_tracks)) batch_size = 50 tracks = [] start_idx = 0 print("Requesting track info...") for start_idx in tqdm(range(0, num_tracks, batch_size)): end_idx = start_idx + batch_size track_ids = feed_log["track_ids"][start_idx:end_idx] tracks += self.sp.tracks(track_ids)["tracks"] start_idx = end_idx for track, timestamp in zip(tracks, feed_log["timestamps"]): safe_print("{} - {} - {} - {}".format(track["artists"][0]["name"], track["name"], timestamp, track["id"]))
def messageWriter(): time.sleep(random.random() * 10.0) output = [] while 1: # Collect all available log messages. while 1: try: message = messageQ.get(False) except Queue.Empty: break output.append(message) messageQ.task_done() # Write all log messages in one call to minimize IO. if output: try: with open(logFileName, 'a') as fp: fp.write(''.join(output)) output = [] except Exception as e: safe_print(e) # Sleep a random duration to minimize file write collisions between multiple RaceDB instances. time.sleep(60.0 + random.random() * 60.0 * 4.0)
def main(): cli = dict((key.lstrip("-<").rstrip(">"), value) for key, value in docopt(__doc__).items()) print_ = safe_print if not cli['quiet'] else lambda *args, **kwargs: None mcw = MobileClientWrapper() mcw.login(cli['user'], cli['pass']) delete_songs = mcw.get_google_songs(filters=cli['filter'], filter_all=cli['all']) if cli['dry-run']: print_("Found {0} songs to delete".format(len(delete_songs))) if delete_songs: safe_print("\nSongs to delete:\n") for song in delete_songs: safe_print("{0} by {1}".format(song['title'], song['artist'])) else: safe_print("\nNo songs to delete") else: if delete_songs: if cli['yes'] or raw_input("Are you sure you want to delete {0} songs from Google Music? (y/n) ".format(len(delete_songs))).lower() == "y": print_("\nDeleting {0} songs from Google Music\n".format(len(delete_songs))) songnum = 0 total = len(delete_songs) pad = len(str(total)) for song in delete_songs: mcw.api.delete_songs(song['id']) songnum += 1 print_("Deleted {num:>{pad}}/{total} songs from Google Music".format(num=songnum, pad=pad, total=total), end="\r") sys.stdout.flush() print_() else: print_("No songs deleted.") else: safe_print("\nNo songs to delete") mcw.logout() print_("\nAll done!")
def main(): cli = dict((key.lstrip("-<").rstrip(">"), value) for key, value in docopt(__doc__).items()) print_ = safe_print if not cli['quiet'] else lambda *args, **kwargs: None if not cli['output']: cli['output'] = os.getcwd() mmw = MusicManagerWrapper(log=cli['log']) mmw.login(oauth_file=cli['cred'], uploader_id=cli['uploader-id']) download_songs = mmw.get_google_songs(filters=cli['filter'], filter_all=cli['all']) download_songs.sort( key=lambda song: (song['artist'], song['album'], song['track_number'])) if cli['dry-run']: print_("Found {0} songs to download".format(len(download_songs))) if download_songs: safe_print("\nSongs to download:\n") for song in download_songs: safe_print("{0} by {1}".format(song['title'], song['artist'])) else: safe_print("\nNo songs to download") else: if download_songs: print_("Downloading {0} songs from Google Music\n".format( len(download_songs))) mmw.download(download_songs, cli['output']) else: safe_print("\nNo songs to download") mmw.logout() print_("\nAll done!")
def main(): cli = dict((key.lstrip("-<").rstrip(">"), value) for key, value in docopt(__doc__).items()) print_ = safe_print if not cli['quiet'] else lambda *args, **kwargs: None if not cli['input']: cli['input'] = [os.getcwd()] mmw = MusicManagerWrapper(log=cli['log']) mmw.login(oauth_file=cli['cred'], uploader_id=cli['uploader-id']) excludes = "|".join(pattern.decode('utf8') for pattern in cli['exclude']) if cli['exclude'] else None upload_songs, exclude_songs = mmw.get_local_songs(cli['input'], exclude_patterns=excludes, filters=cli['filter'], filter_all=cli['all']) upload_songs.sort() exclude_songs.sort() if cli['dry-run']: print_("Found {0} songs to upload".format(len(upload_songs))) if upload_songs: safe_print("\nSongs to upload:\n") for song in upload_songs: safe_print(song) else: safe_print("\nNo songs to upload") if exclude_songs: safe_print("\nSongs to exclude:\n") for song in exclude_songs: safe_print(song) else: safe_print("\nNo songs to exclude") else: if upload_songs: print_("Uploading {0} songs to Google Music\n".format(len(upload_songs))) mmw.upload(upload_songs, enable_matching=cli['match']) else: safe_print("\nNo songs to upload") mmw.logout() print("\nAll done!")
def main(): cli = dict((key.lstrip("-<").rstrip(">"), value) for key, value in docopt(__doc__).items()) print_ = safe_print if not cli['quiet'] else lambda *args, **kwargs: None if not cli['input']: cli['input'] = [os.getcwd()] if not cli['output']: cli['output'] = os.getcwd() # Pre-compile regex for exclude option. excludes = "|".join( pattern.decode('utf8') for pattern in cli['exclude']) if cli['exclude'] else None mmw = MusicManagerWrapper(log=cli['log'], quiet=cli['quiet']) mmw.login(oauth_file=cli['cred'], uploader_id=cli['uploader-id']) if cli['down']: google_songs = mmw.get_google_songs(filters=cli['filter'], filter_all=cli['all']) cli['input'] = template_to_base_path(google_songs, cli['output']) local_songs, exclude_songs = mmw.get_local_songs( cli['input'], exclude_patterns=excludes) print_("Scanning for missing songs...") download_songs = compare_song_collections(google_songs, local_songs) download_songs.sort(key=lambda song: (song['artist'], song['album'], song['track_number'])) if cli['dry-run']: print_("Found {0} songs to download".format(len(download_songs))) if download_songs: safe_print("\nSongs to download:\n") for song in download_songs: safe_print("{0} by {1}".format(song['title'], song['artist'])) else: safe_print("\nNo songs to download") else: if download_songs: print_("Downloading {0} songs from Google Music\n".format( len(download_songs))) mmw.download(download_songs, cli['output']) else: safe_print("\nNo songs to download") else: google_songs = mmw.get_google_songs() local_songs, exclude_songs = mmw.get_local_songs( cli['input'], exclude_patterns=excludes, filters=cli['filter'], filter_all=cli['all']) print_("Scanning for missing songs...") upload_songs = compare_song_collections(local_songs, google_songs) # Sort lists for sensible output. upload_songs.sort() exclude_songs.sort() if cli['dry-run']: print_("Found {0} songs to upload".format(len(upload_songs))) if upload_songs: safe_print("\nSongs to upload:\n") for song in upload_songs: safe_print(song) else: safe_print("\nNo songs to upload") if exclude_songs: safe_print("\nSongs to exclude:\n") for song in exclude_songs: safe_print(song) else: safe_print("\nNo songs to exclude") else: if upload_songs: print_("Uploading {0} songs to Google Music\n".format( len(upload_songs))) mmw.upload(upload_songs, enable_matching=cli['match']) else: safe_print("\nNo songs to upload") mmw.logout() print_("\nAll done!")
def _eval_python(loop, context, params=None, add_boilerplate=False, namespace=None): """Convert the given loop to Python and emulate the loop directly in Python. @param loop (VBA_Object object) The loop for which to generate Python JIT code. @param context (Context object) The current program state. @param params (list) Any VB params used by the given loop. @param add_boilerplate (boolean) If True add setup boilerplate code (imports, etc.) to the start of the generated Python JIT code. Don't add boilerplate if False. @param namespace (dict) The Python namespace in which to evaluate the generated Python JIT code. If None the locals() namespace will be used. """ params = params # pylint # Are we actually doing this? if (not context.do_jit): return False # Emulating full VB programs in Python is difficult, so for now skip loops # that Execute() dynamic VB. full_code_vba = safe_str_convert(loop).replace("\n", "\\n") code_vba = full_code_vba[:20] code_vba_lower = full_code_vba.lower() if (not context.throttle_logging): log.info("Starting JIT emulation of '" + code_vba + "...' ...") if (("Execute(".lower() in code_vba_lower) or ("ExecuteGlobal(".lower() in code_vba_lower) or ("Eval(".lower() in code_vba_lower)): log.warning("Loop Execute()s dynamic code. Not JIT emulating.") return False if (".Item(".lower() in code_vba_lower): log.warning("Loop references forms with .Item(). Not JIT emulating.") return False # Generate the Python code for the VB code and execute the generated Python code. # TODO: Remove dangerous functions from what can be exec'ed. code_python = "" try: # For JIT handling we modify the values of certain variables to # handle recursive python code generation, so make a copy of the # original context. tmp_context = Context(context=context, _locals=context.locals, copy_globals=True) # Get the Python code for the loop. if (not context.throttle_logging): log.info("Generating Python JIT code...") code_python = to_python(loop, tmp_context) if add_boilerplate: var_inits, _ = _loop_vars_to_python(loop, tmp_context, 0) func_defns = _called_funcs_to_python(loop, tmp_context, 0) code_python = _boilerplate_to_python(0) + "\n" + \ func_defns + "\n" + \ var_inits + "\n" + \ code_python + "\n" + \ _check_for_iocs(loop, tmp_context, 0) + "\n" + \ _updated_vars_to_python(loop, tmp_context, 0) if (log.getEffectiveLevel() == logging.DEBUG): safe_print("JIT CODE!!") safe_print(code_python) #print "REMOVE THIS!!!" #sys.exit(0) if (not context.throttle_logging): log.info("Done generating Python JIT code.") # Extended ASCII strings are handled differently in VBScript and VBA. # Punt if we are emulating VBA and we have what appears to be extended ASCII # strings. For performance we are not handling the MS VBA extended ASCII in the python # JIT code. if (not context.is_vbscript): # Look for non-ASCII strings. non_ascii_pat = r'"[^"]*[\x7f-\xff][^"]*"' non_ascii_pat1 = r'"[^"]*(?:\\x7f|\\x[89a-f][0-9a-f])[^"]*"' if ((re.search(non_ascii_pat1, code_python) is not None) or (re.search(non_ascii_pat, code_python) is not None)): log.warning("VBA code contains Microsoft specific extended ASCII strings. Not JIT emulating.") return False # Check for dynamic code execution in called functions. if (('"Execute", ' in code_python) or ('"ExecuteGlobal", ' in code_python) or ('"Eval", ' in code_python)): log.warning("Functions called by loop Execute() dynamic code. Not JIT emulating.") return False # Run the Python code. # Have we already run this exact loop? if (code_python in jit_cache): var_updates = jit_cache[code_python] if (not context.throttle_logging): log.info("Using cached JIT loop results.") if (var_updates == "ERROR"): log.error("Previous run of Python JIT loop emulation failed. Using fallback emulation for loop.") return False # No cached results. Run the loop. elif (namespace is None): # JIT code execution goes not involve emulating VB GOTOs. context.goto_executed = False # Magic. For some reason exec'ing in locals() makes the dynamically generated # code recognize functions defined in the dynamic code. I don't know why. if (not context.throttle_logging): log.info("Evaluating Python JIT code...") exec code_python in locals() else: # JIT code execution goes not involve emulating VB GOTOs. context.goto_executed = False # Run the JIT code in the given namespace. exec(code_python, namespace) var_updates = namespace["var_updates"] if (not context.throttle_logging): log.info("Done JIT emulation of '" + code_vba + "...' .") # Cache the loop results. jit_cache[code_python] = var_updates # Update the context with the variable values from the JIT code execution. try: for updated_var in var_updates.keys(): if (updated_var == "__shell_code__"): continue context.set(updated_var, var_updates[updated_var]) except (NameError, UnboundLocalError): log.warning("No variables set by Python JIT code.") # Update shellcode bytes from the JIT emulation. import vba_context vba_context.shellcode = var_updates["__shell_code__"] except NotImplementedError as e: log.error("Python JIT emulation of loop failed. " + safe_str_convert(e) + ". Using fallback emulation method for loop...") #safe_print("REMOVE THIS!!") #raise e return False except Exception as e: # Cache the error. jit_cache[code_python] = "ERROR" # If we bombed out due to a potential infinite loop we # are done. if ("Infinite Loop" in safe_str_convert(e)): log.warning("Detected infinite loop. Terminating loop.") return True # We had some other error. Emulating the loop in Python failed. log.error("Python JIT emulation of loop failed. " + safe_str_convert(e) + ". Using fallback emulation method for loop...") if (log.getEffectiveLevel() == logging.DEBUG): traceback.print_exc(file=sys.stdout) safe_print("-*-*-*-*-\n" + code_python + "\n-*-*-*-*-") return False # Done. return True
def main(): cli = dict((key.lstrip("-<").rstrip(">"), value) for key, value in docopt(__doc__).items()) print_ = safe_print if not cli['quiet'] else lambda *args, **kwargs: None if not cli['input']: cli['input'] = [os.getcwd()] if not cli['output']: cli['output'] = os.getcwd() # Pre-compile regex for exclude option. excludes = "|".join(pattern.decode('utf8') for pattern in cli['exclude']) if cli['exclude'] else None mmw = MusicManagerWrapper(log=cli['log'], quiet=cli['quiet']) mmw.login(oauth_file=cli['cred'], uploader_id=cli['uploader-id']) if cli['down']: google_songs = mmw.get_google_songs(filters=cli['filter'], filter_all=cli['all']) cli['input'] = template_to_base_path(google_songs, cli['output']) local_songs, exclude_songs = mmw.get_local_songs(cli['input'], exclude_patterns=excludes) print_("Scanning for missing songs...") download_songs = compare_song_collections(google_songs, local_songs) download_songs.sort(key=lambda song: (song['artist'], song['album'], song['track_number'])) if cli['dry-run']: print_("Found {0} songs to download".format(len(download_songs))) if download_songs: safe_print("\nSongs to download:\n") for song in download_songs: safe_print("{0} by {1}".format(song['title'], song['artist'])) else: safe_print("\nNo songs to download") else: if download_songs: print_("Downloading {0} songs from Google Music\n".format(len(download_songs))) mmw.download(download_songs, cli['output']) else: safe_print("\nNo songs to download") else: google_songs = mmw.get_google_songs() local_songs, exclude_songs = mmw.get_local_songs(cli['input'], exclude_patterns=excludes, filters=cli['filter'], filter_all=cli['all']) print_("Scanning for missing songs...") upload_songs = compare_song_collections(local_songs, google_songs) # Sort lists for sensible output. upload_songs.sort() exclude_songs.sort() if cli['dry-run']: print_("Found {0} songs to upload".format(len(upload_songs))) if upload_songs: safe_print("\nSongs to upload:\n") for song in upload_songs: safe_print(song) else: safe_print("\nNo songs to upload") if exclude_songs: safe_print("\nSongs to exclude:\n") for song in exclude_songs: safe_print(song) else: safe_print("\nNo songs to exclude") else: if upload_songs: print_("Uploading {0} songs to Google Music\n".format(len(upload_songs))) mmw.upload(upload_songs, enable_matching=cli['match']) else: safe_print("\nNo songs to upload") mmw.logout() print_("\nAll done!")
def main(): cli = dict((key.lstrip("-<").rstrip(">"), value) for key, value in docopt(__doc__).items()) print_ = safe_print if not cli['quiet'] else lambda *args, **kwargs: None if not cli['input']: cli['input'] = [os.getcwd()] mmw = MusicManagerWrapper(log=cli['log']) mmw.login(oauth_file=cli['cred'], uploader_id=cli['uploader-id']) excludes = "|".join( pattern.decode('utf8') for pattern in cli['exclude']) if cli['exclude'] else None upload_songs, exclude_songs = mmw.get_local_songs( cli['input'], exclude_patterns=excludes, filters=cli['filter'], filter_all=cli['all']) upload_songs.sort() exclude_songs.sort() if cli['dry-run']: print_("Found {0} songs to upload".format(len(upload_songs))) if upload_songs: safe_print("\nSongs to upload:\n") for song in upload_songs: safe_print(song) else: safe_print("\nNo songs to upload") if exclude_songs: safe_print("\nSongs to exclude:\n") for song in exclude_songs: safe_print(song) else: safe_print("\nNo songs to exclude") else: if upload_songs: print_("Uploading {0} songs to Google Music\n".format( len(upload_songs))) mmw.upload(upload_songs, enable_matching=cli['match']) else: safe_print("\nNo songs to upload") mmw.logout() print("\nAll done!")
def print_playlists(self, own=False, follow=False, subscribed=True): if own: safe_print("Own playlists:") for playlist in self.user_playlists.values(): safe_print(playlist["name"]) if follow: safe_print("\nFollowed playlists:") for playlist in self.followed_playlists.values(): safe_print(playlist["name"], playlist["owner"]["id"]) if subscribed: safe_print("\nCurrently subscribed to the following playlists:") for playlist in self.subscribed_playlists.values(): safe_print(playlist) safe_print()
def update_feed(self, add_own=False): """ Add_own denotes whether to add songs that the user added to a playlist themselves. This may happen for example in collaborative playlists. """ last_update = self.subscription_feed.last_update track_ids = [] num_added_tracks = 0 for playlist_id, playlist in self.subscribed_playlists.items(): # safe_print("Checking playlist {}".format(playlist.name)) new_tracks, snapshot = self._get_playlist_tracks( playlist.owner_id, playlist_id, min_timestamp=last_update, compare_snapshot=playlist.snapshot_id, return_snapshot=True, ) # Update the playlist snapshot so that we quickly know if it has changed next time playlist.snapshot_id = snapshot added = 0 for track in new_tracks: if track.id == None: print(f"Track with id None: {track}") if add_own or track.added_by != self.user_id: try: # Only add the track if it wasn't already in the list when we subbed if track.id not in playlist.track_ids.keys(): track_ids.append(track.id) # Add the ID to the track ID list so we know not to add it in the future playlist.track_ids[track.id] = datetime.utcnow() added += 1 # TODO: correctly upgrade objects if storage consists of SubscribedPlaylists without ID list. except AttributeError: track_ids.append(track.id) added += 1 if added > 0: safe_print("Obtained {} new tracks from playlist {}!".format( added, playlist.name)) if len(track_ids) > 0: unique_ids = np.unique(track_ids) # If a feed log exists, filter all track IDs that have already been added to the feed before. if os.path.exists(self._feed_log_path): feed_log = pickle.load(open(self._feed_log_path, "rb")) filtered_indices = np.where( ~np.isin(unique_ids, feed_log["track_ids"])) unique_ids = unique_ids[filtered_indices] # We can add at most 100 tracks to a playlist in a single request. if unique_ids.size <= 100: self.sp.user_playlist_add_tracks(self.user_id, self.subscription_feed.id, unique_ids) else: # Split array into near-equal sections that are smaller than 100 tracks for id_array in np.array_split( unique_ids, np.ceil(unique_ids.size / 100).astype(int)): self.sp.user_playlist_add_tracks(self.user_id, self.subscription_feed.id, id_array) num_added_tracks = unique_ids.size self._log_feed_updates(unique_ids) # Update the timestamp and save to file self.subscription_feed.last_update = datetime.utcnow() self._save() return num_added_tracks