if cur_val == new_val:
continue
self.values[key].value = new_val
- changed_keys.add(f"values.{key}")
+ changed_keys.add(f"values/{key}")
return changed_keys
from music_assistant.common.helpers.json import serialize_to_json
from music_assistant.common.models.enums import EventType, ProviderFeature
-from music_assistant.common.models.errors import MediaNotFoundError, UnsupportedFeaturedException
+from music_assistant.common.models.errors import (
+ MediaNotFoundError,
+ ProviderUnavailableError,
+ UnsupportedFeaturedException,
+)
from music_assistant.common.models.media_items import (
Album,
AlbumType,
provider_instance: str | None = None,
) -> list[Track]:
"""Return album tracks for the given provider album id."""
- prov = self.mass.get_provider(provider_instance or provider_domain)
- if not prov:
+ try:
+ prov = self.mass.get_provider(provider_instance or provider_domain)
+ except ProviderUnavailableError:
return []
+
full_album = await self.get_provider_item(item_id, provider_instance or provider_domain)
# prefer cache items (if any)
cache_key = f"{prov.instance_id}.albumtracks.{item_id}"
limit: int = 25,
):
"""Generate a dynamic list of tracks based on the album content."""
- prov = self.mass.get_provider(provider_instance or provider_domain)
- if not prov or ProviderFeature.SIMILAR_TRACKS not in prov.supported_features:
+ try:
+ prov = self.mass.get_provider(provider_instance or provider_domain)
+ except ProviderUnavailableError:
+ return []
+ if ProviderFeature.SIMILAR_TRACKS not in prov.supported_features:
return []
album_tracks = await self._get_provider_album_tracks(
item_id=item_id,
from music_assistant.common.helpers.json import serialize_to_json
from music_assistant.common.models.enums import EventType, ProviderFeature
-from music_assistant.common.models.errors import MediaNotFoundError, UnsupportedFeaturedException
+from music_assistant.common.models.errors import (
+ MediaNotFoundError,
+ ProviderUnavailableError,
+ UnsupportedFeaturedException,
+)
from music_assistant.common.models.media_items import (
Album,
AlbumType,
cache_checksum: Any = None,
) -> list[Track]:
"""Return top tracks for an artist on given provider."""
- prov = self.mass.get_provider(provider_instance or provider_domain)
- if not prov:
+ try:
+ prov = self.mass.get_provider(provider_instance or provider_domain)
+ except ProviderUnavailableError:
return []
# prefer cache items (if any)
cache_key = f"{prov.instance_id}.artist_toptracks.{item_id}"
cache_checksum: Any = None,
) -> list[Album]:
"""Return albums for an artist on given provider."""
- prov = self.mass.get_provider(provider_instance or provider_domain)
- if not prov:
+ try:
+ prov = self.mass.get_provider(provider_instance or provider_domain)
+ except ProviderUnavailableError:
return []
# prefer cache items (if any)
cache_key = f"{prov.instance_id}.artist_albums.{item_id}"
limit: int = 25,
):
"""Generate a dynamic list of tracks based on the artist's top tracks."""
- prov = self.mass.get_provider(provider_instance or provider_domain)
- if not prov or ProviderFeature.SIMILAR_TRACKS not in prov.supported_features:
+ try:
+ prov = self.mass.get_provider(provider_instance or provider_domain)
+ except ProviderUnavailableError:
+ return []
+ if ProviderFeature.SIMILAR_TRACKS not in prov.supported_features:
return []
top_tracks = await self.get_provider_artist_toptracks(
item_id=item_id,
from music_assistant.common.helpers.json import serialize_to_json
from music_assistant.common.models.enums import EventType, MediaType, ProviderFeature
-from music_assistant.common.models.errors import MediaNotFoundError
+from music_assistant.common.models.errors import MediaNotFoundError, ProviderUnavailableError
from music_assistant.common.models.media_items import (
MediaItemType,
PagedItems,
for db_row in await self.mass.music.database.search(self.db_table, search_query)
]
- prov = self.mass.get_provider(provider_instance or provider_domain)
- if not prov or ProviderFeature.SEARCH not in prov.supported_features:
+ try:
+ prov = self.mass.get_provider(provider_instance or provider_domain)
+ except ProviderUnavailableError:
+ return []
+ if ProviderFeature.SEARCH not in prov.supported_features:
return []
if not prov.library_supported(self.media_type):
# assume library supported also means that this mediatype is supported
"""Return a dynamic list of tracks based on the given item."""
ref_item = await self.get(item_id, provider_domain, provider_instance)
for prov_mapping in ref_item.provider_mappings:
- prov = self.mass.get_provider(prov_mapping.provider_instance)
+ try:
+ prov = self.mass.get_provider(prov_mapping.provider_instance)
+ except ProviderUnavailableError:
+ continue
if not prov.available:
continue
if ProviderFeature.SIMILAR_TRACKS not in prov.supported_features:
from music_assistant.common.helpers.json import serialize_to_json
from music_assistant.common.models.enums import EventType, MediaType, ProviderFeature
-from music_assistant.common.models.errors import MediaNotFoundError, UnsupportedFeaturedException
+from music_assistant.common.models.errors import (
+ MediaNotFoundError,
+ ProviderUnavailableError,
+ UnsupportedFeaturedException,
+)
from music_assistant.common.models.media_items import (
Album,
Artist,
limit: int = 25,
):
"""Generate a dynamic list of tracks based on the track."""
- prov = self.mass.get_provider(provider_instance or provider_domain)
- if not prov or ProviderFeature.SIMILAR_TRACKS not in prov.supported_features:
+ try:
+ prov = self.mass.get_provider(provider_instance or provider_domain)
+ except ProviderUnavailableError:
+ return []
+ if ProviderFeature.SIMILAR_TRACKS not in prov.supported_features:
return []
# Grab similar tracks from the music provider
similar_tracks = await prov.get_similar_tracks(prov_track_id=item_id, limit=limit)
from music_assistant.common.helpers.datetime import utc_timestamp
from music_assistant.common.helpers.uri import parse_uri
from music_assistant.common.models.enums import EventType, MediaType, ProviderFeature, ProviderType
-from music_assistant.common.models.errors import MusicAssistantError
+from music_assistant.common.models.errors import MusicAssistantError, ProviderUnavailableError
from music_assistant.common.models.media_items import (
BrowseFolder,
MediaItem,
:param limit: number of items to return in the search (per type).
"""
assert provider_domain or provider_instance, "Provider needs to be supplied"
- prov = self.mass.get_provider(provider_instance or provider_domain)
+ try:
+ prov = self.mass.get_provider(provider_instance or provider_domain)
+ except ProviderUnavailableError:
+ return []
if ProviderFeature.SEARCH not in prov.supported_features:
return []
),
)
+NEED_BRIDGE_RESTART = {"values/read_ahead", "values/encryption", "values/alac_encode"}
+
class AirplayProvider(PlayerProvider):
"""Player provider for Airplay based players, using the slimproto bridge."""
_bridge_bin: str | None = None
_bridge_proc: asyncio.subprocess.Process | None = None
+ _timer_handle: asyncio.TimerHandle | None = None
_closing: bool = False
_config_file: str | None = None
async def update_config():
# stop bridge (it will be auto restarted)
- # TODO: only restart bridge if actual xml values changed
- await self._stop_bridge()
- # update the config
- await self._check_config_xml()
+ if changed_keys.intersection(NEED_BRIDGE_RESTART):
+ self.restart_bridge()
asyncio.create_task(update_config())
async def _bridge_process_runner(self) -> None:
"""Run the bridge binary in the background."""
- log_file = os.path.join(self.mass.storage_path, "airplay_bridge.log")
self.logger.debug(
"Starting Airplay bridge using config file %s",
self._config_file,
"localhost",
"-x",
self._config_file,
- "-f",
- log_file,
"-I",
"-Z",
"-d",
- "all=info",
+ "all=warn",
]
start_success = False
while True:
# save config file
async with aiofiles.open(self._config_file, "w") as _file:
await _file.write(ET.tostring(xml_root).decode())
+
+ def restart_bridge(self) -> None:
+ """Schedule restart of bridge process."""
+ if self._timer_handle is not None:
+ self._timer_handle.cancel()
+ self._timer_handle = None
+
+ async def restart_bridge():
+ self.logger.info("Restarting Airplay bridge (due to config changes)")
+ await self._stop_bridge()
+ await self._check_config_xml()
+
+ # schedule the action for later
+ self._timer_handle = self.mass.loop.call_later(10, self.mass.create_task, restart_bridge)
def _create_queue_item(queue_item: QueueItem, stream_url: str):
"""Create CC queue item from MA QueueItem."""
duration = int(queue_item.duration) if queue_item.duration else None
- if queue_item.media_type == MediaType.TRACK:
+ if queue_item.media_type == MediaType.TRACK and queue_item.media_item:
stream_type = STREAM_TYPE_BUFFERED
metadata = {
"metadataType": 3,
- "albumName": queue_item.media_item.album.name,
+ "albumName": queue_item.media_item.album.name
+ if queue_item.media_item.album
+ else "",
"songName": queue_item.media_item.name,
- "artist": queue_item.media_item.artist.name,
+ "artist": queue_item.media_item.artist.name if queue_item.media_item.artist else "",
"title": queue_item.name,
"images": [{"url": queue_item.image.url}] if queue_item.image else None,
}