From f22963e038cc9d95b46fc4b0c733be45ef03c2a7 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 30 Dec 2023 04:43:05 +0100 Subject: [PATCH] A few small bugfixes (#980) * Fix (de)serialization of queue item streamdetails * mark more musicbrainz fields as optional * disable player on config removal * add protocol whitelist to analyze job for ffmpeg * typo * add data to protocol whitelist * resume player after changing config * better parsing of mpeg dash streams * optimistically update player after sync/unsync --- music_assistant/common/models/queue_item.py | 7 ------- music_assistant/server/controllers/config.py | 10 ++++++++-- music_assistant/server/controllers/players.py | 9 +++++++-- music_assistant/server/helpers/audio.py | 4 +++- .../server/providers/musicbrainz/__init__.py | 16 ++++++++-------- .../server/providers/radiobrowser/__init__.py | 16 ++++++++++++++++ .../server/providers/tunein/__init__.py | 15 +++++++++++---- 7 files changed, 53 insertions(+), 24 deletions(-) diff --git a/music_assistant/common/models/queue_item.py b/music_assistant/common/models/queue_item.py index bb73d34e..34729b44 100644 --- a/music_assistant/common/models/queue_item.py +++ b/music_assistant/common/models/queue_item.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any from uuid import uuid4 from mashumaro import DataClassDictMixin @@ -33,12 +32,6 @@ class QueueItem(DataClassDictMixin): if not self.name: self.name = self.uri - @classmethod - def __pre_deserialize__(cls, d: dict[Any, Any]) -> dict[Any, Any]: - """Run actions before deserialization.""" - d.pop("streamdetails", None) - return d - @property def uri(self) -> str: """Return uri for this QueueItem (for logging purposes).""" diff --git a/music_assistant/server/controllers/config.py b/music_assistant/server/controllers/config.py index feefaa4c..df32fb41 100644 --- a/music_assistant/server/controllers/config.py +++ b/music_assistant/server/controllers/config.py @@ -25,7 +25,7 @@ from music_assistant.common.models.config_entries import ( PlayerConfig, ProviderConfig, ) -from music_assistant.common.models.enums import EventType, ProviderType +from music_assistant.common.models.enums import EventType, PlayerState, ProviderType from music_assistant.common.models.errors import InvalidDataError, PlayerUnavailableError from music_assistant.constants import ( CONF_CORE, @@ -389,8 +389,8 @@ class ConfigController: data=config, ) # signal update to the player manager + player = self.mass.players.get(config.player_id) with suppress(PlayerUnavailableError, AttributeError, KeyError): - player = self.mass.players.get(config.player_id) if config.enabled: player_prov = self.mass.players.get_player_provider(player_id) await player_prov.poll_player(player_id) @@ -401,6 +401,9 @@ class ConfigController: with suppress(PlayerUnavailableError): if provider := self.mass.get_provider(config.provider): provider.on_player_config_changed(config, changed_keys) + # if the player was playing, restart playback + if player and player.state == PlayerState.PLAYING: + self.mass.create_task(self.mass.player_queues.resume(player.active_source)) # return full player config (just in case) return await self.get_player_config(player_id) @@ -412,6 +415,9 @@ class ConfigController: if not existing: raise KeyError(f"Player {player_id} does not exist") self.remove(conf_key) + if (player := self.mass.players.get(player_id)) and player.available: + player.enabled = False + self.mass.players.update(player_id, force_update=True) if provider := self.mass.get_provider(existing["provider"]): assert isinstance(provider, PlayerProvider) provider.on_player_config_removed(player_id) diff --git a/music_assistant/server/controllers/players.py b/music_assistant/server/controllers/players.py index b14855fb..acc5f981 100755 --- a/music_assistant/server/controllers/players.py +++ b/music_assistant/server/controllers/players.py @@ -572,9 +572,11 @@ class PlayerController(CoreController): if child_player.state == PlayerState.PLAYING: await self.cmd_stop(player_id) # all checks passed, forward command to the player provider - child_player.hidden_by.add(target_player) player_provider = self.get_player_provider(player_id) await player_provider.cmd_sync(player_id, target_player) + child_player.hidden_by.add(target_player) + # optimistically update the player to update the UI as fast as possible + player_provider.poll_player(player_id) @api_command("players/cmd/unsync") @log_player_command @@ -593,7 +595,8 @@ class PlayerController(CoreController): if not player.synced_to: LOGGER.info( "Ignoring command to unsync player %s " - "because it is currently not part of a (sync)group." + "because it is currently not synced to another player.", + player.display_name, ) return @@ -602,6 +605,8 @@ class PlayerController(CoreController): player.hidden_by.remove(player.synced_to) player_provider = self.get_player_provider(player_id) await player_provider.cmd_unsync(player_id) + # optimistically update the player to update the UI as fast as possible + player_provider.poll_player(player_id) def _check_redirect(self, player_id: str) -> str: """Check if playback related command should be redirected.""" diff --git a/music_assistant/server/helpers/audio.py b/music_assistant/server/helpers/audio.py index 4ef354ec..06038aec 100644 --- a/music_assistant/server/helpers/audio.py +++ b/music_assistant/server/helpers/audio.py @@ -174,6 +174,8 @@ async def analyze_audio(mass: MusicAssistant, streamdetails: StreamDetails) -> N input_file = streamdetails.direct or "-" proc_args = [ "ffmpeg", + "-protocol_whitelist", + "file,http,https,tcp,tls,crypto,pipe,fd", "-t", "300", # limit to 5 minutes to prevent OOM "-i", @@ -761,7 +763,7 @@ async def _get_ffmpeg_args( "warning" if LOGGER.isEnabledFor(logging.DEBUG) else "quiet", "-ignore_unknown", "-protocol_whitelist", - "file,http,https,tcp,tls,crypto,pipe,fd", # support nested protocols (e.g. within playlist) + "file,http,https,tcp,tls,crypto,pipe,data,fd", ] # collect input args input_args = [ diff --git a/music_assistant/server/providers/musicbrainz/__init__.py b/music_assistant/server/providers/musicbrainz/__init__.py index ec7ce03b..a69a5958 100644 --- a/music_assistant/server/providers/musicbrainz/__init__.py +++ b/music_assistant/server/providers/musicbrainz/__init__.py @@ -142,18 +142,18 @@ class MusicBrainzTrack(DataClassDictMixin): id: str number: str title: str - length: int + length: int | None = None @dataclass class MusicBrainzMedia(DataClassDictMixin): """Model for a (basic) Media object from MusicBrainz.""" - position: int format: str track: list[MusicBrainzTrack] - track_count: int - track_offset: int + position: int = 0 + track_count: int = 0 + track_offset: int = 0 @dataclass @@ -167,7 +167,7 @@ class MusicBrainzRelease(DataClassDictMixin): status: str artist_credit: list[MusicBrainzArtistCredit] release_group: MusicBrainzReleaseGroup - track_count: int + track_count: int = 0 # optional fields media: list[MusicBrainzMedia] = field(default_factory=list) @@ -183,10 +183,10 @@ class MusicBrainzRecording(DataClassDictMixin): id: str title: str - length: int | None - first_release_date: str | None - artist_credit: list[MusicBrainzArtistCredit] + artist_credit: list[MusicBrainzArtistCredit] = field(default_factory=list) # optional fields + length: int | None = None + first_release_date: str | None = None isrcs: list[str] | None = None tags: list[MusicBrainzTag] | None = None disambiguation: str | None = None # version (e.g. live, karaoke etc.) diff --git a/music_assistant/server/providers/radiobrowser/__init__.py b/music_assistant/server/providers/radiobrowser/__init__.py index 13490061..81e19bdd 100644 --- a/music_assistant/server/providers/radiobrowser/__init__.py +++ b/music_assistant/server/providers/radiobrowser/__init__.py @@ -9,6 +9,7 @@ from radios import FilterBy, Order, RadioBrowser, RadioBrowserError from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType from music_assistant.common.models.enums import LinkType, ProviderFeature +from music_assistant.common.models.errors import InvalidDataError from music_assistant.common.models.media_items import ( AudioFormat, BrowseFolder, @@ -24,6 +25,7 @@ from music_assistant.common.models.media_items import ( StreamDetails, ) from music_assistant.server.helpers.audio import get_radio_stream +from music_assistant.server.helpers.playlists import fetch_playlist from music_assistant.server.models.music_provider import MusicProvider SUPPORTED_FEATURES = (ProviderFeature.SEARCH, ProviderFeature.BROWSE) @@ -279,6 +281,19 @@ class RadioBrowserProvider(MusicProvider): stream = await self.radios.station(uuid=item_id) url_resolved = stream.url_resolved await self.radios.station_click(uuid=item_id) + direct = None + if ".m3u" in url_resolved or ".pls" in url_resolved: + # url is playlist, try to figure out how to handle it + # if it is an mpeg-dash stream, let ffmpeg handle that + try: + playlist = await fetch_playlist(self.mass, url_resolved) + if len(playlist) > 1 or ".m3u" in playlist[0] or ".pls" in playlist[0]: + direct = playlist[0] + elif playlist: + url_resolved = playlist[0] + except (InvalidDataError, IndexError): + # empty playlist ?! + direct = url_resolved return StreamDetails( provider=self.domain, item_id=item_id, @@ -287,6 +302,7 @@ class RadioBrowserProvider(MusicProvider): ), media_type=MediaType.RADIO, data=url_resolved, + direct=direct, expires=time() + 24 * 3600, ) diff --git a/music_assistant/server/providers/tunein/__init__.py b/music_assistant/server/providers/tunein/__init__.py index e41ed3df..e2d56058 100644 --- a/music_assistant/server/providers/tunein/__init__.py +++ b/music_assistant/server/providers/tunein/__init__.py @@ -230,12 +230,19 @@ class TuneInProvider(MusicProvider): # check if the radio stream is not a playlist url = stream["url"] direct = None - if stream.get("playlist_type"): # noqa: SIM102 - if playlist := await fetch_playlist(self.mass, url): + direct = None + if ".m3u" in url or ".pls" in url or stream.get("playlist_type"): + # url is playlist, try to figure out how to handle it + # if it is an mpeg-dash stream, let ffmpeg handle that + try: + playlist = await fetch_playlist(self.mass, url) if len(playlist) > 1 or ".m3u" in playlist[0] or ".pls" in playlist[0]: - # this is most likely an mpeg-dash stream, let ffmpeg handle that direct = playlist[0] - url = playlist[0] + elif playlist: + url_resolved = playlist[0] + except (InvalidDataError, IndexError): + # empty playlist ?! + direct = url_resolved return StreamDetails( provider=self.domain, item_id=item_id, -- 2.34.1