From: Marcel van der Veldt Date: Mon, 15 Sep 2025 13:14:13 +0000 (+0200) Subject: Bump models to 1.1.55 (#2397) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=15cf48e8417ad30f3cb8797b368d458cafc11ddc;p=music-assistant-server.git Bump models to 1.1.55 (#2397) --- diff --git a/music_assistant/controllers/player_queues.py b/music_assistant/controllers/player_queues.py index 17d847fe..7356c568 100644 --- a/music_assistant/controllers/player_queues.py +++ b/music_assistant/controllers/player_queues.py @@ -1283,7 +1283,7 @@ class PlayerQueuesController(CoreController): title="Music Assistant" if flow_mode else queue_item.name, image_url=MASS_LOGO_ONLINE, duration=duration, - queue_id=queue_item.queue_id, + source_id=queue_item.queue_id, queue_item_id=queue_item.queue_item_id, ) if not flow_mode and queue_item.media_item: @@ -1940,7 +1940,7 @@ class PlayerQueuesController(CoreController): if not player.current_media: return None # prefer queue_id and queue_item_id within the current media - if player.current_media.queue_id == queue_id and player.current_media.queue_item_id: + if player.current_media.source_id == queue_id and player.current_media.queue_item_id: return player.current_media.queue_item_id # special case for sonos players if ( diff --git a/music_assistant/helpers/upnp.py b/music_assistant/helpers/upnp.py index 3a0f13a0..c4b21a26 100644 --- a/music_assistant/helpers/upnp.py +++ b/music_assistant/helpers/upnp.py @@ -149,7 +149,7 @@ def create_didl_metadata(media: PlayerMedia) -> str: return ( '' - f'' + f'' f"{escape_metadata(media.title or media.uri)}" f"{escape_metadata(media.artist or '')}" f"{escape_metadata(media.album or '')}" diff --git a/music_assistant/models/player.py b/music_assistant/models/player.py index a9f44831..05979cd8 100644 --- a/music_assistant/models/player.py +++ b/music_assistant/models/player.py @@ -1047,7 +1047,7 @@ class Player(ABC): album: str | None = None, image_url: str | None = None, duration: int | None = None, - queue_id: str | None = None, + source_id: str | None = None, queue_item_id: str | None = None, custom_data: dict[str, Any] | None = None, clear_all: bool = False, @@ -1075,8 +1075,8 @@ class Player(ABC): self._attr_current_media.image_url = image_url if duration: self._attr_current_media.duration = duration - if queue_id: - self._attr_current_media.queue_id = queue_id + if source_id: + self._attr_current_media.source_id = source_id if queue_item_id: self._attr_current_media.queue_item_id = queue_item_id if custom_data: @@ -1622,7 +1622,7 @@ class SyncGroupPlayer(GroupPlayer): if sync_leader := self.sync_leader: await sync_leader.play_media(media) self._attr_current_media = media - self._attr_active_source = media.queue_id + self._attr_active_source = media.source_id self.update_state() else: raise RuntimeError("an empty group cannot play media, consider adding members first") diff --git a/music_assistant/providers/airplay/player.py b/music_assistant/providers/airplay/player.py index be1eeab0..c4c1eeee 100644 --- a/music_assistant/providers/airplay/player.py +++ b/music_assistant/providers/airplay/player.py @@ -211,7 +211,7 @@ class AirPlayPlayer(Player): # set the active source for the player to the media queue # this accounts for syncgroups and linked players (e.g. sonos) - self._attr_active_source = media.queue_id + self._attr_active_source = media.source_id self._attr_current_media = media # select audio source @@ -235,19 +235,21 @@ class AirPlayPlayer(Player): # because this could have been a group player_id=media.custom_data["player_id"], ) - elif media.queue_id and media.queue_id.startswith(UGP_PREFIX): + elif media.source_id and media.source_id.startswith(UGP_PREFIX): # special case: UGP stream - ugp_player = cast("UniversalGroupPlayer", self.mass.players.get(media.queue_id)) + ugp_player = cast("UniversalGroupPlayer", self.mass.players.get(media.source_id)) ugp_stream = ugp_player.stream assert ugp_stream is not None # for type checker input_format = ugp_stream.base_pcm_format audio_source = ugp_stream.subscribe_raw() - elif media.queue_id and media.queue_item_id: + elif media.source_id and media.queue_item_id: # regular queue (flow) stream request input_format = AIRPLAY_FLOW_PCM_FORMAT - queue = self.mass.player_queues.get(media.queue_id) + queue = self.mass.player_queues.get(media.source_id) assert queue - start_queue_item = self.mass.player_queues.get_item(media.queue_id, media.queue_item_id) + start_queue_item = self.mass.player_queues.get_item( + media.source_id, media.queue_item_id + ) assert start_queue_item audio_source = self.mass.streams.get_queue_flow_stream( queue=queue, diff --git a/music_assistant/providers/bluesound/player.py b/music_assistant/providers/bluesound/player.py index 8e3e70d7..8c5c992d 100644 --- a/music_assistant/providers/bluesound/player.py +++ b/music_assistant/providers/bluesound/player.py @@ -183,7 +183,7 @@ class BluesoundPlayer(Player): # Optimistically update state self._attr_current_media = media - self._attr_active_source = media.queue_id + self._attr_active_source = media.source_id self._attr_elapsed_time = 0 self._attr_elapsed_time_last_updated = time.time() self.update_state() diff --git a/music_assistant/providers/builtin_player/player.py b/music_assistant/providers/builtin_player/player.py index 1082f179..cb135027 100644 --- a/music_assistant/providers/builtin_player/player.py +++ b/music_assistant/providers/builtin_player/player.py @@ -175,7 +175,7 @@ class BuiltinPlayer(Player): url = f"builtin_player/flow/{self.player_id}.mp3" self._attr_current_media = media self._attr_playback_state = PlaybackState.PLAYING - self._attr_active_source = media.queue_id + self._attr_active_source = media.source_id self.update_state() self.mass.signal_event( EventType.BUILTIN_PLAYER, @@ -254,10 +254,10 @@ class BuiltinPlayer(Player): if queue is None or media is None: raise web.HTTPNotFound(reason="No active queue or media found!") - if media.queue_id is None: + if media.source_id is None: raise web.HTTPError # TODO: better error - queue_item = self.mass.player_queues.get_item(media.queue_id, media.queue_item_id) + queue_item = self.mass.player_queues.get_item(media.source_id, media.queue_item_id) if queue_item is None: raise web.HTTPError # TODO: better error diff --git a/music_assistant/providers/hass_players/player.py b/music_assistant/providers/hass_players/player.py index ab3ce576..4b7ccc4e 100644 --- a/music_assistant/providers/hass_players/player.py +++ b/music_assistant/providers/hass_players/player.py @@ -256,7 +256,7 @@ class HomeAssistantPlayer(Player): # Optimistically update state self._attr_current_media = media - self._attr_active_source = media.queue_id + self._attr_active_source = media.source_id self._attr_elapsed_time = 0 self._attr_elapsed_time_last_updated = time.time() self._attr_playback_state = PlaybackState.PLAYING diff --git a/music_assistant/providers/snapcast/player.py b/music_assistant/providers/snapcast/player.py index 1b21f2f5..0cbeabd1 100644 --- a/music_assistant/providers/snapcast/player.py +++ b/music_assistant/providers/snapcast/player.py @@ -175,7 +175,7 @@ class SnapCastPlayer(Player): # get stream or create new one stream_name = self._get_stream_name(SnapCastStreamType.MUSIC) - stream = await self._get_or_create_stream(stream_name, media.queue_id or self.player_id) + stream = await self._get_or_create_stream(stream_name, media.source_id or self.player_id) # if no announcement is playing we activate the stream now, otherwise it # will be activated by play_announcement when the announcement is over. @@ -185,7 +185,7 @@ class SnapCastPlayer(Player): await snap_group.set_stream(stream.identifier) self._attr_current_media = media - self._attr_active_source = media.queue_id + self._attr_active_source = media.source_id # select audio source if media.media_type == MediaType.PLUGIN_SOURCE: @@ -197,18 +197,20 @@ class SnapCastPlayer(Player): output_format=DEFAULT_SNAPCAST_FORMAT, player_id=self.player_id, ) - elif media.queue_id and media.queue_id.startswith(UGP_PREFIX): + elif media.source_id and media.source_id.startswith(UGP_PREFIX): # special case: UGP stream - ugp_player = cast("UniversalGroupPlayer", self.mass.players.get(media.queue_id)) + ugp_player = cast("UniversalGroupPlayer", self.mass.players.get(media.source_id)) ugp_stream = ugp_player.stream assert ugp_stream is not None # for type checker input_format = ugp_stream.base_pcm_format audio_source = ugp_stream.subscribe_raw() - elif media.queue_id and media.queue_item_id: + elif media.source_id and media.queue_item_id: # regular queue (flow) stream request input_format = DEFAULT_SNAPCAST_PCM_FORMAT - queue = self.mass.player_queues.get(media.queue_id) - start_queue_item = self.mass.player_queues.get_item(media.queue_id, media.queue_item_id) + queue = self.mass.player_queues.get(media.source_id) + start_queue_item = self.mass.player_queues.get_item( + media.source_id, media.queue_item_id + ) assert queue is not None # for type checking assert start_queue_item is not None # for type checking audio_source = self.mass.streams.get_queue_flow_stream( diff --git a/music_assistant/providers/sonos/player.py b/music_assistant/providers/sonos/player.py index 01d809c4..22db88be 100644 --- a/music_assistant/providers/sonos/player.py +++ b/music_assistant/providers/sonos/player.py @@ -407,25 +407,25 @@ class SonosPlayer(Player): if media.media_type in ( MediaType.PLUGIN_SOURCE, MediaType.FLOW_STREAM, - ) or media.queue_id.startswith(UGP_PREFIX): + ) or media.source_id.startswith(UGP_PREFIX): # flow stream or plugin source playback # always use the legacy (UPNP) playback method for this await self._play_media_legacy(media) _update_state() return - if media.queue_id: + if media.source_id and media.queue_item_id: # Regular Queue item playback # create a sonos cloud queue and load it cloud_queue_url = f"{self.mass.streams.base_url}/sonos_queue/v2.3/" - mass_queue = self.mass.player_queues.get(media.queue_id) + mass_queue = self.mass.player_queues.get(media.source_id) await self.client.player.group.play_cloud_queue( cloud_queue_url, - http_authorization=media.queue_id, + http_authorization=media.source_id, item_id=media.queue_item_id, queue_version=str(int(mass_queue.items_last_updated)), ) - self.mass.call_later(5, self.sync_play_modes, media.queue_id) + self.mass.call_later(5, self.sync_play_modes, media.source_id) _update_state() return @@ -673,7 +673,7 @@ class SonosPlayer(Player): image_url=track_image_url, ) if active_service == MusicService.MUSIC_ASSISTANT: - current_media.queue_id = self._attr_active_source + current_media.source_id = self._attr_active_source current_media.queue_item_id = current_item["id"] # radio stream info if container and container.get("name") and active_group.playback_metadata.get("streamInfo"): @@ -831,7 +831,7 @@ class SonosPlayer(Player): player_id = self.player_id if ( airplay_player.playback_state == PlaybackState.PLAYING - and airplay_player.active_source == media.queue_id + and airplay_player.active_source == media.source_id ): # if the airplay player is already playing, # the stream will be reused so no need to do the whole grouping thing below diff --git a/music_assistant/providers/squeezelite/player.py b/music_assistant/providers/squeezelite/player.py index f750dc28..1ce1699d 100644 --- a/music_assistant/providers/squeezelite/player.py +++ b/music_assistant/providers/squeezelite/player.py @@ -233,18 +233,18 @@ class SqueezelitePlayer(Player): # because this could have been a group player_id=media.custom_data["player_id"], ) - elif media.queue_id.startswith("ugp_"): + elif media.source_id.startswith("ugp_"): # special case: UGP stream - ugp_player: UniversalGroupPlayer = self.mass.players.get(media.queue_id) + ugp_player: UniversalGroupPlayer = self.mass.players.get(media.source_id) ugp_stream = ugp_player.stream # Filter is later applied in MultiClientStream audio_source = ugp_stream.get_stream(master_audio_format, filter_params=None) - elif media.queue_id and media.queue_item_id: + elif media.source_id and media.queue_item_id: # regular queue stream request audio_source = self.mass.streams.get_queue_flow_stream( - queue=self.mass.player_queues.get(media.queue_id), + queue=self.mass.player_queues.get(media.source_id), start_queue_item=self.mass.player_queues.get_item( - media.queue_id, media.queue_item_id + media.source_id, media.queue_item_id ), pcm_format=master_audio_format, ) @@ -412,10 +412,10 @@ class SqueezelitePlayer(Player): "artist": media.artist, "image_url": media.image_url, "duration": media.duration, - "queue_id": media.queue_id, + "queue_id": media.source_id, "queue_item_id": media.queue_item_id, } - if queue := self.mass.player_queues.get(media.queue_id): + if queue := self.mass.player_queues.get(media.source_id): self.extra_data["playlist repeat"] = REPEATMODE_MAP[queue.repeat_mode] self.extra_data["playlist shuffle"] = int(queue.shuffle_enabled) await slimplayer.play_url( diff --git a/music_assistant/providers/universal_group/player.py b/music_assistant/providers/universal_group/player.py index 3d2ac019..e4644847 100644 --- a/music_assistant/providers/universal_group/player.py +++ b/music_assistant/providers/universal_group/player.py @@ -220,13 +220,13 @@ class UniversalGroupPlayer(GroupPlayer): output_format=UGP_FORMAT, player_id=media.custom_data["player_id"], ) - elif media.queue_id and media.queue_item_id: + elif media.source_id and media.queue_item_id: # regular queue stream request - queue = self.mass.player_queues.get(media.queue_id) - queue_item = self.mass.player_queues.get_item(media.queue_id, media.queue_item_id) + queue = self.mass.player_queues.get(media.source_id) + queue_item = self.mass.player_queues.get_item(media.source_id, media.queue_item_id) if not queue or not queue_item: # this should not happen, but guard just in case - raise RuntimeError(f"Invalid queue(item): {media.queue_id}, {media.queue_item_id}") + raise RuntimeError(f"Invalid queue(item): {media.source_id}, {media.queue_item_id}") audio_source = self.mass.streams.get_queue_flow_stream( queue=queue, start_queue_item=queue_item, @@ -252,7 +252,7 @@ class UniversalGroupPlayer(GroupPlayer): self._attr_elapsed_time = 0 self._attr_elapsed_time_last_updated = time() - 1 self._attr_playback_state = PlaybackState.PLAYING - self._attr_active_source = media.queue_id + self._attr_active_source = media.source_id self.update_state() # forward to downstream play_media commands @@ -266,7 +266,7 @@ class UniversalGroupPlayer(GroupPlayer): uri=f"{base_url}?player_id={member.player_id}", media_type=MediaType.FLOW_STREAM, title=self.display_name, - queue_id=self.player_id, + source_id=self.player_id, ) ) ) @@ -303,7 +303,7 @@ class UniversalGroupPlayer(GroupPlayer): uri=f"{base_url}?player_id={player_id}", media_type=MediaType.FLOW_STREAM, title=self.display_name, - queue_id=child_player.player_id, + source_id=child_player.player_id, ), ) # handle removals diff --git a/pyproject.toml b/pyproject.toml index 4c0e0950..07ddc3ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dependencies = [ "ifaddr==0.2.0", "mashumaro==3.16", "music-assistant-frontend==2.15.4", - "music-assistant-models==1.1.53", + "music-assistant-models==1.1.55", "mutagen==1.47.0", "orjson==3.11.3", "pillow==11.3.0", diff --git a/requirements_all.txt b/requirements_all.txt index a0d1ab5b..8f6b306e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -32,7 +32,7 @@ liblistenbrainz==0.6.0 lyricsgenius==3.6.5 mashumaro==3.16 music-assistant-frontend==2.15.4 -music-assistant-models==1.1.53 +music-assistant-models==1.1.55 mutagen==1.47.0 orjson==3.11.3 pillow==11.3.0 diff --git a/tests/providers/jellyfin/__snapshots__/test_parsers.ambr b/tests/providers/jellyfin/__snapshots__/test_parsers.ambr index 0bef29cb..5ca5c0f5 100644 --- a/tests/providers/jellyfin/__snapshots__/test_parsers.ambr +++ b/tests/providers/jellyfin/__snapshots__/test_parsers.ambr @@ -39,6 +39,7 @@ 'description': None, 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'http://localhost:1234/Items/70b7288088b42d318f75dbcc41fd0091/Images/Primary?api_key=ACCESS_TOKEN', @@ -130,6 +131,7 @@ 'description': None, 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'http://localhost:1234/Items/32ed6a0091733dcff57eae67010f3d4b/Images/Primary?api_key=ACCESS_TOKEN', @@ -213,6 +215,7 @@ 'description': None, 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ ]), 'label': None, @@ -276,6 +279,7 @@ 'description': None, 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'http://localhost:1234/Items/dd954bbf54398e247d803186d3585b79/Images/Backdrop/0?api_key=ACCESS_TOKEN', @@ -387,6 +391,7 @@ 'description': None, 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'http://localhost:1234/Items/da9c458e425584680765ddc3a89cbc0c/Images/Primary?api_key=ACCESS_TOKEN', @@ -487,6 +492,7 @@ 'description': None, 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ ]), 'label': None, @@ -567,6 +573,7 @@ 'description': None, 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'http://localhost:1234/Items/54918f75ee8f6c8b8dc5efd680644f29/Images/Primary?api_key=ACCESS_TOKEN', @@ -686,6 +693,7 @@ 'description': None, 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'http://localhost:1234/Items/fb12a77f49616a9fc56a6fab2b94174c/Images/Primary?api_key=ACCESS_TOKEN', diff --git a/tests/providers/opensubsonic/__snapshots__/test_parsers.ambr b/tests/providers/opensubsonic/__snapshots__/test_parsers.ambr index 8f842d3a..bee5131a 100644 --- a/tests/providers/opensubsonic/__snapshots__/test_parsers.ambr +++ b/tests/providers/opensubsonic/__snapshots__/test_parsers.ambr @@ -16,6 +16,7 @@ 'description': None, 'explicit': None, 'genres': None, + 'grouping': None, 'images': None, 'label': None, 'languages': None, @@ -78,6 +79,7 @@ 'East coast', 'Hip-Hop', ]), + 'grouping': None, 'images': list([ dict({ 'path': 'al-ad0f112b6dcf83de5e9cae85d07f0d35_640a93a8', @@ -146,6 +148,7 @@ 'description': None, 'explicit': None, 'genres': None, + 'grouping': None, 'images': None, 'label': None, 'languages': None, @@ -208,6 +211,7 @@ 'East coast', 'Hip-Hop', ]), + 'grouping': None, 'images': list([ dict({ 'path': 'al-ad0f112b6dcf83de5e9cae85d07f0d35_640a93a8', @@ -334,6 +338,7 @@ 'East coast', 'Hip-Hop', ]), + 'grouping': None, 'images': list([ dict({ 'path': 'al-ad0f112b6dcf83de5e9cae85d07f0d35_640a93a8', @@ -454,6 +459,7 @@ 'East coast', 'Hip-Hop', ]), + 'grouping': None, 'images': list([ dict({ 'path': 'al-ad0f112b6dcf83de5e9cae85d07f0d35_640a93a8', @@ -529,6 +535,7 @@ 'description': None, 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'https://demo.org/image.jpg', @@ -603,6 +610,7 @@ 'description': 'Empty biography', 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'https://demo.org/image.jpg', @@ -683,6 +691,7 @@ 'description': None, 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'https://demo.org/image.jpg', @@ -757,6 +766,7 @@ 'description': 'Empty biography', 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'https://demo.org/image.jpg', @@ -833,6 +843,7 @@ 'description': None, 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'ar-100000002', @@ -897,6 +908,7 @@ 'description': 'Empty biography', 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'ar-100000002', @@ -969,6 +981,7 @@ 'description': 'The history of The History of Rome...Why the Western Empire Fell when it did...Some thoughts on the future...Thank you, goodnight.', 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'pd-5', @@ -1005,6 +1018,7 @@ 'description': 'A weekly podcast tracing the rise, decline and fall of the Roman Empire. Now complete!', 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'pd-5', @@ -1100,6 +1114,7 @@ 'description': 'The history of The History of Rome...Why the Western Empire Fell when it did...Some thoughts on the future...Thank you, goodnight.', 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'pd-5', @@ -1136,6 +1151,7 @@ 'description': 'A weekly podcast tracing the rise, decline and fall of the Roman Empire. Now complete!', 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'pd-5', @@ -1231,6 +1247,7 @@ 'description': 'The history of The History of Rome...Why the Western Empire Fell when it did...Some thoughts on the future...Thank you, goodnight.', 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'pd-5', @@ -1267,6 +1284,7 @@ 'description': 'A weekly podcast tracing the rise, decline and fall of the Roman Empire. Now complete!', 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'pd-5', @@ -1362,6 +1380,7 @@ 'description': None, 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'al-2250', @@ -1427,6 +1446,7 @@ 'description': 'A weekly podcast tracing the rise, decline and fall of the Roman Empire. Now complete!', 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'pd-5', @@ -1493,6 +1513,7 @@ 'description': 'A weekly podcast tracing the rise, decline and fall of the Roman Empire. Now complete!', 'explicit': None, 'genres': None, + 'grouping': None, 'images': list([ dict({ 'path': 'pd-5', @@ -1576,6 +1597,7 @@ 'description': None, 'explicit': None, 'genres': None, + 'grouping': None, 'images': None, 'label': None, 'languages': None, @@ -1641,6 +1663,7 @@ 'East coast', 'Hip-Hop', ]), + 'grouping': None, 'images': None, 'label': None, 'languages': None, @@ -1773,6 +1796,7 @@ 'description': None, 'explicit': None, 'genres': None, + 'grouping': None, 'images': None, 'label': None, 'languages': None, @@ -1838,6 +1862,7 @@ 'East coast', 'Hip-Hop', ]), + 'grouping': None, 'images': None, 'label': None, 'languages': None, @@ -1917,6 +1942,7 @@ 'description': None, 'explicit': None, 'genres': None, + 'grouping': None, 'images': None, 'label': None, 'languages': None, @@ -1982,6 +2008,7 @@ 'East coast', 'Hip-Hop', ]), + 'grouping': None, 'images': None, 'label': None, 'languages': None, @@ -2114,6 +2141,7 @@ 'description': None, 'explicit': None, 'genres': None, + 'grouping': None, 'images': None, 'label': None, 'languages': None, @@ -2179,6 +2207,7 @@ 'East coast', 'Hip-Hop', ]), + 'grouping': None, 'images': None, 'label': None, 'languages': None, @@ -2313,6 +2342,7 @@ 'East coast', 'Hip-Hop', ]), + 'grouping': None, 'images': None, 'label': None, 'languages': None, @@ -2500,6 +2530,7 @@ 'East coast', 'Hip-Hop', ]), + 'grouping': None, 'images': None, 'label': None, 'languages': None,