Newer
Older

Robert Goldmann
committed
import json

Robert Goldmann
committed
import random

Robert Goldmann
committed
from typing import Dict, List

Robert Goldmann
committed
import click

Robert Goldmann
committed
import spotipy
from TheCodeLabs_BaseUtils.DefaultLogger import DefaultLogger
from spotipy import CacheFileHandler
from spotipy.oauth2 import SpotifyOAuth

Robert Goldmann
committed
LOG_FORMAT = '[%(levelname)-7s] - %(asctime)s - %(message)s'
LOGGER = DefaultLogger().create_logger_if_not_exists('SpotifyAutoPlaylistCreator', logFormat=LOG_FORMAT)

Robert Goldmann
committed
MODE_LATEST = 'LATEST'
MODE_RANDOM = 'RANDOM'

Robert Goldmann
committed
class SpotifyAutoPlaylistCreator:
def __init__(self, clientID: str,
clientSecret: str,
playlistInfo: List[Dict[str, str]],
destinationPlaylistInfo: Dict[str, str],
numberOfTracks: int,

Robert Goldmann
committed
mode: str,
openBrowser: bool,
cacheFilePath: str):

Robert Goldmann
committed
self._clientID = clientID
self._clientSecret = clientSecret
self._playlistInfo = playlistInfo
self._destinationPlaylistInfo = destinationPlaylistInfo
self._numberOfTracks = numberOfTracks

Robert Goldmann
committed
self._mode = mode
self._openBrowser = openBrowser
self._cacheFilePath = cacheFilePath

Robert Goldmann
committed
self._spotify = self.login()
def login(self) -> spotipy.Spotify:
client_credentials_manager = SpotifyOAuth(client_id=self._clientID,
client_secret=self._clientSecret,
redirect_uri=self._redirectUrl,
scope='playlist-modify-private,playlist-modify-public',
open_browser=self._openBrowser,
cache_handler=CacheFileHandler(cache_path=self._cacheFilePath))

Robert Goldmann
committed
return spotipy.Spotify(client_credentials_manager=client_credentials_manager)
def run(self):
destinationPlaylist = self.__get_playlist(self._destinationPlaylistInfo['user'],
self._destinationPlaylistInfo['id'])
self.__CleanupDestinationPlaylist(destinationPlaylist)
LOGGER.info(f'>>> Fetching tracks for all source playlists...')
allTracks = []

Robert Goldmann
committed
for playlistInfo in self._playlistInfo:

Robert Goldmann
committed
playlist = self.__get_playlist(playlistInfo['user'], playlistInfo['id'])
allTracks.extend(self.__get_tracks(playlist))

Robert Goldmann
committed
LOGGER.info(f'>>> Run mode {self._mode}...')
if self._mode == MODE_LATEST:
self.__RunModeLatestTracks(allTracks, destinationPlaylist)
elif self._mode == MODE_RANDOM:
self.__RunModeRandom(allTracks, destinationPlaylist)
else:
raise RuntimeError(f'Unknown mode {self._mode}')
def __RunModeLatestTracks(self, allTracks: List, destinationPlaylist) -> None:

Robert Goldmann
committed
sortedTracks = sorted(allTracks, key=lambda d: d['added_at'], reverse=True)
LOGGER.info(f'>>> Collecting latest tracks (limit: {self._numberOfTracks})...')
latestTrackUris = self.__CollectLatestTracks(sortedTracks)
LOGGER.info(f'>>> Found {len(latestTrackUris)} latest tracks (limit: {self._numberOfTracks})')
LOGGER.info(f'>>> Adding tracks to destination playlist "{destinationPlaylist["name"]}"...')
self._spotify.playlist_add_items(self._destinationPlaylistInfo['id'], latestTrackUris)

Robert Goldmann
committed
def __RunModeRandom(self, allTracks: List, destinationPlaylist) -> None:
randomTracks = []
filteredTracks = [t for t in allTracks if not t['is_local']]
while len(randomTracks) < self._numberOfTracks:
numberOfExercisesToGenerate = self._numberOfTracks - len(randomTracks)
randomTracks.extend(random.sample(filteredTracks, min(len(filteredTracks), numberOfExercisesToGenerate)))
LOGGER.info(f'>>> Found {len(randomTracks)} random tracks (limit: {self._numberOfTracks})')
LOGGER.info(f'>>> Adding tracks to destination playlist "{destinationPlaylist["name"]}"...')
randomTrackUris = self.__extract_track_uris(randomTracks)

Robert Goldmann
committed
for batch in self.__chunk_list(randomTrackUris, 100):
self._spotify.playlist_add_items(self._destinationPlaylistInfo['id'], batch)
@staticmethod
def __chunk_list(items, batchSize):
for i in range(0, len(items), batchSize):
yield items[i:i + batchSize]

Robert Goldmann
committed

Robert Goldmann
committed
def __CollectLatestTracks(self, sortedTracks):
tracksToAdd = []
for track in sortedTracks:
if len(tracksToAdd) >= self._numberOfTracks:
break
if track['is_local']:
# It's not possible to add a local track to a playlist using the web API.
# https://github.com/plamere/spotipy/issues/793#issuecomment-1082421408

Robert Goldmann
committed
LOGGER.info(f'Skipping local track "{track["track"]["name"]}"')
continue
LOGGER.info(f'Appending track "{track["track"]["name"]}"')
tracksToAdd.append(track)
return self.__extract_track_uris(tracksToAdd)
def __CleanupDestinationPlaylist(self, destinationPlaylist):
existingTracks = self.__get_tracks(destinationPlaylist)
LOGGER.info(f'>>> Removing {len(existingTracks)} tracks from destination '
f'playlist "{destinationPlaylist["name"]}"...')
existingTracksUris = self.__extract_track_uris(existingTracks)

Robert Goldmann
committed
if existingTracksUris:
self._spotify.playlist_remove_all_occurrences_of_items(self._destinationPlaylistInfo['id'], existingTracksUris)

Robert Goldmann
committed
def __get_playlist(self, username: str, playlistID: str) -> Dict:
LOGGER.info(f'>>> Fetching playlist with ID: {playlistID} by {username}...')
identifier = f'spotify:user:{username}:playlist:{playlistID}'
playlist = self._spotify.playlist(identifier)
LOGGER.info(f'Found playlist "{playlist["name"]}"')
return playlist
def __get_tracks(self, playlist) -> List[Dict]:
tracks = playlist['tracks']
results = tracks['items']
while tracks['next']:
tracks = self._spotify.next(tracks)
results.extend(tracks['items'])
LOGGER.info(f'Fetched {len(results)} tracks')
return results
def __extract_track_uris(self, tracks: List) -> List[str]:
return [track['track']['uri'] for track in tracks]

Robert Goldmann
committed
@click.command()
@click.option('--settings-path', '-s', 'settingsPath', help='Path to the settings file.', required=True)
def start(settingsPath) -> None:
with open(settingsPath, 'r', encoding='utf-8') as f:

Robert Goldmann
committed
SETTINGS = json.load(f)
spotifyBackup = SpotifyAutoPlaylistCreator(SETTINGS['spotifyAPI']['clientID'],
SETTINGS['spotifyAPI']['clientSecret'],
SETTINGS['playlists'],
SETTINGS['destinationPlaylist'],

Robert Goldmann
committed
SETTINGS['mode'],
SETTINGS['openBrowser'],
SETTINGS['cacheFilePath'])

Robert Goldmann
committed
spotifyBackup.run()
LOGGER.info('### DONE ###')

Robert Goldmann
committed
if __name__ == '__main__':
start()