finishing touches
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 1 Aug 2021 20:34:37 +0000 (22:34 +0200)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 1 Aug 2021 20:34:37 +0000 (22:34 +0200)
music_assistant/constants.py
music_assistant/helpers/audio.py
music_assistant/helpers/process.py
music_assistant/managers/players.py
music_assistant/models/player_queue.py
music_assistant/models/streamdetails.py
music_assistant/providers/chromecast/player.py
music_assistant/web/stream.py

index 31a11b9d99abd1381132ede1842c8e46de3ba323..165d9d1cff78efdd623cc098f19b14d217213b85 100755 (executable)
@@ -1,6 +1,6 @@
 """All constants for Music Assistant."""
 
-__version__ = "0.1.10"
+__version__ = "0.2.0"
 REQUIRED_PYTHON_VER = "3.8"
 
 # configuration keys/attributes
index dab87cd5954c3fa815c8d1ec1dfc6f814b5e44eb..80283eb977062a219ef0839f788012f63a11e3cd 100644 (file)
@@ -129,8 +129,6 @@ async def get_stream_details(
                 item_id=queue_item.item_id,
                 path=queue_item.uri,
                 content_type=ContentType(queue_item.uri.split(".")[-1]),
-                sample_rate=44100,
-                bit_depth=16,
             )
     else:
         # always request the full db track as there might be other qualities available
@@ -171,12 +169,15 @@ async def get_stream_details(
         # set player_id on the streamdetails so we know what players stream
         streamdetails.player_id = player_id
         # get gain correct / replaygain
-        if not queue_item.name == "alert":
+        if queue_item.name == "alert":
+            loudness = 0
+            gain_correct = 0
+        else:
             loudness, gain_correct = await get_gain_correct(
                 mass, player_id, streamdetails.item_id, streamdetails.provider
             )
-            streamdetails.gain_correct = gain_correct
-            streamdetails.loudness = loudness
+        streamdetails.gain_correct = gain_correct
+        streamdetails.loudness = loudness
         # set streamdetails as attribute on the media_item
         # this way the app knows what content is playing
         queue_item.streamdetails = streamdetails
index db211cd476807d39a3f4a5595c96b6800d87d398..236499e3919953ce8d8e0c9a33020e5692fcabc3 100644 (file)
@@ -13,8 +13,8 @@ from async_timeout import timeout
 
 LOGGER = logging.getLogger("AsyncProcess")
 
-DEFAULT_CHUNKSIZE = 256000
-DEFAULT_TIMEOUT = 10
+DEFAULT_CHUNKSIZE = 512000
+DEFAULT_TIMEOUT = 120
 
 
 class AsyncProcess:
@@ -36,7 +36,7 @@ class AsyncProcess:
                 self._args,
                 stdin=asyncio.subprocess.PIPE if self._enable_write else None,
                 stdout=asyncio.subprocess.PIPE,
-                limit=8000000,
+                limit=4000000,
                 close_fds=True,
             )
         else:
@@ -44,7 +44,7 @@ class AsyncProcess:
                 *self._args,
                 stdin=asyncio.subprocess.PIPE if self._enable_write else None,
                 stdout=asyncio.subprocess.PIPE,
-                limit=8000000,
+                limit=4000000,
                 close_fds=True,
             )
         return self
@@ -76,8 +76,6 @@ class AsyncProcess:
 
     async def read(self, chunk_size: int = DEFAULT_CHUNKSIZE) -> bytes:
         """Read x bytes from the process stdout."""
-        if self._proc.stdout.at_eof() or self._proc.returncode is not None:
-            return b""
         try:
             async with timeout(DEFAULT_TIMEOUT):
                 if chunk_size is None:
index c7d9daff2c3c9520f1a15f9de567413df5ed8ef5..36f180d0bd31d5206cb3d70f8b673cadc190d2ac 100755 (executable)
@@ -6,6 +6,7 @@ import pathlib
 from typing import Dict, List, Optional, Set, Tuple, Union
 
 from music_assistant.constants import (
+    CONF_CROSSFADE_DURATION,
     CONF_POWER_CONTROL,
     CONF_VOLUME_CONTROL,
     EVENT_PLAYER_ADDED,
@@ -23,7 +24,6 @@ from music_assistant.models.player import (
 )
 from music_assistant.models.player_queue import PlayerQueue, QueueItem, QueueOption
 from music_assistant.models.provider import PlayerProvider, ProviderType
-from music_assistant.models.streamdetails import ContentType, StreamDetails, StreamType
 
 POLL_INTERVAL = 30
 
@@ -397,9 +397,10 @@ class PlayerManager:
         self,
         player_id: str,
         url: str,
-        gain_adjust: int = 0,
+        volume: int = 0,
         force: bool = True,
         announce: bool = False,
+        duration: int = 10,
     ):
         """
         Play alert (e.g. tts message) on selected player.
@@ -408,14 +409,17 @@ class PlayerManager:
 
             :param player_id: player_id of the player to handle the command.
             :param url: Url to the sound effect/tts message that should be played.
-            :param gain_adjust: Adjust volume/gain of audio.
+            :param volume: Volume relative to current player's volume.
             :param force: Play alert even if player is currently powered off.
             :param announce: Prepend alert sound.
+            :param duration: Amount of time after which queue is restored, default 10 seconds.
         """
         player = self.get_player(player_id)
         player_queue = self.get_player_queue(player_id)
         prev_state = player.calculated_state.state
         prev_power = player.calculated_state.powered
+        prev_volume = player.calculated_state.volume_level
+        prev_repeat = player_queue.repeat_enabled
         if not player.calculated_state.powered:
             if not force:
                 LOGGER.debug(
@@ -424,7 +428,25 @@ class PlayerManager:
                 )
                 return
             await self.cmd_power_on(player_id)
-
+        # snapshot the queue
+        prev_queue_items = player_queue.items
+        prev_queue_index = player_queue.cur_index
+        prev_queue_crossfade = self.mass.config.get_player_config(
+            player_queue.queue_id
+        )[CONF_CROSSFADE_DURATION]
+
+        # pause playback
+        if prev_state == PlayerState.PLAYING:
+            await self.cmd_pause(player_queue.queue_id)
+        # disable crossfade and repeat if needed
+        if prev_queue_crossfade:
+            self.mass.config.player_settings[player_id][CONF_CROSSFADE_DURATION] = 0
+        if prev_repeat:
+            await player_queue.set_repeat_enabled(False)
+        # set alert volume
+        if volume != 0:
+            await self.cmd_volume_set(player_id, prev_volume + volume)
+        # load alert items in player queue
         queue_items = []
         if announce:
             alert_announce = (
@@ -437,15 +459,8 @@ class PlayerManager:
                 item_id="alert_announce",
                 provider="url",
                 name="alert",
-                duration=2,
-                streamdetails=StreamDetails(
-                    type=StreamType.URL,
-                    provider="url",
-                    item_id="alert_announce",
-                    path=str(alert_announce),
-                    content_type=ContentType(url.split(".")[-1]),
-                    gain_correct=10,
-                ),
+                duration=3,
+                uri=str(alert_announce),
             )
             queue_item.stream_url = "%s/queue/%s/%s" % (
                 self.mass.web.stream_url,
@@ -458,44 +473,37 @@ class PlayerManager:
             item_id="alert_sound",
             provider="url",
             name="alert",
-            duration=10,
-            streamdetails=StreamDetails(
-                type=StreamType.URL,
-                provider="url",
-                item_id="alert_sound",
-                path=url,
-                content_type=ContentType(url.split(".")[-1]),
-                gain_correct=gain_adjust,
-            ),
+            duration=duration,
+            uri=url,
         )
-        queue_item.stream_url = "%s/queue/%s/%s?alert=true" % (
+        queue_item.stream_url = "%s/queue/%s/%s" % (
             self.mass.web.stream_url,
             player_id,
             queue_item.queue_item_id,
         )
         queue_items.append(queue_item)
 
-        await player_queue.insert(queue_items, 0)
-
-        if prev_power and prev_state in [PlayerState.PLAYING, PlayerState.PAUSED]:
-            return
+        # load queue items
+        await player_queue.load(queue_items)
+
+        async def restore_queue():
+            # restore queue
+            if volume:
+                await self.cmd_volume_set(player_id, prev_volume)
+            if prev_queue_crossfade:
+                self.mass.config.player_settings[player_id][
+                    CONF_CROSSFADE_DURATION
+                ] = prev_queue_crossfade
+            await player_queue.set_repeat_enabled(prev_repeat)
+            # pylint: disable=protected-access
+            player_queue._items = prev_queue_items
+            player_queue._cur_index = prev_queue_index
+            if prev_power:
+                await player_queue.resume()
+            else:
+                await self.cmd_power_off(player_id)
 
-        # wait until playback completed
-        playback_started = False
-        count = 0
-        while True:
-            if not playback_started and player_queue.state == PlayerState.PLAYING:
-                playback_started = True
-            elif playback_started and (
-                player_queue.state != PlayerState.PLAYING
-                or (player_queue.cur_item and player_queue.cur_item.name != "alert")
-            ):
-                break
-            if count == 20:
-                break
-            count += 0.2
-            await asyncio.sleep(0.2)
-        await self.cmd_power_off(player_id)
+        self.mass.loop.call_later(duration, create_task, restore_queue)
 
     @api_route("players/{player_id}/cmd/stop", method="PUT")
     async def cmd_stop(self, player_id: str) -> None:
@@ -613,11 +621,8 @@ class PlayerManager:
         player = self.get_player(player_id)
         if not player:
             return
-        # send stop if player is playing
-        if player.active_queue == player_id and player.state in [
-            PlayerState.PLAYING,
-            PlayerState.PAUSED,
-        ]:
+        # send stop if player is active queue
+        if player.active_queue == player_id and player.state != PlayerState.OFF:
             await self.cmd_stop(player_id)
         player_config = self.mass.config.player_settings[player.player_id]
         # turn off player
index 9afcdec3fc5429d22e026841cdd1e781ebff5420..2c019b8e192eca7abd1601ab171b4e224417971d 100755 (executable)
@@ -370,7 +370,7 @@ class PlayerQueue:
         """Load (overwrite) queue with new items."""
         for index, item in enumerate(queue_items):
             item.sort_index = index
-        if self._shuffle_enabled:
+        if self._shuffle_enabled and len(queue_items) > 2:
             queue_items = self.__shuffle_items(queue_items)
         self._items = queue_items
         if self.use_queue_stream:
index f7452ccaef6ce5a4c50be31065e84df8e420a395..e0cf04055fd8d9066797fcc689c44478fa60de23 100644 (file)
@@ -44,8 +44,8 @@ class StreamDetails(DataClassDictMixin):
     seconds_played: int = 0
     gain_correct: float = 0
     loudness: Optional[float] = None
-    sample_rate: int = 44100
-    bit_depth: int = 16
+    sample_rate: Optional[int] = None
+    bit_depth: Optional[int] = None
     media_type: MediaType = MediaType.TRACK
 
     def to_dict(
index 874e4e4e307442f5f8106c054b86e5cd25728f8b..78614d98afbe0002537a38efd7621c3aa53974ea 100644 (file)
@@ -49,7 +49,7 @@ class ChromecastPlayer(Player):
         self._available = False
         self._status_listener: Optional[CastStatusListener] = None
         self._is_speaker_group = False
-        self._throttler = Throttler(rate_limit=1, period=0.2)
+        self._throttler = Throttler(rate_limit=1, period=0.1)
 
     @property
     def player_id(self) -> str:
@@ -339,7 +339,7 @@ class ChromecastPlayer(Player):
     async def cmd_queue_load(self, queue_items: List[QueueItem]) -> None:
         """Load (overwrite) queue with new items."""
         player_queue = self.mass.players.get_player_queue(self.player_id)
-        cc_queue_items = self.__create_queue_items(queue_items[:25])
+        cc_queue_items = self.__create_queue_items(queue_items[:50])
         repeat_enabled = player_queue.use_queue_stream or player_queue.repeat_enabled
         queuedata = {
             "type": "QUEUE_LOAD",
@@ -347,16 +347,16 @@ class ChromecastPlayer(Player):
             "shuffle": False,  # handled by our queue controller
             "queueType": "PLAYLIST",
             "startIndex": 0,  # Item index to play after this request or keep same item if undefined
-            "items": cc_queue_items,  # only load 25 tracks at once or the socket will crash
+            "items": cc_queue_items,  # only load 50 tracks at once or the socket will crash
         }
         await self.chromecast_command(self.__send_player_queue, queuedata)
         if len(queue_items) > 50:
-            await self.cmd_queue_append(queue_items[26:])
+            await self.cmd_queue_append(queue_items[51:])
 
     async def cmd_queue_append(self, queue_items: List[QueueItem]) -> None:
         """Append new items at the end of the queue."""
         cc_queue_items = self.__create_queue_items(queue_items)
-        async for chunk in yield_chunks(cc_queue_items, 25):
+        async for chunk in yield_chunks(cc_queue_items, 50):
             queuedata = {
                 "type": "QUEUE_INSERT",
                 "insertBefore": None,
@@ -374,7 +374,7 @@ class ChromecastPlayer(Player):
         return {
             "opt_itemId": queue_item.queue_item_id,
             "autoplay": True,
-            "preloadTime": 10,
+            "preloadTime": 0,
             "playbackDuration": int(queue_item.duration),
             "startTime": 0,
             "activeTrackIds": [],
@@ -389,9 +389,7 @@ class ChromecastPlayer(Player):
                 "streamType": "LIVE" if player_queue.use_queue_stream else "BUFFERED",
                 "metadata": {
                     "title": queue_item.name,
-                    "artist": next(iter(queue_item.artists)).name
-                    if queue_item.artists
-                    else "",
+                    "artist": "/".join(x.name for x in queue_item.artists),
                 },
                 "duration": int(queue_item.duration),
             },
index 963eeac5f3be376171896ebe1e472fe45cca3f2b..a3d617d0c6c8bf3c87548f009b7b4428d62a2dde 100644 (file)
@@ -98,7 +98,7 @@ async def stream_queue(request: Request):
                 "Queue stream finished for: %s",
                 player_queue.player.name,
             )
-            return resp
+    return resp
 
 
 @routes.get("/stream/queue/{player_id}/{queue_item_id}")
@@ -111,7 +111,7 @@ async def stream_single_queue_item(request: Request):
     player_queue = mass.players.get_player_queue(player_id)
     if not player_queue:
         raise HTTPNotFound(reason="invalid player_id")
-    if player_queue.use_queue_stream and not request.query.get("alert"):
+    if player_queue.use_queue_stream:
         # redirect request if player switched to queue streaming
         return await stream_queue(request)
     LOGGER.debug("Stream request for %s", player_queue.player.name)