From ff2bd3f7ad88b964ef9c068533ecd03d81740c69 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 16 Jul 2022 08:53:13 +0200 Subject: [PATCH] Create playlist implementation (#424) * Implement playlist_create on filesystem provider * some cleanup/fixes from previous changes --- music_assistant/controllers/music/artists.py | 26 ++++++++- music_assistant/models/music_provider.py | 6 ++ music_assistant/music_providers/filesystem.py | 55 ++++--------------- music_assistant/music_providers/qobuz.py | 2 - music_assistant/music_providers/spotify.py | 2 - music_assistant/music_providers/url.py | 7 --- 6 files changed, 42 insertions(+), 56 deletions(-) diff --git a/music_assistant/controllers/music/artists.py b/music_assistant/controllers/music/artists.py index fc241bd9..bb587b9c 100644 --- a/music_assistant/controllers/music/artists.py +++ b/music_assistant/controllers/music/artists.py @@ -138,7 +138,18 @@ class ArtistsController(MediaControllerBase[Artist]): if cache := await self.mass.cache.get(cache_key, checksum=cache_checksum): return [Track.from_dict(x) for x in cache] # no items in cache - get listing from provider - items = await prov.get_artist_toptracks(item_id) + if MusicProviderFeature.ARTIST_TOPTRACKS in prov.supported_features: + items = await prov.get_artist_toptracks(item_id) + else: + # fallback implementation using the db + if db_artist := await self.mass.music.artists.get_db_item_by_prov_id( + item_id, provider=provider, provider_id=provider_id + ): + prov_id = provider_id or provider.value + # TODO: adjust to json query instead of text search? + query = f"SELECT * FROM tracks WHERE artists LIKE '%\"{db_artist.item_id}\"%'" + query += f" AND provider_ids LIKE '%\"{prov_id}\"%'" + items = await self.mass.music.tracks.get_db_items_by_query(query) # store (serializable items) in cache self.mass.create_task( self.mass.cache.set( @@ -163,7 +174,18 @@ class ArtistsController(MediaControllerBase[Artist]): if cache := await self.mass.cache.get(cache_key, checksum=cache_checksum): return [Album.from_dict(x) for x in cache] # no items in cache - get listing from provider - items = await prov.get_artist_albums(item_id) + if MusicProviderFeature.ARTIST_ALBUMS in prov.supported_features: + items = await prov.get_artist_albums(item_id) + else: + # fallback implementation using the db + if db_artist := await self.mass.music.artists.get_db_item_by_prov_id( + item_id, provider=provider, provider_id=provider_id + ): + prov_id = provider_id or provider.value + # TODO: adjust to json query instead of text search? + query = f"SELECT * FROM albums WHERE artists LIKE '%\"{db_artist.item_id}\"%'" + query += f" AND provider_ids LIKE '%\"{prov_id}\"%'" + items = await self.mass.music.albums.get_db_items_by_query(query) # store (serializable items) in cache self.mass.create_task( self.mass.cache.set( diff --git a/music_assistant/models/music_provider.py b/music_assistant/models/music_provider.py index 6fe44bd5..2a1ce99f 100644 --- a/music_assistant/models/music_provider.py +++ b/music_assistant/models/music_provider.py @@ -224,6 +224,12 @@ class MusicProvider: if MusicProviderFeature.PLAYLIST_TRACKS_EDIT in self.supported_features: raise NotImplementedError + async def create_playlist( + self, name: str, initial_items: Optional[List[Track]] = None + ) -> Playlist: + """Create a new playlist on provider with given name.""" + raise NotImplementedError + async def get_stream_details(self, item_id: str) -> StreamDetails | None: """Get streamdetails for a track/radio.""" raise NotImplementedError diff --git a/music_assistant/music_providers/filesystem.py b/music_assistant/music_providers/filesystem.py index fb02d8f8..7b864ace 100644 --- a/music_assistant/music_providers/filesystem.py +++ b/music_assistant/music_providers/filesystem.py @@ -101,17 +101,10 @@ class FileSystemProvider(MusicProvider): MusicProviderFeature.LIBRARY_ALBUMS, MusicProviderFeature.LIBRARY_TRACKS, MusicProviderFeature.LIBRARY_PLAYLISTS, - MusicProviderFeature.LIBRARY_RADIOS, - MusicProviderFeature.LIBRARY_ARTISTS_EDIT, - MusicProviderFeature.LIBRARY_ALBUMS_EDIT, - MusicProviderFeature.LIBRARY_PLAYLISTS_EDIT, - MusicProviderFeature.LIBRARY_RADIOS_EDIT, - MusicProviderFeature.LIBRARY_TRACKS_EDIT, MusicProviderFeature.PLAYLIST_TRACKS_EDIT, + MusicProviderFeature.PLAYLIST_CREATE, MusicProviderFeature.BROWSE, MusicProviderFeature.SEARCH, - MusicProviderFeature.ARTIST_ALBUMS, - MusicProviderFeature.ARTIST_TOPTRACKS, ) async def setup(self) -> bool: @@ -369,41 +362,6 @@ class FileSystemProvider(MusicProvider): ) return None - async def get_artist_albums(self, prov_artist_id: str) -> List[Album]: - """Get a list of albums for the given artist.""" - # filesystem items are always stored in db so we can query the database - db_artist = await self.mass.music.artists.get_db_item_by_prov_id( - prov_artist_id, provider_id=self.id - ) - if db_artist is None: - raise MediaNotFoundError(f"Artist not found: {prov_artist_id}") - # TODO: adjust to json query instead of text search - query = f"SELECT * FROM albums WHERE artists LIKE '%\"{db_artist.item_id}\"%'" - query += f" AND provider_ids LIKE '%\"{self.type.value}\"%'" - return await self.mass.music.albums.get_db_items_by_query(query) - - async def get_artist_toptracks(self, prov_artist_id: str) -> List[Track]: - """Get a list of all tracks as we have no clue about preference.""" - # filesystem items are always stored in db so we can query the database - db_artist = await self.mass.music.artists.get_db_item_by_prov_id( - prov_artist_id, provider_id=self.id - ) - if db_artist is None: - raise MediaNotFoundError(f"Artist not found: {prov_artist_id}") - # TODO: adjust to json query instead of text search - query = f"SELECT * FROM tracks WHERE artists LIKE '%\"{db_artist.item_id}\"%'" - query += f" AND provider_ids LIKE '%\"{self.type.value}\"%'" - return await self.mass.music.tracks.get_db_items_by_query(query) - - async def library_add(self, *args, **kwargs) -> bool: - """Add item to provider's library. Return true on succes.""" - # already handled by database - - async def library_remove(self, *args, **kwargs) -> bool: - """Remove item from provider's library. Return true on succes.""" - # already handled by database - # TODO: do we want to process/offer deletions here ? - async def add_playlist_tracks( self, prov_playlist_id: str, prov_track_ids: List[str] ) -> None: @@ -435,6 +393,17 @@ class FileSystemProvider(MusicProvider): for uri in cur_lines: await _file.write(f"{uri}\n") + async def create_playlist( + self, name: str, initial_items: Optional[List[Track]] = None + ) -> Playlist: + """Create a new playlist on provider with given name.""" + # creating a new playlist on the filesystem is as easy + # as creating a new (empty) file with the m3u extension... + async with self.open_file(name, "w") as _file: + for item in initial_items or []: + await _file.write(item.uri + "\n") + await _file.write("\n") + async def get_stream_details(self, item_id: str) -> StreamDetails: """Return the content details for the given track when it will be streamed.""" itempath = await self._get_filepath(MediaType.TRACK, item_id) diff --git a/music_assistant/music_providers/qobuz.py b/music_assistant/music_providers/qobuz.py index 7a8f926b..2e68403f 100644 --- a/music_assistant/music_providers/qobuz.py +++ b/music_assistant/music_providers/qobuz.py @@ -50,11 +50,9 @@ class QobuzProvider(MusicProvider): MusicProviderFeature.LIBRARY_ALBUMS, MusicProviderFeature.LIBRARY_TRACKS, MusicProviderFeature.LIBRARY_PLAYLISTS, - MusicProviderFeature.LIBRARY_RADIOS, MusicProviderFeature.LIBRARY_ARTISTS_EDIT, MusicProviderFeature.LIBRARY_ALBUMS_EDIT, MusicProviderFeature.LIBRARY_PLAYLISTS_EDIT, - MusicProviderFeature.LIBRARY_RADIOS_EDIT, MusicProviderFeature.LIBRARY_TRACKS_EDIT, MusicProviderFeature.PLAYLIST_TRACKS_EDIT, MusicProviderFeature.BROWSE, diff --git a/music_assistant/music_providers/spotify.py b/music_assistant/music_providers/spotify.py index 4e4aa613..57605d57 100644 --- a/music_assistant/music_providers/spotify.py +++ b/music_assistant/music_providers/spotify.py @@ -60,11 +60,9 @@ class SpotifyProvider(MusicProvider): MusicProviderFeature.LIBRARY_ALBUMS, MusicProviderFeature.LIBRARY_TRACKS, MusicProviderFeature.LIBRARY_PLAYLISTS, - MusicProviderFeature.LIBRARY_RADIOS, MusicProviderFeature.LIBRARY_ARTISTS_EDIT, MusicProviderFeature.LIBRARY_ALBUMS_EDIT, MusicProviderFeature.LIBRARY_PLAYLISTS_EDIT, - MusicProviderFeature.LIBRARY_RADIOS_EDIT, MusicProviderFeature.LIBRARY_TRACKS_EDIT, MusicProviderFeature.PLAYLIST_TRACKS_EDIT, MusicProviderFeature.BROWSE, diff --git a/music_assistant/music_providers/url.py b/music_assistant/music_providers/url.py index 302366ca..f0803830 100644 --- a/music_assistant/music_providers/url.py +++ b/music_assistant/music_providers/url.py @@ -17,7 +17,6 @@ from music_assistant.models.enums import ( ImageType, MediaQuality, MediaType, - MusicProviderFeature, ProviderType, ) from music_assistant.models.media_items import ( @@ -44,12 +43,6 @@ class URLProvider(MusicProvider): _attr_available: bool = True _full_url = {} - @property - def supported_features(self) -> Tuple[MusicProviderFeature]: - """Return the features supported by this MusicProvider.""" - # return empty tuple because we do not really support features directly here - return tuple() - async def setup(self) -> bool: """ Handle async initialization of the provider. -- 2.34.1