From 268ffb51abcc6afd90fad2ceaa83fd19687b01c4 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 20 Nov 2024 21:56:00 +0100 Subject: [PATCH] Feat: Bump models to 1.1.2 Feat: Bump models to 1.1.2 Feat: Adjust code to changes in the models Feat: Refactor sync to group to make it more universal --- music_assistant/controllers/players.py | 195 ++++++++++-------- music_assistant/helpers/tags.py | 28 +-- music_assistant/models/metadata_provider.py | 6 +- music_assistant/models/player_provider.py | 16 +- music_assistant/models/provider.py | 2 +- .../_template_music_provider/__init__.py | 2 +- .../_template_player_provider/__init__.py | 14 +- music_assistant/providers/airplay/provider.py | 19 +- .../providers/apple_music/__init__.py | 6 +- .../providers/bluesound/__init__.py | 28 +-- music_assistant/providers/builtin/__init__.py | 6 +- .../providers/chromecast/__init__.py | 14 +- music_assistant/providers/deezer/__init__.py | 6 +- music_assistant/providers/dlna/__init__.py | 6 +- .../providers/fanarttv/__init__.py | 6 +- .../providers/filesystem_local/__init__.py | 10 +- .../providers/fully_kiosk/__init__.py | 2 +- .../providers/hass_players/__init__.py | 16 +- .../providers/jellyfin/__init__.py | 6 +- .../providers/musicbrainz/__init__.py | 4 +- .../providers/opensubsonic/sonic_provider.py | 6 +- .../providers/player_group/__init__.py | 55 +++-- music_assistant/providers/plex/__init__.py | 19 +- music_assistant/providers/qobuz/__init__.py | 6 +- .../providers/radiobrowser/__init__.py | 6 +- .../providers/siriusxm/__init__.py | 6 +- .../providers/slimproto/__init__.py | 19 +- .../providers/snapcast/__init__.py | 18 +- music_assistant/providers/sonos/const.py | 2 +- music_assistant/providers/sonos/player.py | 18 +- music_assistant/providers/sonos/provider.py | 22 +- .../providers/sonos_s1/__init__.py | 23 ++- music_assistant/providers/sonos_s1/player.py | 12 +- .../providers/soundcloud/__init__.py | 6 +- music_assistant/providers/spotify/__init__.py | 10 +- music_assistant/providers/test/__init__.py | 4 +- .../providers/theaudiodb/__init__.py | 6 +- music_assistant/providers/tidal/__init__.py | 6 +- music_assistant/providers/tunein/__init__.py | 6 +- music_assistant/providers/ytmusic/__init__.py | 6 +- pyproject.toml | 2 +- requirements_all.txt | 2 +- 42 files changed, 321 insertions(+), 331 deletions(-) diff --git a/music_assistant/controllers/players.py b/music_assistant/controllers/players.py index 24c8d282d..c0ee63978 100644 --- a/music_assistant/controllers/players.py +++ b/music_assistant/controllers/players.py @@ -110,6 +110,12 @@ def __init__(self, *args, **kwargs) -> None: self.manifest.icon = "speaker-multiple" self._poll_task: asyncio.Task | None = None self._player_throttlers: dict[str, Throttler] = {} + # TEMP 2024-11-20: register some aliases for renamed commands + # remove after a few releases + self.mass.register_api_command("players/cmd/sync", self.cmd_group) + self.mass.register_api_command("players/cmd/unsync", self.cmd_ungroup) + self.mass.register_api_command("players/cmd/sync_many", self.cmd_group_many) + self.mass.register_api_command("players/cmd/unsync_many", self.cmd_ungroup_many) async def setup(self, config: CoreConfig) -> None: """Async initialize of module.""" @@ -318,10 +324,10 @@ async def cmd_power(self, player_id: str, powered: bool, skip_update: bool = Fal if player.powered == powered: return # nothing to do - # unsync player at power off + # ungroup player at power off player_was_synced = player.synced_to is not None if not powered and (player.synced_to): - await self.cmd_unsync(player_id) + await self.cmd_ungroup(player_id) # always stop player at power off if ( @@ -615,84 +621,26 @@ async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: async with self._player_throttlers[player_id]: await player_prov.enqueue_next_media(player_id=player_id, media=media) - @api_command("players/cmd/sync") + @api_command("players/cmd/group") @handle_player_command - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for given player. + async def cmd_group(self, player_id: str, target_player: str) -> None: + """Handle GROUP command for given player. Join/add the given player(id) to the given (leader) player/sync group. - If the player is already synced to another player, it will be unsynced there first. If the target player itself is already synced to another player, this may fail. If the player can not be synced with the given target player, this may fail. - player_id: player_id of the player to handle the command. - target_player: player_id of the syncgroup leader or group player. """ - await self.cmd_sync_many(target_player, [player_id]) + await self.cmd_group_many(target_player, [player_id]) - @api_command("players/cmd/unsync") - @handle_player_command - async def cmd_unsync(self, player_id: str) -> None: - """Handle UNSYNC command for given player. - - Remove the given player from any syncgroups it currently is synced to. - If the player is not currently synced to any other player, - this will silently be ignored. - - - player_id: player_id of the player to handle the command. - """ - if not (player := self.get(player_id)): - self.logger.warning("Player %s is not available", player_id) - return - if PlayerFeature.SYNC not in player.supported_features: - self.logger.warning("Player %s does not support (un)sync commands", player.name) - return - if not (player.synced_to or player.group_childs): - return # nothing to do - - if player.active_group and ( - (group_provider := self.get_player_provider(player.active_group)) - and group_provider.domain == "player_group" - ): - # the player is part of a permanent (sync)group and the user tries to unsync - # redirect the command to the group provider - group_provider = cast(PlayerGroupProvider, group_provider) - await group_provider.cmd_unsync_member(player_id, player.active_group) - return - - # handle (edge)case where un unsync command is sent to a sync leader; - # we dissolve the entire syncgroup in this case. - # while maybe not strictly needed to do this for all player providers, - # we do this to keep the functionality consistent across all providers - if player.group_childs: - self.logger.warning( - "Detected unsync command to player %s which is a sync(group) leader, " - "all sync members will be unsynced!", - player.name, - ) - async with TaskManager(self.mass) as tg: - for group_child_id in player.group_childs: - if group_child_id == player_id: - continue - tg.create_task(self.cmd_unsync(group_child_id)) - return - - # (optimistically) reset active source player if it is unsynced - player.active_source = None - - # forward command to the player provider - if player_provider := self.get_player_provider(player_id): - await player_provider.cmd_unsync(player_id) - # if the command succeeded we optimistically reset the sync state - # this is to prevent race conditions and to update the UI as fast as possible - player.synced_to = None - - @api_command("players/cmd/sync_many") - async def cmd_sync_many(self, target_player: str, child_player_ids: list[str]) -> None: - """Create temporary sync group by joining given players to target player.""" + @api_command("players/cmd/group_many") + async def cmd_group_many(self, target_player: str, child_player_ids: list[str]) -> None: + """Join given player(s) to target player.""" parent_player: Player = self.get(target_player, True) prev_group_childs = parent_player.group_childs.copy() - if PlayerFeature.SYNC not in parent_player.supported_features: + if PlayerFeature.SET_MEMBERS not in parent_player.supported_features: msg = f"Player {parent_player.name} does not support sync commands" raise UnsupportedFeaturedException(msg) @@ -700,7 +648,7 @@ async def cmd_sync_many(self, target_player: str, child_player_ids: list[str]) - # guard edge case: player already synced to another player raise PlayerCommandFailed( f"Player {parent_player.name} is already synced to another player on its own, " - "you need to unsync it first before you can join other players to it.", + "you need to ungroup it first before you can join other players to it.", ) # filter all player ids on compatibility and availability @@ -711,10 +659,16 @@ async def cmd_sync_many(self, target_player: str, child_player_ids: list[str]) - if not (child_player := self.get(child_player_id)) or not child_player.available: self.logger.warning("Player %s is not available", child_player_id) continue - if PlayerFeature.SYNC not in child_player.supported_features: - # this should not happen, but just in case bad things happen, guard it - self.logger.warning("Player %s does not support sync commands", child_player.name) - continue + # check if player can be synced/grouped with the target player + if not ( + child_player_id in parent_player.can_group_with + or child_player.provider in parent_player.can_group_with + or "*" in parent_player.can_group_with + ): + raise UnsupportedFeaturedException( + f"Player {child_player.name} can not be grouped with {parent_player.name}" + ) + if child_player.synced_to and child_player.synced_to == target_player: continue # already synced to this target @@ -722,15 +676,15 @@ async def cmd_sync_many(self, target_player: str, child_player_ids: list[str]) - # guard edge case: childplayer is already a sync leader on its own raise PlayerCommandFailed( f"Player {child_player.name} is already synced with other players, " - "you need to unsync it first before you can join it to another player.", + "you need to ungroup it first before you can join it to another player.", ) if child_player.synced_to: - # player already synced to another player, unsync first + # player already synced to another player, ungroup first self.logger.warning( - "Player %s is already synced to another player, unsyncing first", + "Player %s is already synced to another player, ungrouping first", child_player.name, ) - await self.cmd_unsync(child_player.player_id) + await self.cmd_ungroup(child_player.player_id) # power on the player if needed if not child_player.powered: await self.cmd_power(child_player.player_id, True, skip_update=True) @@ -743,17 +697,74 @@ async def cmd_sync_many(self, target_player: str, child_player_ids: list[str]) - player_provider = self.get_player_provider(target_player) async with self._player_throttlers[target_player]: try: - await player_provider.cmd_sync_many(target_player, final_player_ids) + await player_provider.cmd_group_many(target_player, final_player_ids) except Exception: # restore sync state if the command failed - parent_player.group_childs = prev_group_childs + parent_player.group_childs.set(prev_group_childs) raise - @api_command("players/cmd/unsync_many") - async def cmd_unsync_many(self, player_ids: list[str]) -> None: - """Handle UNSYNC command for all the given players.""" + @api_command("players/cmd/ungroup") + @handle_player_command + async def cmd_ungroup(self, player_id: str) -> None: + """Handle UNGROUP command for given player. + + Remove the given player from any (sync)groups it currently is synced to. + If the player is not currently grouped to any other player, + this will silently be ignored. + + - player_id: player_id of the player to handle the command. + """ + if not (player := self.get(player_id)): + self.logger.warning("Player %s is not available", player_id) + return + if PlayerFeature.SET_MEMBERS not in player.supported_features: + self.logger.warning("Player %s does not support (un)group commands", player.name) + return + if not (player.synced_to or player.group_childs): + return # nothing to do + + if player.active_group and ( + (group_provider := self.get_player_provider(player.active_group)) + and group_provider.domain == "player_group" + ): + # the player is part of a permanent (sync)group and the user tries to ungroup + # redirect the command to the group provider + group_provider = cast(PlayerGroupProvider, group_provider) + await group_provider.cmd_ungroup_member(player_id, player.active_group) + return + + # handle (edge)case where un ungroup command is sent to a sync leader; + # we dissolve the entire syncgroup in this case. + # while maybe not strictly needed to do this for all player providers, + # we do this to keep the functionality consistent across all providers + if player.group_childs: + self.logger.warning( + "Detected ungroup command to player %s which is a sync(group) leader, " + "all sync members will be ungrouped!", + player.name, + ) + async with TaskManager(self.mass) as tg: + for group_child_id in player.group_childs: + if group_child_id == player_id: + continue + tg.create_task(self.cmd_ungroup(group_child_id)) + return + + # (optimistically) reset active source player if it is ungrouped + player.active_source = None + + # forward command to the player provider + if player_provider := self.get_player_provider(player_id): + await player_provider.cmd_ungroup(player_id) + # if the command succeeded we optimistically reset the sync state + # this is to prevent race conditions and to update the UI as fast as possible + player.synced_to = None + + @api_command("players/cmd/ungroup_many") + async def cmd_ungroup_many(self, player_ids: list[str]) -> None: + """Handle UNGROUP command for all the given players.""" for player_id in list(player_ids): - await self.cmd_unsync(player_id) + await self.cmd_ungroup(player_id) def set(self, player: Player) -> None: """Set/Update player details on the controller.""" @@ -851,8 +862,10 @@ def update( player.active_source = self._get_active_source(player) player.volume_level = player.volume_level or 0 # guard for None volume # correct group_members if needed - if player.group_childs == {player.player_id}: - player.group_childs = set() + if player.group_childs == [player.player_id]: + player.group_childs.clear() + elif player.group_childs and player.player_id not in player.group_childs: + player.group_childs.set([player.player_id, *player.group_childs]) # Auto correct player state if player is synced (or group child) # This is because some players/providers do not accurately update this info # for the sync child's. @@ -1135,7 +1148,7 @@ def _get_group_volume_level(self, player: Player) -> int: def _handle_player_unavailable(self, player: Player) -> None: """Handle a player becoming unavailable.""" if player.synced_to: - self.mass.create_task(self.cmd_unsync(player.player_id)) + self.mass.create_task(self.cmd_ungroup(player.player_id)) # also set this optimistically because the above command will most likely fail player.synced_to = None return @@ -1146,7 +1159,7 @@ def _handle_player_unavailable(self, player: Player) -> None: self.mass.create_task(self.cmd_power(group_child_id, False, True)) # also set this optimistically because the above command will most likely fail child_player.synced_to = None - player.group_childs = set() + player.group_childs.clear() if player.active_group and (group_player := self.get(player.active_group)): # remove player from group if its part of a group group_player = self.get(player.active_group) @@ -1179,13 +1192,13 @@ async def _play_announcement( queue = self.mass.player_queues.get(player.active_source) prev_queue_active = queue and queue.active prev_item_id = player.current_item_id - # unsync player if its currently synced + # ungroup player if its currently synced if prev_synced_to: self.logger.debug( - "Announcement to player %s - unsyncing player...", + "Announcement to player %s - ungrouping player...", player.display_name, ) - await self.cmd_unsync(player.player_id) + await self.cmd_ungroup(player.player_id) # stop player if its currently playing elif prev_state in (PlayerState.PLAYING, PlayerState.PAUSED): self.logger.debug( @@ -1267,7 +1280,7 @@ async def _play_announcement( await self.cmd_power(player.player_id, False) return elif prev_synced_to: - await self.cmd_sync(player.player_id, prev_synced_to) + await self.cmd_group(player.player_id, prev_synced_to) elif prev_queue_active and prev_state == PlayerState.PLAYING: await self.mass.player_queues.resume(queue.queue_id, True) elif prev_state == PlayerState.PLAYING: diff --git a/music_assistant/helpers/tags.py b/music_assistant/helpers/tags.py index 818c2d310..55def74b6 100644 --- a/music_assistant/helpers/tags.py +++ b/music_assistant/helpers/tags.py @@ -14,7 +14,6 @@ import eyed3 from music_assistant_models.enums import AlbumType from music_assistant_models.errors import InvalidDataError -from music_assistant_models.media_items import MediaItemChapter from music_assistant.constants import MASS_LOGGER_NAME, UNKNOWN_ARTIST from music_assistant.helpers.process import AsyncProcess @@ -260,14 +259,14 @@ def album_artist_sort_names(self) -> tuple[str, ...]: """Return artist sort name tag(s) if present.""" return split_items(self.tags.get("albumartistsort"), False) + @property + def is_audiobook(self) -> bool: + """Return True if this is an audiobook.""" + return self.filename.endswith("m4b") and len(self.chapters) > 1 + @property def album_type(self) -> AlbumType: """Return albumtype tag if present.""" - # handle audiobook/podcast - if self.filename.endswith("m4b") and len(self.chapters) > 1: - return AlbumType.AUDIOBOOK - if "podcast" in self.tags.get("genre", "").lower() and len(self.chapters) > 1: - return AlbumType.PODCAST if self.tags.get("compilation", "") == "1": return AlbumType.COMPILATION tag = ( @@ -280,8 +279,6 @@ def album_type(self) -> AlbumType: # the album type tag is messy within id3 and may even contain multiple types # try to parse one in order of preference for album_type in ( - AlbumType.PODCAST, - AlbumType.AUDIOBOOK, AlbumType.COMPILATION, AlbumType.EP, AlbumType.SINGLE, @@ -316,20 +313,9 @@ def barcode(self) -> str | None: return None @property - def chapters(self) -> list[MediaItemChapter]: + def chapters(self) -> list[dict[str, Any]]: """Return chapters in MediaItem (if any).""" - chapters: list[MediaItemChapter] = [] - if raw_chapters := self.raw.get("chapters"): - for chapter_data in raw_chapters: - chapters.append( - MediaItemChapter( - chapter_id=chapter_data["id"], - position_start=chapter_data["start"], - position_end=chapter_data["end"], - title=chapter_data.get("tags", {}).get("title"), - ) - ) - return chapters + return self.raw.get("chapters") or [] @property def lyrics(self) -> str | None: diff --git a/music_assistant/models/metadata_provider.py b/music_assistant/models/metadata_provider.py index 25f28aa70..bf674961b 100644 --- a/music_assistant/models/metadata_provider.py +++ b/music_assistant/models/metadata_provider.py @@ -13,11 +13,11 @@ # ruff: noqa: ARG001, ARG002 -DEFAULT_SUPPORTED_FEATURES = ( +DEFAULT_SUPPORTED_FEATURES = { ProviderFeature.ARTIST_METADATA, ProviderFeature.ALBUM_METADATA, ProviderFeature.TRACK_METADATA, -) +} class MetadataProvider(Provider): @@ -27,7 +27,7 @@ class MetadataProvider(Provider): """ @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" return DEFAULT_SUPPORTED_FEATURES diff --git a/music_assistant/models/player_provider.py b/music_assistant/models/player_provider.py index a9491a3f4..784e04069 100644 --- a/music_assistant/models/player_provider.py +++ b/music_assistant/models/player_provider.py @@ -169,8 +169,8 @@ async def cmd_previous(self, player_id: str) -> None: # will only be called for players with 'next_previous' feature set. raise NotImplementedError - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for given player. + async def cmd_group(self, player_id: str, target_player: str) -> None: + """Handle GROUP command for given player. Join/add the given player(id) to the given (master) player/sync group. @@ -180,21 +180,21 @@ async def cmd_sync(self, player_id: str, target_player: str) -> None: # will only be called for players with SYNC feature set. raise NotImplementedError - async def cmd_unsync(self, player_id: str) -> None: - """Handle UNSYNC command for given player. + async def cmd_ungroup(self, player_id: str) -> None: + """Handle UNGROUP command for given player. - Remove the given player from any syncgroups it currently is synced to. + Remove the given player from any (sync)groups it currently is grouped to. - player_id: player_id of the player to handle the command. """ # will only be called for players with SYNC feature set. raise NotImplementedError - async def cmd_sync_many(self, target_player: str, child_player_ids: list[str]) -> None: + async def cmd_group_many(self, target_player: str, child_player_ids: list[str]) -> None: """Create temporary sync group by joining given players to target player.""" for child_id in child_player_ids: - # default implementation, simply call the cmd_sync for all child players - await self.cmd_sync(child_id, target_player) + # default implementation, simply call the cmd_group for all child players + await self.cmd_group(child_id, target_player) async def poll_player(self, player_id: str) -> None: """Poll player for state updates. diff --git a/music_assistant/models/provider.py b/music_assistant/models/provider.py index 89b133799..7b406df67 100644 --- a/music_assistant/models/provider.py +++ b/music_assistant/models/provider.py @@ -42,7 +42,7 @@ def __init__( self.available = False @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" return () diff --git a/music_assistant/providers/_template_music_provider/__init__.py b/music_assistant/providers/_template_music_provider/__init__.py index 89cbba27d..89b787b3f 100644 --- a/music_assistant/providers/_template_music_provider/__init__.py +++ b/music_assistant/providers/_template_music_provider/__init__.py @@ -127,7 +127,7 @@ class MyDemoMusicprovider(MusicProvider): """ @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" # MANDATORY # you should return a tuple of provider-level features diff --git a/music_assistant/providers/_template_player_provider/__init__.py b/music_assistant/providers/_template_player_provider/__init__.py index ae3cd3090..6cbb659bb 100644 --- a/music_assistant/providers/_template_player_provider/__init__.py +++ b/music_assistant/providers/_template_player_provider/__init__.py @@ -105,7 +105,7 @@ class MyDemoPlayerprovider(PlayerProvider): """ @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" # MANDATORY # you should return a tuple of provider-level features @@ -210,7 +210,7 @@ async def on_mdns_service_state_change( device_info=DeviceInfo( model="Model XYX", manufacturer="Super Brand", - address=cur_address, + ip_address=cur_address, ), # set the supported features for this player only with # the ones the player actually supports @@ -339,8 +339,8 @@ async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None: """ # this method should handle the enqueuing of the next queue item on the player. - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for given player. + async def cmd_group(self, player_id: str, target_player: str) -> None: + """Handle GROUP command for given player. Join/add the given player(id) to the given (master) player/sync group. @@ -351,10 +351,10 @@ async def cmd_sync(self, player_id: str, target_player: str) -> None: # this method should handle the sync command for the given player. # you should join the given player to the target_player/syncgroup. - async def cmd_unsync(self, player_id: str) -> None: - """Handle UNSYNC command for given player. + async def cmd_ungroup(self, player_id: str) -> None: + """Handle UNGROUP command for given player. - Remove the given player from any syncgroups it currently is synced to. + Remove the given player from any (sync)groups it currently is grouped to. - player_id: player_id of the player to handle the command. """ diff --git a/music_assistant/providers/airplay/provider.py b/music_assistant/providers/airplay/provider.py index b625ed894..143d4de16 100644 --- a/music_assistant/providers/airplay/provider.py +++ b/music_assistant/providers/airplay/provider.py @@ -145,7 +145,7 @@ class AirplayProvider(PlayerProvider): _play_media_lock: asyncio.Lock = asyncio.Lock() @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" return (ProviderFeature.SYNC_PLAYERS,) @@ -210,7 +210,7 @@ async def on_mdns_service_state_change( mass_player.device_info = DeviceInfo( model=mass_player.device_info.model, manufacturer=mass_player.device_info.manufacturer, - address=str(cur_address), + ip_address=str(cur_address), ) if not mass_player.available: self.logger.debug("Player back online: %s", display_name) @@ -347,8 +347,8 @@ async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: await self.mass.cache.set(player_id, volume_level, base_key=CACHE_KEY_PREV_VOLUME) @lock - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for given player. + async def cmd_group(self, player_id: str, target_player: str) -> None: + """Handle GROUP command for given player. Join/add the given player(id) to the given (master) player/sync group. @@ -398,10 +398,10 @@ async def cmd_sync(self, player_id: str, target_player: str) -> None: self.mass.players.update(parent_player.player_id, skip_forward=True) @lock - async def cmd_unsync(self, player_id: str) -> None: - """Handle UNSYNC command for given player. + async def cmd_ungroup(self, player_id: str) -> None: + """Handle UNGROUP command for given player. - Remove the given player from any syncgroups it currently is synced to. + Remove the given player from any (sync)groups it currently is grouped to. - player_id: player_id of the player to handle the command. """ @@ -514,14 +514,15 @@ async def _setup_player( device_info=DeviceInfo( model=model, manufacturer=manufacturer, - address=address, + ip_address=address, ), supported_features=( PlayerFeature.PAUSE, - PlayerFeature.SYNC, + PlayerFeature.SET_MEMBERS, PlayerFeature.VOLUME_SET, ), volume_level=volume, + can_group_with={self.instance_id}, ) await self.mass.players.register_or_update(mass_player) diff --git a/music_assistant/providers/apple_music/__init__.py b/music_assistant/providers/apple_music/__init__.py index 364e53d81..9474c50d7 100644 --- a/music_assistant/providers/apple_music/__init__.py +++ b/music_assistant/providers/apple_music/__init__.py @@ -53,7 +53,7 @@ from music_assistant.models import ProviderInstanceType -SUPPORTED_FEATURES = ( +SUPPORTED_FEATURES = { ProviderFeature.LIBRARY_ARTISTS, ProviderFeature.LIBRARY_ALBUMS, ProviderFeature.LIBRARY_TRACKS, @@ -63,7 +63,7 @@ ProviderFeature.ARTIST_ALBUMS, ProviderFeature.ARTIST_TOPTRACKS, ProviderFeature.SIMILAR_TRACKS, -) +} DEVELOPER_TOKEN = app_var(8) WIDEVINE_BASE_PATH = "/usr/local/bin/widevine_cdm" @@ -127,7 +127,7 @@ async def handle_async_init(self) -> None: self._decrypt_private_key = await _file.read() @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" return SUPPORTED_FEATURES diff --git a/music_assistant/providers/bluesound/__init__.py b/music_assistant/providers/bluesound/__init__.py index ab2258cf7..22f8e7473 100644 --- a/music_assistant/providers/bluesound/__init__.py +++ b/music_assistant/providers/bluesound/__init__.py @@ -37,7 +37,7 @@ PLAYER_FEATURES_BASE = { - PlayerFeature.SYNC, + PlayerFeature.SET_MEMBERS, PlayerFeature.VOLUME_MUTE, PlayerFeature.PAUSE, } @@ -188,9 +188,10 @@ async def update_attributes(self) -> None: if self.sync_status.leader is None: if self.sync_status.followers: - self.mass_player.group_childs = ( - self.sync_status.followers if len(self.sync_status.followers) > 1 else set() - ) + if len(self.sync_status.followers) > 1: + self.mass_player.group_childs.set(self.sync_status.followers) + else: + self.mass_player.group_childs.clear() self.mass_player.synced_to = None if self.status.state == "stream": @@ -205,7 +206,7 @@ async def update_attributes(self) -> None: self.mass_player.current_media = None else: - self.mass_player.group_childs = set() + self.mass_player.group_childs.clear() self.mass_player.synced_to = self.sync_status.leader self.mass_player.active_source = self.sync_status.leader @@ -219,9 +220,9 @@ class BluesoundPlayerProvider(PlayerProvider): bluos_players: dict[str, BluesoundPlayer] @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" - return (ProviderFeature.SYNC_PLAYERS,) + return {ProviderFeature.SYNC_PLAYERS} async def handle_async_init(self) -> None: """Handle async initialization of the provider.""" @@ -257,7 +258,7 @@ async def on_mdns_service_state_change( mass_player.device_info = DeviceInfo( model=mass_player.device_info.model, manufacturer=mass_player.device_info.manufacturer, - address=str(cur_address), + ip_address=str(cur_address), ) if not mass_player.available: self.logger.debug("Player back online: %s", mass_player.display_name) @@ -284,7 +285,7 @@ async def on_mdns_service_state_change( device_info=DeviceInfo( model="BluOS speaker", manufacturer="Bluesound", - address=cur_address, + ip_address=cur_address, ), # Set the supported features for this player supported_features=( @@ -294,6 +295,7 @@ async def on_mdns_service_state_change( ), needs_poll=True, poll_interval=30, + can_group_with={self.instance_id}, ) await self.mass.players.register(mass_player) @@ -392,12 +394,12 @@ async def poll_player(self, player_id: str) -> None: if bluos_player := self.bluos_players[player_id]: await bluos_player.update_attributes() - # TODO fix sync & unsync + # TODO fix sync & ungroup - async def cmd_sync(self, player_id: str, target_player: str) -> None: + async def cmd_group(self, player_id: str, target_player: str) -> None: """Handle SYNC command for BluOS player.""" - async def cmd_unsync(self, player_id: str) -> None: - """Handle UNSYNC command for BluOS player.""" + async def cmd_ungroup(self, player_id: str) -> None: + """Handle UNGROUP command for BluOS player.""" if bluos_player := self.bluos_players[player_id]: await bluos_player.client.player.leave_group() diff --git a/music_assistant/providers/builtin/__init__.py b/music_assistant/providers/builtin/__init__.py index 6872d751d..74d955e0e 100644 --- a/music_assistant/providers/builtin/__init__.py +++ b/music_assistant/providers/builtin/__init__.py @@ -165,9 +165,9 @@ def is_streaming_provider(self) -> bool: return False @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" - return ( + return { ProviderFeature.BROWSE, ProviderFeature.LIBRARY_TRACKS, ProviderFeature.LIBRARY_RADIOS, @@ -176,7 +176,7 @@ def supported_features(self) -> tuple[ProviderFeature, ...]: ProviderFeature.LIBRARY_RADIOS_EDIT, ProviderFeature.PLAYLIST_CREATE, ProviderFeature.PLAYLIST_TRACKS_EDIT, - ) + } async def get_track(self, prov_track_id: str) -> Track: """Get full track details by id.""" diff --git a/music_assistant/providers/chromecast/__init__.py b/music_assistant/providers/chromecast/__init__.py index 549a7c269..b257eff40 100644 --- a/music_assistant/providers/chromecast/__init__.py +++ b/music_assistant/providers/chromecast/__init__.py @@ -386,7 +386,7 @@ def _on_chromecast_discovered(self, uuid, _) -> None: powered=False, device_info=DeviceInfo( model=cast_info.model_name, - address=f"{cast_info.host}:{cast_info.port}", + ip_address=f"{cast_info.host}:{cast_info.port}", manufacturer=cast_info.manufacturer, ), supported_features=( @@ -437,19 +437,19 @@ def on_new_cast_status(self, castplayer: CastPlayer, status: CastStatus) -> None # handle stereo pairs if castplayer.cast_info.is_multichannel_group: castplayer.player.type = PlayerType.STEREO_PAIR - castplayer.player.group_childs = set() + castplayer.player.group_childs.clear() # handle cast groups if castplayer.cast_info.is_audio_group and not castplayer.cast_info.is_multichannel_group: castplayer.player.type = PlayerType.GROUP - castplayer.player.group_childs = { + castplayer.player.group_childs.set( str(UUID(x)) for x in castplayer.mz_controller.members - } - castplayer.player.supported_features = ( + ) + castplayer.player.supported_features = { PlayerFeature.POWER, PlayerFeature.VOLUME_SET, PlayerFeature.PAUSE, PlayerFeature.ENQUEUE, - ) + } # update player status castplayer.player.name = castplayer.cast_info.friendly_name @@ -578,7 +578,7 @@ def on_new_connection_status(self, castplayer: CastPlayer, status: ConnectionSta castplayer.player.available = new_available castplayer.player.device_info = DeviceInfo( model=castplayer.cast_info.model_name, - address=f"{castplayer.cast_info.host}:{castplayer.cast_info.port}", + ip_address=f"{castplayer.cast_info.host}:{castplayer.cast_info.port}", manufacturer=castplayer.cast_info.manufacturer, ) self.mass.loop.call_soon_threadsafe(self.mass.players.update, castplayer.player_id) diff --git a/music_assistant/providers/deezer/__init__.py b/music_assistant/providers/deezer/__init__.py index eb79c4900..3b6b89d5b 100644 --- a/music_assistant/providers/deezer/__init__.py +++ b/music_assistant/providers/deezer/__init__.py @@ -48,7 +48,7 @@ from .gw_client import GWClient -SUPPORTED_FEATURES = ( +SUPPORTED_FEATURES = { ProviderFeature.LIBRARY_ARTISTS, ProviderFeature.LIBRARY_ALBUMS, ProviderFeature.LIBRARY_TRACKS, @@ -68,7 +68,7 @@ ProviderFeature.PLAYLIST_CREATE, ProviderFeature.RECOMMENDATIONS, ProviderFeature.SIMILAR_TRACKS, -) +} @dataclass @@ -190,7 +190,7 @@ async def handle_async_init(self) -> None: await self.gw_client.setup() @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" return SUPPORTED_FEATURES diff --git a/music_assistant/providers/dlna/__init__.py b/music_assistant/providers/dlna/__init__.py index 10f651ee2..463159205 100644 --- a/music_assistant/providers/dlna/__init__.py +++ b/music_assistant/providers/dlna/__init__.py @@ -493,7 +493,7 @@ async def _device_discovered(self, udn: str, description_url: str) -> None: # device info will be discovered later after connect device_info=DeviceInfo( model="unknown", - address=description_url, + ip_address=description_url, manufacturer="unknown", ), needs_poll=True, @@ -542,7 +542,7 @@ async def _device_connect(self, dlna_player: DLNAPlayer) -> None: # connect was successful, update device info dlna_player.player.device_info = DeviceInfo( model=dlna_player.device.model_name, - address=dlna_player.device.device.presentation_url + ip_address=dlna_player.device.device.presentation_url or dlna_player.description_url, manufacturer=dlna_player.device.manufacturer, ) @@ -611,4 +611,4 @@ def _set_player_features(self, dlna_player: DLNAPlayer) -> None: supported_features.add(PlayerFeature.VOLUME_MUTE) if dlna_player.device.has_pause: supported_features.add(PlayerFeature.PAUSE) - dlna_player.player.supported_features = tuple(supported_features) + dlna_player.player.supported_features = supported_features diff --git a/music_assistant/providers/fanarttv/__init__.py b/music_assistant/providers/fanarttv/__init__.py index 9b8a65c14..a8e68a74c 100644 --- a/music_assistant/providers/fanarttv/__init__.py +++ b/music_assistant/providers/fanarttv/__init__.py @@ -23,10 +23,10 @@ from music_assistant import MusicAssistant from music_assistant.models import ProviderInstanceType -SUPPORTED_FEATURES = ( +SUPPORTED_FEATURES = { ProviderFeature.ARTIST_METADATA, ProviderFeature.ALBUM_METADATA, -) +} CONF_ENABLE_ARTIST_IMAGES = "enable_artist_images" CONF_ENABLE_ALBUM_IMAGES = "enable_album_images" @@ -101,7 +101,7 @@ async def handle_async_init(self) -> None: self.throttler = Throttler(rate_limit=1, period=30) @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" return SUPPORTED_FEATURES diff --git a/music_assistant/providers/filesystem_local/__init__.py b/music_assistant/providers/filesystem_local/__init__.py index 317d8aa62..dc530474c 100644 --- a/music_assistant/providers/filesystem_local/__init__.py +++ b/music_assistant/providers/filesystem_local/__init__.py @@ -111,14 +111,14 @@ SEEKABLE_FILES = (ContentType.MP3, ContentType.WAV, ContentType.FLAC) -SUPPORTED_FEATURES = ( +SUPPORTED_FEATURES = { ProviderFeature.LIBRARY_ARTISTS, ProviderFeature.LIBRARY_ALBUMS, ProviderFeature.LIBRARY_TRACKS, ProviderFeature.LIBRARY_PLAYLISTS, ProviderFeature.BROWSE, ProviderFeature.SEARCH, -) +} listdir = wrap(os.listdir) isdir = wrap(os.path.isdir) @@ -175,14 +175,14 @@ class LocalFileSystemProvider(MusicProvider): scan_limiter = asyncio.Semaphore(25) @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" if self.write_access: - return ( + return { *SUPPORTED_FEATURES, ProviderFeature.PLAYLIST_CREATE, ProviderFeature.PLAYLIST_TRACKS_EDIT, - ) + } return SUPPORTED_FEATURES @property diff --git a/music_assistant/providers/fully_kiosk/__init__.py b/music_assistant/providers/fully_kiosk/__init__.py index e773dcd0f..ad4076e1f 100644 --- a/music_assistant/providers/fully_kiosk/__init__.py +++ b/music_assistant/providers/fully_kiosk/__init__.py @@ -125,7 +125,7 @@ async def loaded_in_mass(self) -> None: device_info=DeviceInfo( model=self._fully.deviceInfo["deviceModel"], manufacturer=self._fully.deviceInfo["deviceManufacturer"], - address=address, + ip_address=address, ), supported_features=(PlayerFeature.VOLUME_SET,), needs_poll=True, diff --git a/music_assistant/providers/hass_players/__init__.py b/music_assistant/providers/hass_players/__init__.py index 5095749c3..40790479a 100644 --- a/music_assistant/providers/hass_players/__init__.py +++ b/music_assistant/providers/hass_players/__init__.py @@ -334,8 +334,8 @@ async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: target={"entity_id": player_id}, ) - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for given player. + async def cmd_group(self, player_id: str, target_player: str) -> None: + """Handle GROUP command for given player. Join/add the given player(id) to the given (master) player/sync group. @@ -350,10 +350,10 @@ async def cmd_sync(self, player_id: str, target_player: str) -> None: target={"entity_id": target_player}, ) - async def cmd_unsync(self, player_id: str) -> None: - """Handle UNSYNC command for given player. + async def cmd_ungroup(self, player_id: str) -> None: + """Handle UNGROUP command for given player. - Remove the given player from any syncgroups it currently is synced to. + Remove the given player from any (sync)groups it currently is grouped to. - player_id: player_id of the player to handle the command. """ @@ -460,13 +460,13 @@ def _update_player_attributes(self, player: Player, attributes: dict[str, Any]) player.current_item_id = value if key == "group_members": if value and value[0] == player.player_id: - player.group_childs = value + player.group_childs.set(value) player.synced_to = None elif value and value[0] != player.player_id: - player.group_childs = set() + player.group_childs.clear() player.synced_to = value[0] else: - player.group_childs = set() + player.group_childs.clear() player.synced_to = None async def _late_add_player(self, entity_id: str) -> None: diff --git a/music_assistant/providers/jellyfin/__init__.py b/music_assistant/providers/jellyfin/__init__.py index f6a03ed95..a76b235a1 100644 --- a/music_assistant/providers/jellyfin/__init__.py +++ b/music_assistant/providers/jellyfin/__init__.py @@ -153,9 +153,9 @@ async def handle_async_init(self) -> None: raise LoginFailed(f"Authentication failed: {err}") from err @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return a list of supported features.""" - return ( + return { ProviderFeature.LIBRARY_ARTISTS, ProviderFeature.LIBRARY_ALBUMS, ProviderFeature.LIBRARY_TRACKS, @@ -164,7 +164,7 @@ def supported_features(self) -> tuple[ProviderFeature, ...]: ProviderFeature.SEARCH, ProviderFeature.ARTIST_ALBUMS, ProviderFeature.SIMILAR_TRACKS, - ) + } @property def is_streaming_provider(self) -> bool: diff --git a/music_assistant/providers/musicbrainz/__init__.py b/music_assistant/providers/musicbrainz/__init__.py index 95918ad5f..88ee5a5ba 100644 --- a/music_assistant/providers/musicbrainz/__init__.py +++ b/music_assistant/providers/musicbrainz/__init__.py @@ -33,7 +33,7 @@ LUCENE_SPECIAL = r'([+\-&|!(){}\[\]\^"~*?:\\\/])' -SUPPORTED_FEATURES = () +SUPPORTED_FEATURES = set() async def setup( @@ -200,7 +200,7 @@ async def handle_async_init(self) -> None: self.cache = self.mass.cache @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" return SUPPORTED_FEATURES diff --git a/music_assistant/providers/opensubsonic/sonic_provider.py b/music_assistant/providers/opensubsonic/sonic_provider.py index 855946094..9aeae69cb 100644 --- a/music_assistant/providers/opensubsonic/sonic_provider.py +++ b/music_assistant/providers/opensubsonic/sonic_provider.py @@ -112,9 +112,9 @@ async def handle_async_init(self) -> None: self.logger.info("Server does not support transcodeOffset, seeking in player provider") @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return a list of supported features.""" - return ( + return { ProviderFeature.LIBRARY_ARTISTS, ProviderFeature.LIBRARY_ALBUMS, ProviderFeature.LIBRARY_TRACKS, @@ -127,7 +127,7 @@ def supported_features(self) -> tuple[ProviderFeature, ...]: ProviderFeature.SIMILAR_TRACKS, ProviderFeature.PLAYLIST_TRACKS_EDIT, ProviderFeature.PLAYLIST_CREATE, - ) + } @property def is_streaming_provider(self) -> bool: diff --git a/music_assistant/providers/player_group/__init__.py b/music_assistant/providers/player_group/__init__.py index 32afe391a..c78a80790 100644 --- a/music_assistant/providers/player_group/__init__.py +++ b/music_assistant/providers/player_group/__init__.py @@ -158,9 +158,9 @@ def __init__( ] @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" - return (ProviderFeature.REMOVE_PLAYER,) + return {ProviderFeature.REMOVE_PLAYER} async def loaded_in_mass(self) -> None: """Call after the provider has been loaded.""" @@ -279,22 +279,18 @@ async def on_player_config_change(self, config: PlayerConfig, changed_keys: set[ # ensure we filter invalid members members = self._filter_members(config.get_value(CONF_GROUP_TYPE), members) if group_player := self.mass.players.get(config.player_id): - group_player.group_childs = members + group_player.group_childs.set(members) if group_player.powered: # power on group player (which will also resync) if needed await self.cmd_power(group_player.player_id, True) if f"values/{CONFIG_ENTRY_DYNAMIC_MEMBERS.key}" in changed_keys: # dynamic members feature changed if group_player := self.mass.players.get(config.player_id): - if PlayerFeature.SYNC in group_player.supported_features: - group_player.supported_features = tuple( - x for x in group_player.supported_features if x != PlayerFeature.SYNC - ) + if PlayerFeature.SET_MEMBERS in group_player.supported_features: + group_player.supported_features.remove(PlayerFeature.SET_MEMBERS) + group_player.can_group_with.clear() else: - group_player.supported_features = ( - *group_player.supported_features, - PlayerFeature.SYNC, - ) + group_player.supported_features.add(PlayerFeature.SET_MEMBERS) await super().on_player_config_change(config, changed_keys) async def cmd_stop(self, player_id: str) -> None: @@ -352,13 +348,13 @@ async def cmd_power(self, player_id: str, powered: bool) -> None: group_member_ids = self.mass.config.get_raw_player_config_value( player_id, CONF_GROUP_MEMBERS, [] ) - group_player.group_childs = { + group_player.group_childs.set( x for x in group_member_ids if (child_player := self.mass.players.get(x)) and child_player.available and child_player.enabled - } + ) if powered: # handle TURN_ON of the group player by turning on all members @@ -382,7 +378,7 @@ async def cmd_power(self, player_id: str, powered: bool) -> None: else: # handle TURN_OFF of the group player by turning off all members # optimistically set the group state to prevent race conditions - # with the unsync command + # with the ungroup command group_player.powered = False for member in self.mass.players.iter_group_members( group_player, only_powered=True, active_only=True @@ -401,7 +397,7 @@ async def cmd_power(self, player_id: str, powered: bool) -> None: self.mass.players.update(group_player.player_id) if not powered: # reset the group members when powered off - group_player.group_childs = set( + group_player.group_childs.set( self.mass.config.get_raw_player_config_value(player_id, CONF_GROUP_MEMBERS, []) ) @@ -549,11 +545,11 @@ async def remove_player(self, player_id: str) -> None: return if group_player.powered: # edge case: the group player is powered and being removed - # make sure to turn it off first (which will also unsync a syncgroup) + # make sure to turn it off first (which will also ungroup a syncgroup) await self.cmd_power(player_id, False) - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for given player. + async def cmd_group(self, player_id: str, target_player: str) -> None: + """Handle GROUP command for given player. Join/add the given player(id) to the given (master) player/sync group. @@ -576,17 +572,17 @@ async def cmd_sync(self, player_id: str, target_player: str) -> None: f"Adjusting group members is not allowed for group {group_player.display_name}" ) new_members = self._filter_members(group_type, [*group_player.group_childs, player_id]) - group_player.group_childs = new_members + group_player.group_childs.set(new_members) if group_player.powered: # power on group player (which will also resync) if needed await self.cmd_power(target_player, True) - async def cmd_unsync_member(self, player_id: str, target_player: str) -> None: - """Handle UNSYNC command for given player. + async def cmd_ungroup_member(self, player_id: str, target_player: str) -> None: + """Handle UNGROUP command for given player. Remove the given player(id) from the given (master) player/sync group. - - player_id: player_id of the (child) player to unsync from the group. + - player_id: player_id of the (child) player to ungroup from the group. - target_player: player_id of the group player. """ group_player = self.mass.players.get(target_player, raise_unavailable=True) @@ -607,12 +603,12 @@ async def cmd_unsync_member(self, player_id: str, target_player: str) -> None: was_playing = child_player.state == PlayerState.PLAYING # forward command to the player provider if player_provider := self.mass.players.get_player_provider(child_player.player_id): - await player_provider.cmd_unsync(child_player.player_id) + await player_provider.cmd_ungroup(child_player.player_id) child_player.active_group = None child_player.active_source = None - group_player.group_childs = {x for x in group_player.group_childs if x != player_id} + group_player.group_childs.set({x for x in group_player.group_childs if x != player_id}) if is_sync_leader and was_playing: - # unsyncing the sync leader will stop the group so we need to resume + # ungrouping the sync leader will stop the group so we need to resume self.mass.call_later(2, self.mass.players.cmd_play, group_player.player_id) elif group_player.powered: # power on group player (which will also resync) if needed @@ -670,7 +666,7 @@ async def _register_group_player( CONFIG_ENTRY_DYNAMIC_MEMBERS.key, CONFIG_ENTRY_DYNAMIC_MEMBERS.default_value, ): - player_features.add(PlayerFeature.SYNC) + player_features.add(PlayerFeature.SET_MEMBERS) player = Player( player_id=group_player_id, @@ -681,7 +677,6 @@ async def _register_group_player( powered=False, device_info=DeviceInfo(model=model_name, manufacturer=manufacturer), supported_features=tuple(player_features), - group_childs=set(members), active_source=group_player_id, needs_poll=True, poll_interval=30, @@ -741,8 +736,8 @@ async def _sync_syncgroup(self, group_player: Player) -> None: members_to_sync: list[str] = [] for member in self.mass.players.iter_group_members(group_player, active_only=False): if member.synced_to and member.synced_to != sync_leader.player_id: - # unsync first - await self.mass.players.cmd_unsync(member.player_id) + # ungroup first + await self.mass.players.cmd_ungroup(member.player_id) if sync_leader.player_id == member.player_id: # skip sync leader continue @@ -754,7 +749,7 @@ async def _sync_syncgroup(self, group_player: Player) -> None: continue members_to_sync.append(member.player_id) if members_to_sync: - await self.mass.players.cmd_sync_many(sync_leader.player_id, members_to_sync) + await self.mass.players.cmd_group_many(sync_leader.player_id, members_to_sync) async def _on_mass_player_added_event(self, event: MassEvent) -> None: """Handle player added event from player controller.""" diff --git a/music_assistant/providers/plex/__init__.py b/music_assistant/providers/plex/__init__.py index 5bf783d98..519a8feae 100644 --- a/music_assistant/providers/plex/__init__.py +++ b/music_assistant/providers/plex/__init__.py @@ -37,7 +37,6 @@ AudioFormat, ItemMapping, MediaItem, - MediaItemChapter, MediaItemImage, Playlist, ProviderMapping, @@ -380,9 +379,9 @@ def connect() -> PlexServer: raise SetupFailedError from err @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return a list of supported features.""" - return ( + return { ProviderFeature.LIBRARY_ARTISTS, ProviderFeature.LIBRARY_ALBUMS, ProviderFeature.LIBRARY_TRACKS, @@ -390,7 +389,7 @@ def supported_features(self) -> tuple[ProviderFeature, ...]: ProviderFeature.BROWSE, ProviderFeature.SEARCH, ProviderFeature.ARTIST_ALBUMS, - ) + } @property def is_streaming_provider(self) -> bool: @@ -720,17 +719,7 @@ async def _parse_track(self, plex_track: PlexTrack) -> Track: if plex_track.duration: track.duration = int(plex_track.duration / 1000) if plex_track.chapters: - track.metadata.chapters = UniqueList( - [ - MediaItemChapter( - chapter_id=plex_chapter.id, - position_start=plex_chapter.start, - position_end=plex_chapter.end, - title=plex_chapter.title, - ) - for plex_chapter in plex_track.chapters - ] - ) + pass # TODO! return track diff --git a/music_assistant/providers/qobuz/__init__.py b/music_assistant/providers/qobuz/__init__.py index ee502f41b..34402a8ff 100644 --- a/music_assistant/providers/qobuz/__init__.py +++ b/music_assistant/providers/qobuz/__init__.py @@ -61,7 +61,7 @@ from music_assistant.models import ProviderInstanceType -SUPPORTED_FEATURES = ( +SUPPORTED_FEATURES = { ProviderFeature.LIBRARY_ARTISTS, ProviderFeature.LIBRARY_ALBUMS, ProviderFeature.LIBRARY_TRACKS, @@ -75,7 +75,7 @@ ProviderFeature.SEARCH, ProviderFeature.ARTIST_ALBUMS, ProviderFeature.ARTIST_TOPTRACKS, -) +} VARIOUS_ARTISTS_ID = "145383" @@ -137,7 +137,7 @@ async def handle_async_init(self) -> None: raise LoginFailed(msg) @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" return SUPPORTED_FEATURES diff --git a/music_assistant/providers/radiobrowser/__init__.py b/music_assistant/providers/radiobrowser/__init__.py index a38b9f44a..b365bdb6a 100644 --- a/music_assistant/providers/radiobrowser/__init__.py +++ b/music_assistant/providers/radiobrowser/__init__.py @@ -33,7 +33,7 @@ from music_assistant.controllers.cache import use_cache from music_assistant.models.music_provider import MusicProvider -SUPPORTED_FEATURES = ( +SUPPORTED_FEATURES = { ProviderFeature.SEARCH, ProviderFeature.BROWSE, # RadioBrowser doesn't support a library feature at all @@ -41,7 +41,7 @@ # have that included in backups so we store it in the config. ProviderFeature.LIBRARY_RADIOS, ProviderFeature.LIBRARY_RADIOS_EDIT, -) +} if TYPE_CHECKING: from music_assistant_models.config_entries import ConfigValueType, ProviderConfig @@ -93,7 +93,7 @@ class RadioBrowserProvider(MusicProvider): """Provider implementation for RadioBrowser.""" @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" return SUPPORTED_FEATURES diff --git a/music_assistant/providers/siriusxm/__init__.py b/music_assistant/providers/siriusxm/__init__.py index 339e86229..d90687301 100644 --- a/music_assistant/providers/siriusxm/__init__.py +++ b/music_assistant/providers/siriusxm/__init__.py @@ -112,12 +112,12 @@ class SiriusXMProvider(MusicProvider): _current_stream_details: StreamDetails | None = None @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" - return ( + return { ProviderFeature.BROWSE, ProviderFeature.LIBRARY_RADIOS, - ) + } async def handle_async_init(self) -> None: """Handle async initialization of the provider.""" diff --git a/music_assistant/providers/slimproto/__init__.py b/music_assistant/providers/slimproto/__init__.py index 7394d17f0..c355660c0 100644 --- a/music_assistant/providers/slimproto/__init__.py +++ b/music_assistant/providers/slimproto/__init__.py @@ -225,9 +225,9 @@ class SlimprotoProvider(PlayerProvider): _multi_streams: dict[str, MultiClientStream] @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" - return (ProviderFeature.SYNC_PLAYERS,) + return {ProviderFeature.SYNC_PLAYERS} async def handle_async_init(self) -> None: """Handle async initialization of the provider.""" @@ -535,8 +535,8 @@ async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: if slimplayer := self.slimproto.get_player(player_id): await slimplayer.mute(muted) - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for given player.""" + async def cmd_group(self, player_id: str, target_player: str) -> None: + """Handle GROUP command for given player.""" child_player = self.mass.players.get(player_id) assert child_player # guard parent_player = self.mass.players.get(target_player) @@ -569,10 +569,10 @@ async def cmd_sync(self, player_id: str, target_player: str) -> None: self.mass.players.update(child_player.player_id, skip_forward=True) self.mass.players.update(parent_player.player_id, skip_forward=True) - async def cmd_unsync(self, player_id: str) -> None: - """Handle UNSYNC command for given player. + async def cmd_ungroup(self, player_id: str) -> None: + """Handle UNGROUP command for given player. - Remove the given player from any syncgroups it currently is synced to. + Remove the given player from any (sync)groups it currently is grouped to. - player_id: player_id of the player to handle the command. """ @@ -638,17 +638,18 @@ async def _handle_player_update(self, slimplayer: SlimClient) -> None: powered=slimplayer.powered, device_info=DeviceInfo( model=slimplayer.device_model, - address=slimplayer.device_address, + ip_address=slimplayer.device_address, manufacturer=slimplayer.device_type, ), supported_features=( PlayerFeature.POWER, - PlayerFeature.SYNC, + PlayerFeature.SET_MEMBERS, PlayerFeature.VOLUME_SET, PlayerFeature.PAUSE, PlayerFeature.VOLUME_MUTE, PlayerFeature.ENQUEUE, ), + can_group_with={self.instance_id}, ) await self.mass.players.register_or_update(player) diff --git a/music_assistant/providers/snapcast/__init__.py b/music_assistant/providers/snapcast/__init__.py index b987c483a..373d7332d 100644 --- a/music_assistant/providers/snapcast/__init__.py +++ b/music_assistant/providers/snapcast/__init__.py @@ -272,9 +272,9 @@ def _generate_and_register_id(self, snap_client_id) -> str: return self._get_ma_id(snap_client_id) @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" - return (ProviderFeature.SYNC_PLAYERS,) + return {ProviderFeature.SYNC_PLAYERS} async def handle_async_init(self) -> None: """Handle async initialization of the provider.""" @@ -373,16 +373,16 @@ def _handle_player_init(self, snap_client: Snapclient) -> None: powered=snap_client.connected, device_info=DeviceInfo( model=snap_client._client.get("host").get("os"), - address=snap_client._client.get("host").get("ip"), + ip_address=snap_client._client.get("host").get("ip"), manufacturer=snap_client._client.get("host").get("arch"), ), supported_features=( - PlayerFeature.SYNC, + PlayerFeature.SET_MEMBERS, PlayerFeature.VOLUME_SET, PlayerFeature.VOLUME_MUTE, ), - group_childs=set(), synced_to=self._synced_to(player_id), + can_group_with={self.instance_id}, ) asyncio.run_coroutine_threadsafe( self.mass.players.register_or_update(player), loop=self.mass.loop @@ -449,7 +449,7 @@ async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: ma_player.volume_muted = snapclient.muted self.mass.players.update(player_id) - async def cmd_sync(self, player_id: str, target_player: str) -> None: + async def cmd_group(self, player_id: str, target_player: str) -> None: """Sync Snapcast player.""" group = self._get_snapgroup(target_player) mass_target_player = self.mass.players.get(target_player) @@ -461,13 +461,13 @@ async def cmd_sync(self, player_id: str, target_player: str) -> None: self.mass.players.update(player_id) self.mass.players.update(target_player) - async def cmd_unsync(self, player_id: str) -> None: - """Unsync Snapcast player.""" + async def cmd_ungroup(self, player_id: str) -> None: + """Ungroup Snapcast player.""" mass_player = self.mass.players.get(player_id) if mass_player.synced_to is None: for mass_child_id in list(mass_player.group_childs): if mass_child_id != player_id: - await self.cmd_unsync(mass_child_id) + await self.cmd_ungroup(mass_child_id) return mass_sync_master_player = self.mass.players.get(mass_player.synced_to) mass_sync_master_player.group_childs.remove(player_id) diff --git a/music_assistant/providers/sonos/const.py b/music_assistant/providers/sonos/const.py index 1daa076ad..5e0245663 100644 --- a/music_assistant/providers/sonos/const.py +++ b/music_assistant/providers/sonos/const.py @@ -13,7 +13,7 @@ } PLAYER_FEATURES_BASE = { - PlayerFeature.SYNC, + PlayerFeature.SET_MEMBERS, PlayerFeature.VOLUME_MUTE, PlayerFeature.PAUSE, PlayerFeature.ENQUEUE, diff --git a/music_assistant/providers/sonos/player.py b/music_assistant/providers/sonos/player.py index a19d3c4ad..67a307df1 100644 --- a/music_assistant/providers/sonos/player.py +++ b/music_assistant/providers/sonos/player.py @@ -128,9 +128,12 @@ async def setup(self) -> None: device_info=DeviceInfo( model=self.discovery_info["device"]["modelDisplayName"], manufacturer=self.prov.manifest.name, - address=self.ip_address, + ip_address=self.ip_address, ), - supported_features=tuple(supported_features), + supported_features=supported_features, + # NOTE: strictly taken we can have multiple sonos households + # but for now we assume we only have one + can_group_with={self.prov.instance_id}, ) self.update_attributes() await self.mass.players.register_or_update(mass_player) @@ -247,11 +250,10 @@ def update_attributes(self) -> None: # noqa: PLR0915 if self.client.player.is_coordinator: # player is group coordinator active_group = self.client.player.group - self.mass_player.group_childs = ( - set(self.client.player.group_members) - if len(self.client.player.group_members) > 1 - else set() - ) + if len(self.client.player.group_members) > 1: + self.mass_player.group_childs.set(self.client.player.group_members) + else: + self.mass_player.group_childs.clear() self.mass_player.synced_to = None else: # player is group child (synced to another player) @@ -260,7 +262,7 @@ def update_attributes(self) -> None: # noqa: PLR0915 # handle race condition where the group parent is not yet discovered return active_group = group_parent.client.player.group - self.mass_player.group_childs = set() + self.mass_player.group_childs.clear() self.mass_player.synced_to = active_group.coordinator_id self.mass_player.active_source = active_group.coordinator_id diff --git a/music_assistant/providers/sonos/provider.py b/music_assistant/providers/sonos/provider.py index fed714df9..99d8d1769 100644 --- a/music_assistant/providers/sonos/provider.py +++ b/music_assistant/providers/sonos/provider.py @@ -47,9 +47,9 @@ class SonosPlayerProvider(PlayerProvider): sonos_players: dict[str, SonosPlayer] @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" - return (ProviderFeature.SYNC_PLAYERS,) + return {ProviderFeature.SYNC_PLAYERS} async def handle_async_init(self) -> None: """Handle async initialization of the provider.""" @@ -102,7 +102,7 @@ async def on_mdns_service_state_change( mass_player.device_info = DeviceInfo( model=mass_player.device_info.model, manufacturer=mass_player.device_info.manufacturer, - address=str(cur_address), + ip_address=str(cur_address), ) if not sonos_player.connected: self.logger.debug("Player back online: %s", mass_player.display_name) @@ -188,27 +188,27 @@ async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: if sonos_player := self.sonos_players[player_id]: await sonos_player.cmd_volume_mute(muted) - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for given player. + async def cmd_group(self, player_id: str, target_player: str) -> None: + """Handle GROUP command for given player. Join/add the given player(id) to the given (master) player/sync group. - player_id: player_id of the player to handle the command. - target_player: player_id of the syncgroup master or group player. """ - await self.cmd_sync_many(target_player, [player_id]) + await self.cmd_group_many(target_player, [player_id]) - async def cmd_sync_many(self, target_player: str, child_player_ids: list[str]) -> None: + async def cmd_group_many(self, target_player: str, child_player_ids: list[str]) -> None: """Create temporary sync group by joining given players to target player.""" sonos_player = self.sonos_players[target_player] await sonos_player.client.player.group.modify_group_members( player_ids_to_add=child_player_ids, player_ids_to_remove=[] ) - async def cmd_unsync(self, player_id: str) -> None: - """Handle UNSYNC command for given player. + async def cmd_ungroup(self, player_id: str) -> None: + """Handle UNGROUP command for given player. - Remove the given player from any syncgroups it currently is synced to. + Remove the given player from any (sync)groups it currently is grouped to. - player_id: player_id of the player to handle the command. """ @@ -244,7 +244,7 @@ async def play_media( x for x in sonos_player.client.player.group.player_ids if x != player_id ] if group_childs: - await self.mass.players.cmd_unsync_many(group_childs) + await self.mass.players.cmd_ungroup_many(group_childs) await self.mass.players.play_media(airplay.player_id, media) if group_childs: # ensure master player is first in the list diff --git a/music_assistant/providers/sonos_s1/__init__.py b/music_assistant/providers/sonos_s1/__init__.py index 7a3fdab49..9a9007ad4 100644 --- a/music_assistant/providers/sonos_s1/__init__.py +++ b/music_assistant/providers/sonos_s1/__init__.py @@ -55,7 +55,7 @@ PLAYER_FEATURES = ( - PlayerFeature.SYNC, + PlayerFeature.SET_MEMBERS, PlayerFeature.VOLUME_MUTE, PlayerFeature.PAUSE, PlayerFeature.ENQUEUE, @@ -138,9 +138,9 @@ class SonosPlayerProvider(PlayerProvider): _discovery_reschedule_timer: asyncio.TimerHandle | None = None @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" - return (ProviderFeature.SYNC_PLAYERS,) + return {ProviderFeature.SYNC_PLAYERS} async def handle_async_init(self) -> None: """Handle async initialization of the provider.""" @@ -255,8 +255,8 @@ def set_volume_mute(player_id: str, muted: bool) -> None: await asyncio.to_thread(set_volume_mute, player_id, muted) - async def cmd_sync(self, player_id: str, target_player: str) -> None: - """Handle SYNC command for given player. + async def cmd_group(self, player_id: str, target_player: str) -> None: + """Handle GROUP command for given player. Join/add the given player(id) to the given (master) player/sync group. @@ -268,10 +268,10 @@ async def cmd_sync(self, player_id: str, target_player: str) -> None: await sonos_master_player.join([sonos_player]) self.mass.call_later(2, sonos_player.poll_speaker) - async def cmd_unsync(self, player_id: str) -> None: - """Handle UNSYNC command for given player. + async def cmd_ungroup(self, player_id: str) -> None: + """Handle UNGROUP command for given player. - Remove the given player from any syncgroups it currently is synced to. + Remove the given player from any (sync)groups it currently is grouped to. - player_id: player_id of the player to handle the command. """ @@ -438,11 +438,12 @@ def _add_player(self, soco: SoCo) -> None: supported_features=PLAYER_FEATURES, device_info=DeviceInfo( model=speaker_info["model_name"], - address=soco.ip_address, + ip_address=soco.ip_address, manufacturer="SONOS", ), needs_poll=True, poll_interval=30, + can_group_with={self.instance_id}, ) self.sonosplayers[player_id] = sonos_player = SonosPlayer( self, @@ -450,10 +451,10 @@ def _add_player(self, soco: SoCo) -> None: mass_player=mass_player, ) if not soco.fixed_volume: - mass_player.supported_features = ( + mass_player.supported_features = { *mass_player.supported_features, PlayerFeature.VOLUME_SET, - ) + } asyncio.run_coroutine_threadsafe( self.mass.players.register_or_update(sonos_player.mass_player), loop=self.mass.loop ) diff --git a/music_assistant/providers/sonos_s1/player.py b/music_assistant/providers/sonos_s1/player.py index a0d0d525e..a66406ce6 100644 --- a/music_assistant/providers/sonos_s1/player.py +++ b/music_assistant/providers/sonos_s1/player.py @@ -44,7 +44,7 @@ LOGGER = logging.getLogger(__name__) PLAYER_FEATURES = ( - PlayerFeature.SYNC, + PlayerFeature.SET_MEMBERS, PlayerFeature.VOLUME_MUTE, PlayerFeature.VOLUME_SET, ) @@ -285,7 +285,7 @@ def update_ip(self, ip_address: str) -> None: self.setup() self.mass_player.device_info = DeviceInfo( model=self.mass_player.device_info.model, - address=ip_address, + ip_address=ip_address, manufacturer=self.mass_player.device_info.manufacturer, ) self.update_player() @@ -621,7 +621,7 @@ def _update_attributes(self) -> None: self.mass_player.powered = False self.mass_player.state = PlayerState.IDLE self.mass_player.synced_to = None - self.mass_player.group_childs = set() + self.mass_player.group_childs.clear() return # transport info (playback state) @@ -651,16 +651,16 @@ def _update_attributes(self) -> None: if self.sync_coordinator: # player is synced to another player self.mass_player.synced_to = self.sync_coordinator.player_id - self.mass_player.group_childs = set() + self.mass_player.group_childs.clear() self.mass_player.active_source = self.sync_coordinator.mass_player.active_source elif len(self.group_members_ids) > 1: # this player is the sync leader in a group self.mass_player.synced_to = None - self.mass_player.group_childs = set(self.group_members_ids) + self.mass_player.group_childs.extend(self.group_members_ids) else: # standalone player, not synced self.mass_player.synced_to = None - self.mass_player.group_childs = set() + self.mass_player.group_childs.clear() def _set_basic_track_info(self, update_position: bool = False) -> None: """Query the speaker to update media metadata and position info.""" diff --git a/music_assistant/providers/soundcloud/__init__.py b/music_assistant/providers/soundcloud/__init__.py index 505f49845..82aaebf7b 100644 --- a/music_assistant/providers/soundcloud/__init__.py +++ b/music_assistant/providers/soundcloud/__init__.py @@ -34,7 +34,7 @@ CONF_CLIENT_ID = "client_id" CONF_AUTHORIZATION = "authorization" -SUPPORTED_FEATURES = ( +SUPPORTED_FEATURES = { ProviderFeature.LIBRARY_ARTISTS, ProviderFeature.LIBRARY_TRACKS, ProviderFeature.LIBRARY_PLAYLISTS, @@ -42,7 +42,7 @@ ProviderFeature.SEARCH, ProviderFeature.ARTIST_TOPTRACKS, ProviderFeature.SIMILAR_TRACKS, -) +} if TYPE_CHECKING: @@ -117,7 +117,7 @@ async def handle_async_init(self) -> None: self._user_id = self._me["id"] @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" return SUPPORTED_FEATURES diff --git a/music_assistant/providers/spotify/__init__.py b/music_assistant/providers/spotify/__init__.py index abc1770ad..a8df34983 100644 --- a/music_assistant/providers/spotify/__init__.py +++ b/music_assistant/providers/spotify/__init__.py @@ -91,7 +91,7 @@ CALLBACK_REDIRECT_URL = "https://music-assistant.io/callback" LIKED_SONGS_FAKE_PLAYLIST_ID_PREFIX = "liked_songs" -SUPPORTED_FEATURES = ( +SUPPORTED_FEATURES = { ProviderFeature.LIBRARY_ARTISTS, ProviderFeature.LIBRARY_ALBUMS, ProviderFeature.LIBRARY_TRACKS, @@ -106,7 +106,7 @@ ProviderFeature.ARTIST_ALBUMS, ProviderFeature.ARTIST_TOPTRACKS, ProviderFeature.SIMILAR_TRACKS, -) +} async def setup( @@ -257,9 +257,9 @@ async def handle_async_init(self) -> None: await self.login() @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" - return ( + return { ProviderFeature.LIBRARY_ARTISTS, ProviderFeature.LIBRARY_ALBUMS, ProviderFeature.LIBRARY_TRACKS, @@ -275,7 +275,7 @@ def supported_features(self) -> tuple[ProviderFeature, ...]: ProviderFeature.ARTIST_ALBUMS, ProviderFeature.ARTIST_TOPTRACKS, ProviderFeature.SIMILAR_TRACKS, - ) + } @property def name(self) -> str: diff --git a/music_assistant/providers/test/__init__.py b/music_assistant/providers/test/__init__.py index 630f64bc6..40f5562f5 100644 --- a/music_assistant/providers/test/__init__.py +++ b/music_assistant/providers/test/__init__.py @@ -82,9 +82,9 @@ def is_streaming_provider(self) -> bool: return False @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" - return (ProviderFeature.LIBRARY_TRACKS,) + return {ProviderFeature.LIBRARY_TRACKS} async def get_track(self, prov_track_id: str) -> Track: """Get full track details by id.""" diff --git a/music_assistant/providers/theaudiodb/__init__.py b/music_assistant/providers/theaudiodb/__init__.py index 73777653b..cfaca28be 100644 --- a/music_assistant/providers/theaudiodb/__init__.py +++ b/music_assistant/providers/theaudiodb/__init__.py @@ -38,11 +38,11 @@ from music_assistant import MusicAssistant from music_assistant.models import ProviderInstanceType -SUPPORTED_FEATURES = ( +SUPPORTED_FEATURES = { ProviderFeature.ARTIST_METADATA, ProviderFeature.ALBUM_METADATA, ProviderFeature.TRACK_METADATA, -) +} IMG_MAPPING = { "strArtistThumb": ImageType.THUMB, @@ -143,7 +143,7 @@ async def handle_async_init(self) -> None: self.throttler = Throttler(rate_limit=1, period=1) @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" return SUPPORTED_FEATURES diff --git a/music_assistant/providers/tidal/__init__.py b/music_assistant/providers/tidal/__init__.py index dedfa653b..097115265 100644 --- a/music_assistant/providers/tidal/__init__.py +++ b/music_assistant/providers/tidal/__init__.py @@ -372,9 +372,9 @@ async def handle_async_init(self) -> None: raise @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" - return ( + return { ProviderFeature.LIBRARY_ARTISTS, ProviderFeature.LIBRARY_ALBUMS, ProviderFeature.LIBRARY_TRACKS, @@ -390,7 +390,7 @@ def supported_features(self) -> tuple[ProviderFeature, ...]: ProviderFeature.SIMILAR_TRACKS, ProviderFeature.BROWSE, ProviderFeature.PLAYLIST_TRACKS_EDIT, - ) + } async def search( self, diff --git a/music_assistant/providers/tunein/__init__.py b/music_assistant/providers/tunein/__init__.py index 835441150..8be3df297 100644 --- a/music_assistant/providers/tunein/__init__.py +++ b/music_assistant/providers/tunein/__init__.py @@ -26,10 +26,10 @@ from music_assistant.helpers.throttle_retry import Throttler from music_assistant.models.music_provider import MusicProvider -SUPPORTED_FEATURES = ( +SUPPORTED_FEATURES = { ProviderFeature.LIBRARY_RADIOS, ProviderFeature.BROWSE, -) +} if TYPE_CHECKING: from collections.abc import AsyncGenerator @@ -82,7 +82,7 @@ class TuneInProvider(MusicProvider): _throttler: Throttler @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" return SUPPORTED_FEATURES diff --git a/music_assistant/providers/ytmusic/__init__.py b/music_assistant/providers/ytmusic/__init__.py index 0443569a9..5672e6ffd 100644 --- a/music_assistant/providers/ytmusic/__init__.py +++ b/music_assistant/providers/ytmusic/__init__.py @@ -100,7 +100,7 @@ ) YTM_PREMIUM_CHECK_TRACK_ID = "dQw4w9WgXcQ" -SUPPORTED_FEATURES = ( +SUPPORTED_FEATURES = { ProviderFeature.LIBRARY_ARTISTS, ProviderFeature.LIBRARY_ALBUMS, ProviderFeature.LIBRARY_TRACKS, @@ -110,7 +110,7 @@ ProviderFeature.ARTIST_ALBUMS, ProviderFeature.ARTIST_TOPTRACKS, ProviderFeature.SIMILAR_TRACKS, -) +} # TODO: fix disabled tests @@ -185,7 +185,7 @@ async def handle_async_init(self) -> None: raise LoginFailed("User does not have Youtube Music Premium") @property - def supported_features(self) -> tuple[ProviderFeature, ...]: + def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" return SUPPORTED_FEATURES diff --git a/pyproject.toml b/pyproject.toml index 715ababb4..cd9e330c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dependencies = [ "mashumaro==3.14", "memory-tempfile==2.2.3", "music-assistant-frontend==v2.9.15", - "music-assistant-models==1.1.0", + "music-assistant-models==1.1.2", "orjson==3.10.7", "pillow==11.0.0", "python-slugify==8.0.4", diff --git a/requirements_all.txt b/requirements_all.txt index 02eb48d9a..a0900ae09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -23,7 +23,7 @@ ifaddr==0.2.0 mashumaro==3.14 memory-tempfile==2.2.3 music-assistant-frontend==v2.9.15 -music-assistant-models==1.1.0 +music-assistant-models==1.1.2 orjson==3.10.7 pillow==11.0.0 pkce==1.0.3