/
/
/
1"""Streaming operations for YouSee Musik."""
2
3from __future__ import annotations
4
5import re
6from base64 import b64encode
7from typing import TYPE_CHECKING
8
9from music_assistant_models.enums import ContentType, MediaType, StreamType
10from music_assistant_models.errors import MediaNotFoundError, ResourceTemporarilyUnavailable
11from music_assistant_models.media_items import AudioFormat
12from music_assistant_models.streamdetails import StreamDetails
13
14from music_assistant.helpers.datetime import iso_from_utc_timestamp, utc_timestamp
15from music_assistant.providers.yousee.constants import CONF_QUALITY
16
17if TYPE_CHECKING:
18 from music_assistant.providers.yousee.provider import YouSeeMusikProvider
19
20
21class YouSeeStreamingManager:
22 """Manages YouSee Musik streaming operations."""
23
24 def __init__(self, provider: YouSeeMusikProvider):
25 """Initialize streaming manager."""
26 self.provider = provider
27 self.api = provider.api
28 self.mass = provider.mass
29 self.logger = provider.logger
30
31 async def get_stream_details(self, item_id: str, media_type: MediaType) -> StreamDetails:
32 """Get streamdetails for a track."""
33 query = """
34 query playbackFull($id: ID!, $quality: StreamQuality!) {
35 playback(trackId: $id) {
36 full(quality: $quality)
37 }
38 }
39 """
40
41 if media_type != MediaType.TRACK:
42 raise MediaNotFoundError(f"Streaming of media type {media_type} is not supported")
43
44 variables = {
45 "id": item_id,
46 "quality": f"KBPS_{self.provider.config.get_value(CONF_QUALITY)}",
47 }
48
49 result = await self.api.post_graphql(query, variables)
50
51 playback_url = result.get("data", {}).get("playback", {}).get("full")
52 if not playback_url:
53 raise ResourceTemporarilyUnavailable(f"Track {item_id} is not available for streaming")
54
55 matches = re.search(r"mp4-(\d+)kbps", playback_url)
56 returned_playback_quality = int(matches.group(1)) if matches else None
57
58 return StreamDetails(
59 provider=self.provider.instance_id,
60 item_id=item_id,
61 audio_format=AudioFormat(
62 content_type=ContentType.MP4,
63 bit_rate=returned_playback_quality,
64 ),
65 media_type=MediaType.TRACK,
66 stream_type=StreamType.HLS,
67 allow_seek=True,
68 can_seek=True,
69 path=playback_url,
70 data={"start_ts": utc_timestamp()},
71 )
72
73 async def report_playback(
74 self,
75 streamdetails: StreamDetails,
76 ) -> None:
77 """Handle callback when given streamdetails completed streaming."""
78 mutation = """
79 mutation reportPlayback($report: ReportPlaybackInput!) {
80 reportPlayback(report: $report) {
81 ok
82 }
83 }
84 """
85
86 seconds_streamed = min(
87 utc_timestamp() - streamdetails.data["start_ts"],
88 streamdetails.seconds_streamed,
89 )
90
91 variables = {
92 "playbackUrl": streamdetails.path,
93 "playbackContext": b64encode(
94 f"catalog:track;{streamdetails.item_id}".encode()
95 ).decode(),
96 "playedSeconds": int(seconds_streamed),
97 "playedAt": iso_from_utc_timestamp(utc_timestamp()),
98 }
99
100 result = await self.api.post_graphql(mutation, {"report": variables})
101
102 if not result.get("data", {}).get("reportPlayback", {}).get("ok"):
103 self.logger.warning(
104 "Reporting playback for track %s failed with result %s",
105 streamdetails.item_id,
106 result,
107 )
108