Add preview stream feature (#259)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 21 Apr 2022 11:31:03 +0000 (13:31 +0200)
committerGitHub <noreply@github.com>
Thu, 21 Apr 2022 11:31:03 +0000 (13:31 +0200)
music_assistant/controllers/stream.py
music_assistant/helpers/audio.py
music_assistant/providers/spotify/__init__.py

index c7ef8683562db115605900d65869c442842bc0d6..373a465b6ffe01dc371f5dec9161867f8105083d 100644 (file)
@@ -13,6 +13,7 @@ from music_assistant.helpers.audio import (
     check_audio_support,
     crossfade_pcm_parts,
     get_media_stream,
+    get_preview_stream,
     get_sox_args_for_pcm_stream,
     get_stream_details,
     strip_silence,
@@ -47,10 +48,18 @@ class StreamController:
             return f"http://{self._ip}:{self._port}/{queue_id}/{child_player}.{fmt}"
         return f"http://{self._ip}:{self._port}/{queue_id}.{fmt}"
 
+    async def get_preview_url(self, provider: str, track_id: str) -> str:
+        """Return url to short preview sample."""
+        track = await self.mass.music.tracks.get_provider_item(track_id, provider)
+        if preview := track.metadata.get("preview"):
+            return preview
+        return f"http://{self._ip}:{self._port}/preview/{provider}/{track_id}.mp3"
+
     async def setup(self) -> None:
         """Async initialize of module."""
         app = web.Application()
 
+        app.router.add_get("/preview/{provider}/{item_id}.mp3", self.serve_preview)
         app.router.add_get(
             "/{queue_id}/{player_id}.{format}",
             self.serve_multi_client_queue_stream,
@@ -89,6 +98,18 @@ class StreamController:
 
         self.logger.info("Started stream server on port %s", self._port)
 
+    async def serve_preview(self, request: web.Request):
+        """Serve short preview sample."""
+        provider = request.match_info["provider"]
+        item_id = request.match_info["item_id"]
+        resp = web.StreamResponse(
+            status=200, reason="OK", headers={"Content-Type": "audio/mp3"}
+        )
+        await resp.prepare(request)
+        async for _, chunk in get_preview_stream(self.mass, provider, item_id):
+            await resp.write(chunk)
+        return resp
+
     async def serve_queue_stream(self, request: web.Request):
         """Serve queue audio stream to a single player (encoded to fileformat of choice)."""
         queue_id = request.match_info["queue_id"]
index ca07938f774f3f4287a355313f8ea46191f6dc3c..7776535eccdaf48ffed10a6f9eb6f35dbc7c2eb3 100644 (file)
@@ -570,3 +570,66 @@ async def get_sox_args_for_pcm_stream(
     else:
         output_args = ["-t", output_format.sox_format(), "-"]
     return input_args + output_args
+
+
+async def get_preview_stream(
+    mass: MusicAssistant,
+    provider: str,
+    track_id: str,
+) -> AsyncGenerator[Tuple[bool, bytes], None]:
+    """Get the audio stream for the given streamdetails."""
+    music_prov = mass.music.get_provider(provider)
+
+    streamdetails = await music_prov.get_stream_details(track_id)
+
+    mass.signal_event(
+        MassEvent(
+            EventType.STREAM_STARTED,
+            object_id=streamdetails.provider,
+            data=streamdetails,
+        )
+    )
+    if streamdetails.type == StreamType.EXECUTABLE:
+        # stream from executable
+        input_args = [
+            streamdetails.path,
+            "|",
+            "ffmpeg",
+            "-hide_banner",
+            "-loglevel",
+            "error",
+            "-f",
+            streamdetails.content_type.value,
+            "-i",
+            "-",
+        ]
+    else:
+        input_args = [
+            "ffmpeg",
+            "-hide_banner",
+            "-loglevel",
+            "error",
+            "-i",
+            streamdetails.path,
+        ]
+    output_args = ["-ss", "30", "-to", "60", "-f", "mp3", "-q:a", "9", "-"]
+    async with AsyncProcess(input_args + output_args) as proc:
+
+        # yield chunks from stdout
+        # we keep 1 chunk behind to detect end of stream properly
+        try:
+            prev_chunk = b""
+            async for chunk in proc.iterate_chunks():
+                if prev_chunk:
+                    yield (False, prev_chunk)
+                prev_chunk = chunk
+            # send last chunk
+            yield (True, prev_chunk)
+        finally:
+            mass.signal_event(
+                MassEvent(
+                    EventType.STREAM_ENDED,
+                    object_id=streamdetails.provider,
+                    data=streamdetails,
+                )
+            )
index f3da3a1eae4bca5d0bfa2b3fc21299d91135e486..e1d93cec47ece3556c90f551e5d70adc36f706d1 100644 (file)
@@ -355,6 +355,8 @@ class SpotifyProvider(MusicProvider):
                 track.artists.append(artist)
 
         track.metadata["explicit"] = str(track_obj["explicit"]).lower()
+        if "preview_url" in track_obj:
+            track.metadata["preview"] = track_obj["preview_url"]
         if "external_ids" in track_obj and "isrc" in track_obj["external_ids"]:
             track.isrc = track_obj["external_ids"]["isrc"]
         if "album" in track_obj: