Redid model to reflect service, using dataclasses
This commit is contained in:
parent
144f198424
commit
dd802a0d8f
@ -6,8 +6,8 @@ import spotframework.util.monthstrings as monthstrings
|
||||
from spotframework.engine.processor.added import AddedSince
|
||||
|
||||
from typing import List, Optional
|
||||
from spotframework.model.track import SpotifyTrack
|
||||
from spotframework.model.playlist import SpotifyPlaylist
|
||||
from spotframework.model.track import TrackFull
|
||||
from spotframework.model.playlist import FullPlaylist
|
||||
from spotframework.model.uri import Uri
|
||||
from spotframework.net.network import Network
|
||||
from spotframework.engine.processor.abstract import AbstractProcessor
|
||||
@ -47,7 +47,7 @@ class PlaylistEngine:
|
||||
|
||||
def make_playlist(self,
|
||||
params: List[SourceParameter],
|
||||
processors: List[AbstractProcessor] = None) -> List[SpotifyTrack]:
|
||||
processors: List[AbstractProcessor] = None) -> List[TrackFull]:
|
||||
|
||||
tracks = []
|
||||
|
||||
@ -77,7 +77,7 @@ class PlaylistEngine:
|
||||
boundary_date: datetime,
|
||||
processors: List[AbstractProcessor] = None,
|
||||
add_this_month: bool = False,
|
||||
add_last_month: bool = False) -> List[SpotifyTrack]:
|
||||
add_last_month: bool = False) -> List[TrackFull]:
|
||||
if processors is None:
|
||||
processors = []
|
||||
|
||||
@ -151,7 +151,7 @@ class PlaylistEngine:
|
||||
tracks_to_sort.remove(counter_track)
|
||||
|
||||
def execute_playlist(self,
|
||||
tracks: List[SpotifyTrack],
|
||||
tracks: List[TrackFull],
|
||||
uri: Uri) -> Optional[Response]:
|
||||
|
||||
resp = self.net.replace_playlist_tracks(uri=uri, uris=[i.uri for i in tracks])
|
||||
@ -197,7 +197,7 @@ class TrackSource(ABC):
|
||||
self.loaded = True
|
||||
|
||||
@abstractmethod
|
||||
def process(self, params: SourceParameter) -> List[SpotifyTrack]:
|
||||
def process(self, params: SourceParameter) -> List[TrackFull]:
|
||||
pass
|
||||
|
||||
|
||||
@ -227,7 +227,7 @@ class PlaylistSource(TrackSource):
|
||||
logger.error('error getting playlists')
|
||||
|
||||
def get_playlist_tracks(self,
|
||||
playlist: SpotifyPlaylist) -> None:
|
||||
playlist: FullPlaylist) -> None:
|
||||
logger.info(f"pulling tracks for {playlist.name}")
|
||||
|
||||
tracks = self.net.get_playlist_tracks(playlist.uri)
|
||||
@ -247,7 +247,7 @@ class PlaylistSource(TrackSource):
|
||||
|
||||
super().load()
|
||||
|
||||
def process(self, params: Params) -> List[SpotifyTrack]:
|
||||
def process(self, params: Params) -> List[TrackFull]:
|
||||
|
||||
playlists = []
|
||||
|
||||
@ -313,7 +313,7 @@ class LibraryTrackSource(TrackSource):
|
||||
|
||||
super().load()
|
||||
|
||||
def process(self, params: SourceParameter) -> List[SpotifyTrack]:
|
||||
def process(self, params: SourceParameter) -> List[TrackFull]:
|
||||
|
||||
tracks = copy.deepcopy(self.tracks)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
from spotframework.model.track import Track
|
||||
from spotframework.model.track import SimplifiedTrack
|
||||
from spotframework.model.uri import Uri
|
||||
|
||||
|
||||
@ -19,20 +19,20 @@ class AbstractProcessor(ABC):
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
def process(self, tracks: List[Track]) -> List[Track]:
|
||||
def process(self, tracks: List[SimplifiedTrack]) -> List[SimplifiedTrack]:
|
||||
pass
|
||||
|
||||
|
||||
class BatchSingleProcessor(AbstractProcessor, ABC):
|
||||
|
||||
@staticmethod
|
||||
def process_single(track: Track) -> Track:
|
||||
def process_single(track: SimplifiedTrack) -> SimplifiedTrack:
|
||||
return track
|
||||
|
||||
def process_batch(self, tracks: List[Track]) -> List[Track]:
|
||||
def process_batch(self, tracks: List[SimplifiedTrack]) -> List[SimplifiedTrack]:
|
||||
return [self.process_single(track) for track in tracks]
|
||||
|
||||
def process(self, tracks: List[Track]) -> List[Track]:
|
||||
def process(self, tracks: List[SimplifiedTrack]) -> List[SimplifiedTrack]:
|
||||
return [i for i in self.process_batch(tracks) if i is not None]
|
||||
|
||||
|
||||
@ -50,7 +50,7 @@ class BatchSingleTypeAwareProcessor(BatchSingleProcessor, ABC):
|
||||
self.instance_check = [instance_check]
|
||||
self.append_malformed = append_malformed
|
||||
|
||||
def process(self, tracks: List[Track]) -> List[Track]:
|
||||
def process(self, tracks: List[SimplifiedTrack]) -> List[SimplifiedTrack]:
|
||||
|
||||
if self.instance_check:
|
||||
return_tracks = []
|
||||
|
@ -1,7 +1,7 @@
|
||||
from spotframework.engine.processor.abstract import BatchSingleProcessor
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
from spotframework.model.track import SpotifyTrack
|
||||
from spotframework.model.track import TrackFull
|
||||
from spotframework.model.uri import Uri
|
||||
|
||||
|
||||
@ -15,14 +15,14 @@ class AudioFeaturesProcessor(BatchSingleProcessor, ABC):
|
||||
uris=uris)
|
||||
self.append_malformed = append_malformed
|
||||
|
||||
def process(self, tracks: List[SpotifyTrack]) -> List[SpotifyTrack]:
|
||||
def process(self, tracks: List[TrackFull]) -> List[TrackFull]:
|
||||
|
||||
return_tracks = []
|
||||
malformed_tracks = []
|
||||
|
||||
for track in tracks:
|
||||
|
||||
if isinstance(track, SpotifyTrack) and track.audio_features is not None:
|
||||
if isinstance(track, TrackFull) and track.audio_features is not None:
|
||||
return_tracks.append(track)
|
||||
else:
|
||||
malformed_tracks.append(track)
|
||||
@ -50,10 +50,10 @@ class FloatFilter(AudioFeaturesProcessor, ABC):
|
||||
self.greater_than = greater_than
|
||||
|
||||
@abstractmethod
|
||||
def get_variable_value(self, track: SpotifyTrack) -> float:
|
||||
def get_variable_value(self, track: TrackFull) -> float:
|
||||
pass
|
||||
|
||||
def process_single(self, track: SpotifyTrack):
|
||||
def process_single(self, track: TrackFull):
|
||||
if self.greater_than:
|
||||
if self.get_variable_value(track) > self.boundary:
|
||||
return track
|
||||
@ -67,25 +67,25 @@ class FloatFilter(AudioFeaturesProcessor, ABC):
|
||||
|
||||
|
||||
class EnergyFilter(FloatFilter):
|
||||
def get_variable_value(self, track: SpotifyTrack) -> float:
|
||||
def get_variable_value(self, track: TrackFull) -> float:
|
||||
return track.audio_features.energy
|
||||
|
||||
|
||||
class ValenceFilter(FloatFilter):
|
||||
def get_variable_value(self, track: SpotifyTrack) -> float:
|
||||
def get_variable_value(self, track: TrackFull) -> float:
|
||||
return track.audio_features.valence
|
||||
|
||||
|
||||
class TempoFilter(FloatFilter):
|
||||
def get_variable_value(self, track: SpotifyTrack) -> float:
|
||||
def get_variable_value(self, track: TrackFull) -> float:
|
||||
return track.audio_features.tempo
|
||||
|
||||
|
||||
class DanceabilityFilter(FloatFilter):
|
||||
def get_variable_value(self, track: SpotifyTrack) -> float:
|
||||
def get_variable_value(self, track: TrackFull) -> float:
|
||||
return track.audio_features.danceability
|
||||
|
||||
|
||||
class AcousticnessFilter(FloatFilter):
|
||||
def get_variable_value(self, track: SpotifyTrack) -> float:
|
||||
def get_variable_value(self, track: TrackFull) -> float:
|
||||
return track.audio_features.acousticness
|
||||
|
@ -1,7 +1,7 @@
|
||||
from spotframework.engine.processor.abstract import BatchSingleProcessor, BatchSingleTypeAwareProcessor
|
||||
from typing import List
|
||||
import logging
|
||||
from spotframework.model.track import Track, SpotifyTrack
|
||||
from spotframework.model.track import SimplifiedTrack, TrackFull
|
||||
from spotframework.model.uri import Uri
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -15,10 +15,10 @@ class DeduplicateByID(BatchSingleTypeAwareProcessor):
|
||||
append_malformed: bool = True):
|
||||
super().__init__(names=names,
|
||||
uris=uris,
|
||||
instance_check=SpotifyTrack,
|
||||
instance_check=TrackFull,
|
||||
append_malformed=append_malformed)
|
||||
|
||||
def process_batch(self, tracks: List[SpotifyTrack]) -> List[SpotifyTrack]:
|
||||
def process_batch(self, tracks: List[TrackFull]) -> List[TrackFull]:
|
||||
return_tracks = []
|
||||
|
||||
for track in tracks:
|
||||
@ -30,7 +30,7 @@ class DeduplicateByID(BatchSingleTypeAwareProcessor):
|
||||
|
||||
class DeduplicateByName(BatchSingleProcessor):
|
||||
|
||||
def process_batch(self, tracks: List[Track]) -> List[Track]:
|
||||
def process_batch(self, tracks: List[SimplifiedTrack]) -> List[SimplifiedTrack]:
|
||||
return_tracks = []
|
||||
|
||||
for to_check in tracks:
|
||||
|
@ -1,6 +1,6 @@
|
||||
from spotframework.engine.processor.abstract import BatchSingleTypeAwareProcessor
|
||||
from typing import List
|
||||
from spotframework.model.track import SpotifyTrack
|
||||
from spotframework.model.track import TrackFull
|
||||
from spotframework.model.uri import Uri
|
||||
|
||||
|
||||
@ -13,10 +13,10 @@ class SortPopularity(BatchSingleTypeAwareProcessor):
|
||||
reverse: bool = False):
|
||||
super().__init__(names=names,
|
||||
uris=uris,
|
||||
instance_check=SpotifyTrack,
|
||||
instance_check=TrackFull,
|
||||
append_malformed=append_malformed)
|
||||
self.reverse = reverse
|
||||
|
||||
def process_batch(self, tracks: List[SpotifyTrack]) -> List[SpotifyTrack]:
|
||||
def process_batch(self, tracks: List[TrackFull]) -> List[TrackFull]:
|
||||
tracks.sort(key=lambda x: x.popularity, reverse=self.reverse)
|
||||
return tracks
|
||||
|
@ -1,13 +1,13 @@
|
||||
from .abstract import AbstractProcessor
|
||||
import random
|
||||
from typing import List
|
||||
from spotframework.model.track import Track
|
||||
from spotframework.model.track import SimplifiedTrack
|
||||
from spotframework.model.uri import Uri
|
||||
|
||||
|
||||
class Shuffle(AbstractProcessor):
|
||||
|
||||
def process(self, tracks: List[Track]) -> List[Track]:
|
||||
def process(self, tracks: List[SimplifiedTrack]) -> List[SimplifiedTrack]:
|
||||
random.shuffle(tracks)
|
||||
return tracks
|
||||
|
||||
@ -21,5 +21,5 @@ class RandomSample(Shuffle):
|
||||
super().__init__(names=names, uris=uris)
|
||||
self.sample_size = sample_size
|
||||
|
||||
def process(self, tracks: List[Track]) -> List[Track]:
|
||||
def process(self, tracks: List[SimplifiedTrack]) -> List[SimplifiedTrack]:
|
||||
return super().process(tracks)[:self.sample_size]
|
||||
|
@ -1,7 +1,7 @@
|
||||
from abc import ABC
|
||||
from .abstract import AbstractProcessor, BatchSingleTypeAwareProcessor
|
||||
from typing import List
|
||||
from spotframework.model.track import Track, PlaylistTrack
|
||||
from spotframework.model.track import SimplifiedTrack, PlaylistTrack
|
||||
from spotframework.model.uri import Uri
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ class BasicReversibleSort(AbstractProcessor, ABC):
|
||||
|
||||
class SortReleaseDate(BasicReversibleSort):
|
||||
|
||||
def process(self, tracks: List[Track]) -> List[Track]:
|
||||
def process(self, tracks: List[SimplifiedTrack]) -> List[SimplifiedTrack]:
|
||||
tracks.sort(key=lambda x: (x.artists[0].name.lower(),
|
||||
x.album.name.lower(),
|
||||
x.track_number))
|
||||
@ -26,7 +26,7 @@ class SortReleaseDate(BasicReversibleSort):
|
||||
|
||||
class SortArtistName(BasicReversibleSort):
|
||||
|
||||
def process(self, tracks: List[Track]) -> List[Track]:
|
||||
def process(self, tracks: List[SimplifiedTrack]) -> List[SimplifiedTrack]:
|
||||
tracks.sort(key=lambda x: (x.album.name.lower(),
|
||||
x.track_number))
|
||||
tracks.sort(key=lambda x: x.artists[0].name.lower(), reverse=self.reverse)
|
||||
|
@ -1,4 +1,8 @@
|
||||
from typing import List
|
||||
import logging
|
||||
from spotframework.model.track import SimplifiedTrack, LibraryTrack, PlayedTrack, PlaylistTrack
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def remove_local(tracks: List, include_malformed=True) -> List:
|
||||
@ -14,3 +18,20 @@ def remove_local(tracks: List, include_malformed=True) -> List:
|
||||
return_tracks.append(track)
|
||||
|
||||
return return_tracks
|
||||
|
||||
|
||||
def get_track_objects(tracks: List) -> (List, List):
|
||||
|
||||
inner_tracks = []
|
||||
whole_tracks = []
|
||||
for track in tracks:
|
||||
if isinstance(track, SimplifiedTrack):
|
||||
inner_tracks.append(track)
|
||||
whole_tracks.append(track)
|
||||
elif isinstance(track, (PlaylistTrack, PlayedTrack, LibraryTrack)):
|
||||
inner_tracks.append(track.track)
|
||||
whole_tracks.append(track)
|
||||
else:
|
||||
logger.warning(f'invalid type found for {track} ({type(track)}), discarding')
|
||||
|
||||
return inner_tracks, whole_tracks
|
||||
|
@ -1,9 +1,9 @@
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from spotframework.model.track import SpotifyTrack
|
||||
from spotframework.model.album import SpotifyAlbum
|
||||
from spotframework.model.track import TrackFull
|
||||
from spotframework.model.uri import Uri
|
||||
from spotframework.filter import get_track_objects
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -12,13 +12,13 @@ def deduplicate_by_id(tracks: List, include_malformed=True) -> List:
|
||||
prop = 'uri'
|
||||
|
||||
return_tracks = []
|
||||
for track in tracks:
|
||||
if hasattr(track, prop) and isinstance(getattr(track, prop), Uri):
|
||||
if getattr(track, prop) not in [getattr(i, prop) for i in return_tracks]:
|
||||
return_tracks.append(track)
|
||||
for inner_track, whole_track in zip(*get_track_objects(tracks)):
|
||||
if hasattr(inner_track, prop) and isinstance(getattr(inner_track, prop), Uri):
|
||||
if getattr(inner_track, prop) not in [getattr(i, prop) for i in return_tracks]:
|
||||
return_tracks.append(whole_track)
|
||||
else:
|
||||
if include_malformed:
|
||||
return_tracks.append(track)
|
||||
return_tracks.append(whole_track)
|
||||
|
||||
return return_tracks
|
||||
|
||||
@ -26,35 +26,27 @@ def deduplicate_by_id(tracks: List, include_malformed=True) -> List:
|
||||
def deduplicate_by_name(tracks: List, include_malformed=True) -> List:
|
||||
return_tracks = []
|
||||
|
||||
for track in tracks:
|
||||
if isinstance(track, SpotifyTrack):
|
||||
to_check_artists = [i.name.lower() for i in track.artists]
|
||||
for inner_track, whole_track in zip(*get_track_objects(tracks)):
|
||||
if isinstance(inner_track, TrackFull):
|
||||
to_check_artists = [i.name.lower() for i in inner_track.artists]
|
||||
|
||||
for index, _track in enumerate(return_tracks):
|
||||
if track.name.lower() == _track.name.lower():
|
||||
for index, (_inner_track, _whole_track) in enumerate(zip(*get_track_objects(return_tracks))):
|
||||
if inner_track.name.lower() == _inner_track.name.lower():
|
||||
|
||||
_track_artists = [i.name.lower() for i in _track.artists]
|
||||
_track_artists = [i.name.lower() for i in _inner_track.artists]
|
||||
if all((i in _track_artists for i in to_check_artists)): # CHECK ARTISTS MATCH
|
||||
|
||||
if not isinstance(track.album, SpotifyAlbum):
|
||||
logger.warning(f'{track.name} album not of type SpotifyAlbum')
|
||||
continue
|
||||
|
||||
if not isinstance(_track.album, SpotifyAlbum):
|
||||
logger.warning(f'{_track.name} album not of type SpotifyAlbum')
|
||||
continue
|
||||
|
||||
# CHECK ALBUM TYPE, PREFER ALBUMS OVER SINGLES ETC
|
||||
if track.album.album_type.value > _track.album.album_type.value:
|
||||
logger.debug(f'better track source found, {track} ({track.album.album_type}) '
|
||||
f'> {_track} ({_track.album.album_type})')
|
||||
return_tracks[index] = track # REPLACE
|
||||
if inner_track.album.album_type.value > _inner_track.album.album_type.value:
|
||||
logger.debug(f'better track source found, {inner_track} ({inner_track.album.album_type}) '
|
||||
f'> {_inner_track} ({_inner_track.album.album_type})')
|
||||
return_tracks[index] = whole_track # REPLACE
|
||||
break # FOUND, ESCAPE
|
||||
else:
|
||||
return_tracks.append(track) # NOT FOUND, ADD TO RETURN
|
||||
return_tracks.append(whole_track) # NOT FOUND, ADD TO RETURN
|
||||
|
||||
else:
|
||||
if include_malformed:
|
||||
return_tracks.append(track)
|
||||
return_tracks.append(whole_track)
|
||||
|
||||
return return_tracks
|
||||
|
@ -1,47 +1,50 @@
|
||||
import logging
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
|
||||
from spotframework.model.track import Track
|
||||
from spotframework.model.album import SimplifiedAlbum
|
||||
from spotframework.filter import get_track_objects
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def sort_by_popularity(tracks: List, reverse: bool = False, include_malformed=False) -> List:
|
||||
def sort_by_popularity(tracks: List, reverse: bool = False) -> List:
|
||||
prop = 'popularity'
|
||||
|
||||
return_tracks = sorted([i for i in tracks if hasattr(i, prop) and isinstance(getattr(i, prop), int)],
|
||||
key=lambda x: x.popularity, reverse=reverse)
|
||||
|
||||
if include_malformed:
|
||||
return_tracks += [i for i in tracks
|
||||
if not hasattr(i, prop)
|
||||
or (hasattr(i, prop) and not isinstance(getattr(i, prop), int))]
|
||||
|
||||
return return_tracks
|
||||
return [j for i, j
|
||||
in sorted([(k, l) for k, l in zip(*get_track_objects(tracks))
|
||||
if hasattr(k, prop) and isinstance(getattr(k, prop), int)],
|
||||
key=lambda x: x[0].popularity, reverse=reverse
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def sort_by_release_date(tracks: List, reverse: bool = False) -> List:
|
||||
return sorted(sort_artist_album_track_number(tracks),
|
||||
key=lambda x: x.album.release_date, reverse=reverse)
|
||||
|
||||
|
||||
def sort_by_artist_name(tracks: List, reverse: bool = False) -> List:
|
||||
return_tracks = sorted([i for i in tracks if isinstance(i, Track)],
|
||||
key=lambda x: (x.album.name.lower(),
|
||||
x.track_number))
|
||||
return_tracks.sort(key=lambda x: x.artists[0].name.lower(), reverse=reverse)
|
||||
return return_tracks
|
||||
return [j for i, j
|
||||
in sorted([(k, l) for k, l in sort_artist_album_track_number(tracks, inner_tracks_only=False)
|
||||
if hasattr(k, 'album') and isinstance(getattr(k, 'album'), SimplifiedAlbum)],
|
||||
key=lambda x: x[0].album.release_date, reverse=reverse
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def sort_by_added_date(tracks: List, reverse: bool = False) -> List:
|
||||
return sorted(sort_artist_album_track_number(tracks),
|
||||
key=lambda x: x.added_at,
|
||||
reverse=reverse)
|
||||
return [j for i, j
|
||||
in sorted([(k, l) for k, l in sort_artist_album_track_number(tracks, inner_tracks_only=False)
|
||||
if hasattr(l, 'added_at') and isinstance(getattr(l, 'added_at'), datetime)],
|
||||
key=lambda x: x[1].added_at,
|
||||
reverse=reverse
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def sort_artist_album_track_number(tracks: List) -> List:
|
||||
return sorted([i for i in tracks if isinstance(i, Track)],
|
||||
key=lambda x: (x.artists[0].name.lower(),
|
||||
x.album.name.lower(),
|
||||
x.track_number))
|
||||
def sort_artist_album_track_number(tracks: List, inner_tracks_only: bool = False) -> List:
|
||||
sorted_tracks = sorted([(i, w) for i, w in zip(*get_track_objects(tracks))
|
||||
if hasattr(i, 'album') and isinstance(getattr(i, 'album'), SimplifiedAlbum)],
|
||||
key=lambda x: (x[0].artists[0].name.lower(),
|
||||
x[0].album.name.lower(),
|
||||
x[0].track_number))
|
||||
if inner_tracks_only:
|
||||
return [i for i, w in sorted_tracks]
|
||||
|
||||
return sorted_tracks
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
from spotframework.model.service import CurrentlyPlaying
|
||||
from spotframework.model.track import CurrentlyPlaying
|
||||
from spotframework.net.network import Network
|
||||
|
||||
from typing import Optional
|
||||
|
@ -1,23 +1,60 @@
|
||||
from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import List, Union
|
||||
from spotframework.util.console import Color
|
||||
from spotframework.model.uri import Uri
|
||||
if TYPE_CHECKING:
|
||||
from spotframework.model.artist import Artist
|
||||
from spotframework.model.track import Track
|
||||
import spotframework.model.artist
|
||||
import spotframework.model.service
|
||||
import spotframework.model.track
|
||||
|
||||
|
||||
class Album:
|
||||
def __init__(self, name: str, artists: List[Artist], tracks: List[Track] = None):
|
||||
self.name = name
|
||||
self.artists = artists
|
||||
if tracks is not None:
|
||||
self.tracks = tracks
|
||||
else:
|
||||
self.tracks = []
|
||||
@dataclass
|
||||
class SimplifiedAlbum:
|
||||
class Type(Enum):
|
||||
single = 0
|
||||
compilation = 1
|
||||
album = 2
|
||||
|
||||
album_type: SimplifiedAlbum.Type
|
||||
artists: List[spotframework.model.artist.SimplifiedArtist]
|
||||
available_markets: List[str]
|
||||
external_urls: dict
|
||||
href: str
|
||||
id: str
|
||||
images: List[spotframework.model.service.Image]
|
||||
name: str
|
||||
release_date: datetime
|
||||
release_date_precision: str
|
||||
type: str
|
||||
uri: Union[str, Uri]
|
||||
total_tracks: int = None
|
||||
|
||||
def __post_init__(self):
|
||||
|
||||
if isinstance(self.album_type, str):
|
||||
self.album_type = SimplifiedAlbum.Type[self.album_type.strip().lower()]
|
||||
|
||||
if isinstance(self.uri, str):
|
||||
self.uri = Uri(self.uri)
|
||||
|
||||
if self.uri:
|
||||
if self.uri.object_type != Uri.ObjectType.album:
|
||||
raise TypeError('provided uri not for an album')
|
||||
|
||||
if all((isinstance(i, dict) for i in self.artists)):
|
||||
self.artists = [spotframework.model.artist.SimplifiedArtist(**i) for i in self.artists]
|
||||
|
||||
if all((isinstance(i, dict) for i in self.images)):
|
||||
self.images = [spotframework.model.service.Image(**i) for i in self.images]
|
||||
|
||||
if isinstance(self.release_date, str):
|
||||
if self.release_date_precision == 'year':
|
||||
self.release_date = datetime.strptime(self.release_date, '%Y')
|
||||
elif self.release_date_precision == 'month':
|
||||
self.release_date = datetime.strptime(self.release_date, '%Y-%m')
|
||||
elif self.release_date_precision == 'day':
|
||||
self.release_date = datetime.strptime(self.release_date, '%Y-%m-%d')
|
||||
|
||||
@property
|
||||
def artists_names(self) -> str:
|
||||
@ -32,110 +69,56 @@ class Album:
|
||||
|
||||
return f'{self.name} / {artists}'
|
||||
|
||||
def __repr__(self):
|
||||
return Color.DARKCYAN + Color.BOLD + 'Album' + Color.END + \
|
||||
f': {self.name}, [{self.artists}]'
|
||||
|
||||
def __len__(self):
|
||||
return len(self.tracks)
|
||||
@dataclass
|
||||
class AlbumFull(SimplifiedAlbum):
|
||||
|
||||
@staticmethod
|
||||
def wrap(name: str = None,
|
||||
artists: Union[str, List[str]] = None):
|
||||
return Album(name=name, artists=[Artist(i) for i in artists])
|
||||
copyrights: List[dict] = None
|
||||
external_ids: dict = None
|
||||
genres: List[str] = None
|
||||
|
||||
|
||||
class SpotifyAlbum(Album):
|
||||
|
||||
class Type(Enum):
|
||||
single = 0
|
||||
compilation = 1
|
||||
album = 2
|
||||
|
||||
def __init__(self,
|
||||
name: str,
|
||||
artists: List[Artist],
|
||||
album_type: Type,
|
||||
|
||||
href: str = None,
|
||||
uri: Union[str, Uri] = None,
|
||||
|
||||
genres: List[str] = None,
|
||||
tracks: List[Track] = None,
|
||||
|
||||
release_date: str = None,
|
||||
release_date_precision: str = None,
|
||||
|
||||
label: str = None,
|
||||
label: str = None
|
||||
popularity: int = None
|
||||
):
|
||||
super().__init__(name, artists, tracks=tracks)
|
||||
tracks: List[spotframework.model.track.SimplifiedTrack] = None
|
||||
|
||||
self.href = href
|
||||
if isinstance(uri, str):
|
||||
self.uri = Uri(uri)
|
||||
else:
|
||||
self.uri = uri
|
||||
def __post_init__(self):
|
||||
|
||||
if isinstance(self.album_type, str):
|
||||
self.album_type = SimplifiedAlbum.Type[self.album_type]
|
||||
|
||||
if isinstance(self.uri, str):
|
||||
self.uri = Uri(self.uri)
|
||||
|
||||
if self.uri:
|
||||
if self.uri.object_type != Uri.ObjectType.album:
|
||||
raise TypeError('provided uri not for an album')
|
||||
|
||||
self.album_type = album_type
|
||||
if all((isinstance(i, dict) for i in self.artists)):
|
||||
self.artists = [spotframework.model.artist.SimplifiedArtist(**i) for i in self.artists]
|
||||
|
||||
self.genres = genres
|
||||
if all((isinstance(i, dict) for i in self.images)):
|
||||
self.images = [spotframework.model.service.Image(**i) for i in self.images]
|
||||
|
||||
self.release_date = release_date
|
||||
self.release_date_precision = release_date_precision
|
||||
if all((isinstance(i, dict) for i in self.tracks)):
|
||||
self.tracks = [spotframework.model.track.SimplifiedTrack(**i) for i in self.tracks]
|
||||
|
||||
self.label = label
|
||||
self.popularity = popularity
|
||||
|
||||
def __repr__(self):
|
||||
return Color.DARKCYAN + Color.BOLD + 'SpotifyAlbum' + Color.END + \
|
||||
f': {self.name}, {self.artists}, {self.uri}, {self.tracks}'
|
||||
|
||||
@staticmethod
|
||||
def wrap(uri: Uri = None,
|
||||
name: str = None,
|
||||
artists: Union[str, List[str]] = None):
|
||||
|
||||
if uri:
|
||||
return SpotifyAlbum(name=None, artists=None, uri=uri)
|
||||
else:
|
||||
return super().wrap(name=name, artists=artists)
|
||||
if isinstance(self.release_date, str):
|
||||
if self.release_date_precision == 'year':
|
||||
self.release_date = datetime.strptime(self.release_date, '%Y')
|
||||
elif self.release_date_precision == 'month':
|
||||
self.release_date = datetime.strptime(self.release_date, '%Y-%m')
|
||||
elif self.release_date_precision == 'day':
|
||||
self.release_date = datetime.strptime(self.release_date, '%Y-%m-%d')
|
||||
|
||||
|
||||
class LibraryAlbum(SpotifyAlbum):
|
||||
def __init__(self,
|
||||
name: str,
|
||||
artists: List[Artist],
|
||||
@dataclass
|
||||
class LibraryAlbum:
|
||||
added_at: datetime
|
||||
album: AlbumFull
|
||||
|
||||
album_type: SpotifyAlbum.Type,
|
||||
def __post_init__(self):
|
||||
if isinstance(self.album, dict):
|
||||
self.album = AlbumFull(**self.album)
|
||||
|
||||
href: str = None,
|
||||
uri: Union[str, Uri] = None,
|
||||
|
||||
genres: List[str] = None,
|
||||
tracks: List = None,
|
||||
|
||||
release_date: str = None,
|
||||
release_date_precision: str = None,
|
||||
|
||||
label: str = None,
|
||||
popularity: int = None,
|
||||
|
||||
added_at: datetime = None
|
||||
):
|
||||
super().__init__(name=name,
|
||||
artists=artists,
|
||||
album_type=album_type,
|
||||
href=href,
|
||||
uri=uri,
|
||||
genres=genres,
|
||||
tracks=tracks,
|
||||
release_date=release_date,
|
||||
release_date_precision=release_date_precision,
|
||||
label=label,
|
||||
popularity=popularity)
|
||||
self.added_at = added_at
|
||||
if isinstance(self.added_at, str):
|
||||
self.added_at = datetime.strptime(self.added_at, '%Y-%m-%dT%H:%M:%S%z')
|
||||
|
@ -1,51 +1,43 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Union
|
||||
from spotframework.util.console import Color
|
||||
from spotframework.model.uri import Uri
|
||||
from spotframework.model.service import Image
|
||||
|
||||
|
||||
class Artist:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
@dataclass
|
||||
class SimplifiedArtist:
|
||||
name: str
|
||||
external_urls: dict
|
||||
href: str
|
||||
id: str
|
||||
uri: Union[str, Uri]
|
||||
type: str
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name}'
|
||||
|
||||
def __repr__(self):
|
||||
return Color.PURPLE + Color.BOLD + 'Artist' + Color.END + \
|
||||
f': {self.name}'
|
||||
|
||||
|
||||
class SpotifyArtist(Artist):
|
||||
def __init__(self,
|
||||
name: str,
|
||||
|
||||
href: str = None,
|
||||
uri: Union[str, Uri] = None,
|
||||
|
||||
genres: List[str] = None,
|
||||
|
||||
popularity: int = None
|
||||
):
|
||||
super().__init__(name)
|
||||
|
||||
self.href = href
|
||||
if isinstance(uri, str):
|
||||
self.uri = Uri(uri)
|
||||
else:
|
||||
self.uri = uri
|
||||
def __post_init__(self):
|
||||
if isinstance(self.uri, str):
|
||||
self.uri = Uri(self.uri)
|
||||
|
||||
if self.uri:
|
||||
if self.uri.object_type != Uri.ObjectType.artist:
|
||||
raise TypeError('provided uri not for an artist')
|
||||
|
||||
self.genres = genres
|
||||
def __str__(self):
|
||||
return f'{self.name}'
|
||||
|
||||
self.popularity = popularity
|
||||
|
||||
def __repr__(self):
|
||||
return Color.PURPLE + Color.BOLD + 'SpotifyArtist' + Color.END + \
|
||||
f': {self.name}, {self.uri}'
|
||||
@dataclass
|
||||
class ArtistFull(SimplifiedArtist):
|
||||
genres: List[str]
|
||||
images: List[Image]
|
||||
popularity: int
|
||||
|
||||
@staticmethod
|
||||
def wrap(uri: Uri):
|
||||
return SpotifyArtist(name=None, uri=uri)
|
||||
def __post_init__(self):
|
||||
if isinstance(self.uri, str):
|
||||
self.uri = Uri(self.uri)
|
||||
|
||||
if self.uri:
|
||||
if self.uri.object_type != Uri.ObjectType.artist:
|
||||
raise TypeError('provided uri not for an artist')
|
||||
|
||||
if all((isinstance(i, dict) for i in self.images)):
|
||||
self.images = [Image(**i) for i in self.images]
|
||||
|
@ -1,7 +1,8 @@
|
||||
from spotframework.model.user import User
|
||||
from spotframework.model.track import Track, SpotifyTrack, PlaylistTrack
|
||||
from dataclasses import dataclass
|
||||
from spotframework.model.user import PublicUser
|
||||
from spotframework.model.track import TrackFull, PlaylistTrack
|
||||
from spotframework.model.uri import Uri
|
||||
from spotframework.util.console import Color
|
||||
from spotframework.model.service import Image
|
||||
from tabulate import tabulate
|
||||
from typing import List, Union
|
||||
import logging
|
||||
@ -9,68 +10,65 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Playlist:
|
||||
@dataclass
|
||||
class SimplifiedPlaylist:
|
||||
collaborative: bool
|
||||
description: str
|
||||
external_urls: dict
|
||||
href: str
|
||||
id: str
|
||||
images: List[Image]
|
||||
name: str
|
||||
owner: PublicUser
|
||||
primary_color: str
|
||||
public: bool
|
||||
snapshot_id: str
|
||||
tracks: List[PlaylistTrack]
|
||||
type: str
|
||||
uri: Union[str, Uri]
|
||||
|
||||
def __init__(self,
|
||||
name: str = None,
|
||||
description: str = None):
|
||||
self._tracks = []
|
||||
self.name = name
|
||||
self.description = description
|
||||
def __post_init__(self):
|
||||
if isinstance(self.tracks, dict):
|
||||
self.tracks = []
|
||||
|
||||
if isinstance(self.uri, str):
|
||||
self.uri = Uri(self.uri)
|
||||
|
||||
if self.uri:
|
||||
if self.uri.object_type != Uri.ObjectType.playlist:
|
||||
raise TypeError('provided uri not for a playlist')
|
||||
|
||||
if all((isinstance(i, dict) for i in self.images)):
|
||||
self.images = [Image(**i) for i in self.images]
|
||||
|
||||
if isinstance(self.owner, dict):
|
||||
self.owner = PublicUser(**self.owner)
|
||||
|
||||
def has_tracks(self) -> bool:
|
||||
if len(self.tracks) > 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return bool(len(self.tracks) > 0)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.tracks)
|
||||
|
||||
def __getitem__(self, item) -> Track:
|
||||
def __getitem__(self, item) -> PlaylistTrack:
|
||||
return self.tracks[item]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.tracks)
|
||||
|
||||
@property
|
||||
def tracks(self) -> List[Track]:
|
||||
return self._tracks
|
||||
|
||||
@tracks.setter
|
||||
def tracks(self, value: List[Track]):
|
||||
tracks = []
|
||||
not_tracks = []
|
||||
|
||||
for track in value:
|
||||
if isinstance(track, Track):
|
||||
tracks.append(track)
|
||||
else:
|
||||
not_tracks.append(track)
|
||||
|
||||
if len(not_tracks) > 0:
|
||||
logger.error('playlist tracks must be off type Track')
|
||||
|
||||
self._tracks = tracks
|
||||
|
||||
def __str__(self):
|
||||
|
||||
prefix = f'\n==={self.name}===\n\n' if self.name is not None else ''
|
||||
|
||||
table = prefix + self.get_tracks_string() + '\n' + f'total: {len(self)}'
|
||||
return table
|
||||
|
||||
def __repr__(self):
|
||||
return Color.GREEN + Color.BOLD + 'Playlist' + Color.END + \
|
||||
f': {self.name}, ({len(self)})'
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, Track):
|
||||
if isinstance(other, PlaylistTrack):
|
||||
self.tracks.append(other)
|
||||
return self
|
||||
|
||||
elif isinstance(other, list):
|
||||
if all((isinstance(i, Track) for i in other)):
|
||||
if all((isinstance(i, PlaylistTrack) for i in other)):
|
||||
self.tracks += other
|
||||
return self
|
||||
else:
|
||||
@ -82,12 +80,12 @@ class Playlist:
|
||||
raise TypeError('list of tracks needed to add')
|
||||
|
||||
def __sub__(self, other):
|
||||
if isinstance(other, Track):
|
||||
if isinstance(other, PlaylistTrack):
|
||||
self.tracks.remove(other)
|
||||
return self
|
||||
|
||||
elif isinstance(other, list):
|
||||
if all((isinstance(i, Track) for i in other)):
|
||||
if all((isinstance(i, PlaylistTrack) for i in other)):
|
||||
self.tracks -= other
|
||||
return self
|
||||
else:
|
||||
@ -103,12 +101,12 @@ class Playlist:
|
||||
rows = []
|
||||
headers = ['name', 'album', 'artist', 'added at', 'popularity', 'uri']
|
||||
for track in self.tracks:
|
||||
track_row = [track.name,
|
||||
track.album.name,
|
||||
track.artists_names,
|
||||
track_row = [track.track.name,
|
||||
track.track.album.name,
|
||||
track.track.artists_names,
|
||||
track.added_at if isinstance(track, PlaylistTrack) else '',
|
||||
track.popularity if isinstance(track, SpotifyTrack) else '',
|
||||
track.uri if isinstance(track, SpotifyTrack) else '']
|
||||
track.popularity if isinstance(track, TrackFull) else '',
|
||||
track.uri if isinstance(track, TrackFull) else '']
|
||||
|
||||
rows.append(track_row)
|
||||
|
||||
@ -117,49 +115,14 @@ class Playlist:
|
||||
return table
|
||||
|
||||
|
||||
class SpotifyPlaylist(Playlist):
|
||||
|
||||
def __init__(self,
|
||||
uri: Union[str, Uri],
|
||||
|
||||
name: str = None,
|
||||
owner: User = None,
|
||||
description: str = None,
|
||||
|
||||
href: str = None,
|
||||
|
||||
collaborative: bool = None,
|
||||
public: bool = None,
|
||||
ext_spotify: str = None,
|
||||
images: List[str] = None):
|
||||
|
||||
super().__init__(name=name, description=description)
|
||||
|
||||
self.owner = owner
|
||||
|
||||
self.href = href
|
||||
if isinstance(uri, str):
|
||||
self.uri = Uri(uri)
|
||||
else:
|
||||
self.uri = uri
|
||||
|
||||
if self.uri.object_type != Uri.ObjectType.playlist:
|
||||
raise TypeError('provided uri not for a playlist')
|
||||
|
||||
self.collaborative = collaborative
|
||||
self.public = public
|
||||
self.ext_spotify = ext_spotify
|
||||
self.images = images
|
||||
@dataclass
|
||||
class FullPlaylist(SimplifiedPlaylist):
|
||||
followers: dict = None
|
||||
|
||||
def __str__(self):
|
||||
|
||||
prefix = f'\n==={self.name}===\n\n' if self.name is not None else ''
|
||||
prefix += f'uri: {self.uri}\n' if self.uri is not None else ''
|
||||
|
||||
table = prefix + self.get_tracks_string() + '\n' + f'total: {len(self)}'
|
||||
|
||||
return table
|
||||
|
||||
def __repr__(self):
|
||||
return Color.GREEN + Color.BOLD + 'SpotifyPlaylist' + Color.END + \
|
||||
f': {self.name} ({self.owner}), ({len(self)}), {self.uri}'
|
||||
|
@ -1,116 +1,12 @@
|
||||
from datetime import datetime
|
||||
from spotframework.model.track import Track
|
||||
from spotframework.model.uri import Uri
|
||||
from enum import Enum
|
||||
from typing import Union
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class Context:
|
||||
def __init__(self,
|
||||
uri: Union[str, Uri],
|
||||
object_type: str = None,
|
||||
href: str = None,
|
||||
external_spot: str = None):
|
||||
if isinstance(uri, str):
|
||||
self.uri = Uri(uri)
|
||||
else:
|
||||
self.uri = uri
|
||||
if self.uri.object_type not in [Uri.ObjectType.album, Uri.ObjectType.artist, Uri.ObjectType.playlist]:
|
||||
raise TypeError('context uri must be one of album, artist, playlist')
|
||||
self.object_type = object_type
|
||||
self.href = href
|
||||
self.external_spot = external_spot
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Context) and other.uri == self.uri
|
||||
|
||||
def __repr__(self):
|
||||
return f'Context: {self.object_type} uri({self.uri})'
|
||||
|
||||
def __str__(self):
|
||||
return str(self.uri)
|
||||
|
||||
|
||||
class Device:
|
||||
|
||||
class DeviceType(Enum):
|
||||
COMPUTER = 1
|
||||
TABLET = 2
|
||||
SMARTPHONE = 3
|
||||
SPEAKER = 4
|
||||
TV = 5
|
||||
AVR = 6
|
||||
STB = 7
|
||||
AUDIODONGLE = 8
|
||||
GAMECONSOLE = 9
|
||||
CASTVIDEO = 10
|
||||
CASTAUDIO = 11
|
||||
AUTOMOBILE = 12
|
||||
UNKNOWN = 13
|
||||
|
||||
def __init__(self,
|
||||
device_id: str,
|
||||
is_active: bool,
|
||||
is_private_session: bool,
|
||||
is_restricted: bool,
|
||||
name: str,
|
||||
object_type: DeviceType,
|
||||
volume: int):
|
||||
self.device_id = device_id
|
||||
self.is_active = is_active
|
||||
self.is_private_session = is_private_session
|
||||
self.is_restricted = is_restricted
|
||||
self.name = name
|
||||
self.object_type = object_type
|
||||
self.volume = volume
|
||||
|
||||
def __repr__(self):
|
||||
return f'Device: {self.name} active({self.is_active}) type({self.object_type}) vol({self.volume})'
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class CurrentlyPlaying:
|
||||
def __init__(self,
|
||||
context: Context,
|
||||
timestamp: datetime,
|
||||
progress_ms: int,
|
||||
is_playing: bool,
|
||||
track: Track,
|
||||
device: Device,
|
||||
shuffle: bool,
|
||||
repeat: bool,
|
||||
currently_playing_type: str):
|
||||
self.context = context
|
||||
self.timestamp = timestamp
|
||||
self.progress_ms = progress_ms
|
||||
self.is_playing = is_playing
|
||||
self.track = track
|
||||
self.device = device
|
||||
self.shuffle = shuffle
|
||||
self.repeat = repeat
|
||||
self.currently_playing_type = currently_playing_type
|
||||
|
||||
def __repr__(self):
|
||||
return f'CurrentlyPlaying: is_playing({self.is_playing}) progress({self.progress_ms}) ' \
|
||||
f'context({self.context}) track({self.track}) device({self.device}) shuffle({self.shuffle}) ' \
|
||||
f'repeat({self.repeat}) time({self.timestamp})'
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, CurrentlyPlaying) and other.track == self.track and other.context == self.context
|
||||
@dataclass
|
||||
class Image:
|
||||
height: int
|
||||
width: int
|
||||
url: str
|
||||
|
||||
@staticmethod
|
||||
def _format_duration(duration):
|
||||
total_seconds = duration / 1000
|
||||
minutes = int((total_seconds/60) % 60)
|
||||
seconds = int(total_seconds % 60)
|
||||
return f'{minutes}:{seconds}'
|
||||
|
||||
def __str__(self):
|
||||
if self.is_playing:
|
||||
playing = 'playing'
|
||||
else:
|
||||
playing = '(paused)'
|
||||
|
||||
return f'{playing} {self.track} on {self.device} from {self.context} ({self._format_duration(self.progress_ms)})'
|
||||
def wrap(**kwargs):
|
||||
return Image(**kwargs)
|
||||
|
@ -1,322 +1,182 @@
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import List, Union
|
||||
from typing import TYPE_CHECKING, Union, List
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import spotframework.model
|
||||
from spotframework.model.uri import Uri
|
||||
from spotframework.util.console import Color
|
||||
from spotframework.util import convert_ms_to_minute_string
|
||||
from enum import Enum
|
||||
import spotframework.model.album
|
||||
import spotframework.model.artist
|
||||
import spotframework.model.service
|
||||
if TYPE_CHECKING:
|
||||
from spotframework.model.album import Album, SpotifyAlbum
|
||||
from spotframework.model.artist import Artist
|
||||
from spotframework.model.user import User
|
||||
from spotframework.model.service import Context
|
||||
from spotframework.model.user import PublicUser
|
||||
|
||||
|
||||
class Track:
|
||||
def __init__(self,
|
||||
name: str,
|
||||
album: Album,
|
||||
artists: List[Artist],
|
||||
@dataclass
|
||||
class SimplifiedTrack:
|
||||
artists: List[spotframework.model.artist.SimplifiedArtist]
|
||||
available_markets: List[str]
|
||||
disc_number: int
|
||||
duration_ms: int
|
||||
external_urls: dict
|
||||
explicit: bool
|
||||
href: str
|
||||
id: str
|
||||
name: str
|
||||
preview_url: str
|
||||
track_number: int
|
||||
type: str
|
||||
uri: Union[str, Uri]
|
||||
is_local: bool
|
||||
is_playable: bool = None
|
||||
episode: bool = None
|
||||
track: bool = None
|
||||
|
||||
disc_number: int = None,
|
||||
track_number: int = None,
|
||||
duration_ms: int = None,
|
||||
excplicit: bool = None
|
||||
):
|
||||
self.name = name
|
||||
self.album = album
|
||||
self.artists = artists
|
||||
def __post_init__(self):
|
||||
if isinstance(self.uri, str):
|
||||
self.uri = Uri(self.uri)
|
||||
|
||||
self.disc_number = disc_number
|
||||
self.track_number = track_number
|
||||
self.duration_ms = duration_ms
|
||||
self.explicit = excplicit
|
||||
if self.uri:
|
||||
if self.uri.object_type != Uri.ObjectType.track:
|
||||
raise TypeError('provided uri not for a track')
|
||||
|
||||
if all((isinstance(i, dict) for i in self.artists)):
|
||||
self.artists = [spotframework.model.artist.SimplifiedArtist(**i) for i in self.artists]
|
||||
|
||||
@property
|
||||
def artists_names(self) -> str:
|
||||
return self._join_strings([i.name for i in self.artists])
|
||||
|
||||
@property
|
||||
def album_artists_names(self) -> str:
|
||||
return self.album.artists_names
|
||||
|
||||
@staticmethod
|
||||
def _join_strings(string_list: List[str]):
|
||||
return ', '.join(string_list)
|
||||
|
||||
def __str__(self):
|
||||
album = self.album.name if self.album is not None else 'n/a'
|
||||
artists = ', '.join([i.name for i in self.artists]) if self.artists is not None else 'n/a'
|
||||
|
||||
return f'{self.name} / {album} / {artists}'
|
||||
|
||||
def __repr__(self):
|
||||
return Color.YELLOW + Color.BOLD + 'Track' + Color.END + \
|
||||
f': {self.name}, ({self.album}), {self.artists}'
|
||||
return f'{self.name} / {artists}'
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Track) and other.name == self.name and other.artists == self.artists
|
||||
|
||||
@staticmethod
|
||||
def wrap(name: str = None,
|
||||
artists: List[str] = None,
|
||||
album: str = None,
|
||||
album_artists: List[str] = None):
|
||||
return Track(name=name,
|
||||
album=Album.wrap(name=album, artists=album_artists),
|
||||
artists=[Artist(i) for i in artists])
|
||||
return isinstance(other, SimplifiedTrack) and other.name == self.name and other.artists == self.artists
|
||||
|
||||
|
||||
class SpotifyTrack(Track):
|
||||
def __init__(self,
|
||||
name: str,
|
||||
album: SpotifyAlbum,
|
||||
artists: List[Artist],
|
||||
@dataclass
|
||||
class TrackFull(SimplifiedTrack):
|
||||
album: spotframework.model.album.SimplifiedAlbum = None
|
||||
external_ids: dict = None
|
||||
popularity: int = None
|
||||
|
||||
href: str = None,
|
||||
uri: Union[str, Uri] = None,
|
||||
@property
|
||||
def album_artists_names(self) -> str:
|
||||
return self.album.artists_names
|
||||
|
||||
disc_number: int = None,
|
||||
track_number: int = None,
|
||||
duration_ms: int = None,
|
||||
explicit: bool = None,
|
||||
is_playable: bool = None,
|
||||
|
||||
popularity: int = None,
|
||||
|
||||
audio_features: AudioFeatures = None
|
||||
):
|
||||
super().__init__(name=name, album=album, artists=artists,
|
||||
disc_number=disc_number,
|
||||
track_number=track_number,
|
||||
duration_ms=duration_ms,
|
||||
excplicit=explicit)
|
||||
|
||||
self.href = href
|
||||
if isinstance(uri, str):
|
||||
self.uri = Uri(uri)
|
||||
else:
|
||||
self.uri = uri
|
||||
def __post_init__(self):
|
||||
if isinstance(self.uri, str):
|
||||
self.uri = Uri(self.uri)
|
||||
|
||||
if self.uri:
|
||||
if self.uri.object_type != Uri.ObjectType.track:
|
||||
raise TypeError('provided uri not for a track')
|
||||
|
||||
self.is_playable = is_playable
|
||||
if all((isinstance(i, dict) for i in self.artists)):
|
||||
self.artists = [spotframework.model.artist.SimplifiedArtist(**i) for i in self.artists]
|
||||
|
||||
self.popularity = popularity
|
||||
|
||||
self.audio_features = audio_features
|
||||
|
||||
def __repr__(self):
|
||||
string = Color.BOLD + Color.YELLOW + 'SpotifyTrack' + Color.END + \
|
||||
f': {self.name}, ({self.album}), {self.artists}, {self.uri}'
|
||||
|
||||
if self.audio_features is not None:
|
||||
string += ' ' + repr(self.audio_features)
|
||||
|
||||
return string
|
||||
if isinstance(self.album, dict):
|
||||
self.album = spotframework.model.album.SimplifiedAlbum(**self.album)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, SpotifyTrack) and other.uri == self.uri
|
||||
|
||||
@staticmethod
|
||||
def wrap(uri: Uri = None,
|
||||
name: str = None,
|
||||
artists: Union[str, List[str]] = None,
|
||||
album: str = None,
|
||||
album_artists: Union[str, List[str]] = None):
|
||||
if uri:
|
||||
return SpotifyTrack(name=None, album=None, artists=None, uri=uri)
|
||||
else:
|
||||
return super().wrap(name=name, artists=artists, album=album, album_artists=album_artists)
|
||||
return isinstance(other, TrackFull) and other.uri == self.uri
|
||||
|
||||
|
||||
class LibraryTrack(SpotifyTrack):
|
||||
def __init__(self,
|
||||
name: str,
|
||||
album: SpotifyAlbum,
|
||||
artists: List[Artist],
|
||||
@dataclass
|
||||
class LibraryTrack:
|
||||
added_at: datetime
|
||||
track: TrackFull
|
||||
|
||||
href: str = None,
|
||||
uri: Union[str, Uri] = None,
|
||||
def __post_init__(self):
|
||||
if isinstance(self.track, dict):
|
||||
self.track = TrackFull(**self.track)
|
||||
|
||||
disc_number: int = None,
|
||||
track_number: int = None,
|
||||
duration_ms: int = None,
|
||||
explicit: bool = None,
|
||||
is_playable: bool = None,
|
||||
|
||||
popularity: int = None,
|
||||
|
||||
audio_features: AudioFeatures = None,
|
||||
|
||||
added_at: datetime = None
|
||||
):
|
||||
super().__init__(name=name, album=album, artists=artists,
|
||||
href=href,
|
||||
uri=uri,
|
||||
|
||||
disc_number=disc_number,
|
||||
track_number=track_number,
|
||||
duration_ms=duration_ms,
|
||||
explicit=explicit,
|
||||
is_playable=is_playable,
|
||||
popularity=popularity,
|
||||
audio_features=audio_features)
|
||||
|
||||
self.added_at = added_at
|
||||
|
||||
def __repr__(self):
|
||||
string = Color.BOLD + Color.YELLOW + 'LibraryTrack' + Color.END + \
|
||||
f': {self.name}, ({self.album}), {self.artists}, {self.uri}, {self.added_at}'
|
||||
|
||||
if self.audio_features is not None:
|
||||
string += ' ' + repr(self.audio_features)
|
||||
|
||||
return string
|
||||
if isinstance(self.added_at, str):
|
||||
self.added_at = datetime.strptime(self.added_at, '%Y-%m-%dT%H:%M:%S%z')
|
||||
|
||||
|
||||
class PlaylistTrack(SpotifyTrack):
|
||||
def __init__(self,
|
||||
name: str,
|
||||
album: SpotifyAlbum,
|
||||
artists: List[Artist],
|
||||
@dataclass
|
||||
class PlaylistTrack:
|
||||
added_at: datetime
|
||||
added_by: PublicUser
|
||||
is_local: bool
|
||||
primary_color: str
|
||||
track: TrackFull
|
||||
video_thumbnail: dict
|
||||
|
||||
added_at: datetime,
|
||||
added_by: User,
|
||||
is_local: bool,
|
||||
def __post_init__(self):
|
||||
if isinstance(self.track, dict):
|
||||
self.track = TrackFull(**self.track)
|
||||
|
||||
href: str = None,
|
||||
uri: Union[str, Uri] = None,
|
||||
|
||||
disc_number: int = None,
|
||||
track_number: int = None,
|
||||
duration_ms: int = None,
|
||||
explicit: bool = None,
|
||||
is_playable: bool = None,
|
||||
|
||||
popularity: int = None,
|
||||
|
||||
audio_features: AudioFeatures = None
|
||||
):
|
||||
super().__init__(name=name, album=album, artists=artists,
|
||||
href=href,
|
||||
uri=uri,
|
||||
|
||||
disc_number=disc_number,
|
||||
track_number=track_number,
|
||||
duration_ms=duration_ms,
|
||||
explicit=explicit,
|
||||
is_playable=is_playable,
|
||||
popularity=popularity,
|
||||
audio_features=audio_features)
|
||||
|
||||
self.added_at = added_at
|
||||
self.added_by = added_by
|
||||
self.is_local = is_local
|
||||
|
||||
def __repr__(self):
|
||||
string = Color.BOLD + Color.YELLOW + 'PlaylistTrack' + Color.END + \
|
||||
f': {self.name}, ({self.album}), {self.artists}, {self.uri}, {self.added_at}'
|
||||
|
||||
if self.audio_features is not None:
|
||||
string += ' ' + repr(self.audio_features)
|
||||
|
||||
return string
|
||||
if isinstance(self.added_at, str):
|
||||
self.added_at = datetime.strptime(self.added_at, '%Y-%m-%dT%H:%M:%S%z')
|
||||
|
||||
|
||||
class PlayedTrack(SpotifyTrack):
|
||||
def __init__(self,
|
||||
name: str,
|
||||
album: SpotifyAlbum,
|
||||
artists: List[Artist],
|
||||
@dataclass
|
||||
class PlayedTrack:
|
||||
played_at: datetime
|
||||
context: Context
|
||||
track: SimplifiedTrack
|
||||
|
||||
href: str = None,
|
||||
uri: Union[str, Uri] = None,
|
||||
|
||||
disc_number: int = None,
|
||||
track_number: int = None,
|
||||
duration_ms: int = None,
|
||||
explicit: bool = None,
|
||||
is_playable: bool = None,
|
||||
|
||||
popularity: int = None,
|
||||
|
||||
audio_features: AudioFeatures = None,
|
||||
|
||||
played_at: datetime = None,
|
||||
context: Context = None
|
||||
):
|
||||
super().__init__(name=name, album=album, artists=artists,
|
||||
href=href,
|
||||
uri=uri,
|
||||
|
||||
disc_number=disc_number,
|
||||
duration_ms=duration_ms,
|
||||
track_number=track_number,
|
||||
explicit=explicit,
|
||||
is_playable=is_playable,
|
||||
popularity=popularity,
|
||||
audio_features=audio_features)
|
||||
self.played_at = played_at
|
||||
self.context = context
|
||||
|
||||
def __repr__(self):
|
||||
string = Color.BOLD + Color.YELLOW + 'PlayedTrack' + Color.END + \
|
||||
f': {self.name}, ({self.album}), {self.artists}, {self.uri}, {self.played_at}'
|
||||
|
||||
if self.audio_features is not None:
|
||||
string += ' ' + repr(self.audio_features)
|
||||
|
||||
return string
|
||||
def __post_init__(self):
|
||||
if isinstance(self.context, dict):
|
||||
self.context = Context(**self.context)
|
||||
if isinstance(self.track, dict):
|
||||
self.track = TrackFull(**self.track)
|
||||
if isinstance(self.played_at, str):
|
||||
self.played_at = datetime.strptime(self.played_at, '%Y-%m-%dT%H:%M:%S%z')
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioFeatures:
|
||||
acousticness: float
|
||||
analysis_url: str
|
||||
danceability: float
|
||||
duration_ms: int
|
||||
energy: float
|
||||
uri: Uri
|
||||
instrumentalness: float
|
||||
key: int
|
||||
liveness: float
|
||||
loudness: float
|
||||
mode: AudioFeatures.Mode
|
||||
speechiness: float
|
||||
tempo: float
|
||||
time_signature: int
|
||||
track_href: str
|
||||
valence: float
|
||||
type: str
|
||||
id: str
|
||||
|
||||
class Mode(Enum):
|
||||
MINOR = 0
|
||||
MAJOR = 1
|
||||
|
||||
def __init__(self,
|
||||
acousticness: float,
|
||||
analysis_url: str,
|
||||
danceability: float,
|
||||
duration_ms: int,
|
||||
energy: float,
|
||||
uri: Uri,
|
||||
instrumentalness: float,
|
||||
key: int,
|
||||
liveness: float,
|
||||
loudness: float,
|
||||
mode: int,
|
||||
speechiness: float,
|
||||
tempo: float,
|
||||
time_signature: int,
|
||||
track_href: str,
|
||||
valence: float):
|
||||
self.acousticness = self.check_float(acousticness)
|
||||
self.analysis_url = analysis_url
|
||||
self.danceability = self.check_float(danceability)
|
||||
self.duration_ms = duration_ms
|
||||
self.energy = self.check_float(energy)
|
||||
self.uri = uri
|
||||
self.instrumentalness = self.check_float(instrumentalness)
|
||||
self._key = key
|
||||
self.liveness = self.check_float(liveness)
|
||||
self.loudness = loudness
|
||||
def __post_init__(self):
|
||||
self.acousticness = self.check_float(self.acousticness)
|
||||
self.danceability = self.check_float(self.danceability)
|
||||
self.energy = self.check_float(self.energy)
|
||||
self.instrumentalness = self.check_float(self.instrumentalness)
|
||||
self.liveness = self.check_float(self.liveness)
|
||||
|
||||
if mode == 0:
|
||||
if self.mode == 0:
|
||||
self.mode = self.Mode.MINOR
|
||||
elif mode == 1:
|
||||
elif self.mode == 1:
|
||||
self.mode = self.Mode.MAJOR
|
||||
else:
|
||||
raise ValueError('illegal value for mode')
|
||||
self.speechiness = self.check_float(speechiness)
|
||||
self.tempo = tempo
|
||||
self.time_signature = time_signature
|
||||
self.track_href = track_href
|
||||
self.valence = self.check_float(valence)
|
||||
self.speechiness = self.check_float(self.speechiness)
|
||||
self.valence = self.check_float(self.valence)
|
||||
|
||||
if isinstance(self.mode, int):
|
||||
self.mode = AudioFeatures.Mode(self.mode)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
@ -340,7 +200,7 @@ class AudioFeatures:
|
||||
}
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
def key_str(self) -> str:
|
||||
legend = {
|
||||
0: 'C',
|
||||
1: 'C#',
|
||||
@ -355,21 +215,22 @@ class AudioFeatures:
|
||||
10: 'A#',
|
||||
11: 'B'
|
||||
}
|
||||
if legend.get(self._key, None):
|
||||
return legend.get(self._key, None)
|
||||
if legend.get(self.key, None):
|
||||
return legend.get(self.key, None)
|
||||
else:
|
||||
raise ValueError('key value out of bounds')
|
||||
|
||||
@key.setter
|
||||
def key(self, value):
|
||||
@key_str.setter
|
||||
def key_str(self, value):
|
||||
if isinstance(value, int):
|
||||
if 0 <= value <= 11:
|
||||
self._key = value
|
||||
self.key = value
|
||||
else:
|
||||
raise ValueError('key value out of bounds')
|
||||
else:
|
||||
raise ValueError('key value not integer')
|
||||
|
||||
@property
|
||||
def is_live(self):
|
||||
if self.liveness is not None:
|
||||
if self.liveness > 0.8:
|
||||
@ -379,6 +240,7 @@ class AudioFeatures:
|
||||
else:
|
||||
raise ValueError('no value for liveness')
|
||||
|
||||
@property
|
||||
def is_instrumental(self):
|
||||
if self.instrumentalness is not None:
|
||||
if self.instrumentalness > 0.5:
|
||||
@ -388,6 +250,7 @@ class AudioFeatures:
|
||||
else:
|
||||
raise ValueError('no value for instrumentalness')
|
||||
|
||||
@property
|
||||
def is_spoken_word(self):
|
||||
if self.speechiness is not None:
|
||||
if self.speechiness > 0.66:
|
||||
@ -409,10 +272,122 @@ class AudioFeatures:
|
||||
else:
|
||||
raise ValueError(f'value {value} is not float')
|
||||
|
||||
def __repr__(self):
|
||||
return Color.BOLD + Color.DARKCYAN + 'AudioFeatures' + Color.END + \
|
||||
f': acoustic:{self.acousticness}, dance:{self.danceability}, ' \
|
||||
f'duration:{convert_ms_to_minute_string(self.duration_ms)}, energy:{self.energy}, ' \
|
||||
f'instrumental:{self.instrumentalness}, key:{self.key}, live:{self.liveness}, ' \
|
||||
f'volume:{self.loudness}db, mode:{self.mode.name}, speech:{self.speechiness}, tempo:{self.tempo}, ' \
|
||||
f'time_sig:{self.time_signature}, valence:{self.valence}'
|
||||
|
||||
@dataclass
|
||||
class Context:
|
||||
uri: Union[str, Uri]
|
||||
type: str = None
|
||||
href: str = None
|
||||
external_urls: dict = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self):
|
||||
if isinstance(self.uri, str):
|
||||
self.uri = Uri(self.uri)
|
||||
if self.uri:
|
||||
if self.uri.object_type not in [Uri.ObjectType.album, Uri.ObjectType.artist, Uri.ObjectType.playlist]:
|
||||
raise TypeError('context uri must be one of album, artist, playlist')
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Context) and other.uri == self.uri
|
||||
|
||||
def __str__(self):
|
||||
return str(self.uri)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Device:
|
||||
|
||||
class DeviceType(Enum):
|
||||
COMPUTER = 1
|
||||
TABLET = 2
|
||||
SMARTPHONE = 3
|
||||
SPEAKER = 4
|
||||
TV = 5
|
||||
AVR = 6
|
||||
STB = 7
|
||||
AUDIODONGLE = 8
|
||||
GAMECONSOLE = 9
|
||||
CASTVIDEO = 10
|
||||
CASTAUDIO = 11
|
||||
AUTOMOBILE = 12
|
||||
UNKNOWN = 13
|
||||
|
||||
id: str
|
||||
is_active: bool
|
||||
is_private_session: bool
|
||||
is_restricted: bool
|
||||
name: str
|
||||
type: DeviceType
|
||||
volume_percent: int
|
||||
|
||||
def __post_init__(self):
|
||||
if isinstance(self.type, str):
|
||||
self.type = Device.DeviceType[self.type.upper()]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
@dataclass
|
||||
class CurrentlyPlaying:
|
||||
context: Context
|
||||
timestamp: str
|
||||
progress_ms: int
|
||||
is_playing: bool
|
||||
item: spotframework.model.track.SimplifiedTrack
|
||||
device: Device
|
||||
shuffle_state: bool
|
||||
repeat_state: bool
|
||||
currently_playing_type: str
|
||||
actions: dict
|
||||
|
||||
def __post_init__(self):
|
||||
if isinstance(self.context, Context):
|
||||
self.context = Context(**self.context)
|
||||
|
||||
if isinstance(self.item, spotframework.model.track.SimplifiedTrack):
|
||||
self.item = spotframework.model.track.SimplifiedTrack(**self.item)
|
||||
|
||||
if isinstance(self.device, Device):
|
||||
self.device = Device(**self.device)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, CurrentlyPlaying) and other.item == self.item and other.context == self.context
|
||||
|
||||
@staticmethod
|
||||
def _format_duration(duration):
|
||||
total_seconds = duration / 1000
|
||||
minutes = int((total_seconds/60) % 60)
|
||||
seconds = int(total_seconds % 60)
|
||||
return f'{minutes}:{seconds}'
|
||||
|
||||
def __str__(self):
|
||||
if self.is_playing:
|
||||
playing = 'playing'
|
||||
else:
|
||||
playing = '(paused)'
|
||||
|
||||
return f'{playing} {self.item} on {self.device} from {self.context} ({self._format_duration(self.progress_ms)})'
|
||||
|
||||
|
||||
@dataclass
|
||||
class RecommendationsSeed:
|
||||
afterFilteringSize: int
|
||||
afterRelinkingSize: int
|
||||
href: str
|
||||
id: str
|
||||
initialPoolSize: int
|
||||
type: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Recommendations:
|
||||
seeds: List[RecommendationsSeed]
|
||||
tracks: List[spotframework.model.track.SimplifiedTrack]
|
||||
|
||||
def __post_init__(self):
|
||||
if all((isinstance(i, dict) for i in self.seeds)):
|
||||
self.seeds = [RecommendationsSeed(**i) for i in self.seeds]
|
||||
|
||||
if all((isinstance(i, dict) for i in self.tracks)):
|
||||
self.tracks = [spotframework.model.track.TrackFull(**i) for i in self.tracks]
|
@ -1,31 +1,39 @@
|
||||
from spotframework.util.console import Color
|
||||
from typing import Union, List
|
||||
from dataclasses import dataclass, field
|
||||
from spotframework.model.uri import Uri
|
||||
from typing import Union
|
||||
from spotframework.model.service import Image
|
||||
|
||||
|
||||
class User:
|
||||
def __init__(self,
|
||||
username: str,
|
||||
@dataclass
|
||||
class PublicUser:
|
||||
href: str
|
||||
id: str
|
||||
uri: Union[str, Uri]
|
||||
display_name: str
|
||||
external_urls: dict
|
||||
type: str
|
||||
|
||||
href: str = None,
|
||||
uri: Union[str, Uri] = None,
|
||||
followers: dict = field(default_factory=dict)
|
||||
images: List[Image] = field(default_factory=list)
|
||||
|
||||
display_name: str = None,
|
||||
ext_spotify: str = None):
|
||||
self.username = username
|
||||
def __post_init__(self):
|
||||
if isinstance(self.uri, str):
|
||||
self.uri = Uri(self.uri)
|
||||
|
||||
self.href = href
|
||||
if isinstance(uri, str):
|
||||
self.uri = Uri(uri)
|
||||
else:
|
||||
self.uri = uri
|
||||
if self.uri:
|
||||
if self.uri.object_type != Uri.ObjectType.user:
|
||||
raise TypeError('provided uri not for a user')
|
||||
|
||||
self.display_name = display_name
|
||||
self.ext_spotify = ext_spotify
|
||||
if all((isinstance(i, dict) for i in self.images)):
|
||||
self.images = [Image(**i) for i in self.images]
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.username}'
|
||||
return f'{self.display_name}'
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrivateUser(PublicUser):
|
||||
country: str = None
|
||||
email: str = None
|
||||
product: str = None
|
||||
|
||||
def __repr__(self):
|
||||
return Color.RED + Color.BOLD + 'User' + Color.END + \
|
||||
f': {self.username}, {self.display_name}, {self.uri}'
|
||||
|
@ -2,17 +2,18 @@ import requests
|
||||
import random
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Union
|
||||
import datetime
|
||||
|
||||
from spotframework.model.artist import SpotifyArtist
|
||||
from spotframework.model.user import User
|
||||
from spotframework.model.artist import ArtistFull
|
||||
from spotframework.model.user import PublicUser
|
||||
from . import const
|
||||
from spotframework.net.user import NetworkUser
|
||||
from spotframework.model.playlist import SpotifyPlaylist
|
||||
from spotframework.model.track import Track, SpotifyTrack, PlaylistTrack, PlayedTrack, LibraryTrack, AudioFeatures
|
||||
from spotframework.model.album import LibraryAlbum, SpotifyAlbum
|
||||
from spotframework.model.service import CurrentlyPlaying, Device, Context
|
||||
from spotframework.model.playlist import SimplifiedPlaylist, FullPlaylist
|
||||
from spotframework.model.track import SimplifiedTrack, TrackFull, PlaylistTrack, PlayedTrack, LibraryTrack, \
|
||||
AudioFeatures, Device, CurrentlyPlaying, Recommendations
|
||||
from spotframework.model.album import AlbumFull, LibraryAlbum, SimplifiedAlbum
|
||||
from spotframework.model.uri import Uri
|
||||
from requests.models import Response
|
||||
|
||||
@ -21,16 +22,12 @@ limit = 50
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SearchResponse:
|
||||
def __init__(self,
|
||||
tracks: List[SpotifyTrack],
|
||||
albums: List[SpotifyAlbum],
|
||||
artists: List[SpotifyArtist],
|
||||
playlists: List[SpotifyPlaylist]):
|
||||
self.tracks = tracks
|
||||
self.albums = albums
|
||||
self.artists = artists
|
||||
self.playlists = playlists
|
||||
tracks: List[TrackFull]
|
||||
albums: List[SimplifiedAlbum]
|
||||
artists: List[ArtistFull]
|
||||
playlists: List[SimplifiedPlaylist]
|
||||
|
||||
@property
|
||||
def all(self):
|
||||
@ -95,7 +92,7 @@ class Network:
|
||||
|
||||
elif req.status_code == 401:
|
||||
logger.warning(f'{method} access token expired, refreshing')
|
||||
self.user.refresh_token()
|
||||
self.user.refresh_access_token()
|
||||
if self.refresh_counter < 5:
|
||||
self.refresh_counter += 1
|
||||
return self.get_request(method, url, params, headers)
|
||||
@ -153,7 +150,7 @@ class Network:
|
||||
|
||||
elif req.status_code == 401:
|
||||
logger.warning(f'{method} access token expired, refreshing')
|
||||
self.user.refresh_token()
|
||||
self.user.refresh_access_token()
|
||||
if self.refresh_counter < 5:
|
||||
self.refresh_counter += 1
|
||||
return self.post_request(method, url, params, json, headers)
|
||||
@ -211,7 +208,7 @@ class Network:
|
||||
|
||||
elif req.status_code == 401:
|
||||
logger.warning(f'{method} access token expired, refreshing')
|
||||
self.user.refresh_token()
|
||||
self.user.refresh_access_token()
|
||||
if self.refresh_counter < 5:
|
||||
self.refresh_counter += 1
|
||||
return self.put_request(method, url, params, json, headers)
|
||||
@ -232,7 +229,7 @@ class Network:
|
||||
def get_playlist(self,
|
||||
uri: Uri = None,
|
||||
uri_string: str = None,
|
||||
tracks: bool = True) -> Optional[SpotifyPlaylist]:
|
||||
tracks: bool = True) -> Optional[FullPlaylist]:
|
||||
"""get playlist object with tracks for uri
|
||||
|
||||
:param uri: target request uri
|
||||
@ -252,19 +249,19 @@ class Network:
|
||||
resp = self.get_request('getPlaylist', f'playlists/{uri.object_id}')
|
||||
|
||||
if resp:
|
||||
playlist = self.parse_playlist(resp)
|
||||
playlist = FullPlaylist(**resp)
|
||||
|
||||
if tracks and resp.get('tracks'):
|
||||
if resp.get('tracks'):
|
||||
if 'next' in resp['tracks']:
|
||||
logger.debug(f'paging tracks for {uri}')
|
||||
|
||||
track_pager = PageCollection(net=self, page=resp['tracks'])
|
||||
track_pager.continue_iteration()
|
||||
|
||||
playlist.tracks = [self.parse_track(i) for i in track_pager.items]
|
||||
playlist.tracks = [PlaylistTrack(**i) for i in track_pager.items]
|
||||
else:
|
||||
logger.debug(f'parsing {len(resp.get("tracks"))} tracks for {uri}')
|
||||
playlist.tracks = [self.parse_track(i) for i in resp.get('tracks', [])]
|
||||
playlist.tracks = [PlaylistTrack(**i) for i in resp.get('tracks', [])]
|
||||
|
||||
return playlist
|
||||
else:
|
||||
@ -276,7 +273,7 @@ class Network:
|
||||
name: str = 'New Playlist',
|
||||
public: bool = True,
|
||||
collaborative: bool = False,
|
||||
description: bool = None) -> Optional[SpotifyPlaylist]:
|
||||
description: bool = None) -> Optional[FullPlaylist]:
|
||||
"""create playlist for user
|
||||
|
||||
:param username: username for playlist creation
|
||||
@ -297,12 +294,12 @@ class Network:
|
||||
req = self.post_request('createPlaylist', f'users/{username}/playlists', json=json)
|
||||
|
||||
if 200 <= req.status_code < 300:
|
||||
return self.parse_playlist(req.json())
|
||||
return FullPlaylist(**req.json())
|
||||
else:
|
||||
logger.error('error creating playlist')
|
||||
return None
|
||||
|
||||
def get_playlists(self, response_limit: int = None) -> Optional[List[SpotifyPlaylist]]:
|
||||
def get_playlists(self, response_limit: int = None) -> Optional[List[FullPlaylist]]:
|
||||
"""get current users playlists
|
||||
|
||||
:param response_limit: max playlists to return
|
||||
@ -316,7 +313,7 @@ class Network:
|
||||
pager.total_limit = response_limit
|
||||
pager.iterate()
|
||||
|
||||
return_items = [self.parse_playlist(i) for i in pager.items]
|
||||
return_items = [SimplifiedPlaylist(**i) for i in pager.items]
|
||||
|
||||
if len(return_items) == 0:
|
||||
logger.error('no playlists returned')
|
||||
@ -337,7 +334,7 @@ class Network:
|
||||
pager.total_limit = response_limit
|
||||
pager.iterate()
|
||||
|
||||
return_items = [self.parse_album(i) for i in pager.items]
|
||||
return_items = [LibraryAlbum(**i) for i in pager.items]
|
||||
|
||||
if len(return_items) == 0:
|
||||
logger.error('no albums returned')
|
||||
@ -358,14 +355,14 @@ class Network:
|
||||
pager.total_limit = response_limit
|
||||
pager.iterate()
|
||||
|
||||
return_items = [self.parse_track(i) for i in pager.items]
|
||||
return_items = [LibraryTrack(**i) for i in pager.items]
|
||||
|
||||
if len(return_items) == 0:
|
||||
logger.error('no tracks returned')
|
||||
|
||||
return return_items
|
||||
|
||||
def get_user_playlists(self) -> Optional[List[SpotifyPlaylist]]:
|
||||
def get_user_playlists(self) -> Optional[List[FullPlaylist]]:
|
||||
"""retrieve user owned playlists
|
||||
|
||||
:return: List of user owned playlists if available
|
||||
@ -375,12 +372,12 @@ class Network:
|
||||
|
||||
playlists = self.get_playlists()
|
||||
|
||||
if self.user.username is None:
|
||||
if self.user.user.id is None:
|
||||
logger.debug('no user info, refreshing for filter')
|
||||
self.user.refresh_info()
|
||||
|
||||
if playlists is not None:
|
||||
return list(filter(lambda x: x.owner.username == self.user.username, playlists))
|
||||
return list(filter(lambda x: x.owner.id == self.user.user.id, playlists))
|
||||
else:
|
||||
logger.error('no playlists returned to filter')
|
||||
|
||||
@ -409,7 +406,7 @@ class Network:
|
||||
pager.total_limit = response_limit
|
||||
pager.iterate()
|
||||
|
||||
return_items = [self.parse_track(i) for i in pager.items]
|
||||
return_items = [PlaylistTrack(**i) for i in pager.items]
|
||||
|
||||
if len(return_items) == 0:
|
||||
logger.error('no tracks returned')
|
||||
@ -425,7 +422,7 @@ class Network:
|
||||
if resp:
|
||||
if len(resp['devices']) == 0:
|
||||
logger.error('no devices returned')
|
||||
return [self.parse_device(i) for i in resp['devices']]
|
||||
return [Device(**i) for i in resp['devices']]
|
||||
else:
|
||||
logger.error('no devices returned')
|
||||
return None
|
||||
@ -462,7 +459,7 @@ class Network:
|
||||
pager.total_limit = 20
|
||||
pager.continue_iteration()
|
||||
|
||||
return [self.parse_track(i) for i in pager.items]
|
||||
return [PlayedTrack(**i) for i in pager.items]
|
||||
else:
|
||||
logger.error('no tracks returned')
|
||||
|
||||
@ -473,7 +470,7 @@ class Network:
|
||||
|
||||
resp = self.get_request('getPlayer', 'me/player')
|
||||
if resp:
|
||||
return self.parse_currently_playing(resp)
|
||||
return CurrentlyPlaying(**resp)
|
||||
else:
|
||||
logger.info('no player returned')
|
||||
|
||||
@ -490,12 +487,21 @@ class Network:
|
||||
if devices:
|
||||
device = next((i for i in devices if i.name == device_name), None)
|
||||
if device:
|
||||
return device.device_id
|
||||
return device.id
|
||||
else:
|
||||
logger.error(f'{device_name} not found')
|
||||
else:
|
||||
logger.error('no devices returned')
|
||||
|
||||
def get_current_user(self) -> Optional[PublicUser]:
|
||||
logger.info(f"getting current user")
|
||||
|
||||
resp = self.get_request('getCurrentUser', 'me')
|
||||
if resp:
|
||||
return PublicUser(**resp)
|
||||
else:
|
||||
logger.info('no user returned')
|
||||
|
||||
def change_playback_device(self, device_id: str):
|
||||
"""migrate playback to different device"""
|
||||
|
||||
@ -733,7 +739,7 @@ class Network:
|
||||
def get_recommendations(self,
|
||||
tracks: List[str] = None,
|
||||
artists: List[str] = None,
|
||||
response_limit=10) -> Optional[List[Track]]:
|
||||
response_limit=10) -> Optional[Recommendations]:
|
||||
|
||||
logger.info(f'getting {response_limit} recommendations, '
|
||||
f'tracks: {len(tracks) if tracks is not None else 0}, '
|
||||
@ -754,17 +760,13 @@ class Network:
|
||||
else:
|
||||
resp = self.get_request('getRecommendations', 'recommendations', params=params)
|
||||
if resp:
|
||||
if 'tracks' in resp:
|
||||
return [self.parse_track(i) for i in resp['tracks']]
|
||||
else:
|
||||
logger.error('no tracks returned')
|
||||
return None
|
||||
return Recommendations(**resp)
|
||||
else:
|
||||
logger.error('error getting recommendations')
|
||||
return None
|
||||
|
||||
def write_playlist_object(self,
|
||||
playlist: SpotifyPlaylist,
|
||||
playlist: FullPlaylist,
|
||||
append_tracks: bool = False):
|
||||
logger.info(f'writing {playlist.name}, append tracks: {append_tracks}')
|
||||
|
||||
@ -775,10 +777,10 @@ class Network:
|
||||
elif playlist.tracks:
|
||||
if append_tracks:
|
||||
self.add_playlist_tracks(playlist.uri, [i.uri for i in playlist.tracks if
|
||||
isinstance(i, SpotifyTrack)])
|
||||
isinstance(i, SimplifiedTrack)])
|
||||
else:
|
||||
self.replace_playlist_tracks(uri=playlist.uri, uris=[i.uri for i in playlist.tracks if
|
||||
isinstance(i, SpotifyTrack)])
|
||||
isinstance(i, SimplifiedTrack)])
|
||||
|
||||
if playlist.name or playlist.collaborative or playlist.public or playlist.description:
|
||||
self.change_playlist_details(playlist.uri,
|
||||
@ -819,7 +821,7 @@ class Network:
|
||||
else:
|
||||
logger.error('error reordering playlist')
|
||||
|
||||
def get_track_audio_features(self, uris: List[Uri]):
|
||||
def get_track_audio_features(self, uris: List[Uri]) -> Optional[List[AudioFeatures]]:
|
||||
logger.info(f'getting {len(uris)} features')
|
||||
|
||||
audio_features = []
|
||||
@ -831,13 +833,7 @@ class Network:
|
||||
|
||||
if resp:
|
||||
if resp.get('audio_features', None):
|
||||
|
||||
for feature in resp['audio_features']:
|
||||
if feature is not None:
|
||||
audio_features.append(self.parse_audio_features(feature))
|
||||
else:
|
||||
audio_features.append(None)
|
||||
|
||||
return [AudioFeatures(**i) for i in resp['audio_features']]
|
||||
else:
|
||||
logger.error('no audio features included')
|
||||
else:
|
||||
@ -848,10 +844,10 @@ class Network:
|
||||
else:
|
||||
logger.error('mismatched length of input and response')
|
||||
|
||||
def populate_track_audio_features(self, tracks=Union[SpotifyTrack, List[SpotifyTrack]]):
|
||||
def populate_track_audio_features(self, tracks=Union[TrackFull, List[TrackFull]]):
|
||||
logger.info(f'populating {len(tracks)} features')
|
||||
|
||||
if isinstance(tracks, SpotifyTrack):
|
||||
if isinstance(tracks, TrackFull):
|
||||
audio_features = self.get_track_audio_features([tracks.uri])
|
||||
|
||||
if audio_features:
|
||||
@ -864,7 +860,7 @@ class Network:
|
||||
logger.error(f'no audio features returned for {tracks.uri}')
|
||||
|
||||
elif isinstance(tracks, List):
|
||||
if all(isinstance(i, SpotifyTrack) for i in tracks):
|
||||
if all(isinstance(i, TrackFull) for i in tracks):
|
||||
audio_features = self.get_track_audio_features([i.uri for i in tracks])
|
||||
|
||||
if audio_features:
|
||||
@ -882,7 +878,7 @@ class Network:
|
||||
|
||||
def get_tracks(self,
|
||||
uris: List[Uri] = None,
|
||||
uri_strings: List[str] = None) -> List[SpotifyTrack]:
|
||||
uri_strings: List[str] = None) -> List[TrackFull]:
|
||||
|
||||
if uris is None and uri_strings is None:
|
||||
raise NameError('no uris provided')
|
||||
@ -900,11 +896,11 @@ class Network:
|
||||
for chunk in chunked_uris:
|
||||
resp = self.get_request(method='getTracks', url='tracks', params={'ids': ','.join([i.object_id for i in chunk])})
|
||||
if resp:
|
||||
tracks += [self.parse_track(i) for i in resp.get('tracks', [])]
|
||||
tracks += [TrackFull(**i) for i in resp.get('tracks', [])]
|
||||
|
||||
return tracks
|
||||
|
||||
def get_track(self, uri: Uri = None, uri_string: str = None) -> Optional[SpotifyTrack]:
|
||||
def get_track(self, uri: Uri = None, uri_string: str = None) -> Optional[TrackFull]:
|
||||
|
||||
if uri is None and uri_string is None:
|
||||
raise NameError('no uri provided')
|
||||
@ -918,7 +914,7 @@ class Network:
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_albums(self, uris: List[Uri] = None, uri_strings: List[str] = None) -> List[SpotifyAlbum]:
|
||||
def get_albums(self, uris: List[Uri] = None, uri_strings: List[str] = None) -> List[AlbumFull]:
|
||||
|
||||
if uris is None and uri_strings is None:
|
||||
raise NameError('no uris provided')
|
||||
@ -936,11 +932,11 @@ class Network:
|
||||
for chunk in chunked_uris:
|
||||
resp = self.get_request(method='getAlbums', url='albums', params={'ids': ','.join([i.object_id for i in chunk])})
|
||||
if resp:
|
||||
albums += [self.parse_album(i) for i in resp.get('albums', [])]
|
||||
albums += [AlbumFull(**i) for i in resp.get('albums', [])]
|
||||
|
||||
return albums
|
||||
|
||||
def get_album(self, uri: Uri = None, uri_string: str = None) -> Optional[SpotifyAlbum]:
|
||||
def get_album(self, uri: Uri = None, uri_string: str = None) -> Optional[AlbumFull]:
|
||||
|
||||
if uri is None and uri_string is None:
|
||||
raise NameError('no uri provided')
|
||||
@ -954,7 +950,7 @@ class Network:
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_artists(self, uris: List[Uri] = None, uri_strings: List[str] = None) -> List[SpotifyArtist]:
|
||||
def get_artists(self, uris: List[Uri] = None, uri_strings: List[str] = None) -> List[ArtistFull]:
|
||||
|
||||
if uris is None and uri_strings is None:
|
||||
raise NameError('no uris provided')
|
||||
@ -972,11 +968,11 @@ class Network:
|
||||
for chunk in chunked_uris:
|
||||
resp = self.get_request(method='getArtists', url='artists', params={'ids': ','.join([i.object_id for i in chunk])})
|
||||
if resp:
|
||||
artists += [self.parse_artist(i) for i in resp.get('artists', [])]
|
||||
artists += [ArtistFull(**i) for i in resp.get('artists', [])]
|
||||
|
||||
return artists
|
||||
|
||||
def get_artist(self, uri: Uri = None, uri_string: str = None) -> Optional[SpotifyArtist]:
|
||||
def get_artist(self, uri: Uri = None, uri_string: str = None) -> Optional[ArtistFull]:
|
||||
|
||||
if uri is None and uri_string is None:
|
||||
raise NameError('no uri provided')
|
||||
@ -1022,321 +1018,13 @@ class Network:
|
||||
|
||||
resp = self.get_request(method='search', url='search', params=params)
|
||||
|
||||
albums = [self.parse_album(i) for i in resp.get('albums', {}).get('items', [])]
|
||||
artists = [self.parse_artist(i) for i in resp.get('artists', {}).get('items', [])]
|
||||
tracks = [self.parse_track(i) for i in resp.get('tracks', {}).get('items', [])]
|
||||
playlists = [self.parse_playlist(i) for i in resp.get('playlists', {}).get('items', [])]
|
||||
albums = [SimplifiedAlbum(**i) for i in resp.get('albums', {}).get('items', [])]
|
||||
artists = [ArtistFull(**i) for i in resp.get('artists', {}).get('items', [])]
|
||||
tracks = [TrackFull(**i) for i in resp.get('tracks', {}).get('items', [])]
|
||||
playlists = [SimplifiedPlaylist(**i) for i in resp.get('playlists', {}).get('items', [])]
|
||||
|
||||
return SearchResponse(tracks=tracks, albums=albums, artists=artists, playlists=playlists)
|
||||
|
||||
@staticmethod
|
||||
def parse_artist(artist_dict) -> SpotifyArtist:
|
||||
|
||||
name = artist_dict.get('name', None)
|
||||
|
||||
href = artist_dict.get('href', None)
|
||||
uri = artist_dict.get('uri', None)
|
||||
|
||||
genres = artist_dict.get('genres', None)
|
||||
popularity = artist_dict.get('popularity', None)
|
||||
|
||||
if name is None:
|
||||
raise KeyError('artist name not found')
|
||||
|
||||
return SpotifyArtist(name,
|
||||
href=href,
|
||||
uri=uri,
|
||||
|
||||
genres=genres,
|
||||
popularity=popularity)
|
||||
|
||||
def parse_album(self, album_dict) -> Union[SpotifyAlbum, LibraryAlbum]:
|
||||
if 'album' in album_dict:
|
||||
album = album_dict.get('album', None)
|
||||
else:
|
||||
album = album_dict
|
||||
|
||||
name = album.get('name', None)
|
||||
if name is None:
|
||||
raise KeyError('album name not found')
|
||||
|
||||
artists = [self.parse_artist(i) for i in album.get('artists', [])]
|
||||
|
||||
if album.get("album_type") is not None:
|
||||
album_type = SpotifyAlbum.Type[album.get('album_type').lower()]
|
||||
else:
|
||||
album_type = SpotifyAlbum.Type.single
|
||||
|
||||
href = album.get('href', None)
|
||||
uri = album.get('uri', None)
|
||||
|
||||
genres = album.get('genres', None)
|
||||
if album.get('tracks'):
|
||||
if 'next' in album['tracks']:
|
||||
|
||||
track_pager = PageCollection(net=self, page=album['tracks'])
|
||||
track_pager.continue_iteration()
|
||||
|
||||
tracks = [self.parse_track(i) for i in track_pager.items]
|
||||
else:
|
||||
tracks = [self.parse_track(i) for i in album.get('tracks', [])]
|
||||
else:
|
||||
tracks = []
|
||||
|
||||
release_date = album.get('release_date', None)
|
||||
release_date_precision = album.get('release_date_precision', None)
|
||||
|
||||
label = album.get('label', None)
|
||||
popularity = album.get('popularity', None)
|
||||
|
||||
added_at = album_dict.get('added_at', None)
|
||||
if added_at:
|
||||
added_at = datetime.datetime.strptime(added_at, '%Y-%m-%dT%H:%M:%S%z')
|
||||
|
||||
if added_at:
|
||||
return LibraryAlbum(name=name,
|
||||
artists=artists,
|
||||
|
||||
album_type=album_type,
|
||||
|
||||
href=href,
|
||||
uri=uri,
|
||||
|
||||
genres=genres,
|
||||
tracks=tracks,
|
||||
|
||||
release_date=release_date,
|
||||
release_date_precision=release_date_precision,
|
||||
|
||||
label=label,
|
||||
popularity=popularity,
|
||||
|
||||
added_at=added_at)
|
||||
else:
|
||||
return SpotifyAlbum(name=name,
|
||||
artists=artists,
|
||||
|
||||
album_type=album_type,
|
||||
|
||||
href=href,
|
||||
uri=uri,
|
||||
|
||||
genres=genres,
|
||||
tracks=tracks,
|
||||
|
||||
release_date=release_date,
|
||||
release_date_precision=release_date_precision,
|
||||
|
||||
label=label,
|
||||
popularity=popularity)
|
||||
|
||||
def parse_track(self, track_dict) -> Union[Track, SpotifyTrack, PlaylistTrack, PlayedTrack, LibraryTrack]:
|
||||
|
||||
if 'track' in track_dict:
|
||||
track = track_dict.get('track', None)
|
||||
else:
|
||||
track = track_dict
|
||||
|
||||
name = track.get('name', None)
|
||||
if name is None:
|
||||
raise KeyError('track name not found')
|
||||
|
||||
if track.get('album', None):
|
||||
album = self.parse_album(track['album'])
|
||||
else:
|
||||
album = None
|
||||
|
||||
artists = [self.parse_artist(i) for i in track.get('artists', [])]
|
||||
|
||||
href = track.get('href', None)
|
||||
uri = track.get('uri', None)
|
||||
|
||||
disc_number = track.get('disc_number', None)
|
||||
track_number = track.get('track_number', None)
|
||||
duration_ms = track.get('duration_ms', None)
|
||||
explicit = track.get('explicit', None)
|
||||
is_playable = track.get('is_playable', None)
|
||||
|
||||
popularity = track.get('popularity', None)
|
||||
|
||||
added_by = self.parse_user(track_dict.get('added_by')) if track_dict.get('added_by', None) else None
|
||||
added_at = track_dict.get('added_at', None)
|
||||
if added_at:
|
||||
added_at = datetime.datetime.strptime(added_at, '%Y-%m-%dT%H:%M:%S%z')
|
||||
is_local = track_dict.get('is_local', None)
|
||||
|
||||
played_at = track_dict.get('played_at', None)
|
||||
if played_at:
|
||||
played_at = datetime.datetime.strptime(played_at, '%Y-%m-%dT%H:%M:%S.%f%z')
|
||||
context = track_dict.get('context', None)
|
||||
if context:
|
||||
context = self.parse_context(context)
|
||||
|
||||
if added_by or is_local:
|
||||
return PlaylistTrack(name=name,
|
||||
album=album,
|
||||
artists=artists,
|
||||
|
||||
added_at=added_at,
|
||||
added_by=added_by,
|
||||
is_local=is_local,
|
||||
|
||||
href=href,
|
||||
uri=uri,
|
||||
|
||||
disc_number=disc_number,
|
||||
track_number=track_number,
|
||||
duration_ms=duration_ms,
|
||||
explicit=explicit,
|
||||
is_playable=is_playable,
|
||||
|
||||
popularity=popularity)
|
||||
elif added_at:
|
||||
return LibraryTrack(name=name,
|
||||
album=album,
|
||||
artists=artists,
|
||||
|
||||
href=href,
|
||||
uri=uri,
|
||||
|
||||
disc_number=disc_number,
|
||||
track_number=track_number,
|
||||
duration_ms=duration_ms,
|
||||
explicit=explicit,
|
||||
is_playable=is_playable,
|
||||
|
||||
popularity=popularity,
|
||||
added_at=added_at)
|
||||
elif played_at or context:
|
||||
return PlayedTrack(name=name,
|
||||
album=album,
|
||||
artists=artists,
|
||||
|
||||
href=href,
|
||||
uri=uri,
|
||||
|
||||
disc_number=disc_number,
|
||||
track_number=track_number,
|
||||
duration_ms=duration_ms,
|
||||
explicit=explicit,
|
||||
is_playable=is_playable,
|
||||
|
||||
popularity=popularity,
|
||||
played_at=played_at,
|
||||
context=context)
|
||||
else:
|
||||
return SpotifyTrack(name=name,
|
||||
album=album,
|
||||
artists=artists,
|
||||
|
||||
href=href,
|
||||
uri=uri,
|
||||
|
||||
disc_number=disc_number,
|
||||
track_number=track_number,
|
||||
duration_ms=duration_ms,
|
||||
explicit=explicit,
|
||||
is_playable=is_playable,
|
||||
|
||||
popularity=popularity)
|
||||
|
||||
@staticmethod
|
||||
def parse_user(user_dict) -> User:
|
||||
display_name = user_dict.get('display_name', None)
|
||||
|
||||
spotify_id = user_dict.get('id', None)
|
||||
href = user_dict.get('href', None)
|
||||
uri = user_dict.get('uri', None)
|
||||
|
||||
return User(spotify_id,
|
||||
href=href,
|
||||
uri=uri,
|
||||
display_name=display_name)
|
||||
|
||||
def parse_playlist(self, playlist_dict) -> SpotifyPlaylist:
|
||||
|
||||
collaborative = playlist_dict.get('collaborative', None)
|
||||
|
||||
ext_spotify = None
|
||||
if playlist_dict.get('external_urls', None):
|
||||
if playlist_dict['external_urls'].get('spotify', None):
|
||||
ext_spotify = playlist_dict['external_urls']['spotify']
|
||||
|
||||
href = playlist_dict.get('href', None)
|
||||
description = playlist_dict.get('description', None)
|
||||
|
||||
name = playlist_dict.get('name', None)
|
||||
|
||||
if playlist_dict.get('owner', None):
|
||||
owner = self.parse_user(playlist_dict.get('owner'))
|
||||
else:
|
||||
owner = None
|
||||
|
||||
public = playlist_dict.get('public', None)
|
||||
uri = playlist_dict.get('uri', None)
|
||||
|
||||
images = playlist_dict.get('images', [])
|
||||
images.sort(key=lambda x: x.get('height', 0))
|
||||
images = [i.get('url') for i in images]
|
||||
|
||||
return SpotifyPlaylist(uri=uri,
|
||||
name=name,
|
||||
owner=owner,
|
||||
description=description,
|
||||
href=href,
|
||||
collaborative=collaborative,
|
||||
public=public,
|
||||
ext_spotify=ext_spotify,
|
||||
images=images)
|
||||
|
||||
@staticmethod
|
||||
def parse_context(context_dict) -> Context:
|
||||
return Context(object_type=context_dict['type'],
|
||||
href=context_dict['href'],
|
||||
external_spot=context_dict['external_urls']['spotify'],
|
||||
uri=context_dict['uri'])
|
||||
|
||||
def parse_currently_playing(self, play_dict) -> CurrentlyPlaying:
|
||||
return CurrentlyPlaying(
|
||||
context=self.parse_context(play_dict['context']) if play_dict['context'] is not None else None,
|
||||
timestamp=datetime.datetime.fromtimestamp(play_dict['timestamp'] / 1000),
|
||||
progress_ms=play_dict['progress_ms'],
|
||||
is_playing=play_dict['is_playing'],
|
||||
track=self.parse_track(play_dict['item']),
|
||||
device=self.parse_device(play_dict['device']),
|
||||
shuffle=play_dict['shuffle_state'],
|
||||
repeat=play_dict['repeat_state'],
|
||||
currently_playing_type=play_dict['currently_playing_type'])
|
||||
|
||||
@staticmethod
|
||||
def parse_device(device_dict) -> Device:
|
||||
return Device(device_id=device_dict['id'],
|
||||
is_active=device_dict['is_active'],
|
||||
is_private_session=device_dict['is_private_session'],
|
||||
is_restricted=device_dict['is_restricted'],
|
||||
name=device_dict['name'],
|
||||
object_type=Device.DeviceType[device_dict['type'].upper()],
|
||||
volume=device_dict['volume_percent'])
|
||||
|
||||
@staticmethod
|
||||
def parse_audio_features(feature_dict) -> AudioFeatures:
|
||||
return AudioFeatures(acousticness=feature_dict['acousticness'],
|
||||
analysis_url=feature_dict['analysis_url'],
|
||||
danceability=feature_dict['danceability'],
|
||||
duration_ms=feature_dict['duration_ms'],
|
||||
energy=feature_dict['energy'],
|
||||
uri=Uri(feature_dict['uri']),
|
||||
instrumentalness=feature_dict['instrumentalness'],
|
||||
key=feature_dict['key'],
|
||||
liveness=feature_dict['liveness'],
|
||||
loudness=feature_dict['loudness'],
|
||||
mode=feature_dict['mode'],
|
||||
speechiness=feature_dict['speechiness'],
|
||||
tempo=feature_dict['tempo'],
|
||||
time_signature=feature_dict['time_signature'],
|
||||
track_href=feature_dict['track_href'],
|
||||
valence=feature_dict['valence'])
|
||||
|
||||
@staticmethod
|
||||
def chunk(l, n):
|
||||
for i in range(0, len(l), n):
|
||||
|
@ -1,37 +1,34 @@
|
||||
from __future__ import annotations
|
||||
import requests
|
||||
from spotframework.model.user import User
|
||||
from spotframework.model.user import PublicUser
|
||||
from spotframework.util.console import Color
|
||||
from dataclasses import dataclass, field
|
||||
from base64 import b64encode
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
from datetime import datetime, timezone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NetworkUser(User):
|
||||
@dataclass
|
||||
class NetworkUser:
|
||||
|
||||
def __init__(self, client_id, client_secret, refresh_token, access_token=None):
|
||||
super().__init__(None)
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
|
||||
self.access_token = access_token
|
||||
self.refresh_token = refresh_token
|
||||
client_id: str
|
||||
client_secret: str
|
||||
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
user: PublicUser = field(default=None, init=False)
|
||||
|
||||
self.last_refreshed = None
|
||||
self.token_expiry = None
|
||||
last_refreshed: datetime = field(default=None, init=False)
|
||||
token_expiry: datetime = field(default=None, init=False)
|
||||
|
||||
self.on_refresh = []
|
||||
on_refresh: List = field(default_factory=list, init=False)
|
||||
|
||||
self.refresh_counter = 0
|
||||
|
||||
def __repr__(self):
|
||||
return Color.RED + Color.BOLD + 'NetworkUser' + Color.END + \
|
||||
f': {self.username}, {self.display_name}, {self.uri}'
|
||||
refresh_counter: int = field(default=0, init=False)
|
||||
|
||||
def refresh_access_token(self) -> NetworkUser:
|
||||
|
||||
@ -70,7 +67,7 @@ class NetworkUser(User):
|
||||
if retry_after:
|
||||
logger.warning(f'rate limit reached: retrying in {retry_after} seconds')
|
||||
time.sleep(int(retry_after) + 1)
|
||||
return self.refresh_token()
|
||||
return self.refresh_access_token()
|
||||
else:
|
||||
logger.error('rate limit reached: cannot find Retry-After header')
|
||||
|
||||
@ -82,23 +79,7 @@ class NetworkUser(User):
|
||||
return self
|
||||
|
||||
def refresh_info(self) -> None:
|
||||
info = self.get_info()
|
||||
|
||||
if info.get('display_name', None):
|
||||
self.display_name = info['display_name']
|
||||
|
||||
if info.get('external_urls', None):
|
||||
if info['external_urls'].get('spotify', None):
|
||||
self.ext_spotify = info['external_urls']['spotify']
|
||||
|
||||
if info.get('href', None):
|
||||
self.href = info['href']
|
||||
|
||||
if info.get('id', None):
|
||||
self.username = info['id']
|
||||
|
||||
if info.get('uri', None):
|
||||
self.uri = info['uri']
|
||||
self.user = PublicUser(**self.get_info())
|
||||
|
||||
def get_info(self) -> Optional[dict]:
|
||||
|
||||
@ -123,7 +104,7 @@ class NetworkUser(User):
|
||||
|
||||
elif req.status_code == 401:
|
||||
logger.warning('access token expired, refreshing')
|
||||
self.refresh_token()
|
||||
self.refresh_access_token()
|
||||
if self.refresh_counter < 5:
|
||||
self.refresh_counter += 1
|
||||
return self.get_info()
|
||||
|
@ -1,8 +1,8 @@
|
||||
from spotframework.net.network import Network
|
||||
from spotframework.model.track import SpotifyTrack
|
||||
from spotframework.model.album import SpotifyAlbum
|
||||
from spotframework.model.playlist import SpotifyPlaylist
|
||||
from spotframework.model.service import Context, Device
|
||||
from spotframework.model.track import SimplifiedTrack, Context, Device
|
||||
from spotframework.model.album import AlbumFull
|
||||
from spotframework.model.playlist import FullPlaylist
|
||||
from spotframework.model.uri import Uri
|
||||
from typing import List, Union
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -16,7 +16,7 @@ class Player:
|
||||
self.last_status = None
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.net.user.username} - {self.status}'
|
||||
return f'{self.net.user.user.display_name} - {self.status}'
|
||||
|
||||
def __repr__(self):
|
||||
return f'Player: {self.net.user} - {self.status}'
|
||||
@ -33,8 +33,9 @@ class Player:
|
||||
return self.last_status
|
||||
|
||||
def play(self,
|
||||
context: Union[Context, SpotifyAlbum, SpotifyPlaylist] = None,
|
||||
tracks: List[SpotifyTrack] = None,
|
||||
context: Union[Context, AlbumFull, FullPlaylist] = None,
|
||||
tracks: List[SimplifiedTrack] = None,
|
||||
uris: List = None,
|
||||
device: Device = None,
|
||||
device_name: str = None):
|
||||
if device_name:
|
||||
@ -42,23 +43,30 @@ class Player:
|
||||
if searched_device:
|
||||
device = searched_device
|
||||
|
||||
if context and tracks:
|
||||
if context and (tracks or uris):
|
||||
raise Exception('cant execute context and track list')
|
||||
if context:
|
||||
if device:
|
||||
self.net.play(uri=context.uri, deviceid=device.device_id)
|
||||
self.net.play(uri=context.uri, deviceid=device.id)
|
||||
else:
|
||||
self.net.play(uri=context.uri)
|
||||
elif tracks:
|
||||
elif tracks or uris:
|
||||
|
||||
if tracks is None:
|
||||
tracks = []
|
||||
|
||||
if uris is None:
|
||||
uris = []
|
||||
|
||||
if device:
|
||||
self.net.play(uris=[i.uri for i in tracks], deviceid=device.device_id)
|
||||
self.net.play(uris=[i.uri for i in tracks] + uris, deviceid=device.id)
|
||||
else:
|
||||
self.net.play(uris=[i.uri for i in tracks])
|
||||
self.net.play(uris=[i.uri for i in tracks] + uris)
|
||||
else:
|
||||
self.net.play()
|
||||
|
||||
def change_device(self, device: Device):
|
||||
self.net.change_playback_device(device.device_id)
|
||||
self.net.change_playback_device(device.id)
|
||||
|
||||
def pause(self):
|
||||
self.net.pause()
|
||||
@ -88,7 +96,7 @@ class Player:
|
||||
raise TypeError(f'{state} is not bool')
|
||||
else:
|
||||
status = self.status
|
||||
if status.shuffle:
|
||||
if status.shuffle_state:
|
||||
self.shuffle(state=False)
|
||||
else:
|
||||
self.shuffle(state=True)
|
||||
@ -97,7 +105,7 @@ class Player:
|
||||
|
||||
if 0 <= int(value) <= 100:
|
||||
if device:
|
||||
self.net.set_volume(value, deviceid=device.device_id)
|
||||
self.net.set_volume(value, deviceid=device.id)
|
||||
else:
|
||||
self.net.set_volume(value)
|
||||
else:
|
||||
|
@ -1,4 +1,5 @@
|
||||
import math
|
||||
from spotframework.model.uri import Uri
|
||||
|
||||
|
||||
def convert_ms_to_minute_string(ms):
|
||||
@ -6,3 +7,11 @@ def convert_ms_to_minute_string(ms):
|
||||
minutes = math.floor(seconds / 60)
|
||||
|
||||
return f'{minutes}:{math.floor(seconds%60)}'
|
||||
|
||||
|
||||
def validate_uri_string(uri_string: str):
|
||||
try:
|
||||
uri = Uri(uri_string)
|
||||
return uri
|
||||
except ValueError:
|
||||
return False
|
||||
|
Loading…
Reference in New Issue
Block a user