A few small bugfixes (#980)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sat, 30 Dec 2023 03:43:05 +0000 (04:43 +0100)
committerGitHub <noreply@github.com>
Sat, 30 Dec 2023 03:43:05 +0000 (04:43 +0100)
* 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
music_assistant/server/controllers/config.py
music_assistant/server/controllers/players.py
music_assistant/server/helpers/audio.py
music_assistant/server/providers/musicbrainz/__init__.py
music_assistant/server/providers/radiobrowser/__init__.py
music_assistant/server/providers/tunein/__init__.py

index bb73d34ee0c2f61fe9b15502241f1926179a1cfb..34729b44f6bae742c898ae696aee924eb4793ff7 100644 (file)
@@ -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)."""
index feefaa4cdeb92cb7163b08d81fb230d9be586572..df32fb41df5c7c9723925a9ba31e8a35b08332d9 100644 (file)
@@ -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)
index b14855fbd2266bae22d95cca9da5cfce8c955eae..acc5f9811ed104c013671c81503ce33023a90fcd 100755 (executable)
@@ -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."""
index 4ef354ecca1ec74a6bf7420803056743e7a76255..06038aec58373daebce563e742002682e8b5b37b 100644 (file)
@@ -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 = [
index ec7ce03b84da75b7ba3bf8789721ded0ee121451..a69a5958d55fd5b4e0730cdf056889d71b026f84 100644 (file)
@@ -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.)
index 13490061156126e079ebb66aa3b3a0b6b4520836..81e19bdd39e221e8ce7d687667af45298aa6fa4d 100644 (file)
@@ -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,
         )
 
index e41ed3dfbecca96505f1624fa03d665f7763abab..e2d560583145e692da637b9384c4c54efef21312 100644 (file)
@@ -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,