From: Marcel van der Veldt Date: Sun, 11 Aug 2024 01:17:09 +0000 (+0200) Subject: Improve metadata handling (#1552) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=04dbea957d688a103fd32ba31035edf44f4f5448;p=music-assistant-server.git Improve metadata handling (#1552) --- diff --git a/music_assistant/common/models/provider.py b/music_assistant/common/models/provider.py index 7b706b6b..d9a9e895 100644 --- a/music_assistant/common/models/provider.py +++ b/music_assistant/common/models/provider.py @@ -31,12 +31,10 @@ class ProviderManifest(DataClassORJSONMixin): documentation: str | None = None # multi_instance: whether multiple instances of the same provider are allowed/possible multi_instance: bool = False - # builtin: whether this provider is a system/builtin and can not disabled/removed + # builtin: whether this provider is a system/builtin provider, loaded by default builtin: bool = False - # hidden: hide entry in the UI - hidden: bool = False - # load_by_default: load this provider by default (may be used together with `builtin`) - load_by_default: bool = False + # allow_disable: whether this provider can be disabled (used with builtin) + allow_disable: bool = True # depends_on: depends on another provider to function depends_on: str | None = None # icon: name of the material design icon (https://pictogrammers.com/library/mdi) diff --git a/music_assistant/server/controllers/config.py b/music_assistant/server/controllers/config.py index 5af24ec3..bd081593 100644 --- a/music_assistant/server/controllers/config.py +++ b/music_assistant/server/controllers/config.py @@ -276,15 +276,6 @@ class ConfigController: msg = f"Provider {instance_id} does not exist" raise KeyError(msg) prov_manifest = self.mass.get_provider_manifest(existing["domain"]) - if prov_manifest.load_by_default and instance_id == prov_manifest.domain: - # Guard for a provider that is loaded by default - LOGGER.warning( - "Provider %s can not be removed, disabling instead...", - prov_manifest.name, - ) - existing["enabled"] = False - await self._update_provider_config(instance_id, existing) - return if prov_manifest.builtin: msg = f"Builtin provider {prov_manifest.name} can not be removed." raise RuntimeError(msg) @@ -756,8 +747,8 @@ class ConfigController: else: # disable provider prov_manifest = self.mass.get_provider_manifest(config.domain) - if prov_manifest.builtin: - msg = "Builtin provider can not be disabled." + if not prov_manifest.allow_disable: + msg = "Provider can not be disabled." raise RuntimeError(msg) # also unload any other providers dependent of this provider for dep_prov in self.mass.providers: diff --git a/music_assistant/server/controllers/metadata.py b/music_assistant/server/controllers/metadata.py index db6f7e1e..84fb4a6b 100644 --- a/music_assistant/server/controllers/metadata.py +++ b/music_assistant/server/controllers/metadata.py @@ -101,12 +101,14 @@ LOCALES = { DEFAULT_LANGUAGE = "en_US" REFRESH_INTERVAL = 60 * 60 * 24 * 90 MAX_ONLINE_CALLS_PER_DAY = 30 +CONF_ENABLE_ONLINE_METADATA = "enable_online_metadata" class MetaDataController(CoreController): """Several helpers to search and store metadata for mediaitems.""" domain: str = "metadata" + config: CoreConfig def __init__(self, *args, **kwargs) -> None: """Initialize class.""" @@ -139,10 +141,26 @@ class MetaDataController(CoreController): "in your preferred language is not available.", options=tuple(ConfigValueOption(value, key) for key, value in LOCALES.items()), ), + ConfigEntry( + key=CONF_ENABLE_ONLINE_METADATA, + type=ConfigEntryType.BOOLEAN, + label="Enable metadata retrieval from online metadata providers", + required=False, + default_value=True, + description="Enable online metadata lookups.\n\n" + "This will allow Music Assistant to fetch additional metadata from (enabled) " + "metadata providers, such as The Audio DB and Fanart.tv.\n\n" + "Note that these online sources are only queried when no information is already " + "available from local files or the music providers.\n\n" + "The retrieval of additional rich metadata is a process that is executed slowly " + "in the background to not overload these free services with requests. " + "You can speedup the process by storing the images and other metadata locally.", + ), ) async def setup(self, config: CoreConfig) -> None: """Async initialize of module.""" + self.config = config if not self.logger.isEnabledFor(VERBOSE_LOG_LEVEL): # silence PIL logger logging.getLogger("PIL").setLevel(logging.WARNING) @@ -219,15 +237,15 @@ class MetaDataController(CoreController): # this shouldn't happen but just in case. raise RuntimeError("Metadata can only be updated for library items") if item.media_type == MediaType.ARTIST: - await self._update_artist_metadata(item) + await self._update_artist_metadata(item, force_refresh=force_refresh) if item.media_type == MediaType.ALBUM: - await self._update_album_metadata(item) + await self._update_album_metadata(item, force_refresh=force_refresh) if item.media_type == MediaType.TRACK: - await self._update_track_metadata(item) + await self._update_track_metadata(item, force_refresh=force_refresh) if item.media_type == MediaType.PLAYLIST: - await self._update_playlist_metadata(item) + await self._update_playlist_metadata(item, force_refresh=force_refresh) if item.media_type == MediaType.RADIO: - await self._update_radio_metadata(item) + await self._update_radio_metadata(item, force_refresh=force_refresh) @api_command("metadata/scan") async def metadata_scanner(self) -> None: @@ -237,39 +255,39 @@ class MetaDataController(CoreController): return self._scanner_running = True try: - timestamp = int(time() - 60 * 60 * 24 * 7) + timestamp = int(time() - 60 * 60 * 24 * 30) query = ( f"WHERE json_extract({DB_TABLE_ARTISTS}.metadata,'$.last_refresh') ISNULL " f"OR json_extract({DB_TABLE_ARTISTS}.metadata,'$.last_refresh') < {timestamp}" ) for artist in await self.mass.music.artists.library_items( - limit=250, order_by="random", extra_query=query + limit=50, order_by="random", extra_query=query ): await self._update_artist_metadata(artist) # we really need to throttle this - await asyncio.sleep(10) + await asyncio.sleep(30) query = ( f"WHERE json_extract({DB_TABLE_ALBUMS}.metadata,'$.last_refresh') ISNULL " f"OR json_extract({DB_TABLE_ALBUMS}.metadata,'$.last_refresh') < {timestamp}" ) for album in await self.mass.music.albums.library_items( - limit=250, order_by="random", extra_query=query + limit=50, order_by="random", extra_query=query ): await self._update_album_metadata(album) # we really need to throttle this - await asyncio.sleep(10) + await asyncio.sleep(30) query = ( f"WHERE json_extract({DB_TABLE_PLAYLISTS}.metadata,'$.last_refresh') ISNULL " f"OR json_extract({DB_TABLE_PLAYLISTS}.metadata,'$.last_refresh') < {timestamp}" ) for playlist in await self.mass.music.playlists.library_items( - limit=250, order_by="random", extra_query=query + limit=50, order_by="random", extra_query=query ): await self._update_playlist_metadata(playlist) # we really need to throttle this - await asyncio.sleep(10) + await asyncio.sleep(30) query = ( f"WHERE json_extract({DB_TABLE_TRACKS}.metadata,'$.last_refresh') ISNULL " @@ -452,9 +470,12 @@ class MetaDataController(CoreController): # to not overload the (free) metadata providers with api calls # TODO: Utilize a global (cloud) cache for metadata lookups to save on API calls - if force_refresh or ( - self._online_slots_available - and ((time() - (artist.metadata.last_refresh or 0)) > REFRESH_INTERVAL) + if self.config.get_value(CONF_ENABLE_ONLINE_METADATA) and ( + force_refresh + or ( + self._online_slots_available + and ((time() - (artist.metadata.last_refresh or 0)) > REFRESH_INTERVAL) + ) ): self._online_slots_available -= 1 # set timestamp, used to determine when this function was last called @@ -506,10 +527,13 @@ class MetaDataController(CoreController): # NOTE: we only allow this every REFRESH_INTERVAL and a max amount of calls per day # to not overload the (free) metadata providers with api calls # TODO: Utilize a global (cloud) cache for metadata lookups to save on API calls - if force_refresh or ( - self._online_slots_available - and ((time() - (album.metadata.last_refresh or 0)) > REFRESH_INTERVAL) - and (album.mbid or album.artists) + if self.config.get_value(CONF_ENABLE_ONLINE_METADATA) and ( + force_refresh + or ( + self._online_slots_available + and ((time() - (album.metadata.last_refresh or 0)) > REFRESH_INTERVAL) + and (album.mbid or album.artists) + ) ): self._online_slots_available -= 1 # set timestamp, used to determine when this function was last called @@ -551,10 +575,13 @@ class MetaDataController(CoreController): # NOTE: we only allow this every REFRESH_INTERVAL and a max amount of calls per day # to not overload the (free) metadata providers with api calls # TODO: Utilize a global (cloud) cache for metadata lookups to save on API calls - if force_refresh or ( - self._online_slots_available - and ((time() - (track.metadata.last_refresh or 0)) > REFRESH_INTERVAL) - and (track.mbid or track.artists or track.album) + if self.config.get_value(CONF_ENABLE_ONLINE_METADATA) and ( + force_refresh + or ( + self._online_slots_available + and ((time() - (track.metadata.last_refresh or 0)) > REFRESH_INTERVAL) + and (track.mbid or track.artists or track.album) + ) ): self._online_slots_available -= 1 # set timestamp, used to determine when this function was last called diff --git a/music_assistant/server/controllers/music.py b/music_assistant/server/controllers/music.py index 069d6882..0dc7864c 100644 --- a/music_assistant/server/controllers/music.py +++ b/music_assistant/server/controllers/music.py @@ -942,7 +942,7 @@ class MusicController(CoreController): return # all other versions: reset the database - # we only migrate from prtev version to current we do not try to handle + # we only migrate from prev version to current, we do not try to handle # more complex migrations self.logger.warning( "Database schema too old - Resetting library/database - " diff --git a/music_assistant/server/helpers/compare.py b/music_assistant/server/helpers/compare.py index ba9afaab..93b12027 100644 --- a/music_assistant/server/helpers/compare.py +++ b/music_assistant/server/helpers/compare.py @@ -387,11 +387,12 @@ def compare_external_ids( return None -def create_safe_string(input_str: str) -> str: +def create_safe_string(input_str: str, lowercase: bool = True, replace_space: bool = False) -> str: """Return clean lowered string for compare actions.""" - input_str = input_str.lower().strip() + input_str = input_str.lower().strip() if lowercase else input_str.strip() unaccented_string = unidecode.unidecode(input_str) - return re.sub(r"[^a-zA-Z0-9]", "", unaccented_string) + regex = r"[^a-zA-Z0-9]" if replace_space else r"[^a-zA-Z0-9 ]" + return re.sub(regex, "", unaccented_string) def loose_compare_strings(base: str, alt: str) -> bool: diff --git a/music_assistant/server/providers/builtin/manifest.json b/music_assistant/server/providers/builtin/manifest.json index 4ca5046e..c66423bd 100644 --- a/music_assistant/server/providers/builtin/manifest.json +++ b/music_assistant/server/providers/builtin/manifest.json @@ -3,12 +3,9 @@ "domain": "builtin", "name": "Music Assistant", "description": "Built-in/generic provider that handles generic urls and playlists.", - "codeowners": [ - "@music-assistant" - ], + "codeowners": ["@music-assistant"], "requirements": [], "documentation": "https://music-assistant.io/music-providers/builtin/", "multi_instance": false, - "builtin": true, - "hidden": false + "builtin": true } diff --git a/music_assistant/server/providers/fanarttv/__init__.py b/music_assistant/server/providers/fanarttv/__init__.py index 2016cdad..2f2b22d4 100644 --- a/music_assistant/server/providers/fanarttv/__init__.py +++ b/music_assistant/server/providers/fanarttv/__init__.py @@ -7,7 +7,8 @@ from typing import TYPE_CHECKING import aiohttp.client_exceptions -from music_assistant.common.models.enums import ExternalID, ProviderFeature +from music_assistant.common.models.config_entries import ConfigEntry +from music_assistant.common.models.enums import ConfigEntryType, ExternalID, ProviderFeature from music_assistant.common.models.media_items import ImageType, MediaItemImage, MediaItemMetadata from music_assistant.server.controllers.cache import use_cache from music_assistant.server.helpers.app_vars import app_var # pylint: disable=no-name-in-module @@ -15,11 +16,7 @@ from music_assistant.server.helpers.throttle_retry import Throttler from music_assistant.server.models.metadata_provider import MetadataProvider if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueType, - ProviderConfig, - ) + from music_assistant.common.models.config_entries import ConfigValueType, ProviderConfig from music_assistant.common.models.media_items import Album, Artist from music_assistant.common.models.provider import ProviderManifest from music_assistant.server import MusicAssistant @@ -30,8 +27,9 @@ SUPPORTED_FEATURES = ( ProviderFeature.ALBUM_METADATA, ) -# TODO: add support for personal api keys ? - +CONF_ENABLE_ARTIST_IMAGES = "enable_artist_images" +CONF_ENABLE_ALBUM_IMAGES = "enable_album_images" +CONF_CLIENT_KEY = "client_key" IMG_MAPPING = { "artistthumb": ImageType.THUMB, @@ -64,7 +62,29 @@ async def get_config_entries( values: the (intermediate) raw values for config entries sent with the action. """ # ruff: noqa: ARG001 - return () # we do not have any config entries (yet) + return ( + ConfigEntry( + key=CONF_ENABLE_ARTIST_IMAGES, + type=ConfigEntryType.BOOLEAN, + label="Enable retrieval of artist images.", + default_value=True, + ), + ConfigEntry( + key=CONF_ENABLE_ALBUM_IMAGES, + type=ConfigEntryType.BOOLEAN, + label="Enable retrieval of album image(s).", + default_value=True, + ), + ConfigEntry( + key=CONF_CLIENT_KEY, + type=ConfigEntryType.SECURE_STRING, + label="VIP Member Personal API Key (optional)", + description="Support this metadata provider by becoming a VIP Member, " + "resulting in higher rate limits and faster response times among other benefits. " + "See https://wiki.fanart.tv/General/personal%20api/ for more information.", + required=False, + ), + ) class FanartTvMetadataProvider(MetadataProvider): @@ -75,7 +95,11 @@ class FanartTvMetadataProvider(MetadataProvider): async def handle_async_init(self) -> None: """Handle async initialization of the provider.""" self.cache = self.mass.cache - self.throttler = Throttler(rate_limit=1, period=30) + if self.config.get_value(CONF_CLIENT_KEY): + # loosen the throttler when a personal client key is used + self.throttler = Throttler(rate_limit=1, period=1) + else: + self.throttler = Throttler(rate_limit=1, period=30) @property def supported_features(self) -> tuple[ProviderFeature, ...]: @@ -86,6 +110,8 @@ class FanartTvMetadataProvider(MetadataProvider): """Retrieve metadata for artist on fanart.tv.""" if not artist.mbid: return None + if not self.config.get_value(CONF_ENABLE_ARTIST_IMAGES): + return None self.logger.debug("Fetching metadata for Artist %s on Fanart.tv", artist.name) if data := await self._get_data(f"music/{artist.mbid}"): metadata = MediaItemMetadata() @@ -110,6 +136,8 @@ class FanartTvMetadataProvider(MetadataProvider): """Retrieve metadata for album on fanart.tv.""" if (mbid := album.get_external_id(ExternalID.MB_RELEASEGROUP)) is None: return None + if not self.config.get_value(CONF_ENABLE_ALBUM_IMAGES): + return None self.logger.debug("Fetching metadata for Album %s on Fanart.tv", album.name) if data := await self._get_data(f"music/albums/{mbid}"): if data and data.get("albums"): @@ -136,10 +164,14 @@ class FanartTvMetadataProvider(MetadataProvider): async def _get_data(self, endpoint, **kwargs) -> dict | None: """Get data from api.""" url = f"http://webservice.fanart.tv/v3/{endpoint}" - kwargs["api_key"] = app_var(4) + headers = { + "api-key": app_var(4), + } + if client_key := self.config.get_value(CONF_CLIENT_KEY): + headers["client_key"] = client_key async with ( self.throttler, - self.mass.http_session.get(url, params=kwargs, ssl=False) as response, + self.mass.http_session.get(url, params=kwargs, headers=headers, ssl=False) as response, ): try: result = await response.json() diff --git a/music_assistant/server/providers/fanarttv/manifest.json b/music_assistant/server/providers/fanarttv/manifest.json index 083e7606..a39b2593 100644 --- a/music_assistant/server/providers/fanarttv/manifest.json +++ b/music_assistant/server/providers/fanarttv/manifest.json @@ -1,11 +1,9 @@ { "type": "metadata", "domain": "fanarttv", - "name": "fanart.tv Metadata provider", + "name": "fanart.tv", "description": "fanart.tv is a community database of artwork for movies, tv series and music.", - "codeowners": [ - "@music-assistant" - ], + "codeowners": ["@music-assistant"], "requirements": [], "documentation": "", "multi_instance": false, diff --git a/music_assistant/server/providers/filesystem_local/base.py b/music_assistant/server/providers/filesystem_local/base.py index 57f40ef0..21b111e1 100644 --- a/music_assistant/server/providers/filesystem_local/base.py +++ b/music_assistant/server/providers/filesystem_local/base.py @@ -49,7 +49,7 @@ from music_assistant.constants import ( DB_TABLE_TRACK_ARTISTS, VARIOUS_ARTISTS_NAME, ) -from music_assistant.server.helpers.compare import compare_strings +from music_assistant.server.helpers.compare import compare_strings, create_safe_string from music_assistant.server.helpers.playlists import parse_m3u, parse_pls from music_assistant.server.helpers.tags import parse_tags, split_items from music_assistant.server.models.music_provider import MusicProvider @@ -894,10 +894,11 @@ class FileSystemProviderBase(MusicProvider): break else: # check if we have an artist folder for this artist at root level + safe_artist_name = create_safe_string(name, lowercase=False, replace_space=False) if await self.exists(name): artist_path = name - elif await self.exists(name.title()): - artist_path = name.title() + elif await self.exists(safe_artist_name): + artist_path = safe_artist_name if artist_path: # noqa: SIM108 # prefer the path as id diff --git a/music_assistant/server/providers/fully_kiosk/manifest.json b/music_assistant/server/providers/fully_kiosk/manifest.json index 39e616de..1059df8e 100644 --- a/music_assistant/server/providers/fully_kiosk/manifest.json +++ b/music_assistant/server/providers/fully_kiosk/manifest.json @@ -7,6 +7,5 @@ "requirements": ["python-fullykiosk==0.0.14"], "documentation": "https://music-assistant.io/player-support/fully-kiosk/", "multi_instance": true, - "builtin": false, - "load_by_default": false + "builtin": false } diff --git a/music_assistant/server/providers/hass/manifest.json b/music_assistant/server/providers/hass/manifest.json index 6e25489e..4fee6af8 100644 --- a/music_assistant/server/providers/hass/manifest.json +++ b/music_assistant/server/providers/hass/manifest.json @@ -7,7 +7,6 @@ "documentation": "", "multi_instance": false, "builtin": false, - "load_by_default": false, "icon": "md:webhook", "requirements": ["hass-client==1.2.0"] } diff --git a/music_assistant/server/providers/hass_players/manifest.json b/music_assistant/server/providers/hass_players/manifest.json index 30ab0068..3c0fae3e 100644 --- a/music_assistant/server/providers/hass_players/manifest.json +++ b/music_assistant/server/providers/hass_players/manifest.json @@ -3,13 +3,10 @@ "domain": "hass_players", "name": "Home Assistant MediaPlayers", "description": "Use (supported) Home Assistant media players as players in Music Assistant.", - "codeowners": [ - "@music-assistant" - ], + "codeowners": ["@music-assistant"], "documentation": "https://music-assistant.io/player-support/ha/", "multi_instance": false, "builtin": false, - "load_by_default": false, "icon": "md:webhook", "depends_on": "hass", "requirements": [] diff --git a/music_assistant/server/providers/musicbrainz/manifest.json b/music_assistant/server/providers/musicbrainz/manifest.json index 5fbdd54d..bbf49fd1 100644 --- a/music_assistant/server/providers/musicbrainz/manifest.json +++ b/music_assistant/server/providers/musicbrainz/manifest.json @@ -1,14 +1,13 @@ { "type": "metadata", "domain": "musicbrainz", - "name": "MusicBrainz Metadata provider", - "description": "MusicBrainz is an open music encyclopedia that collects music metadata and makes it available to the public.", - "codeowners": [ - "@music-assistant" - ], + "name": "MusicBrainz", + "description": "MusicBrainz is an open music encyclopedia that collects music metadata and makes it available to the public. Music Assistant uses MusicBrainz primary to identify (unique) media items and therefore the provider can not be disabled. However note that lookups will only be performed if this info is absent locally.", + "codeowners": ["@music-assistant"], "requirements": [], "documentation": "", "multi_instance": false, "builtin": true, + "allow_disable": false, "icon": "mdi-folder-information" } diff --git a/music_assistant/server/providers/slimproto/manifest.json b/music_assistant/server/providers/slimproto/manifest.json index 47ad2341..96a10ba0 100644 --- a/music_assistant/server/providers/slimproto/manifest.json +++ b/music_assistant/server/providers/slimproto/manifest.json @@ -3,14 +3,9 @@ "domain": "slimproto", "name": "Slimproto (Squeezebox players)", "description": "Support for slimproto based players (e.g. squeezebox, squeezelite).", - "codeowners": [ - "@music-assistant" - ], - "requirements": [ - "aioslimproto==3.0.1" - ], + "codeowners": ["@music-assistant"], + "requirements": ["aioslimproto==3.0.1"], "documentation": "https://music-assistant.io/player-support/slimproto/", "multi_instance": false, - "builtin": false, - "load_by_default": false + "builtin": false } diff --git a/music_assistant/server/providers/snapcast/manifest.json b/music_assistant/server/providers/snapcast/manifest.json index cd63716c..0f75f6f2 100644 --- a/music_assistant/server/providers/snapcast/manifest.json +++ b/music_assistant/server/providers/snapcast/manifest.json @@ -3,15 +3,9 @@ "domain": "snapcast", "name": "Snapcast", "description": "Support for snapcast server and clients.", - "codeowners": [ - "@SantigoSotoC" - ], - "requirements": [ - "snapcast==2.3.6", - "bidict==0.23.1" - ], + "codeowners": ["@SantigoSotoC"], + "requirements": ["snapcast==2.3.6", "bidict==0.23.1"], "documentation": "https://music-assistant.io/player-support/snapcast/", "multi_instance": false, - "builtin": false, - "load_by_default": false + "builtin": false } diff --git a/music_assistant/server/providers/spotify/__init__.py b/music_assistant/server/providers/spotify/__init__.py index be7e6ebe..06d176c8 100644 --- a/music_assistant/server/providers/spotify/__init__.py +++ b/music_assistant/server/providers/spotify/__init__.py @@ -122,7 +122,7 @@ async def get_config_entries( ConfigEntry( key=CONF_CLIENT_ID, type=ConfigEntryType.SECURE_STRING, - label="Client ID", + label="Client ID (optional)", description="By default, a generic client ID is used which is heavy rate limited. " "It is advised that you create your own Spotify Developer account and use " "that client ID here to speedup performance.", diff --git a/music_assistant/server/providers/test/manifest.json b/music_assistant/server/providers/test/manifest.json index 1f480808..a8cd64d7 100644 --- a/music_assistant/server/providers/test/manifest.json +++ b/music_assistant/server/providers/test/manifest.json @@ -3,12 +3,9 @@ "domain": "test", "name": "Test / demo provider", "description": "Test/Demo provider that creates a collection of fake media items.", - "codeowners": [ - "@music-assistant" - ], + "codeowners": ["@music-assistant"], "requirements": [], "documentation": "", "multi_instance": false, - "builtin": false, - "hidden": false + "builtin": false } diff --git a/music_assistant/server/providers/theaudiodb/__init__.py b/music_assistant/server/providers/theaudiodb/__init__.py index a957f457..d02cc634 100644 --- a/music_assistant/server/providers/theaudiodb/__init__.py +++ b/music_assistant/server/providers/theaudiodb/__init__.py @@ -7,7 +7,8 @@ from typing import TYPE_CHECKING, Any, cast import aiohttp.client_exceptions -from music_assistant.common.models.enums import ExternalID, ProviderFeature +from music_assistant.common.models.config_entries import ConfigEntry +from music_assistant.common.models.enums import ConfigEntryType, ExternalID, ProviderFeature from music_assistant.common.models.media_items import ( Album, AlbumType, @@ -27,11 +28,7 @@ from music_assistant.server.helpers.throttle_retry import Throttler from music_assistant.server.models.metadata_provider import MetadataProvider if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueType, - ProviderConfig, - ) + from music_assistant.common.models.config_entries import ConfigValueType, ProviderConfig from music_assistant.common.models.provider import ProviderManifest from music_assistant.server import MusicAssistant from music_assistant.server.models import ProviderInstanceType @@ -75,6 +72,11 @@ ALBUMTYPE_MAPPING = { "EP": AlbumType.EP, } +CONF_ENABLE_IMAGES = "enable_images" +CONF_ENABLE_ARTIST_METADATA = "enable_artist_metadata" +CONF_ENABLE_ALBUM_METADATA = "enable_album_metadata" +CONF_ENABLE_TRACK_METADATA = "enable_track_metadata" + async def setup( mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig @@ -99,7 +101,32 @@ async def get_config_entries( values: the (intermediate) raw values for config entries sent with the action. """ # ruff: noqa: ARG001 - return () # we do not have any config entries (yet) + return ( + ConfigEntry( + key=CONF_ENABLE_ARTIST_METADATA, + type=ConfigEntryType.BOOLEAN, + label="Enable retrieval of artist metadata.", + default_value=True, + ), + ConfigEntry( + key=CONF_ENABLE_ALBUM_METADATA, + type=ConfigEntryType.BOOLEAN, + label="Enable retrieval of album metadata.", + default_value=True, + ), + ConfigEntry( + key=CONF_ENABLE_TRACK_METADATA, + type=ConfigEntryType.BOOLEAN, + label="Enable retrieval of track metadata.", + default_value=False, + ), + ConfigEntry( + key=CONF_ENABLE_IMAGES, + type=ConfigEntryType.BOOLEAN, + label="Enable retrieval of artist/album/track images", + default_value=True, + ), + ) class AudioDbMetadataProvider(MetadataProvider): @@ -110,7 +137,7 @@ class AudioDbMetadataProvider(MetadataProvider): async def handle_async_init(self) -> None: """Handle async initialization of the provider.""" self.cache = self.mass.cache - self.throttler = Throttler(rate_limit=1, period=30) + self.throttler = Throttler(rate_limit=1, period=1) @property def supported_features(self) -> tuple[ProviderFeature, ...]: @@ -119,6 +146,8 @@ class AudioDbMetadataProvider(MetadataProvider): async def get_artist_metadata(self, artist: Artist) -> MediaItemMetadata | None: """Retrieve metadata for artist on theaudiodb.""" + if not self.config.get_value(CONF_ENABLE_ARTIST_METADATA): + return None if not artist.mbid: # for 100% accuracy we require the musicbrainz id for all lookups return None @@ -129,6 +158,8 @@ class AudioDbMetadataProvider(MetadataProvider): async def get_album_metadata(self, album: Album) -> MediaItemMetadata | None: """Retrieve metadata for album on theaudiodb.""" + if not self.config.get_value(CONF_ENABLE_ALBUM_METADATA): + return None if (mbid := album.get_external_id(ExternalID.MB_RELEASEGROUP)) is None: return None result = await self._get_data("album-mb.php", i=mbid) @@ -148,6 +179,8 @@ class AudioDbMetadataProvider(MetadataProvider): async def get_track_metadata(self, track: Track) -> MediaItemMetadata | None: """Retrieve metadata for track on theaudiodb.""" + if not self.config.get_value(CONF_ENABLE_TRACK_METADATA): + return None if track.mbid: result = await self._get_data("track-mb.php", i=track.mbid) if result and result.get("track"): @@ -200,6 +233,8 @@ class AudioDbMetadataProvider(MetadataProvider): else: metadata.description = artist_obj.get("strBiographyEN") # images + if not self.config.get_value(CONF_ENABLE_IMAGES): + return metadata metadata.images = UniqueList() for key, img_type in IMG_MAPPING.items(): for postfix in ("", "2", "3", "4", "5", "6", "7", "8", "9", "10"): @@ -246,6 +281,8 @@ class AudioDbMetadataProvider(MetadataProvider): metadata.description = album_obj.get("strDescriptionEN") metadata.review = album_obj.get("strReview") # images + if not self.config.get_value(CONF_ENABLE_IMAGES): + return metadata metadata.images = UniqueList() for key, img_type in IMG_MAPPING.items(): for postfix in ("", "2", "3", "4", "5", "6", "7", "8", "9", "10"): @@ -280,6 +317,8 @@ class AudioDbMetadataProvider(MetadataProvider): else: metadata.description = track_obj.get("strDescriptionEN") # images + if not self.config.get_value(CONF_ENABLE_IMAGES): + return metadata metadata.images = UniqueList([]) for key, img_type in IMG_MAPPING.items(): for postfix in ("", "2", "3", "4", "5", "6", "7", "8", "9", "10"): diff --git a/music_assistant/server/providers/theaudiodb/manifest.json b/music_assistant/server/providers/theaudiodb/manifest.json index d7133ce7..9b2eaecf 100644 --- a/music_assistant/server/providers/theaudiodb/manifest.json +++ b/music_assistant/server/providers/theaudiodb/manifest.json @@ -1,11 +1,9 @@ { "type": "metadata", "domain": "theaudiodb", - "name": "TheAudioDB Metadata provider", + "name": "The Audio DB", "description": "TheAudioDB is a community Database of audio artwork and metadata with a JSON API.", - "codeowners": [ - "@music-assistant" - ], + "codeowners": ["@music-assistant"], "requirements": [], "documentation": "", "multi_instance": false, diff --git a/tests/server/providers/filesystem/__init__.py b/tests/server/providers/filesystem/__init__.py new file mode 100644 index 00000000..8803e351 --- /dev/null +++ b/tests/server/providers/filesystem/__init__.py @@ -0,0 +1 @@ +"""Tests for Filesystem provider.""" diff --git a/tests/server/providers/filesystem/test_helpers.py b/tests/server/providers/filesystem/test_helpers.py new file mode 100644 index 00000000..2da4bea1 --- /dev/null +++ b/tests/server/providers/filesystem/test_helpers.py @@ -0,0 +1,33 @@ +"""Tests for utility/helper functions.""" + +from music_assistant.server.providers.filesystem_local import helpers + +# ruff: noqa: S108 + + +def test_get_artist_dir() -> None: + """Test the extraction of an artist dir.""" + album_path = "/tmp/Artist/Album" + artist_name = "Artist" + assert helpers.get_artist_dir(album_path, artist_name) == "/tmp/Artist" + album_path = "/tmp/artist/Album" + assert helpers.get_artist_dir(album_path, artist_name) == "/tmp/artist" + album_path = "/tmp/Album" + assert helpers.get_artist_dir(album_path, artist_name) is None + album_path = "/tmp/ARTIST!/Album" + assert helpers.get_artist_dir(album_path, artist_name) == "/tmp/ARTIST!" + album_path = "/tmp/Artist/Album" + artist_name = "Artist!" + assert helpers.get_artist_dir(album_path, artist_name) == "/tmp/Artist" + album_path = "/tmp/REM/Album" + artist_name = "R.E.M." + assert helpers.get_artist_dir(album_path, artist_name) == "/tmp/REM" + album_path = "/tmp/ACDC/Album" + artist_name = "AC/DC" + assert helpers.get_artist_dir(album_path, artist_name) == "/tmp/ACDC" + album_path = "/tmp/Celine Dion/Album" + artist_name = "Céline Dion" + assert helpers.get_artist_dir(album_path, artist_name) == "/tmp/Celine Dion" + album_path = "/tmp/Antonin Dvorak/Album" + artist_name = "Antonín Dvořák" + assert helpers.get_artist_dir(album_path, artist_name) == "/tmp/Antonin Dvorak"