From: sprocket-9 Date: Thu, 11 Apr 2024 14:49:35 +0000 (+0100) Subject: Add support for using streamer provider publicly shareable URLs in search query ... X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=714ee72d0c5e03bb862a0cb89c3f7fc6cdbc79a4;p=music-assistant-server.git Add support for using streamer provider publicly shareable URLs in search query (#1214) --- diff --git a/music_assistant/common/helpers/uri.py b/music_assistant/common/helpers/uri.py index 11a3997c..5ed68e9c 100644 --- a/music_assistant/common/helpers/uri.py +++ b/music_assistant/common/helpers/uri.py @@ -1,12 +1,28 @@ """Helpers for creating/parsing URI's.""" import os +import re from music_assistant.common.models.enums import MediaType -from music_assistant.common.models.errors import MusicAssistantError +from music_assistant.common.models.errors import InvalidProviderID, InvalidProviderURI +base62_length22_id_pattern = re.compile(r"^[a-zA-Z0-9]{22}$") -def parse_uri(uri: str) -> tuple[MediaType, str, str]: + +def valid_base62_length22(item_id) -> bool: + """Validate Spotify style ID.""" + return bool(base62_length22_id_pattern.match(item_id)) + + +def valid_id(provider: str, item_id: str) -> bool: + """Validate Provider ID.""" + if provider == "spotify": + return valid_base62_length22(item_id) + else: + return True + + +def parse_uri(uri: str, validate_id: bool = False) -> tuple[MediaType, str, str]: """Try to parse URI to Mass identifiers. Returns Tuple: MediaType, provider_instance_id_or_domain, item_id @@ -44,7 +60,10 @@ def parse_uri(uri: str) -> tuple[MediaType, str, str]: raise KeyError except (TypeError, AttributeError, ValueError, KeyError) as err: msg = f"Not a valid Music Assistant uri: {uri}" - raise MusicAssistantError(msg) from err + raise InvalidProviderURI(msg) from err + if validate_id and not valid_id(provider_instance_id_or_domain, item_id): + msg = f"Invalid {provider_instance_id_or_domain} ID: {item_id} found in URI: {uri}" + raise InvalidProviderID(msg) return (media_type, provider_instance_id_or_domain, item_id) diff --git a/music_assistant/common/models/errors.py b/music_assistant/common/models/errors.py index 2c864e41..ca8f3bfe 100644 --- a/music_assistant/common/models/errors.py +++ b/music_assistant/common/models/errors.py @@ -92,3 +92,15 @@ class UnplayableMediaError(MusicAssistantError): """Error thrown when a MediaItem cannot be played properly.""" error_code = 13 + + +class InvalidProviderURI(MusicAssistantError): + """Error thrown when a provider URI does not match a known format.""" + + error_code = 14 + + +class InvalidProviderID(MusicAssistantError): + """Error thrown when a provider media item identifier does not match a known format.""" + + error_code = 15 diff --git a/music_assistant/constants.py b/music_assistant/constants.py index 861d558e..fde91282 100644 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -97,3 +97,4 @@ CONFIGURABLE_CORE_CONTROLLERS = ( ) SYNCGROUP_PREFIX: Final[str] = "syncgroup_" VERBOSE_LOG_LEVEL: Final[int] = 5 +PROVIDERS_WITH_SHAREABLE_URLS = ("spotify", "qobuz") diff --git a/music_assistant/server/controllers/music.py b/music_assistant/server/controllers/music.py index d5d2ac8f..49c8ff58 100644 --- a/music_assistant/server/controllers/music.py +++ b/music_assistant/server/controllers/music.py @@ -20,7 +20,12 @@ from music_assistant.common.models.enums import ( ProviderFeature, ProviderType, ) -from music_assistant.common.models.errors import MediaNotFoundError, MusicAssistantError +from music_assistant.common.models.errors import ( + InvalidProviderID, + InvalidProviderURI, + MediaNotFoundError, + MusicAssistantError, +) from music_assistant.common.models.media_items import BrowseFolder, MediaItemType, SearchResults from music_assistant.common.models.provider import SyncTask from music_assistant.common.models.streamdetails import LoudnessMeasurement @@ -36,6 +41,7 @@ from music_assistant.constants import ( DB_TABLE_SETTINGS, DB_TABLE_TRACK_LOUDNESS, DB_TABLE_TRACKS, + PROVIDERS_WITH_SHAREABLE_URLS, ) from music_assistant.server.helpers.api import api_command from music_assistant.server.helpers.database import DatabaseConnection @@ -157,6 +163,39 @@ class MusicController(CoreController): :param media_types: A list of media_types to include. :param limit: number of items to return in the search (per type). """ + # Check if the search query is a streaming provider public shareable URL + try: + media_type, provider_instance_id_or_domain, item_id = parse_uri( + search_query, validate_id=True + ) + except InvalidProviderURI: + pass + except InvalidProviderID as err: + self.logger.warning("%s", str(err)) + return SearchResults() + else: + if provider_instance_id_or_domain in PROVIDERS_WITH_SHAREABLE_URLS: + try: + item = await self.get_item( + media_type=media_type, + item_id=item_id, + provider_instance_id_or_domain=provider_instance_id_or_domain, + ) + except MusicAssistantError as err: + self.logger.warning("%s", str(err)) + return SearchResults() + else: + if media_type == MediaType.ARTIST: + return SearchResults(artists=[item]) + elif media_type == MediaType.ALBUM: + return SearchResults(albums=[item]) + elif media_type == MediaType.TRACK: + return SearchResults(tracks=[item]) + elif media_type == MediaType.PLAYLIST: + return SearchResults(playlists=[item]) + else: + return SearchResults() + # include results from all (unique) music providers results_per_provider: list[SearchResults] = await asyncio.gather( *[