add album/track versions support on api
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sat, 12 Sep 2020 10:43:30 +0000 (12:43 +0200)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Sat, 12 Sep 2020 10:43:30 +0000 (12:43 +0200)
music_assistant/models/player.py
music_assistant/music_manager.py
music_assistant/player_manager.py
music_assistant/web.py

index acd945cf129e4ae071fadbd5fa19c8eabbda3c2e..8a90ef91bcbf04182101c09547b6cca6c862ab0b 100755 (executable)
@@ -54,13 +54,14 @@ class Player:
     should_poll: bool = False
     features: List[PlayerFeature] = field(default_factory=list)
     config_entries: List[ConfigEntry] = field(default_factory=list)
-    updated_at: datetime = datetime.utcnow()  # managed by playermanager!
-    active_queue: str = ""  # managed by playermanager!
-    group_parents: List[str] = field(default_factory=list)  # managed by playermanager!
-    cur_queue_item_id: str = None  # managed by playermanager!
+    # below attributes are handled by the player manager. No need to set/override them.
+    updated_at: datetime = field(default=datetime.utcnow(), init=False)
+    active_queue: str = field(default="", init=False)
+    group_parents: List[str] = field(init=False, default_factory=list)
+    cur_queue_item_id: str = field(default="", init=False)
 
     def __setattr__(self, name, value):
-        """Event when control is updated. Do not override."""
+        """Watch for attribute updates. Do not override."""
         if name == "updated_at":
             # updated at is set by the on_update callback
             # make sure we do not hit an endless loop
index bfefcfa275d97af38bfde77f39ef248995d22c8d..0ee6368b83966d341a5a02ffd9594d05e054f6df 100755 (executable)
@@ -86,7 +86,7 @@ class MusicManager:
     ################ GET MediaItem(s) by id and provider #################
 
     async def async_get_item(
-        self, item_id: str, provider_id: str, media_type: MediaType, lazy: bool = False
+        self, item_id: str, provider_id: str, media_type: MediaType, lazy: bool = True
     ):
         """Get single music item by id and media type."""
         if media_type == MediaType.Artist:
@@ -166,7 +166,7 @@ class MusicManager:
         self,
         item_id: str,
         provider_id: str,
-        lazy: bool = False,
+        lazy: bool = True,
         track_details: Track = None,
         refresh: bool = False,
     ) -> Track:
@@ -240,32 +240,73 @@ class MusicManager:
     ) -> List[Track]:
         """Return album tracks for the given provider album id. Generator."""
         assert item_id and provider_id
-        if provider_id == "database":
+        album = await self.async_get_album(item_id, provider_id)
+        if album.provider == "database":
             # album tracks are not stored in db, we always fetch them (cached) from the provider.
-            db_item = await self.mass.database.async_get_album(item_id)
-            provider_id = db_item.provider_ids[0].provider
-            item_id = db_item.provider_ids[0].item_id
+            provider_id = album.provider_ids[0].provider
+            item_id = album.provider_ids[0].item_id
         provider = self.mass.get_provider(provider_id)
         cache_key = f"{provider_id}.album_tracks.{item_id}"
-        async for item in async_cached_generator(
-            self.cache, cache_key, provider.async_get_album_tracks(item_id)
-        ):
-            if not item:
-                continue
-            assert item.item_id and item.provider
-            db_id = await self.mass.database.async_get_database_id(
-                item.provider, item.item_id, MediaType.Track
-            )
-            if db_id:
-                # return database track instead if we have a match
-                db_item = await self.mass.database.async_get_track(
-                    db_id, fulldata=False
+        async with self.mass.database.db_conn() as db_conn:
+            async for item in async_cached_generator(
+                self.cache, cache_key, provider.async_get_album_tracks(item_id)
+            ):
+                if not item:
+                    continue
+                db_id = await self.mass.database.async_get_database_id(
+                    item.provider, item.item_id, MediaType.Track, db_conn
                 )
-                db_item.disc_number = item.disc_number
-                db_item.track_number = item.track_number
-                yield db_item
-            else:
-                yield item
+                if db_id:
+                    # return database track instead if we have a match
+                    track = await self.mass.database.async_get_track(
+                        db_id, fulldata=False, db_conn=db_conn
+                    )
+                    track.disc_number = item.disc_number
+                    track.track_number = item.track_number
+                else:
+                    track = item
+                if not track.album:
+                    track.album = album
+                yield track
+
+    async def async_get_album_versions(
+        self, item_id: str, provider_id: str
+    ) -> List[Album]:
+        """Return all versions of an album we can find on all providers. Generator."""
+        album = await self.async_get_album(item_id, provider_id)
+        provider_ids = [
+            item.id for item in self.mass.get_providers(ProviderType.MUSIC_PROVIDER)
+        ]
+        search_query = f"{album.artist.name} - {album.name}"
+        for provider_id in provider_ids:
+            provider_result = await self.async_search_provider(
+                search_query, provider_id, [MediaType.Album], 25
+            )
+            for item in provider_result.albums:
+                if compare_strings(item.artist.name, album.artist.name):
+                    yield item
+
+    async def async_get_track_versions(
+        self, item_id: str, provider_id: str
+    ) -> List[Track]:
+        """Return all versions of a track we can find on all providers. Generator."""
+        track = await self.async_get_track(item_id, provider_id)
+        provider_ids = [
+            item.id for item in self.mass.get_providers(ProviderType.MUSIC_PROVIDER)
+        ]
+        search_query = f"{track.artists[0].name} - {track.name}"
+        for provider_id in provider_ids:
+            provider_result = await self.async_search_provider(
+                search_query, provider_id, [MediaType.Track], 25
+            )
+            for item in provider_result.tracks:
+                if not compare_strings(item.name, track.name):
+                    continue
+                for artist in item.artists:
+                    # artist must match
+                    if compare_strings(artist.name, track.artists[0].name):
+                        yield item
+                        break
 
     async def async_get_playlist_tracks(
         self, item_id: str, provider_id: str
@@ -820,7 +861,7 @@ class MusicManager:
             :param limit: number of items to return in the search (per type).
         """
         result = SearchResult([], [], [], [], [])
-        # include results from all music providers, filter out duplicates
+        # include results from all music providers
         provider_ids = ["database"] + [
             item.id for item in self.mass.get_providers(ProviderType.MUSIC_PROVIDER)
         ]
index 94b92ed77b42ecc1b81029594580d2b2e1df779e..1796cd744eb603616b795b45b09010a8b29b87a5 100755 (executable)
@@ -58,7 +58,7 @@ class PlayerManager:
 
     async def async_close(self):
         """Handle stop/shutdown."""
-        for player_queue in self._player_queues.values():
+        for player_queue in list(self._player_queues.values()):
             await player_queue.async_close()
 
     @run_periodic(1)
index d7706fec18ee8c6e17e0676b8474efa920ed35e7..589df63740c344c7d56164e7440a25d10042b04d 100755 (executable)
@@ -323,7 +323,7 @@ class Web:
         """Get full artist details."""
         item_id = request.match_info.get("item_id")
         provider = request.rel_url.query.get("provider")
-        lazy = request.rel_url.query.get("lazy", "false") != "false"
+        lazy = request.rel_url.query.get("lazy", "true") != "false"
         if item_id is None or provider is None:
             return web.Response(text="invalid item or provider", status=501)
         result = await self.mass.music_manager.async_get_artist(
@@ -337,7 +337,7 @@ class Web:
         """Get full album details."""
         item_id = request.match_info.get("item_id")
         provider = request.rel_url.query.get("provider")
-        lazy = request.rel_url.query.get("lazy", "false") != "false"
+        lazy = request.rel_url.query.get("lazy", "true") != "false"
         if item_id is None or provider is None:
             return web.Response(text="invalid item or provider", status=501)
         result = await self.mass.music_manager.async_get_album(
@@ -351,11 +351,11 @@ class Web:
         """Get full track details."""
         item_id = request.match_info.get("item_id")
         provider = request.rel_url.query.get("provider")
-        lazy = request.rel_url.query.get("lazy", "false") != "false"
+        lazy = request.rel_url.query.get("lazy", "true") != "false"
         if item_id is None or provider is None:
             return web.Response(text="invalid item or provider", status=501)
         result = await self.mass.music_manager.async_get_track(
-            item_id, provider, lazy=lazy, refresh=True
+            item_id, provider, lazy=lazy
         )
         return web.json_response(result, dumps=json_serializer)
 
@@ -467,6 +467,28 @@ class Web:
         iterator = self.mass.music_manager.async_get_album_tracks(item_id, provider)
         return await self.__async_stream_json(request, iterator)
 
+    @login_required
+    @routes.get("/api/albums/{item_id}/versions")
+    async def async_album_versions(self, request):
+        """Get all versions of an album."""
+        item_id = request.match_info.get("item_id")
+        provider = request.rel_url.query.get("provider")
+        if item_id is None or provider is None:
+            return web.Response(text="invalid item_id or provider", status=501)
+        iterator = self.mass.music_manager.async_get_album_versions(item_id, provider)
+        return await self.__async_stream_json(request, iterator)
+
+    @login_required
+    @routes.get("/api/tracks/{item_id}/versions")
+    async def async_track_versions(self, request):
+        """Get all versions of an track."""
+        item_id = request.match_info.get("item_id")
+        provider = request.rel_url.query.get("provider")
+        if item_id is None or provider is None:
+            return web.Response(text="invalid item_id or provider", status=501)
+        iterator = self.mass.music_manager.async_get_track_versions(item_id, provider)
+        return await self.__async_stream_json(request, iterator)
+
     @login_required
     @routes.get("/api/search")
     async def async_search(self, request):