{**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": True}
)
-CONF_ENTRY_FLOW_MODE_ENFORCED = ConfigEntry(
- key=CONF_FLOW_MODE,
- type=ConfigEntryType.BOOLEAN,
- label=CONF_FLOW_MODE,
- default_value=True,
- value=True,
- hidden=True,
+CONF_ENTRY_FLOW_MODE_ENFORCED = ConfigEntry.from_dict(
+ {**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": True, "value": True, "hidden": True}
+)
+
+CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED = ConfigEntry.from_dict(
+ {**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": False, "value": False, "hidden": True}
)
CONF_ENTRY_AUTO_PLAY = ConfigEntry(
limit: int = 500,
offset: int = 0,
order_by: str = "sort_name",
+ provider: str | None = None,
extra_query: str | None = None,
extra_query_params: dict[str, Any] | None = None,
album_artists_only: bool = False,
limit=limit,
offset=offset,
order_by=order_by,
+ provider=provider,
extra_query=extra_query,
extra_query_params=extra_query_params,
)
limit: int = 500,
offset: int = 0,
order_by: str = "sort_name",
+ provider: str | None = None,
extra_query: str | None = None,
extra_query_params: dict[str, Any] | None = None,
) -> list[ItemCls]:
limit=limit,
offset=offset,
order_by=order_by,
+ provider=provider,
extra_query=extra_query,
extra_query_params=extra_query_params,
)
limit: int = 500,
offset: int = 0,
order_by: str | None = None,
+ provider: str | None = None,
extra_query: str | None = None,
extra_query_params: dict[str, Any] | None = None,
) -> list[ItemCls] | int:
if favorite is not None:
query_parts.append(f"{self.db_table}.favorite = :favorite")
query_params["favorite"] = favorite
+ # handle provider filter
+ if provider:
+ query_parts.append(f"{DB_TABLE_PROVIDER_MAPPINGS}.provider_instance = :provider")
+ query_params["provider"] = provider
# handle extra/custom query
if extra_query:
# prevent duplicate where statement
BrowseFolder(item_id="back", provider=provider_instance, path=back_path, name="..")
)
# limit -1 to account for the prepended items
- prov_items = await prov.browse(path, offset=offset, limit=limit)
+ prov_items = await prov.browse(path=path, offset=offset, limit=limit)
return prepend_items + prov_items
@api_command("music/recently_played_items")
await self.__create_database_triggers()
# compact db
self.logger.debug("Compacting database...")
- await self.database.vacuum()
- self.logger.debug("Compacting database done")
+ try:
+ await self.database.vacuum()
+ except Exception as err:
+ self.logger.warning("Database vacuum failed: %s", str(err))
+ else:
+ self.logger.debug("Compacting database done")
async def __migrate_database(self, prev_version: int) -> None:
"""Perform a database migration."""
"""Close/terminate the process and wait for exit."""
if self._stdin_task and not self._stdin_task.done():
self._stdin_task.cancel()
- # make sure the stdin generator is also properly closed
- # by propagating a cancellederror within
- with suppress(RuntimeError):
- task = asyncio.create_task(self.audio_input.__anext__())
- task.cancel()
if not self.collect_log_history:
await super().close(send_signal)
return
async def subscribe_raw(self) -> AsyncGenerator[bytes, None]:
"""Subscribe to the raw/unaltered audio stream."""
try:
- queue = asyncio.Queue(1)
+ queue = asyncio.Queue(2)
self.subscribers.append(queue)
while True:
chunk = await queue.get()
async for chunk in self.audio_source:
if len(self.subscribers) == 0:
return
- async with asyncio.TaskGroup() as tg:
- for sub in list(self.subscribers):
- tg.create_task(sub.put(chunk))
+ await asyncio.gather(
+ *[sub.put(chunk) for sub in self.subscribers], return_exceptions=True
+ )
# EOF: send empty chunk
async with asyncio.TaskGroup() as tg:
for sub in list(self.subscribers):
import json
import logging
import os
+from collections.abc import Iterable
from dataclasses import dataclass
from json import JSONDecodeError
from typing import TYPE_CHECKING, Any
TAG_SPLITTER = ";"
+def clean_tuple(values: Iterable[str]) -> tuple:
+ """Return a tuple with all empty values removed."""
+ return tuple(x.strip() for x in values if x not in (None, "", " "))
+
+
def split_items(org_str: str, allow_unsafe_splitters: bool = False) -> tuple[str, ...]:
"""Split up a tags string by common splitter."""
if org_str is None:
return (x.strip() for x in org_str)
org_str = org_str.strip()
if TAG_SPLITTER in org_str:
- return tuple(x.strip() for x in org_str.split(TAG_SPLITTER))
+ return clean_tuple(org_str.split(TAG_SPLITTER))
if allow_unsafe_splitters and "/" in org_str:
- return tuple(x.strip() for x in org_str.split("/"))
+ return clean_tuple(org_str.split("/"))
if allow_unsafe_splitters and ", " in org_str:
- return tuple(x.strip() for x in org_str.split(", "))
- return (org_str.strip(),)
+ return clean_tuple(org_str.split(", "))
+ return clean_tuple((org_str,))
def split_artists(
if ProviderFeature.BROWSE not in self.supported_features:
# we may NOT use the default implementation if the provider does not support browse
raise NotImplementedError
- items: list[MediaItemType] = []
- index = -1
+
subpath = path.split("://", 1)[1]
# this reference implementation can be overridden with a provider specific approach
- generator: AsyncGenerator[MediaItemType, None] | None = None
if subpath == "artists":
- generator = self.get_library_artists()
- elif subpath == "albums":
- generator = self.get_library_albums()
- elif subpath == "tracks":
- generator = self.get_library_tracks()
- elif subpath == "radios":
- generator = self.get_library_radios()
- elif subpath == "playlists":
- generator = self.get_library_playlists()
- elif subpath:
+ return await self.mass.music.artists.library_items(
+ limit=limit, offset=offset, provider=self.instance_id
+ )
+ if subpath == "albums":
+ return await self.mass.music.albums.library_items(
+ limit=limit, offset=offset, provider=self.instance_id
+ )
+ if subpath == "tracks":
+ return await self.mass.music.tracks.library_items(
+ limit=limit, offset=offset, provider=self.instance_id
+ )
+ if subpath == "radios":
+ return await self.mass.music.radio.library_items(
+ limit=limit, offset=offset, provider=self.instance_id
+ )
+ if subpath == "playlists":
+ return await self.mass.music.playlists.library_items(
+ limit=limit, offset=offset, provider=self.instance_id
+ )
+ if subpath:
# unknown path
msg = "Invalid subpath"
raise KeyError(msg)
- if generator:
- # grab items from library generator
- async for item in generator:
- index += 1
- if index < offset:
- continue
- items.append(item)
- if len(items) >= limit:
- break
- else:
- # no subpath: return main listing
- if ProviderFeature.LIBRARY_ARTISTS in self.supported_features:
- items.append(
- BrowseFolder(
- item_id="artists",
- provider=self.domain,
- path=path + "artists",
- name="",
- label="artists",
- )
+ # no subpath: return main listing
+ items: list[MediaItemType] = []
+ if ProviderFeature.LIBRARY_ARTISTS in self.supported_features:
+ items.append(
+ BrowseFolder(
+ item_id="artists",
+ provider=self.domain,
+ path=path + "artists",
+ name="",
+ label="artists",
)
- if ProviderFeature.LIBRARY_ALBUMS in self.supported_features:
- items.append(
- BrowseFolder(
- item_id="albums",
- provider=self.domain,
- path=path + "albums",
- name="",
- label="albums",
- )
+ )
+ if ProviderFeature.LIBRARY_ALBUMS in self.supported_features:
+ items.append(
+ BrowseFolder(
+ item_id="albums",
+ provider=self.domain,
+ path=path + "albums",
+ name="",
+ label="albums",
)
- if ProviderFeature.LIBRARY_TRACKS in self.supported_features:
- items.append(
- BrowseFolder(
- item_id="tracks",
- provider=self.domain,
- path=path + "tracks",
- name="",
- label="tracks",
- )
+ )
+ if ProviderFeature.LIBRARY_TRACKS in self.supported_features:
+ items.append(
+ BrowseFolder(
+ item_id="tracks",
+ provider=self.domain,
+ path=path + "tracks",
+ name="",
+ label="tracks",
)
- if ProviderFeature.LIBRARY_PLAYLISTS in self.supported_features:
- items.append(
- BrowseFolder(
- item_id="playlists",
- provider=self.domain,
- path=path + "playlists",
- name="",
- label="playlists",
- )
+ )
+ if ProviderFeature.LIBRARY_PLAYLISTS in self.supported_features:
+ items.append(
+ BrowseFolder(
+ item_id="playlists",
+ provider=self.domain,
+ path=path + "playlists",
+ name="",
+ label="playlists",
)
- if ProviderFeature.LIBRARY_RADIOS in self.supported_features:
- items.append(
- BrowseFolder(
- item_id="radios",
- provider=self.domain,
- path=path + "radios",
- name="",
- label="radios",
- )
+ )
+ if ProviderFeature.LIBRARY_RADIOS in self.supported_features:
+ items.append(
+ BrowseFolder(
+ item_id="radios",
+ provider=self.domain,
+ path=path + "radios",
+ name="",
+ label="radios",
)
+ )
return items
async def recommendations(self) -> list[MediaItemType]:
CONF_ENTRY_CROSSFADE,
CONF_ENTRY_CROSSFADE_DURATION,
CONF_ENTRY_SAMPLE_RATES_AIRPLAY,
+ CONF_ENTRY_FLOW_MODE_ENFORCED,
)
return (*base_entries, *PLAYER_CONFIG_ENTRIES, CONF_ENTRY_SAMPLE_RATES_AIRPLAY)
# prefer interactive command to our streamer
await airplay_player.active_stream.send_cli_command("ACTION=PAUSE")
- async def play_media( # noqa: PLR0915
+ async def play_media(
self,
player_id: str,
media: PlayerMedia,
chunk = await buffer.get()
if chunk == b"EOF":
break
- async with asyncio.TaskGroup() as tg:
- for airplay_player in sync_clients:
- tg.create_task(airplay_player.active_stream.write_chunk(chunk))
+ await asyncio.gather(
+ *[x.active_stream.write_chunk(chunk) for x in sync_clients],
+ return_exceptions=True,
+ )
# entire stream consumed: send EOF
- for airplay_player in sync_clients:
- self.mass.create_task(airplay_player.active_stream.write_eof())
+ await asyncio.gather(
+ *[x.active_stream.write_eof() for x in sync_clients],
+ return_exceptions=True,
+ )
+
finally:
if not fill_buffer_task.done():
fill_buffer_task.cancel()
- # make sure the stdin generator is also properly closed
- # by propagating a cancellederror within
- task = asyncio.create_task(audio_source.__anext__())
- task.cancel()
empty_queue(buffer)
# get current ntp and start cliraop
_, stdout = await check_output(f"{self.cliraop_bin} -ntp")
start_ntp = int(stdout.strip())
wait_start = 1250 + (250 * len(sync_clients))
- async with asyncio.TaskGroup() as tg:
- for airplay_player in self._get_sync_clients(player_id):
- tg.create_task(airplay_player.active_stream.start(start_ntp, wait_start))
+ await asyncio.gather(
+ *[x.active_stream.start(start_ntp, wait_start) for x in sync_clients],
+ return_exceptions=True,
+ )
self._players[player_id].active_stream.audio_source_task = asyncio.create_task(
audio_streamer()
)
import asyncio
import os
import os.path
+import re
from typing import TYPE_CHECKING
import aiofiles
)
-async def create_item(base_path: str, entry: os.DirEntry) -> FileSystemItem:
- """Create FileSystemItem from os.DirEntry."""
+def sorted_scandir(base_path: str, sub_path: str) -> list[os.DirEntry]:
+ """Implement os.scandir that returns (naturally) sorted entries."""
- def _create_item():
+ def nat_key(name: str) -> tuple[int, str]:
+ """Sort key for natural sorting."""
+ return tuple(int(s) if s.isdigit() else s for s in re.split(r"(\d+)", name))
+
+ def create_item(entry: os.DirEntry):
+ """Create FileSystemItem from os.DirEntry."""
absolute_path = get_absolute_path(base_path, entry.path)
stat = entry.stat(follow_symlinks=False)
return FileSystemItem(
local_path=absolute_path,
)
- # run in thread because strictly taken this may be blocking IO
- return await asyncio.to_thread(_create_item)
+ return sorted(
+ # filter out invalid dirs and hidden files
+ [
+ create_item(x)
+ for x in os.scandir(sub_path)
+ if x.name not in IGNORE_DIRS and not x.name.startswith(".")
+ ],
+ # sort by (natural) name
+ key=lambda x: nat_key(x.name),
+ )
class LocalFileSystemProvider(FileSystemProviderBase):
"""
abs_path = get_absolute_path(self.base_path, path)
- entries = await asyncio.to_thread(os.scandir, abs_path)
- for entry in entries:
- if entry.name.startswith(".") or any(x in entry.name for x in IGNORE_DIRS):
- # skip invalid/system files and dirs
- continue
- item = await create_item(self.base_path, entry)
- if recursive and item.is_dir:
+ for entry in await asyncio.to_thread(sorted_scandir, self.base_path, abs_path):
+ if recursive and entry.is_dir:
try:
- async for subitem in self.listdir(item.absolute_path, True):
+ async for subitem in self.listdir(entry.absolute_path, True):
yield subitem
except (OSError, PermissionError) as err:
- self.logger.warning("Skip folder %s: %s", item.path, str(err))
+ self.logger.warning("Skip folder %s: %s", entry.path, str(err))
else:
- yield item
+ yield entry
async def resolve(
self,
:param path: The path to browse, (e.g. provid://artists).
"""
+ if offset:
+ # we do not support pagination
+ return []
items: list[MediaItemType] = []
item_path = path.split("://", 1)[1]
if not item_path:
item_path = ""
- index = 0
async for item in self.listdir(item_path, recursive=False):
if not item.is_dir and ("." not in item.filename or not item.ext):
# skip system files and files without extension
continue
- if index < offset:
- continue
-
if item.is_dir:
items.append(
BrowseFolder(
name=item.filename,
)
)
- index += 1
- if len(items) >= limit:
- break
return items
async def sync_library(self, media_types: tuple[MediaType, ...]) -> None:
f"SELECT item_id FROM {DB_TABLE_ARTISTS} "
f"WHERE item_id not in "
f"( select artist_id from {DB_TABLE_TRACK_ARTISTS} "
- f"UNION SELECT artist_id from {DB_TABLE_ALBUM_ARTISTS} )"
- f"AND provider_instance = '{self.instance_id}'"
+ f"UNION SELECT artist_id from {DB_TABLE_ALBUM_ARTISTS} ) "
+ f"AND item_id in ( SELECT item_id from {DB_TABLE_PROVIDER_MAPPINGS} "
+ f"WHERE provider_instance = '{self.instance_id}' and media_type = 'artist' )"
)
for db_row in await self.mass.music.database.get_rows_from_query(
query,
if platform.system() == "Darwin":
password_str = f":{password}" if password else ""
- mount_cmd = f'mount -t smbfs "//{username}{password_str}@{server}/{share}{subfolder}" "{self.base_path}"' # noqa: E501
+ mount_cmd = f'mount -t smbfs "//{username}:{password_str}@{server}/{share}{subfolder}" "{self.base_path}"' # noqa: E501
elif platform.system() == "Linux":
options = [
from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType
from music_assistant.common.models.enums import ConfigEntryType
-from music_assistant.common.models.errors import LoginFailed
+from music_assistant.common.models.errors import LoginFailed, SetupFailedError
from music_assistant.constants import MASS_LOGO_ONLINE
from music_assistant.server.helpers.auth import AuthenticationHelper
from music_assistant.server.models.plugin import PluginProvider
token = self.config.get_value(CONF_AUTH_TOKEN)
logging.getLogger("hass_client").setLevel(self.logger.level + 10)
self.hass = HomeAssistantClient(url, token, self.mass.http_session)
- await self.hass.connect()
+ try:
+ await self.hass.connect()
+ except BaseHassClientError as err:
+ raise SetupFailedError from err
self._listen_task = self.mass.create_task(self._hass_listener())
async def unload(self) -> None:
"load_by_default": false,
"icon": "md:webhook",
"requirements": [
- "hass-client==2.0.0"
+ "hass-client==1.1.1"
]
}
ProviderFeature,
StreamType,
)
-from music_assistant.common.models.errors import InvalidDataError, LoginFailed, MediaNotFoundError
+from music_assistant.common.models.errors import (
+ InvalidDataError,
+ LoginFailed,
+ MediaNotFoundError,
+ SetupFailedError,
+)
from music_assistant.common.models.media_items import (
Album,
Artist,
self._myplex_account = await self.get_myplex_account_and_refresh_token(
self.config.get_value(CONF_AUTH_TOKEN)
)
- self._plex_server = await self._run_async(connect)
- self._plex_library = await self._run_async(self._plex_server.library.section, library_name)
+ try:
+ self._plex_server = await self._run_async(connect)
+ self._plex_library = await self._run_async(
+ self._plex_server.library.section, library_name
+ )
+ except requests.exceptions.ConnectionError as err:
+ raise SetupFailedError from err
@property
def supported_features(self) -> tuple[ProviderFeature, ...]:
async def _run_async(self, call: Callable, *args, **kwargs):
await self.get_myplex_account_and_refresh_token(self.config.get_value(CONF_AUTH_TOKEN))
- return await self.mass.create_task(call, *args, **kwargs)
+ return await asyncio.to_thread(call, *args, **kwargs)
async def _get_data(self, key, cls=None):
return await self._run_async(self._plex_library.fetchItem, key, cls)
SearchResults,
)
from music_assistant.common.models.streamdetails import StreamDetails
+from music_assistant.server.controllers.cache import use_cache
from music_assistant.server.models.music_provider import MusicProvider
SUPPORTED_FEATURES = (ProviderFeature.SEARCH, ProviderFeature.BROWSE)
return result
+ @use_cache(86400 * 7)
async def browse(self, path: str, offset: int, limit: int) -> list[MediaItemType]:
"""Browse this provider's items.
):
try:
await resp.write(chunk)
- except (BrokenPipeError, ConnectionResetError):
+ except (BrokenPipeError, ConnectionResetError, ConnectionError):
# race condition
break
from music_assistant.common.models.config_entries import (
CONF_ENTRY_CROSSFADE,
+ CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED,
ConfigEntry,
ConfigValueType,
create_sample_rates_config_entry,
base_entries = await super().get_player_config_entries(player_id)
if not (sonos_player := self.sonosplayers.get(player_id)):
# most probably a syncgroup
- return (*base_entries, CONF_ENTRY_CROSSFADE)
+ return (*base_entries, CONF_ENTRY_CROSSFADE, CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED)
is_s2 = sonos_player.soco.speaker_info["model_name"] in S2_MODELS
return (
*base_entries,
category="advanced",
),
CONF_ENTRY_SAMPLE_RATES_SONOS_S2 if is_s2 else CONF_ENTRY_SAMPLE_RATES_SONOS_S1,
+ CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED,
)
def on_player_config_changed(
async def get_artist_toptracks(self, prov_artist_id: str) -> list[Track]:
"""Get a list of 10 most popular tracks for the given artist."""
tidal_session = await self._get_tidal_session()
- artist_toptracks_obj = await get_artist_toptracks(tidal_session, prov_artist_id)
- return [self._parse_track(track) for track in artist_toptracks_obj]
+ try:
+ artist_toptracks_obj = await get_artist_toptracks(tidal_session, prov_artist_id)
+ return [self._parse_track(track) for track in artist_toptracks_obj]
+ except tidal_exceptions.ObjectNotFound as err:
+ self.logger.warning(f"Failed to get toptracks for artist {prov_artist_id}: {err}")
+ return []
async def get_playlist_tracks(
self, prov_playlist_id: str, offset: int, limit: int
async def get_artist(self, prov_artist_id: str) -> Artist:
"""Get artist details for given artist id."""
tidal_session = await self._get_tidal_session()
- artist_obj = await get_artist(tidal_session, prov_artist_id)
- return self._parse_artist(artist_obj)
+ try:
+ artist_obj = await get_artist(tidal_session, prov_artist_id)
+ return self._parse_artist(artist_obj)
+ except tidal_exceptions.ObjectNotFound as err:
+ raise MediaNotFoundError from err
@throttle_with_retries
async def get_album(self, prov_album_id: str) -> Album:
"""Get album details for given album id."""
tidal_session = await self._get_tidal_session()
- album_obj = await get_album(tidal_session, prov_album_id)
- return self._parse_album(album_obj)
+ try:
+ album_obj = await get_album(tidal_session, prov_album_id)
+ return self._parse_album(album_obj)
+ except tidal_exceptions.ObjectNotFound as err:
+ raise MediaNotFoundError from err
@throttle_with_retries
async def get_track(self, prov_track_id: str) -> Track:
"""Get track details for given track id."""
tidal_session = await self._get_tidal_session()
track_obj = await get_track(tidal_session, prov_track_id)
- track = self._parse_track(track_obj)
- # get some extra details for the full track info
- with suppress(tidal_exceptions.MetadataNotAvailable, AttributeError):
- lyrics: TidalLyrics = await asyncio.to_thread(track.lyrics)
- track.metadata.lyrics = lyrics.text
- return track
+ try:
+ track = self._parse_track(track_obj)
+ # get some extra details for the full track info
+ with suppress(tidal_exceptions.MetadataNotAvailable, AttributeError):
+ lyrics: TidalLyrics = await asyncio.to_thread(track.lyrics)
+ track.metadata.lyrics = lyrics.text
+ return track
+ except tidal_exceptions.ObjectNotFound as err:
+ raise MediaNotFoundError from err
@throttle_with_retries
async def get_playlist(self, prov_playlist_id: str) -> Playlist:
from music_assistant.common.helpers.util import get_ip_pton
from music_assistant.common.models.api import ServerInfoMessage
from music_assistant.common.models.enums import EventType, ProviderType
-from music_assistant.common.models.errors import SetupFailedError
+from music_assistant.common.models.errors import MusicAssistantError, SetupFailedError
from music_assistant.common.models.event import MassEvent
from music_assistant.common.models.provider import ProviderManifest
from music_assistant.constants import (
def create_task(
self,
- target: Coroutine | Awaitable | Callable | asyncio.Future,
+ target: Coroutine | Awaitable | Callable,
*args: Any,
task_id: str | None = None,
+ eager_start: bool = False,
**kwargs: Any,
) -> asyncio.Task | asyncio.Future:
"""Create Task on (main) event loop from Coroutine(function).
# prevent duplicate tasks if task_id is given and already present
return existing
if asyncio.iscoroutinefunction(target):
- task = self.loop.create_task(target(*args, **kwargs))
+ # coroutine function (with or without eager start)
+ if eager_start:
+ task = asyncio.Task(target(*args, **kwargs), loop=self.loop, eager_start=True)
+ else:
+ task = self.loop.create_task(target(*args, **kwargs))
elif asyncio.iscoroutine(target):
- task = self.loop.create_task(target)
- elif isinstance(target, asyncio.Future):
- task = target
+ # coroutine (with or without eager start)
+ if eager_start:
+ task = asyncio.Task(target, loop=self.loop, eager_start=True)
+ else:
+ task = self.loop.create_task(target)
+ elif eager_start:
+ # regular callback (non async function)
+ task = asyncio.Task(
+ asyncio.to_thread(target, *args, **kwargs), loop=self.loop, eager_start=True
+ )
else:
- # assume normal callable (non coroutine or awaitable)
- # that needs to be run in the executor
task = self.loop.create_task(asyncio.to_thread(target, *args, **kwargs))
- def task_done_callback(_task: asyncio.Future | asyncio.Task) -> None:
+ def task_done_callback(_task: asyncio.Task) -> None:
_task_id = task.task_id
self._tracked_tasks.pop(_task_id)
# log unhandled exceptions
def call_later(
self,
delay: float,
- target: Coroutine | Awaitable | Callable | asyncio.Future,
+ target: Coroutine | Awaitable | Callable,
*args: Any,
task_id: str | None = None,
**kwargs: Any,
self._tracked_timers[task_id] = handle
return handle
- def get_task(self, task_id: str) -> asyncio.Task | asyncio.Future:
+ def get_task(self, task_id: str) -> asyncio.Task:
"""Get existing scheduled task."""
if existing := self._tracked_tasks.get(task_id):
# prevent duplicate tasks if task_id is given and already present
await self._load_provider(prov_conf)
# pylint: disable=broad-except
except Exception as exc:
- LOGGER.exception(
- "Error loading provider(instance) %s",
- prov_conf.name or prov_conf.domain,
- )
+ if isinstance(exc, MusicAssistantError):
+ LOGGER.error(
+ "Error loading provider(instance) %s: %s",
+ prov_conf.name or prov_conf.domain,
+ str(exc),
+ )
+ else:
+ # log full stack trace on unhandled/generic exception
+ LOGGER.exception(
+ "Error loading provider(instance) %s",
+ prov_conf.name or prov_conf.domain,
+ )
if raise_on_error:
raise
# if loading failed, we store the error in the config object
defusedxml==0.7.1
faust-cchardet>=2.1.18
git+https://github.com/MarvinSchenkel/pytube.git
-hass-client==2.0.0
+hass-client==1.1.1
ifaddr==0.2.0
mashumaro==3.13.1
memory-tempfile==2.2.3