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 24c8d282..c0ee6397 100644 --- a/music_assistant/controllers/players.py +++ b/music_assistant/controllers/players.py @@ -110,6 +110,12 @@ class PlayerController(CoreController): 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 @@ class PlayerController(CoreController): 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 @@ class PlayerController(CoreController): 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 @@ class PlayerController(CoreController): # 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 @@ class PlayerController(CoreController): 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 @@ class PlayerController(CoreController): # 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 @@ class PlayerController(CoreController): 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 @@ class PlayerController(CoreController): 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 @@ class PlayerController(CoreController): 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 @@ class PlayerController(CoreController): 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 @@ class PlayerController(CoreController): 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 @@ class PlayerController(CoreController): 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 818c2d31..55def74b 100644 --- a/music_assistant/helpers/tags.py +++ b/music_assistant/helpers/tags.py @@ -14,7 +14,6 @@ from typing import Any 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 @@ class AudioTags: """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 @@ class AudioTags: # 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 @@ class AudioTags: 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 25f28aa7..bf674961 100644 --- a/music_assistant/models/metadata_provider.py +++ b/music_assistant/models/metadata_provider.py @@ -13,11 +13,11 @@ if TYPE_CHECKING: # 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 a9491a3f..784e0406 100644 --- a/music_assistant/models/player_provider.py +++ b/music_assistant/models/player_provider.py @@ -169,8 +169,8 @@ class PlayerProvider(Provider): # 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 @@ class PlayerProvider(Provider): # 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 89b13379..7b406df6 100644 --- a/music_assistant/models/provider.py +++ b/music_assistant/models/provider.py @@ -42,7 +42,7 @@ class Provider: 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 89cbba27..89b787b3 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 ae3cd309..6cbb659b 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 @@ class MyDemoPlayerprovider(PlayerProvider): 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 @@ class MyDemoPlayerprovider(PlayerProvider): """ # 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 @@ class MyDemoPlayerprovider(PlayerProvider): # 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 b625ed89..143d4de1 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 @@ class AirplayProvider(PlayerProvider): 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 @@ class AirplayProvider(PlayerProvider): 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 @@ class AirplayProvider(PlayerProvider): 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 @@ class AirplayProvider(PlayerProvider): 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 364e53d8..9474c50d 100644 --- a/music_assistant/providers/apple_music/__init__.py +++ b/music_assistant/providers/apple_music/__init__.py @@ -53,7 +53,7 @@ if TYPE_CHECKING: from music_assistant.models import ProviderInstanceType -SUPPORTED_FEATURES = ( +SUPPORTED_FEATURES = { ProviderFeature.LIBRARY_ARTISTS, ProviderFeature.LIBRARY_ALBUMS, ProviderFeature.LIBRARY_TRACKS, @@ -63,7 +63,7 @@ SUPPORTED_FEATURES = ( 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 @@ class AppleMusicProvider(MusicProvider): 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 ab2258cf..22f8e747 100644 --- a/music_assistant/providers/bluesound/__init__.py +++ b/music_assistant/providers/bluesound/__init__.py @@ -37,7 +37,7 @@ if TYPE_CHECKING: PLAYER_FEATURES_BASE = { - PlayerFeature.SYNC, + PlayerFeature.SET_MEMBERS, PlayerFeature.VOLUME_MUTE, PlayerFeature.PAUSE, } @@ -188,9 +188,10 @@ class BluesoundPlayer: 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 @@ class BluesoundPlayer: 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 @@ class BluesoundPlayerProvider(PlayerProvider): 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 @@ class BluesoundPlayerProvider(PlayerProvider): 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 @@ class BluesoundPlayerProvider(PlayerProvider): ), needs_poll=True, poll_interval=30, + can_group_with={self.instance_id}, ) await self.mass.players.register(mass_player) @@ -392,12 +394,12 @@ class BluesoundPlayerProvider(PlayerProvider): 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 6872d751..74d955e0 100644 --- a/music_assistant/providers/builtin/__init__.py +++ b/music_assistant/providers/builtin/__init__.py @@ -165,9 +165,9 @@ class BuiltinProvider(MusicProvider): 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 @@ class BuiltinProvider(MusicProvider): 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 549a7c26..b257eff4 100644 --- a/music_assistant/providers/chromecast/__init__.py +++ b/music_assistant/providers/chromecast/__init__.py @@ -386,7 +386,7 @@ class ChromecastProvider(PlayerProvider): 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 @@ class ChromecastProvider(PlayerProvider): # 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 @@ class ChromecastProvider(PlayerProvider): 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 eb79c490..3b6b89d5 100644 --- a/music_assistant/providers/deezer/__init__.py +++ b/music_assistant/providers/deezer/__init__.py @@ -48,7 +48,7 @@ from music_assistant.models.music_provider import MusicProvider from .gw_client import GWClient -SUPPORTED_FEATURES = ( +SUPPORTED_FEATURES = { ProviderFeature.LIBRARY_ARTISTS, ProviderFeature.LIBRARY_ALBUMS, ProviderFeature.LIBRARY_TRACKS, @@ -68,7 +68,7 @@ SUPPORTED_FEATURES = ( ProviderFeature.PLAYLIST_CREATE, ProviderFeature.RECOMMENDATIONS, ProviderFeature.SIMILAR_TRACKS, -) +} @dataclass @@ -190,7 +190,7 @@ class DeezerProvider(MusicProvider): 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 10f651ee..46315920 100644 --- a/music_assistant/providers/dlna/__init__.py +++ b/music_assistant/providers/dlna/__init__.py @@ -493,7 +493,7 @@ class DLNAPlayerProvider(PlayerProvider): # 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 @@ class DLNAPlayerProvider(PlayerProvider): # 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 @@ class DLNAPlayerProvider(PlayerProvider): 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 9b8a65c1..a8e68a74 100644 --- a/music_assistant/providers/fanarttv/__init__.py +++ b/music_assistant/providers/fanarttv/__init__.py @@ -23,10 +23,10 @@ if TYPE_CHECKING: 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 @@ class FanartTvMetadataProvider(MetadataProvider): 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 317d8aa6..dc530474 100644 --- a/music_assistant/providers/filesystem_local/__init__.py +++ b/music_assistant/providers/filesystem_local/__init__.py @@ -111,14 +111,14 @@ IMAGE_EXTENSIONS = ("jpg", "jpeg", "JPG", "JPEG", "png", "PNG", "gif", "GIF") 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 e773dcd0..ad4076e1 100644 --- a/music_assistant/providers/fully_kiosk/__init__.py +++ b/music_assistant/providers/fully_kiosk/__init__.py @@ -125,7 +125,7 @@ class FullyKioskProvider(PlayerProvider): 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 5095749c..40790479 100644 --- a/music_assistant/providers/hass_players/__init__.py +++ b/music_assistant/providers/hass_players/__init__.py @@ -334,8 +334,8 @@ class HomeAssistantPlayers(PlayerProvider): 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 @@ class HomeAssistantPlayers(PlayerProvider): 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 @@ class HomeAssistantPlayers(PlayerProvider): 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 f6a03ed9..a76b235a 100644 --- a/music_assistant/providers/jellyfin/__init__.py +++ b/music_assistant/providers/jellyfin/__init__.py @@ -153,9 +153,9 @@ class JellyfinProvider(MusicProvider): 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 @@ class JellyfinProvider(MusicProvider): 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 95918ad5..88ee5a5b 100644 --- a/music_assistant/providers/musicbrainz/__init__.py +++ b/music_assistant/providers/musicbrainz/__init__.py @@ -33,7 +33,7 @@ if TYPE_CHECKING: LUCENE_SPECIAL = r'([+\-&|!(){}\[\]\^"~*?:\\\/])' -SUPPORTED_FEATURES = () +SUPPORTED_FEATURES = set() async def setup( @@ -200,7 +200,7 @@ class MusicbrainzProvider(MetadataProvider): 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 85594609..9aeae69c 100644 --- a/music_assistant/providers/opensubsonic/sonic_provider.py +++ b/music_assistant/providers/opensubsonic/sonic_provider.py @@ -112,9 +112,9 @@ class OpenSonicProvider(MusicProvider): 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 @@ class OpenSonicProvider(MusicProvider): 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 32afe391..c78a8079 100644 --- a/music_assistant/providers/player_group/__init__.py +++ b/music_assistant/providers/player_group/__init__.py @@ -158,9 +158,9 @@ class PlayerGroupProvider(PlayerProvider): ] @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 @@ class PlayerGroupProvider(PlayerProvider): # 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 @@ class PlayerGroupProvider(PlayerProvider): 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 @@ class PlayerGroupProvider(PlayerProvider): 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 @@ class PlayerGroupProvider(PlayerProvider): 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 @@ class PlayerGroupProvider(PlayerProvider): 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 @@ class PlayerGroupProvider(PlayerProvider): 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 @@ class PlayerGroupProvider(PlayerProvider): 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 @@ class PlayerGroupProvider(PlayerProvider): 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 @@ class PlayerGroupProvider(PlayerProvider): 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 @@ class PlayerGroupProvider(PlayerProvider): 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 @@ class PlayerGroupProvider(PlayerProvider): 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 5bf783d9..519a8fea 100644 --- a/music_assistant/providers/plex/__init__.py +++ b/music_assistant/providers/plex/__init__.py @@ -37,7 +37,6 @@ from music_assistant_models.media_items import ( AudioFormat, ItemMapping, MediaItem, - MediaItemChapter, MediaItemImage, Playlist, ProviderMapping, @@ -380,9 +379,9 @@ class PlexProvider(MusicProvider): 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 @@ class PlexProvider(MusicProvider): ProviderFeature.BROWSE, ProviderFeature.SEARCH, ProviderFeature.ARTIST_ALBUMS, - ) + } @property def is_streaming_provider(self) -> bool: @@ -720,17 +719,7 @@ class PlexProvider(MusicProvider): 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 ee502f41..34402a8f 100644 --- a/music_assistant/providers/qobuz/__init__.py +++ b/music_assistant/providers/qobuz/__init__.py @@ -61,7 +61,7 @@ if TYPE_CHECKING: from music_assistant.models import ProviderInstanceType -SUPPORTED_FEATURES = ( +SUPPORTED_FEATURES = { ProviderFeature.LIBRARY_ARTISTS, ProviderFeature.LIBRARY_ALBUMS, ProviderFeature.LIBRARY_TRACKS, @@ -75,7 +75,7 @@ SUPPORTED_FEATURES = ( ProviderFeature.SEARCH, ProviderFeature.ARTIST_ALBUMS, ProviderFeature.ARTIST_TOPTRACKS, -) +} VARIOUS_ARTISTS_ID = "145383" @@ -137,7 +137,7 @@ class QobuzProvider(MusicProvider): 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 a38b9f44..b365bdb6 100644 --- a/music_assistant/providers/radiobrowser/__init__.py +++ b/music_assistant/providers/radiobrowser/__init__.py @@ -33,7 +33,7 @@ from radios import FilterBy, Order, RadioBrowser, RadioBrowserError, Station 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 @@ SUPPORTED_FEATURES = ( # 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 339e8622..d9068730 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 7394d17f..c355660c 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 @@ class SlimprotoProvider(PlayerProvider): 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 @@ class SlimprotoProvider(PlayerProvider): 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 @@ class SlimprotoProvider(PlayerProvider): 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 b987c483..373d7332 100644 --- a/music_assistant/providers/snapcast/__init__.py +++ b/music_assistant/providers/snapcast/__init__.py @@ -272,9 +272,9 @@ class SnapCastProvider(PlayerProvider): 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 @@ class SnapCastProvider(PlayerProvider): 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 @@ class SnapCastProvider(PlayerProvider): 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 @@ class SnapCastProvider(PlayerProvider): 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 1daa076a..5e024566 100644 --- a/music_assistant/providers/sonos/const.py +++ b/music_assistant/providers/sonos/const.py @@ -13,7 +13,7 @@ PLAYBACK_STATE_MAP = { } 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 a19d3c4a..67a307df 100644 --- a/music_assistant/providers/sonos/player.py +++ b/music_assistant/providers/sonos/player.py @@ -128,9 +128,12 @@ class SonosPlayer: 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 @@ class SonosPlayer: 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 @@ class SonosPlayer: # 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 fed714df..99d8d176 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 @@ class SonosPlayerProvider(PlayerProvider): 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 @@ class SonosPlayerProvider(PlayerProvider): 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 @@ class SonosPlayerProvider(PlayerProvider): 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 7a3fdab4..9a9007ad 100644 --- a/music_assistant/providers/sonos_s1/__init__.py +++ b/music_assistant/providers/sonos_s1/__init__.py @@ -55,7 +55,7 @@ if TYPE_CHECKING: 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 @@ class SonosPlayerProvider(PlayerProvider): 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 @@ class SonosPlayerProvider(PlayerProvider): 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 @@ class SonosPlayerProvider(PlayerProvider): 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 @@ class SonosPlayerProvider(PlayerProvider): 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 a0d0d525..a66406ce 100644 --- a/music_assistant/providers/sonos_s1/player.py +++ b/music_assistant/providers/sonos_s1/player.py @@ -44,7 +44,7 @@ CALLBACK_TYPE = Callable[[], None] LOGGER = logging.getLogger(__name__) PLAYER_FEATURES = ( - PlayerFeature.SYNC, + PlayerFeature.SET_MEMBERS, PlayerFeature.VOLUME_MUTE, PlayerFeature.VOLUME_SET, ) @@ -285,7 +285,7 @@ class SonosPlayer: 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 @@ class SonosPlayer: 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 @@ class SonosPlayer: 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 505f4984..82aaebf7 100644 --- a/music_assistant/providers/soundcloud/__init__.py +++ b/music_assistant/providers/soundcloud/__init__.py @@ -34,7 +34,7 @@ from music_assistant.models.music_provider import MusicProvider 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 @@ SUPPORTED_FEATURES = ( ProviderFeature.SEARCH, ProviderFeature.ARTIST_TOPTRACKS, ProviderFeature.SIMILAR_TRACKS, -) +} if TYPE_CHECKING: @@ -117,7 +117,7 @@ class SoundcloudMusicProvider(MusicProvider): 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 abc1770a..a8df3498 100644 --- a/music_assistant/providers/spotify/__init__.py +++ b/music_assistant/providers/spotify/__init__.py @@ -91,7 +91,7 @@ SCOPE = [ 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 @@ SUPPORTED_FEATURES = ( ProviderFeature.ARTIST_ALBUMS, ProviderFeature.ARTIST_TOPTRACKS, ProviderFeature.SIMILAR_TRACKS, -) +} async def setup( @@ -257,9 +257,9 @@ class SpotifyProvider(MusicProvider): 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 @@ class SpotifyProvider(MusicProvider): 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 630f64bc..40f5562f 100644 --- a/music_assistant/providers/test/__init__.py +++ b/music_assistant/providers/test/__init__.py @@ -82,9 +82,9 @@ class TestProvider(MusicProvider): 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 73777653..cfaca28b 100644 --- a/music_assistant/providers/theaudiodb/__init__.py +++ b/music_assistant/providers/theaudiodb/__init__.py @@ -38,11 +38,11 @@ if TYPE_CHECKING: 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 @@ class AudioDbMetadataProvider(MetadataProvider): 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 dedfa653..09711526 100644 --- a/music_assistant/providers/tidal/__init__.py +++ b/music_assistant/providers/tidal/__init__.py @@ -372,9 +372,9 @@ class TidalProvider(MusicProvider): 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 @@ class TidalProvider(MusicProvider): 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 83544115..8be3df29 100644 --- a/music_assistant/providers/tunein/__init__.py +++ b/music_assistant/providers/tunein/__init__.py @@ -26,10 +26,10 @@ from music_assistant.constants import CONF_USERNAME 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 0443569a..5672e6ff 100644 --- a/music_assistant/providers/ytmusic/__init__.py +++ b/music_assistant/providers/ytmusic/__init__.py @@ -100,7 +100,7 @@ YT_PERSONAL_PLAYLISTS = ( ) YTM_PREMIUM_CHECK_TRACK_ID = "dQw4w9WgXcQ" -SUPPORTED_FEATURES = ( +SUPPORTED_FEATURES = { ProviderFeature.LIBRARY_ARTISTS, ProviderFeature.LIBRARY_ALBUMS, ProviderFeature.LIBRARY_TRACKS, @@ -110,7 +110,7 @@ SUPPORTED_FEATURES = ( ProviderFeature.ARTIST_ALBUMS, ProviderFeature.ARTIST_TOPTRACKS, ProviderFeature.SIMILAR_TRACKS, -) +} # TODO: fix disabled tests @@ -185,7 +185,7 @@ class YoutubeMusicProvider(MusicProvider): 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 715ababb..cd9e330c 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 02eb48d9..a0900ae0 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 -- 2.34.1