Create playlist implementation (#424)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sat, 16 Jul 2022 06:53:13 +0000 (08:53 +0200)
committerGitHub <noreply@github.com>
Sat, 16 Jul 2022 06:53:13 +0000 (08:53 +0200)
* Implement playlist_create on filesystem provider

* some cleanup/fixes from previous changes

music_assistant/controllers/music/artists.py
music_assistant/models/music_provider.py
music_assistant/music_providers/filesystem.py
music_assistant/music_providers/qobuz.py
music_assistant/music_providers/spotify.py
music_assistant/music_providers/url.py

index fc241bd97f911f554d90ecd2bf97ec6c4db0513f..bb587b9ccd4a89f8ad539fd10d840266b0d13f17 100644 (file)
@@ -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(
index 6fe44bd570adae3d482c71724f64485f8dcd4cee..2a1ce99fbaa650f9c56a8de3ee338bb52e11a0cf 100644 (file)
@@ -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
index fb02d8f86214879d8c912a783db1a1f9d42b8b4c..7b864aceda759e256e4b1029d12c300d9c15002c 100644 (file)
@@ -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)
index 7a8f926b9d75824ba72dcd4f8abdbad3fdd2bc53..2e68403f2099f7120b653f04e94c1861c8cd0a98 100644 (file)
@@ -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,
index 4e4aa613586285f58265becb1d786c5cc994cf50..57605d576351da2f4bda6bae59b8a61b08e018af 100644 (file)
@@ -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,
index 302366ca054fed3374cc9fd19db25571e5e769db..f08038309fe2d67974ea48e359690d0e8b3237ae 100644 (file)
@@ -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.