check_audio_support,
crossfade_pcm_parts,
get_media_stream,
+ get_preview_stream,
get_sox_args_for_pcm_stream,
get_stream_details,
strip_silence,
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,
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"]
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,
+ )
+ )
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: