refactor to source based playlist engine framework

This commit is contained in:
aj 2019-09-23 21:08:04 +01:00
parent 91ff0e1bce
commit c34eb4d8d9
7 changed files with 284 additions and 120 deletions

View File

@ -1,7 +1,6 @@
import requests
import os
import logging import logging
import copy import copy
from abc import ABC, abstractmethod
import spotframework.util.monthstrings as monthstrings import spotframework.util.monthstrings as monthstrings
from spotframework.engine.processor.added import AddedSince from spotframework.engine.processor.added import AddedSince
@ -18,129 +17,75 @@ from requests.models import Response
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SourceParameter:
def __init__(self,
source_type,
processors: List[AbstractProcessor] = None):
self.processors = processors if processors is not None else []
self.source_type = source_type
class PlaylistEngine: class PlaylistEngine:
def __init__(self, net: Network): def __init__(self, net: Network):
self.playlists = [] self.sources = []
self.library_tracks = []
self.net = net self.net = net
def load_user_playlists(self) -> None: def init_default_sources(self):
logger.info('loading') self.sources = [PlaylistSource(self.net), RecommendationSource(self.net)]
playlists = self.net.get_playlists() def check_for_source(self, class_type) -> bool:
if playlists and len(playlists) > 0: source = next((i for i in self.sources if isinstance(i, class_type)), None)
self.playlists = playlists
if source:
return True
else: else:
logger.error('error getting playlists') return False
def append_user_playlists(self) -> None: def get_source(self, class_type):
logger.info('loading') return next((i for i in self.sources if isinstance(i, class_type)), None)
playlists = self.net.get_playlists()
if playlists and len(playlists) > 0:
self.playlists += playlists
else:
logger.error('error getting playlists')
def load_library_tracks(self, track_limit: int = None) -> None:
logger.info('loading')
if track_limit:
tracks = self.net.get_library_tracks(response_limit=track_limit)
else:
tracks = self.net.get_library_tracks()
if tracks and len(tracks) > 0:
self.library_tracks = tracks
def get_playlist_tracks(self,
playlist: SpotifyPlaylist) -> None:
logger.info(f"pulling tracks for {playlist.name}")
tracks = self.net.get_playlist_tracks(playlist.uri)
if tracks and len(tracks) > 0:
playlist.tracks = tracks
else:
logger.error('error getting tracks')
def load_playlist_tracks(self, name: str):
playlist = next((i for i in self.playlists if i.name == name), None)
if playlist is not None:
self.get_playlist_tracks(playlist)
else:
logger.error(f'playlist {name} not found')
def make_playlist(self, def make_playlist(self,
playlist_parts: List[str], params: List[SourceParameter],
processors: List[AbstractProcessor] = None, processors: List[AbstractProcessor] = None) -> List[SpotifyTrack]:
include_recommendations: bool = False,
recommendation_limit: int = 10,
include_library_tracks: bool = False,
library_processors: List[AbstractProcessor] = None) -> List[SpotifyTrack]:
if processors is None:
processors = []
if library_processors is None:
library_processors = []
tracks = [] tracks = []
for part in playlist_parts: for param in params:
source = next((i for i in self.sources if isinstance(i, param.source_type)), None)
play = next((i for i in self.playlists if i.name == part), None) if source:
if source.loaded is False:
if play is not None: source.load()
if play.has_tracks() is False:
self.get_playlist_tracks(play)
playlist_tracks = copy.deepcopy(play.tracks)
for processor in [i for i in processors if i.has_targets()]:
if play.name in [i for i in processor.playlist_names]:
playlist_tracks = processor.process(playlist_tracks)
tracks += [i for i in playlist_tracks if i.is_local is False]
if isinstance(source, RecommendationSource) and isinstance(param, RecommendationSource.Params):
tracks += source.process(params=param, uris=[i.uri for i in tracks])
else: else:
logger.warning(f"requested playlist {part} not found") tracks += source.process(params=param)
if 'SLACKHOOK' in os.environ: else:
requests.post(os.environ['SLACKHOOK'], json={"text": f"spot playlists: {part} not found"}) new_source = param.source_type(net=self.net)
new_source.load()
self.sources.append(new_source)
for processor in [i for i in processors if i.has_targets() is False]: if isinstance(new_source, RecommendationSource) and isinstance(param, RecommendationSource.Params):
tracks += new_source.process(params=param, uris=[i.uri for i in tracks])
else:
tracks += new_source.process(params=param)
logger.info(f'adding {str(param.source_type)} source')
if processors:
for processor in processors:
tracks = processor.process(tracks) tracks = processor.process(tracks)
if include_library_tracks:
library_tracks = copy.deepcopy(self.library_tracks)
for processor in library_processors:
library_tracks = processor.process(library_tracks)
tracks += library_tracks
if include_recommendations:
recommendations = self.net.get_recommendations(tracks=[i.uri.object_id for i in tracks],
response_limit=recommendation_limit)
if recommendations and len(recommendations) > 0:
tracks += recommendations
else:
logger.error('error getting recommendations')
return tracks return tracks
def get_recent_playlist(self, def get_recent_playlist(self,
params: List[SourceParameter],
boundary_date: datetime, boundary_date: datetime,
recent_playlist_parts: List[str],
processors: List[AbstractProcessor] = None, processors: List[AbstractProcessor] = None,
include_recommendations: bool = False,
recommendation_limit: int = 10,
add_this_month: bool = False, add_this_month: bool = False,
add_last_month: bool = False) -> List[SpotifyTrack]: add_last_month: bool = False) -> List[SpotifyTrack]:
if processors is None:
processors = []
this_month = monthstrings.get_this_month() this_month = monthstrings.get_this_month()
last_month = monthstrings.get_last_month() last_month = monthstrings.get_last_month()
@ -152,14 +97,12 @@ class PlaylistEngine:
if add_last_month: if add_last_month:
month_playlists.append(last_month) month_playlists.append(last_month)
datefilter = AddedSince(boundary_date, recent_playlist_parts + month_playlists) if PlaylistSource in [i.source_type for i in params]:
processors.append(datefilter) param = next((i for i in params if i.source_type == PlaylistSource), None)
param.names += month_playlists
return self.make_playlist(recent_playlist_parts + month_playlists, return self.make_playlist(params=params, processors=processors + [AddedSince(boundary_date)])
processors,
include_recommendations=include_recommendations,
recommendation_limit=recommendation_limit)
def reorder_playlist_by_added_date(self, def reorder_playlist_by_added_date(self,
name: str = None, name: str = None,
@ -169,10 +112,20 @@ class PlaylistEngine:
logger.error('no playlist name or id provided') logger.error('no playlist name or id provided')
raise ValueError('no playlist name or id provided') raise ValueError('no playlist name or id provided')
if name: playlist_source = self.get_source(PlaylistSource)
playlist = next((i for i in self.playlists if i.name == name), None)
if playlist_source:
if playlist_source.loaded is False:
playlist_source.load()
else: else:
playlist = next((i for i in self.playlists if i.uri == uri), None) playlist_source = PlaylistSource(self.net)
playlist_source.load()
self.sources.append(playlist_source)
if name:
playlist = next((i for i in playlist_source.playlists if i.name == name), None)
else:
playlist = next((i for i in playlist_source.playlists if i.uri == uri), None)
if playlist is None: if playlist is None:
logger.error('playlist not found') logger.error('playlist not found')
@ -225,3 +178,191 @@ class PlaylistEngine:
return resp return resp
else: else:
logger.error('error changing description') logger.error('error changing description')
class TrackSource(ABC):
def __init__(self, net: Network):
self.net = net
self.loaded = False
@abstractmethod
def load(self) -> None:
self.loaded = True
@abstractmethod
def process(self, params: SourceParameter) -> List[SpotifyTrack]:
pass
class PlaylistSource(TrackSource):
class Params(SourceParameter):
def __init__(self,
names: List[str] = None,
uris: List[Uri] = None,
processors: List[AbstractProcessor] = None):
self.names = names if names is not None else []
self.uris = uris if uris is not None else []
super().__init__(processors=processors, source_type=PlaylistSource)
def __init__(self,
net: Network):
self.playlists = []
super().__init__(net)
def append_user_playlists(self) -> None:
logger.info('appending user playlists')
playlists = self.net.get_playlists()
if playlists and len(playlists) > 0:
self.playlists += playlists
else:
logger.error('error getting playlists')
def get_playlist_tracks(self,
playlist: SpotifyPlaylist) -> None:
logger.info(f"pulling tracks for {playlist.name}")
tracks = self.net.get_playlist_tracks(playlist.uri)
if tracks and len(tracks) > 0:
playlist.tracks = tracks
else:
logger.error('error getting tracks')
def load(self) -> None:
logger.info('loading user playlists')
playlists = self.net.get_playlists()
if playlists and len(playlists) > 0:
self.playlists = playlists
else:
logger.error('error getting playlists')
super().load()
def process(self, params: Params) -> List[SpotifyTrack]:
playlists = []
for name in params.names:
playlist = next((i for i in self.playlists if i.name == name), None)
if playlist is not None:
playlists.append(playlist)
else:
logger.warning(f'could not find playlist {name}')
for uri in params.uris:
playlist = next((i for i in self.playlists if i.uri == uri), None)
if playlist:
playlists.append(playlist)
else:
playlist = self.net.get_playlist(uri)
if playlist:
playlists.append(playlist)
self.playlists.append(playlist)
else:
logger.warning(f'could not find playlist {uri}')
tracks = []
for playlist in playlists:
if playlist.has_tracks() is False:
self.get_playlist_tracks(playlist)
playlist_tracks = copy.deepcopy(playlist.tracks)
for processor in [i for i in params.processors if i.has_targets()]:
if playlist.name in [i for i in processor.playlist_names]\
or playlist.uri in [i for i in processor.playlist_uris]:
playlist_tracks = processor.process(playlist_tracks)
tracks += [i for i in playlist_tracks if i.is_local is False]
for processor in [i for i in params.processors if i.has_targets() is False]:
tracks = processor.process(tracks)
return tracks
class LibraryTrackSource(TrackSource):
class Params(SourceParameter):
def __init__(self, processors: List[AbstractProcessor] = None):
super().__init__(processors=processors, source_type=LibraryTrackSource)
def __init__(self,
net: Network):
self.tracks = []
super().__init__(net)
def load(self) -> None:
logger.info('loading library tracks')
tracks = self.net.get_library_tracks()
if tracks and len(tracks) > 0:
self.tracks = tracks
else:
logger.error('error getting tracks')
super().load()
def process(self, params: SourceParameter) -> List[SpotifyTrack]:
tracks = copy.deepcopy(self.tracks)
for index, track in enumerate(tracks):
for processor in [i for i in params.processors if i.playlist_uris]:
if track.uri in [i for i in processor.playlist_uris]:
new_track = processor.process([track])
if new_track and len(new_track) > 0:
tracks[index] = new_track[0]
else:
tracks[index] = None
tracks = [i for i in tracks if i is not None]
for processor in [i for i in params.processors if i.has_targets() is False]:
tracks = processor.process(tracks)
return tracks
class RecommendationSource(TrackSource):
class Params(SourceParameter):
def __init__(self,
uris: List[Uri] = None,
recommendation_limit: int = None,
processors: List[AbstractProcessor] = None):
self.uris = uris
self.recommendation_limit = recommendation_limit if recommendation_limit is not None else 10
super().__init__(processors=processors, source_type=RecommendationSource)
def load(self):
super().load()
def process(self, params: Params, uris: List[Uri] = None):
query_uris = []
if params.uris is not None:
query_uris += params.uris
if uris is not None:
query_uris += uris
if len(query_uris) > 0:
recommendations = self.net.get_recommendations(tracks=[i.object_id for i in query_uris
if i.object_type == Uri.ObjectType.track],
response_limit=params.recommendation_limit)
if recommendations and len(recommendations) > 0:
pass
else:
logger.error('error getting recommendations')
return recommendations
else:
logger.error('no uris to get recommendations for')

View File

@ -1,15 +1,19 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import List from typing import List
from spotframework.model.track import Track from spotframework.model.track import Track
from spotframework.model.uri import Uri
class AbstractProcessor(ABC): class AbstractProcessor(ABC):
def __init__(self, names: List[str] = None): def __init__(self,
names: List[str] = None,
uris: List[Uri] = None):
self.playlist_names = names self.playlist_names = names
self.playlist_uris = uris
def has_targets(self) -> bool: def has_targets(self) -> bool:
if self.playlist_names: if self.playlist_names or self.playlist_uris:
return True return True
else: else:
return False return False
@ -36,10 +40,14 @@ class BatchSingleTypeAwareProcessor(BatchSingleProcessor, ABC):
def __init__(self, def __init__(self,
names: List[str] = None, names: List[str] = None,
uris: List[Uri] = None,
instance_check=None, instance_check=None,
append_malformed: bool = True): append_malformed: bool = True):
super().__init__(names) super().__init__(names, uris)
if isinstance(instance_check, list):
self.instance_check = instance_check self.instance_check = instance_check
else:
self.instance_check = [instance_check]
self.append_malformed = append_malformed self.append_malformed = append_malformed
def process(self, tracks: List[Track]) -> List[Track]: def process(self, tracks: List[Track]) -> List[Track]:
@ -50,7 +58,7 @@ class BatchSingleTypeAwareProcessor(BatchSingleProcessor, ABC):
for track in tracks: for track in tracks:
if isinstance(track, self.instance_check): if any(isinstance(track, i) for i in self.instance_check):
return_tracks.append(track) return_tracks.append(track)
else: else:
malformed_tracks.append(track) malformed_tracks.append(track)

View File

@ -1,7 +1,8 @@
from .abstract import BatchSingleTypeAwareProcessor from .abstract import BatchSingleTypeAwareProcessor
import datetime import datetime
from typing import List from typing import List
from spotframework.model.track import PlaylistTrack from spotframework.model.track import PlaylistTrack, LibraryTrack
from spotframework.model.uri import Uri
from typing import Optional from typing import Optional
@ -10,9 +11,11 @@ class Added(BatchSingleTypeAwareProcessor):
def __init__(self, def __init__(self,
boundary: datetime.datetime, boundary: datetime.datetime,
names: List[str] = None, names: List[str] = None,
uris: List[Uri] = None,
append_malformed: bool = True): append_malformed: bool = True):
super().__init__(names, super().__init__(names=names,
instance_check=PlaylistTrack, uris=uris,
instance_check=[PlaylistTrack, LibraryTrack],
append_malformed=append_malformed) append_malformed=append_malformed)
self.boundary = boundary self.boundary = boundary

View File

@ -1,14 +1,17 @@
from spotframework.engine.processor.abstract import BatchSingleProcessor, BatchSingleTypeAwareProcessor from spotframework.engine.processor.abstract import BatchSingleProcessor, BatchSingleTypeAwareProcessor
from typing import List from typing import List
from spotframework.model.track import Track, SpotifyTrack from spotframework.model.track import Track, SpotifyTrack
from spotframework.model.uri import Uri
class DeduplicateByID(BatchSingleTypeAwareProcessor): class DeduplicateByID(BatchSingleTypeAwareProcessor):
def __init__(self, def __init__(self,
names: List[str] = None, names: List[str] = None,
uris: List[Uri] = None,
append_malformed: bool = True): append_malformed: bool = True):
super().__init__(names, super().__init__(names=names,
uris=uris,
instance_check=SpotifyTrack, instance_check=SpotifyTrack,
append_malformed=append_malformed) append_malformed=append_malformed)

View File

@ -1,15 +1,18 @@
from spotframework.engine.processor.abstract import BatchSingleTypeAwareProcessor from spotframework.engine.processor.abstract import BatchSingleTypeAwareProcessor
from typing import List from typing import List
from spotframework.model.track import SpotifyTrack from spotframework.model.track import SpotifyTrack
from spotframework.model.uri import Uri
class SortPopularity(BatchSingleTypeAwareProcessor): class SortPopularity(BatchSingleTypeAwareProcessor):
def __init__(self, def __init__(self,
names: List[str] = None, names: List[str] = None,
uris: List[Uri] = None,
append_malformed: bool = True, append_malformed: bool = True,
reverse: bool = False): reverse: bool = False):
super().__init__(names, super().__init__(names=names,
uris=uris,
instance_check=SpotifyTrack, instance_check=SpotifyTrack,
append_malformed=append_malformed) append_malformed=append_malformed)
self.reverse = reverse self.reverse = reverse

View File

@ -2,6 +2,7 @@ from .abstract import AbstractProcessor
import random import random
from typing import List from typing import List
from spotframework.model.track import Track from spotframework.model.track import Track
from spotframework.model.uri import Uri
class Shuffle(AbstractProcessor): class Shuffle(AbstractProcessor):
@ -15,8 +16,9 @@ class RandomSample(Shuffle):
def __init__(self, def __init__(self,
sample_size: int, sample_size: int,
names: List[str] = None): names: List[str] = None,
super().__init__(names) uris: List[Uri] = None):
super().__init__(names=names, uris=uris)
self.sample_size = sample_size self.sample_size = sample_size
def process(self, tracks: List[Track]) -> List[Track]: def process(self, tracks: List[Track]) -> List[Track]:

View File

@ -2,13 +2,15 @@ from abc import ABC
from .abstract import AbstractProcessor, BatchSingleTypeAwareProcessor from .abstract import AbstractProcessor, BatchSingleTypeAwareProcessor
from typing import List from typing import List
from spotframework.model.track import Track, PlaylistTrack from spotframework.model.track import Track, PlaylistTrack
from spotframework.model.uri import Uri
class BasicReversibleSort(AbstractProcessor, ABC): class BasicReversibleSort(AbstractProcessor, ABC):
def __init__(self, def __init__(self,
names: List[str] = None, names: List[str] = None,
uris: List[Uri] = None,
reverse: bool = False): reverse: bool = False):
super().__init__(names) super().__init__(names=names, uris=uris)
self.reverse = reverse self.reverse = reverse
@ -30,9 +32,11 @@ class SortAddedDate(BatchSingleTypeAwareProcessor):
def __init__(self, def __init__(self,
names: List[str] = None, names: List[str] = None,
uris: List[Uri] = None,
reverse: bool = False, reverse: bool = False,
append_malformed: bool = True): append_malformed: bool = True):
super().__init__(names=names, super().__init__(names=names,
uris=uris,
instance_check=PlaylistTrack, instance_check=PlaylistTrack,
append_malformed=append_malformed) append_malformed=append_malformed)
self.reverse = reverse self.reverse = reverse