Fix Tune-In Radio playback (#292)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 8 May 2022 22:49:55 +0000 (00:49 +0200)
committerGitHub <noreply@github.com>
Sun, 8 May 2022 22:49:55 +0000 (00:49 +0200)
* Fix Tune-In radio playback

* support old style presets

music_assistant/controllers/music/radio.py
music_assistant/helpers/cache.py
music_assistant/helpers/database.py
music_assistant/models/media_controller.py
music_assistant/providers/tunein.py

index a036fdc331373d4f97e0cbe3fb361b482b88c59b..63ab65190abdec23f8f5257853453f1bc10805d6 100644 (file)
@@ -38,7 +38,7 @@ class RadioController(MediaControllerBase[Radio]):
             radio.sort_name = create_sort_name(radio.name)
         assert radio.provider_ids
         async with self.mass.database.get_db() as _db:
-            match = {"sort_name": radio.sort_name}
+            match = {"name": radio.name}
             if cur_item := await self.mass.database.get_row(
                 self.db_table, match, db=_db
             ):
index f102f1a0f6cdecd85b924b14782a50d68d331ee5..fff87ef1f0516b2d15a96a1e853e894fe3ea4f96 100644 (file)
@@ -80,6 +80,9 @@ class Cache:
         checksum = self._get_checksum(checksum)
         expires = int(time.time() + expiration)
         self._mem_cache[cache_key] = (data, checksum, expires)
+        if (time.time() - expires) < 3600 * 4:
+            # do not cache items in db with short expiration
+            return
         data = await asyncio.get_running_loop().run_in_executor(None, json.dumps, data)
         await self.mass.database.insert_or_replace(
             DB_TABLE,
index b2066cb88f695d7cfc2277d0c020113526a851d9..49db8b8e3f95ade2a62819212cd89c8d5218a49e 100755 (executable)
@@ -11,7 +11,7 @@ from music_assistant.helpers.typing import MusicAssistant
 
 # pylint: disable=invalid-name
 
-SCHEMA_VERSION = 5
+SCHEMA_VERSION = 6
 
 TABLE_PROV_MAPPINGS = "provider_mappings"
 TABLE_TRACK_LOUDNESS = "track_loudness"
@@ -71,7 +71,7 @@ class Database:
         table: str,
         match: dict = None,
         db: Optional[Db] = None,
-    ) -> List[Mapping]:
+    ) -> int:
         """Get row count for given table/query."""
         async with self.get_db(db) as _db:
             sql_query = f"SELECT count() FROM {table}"
@@ -197,6 +197,13 @@ class Database:
                     # delete player_settings table: use generic settings table instead
                     await db.execute("DROP TABLE IF EXISTS queue_settings")
 
+                if prev_version < 6:
+                    # recreate radio items due to some changes
+                    await db.execute(f"DROP TABLE IF EXISTS {TABLE_RADIOS}")
+                    match = {"media_type": "radio"}
+                    if await self.get_count(TABLE_PROV_MAPPINGS, match):
+                        await self.delete(TABLE_PROV_MAPPINGS, match, db=db)
+
             # create db tables
             await self.__create_database_tables(db)
             # store current schema version
index 007cd7be73a13daf8f5f67b3c2ff97e0d4617b3c..bc20ce479638500d53d78883f01143f064a8935a 100644 (file)
@@ -172,11 +172,14 @@ class MediaControllerBase(Generic[ItemCls], metaclass=ABCMeta):
     async def get_provider_item(self, item_id: str, provider_id: str) -> ItemCls:
         """Return item details for the given provider item id."""
         if provider_id == "database":
-            return await self.get_db_item(item_id)
-        provider = self.mass.music.get_provider(provider_id)
-        if not provider:
-            raise ProviderUnavailableError(f"Provider {provider_id} is not available!")
-        item = await provider.get_item(self.media_type, item_id)
+            item = await self.get_db_item(item_id)
+        else:
+            provider = self.mass.music.get_provider(provider_id)
+            if not provider:
+                raise ProviderUnavailableError(
+                    f"Provider {provider_id} is not available!"
+                )
+            item = await provider.get_item(self.media_type, item_id)
         if not item:
             raise MediaNotFoundError(
                 f"{self.media_type.value} {item_id} not found on provider {provider_id}"
index d8a3726bf3ef6d0fcba5747bacaf8fc609904417..a79aa7125b74ee28c3d3a3d16531bb8da14f60a7 100644 (file)
@@ -6,6 +6,7 @@ from typing import List, Optional
 from asyncio_throttle import Throttler
 
 from music_assistant.helpers.cache import use_cache
+from music_assistant.helpers.util import create_sort_name
 from music_assistant.models.media_items import (
     ContentType,
     ImageType,
@@ -53,37 +54,41 @@ class TuneInProvider(MusicProvider):
     async def get_library_radios(self) -> List[Radio]:
         """Retrieve library/subscribed radio stations from the provider."""
 
-        async def parse_api_response(resp: dict, folder: str = None) -> List[Radio]:
+        async def parse_items(items: List[dict], folder: str = None) -> List[Radio]:
             result = []
-            if not resp or "body" not in resp:
-                return result
-            for item in resp["body"]:
+            for item in items:
                 item_type = item.get("type", "")
                 if item_type == "audio":
+                    if "preset_id" not in item:
+                        continue
                     # each radio station can have multiple streams add each one as different quality
-                    stream_info = await self._get_data(
+                    stream_info = await self.__get_data(
                         "Tune.ashx", id=item["preset_id"]
                     )
                     for stream in stream_info["body"]:
                         result.append(await self._parse_radio(item, stream, folder))
                 elif item_type == "link":
-                    # stations are in sublevel
-                    sublevel = await self._get_data(item["URL"], render="json")
-                    result += await parse_api_response(sublevel, item["text"])
+                    # stations are in sublevel (new style)
+                    if sublevel := await self.__get_data(item["URL"], render="json"):
+                        result += await parse_items(sublevel["body"], item["text"])
+                elif item.get("children"):
+                    # stations are in sublevel (old style ?)
+                    result += await parse_items(item["children"], item["text"])
             return result
 
-        data = await self._get_data("Browse.ashx", c="presets")
-        items = await parse_api_response(data)
-        return items
+        data = await self.__get_data("Browse.ashx", c="presets")
+        if data and "body" in data:
+            return await parse_items(data["body"])
+        return []
 
     async def get_radio(self, prov_radio_id: str) -> Radio:
         """Get radio station details."""
         prov_radio_id, media_type = prov_radio_id.split("--", 1)
         params = {"c": "composite", "detail": "listing", "id": prov_radio_id}
-        result = await self._get_data("Describe.ashx", **params)
+        result = await self.__get_data("Describe.ashx", **params)
         if result and result.get("body") and result["body"][0].get("children"):
             item = result["body"][0]["children"][0]
-            stream_info = await self._get_data("Tune.ashx", id=prov_radio_id)
+            stream_info = await self.__get_data("Tune.ashx", id=prov_radio_id)
             for stream in stream_info["body"]:
                 if stream["media_type"] != media_type:
                     continue
@@ -118,11 +123,15 @@ class TuneInProvider(MusicProvider):
                 details=stream["url"],
             )
         )
-        if folder:
+        # preset number is used for sorting (not present at stream time)
+        preset_number = details.get("preset_number")
+        if preset_number and folder:
             radio.sort_name = f'{folder}-{details["preset_number"]}'
-        else:
+        elif preset_number:
             radio.sort_name = details["preset_number"]
-        radio.metadata.description = details["text"]
+        radio.sort_name += create_sort_name(name)
+        if "text" in details:
+            radio.metadata.description = details["text"]
         # images
         if img := details.get("image"):
             radio.metadata.images = {MediaItemImage(ImageType.THUMB, img)}
@@ -133,7 +142,7 @@ class TuneInProvider(MusicProvider):
     async def get_stream_details(self, item_id: str) -> StreamDetails:
         """Get streamdetails for a radio station."""
         item_id, media_type = item_id.split("--", 1)
-        stream_info = await self._get_data("Tune.ashx", id=item_id)
+        stream_info = await self.__get_data("Tune.ashx", id=item_id)
         for stream in stream_info["body"]:
             if stream["media_type"] == media_type:
                 return StreamDetails(
@@ -150,7 +159,7 @@ class TuneInProvider(MusicProvider):
         return None
 
     @use_cache(3600 * 2)
-    async def _get_data(self, endpoint: str, **kwargs):
+    async def __get_data(self, endpoint: str, **kwargs):
         """Get data from api."""
         if endpoint.startswith("http"):
             url = endpoint