From 9d0b9bca625bba088c7bccfcc31baf03194f5564 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 15 Jul 2022 23:10:53 +0200 Subject: [PATCH] Add capabilities attributes/properties for MusicProviders (#423) * Add capability attributes to MusicProvider model * implement on existing providers Co-authored-by: Marvin Schenkel --- music_assistant/controllers/music/albums.py | 4 +- music_assistant/controllers/music/artists.py | 4 +- music_assistant/controllers/music/tracks.py | 4 +- music_assistant/models/enums.py | 27 ++++ music_assistant/models/music_provider.py | 151 +++++++++++++----- music_assistant/music_providers/filesystem.py | 29 +++- music_assistant/music_providers/qobuz.py | 31 +++- music_assistant/music_providers/spotify.py | 32 ++-- music_assistant/music_providers/tunein.py | 28 ++-- music_assistant/music_providers/url.py | 17 +- .../music_providers/ytmusic/ytmusic.py | 24 ++- 11 files changed, 243 insertions(+), 108 deletions(-) diff --git a/music_assistant/controllers/music/albums.py b/music_assistant/controllers/music/albums.py index 58997f46..c9b87b9c 100644 --- a/music_assistant/controllers/music/albums.py +++ b/music_assistant/controllers/music/albums.py @@ -9,7 +9,7 @@ from music_assistant.helpers.compare import compare_album, compare_artist from music_assistant.helpers.database import TABLE_ALBUMS, TABLE_TRACKS from music_assistant.helpers.json import json_serializer from music_assistant.helpers.tags import FALLBACK_ARTIST -from music_assistant.models.enums import ProviderType +from music_assistant.models.enums import MusicProviderFeature, ProviderType from music_assistant.models.media_controller import MediaControllerBase from music_assistant.models.media_items import ( Album, @@ -281,7 +281,7 @@ class AlbumsController(MediaControllerBase[Album]): for provider in self.mass.music.providers: if provider.type in cur_prov_types: continue - if MediaType.ALBUM not in provider.supported_mediatypes: + if MusicProviderFeature.SEARCH not in provider.supported_features: continue if await find_prov_match(provider): cur_prov_types.add(provider.type) diff --git a/music_assistant/controllers/music/artists.py b/music_assistant/controllers/music/artists.py index 0646b807..fc241bd9 100644 --- a/music_assistant/controllers/music/artists.py +++ b/music_assistant/controllers/music/artists.py @@ -7,7 +7,7 @@ from typing import Any, Dict, List, Optional from music_assistant.helpers.database import TABLE_ALBUMS, TABLE_ARTISTS, TABLE_TRACKS from music_assistant.helpers.json import json_serializer -from music_assistant.models.enums import ProviderType +from music_assistant.models.enums import MusicProviderFeature, ProviderType from music_assistant.models.media_controller import MediaControllerBase from music_assistant.models.media_items import ( Album, @@ -111,7 +111,7 @@ class ArtistsController(MediaControllerBase[Artist]): for provider in self.mass.music.providers: if provider.type in cur_prov_types: continue - if MediaType.ARTIST not in provider.supported_mediatypes: + if MusicProviderFeature.SEARCH not in provider.supported_features: continue if await self._match(db_artist, provider): cur_prov_types.add(provider.type) diff --git a/music_assistant/controllers/music/tracks.py b/music_assistant/controllers/music/tracks.py index ecf31984..d90a30a6 100644 --- a/music_assistant/controllers/music/tracks.py +++ b/music_assistant/controllers/music/tracks.py @@ -7,7 +7,7 @@ from typing import List, Optional, Union from music_assistant.helpers.compare import compare_artists, compare_track from music_assistant.helpers.database import TABLE_TRACKS from music_assistant.helpers.json import json_serializer -from music_assistant.models.enums import MediaType, ProviderType +from music_assistant.models.enums import MediaType, MusicProviderFeature, ProviderType from music_assistant.models.media_controller import MediaControllerBase from music_assistant.models.media_items import ( Album, @@ -83,7 +83,7 @@ class TracksController(MediaControllerBase[Track]): # matching only works if we have a full track object db_track = await self.get_db_item(db_track.item_id) for provider in self.mass.music.providers: - if MediaType.TRACK not in provider.supported_mediatypes: + if MusicProviderFeature.SEARCH not in provider.supported_features: continue self.logger.debug( "Trying to match track %s on provider %s", db_track.name, provider.name diff --git a/music_assistant/models/enums.py b/music_assistant/models/enums.py index 3e7b91e1..00a3d918 100644 --- a/music_assistant/models/enums.py +++ b/music_assistant/models/enums.py @@ -221,6 +221,33 @@ class JobStatus(Enum): ERROR = "error" +class MusicProviderFeature(Enum): + """Enum with features for a MusicProvider.""" + + # browse/explore/recommendations + BROWSE = "browse" + SEARCH = "search" + RECOMMENDATIONS = "recommendations" + # library feature per mediatype + LIBRARY_ARTISTS = "library_artists" + LIBRARY_ALBUMS = "library_albums" + LIBRARY_TRACKS = "library_tracks" + LIBRARY_PLAYLISTS = "library_playlists" + LIBRARY_RADIOS = "library_radios" + # additional library features + ARTIST_ALBUMS = "artist_albums" + ARTIST_TOPTRACKS = "artist_toptracks" + # library edit (=add/remove) feature per mediatype + LIBRARY_ARTISTS_EDIT = "library_artists_edit" + LIBRARY_ALBUMS_EDIT = "library_albums_edit" + LIBRARY_TRACKS_EDIT = "library_tracks_edit" + LIBRARY_PLAYLISTS_EDIT = "library_playlists_edit" + LIBRARY_RADIOS_EDIT = "library_radios_edit" + # playlist-specific features + PLAYLIST_TRACKS_EDIT = "playlist_tracks_edit" + PLAYLIST_CREATE = "playlist_create" + + class ProviderType(Enum): """Enum with supported music providers.""" diff --git a/music_assistant/models/music_provider.py b/music_assistant/models/music_provider.py index c802d7b8..6fe44bd5 100644 --- a/music_assistant/models/music_provider.py +++ b/music_assistant/models/music_provider.py @@ -5,7 +5,7 @@ from abc import abstractmethod from typing import TYPE_CHECKING, Any, AsyncGenerator, Dict, List, Optional, Tuple from music_assistant.models.config import MusicProviderConfig -from music_assistant.models.enums import MediaType, ProviderType +from music_assistant.models.enums import MediaType, MusicProviderFeature, ProviderType from music_assistant.models.media_items import ( Album, Artist, @@ -27,8 +27,6 @@ class MusicProvider: _attr_name: str = None _attr_type: ProviderType = None _attr_available: bool = True - _attr_supports_browse: bool = True - _attr_supported_mediatypes: List[MediaType] = [] def __init__(self, mass: MusicAssistant, config: MusicProviderConfig) -> None: """Initialize MusicProvider.""" @@ -37,6 +35,11 @@ class MusicProvider: self.logger = mass.logger self.cache = mass.cache + @property + def supported_features(self) -> Tuple[MusicProviderFeature]: + """Return the features supported by this MusicProvider.""" + return tuple() + @abstractmethod async def setup(self) -> bool: """ @@ -63,16 +66,6 @@ class MusicProvider: """Return boolean if this provider is available/initialized.""" return self._attr_available - @property - def supports_browse(self) -> bool: - """Return boolean if this provider supports browsing.""" - return self._attr_supports_browse - - @property - def supported_mediatypes(self) -> List[MediaType]: - """Return MediaTypes the provider supports.""" - return self._attr_supported_mediatypes - async def search( self, search_query: str, media_types=Optional[List[MediaType]], limit: int = 5 ) -> List[MediaItemType]: @@ -83,31 +76,32 @@ class MusicProvider: :param media_types: A list of media_types to include. All types if None. :param limit: Number of items to return in the search (per type). """ - raise NotImplementedError + if MusicProviderFeature.SEARCH in self.supported_features: + raise NotImplementedError async def get_library_artists(self) -> AsyncGenerator[Artist, None]: """Retrieve library artists from the provider.""" - if MediaType.ARTIST in self.supported_mediatypes: + if MusicProviderFeature.LIBRARY_ARTISTS in self.supported_features: raise NotImplementedError async def get_library_albums(self) -> AsyncGenerator[Album, None]: """Retrieve library albums from the provider.""" - if MediaType.ALBUM in self.supported_mediatypes: + if MusicProviderFeature.LIBRARY_ALBUMS in self.supported_features: raise NotImplementedError async def get_library_tracks(self) -> AsyncGenerator[Track, None]: """Retrieve library tracks from the provider.""" - if MediaType.TRACK in self.supported_mediatypes: + if MusicProviderFeature.LIBRARY_TRACKS in self.supported_features: raise NotImplementedError async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: """Retrieve library/subscribed playlists from the provider.""" - if MediaType.PLAYLIST in self.supported_mediatypes: + if MusicProviderFeature.LIBRARY_PLAYLISTS in self.supported_features: raise NotImplementedError async def get_library_radios(self) -> AsyncGenerator[Radio, None]: """Retrieve library/subscribed radio stations from the provider.""" - if MediaType.RADIO in self.supported_mediatypes: + if MusicProviderFeature.LIBRARY_RADIOS in self.supported_features: raise NotImplementedError async def get_artist(self, prov_artist_id: str) -> Artist: @@ -116,13 +110,13 @@ class MusicProvider: async def get_artist_albums(self, prov_artist_id: str) -> List[Album]: """Get a list of all albums for the given artist.""" - if MediaType.ALBUM in self.supported_mediatypes: + if MusicProviderFeature.ARTIST_ALBUMS in self.supported_features: raise NotImplementedError return [] async def get_artist_toptracks(self, prov_artist_id: str) -> List[Track]: """Get a list of most popular tracks for the given artist.""" - if MediaType.TRACK in self.supported_mediatypes: + if MusicProviderFeature.ARTIST_TOPTRACKS in self.supported_features: raise NotImplementedError return [] @@ -140,41 +134,94 @@ class MusicProvider: async def get_radio(self, prov_radio_id: str) -> Radio: """Get full radio details by id.""" - if MediaType.RADIO in self.supported_mediatypes: - raise NotImplementedError + raise NotImplementedError async def get_album_tracks(self, prov_album_id: str) -> List[Track]: """Get album tracks for given album id.""" - if MediaType.ALBUM in self.supported_mediatypes: - raise NotImplementedError - return [] + raise NotImplementedError async def get_playlist_tracks(self, prov_playlist_id: str) -> List[Track]: """Get all playlist tracks for given playlist id.""" - if MediaType.PLAYLIST in self.supported_mediatypes: - raise NotImplementedError - return [] + raise NotImplementedError async def library_add(self, prov_item_id: str, media_type: MediaType) -> bool: """Add item to provider's library. Return true on succes.""" - return True + if ( + media_type == MediaType.ARTIST + and MusicProviderFeature.LIBRARY_ARTISTS_EDIT in self.supported_features + ): + raise NotImplementedError + if ( + media_type == MediaType.ALBUM + and MusicProviderFeature.LIBRARY_ALBUMS_EDIT in self.supported_features + ): + raise NotImplementedError + if ( + media_type == MediaType.TRACK + and MusicProviderFeature.LIBRARY_TRACKS_EDIT in self.supported_features + ): + raise NotImplementedError + if ( + media_type == MediaType.PLAYLIST + and MusicProviderFeature.LIBRARY_PLAYLISTS_EDIT in self.supported_features + ): + raise NotImplementedError + if ( + media_type == MediaType.RADIO + and MusicProviderFeature.LIBRARY_RADIOS_EDIT in self.supported_features + ): + raise NotImplementedError + self.logger.info( + "Provider %s does not support library edit, " + "the action will only be performed in the local database.", + self.type.value, + ) async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool: """Remove item from provider's library. Return true on succes.""" - return True + if ( + media_type == MediaType.ARTIST + and MusicProviderFeature.LIBRARY_ARTISTS_EDIT in self.supported_features + ): + raise NotImplementedError + if ( + media_type == MediaType.ALBUM + and MusicProviderFeature.LIBRARY_ALBUMS_EDIT in self.supported_features + ): + raise NotImplementedError + if ( + media_type == MediaType.TRACK + and MusicProviderFeature.LIBRARY_TRACKS_EDIT in self.supported_features + ): + raise NotImplementedError + if ( + media_type == MediaType.PLAYLIST + and MusicProviderFeature.LIBRARY_PLAYLISTS_EDIT in self.supported_features + ): + raise NotImplementedError + if ( + media_type == MediaType.RADIO + and MusicProviderFeature.LIBRARY_RADIOS_EDIT in self.supported_features + ): + raise NotImplementedError + self.logger.info( + "Provider %s does not support library edit, " + "the action will only be performed in the local database.", + self.type.value, + ) async def add_playlist_tracks( self, prov_playlist_id: str, prov_track_ids: List[str] ) -> None: """Add track(s) to playlist.""" - if MediaType.PLAYLIST in self.supported_mediatypes: + if MusicProviderFeature.PLAYLIST_TRACKS_EDIT in self.supported_features: raise NotImplementedError async def remove_playlist_tracks( self, prov_playlist_id: str, prov_track_ids: List[str] ) -> None: """Remove track(s) from playlist.""" - if MediaType.PLAYLIST in self.supported_mediatypes: + if MusicProviderFeature.PLAYLIST_TRACKS_EDIT in self.supported_features: raise NotImplementedError async def get_stream_details(self, item_id: str) -> StreamDetails | None: @@ -205,11 +252,14 @@ class MusicProvider: :param path: The path to browse, (e.g. artists) or None for root level. """ + if MusicProviderFeature.BROWSE not in self.supported_features: + # we may NOT use the default implementation if the browser does not support browse + raise NotImplementedError # this reference implementation can be overridden with provider specific approach if not path: # return main listing root_items = [] - if MediaType.ARTIST in self.supported_mediatypes: + if MusicProviderFeature.LIBRARY_ARTISTS in self.supported_features: root_items.append( BrowseFolder( item_id="artists", @@ -219,7 +269,7 @@ class MusicProvider: uri=f"{self.type.value}://artists", ) ) - if MediaType.ALBUM in self.supported_mediatypes: + if MusicProviderFeature.LIBRARY_ALBUMS in self.supported_features: root_items.append( BrowseFolder( item_id="albums", @@ -229,7 +279,7 @@ class MusicProvider: uri=f"{self.type.value}://albums", ) ) - if MediaType.TRACK in self.supported_mediatypes: + if MusicProviderFeature.LIBRARY_TRACKS in self.supported_features: root_items.append( BrowseFolder( item_id="tracks", @@ -239,7 +289,7 @@ class MusicProvider: uri=f"{self.type.value}://tracks", ) ) - if MediaType.PLAYLIST in self.supported_mediatypes: + if MusicProviderFeature.LIBRARY_PLAYLISTS in self.supported_features: root_items.append( BrowseFolder( item_id="playlists", @@ -249,7 +299,7 @@ class MusicProvider: uri=f"{self.type.value}://playlists", ) ) - if MediaType.RADIO in self.supported_mediatypes: + if MusicProviderFeature.LIBRARY_RADIOS in self.supported_features: root_items.append( BrowseFolder( item_id="radios", @@ -272,14 +322,14 @@ class MusicProvider: if path == "playlists": return [x async for x in self.get_library_playlists()] - @abstractmethod async def recommendations(self) -> List[BrowseFolder]: """ Get this provider's recommendations. Returns a list of BrowseFolder items with (max 25) mediaitems in the items attribute. """ - return [] + if MusicProviderFeature.RECOMMENDATIONS in self.supported_features: + raise NotImplementedError async def sync_library( self, media_types: Optional[Tuple[MediaType]] = None @@ -289,8 +339,10 @@ class MusicProvider: # this logic is aimed at streaming/online providers, # which all have more or less the same structure. # filesystem implementation(s) just override this. - for media_type in self.supported_mediatypes: - if media_types is not None and media_type not in media_types: + if media_types is None: + media_types = (x for x in MediaType) + for media_type in media_types: + if not self.library_supported(media_type): continue self.logger.debug("Start sync of %s items.", media_type.value) controller = self.mass.music.get_controller(media_type) @@ -342,9 +394,22 @@ class MusicProvider: "type": self.type.value, "name": self.name, "id": self.id, - "supported_mediatypes": [x.value for x in self.supported_mediatypes], + "supported_features": [x.value for x in self.supported_features], } + def library_supported(self, media_type: MediaType) -> bool: + """Return if Library is upported for given MediaType on this provider.""" + if media_type == MediaType.ARTIST: + return MusicProviderFeature.LIBRARY_ARTISTS in self.supported_features + if media_type == MediaType.ALBUM: + return MusicProviderFeature.LIBRARY_ALBUMS in self.supported_features + if media_type == MediaType.TRACK: + return MusicProviderFeature.LIBRARY_TRACKS in self.supported_features + if media_type == MediaType.PLAYLIST: + return MusicProviderFeature.LIBRARY_PLAYLISTS in self.supported_features + if media_type == MediaType.RADIO: + return MusicProviderFeature.LIBRARY_RADIOS in self.supported_features + def _get_library_gen(self, media_type: MediaType) -> AsyncGenerator[MediaItemType]: """Return library generator for given media_type.""" if media_type == MediaType.ARTIST: diff --git a/music_assistant/music_providers/filesystem.py b/music_assistant/music_providers/filesystem.py index b492a711..fb02d8f8 100644 --- a/music_assistant/music_providers/filesystem.py +++ b/music_assistant/music_providers/filesystem.py @@ -18,7 +18,7 @@ from aiofiles.threadpool.binary import AsyncFileIO from music_assistant.helpers.compare import compare_strings from music_assistant.helpers.tags import FALLBACK_ARTIST, parse_tags, split_items from music_assistant.helpers.util import create_safe_string, parse_title_and_version -from music_assistant.models.enums import ProviderType +from music_assistant.models.enums import MusicProviderFeature, ProviderType from music_assistant.models.errors import MediaNotFoundError, MusicAssistantError from music_assistant.models.media_items import ( Album, @@ -92,12 +92,27 @@ class FileSystemProvider(MusicProvider): _attr_name = "Filesystem" _attr_type = ProviderType.FILESYSTEM_LOCAL - _attr_supported_mediatypes = [ - MediaType.TRACK, - MediaType.PLAYLIST, - MediaType.ARTIST, - MediaType.ALBUM, - ] + + @property + def supported_features(self) -> Tuple[MusicProviderFeature]: + """Return the features supported by this MusicProvider.""" + return ( + MusicProviderFeature.LIBRARY_ARTISTS, + MusicProviderFeature.LIBRARY_ALBUMS, + MusicProviderFeature.LIBRARY_TRACKS, + MusicProviderFeature.LIBRARY_PLAYLISTS, + MusicProviderFeature.LIBRARY_RADIOS, + MusicProviderFeature.LIBRARY_ARTISTS_EDIT, + MusicProviderFeature.LIBRARY_ALBUMS_EDIT, + MusicProviderFeature.LIBRARY_PLAYLISTS_EDIT, + MusicProviderFeature.LIBRARY_RADIOS_EDIT, + MusicProviderFeature.LIBRARY_TRACKS_EDIT, + MusicProviderFeature.PLAYLIST_TRACKS_EDIT, + MusicProviderFeature.BROWSE, + MusicProviderFeature.SEARCH, + MusicProviderFeature.ARTIST_ALBUMS, + MusicProviderFeature.ARTIST_TOPTRACKS, + ) async def setup(self) -> bool: """Handle async initialization of the provider.""" diff --git a/music_assistant/music_providers/qobuz.py b/music_assistant/music_providers/qobuz.py index ae6a18e0..7a8f926b 100644 --- a/music_assistant/music_providers/qobuz.py +++ b/music_assistant/music_providers/qobuz.py @@ -5,7 +5,7 @@ import datetime import hashlib import time from json import JSONDecodeError -from typing import AsyncGenerator, List, Optional +from typing import AsyncGenerator, List, Optional, Tuple import aiohttp from asyncio_throttle import Throttler @@ -14,7 +14,7 @@ from music_assistant.helpers.app_vars import ( # pylint: disable=no-name-in-mod app_var, ) from music_assistant.helpers.util import parse_title_and_version, try_parse_int -from music_assistant.models.enums import ProviderType +from music_assistant.models.enums import MusicProviderFeature, ProviderType from music_assistant.models.errors import LoginFailed, MediaNotFoundError from music_assistant.models.media_items import ( Album, @@ -39,15 +39,30 @@ class QobuzProvider(MusicProvider): _attr_type = ProviderType.QOBUZ _attr_name = "Qobuz" - _attr_supported_mediatypes = [ - MediaType.ARTIST, - MediaType.ALBUM, - MediaType.TRACK, - MediaType.PLAYLIST, - ] _user_auth_info = None _throttler = Throttler(rate_limit=4, period=1) + @property + def supported_features(self) -> Tuple[MusicProviderFeature]: + """Return the features supported by this MusicProvider.""" + return ( + MusicProviderFeature.LIBRARY_ARTISTS, + MusicProviderFeature.LIBRARY_ALBUMS, + MusicProviderFeature.LIBRARY_TRACKS, + MusicProviderFeature.LIBRARY_PLAYLISTS, + MusicProviderFeature.LIBRARY_RADIOS, + MusicProviderFeature.LIBRARY_ARTISTS_EDIT, + MusicProviderFeature.LIBRARY_ALBUMS_EDIT, + MusicProviderFeature.LIBRARY_PLAYLISTS_EDIT, + MusicProviderFeature.LIBRARY_RADIOS_EDIT, + MusicProviderFeature.LIBRARY_TRACKS_EDIT, + MusicProviderFeature.PLAYLIST_TRACKS_EDIT, + MusicProviderFeature.BROWSE, + MusicProviderFeature.SEARCH, + MusicProviderFeature.ARTIST_ALBUMS, + MusicProviderFeature.ARTIST_TOPTRACKS, + ) + async def setup(self) -> bool: """Handle async initialization of the provider.""" if not self.config.enabled: diff --git a/music_assistant/music_providers/spotify.py b/music_assistant/music_providers/spotify.py index b3d9011f..4e4aa613 100644 --- a/music_assistant/music_providers/spotify.py +++ b/music_assistant/music_providers/spotify.py @@ -8,7 +8,7 @@ import platform import time from json.decoder import JSONDecodeError from tempfile import gettempdir -from typing import AsyncGenerator, List, Optional +from typing import AsyncGenerator, List, Optional, Tuple import aiohttp from asyncio_throttle import Throttler @@ -18,7 +18,7 @@ from music_assistant.helpers.app_vars import ( # noqa # pylint: disable=no-name ) from music_assistant.helpers.process import AsyncProcess from music_assistant.helpers.util import parse_title_and_version -from music_assistant.models.enums import ProviderType +from music_assistant.models.enums import MusicProviderFeature, ProviderType from music_assistant.models.errors import LoginFailed, MediaNotFoundError from music_assistant.models.media_items import ( Album, @@ -45,13 +45,6 @@ class SpotifyProvider(MusicProvider): _attr_type = ProviderType.SPOTIFY _attr_name = "Spotify" - _attr_supported_mediatypes = [ - MediaType.ARTIST, - MediaType.ALBUM, - MediaType.TRACK, - MediaType.PLAYLIST - # TODO: Return spotify radio - ] _auth_token = None _sp_user = None _librespot_bin = None @@ -59,6 +52,27 @@ class SpotifyProvider(MusicProvider): _cache_dir = CACHE_DIR _ap_workaround = False + @property + def supported_features(self) -> Tuple[MusicProviderFeature]: + """Return the features supported by this MusicProvider.""" + return ( + MusicProviderFeature.LIBRARY_ARTISTS, + MusicProviderFeature.LIBRARY_ALBUMS, + MusicProviderFeature.LIBRARY_TRACKS, + MusicProviderFeature.LIBRARY_PLAYLISTS, + MusicProviderFeature.LIBRARY_RADIOS, + MusicProviderFeature.LIBRARY_ARTISTS_EDIT, + MusicProviderFeature.LIBRARY_ALBUMS_EDIT, + MusicProviderFeature.LIBRARY_PLAYLISTS_EDIT, + MusicProviderFeature.LIBRARY_RADIOS_EDIT, + MusicProviderFeature.LIBRARY_TRACKS_EDIT, + MusicProviderFeature.PLAYLIST_TRACKS_EDIT, + MusicProviderFeature.BROWSE, + MusicProviderFeature.SEARCH, + MusicProviderFeature.ARTIST_ALBUMS, + MusicProviderFeature.ARTIST_TOPTRACKS, + ) + async def setup(self) -> bool: """Handle async initialization of the provider.""" if not self.config.enabled: diff --git a/music_assistant/music_providers/tunein.py b/music_assistant/music_providers/tunein.py index b28affe5..82f03590 100644 --- a/music_assistant/music_providers/tunein.py +++ b/music_assistant/music_providers/tunein.py @@ -2,21 +2,20 @@ from __future__ import annotations from time import time -from typing import AsyncGenerator, List, Optional +from typing import AsyncGenerator, List, Optional, Tuple from asyncio_throttle import Throttler from music_assistant.helpers.audio import get_radio_stream from music_assistant.helpers.playlists import fetch_playlist from music_assistant.helpers.util import create_sort_name -from music_assistant.models.enums import ProviderType +from music_assistant.models.enums import MusicProviderFeature, ProviderType from music_assistant.models.errors import LoginFailed, MediaNotFoundError from music_assistant.models.media_items import ( ContentType, ImageType, MediaItemImage, MediaItemProviderId, - MediaItemType, MediaQuality, MediaType, Radio, @@ -30,10 +29,16 @@ class TuneInProvider(MusicProvider): _attr_type = ProviderType.TUNEIN _attr_name = "Tune-in Radio" - _attr_supports_browse: bool = False - _attr_supported_mediatypes = [MediaType.RADIO] _throttler = Throttler(rate_limit=1, period=1) + @property + def supported_features(self) -> Tuple[MusicProviderFeature]: + """Return the features supported by this MusicProvider.""" + return ( + MusicProviderFeature.LIBRARY_RADIOS, + MusicProviderFeature.BROWSE, + ) + async def setup(self) -> bool: """Handle async initialization of the provider.""" if not self.config.enabled: @@ -47,19 +52,6 @@ class TuneInProvider(MusicProvider): ) return True - async def search( - self, search_query: str, media_types=Optional[List[MediaType]], limit: int = 5 - ) -> List[MediaItemType]: - """ - Perform search on musicprovider. - - :param search_query: Search query. - :param media_types: A list of media_types to include. All types if None. - :param limit: Number of items to return in the search (per type). - """ - # TODO: search for radio stations - return [] - async def get_library_radios(self) -> AsyncGenerator[Radio, None]: """Retrieve library/subscribed radio stations from the provider.""" diff --git a/music_assistant/music_providers/url.py b/music_assistant/music_providers/url.py index 90a138ca..302366ca 100644 --- a/music_assistant/music_providers/url.py +++ b/music_assistant/music_providers/url.py @@ -2,7 +2,7 @@ from __future__ import annotations import os -from typing import AsyncGenerator, List, Optional, Tuple +from typing import AsyncGenerator, Tuple from music_assistant.helpers.audio import ( get_file_stream, @@ -17,6 +17,7 @@ from music_assistant.models.enums import ( ImageType, MediaQuality, MediaType, + MusicProviderFeature, ProviderType, ) from music_assistant.models.media_items import ( @@ -41,10 +42,14 @@ class URLProvider(MusicProvider): _attr_name: str = "URL" _attr_type: ProviderType = ProviderType.URL _attr_available: bool = True - _attr_supports_browse: bool = False - _attr_supported_mediatypes: List[MediaType] = [] _full_url = {} + @property + def supported_features(self) -> Tuple[MusicProviderFeature]: + """Return the features supported by this MusicProvider.""" + # return empty tuple because we do not really support features directly here + return tuple() + async def setup(self) -> bool: """ Handle async initialization of the provider. @@ -157,12 +162,6 @@ class URLProvider(MusicProvider): await self.mass.cache.set(cache_key, media_info.raw) return (item_id, url, media_info) - async def search( - self, search_query: str, media_types=Optional[List[MediaType]], limit: int = 5 - ) -> List[MediaItemType]: - """Perform search on musicprovider.""" - return [] - async def get_stream_details(self, item_id: str) -> StreamDetails | None: """Get streamdetails for a track/radio.""" item_id, url, media_info = await self._get_media_info(item_id) diff --git a/music_assistant/music_providers/ytmusic/ytmusic.py b/music_assistant/music_providers/ytmusic/ytmusic.py index 1a614f35..df74edd2 100644 --- a/music_assistant/music_providers/ytmusic/ytmusic.py +++ b/music_assistant/music_providers/ytmusic/ytmusic.py @@ -2,13 +2,13 @@ import re from operator import itemgetter from time import time -from typing import AsyncGenerator, Dict, List, Optional +from typing import AsyncGenerator, Dict, List, Optional, Tuple from urllib.parse import unquote import pytube import ytmusicapi -from music_assistant.models.enums import ProviderType +from music_assistant.models.enums import MusicProviderFeature, ProviderType from music_assistant.models.errors import ( InvalidDataError, LoginFailed, @@ -50,18 +50,26 @@ class YoutubeMusicProvider(MusicProvider): _attr_type = ProviderType.YTMUSIC _attr_name = "Youtube Music" - _attr_supported_mediatypes = [ - MediaType.ARTIST, - MediaType.ALBUM, - MediaType.TRACK, - MediaType.PLAYLIST, - ] _headers = None _context = None _cookies = None _signature_timestamp = 0 _cipher = None + @property + def supported_features(self) -> Tuple[MusicProviderFeature]: + """Return the features supported by this MusicProvider.""" + return ( + MusicProviderFeature.LIBRARY_ARTISTS, + MusicProviderFeature.LIBRARY_ALBUMS, + MusicProviderFeature.LIBRARY_TRACKS, + MusicProviderFeature.LIBRARY_PLAYLISTS, + MusicProviderFeature.BROWSE, + MusicProviderFeature.SEARCH, + MusicProviderFeature.ARTIST_ALBUMS, + MusicProviderFeature.ARTIST_TOPTRACKS, + ) + async def setup(self) -> bool: """Set up the YTMusic provider.""" if not self.config.enabled: -- 2.34.1