Fix YoutubeMusic UnplayableMedia issue (#686)
authorMarvin Schenkel <marvinschenkel@gmail.com>
Sun, 28 May 2023 10:54:35 +0000 (12:54 +0200)
committerGitHub <noreply@github.com>
Sun, 28 May 2023 10:54:35 +0000 (12:54 +0200)
* Fix retry of expired timestamp.

* Fix merge conflicts.

music_assistant/server/providers/ytmusic/__init__.py
music_assistant/server/providers/ytmusic/manifest.json
requirements_all.txt

index dfa940596584bf1ec2dd2c46dbc09689925a84fc..e5c9733a88cedbfb8ea638d71833f63635e131df 100644 (file)
@@ -469,7 +469,7 @@ class YoutubeMusicProvider(MusicProvider):
             return tracks
         return []
 
-    async def get_stream_details(self, item_id: str) -> StreamDetails:
+    async def get_stream_details(self, item_id: str, retry=True) -> StreamDetails:
         """Return the content details for the given track when it will be streamed."""
         data = {
             "playbackContext": {
@@ -480,6 +480,14 @@ class YoutubeMusicProvider(MusicProvider):
         track_obj = await self._post_data("player", data=data)
         stream_format = await self._parse_stream_format(track_obj)
         url = await self._parse_stream_url(stream_format=stream_format, item_id=item_id)
+        if not await self._is_valid_deciphered_url(url=url):
+            if not retry:
+                raise UnplayableMediaError(f"Could not resolve a valid URL for item '{item_id}'.")
+            self.logger.debug(
+                "Invalid playback URL encountered. Retrying with new signature timestamp."
+            )
+            self._signature_timestamp = await self._get_signature_timestamp()
+            return await self.get_stream_details(item_id=item_id, retry=False)
         stream_details = StreamDetails(
             provider=self.instance_id,
             item_id=item_id,
@@ -744,27 +752,11 @@ class YoutubeMusicProvider(MusicProvider):
                 ciphered_signature=cipher_parts["s"], item_id=item_id
             )
             url = cipher_parts["url"] + "&sig=" + signature
-            if not await self._is_valid_deciphered_url(url=url):
-                raise UnplayableMediaError(f"Obtained invalid URL for item '{item_id}'.")
-            # Disable caching for now.
-            # Verify if URL is playable. If not, obtain a new cipher and try again.
-            # if not await self._is_valid_deciphered_url(url=url):
-            #     if retry > 50:
-            #         raise UnplayableMediaError(
-            #             f"Cannot obtain a valid URL for item '{item_id}' after renewing
-            #               cipher {retry} times."
-            #         )
-            #     self.logger.debug("Cipher expired. Obtaining new Cipher.")
-            #     self._cipher = None
-            #     return await self._parse_stream_url(
-            #         stream_format=stream_format, item_id=item_id, retry=retry + 1
-            #     )
         elif stream_format.get("url"):
             # Non secured URL
             url = stream_format.get("url")
         else:
-            # TODO: Remove, this is for debugging purposes
-            self.logger.warning(
+            self.logger.debug(
                 f"Something went wrong. No URL found for stream format {stream_format}"
             )
         return url
@@ -780,21 +772,16 @@ class YoutubeMusicProvider(MusicProvider):
             cipher = pytube.cipher.Cipher(js=ytm_js)
             return cipher
 
-        cipher = await asyncio.to_thread(_decipher)
-        return cipher.get_signature(ciphered_signature)
-
-        # if not self._cipher:
-        #     self.logger.debug("Grabbing a new cipher.")
-        #     self._cipher = await asyncio.to_thread(_decipher)
-        #     self.logger.debug(f"Cipher is {self._cipher}")
-        # return self._cipher.get_signature(ciphered_signature)
+        if not self._cipher:
+            self._cipher = await asyncio.to_thread(_decipher)
+        return self._cipher.get_signature(ciphered_signature)
 
     async def _is_valid_deciphered_url(self, url: str) -> bool:
         """Verify whether the URL has been deciphered using a valid cipher."""
         async with self.mass.http_session.head(url) as response:
-            # TODO: Remove, this is for debugging purposes
+            # TODO: Remove after 403 issue has been verified as fixed
             if response.status != 200:
-                self.logger.warn(f"Deciphered URL HTTP status: {response.status}")
+                self.logger.debug(f"Deciphered URL HTTP status: {response.status}")
             return response.status != 403
 
     def _get_item_mapping(self, media_type: MediaType, key: str, name: str) -> ItemMapping:
index 44defd14439c30e3321eb0cb6a9711ec9e5683f2..0bbe9174d7151c90eb948364bf1d3f648f9d10de 100644 (file)
@@ -4,7 +4,7 @@
   "name": "YouTube Music",
   "description": "Support for the YouTube Music streaming provider in Music Assistant.",
   "codeowners": ["@MarvinSchenkel"],
-  "requirements": ["ytmusicapi==1.0.2", "git+https://github.com/pytube/pytube.git@refs/pull/1501/head"],
+  "requirements": ["ytmusicapi==1.0.0", "git+https://github.com/pytube/pytube.git@refs/pull/1501/head"],
   "documentation": "https://github.com/music-assistant/hass-music-assistant/discussions/606",
   "multi_instance": true
 }
index 80a0acc7864a1ce8ee18135ab1589282641768bc..92fb5a6fe2f179872a30079101c7b843c87c966a 100644 (file)
@@ -29,5 +29,5 @@ shortuuid==1.0.11
 soco==0.29.1
 unidecode==1.3.6
 xmltodict==0.13.0
-ytmusicapi==1.0.2
+ytmusicapi==1.0.0
 zeroconf==0.62.0