Add typing for radiobrowser
authorJohn Carr <john.carr@unrouted.co.uk>
Fri, 5 Jul 2024 13:42:06 +0000 (14:42 +0100)
committerJohn Carr <john.carr@unrouted.co.uk>
Fri, 5 Jul 2024 13:42:06 +0000 (14:42 +0100)
music_assistant/server/controllers/cache.py
music_assistant/server/models/music_provider.py
music_assistant/server/providers/radiobrowser/__init__.py
mypy.ini

index 278c2af39d3be823f1d56e47211b00cad4f64056..7e9028ad0004c1ee65eac131ff6c6f7027bbd749 100644 (file)
@@ -8,8 +8,8 @@ import logging
 import os
 import time
 from collections import OrderedDict
-from collections.abc import Iterator, MutableMapping
-from typing import TYPE_CHECKING, Any
+from collections.abc import Callable, Iterator, MutableMapping
+from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar
 
 from music_assistant.common.helpers.json import json_dumps, json_loads
 from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType
@@ -235,12 +235,18 @@ class CacheController(CoreController):
         self.mass.loop.call_later(3600, self.__schedule_cleanup_task)
 
 
-def use_cache(expiration=86400 * 30):
+Param = ParamSpec("Param")
+RetType = TypeVar("RetType")
+
+
+def use_cache(
+    expiration: int = 86400 * 30,
+) -> Callable[[Callable[Param, RetType]], Callable[Param, RetType]]:
     """Return decorator that can be used to cache a method's result."""
 
-    def wrapper(func):
+    def wrapper(func: Callable[Param, RetType]) -> Callable[Param, RetType]:
         @functools.wraps(func)
-        async def wrapped(*args, **kwargs):
+        async def wrapped(*args: Param.args, **kwargs: Param.kwargs):
             method_class = args[0]
             method_class_name = method_class.__class__.__name__
             cache_key_parts = [method_class_name, func.__name__]
index 1449a6b91eb21a746ee9a96a78839abce1c76214..683342bf79ebede64de021c3f9906fae829c4dbb 100644 (file)
@@ -3,6 +3,7 @@
 from __future__ import annotations
 
 import asyncio
+from collections.abc import Sequence
 from typing import TYPE_CHECKING
 
 from music_assistant.common.models.enums import MediaType, ProviderFeature
@@ -282,7 +283,7 @@ class MusicProvider(Provider):
             return await self.get_radio(prov_item_id)
         return await self.get_track(prov_item_id)
 
-    async def browse(self, path: str, offset: int, limit: int) -> list[MediaItemType]:
+    async def browse(self, path: str, offset: int, limit: int) -> Sequence[MediaItemType]:
         """Browse this provider's items.
 
         :param path: The path to browse, (e.g. provider_id://artists).
index 23c550affe92bb1c2cfadd3b44813c26f4329cdc..50a3ac6368477cc6d6817b4b64137c927d918b19 100644 (file)
@@ -2,11 +2,13 @@
 
 from __future__ import annotations
 
+from collections.abc import Sequence
 from typing import TYPE_CHECKING
 
-from radios import FilterBy, Order, RadioBrowser, RadioBrowserError
+from radios import FilterBy, Order, RadioBrowser, RadioBrowserError, Station
 
 from music_assistant.common.models.enums import LinkType, ProviderFeature, StreamType
+from music_assistant.common.models.errors import MediaNotFoundError
 from music_assistant.common.models.media_items import (
     AudioFormat,
     BrowseFolder,
@@ -19,6 +21,7 @@ from music_assistant.common.models.media_items import (
     ProviderMapping,
     Radio,
     SearchResults,
+    UniqueList,
 )
 from music_assistant.common.models.streamdetails import StreamDetails
 from music_assistant.server.controllers.cache import use_cache
@@ -83,7 +86,7 @@ class RadioBrowserProvider(MusicProvider):
             self.logger.exception("%s", err)
 
     async def search(
-        self, search_query: str, media_types=list[MediaType], limit: int = 10
+        self, search_query: str, media_typeslist[MediaType], limit: int = 10
     ) -> SearchResults:
         """Perform search on musicprovider.
 
@@ -102,7 +105,7 @@ class RadioBrowserProvider(MusicProvider):
 
         return result
 
-    async def browse(self, path: str, offset: int, limit: int) -> list[MediaItemType]:
+    async def browse(self, path: str, offset: int, limit: int) -> Sequence[MediaItemType]:
         """Browse this provider's items.
 
         :param path: The path to browse, (e.g. provid://artists).
@@ -168,14 +171,16 @@ class RadioBrowserProvider(MusicProvider):
                     path=path + "/" + country.code.lower(),
                     name=country.name,
                 )
-                folder.metadata.images = [
-                    MediaItemImage(
-                        type=ImageType.THUMB,
-                        path=country.favicon,
-                        provider=self.instance_id,
-                        remotely_accessible=True,
-                    )
-                ]
+                folder.metadata.images = UniqueList(
+                    [
+                        MediaItemImage(
+                            type=ImageType.THUMB,
+                            path=country.favicon,
+                            provider=self.instance_id,
+                            remotely_accessible=True,
+                        )
+                    ]
+                )
                 items.append(folder)
             return items
 
@@ -187,7 +192,7 @@ class RadioBrowserProvider(MusicProvider):
         return []
 
     @use_cache(3600 * 24)
-    async def get_tag_names(self):
+    async def get_tag_names(self) -> Sequence[str]:
         """Get a list of tag names."""
         tags = await self.radios.tags(
             hide_broken=True,
@@ -202,7 +207,7 @@ class RadioBrowserProvider(MusicProvider):
         return tag_names
 
     @use_cache(3600 * 24)
-    async def get_country_codes(self):
+    async def get_country_codes(self) -> Sequence[str]:
         """Get a list of country names."""
         countries = await self.radios.countries(order=Order.NAME, hide_broken=True)
         country_codes = []
@@ -211,7 +216,7 @@ class RadioBrowserProvider(MusicProvider):
         return country_codes
 
     @use_cache(3600)
-    async def get_by_popularity(self):
+    async def get_by_popularity(self) -> Sequence[Radio]:
         """Get radio stations by popularity."""
         stations = await self.radios.stations(
             hide_broken=True,
@@ -225,7 +230,7 @@ class RadioBrowserProvider(MusicProvider):
         return items
 
     @use_cache(3600)
-    async def get_by_tag(self, tag: str):
+    async def get_by_tag(self, tag: str) -> Sequence[Radio]:
         """Get radio stations by tag."""
         items = []
         stations = await self.radios.stations(
@@ -240,7 +245,7 @@ class RadioBrowserProvider(MusicProvider):
         return items
 
     @use_cache(3600)
-    async def get_by_country(self, country_code: str):
+    async def get_by_country(self, country_code: str) -> list[Radio]:
         """Get radio stations by country."""
         items = []
         stations = await self.radios.stations(
@@ -257,9 +262,11 @@ class RadioBrowserProvider(MusicProvider):
     async def get_radio(self, prov_radio_id: str) -> Radio:
         """Get radio station details."""
         radio = await self.radios.station(uuid=prov_radio_id)
+        if not radio:
+            raise MediaNotFoundError(f"Radio station {prov_radio_id} not found")
         return await self._parse_radio(radio)
 
-    async def _parse_radio(self, radio_obj: dict) -> Radio:
+    async def _parse_radio(self, radio_obj: Station) -> Radio:
         """Parse Radio object from json obj returned from api."""
         radio = Radio(
             item_id=radio_obj.uuid,
@@ -273,23 +280,26 @@ class RadioBrowserProvider(MusicProvider):
                 )
             },
         )
-        radio.metadata.label = radio_obj.tags
         radio.metadata.popularity = radio_obj.votes
-        radio.metadata.links = [MediaItemLink(type=LinkType.WEBSITE, url=radio_obj.homepage)]
-        radio.metadata.images = [
-            MediaItemImage(
-                type=ImageType.THUMB,
-                path=radio_obj.favicon,
-                provider=self.instance_id,
-                remotely_accessible=True,
-            )
-        ]
+        radio.metadata.links = {MediaItemLink(type=LinkType.WEBSITE, url=radio_obj.homepage)}
+        radio.metadata.images = UniqueList(
+            [
+                MediaItemImage(
+                    type=ImageType.THUMB,
+                    path=radio_obj.favicon,
+                    provider=self.instance_id,
+                    remotely_accessible=True,
+                )
+            ]
+        )
 
         return radio
 
     async def get_stream_details(self, item_id: str) -> StreamDetails:
         """Get streamdetails for a radio station."""
         stream = await self.radios.station(uuid=item_id)
+        if not stream:
+            raise MediaNotFoundError(f"Radio station {item_id} not found")
         await self.radios.station_click(uuid=item_id)
         return StreamDetails(
             provider=self.domain,
index 2bca30dae39474e2633fc9890d7c713aa8cbebb5..63d2e50898b517e3212baa8fe3824bb5bf5a7a7e 100644 (file)
--- a/mypy.ini
+++ b/mypy.ini
@@ -21,4 +21,4 @@ disallow_untyped_decorators = true
 disallow_untyped_defs = true
 warn_return_any = true
 warn_unreachable = true
-packages=tests,music_assistant.client,music_assistant.common,music_assistant.server.providers.jellyfin
+packages=tests,music_assistant.client,music_assistant.common,music_assistant.server.providers.jellyfin,music_assistant.server.providers.radiobrowser