From: Marcel van der Veldt Date: Sun, 8 May 2022 22:49:55 +0000 (+0200) Subject: Fix Tune-In Radio playback (#292) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=2cb22eb2e106ff8a117c5301d69daf3ab317066c;p=music-assistant-server.git Fix Tune-In Radio playback (#292) * Fix Tune-In radio playback * support old style presets --- diff --git a/music_assistant/controllers/music/radio.py b/music_assistant/controllers/music/radio.py index a036fdc3..63ab6519 100644 --- a/music_assistant/controllers/music/radio.py +++ b/music_assistant/controllers/music/radio.py @@ -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 ): diff --git a/music_assistant/helpers/cache.py b/music_assistant/helpers/cache.py index f102f1a0..fff87ef1 100644 --- a/music_assistant/helpers/cache.py +++ b/music_assistant/helpers/cache.py @@ -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, diff --git a/music_assistant/helpers/database.py b/music_assistant/helpers/database.py index b2066cb8..49db8b8e 100755 --- a/music_assistant/helpers/database.py +++ b/music_assistant/helpers/database.py @@ -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 diff --git a/music_assistant/models/media_controller.py b/music_assistant/models/media_controller.py index 007cd7be..bc20ce47 100644 --- a/music_assistant/models/media_controller.py +++ b/music_assistant/models/media_controller.py @@ -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}" diff --git a/music_assistant/providers/tunein.py b/music_assistant/providers/tunein.py index d8a3726b..a79aa712 100644 --- a/music_assistant/providers/tunein.py +++ b/music_assistant/providers/tunein.py @@ -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