Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-ytmusicapi for 
openSUSE:Factory checked in at 2025-09-01 17:18:06
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-ytmusicapi (Old)
 and      /work/SRC/openSUSE:Factory/.python-ytmusicapi.new.1977 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-ytmusicapi"

Mon Sep  1 17:18:06 2025 rev:11 rq:1302107 version:1.11.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-ytmusicapi/python-ytmusicapi.changes      
2025-08-19 16:48:48.405576746 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-ytmusicapi.new.1977/python-ytmusicapi.changes
    2025-09-01 17:18:50.492595165 +0200
@@ -1,0 +2,12 @@
+Sun Aug 31 08:29:24 UTC 2025 - Christophe Marin <[email protected]>
+
+- Update to 1.11.1
+  * search: added a fallback for top result parsing to fix a
+    KeyError
+  * get_home: fix song artist parsing
+  * get_home: parse recommended channels
+  * get_playlist/get_episodes_playlist: fix metadata
+  * get_library_playlists: Don't throw an error if playlist
+    title is missing
+
+-------------------------------------------------------------------

Old:
----
  ytmusicapi-1.11.0.tar.gz

New:
----
  ytmusicapi-1.11.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-ytmusicapi.spec ++++++
--- /var/tmp/diff_new_pack.tKoLnj/_old  2025-09-01 17:18:50.976615659 +0200
+++ /var/tmp/diff_new_pack.tKoLnj/_new  2025-09-01 17:18:50.980615828 +0200
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-ytmusicapi
-Version:        1.11.0
+Version:        1.11.1
 Release:        0
 Summary:        Unofficial API for YouTube Music
 License:        MIT

++++++ ytmusicapi-1.11.0.tar.gz -> ytmusicapi-1.11.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/.github/workflows/lint.yml 
new/ytmusicapi-1.11.1/.github/workflows/lint.yml
--- old/ytmusicapi-1.11.0/.github/workflows/lint.yml    2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/.github/workflows/lint.yml    2025-08-30 
21:33:06.000000000 +0200
@@ -12,7 +12,7 @@
   ruff:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v5
       - uses: chartboost/ruff-action@v1
         with:
           version: 0.11.5
@@ -23,7 +23,7 @@
   mypy:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v5
       - uses: actions/setup-python@v5
         with:
           python-version: "3.10"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/.github/workflows/pdm.yml 
new/ytmusicapi-1.11.1/.github/workflows/pdm.yml
--- old/ytmusicapi-1.11.0/.github/workflows/pdm.yml     2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/.github/workflows/pdm.yml     2025-08-30 
21:33:06.000000000 +0200
@@ -8,7 +8,7 @@
   update-dependencies:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v5
 
       - name: Update dependencies
         uses: pdm-project/update-deps-action@main
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ytmusicapi-1.11.0/.github/workflows/pythonpublish.yml 
new/ytmusicapi-1.11.1/.github/workflows/pythonpublish.yml
--- old/ytmusicapi-1.11.0/.github/workflows/pythonpublish.yml   2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/.github/workflows/pythonpublish.yml   2025-08-30 
21:33:06.000000000 +0200
@@ -14,7 +14,7 @@
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v5
       - name: Set up Python
         uses: actions/setup-python@v5
         with:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/PKG-INFO 
new/ytmusicapi-1.11.1/PKG-INFO
--- old/ytmusicapi-1.11.0/PKG-INFO      2025-07-31 20:52:19.886589000 +0200
+++ new/ytmusicapi-1.11.1/PKG-INFO      2025-08-30 21:33:15.078060600 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: ytmusicapi
-Version: 1.11.0
+Version: 1.11.1
 Summary: Unofficial API for YouTube Music
 Author-email: sigma67 <[email protected]>
 License: MIT License
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/tests/mixins/test_browsing.py 
new/ytmusicapi-1.11.1/tests/mixins/test_browsing.py
--- old/ytmusicapi-1.11.0/tests/mixins/test_browsing.py 2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/tests/mixins/test_browsing.py 2025-08-30 
21:33:06.000000000 +0200
@@ -13,8 +13,22 @@
     def test_get_home(self, yt, yt_auth):
         result = yt.get_home()
         assert len(result) >= 2
-        result = yt_auth.get_home(limit=15)
+        result = yt_auth.get_home(limit=20)
         assert len(result) >= 15
+        assert all(
+            # ensure we aren't parsing specifiers like "Song" as artist names
+            [
+                item["artists"][0]["id"]
+                or item["artists"][0]["name"].lower() not in 
yt_auth.parser.get_api_result_types()
+                for section in result
+                for item in section["contents"]
+                if item and len(item.get("artists", [])) > 1
+            ]
+        )
+        assert all(
+            # ensure all links are supported by parse_mixed_content
+            [item is not None for section in result for item in 
section["contents"]]
+        )
 
     def test_get_artist(self, yt):
         results = yt.get_artist("MPLAUCmMUZbaYdNH0bEd1PAlAqsA")
@@ -42,6 +56,7 @@
     def test_get_artist_albums(self, yt):
         artist = yt.get_artist("UCAeLFBCQS7FvI8PvBrWvSBg")
         results = yt.get_artist_albums(artist["albums"]["browseId"], 
artist["albums"]["params"])
+        assert all("artists" not in result for result in results)  # artist 
info is omitted from the results
         assert len(results) == 100
         results = yt.get_artist_albums(artist["singles"]["browseId"], 
artist["singles"]["params"])
         assert len(results) == 100
@@ -89,7 +104,7 @@
 
     def test_get_album_browse_id_issue_470(self, yt):
         escaped_browse_id = 
yt.get_album_browse_id("OLAK5uy_nbMYyrfeg5ZgknoOsOGBL268hGxtcbnDM")
-        assert escaped_browse_id == "MPREb_pZhPA6GfQmN"
+        assert len(escaped_browse_id) == 17
 
     def test_get_album_2024(self, yt):
         with open(Path(__file__).parent.parent / "data" / 
"2024_03_get_album.json", encoding="utf8") as f:
@@ -179,6 +194,15 @@
         song = yt_oauth.get_watch_playlist(sample_video)
         song = yt_oauth.get_song_related(song["related"])
         assert len(song) >= 5
+        assert all(
+            # ensure every video is associated with a view count or music album
+            [
+                item.get("views") or item.get("album")
+                for section in song
+                for item in section["contents"]
+                if "videoId" in item
+            ]
+        )
 
     def test_get_lyrics(self, config, yt, sample_video):
         playlist = yt.get_watch_playlist(sample_video)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/tests/mixins/test_explore.py 
new/ytmusicapi-1.11.1/tests/mixins/test_explore.py
--- old/ytmusicapi-1.11.0/tests/mixins/test_explore.py  2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/tests/mixins/test_explore.py  2025-08-30 
21:33:06.000000000 +0200
@@ -16,7 +16,10 @@
         assert all(item["audioPlaylistId"].startswith("OLA") for item in 
explore["new_releases"])
 
         # check top_songs if present
-        assert all(item["videoId"] for item in explore.get("top_songs", 
{"items": []})["items"])
+        assert all(
+            item["videoId"] and (item.get("views") or item.get("album"))
+            for item in explore.get("top_songs", {"items": []})["items"]
+        )
 
         assert all(
             item["videoId"] and all(artist["id"] for artist in item["artists"])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/tests/mixins/test_playlists.py 
new/ytmusicapi-1.11.1/tests/mixins/test_playlists.py
--- old/ytmusicapi-1.11.0/tests/mixins/test_playlists.py        2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/tests/mixins/test_playlists.py        2025-08-30 
21:33:06.000000000 +0200
@@ -80,6 +80,13 @@
         assert playlist["trackCount"] is None  # playlist has no trackCount
         assert len(playlist["tracks"]) >= 100
 
+    def test_get_playlist_author(self, yt):
+        playlist = yt.get_playlist("PL9tY0BWXOZFu4vlBOzIOmvT6wjYb2jNiV")
+        assert "artists" not in playlist  # shouldn't return the "artists" key 
from parse_song_runs
+        assert playlist["author"] == {"name": "Vevo", "id": 
"UC2pmfLm7iq6Ov1UwYrWYkZA"}
+        playlist = 
yt.get_playlist("RDCLAK5uy_l2pHac-aawJYLcesgTf67gaKU-B9ekk1o")
+        assert playlist["author"] == {"name": "YouTube Music", "id": None}
+
     @pytest.mark.parametrize("language", SUPPORTED_LANGUAGES)
     def test_get_playlist_languages(self, language):
         yt = YTMusic(language=language)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/tests/mixins/test_podcasts.py 
new/ytmusicapi-1.11.1/tests/mixins/test_podcasts.py
--- old/ytmusicapi-1.11.0/tests/mixins/test_podcasts.py 2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/tests/mixins/test_podcasts.py 2025-08-30 
21:33:06.000000000 +0200
@@ -49,3 +49,6 @@
     def test_get_episodes_playlist(self, yt_brand):
         playlist = yt_brand.get_episodes_playlist()
         assert len(playlist["episodes"]) > 90
+        assert playlist["description"]
+        assert playlist["year"]
+        assert playlist["author"]["id"] and playlist["author"]["name"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/tests/mixins/test_search.py 
new/ytmusicapi-1.11.1/tests/mixins/test_search.py
--- old/ytmusicapi-1.11.0/tests/mixins/test_search.py   2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/tests/mixins/test_search.py   2025-08-30 
21:33:06.000000000 +0200
@@ -142,6 +142,16 @@
             "name": "Stanford GSB Podcasts",
         }
 
+    def test_search_top_result_video(self, yt):
+        results = yt.search("Fuel Eminem")
+        assert results[0]["category"] == "Top result"
+        assert results[0]["resultType"] == "video"
+        assert results[0]["videoId"] == "t5H_CewqpKA"
+        assert results[0]["artists"] == [
+            {"name": "Eminem", "id": "UCedvOgsKFzcK3hA5taf3KoQ"},
+            {"name": "JID", "id": "UCRlGNubLJBgW9VRCuiUnuYw"},
+        ]
+
     def test_search_uploads(self, config, yt, yt_oauth):
         with pytest.raises(Exception, match="No filter can be set when 
searching uploads"):
             yt.search(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/ytmusicapi/constants.py 
new/ytmusicapi-1.11.1/ytmusicapi/constants.py
--- old/ytmusicapi-1.11.0/ytmusicapi/constants.py       2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/ytmusicapi/constants.py       2025-08-30 
21:33:06.000000000 +0200
@@ -5,7 +5,7 @@
 USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) 
Gecko/20100101 Firefox/88.0"
 # fmt: off
 SUPPORTED_LANGUAGES = {
-    "ar", "de", "en", "es", "fr", "hi", "it", "ja", "ko", "nl", "pt", "ru", 
"tr", "ur", "zh_CN",
+    "ar", "cs", "de", "en", "es", "fr", "hi", "it", "ja", "ko", "nl", "pt", 
"ru", "tr", "ur", "zh_CN",
     "zh_TW"
 }
 SUPPORTED_LOCATIONS = {
Binary files old/ytmusicapi-1.11.0/ytmusicapi/locales/cs/LC_MESSAGES/base.mo 
and new/ytmusicapi-1.11.1/ytmusicapi/locales/cs/LC_MESSAGES/base.mo differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ytmusicapi-1.11.0/ytmusicapi/locales/cs/LC_MESSAGES/base.po 
new/ytmusicapi-1.11.1/ytmusicapi/locales/cs/LC_MESSAGES/base.po
--- old/ytmusicapi-1.11.0/ytmusicapi/locales/cs/LC_MESSAGES/base.po     
1970-01-01 01:00:00.000000000 +0100
+++ new/ytmusicapi-1.11.1/ytmusicapi/locales/cs/LC_MESSAGES/base.po     
2025-08-30 21:33:06.000000000 +0200
@@ -0,0 +1,94 @@
+# Translations for ytmusicapi
+# Copyright (C) 2023 sigma67
+# This file is distributed under the same license as ytmusicapi
+# sigma67 <[email protected]>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-03-13 20:07+0100\n"
+"PO-Revision-Date: 2025-08-21 19:47+0200\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"Language: cs_CZ\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 3.7\n"
+
+#: parsers/i18n.py:32
+msgid "album"
+msgstr "album"
+
+#: parsers/i18n.py:33
+msgid "artist"
+msgstr "interpret"
+
+#: parsers/i18n.py:34
+msgid "playlist"
+msgstr "playlist"
+
+#: parsers/i18n.py:35
+msgid "song"
+msgstr "skladba"
+
+#: parsers/i18n.py:36
+msgid "video"
+msgstr "video"
+
+#: parsers/i18n.py:37
+msgid "station"
+msgstr "stanice"
+
+#: parsers/i18n.py:38
+msgid "profile"
+msgstr "profil"
+
+#: parsers/i18n.py:39
+msgid "podcast"
+msgstr "podcast"
+
+#: parsers/i18n.py:40
+msgid "episode"
+msgstr "epizoda"
+
+#: parsers/i18n.py:46
+msgid "single"
+msgstr "singl"
+
+#: parsers/i18n.py:47
+msgid "ep"
+msgstr "ep"
+
+#: parsers/i18n.py:55
+msgid "albums"
+msgstr "alba"
+
+#: parsers/i18n.py:56
+msgid "singles & eps"
+msgstr "singly a ep"
+
+#: parsers/i18n.py:57
+msgid "shows"
+msgstr "pořady"
+
+#: parsers/i18n.py:58
+msgid "videos"
+msgstr "videa"
+
+#: parsers/i18n.py:59
+msgid "playlists"
+msgstr "playlisty"
+
+#: parsers/i18n.py:60
+msgid "related"
+msgstr "související"
+
+#: parsers/i18n.py:61
+msgid "episodes"
+msgstr "epizody"
+
+#: parsers/i18n.py:62
+msgid "podcasts"
+msgstr "podcasty"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/ytmusicapi/mixins/browsing.py 
new/ytmusicapi-1.11.1/ytmusicapi/mixins/browsing.py
--- old/ytmusicapi-1.11.0/ytmusicapi/mixins/browsing.py 2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/ytmusicapi/mixins/browsing.py 2025-08-30 
21:33:06.000000000 +0200
@@ -1,6 +1,5 @@
 import re
 import warnings
-from collections.abc import Callable
 from typing import Literal, cast, overload
 
 from ytmusicapi.continuations import (
@@ -117,8 +116,7 @@
         body = {"browseId": "FEmusic_home"}
         response = self._send_request(endpoint, body)
         results = nav(response, SINGLE_COLUMN_TAB + SECTION_LIST)
-        home = []
-        home.extend(parse_mixed_content(results))
+        home = parse_mixed_content(results)
 
         section_list = nav(response, [*SINGLE_COLUMN_TAB, 
"sectionListRenderer"])
         if "continuations" in section_list:
@@ -126,11 +124,13 @@
                 endpoint, body, additionalParams
             )
 
-            parse_func: Callable[[JsonList], JsonList] = lambda contents: 
parse_mixed_content(contents)
-
             home.extend(
                 get_continuations(
-                    section_list, "sectionListContinuation", limit - 
len(home), request_func, parse_func
+                    section_list,
+                    "sectionListContinuation",
+                    limit - len(home),
+                    request_func,
+                    parse_mixed_content,
                 )
             )
 
@@ -847,7 +847,9 @@
 
         response = self._send_request("browse", {"browseId": browseId})
         sections = nav(response, ["contents", *SECTION_LIST])
-        return parse_mixed_content(sections)
+        return parse_mixed_content(
+            sections,
+        )
 
     @overload
     def get_lyrics(self, browseId: str, timestamps: Literal[False] = False) -> 
Lyrics | None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/ytmusicapi/mixins/explore.py 
new/ytmusicapi-1.11.1/ytmusicapi/mixins/explore.py
--- old/ytmusicapi-1.11.0/ytmusicapi/mixins/explore.py  2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/ytmusicapi/mixins/explore.py  2025-08-30 
21:33:06.000000000 +0200
@@ -237,7 +237,7 @@
                 case playlist_id if playlist_id.startswith("VLOLA"):
                     explore["trending"] = {
                         "playlist": playlist_id,
-                        "items": parse_content_list(contents, 
parse_trending_song, MRLIR),
+                        "items": parse_content_list(contents, parse_song_flat, 
MRLIR),
                     }
 
         return explore
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/ytmusicapi/mixins/playlists.py 
new/ytmusicapi-1.11.1/ytmusicapi/mixins/playlists.py
--- old/ytmusicapi-1.11.0/ytmusicapi/mixins/playlists.py        2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/ytmusicapi/mixins/playlists.py        2025-08-30 
21:33:06.000000000 +0200
@@ -34,7 +34,10 @@
               "title": "New EDM This Week 03/13/2020",
               "thumbnails": [...]
               "description": "Weekly r/EDM new release roundup. Created with 
github.com/sigma67/spotifyplaylist_to_gmusic",
-              "author": "sigmatics",
+              "author": {
+                  "name": "sigmatics",
+                  "id": "..."
+              },
               "year": "2020",
               "duration": "6+ hours",
               "duration_seconds": 52651,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/ytmusicapi/mixins/search.py 
new/ytmusicapi-1.11.1/ytmusicapi/mixins/search.py
--- old/ytmusicapi-1.11.0/ytmusicapi/mixins/search.py   2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/ytmusicapi/mixins/search.py   2025-08-30 
21:33:06.000000000 +0200
@@ -234,18 +234,14 @@
             else:
                 continue
 
-            api_search_result_types = self.parser.get_api_result_types()
-
-            search_results.extend(
-                parse_search_results(shelf_contents, api_search_result_types, 
result_type, category)
-            )
+            search_results.extend(parse_search_results(shelf_contents, 
result_type, category))
 
             if filter:  # if filter is set, there are continuations
                 request_func: RequestFuncType = lambda additionalParams: 
self._send_request(
                     endpoint, body, additionalParams
                 )
                 parse_func: ParseFuncType = lambda contents: 
parse_search_results(
-                    contents, api_search_result_types, result_type, category
+                    contents, result_type, category
                 )
 
                 search_results.extend(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/ytmusicapi/parsers/_utils.py 
new/ytmusicapi-1.11.1/ytmusicapi/parsers/_utils.py
--- old/ytmusicapi-1.11.0/ytmusicapi/parsers/_utils.py  2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/ytmusicapi/parsers/_utils.py  2025-08-30 
21:33:06.000000000 +0200
@@ -7,6 +7,8 @@
 from ytmusicapi.navigation import *
 from ytmusicapi.type_alias import JsonDict, JsonList
 
+from .constants import DOT_SEPARATOR_RUN
+
 P = ParamSpec("P")
 R = TypeVar("R")
 
@@ -69,7 +71,7 @@
 
 def get_dot_separator_index(runs: JsonList) -> int:
     try:
-        index = runs.index({"text": " • "})
+        index = runs.index(DOT_SEPARATOR_RUN)
     except ValueError:
         index = len(runs)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/ytmusicapi/parsers/browsing.py 
new/ytmusicapi-1.11.1/ytmusicapi/parsers/browsing.py
--- old/ytmusicapi-1.11.0/ytmusicapi/parsers/browsing.py        2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/ytmusicapi/parsers/browsing.py        2025-08-30 
21:33:06.000000000 +0200
@@ -7,7 +7,9 @@
 from .songs import *
 
 
-def parse_mixed_content(rows: JsonList) -> JsonList:
+def parse_mixed_content(
+    rows: JsonList,
+) -> JsonList:
     items = []
     for row in rows:
         if DESCRIPTION_SHELF[0] in row:
@@ -32,7 +34,7 @@
                             content = parse_song(data)
                     elif page_type == "MUSIC_PAGE_TYPE_ALBUM":
                         content = parse_album(data)
-                    elif page_type == "MUSIC_PAGE_TYPE_ARTIST":
+                    elif page_type in ["MUSIC_PAGE_TYPE_ARTIST", 
"MUSIC_PAGE_TYPE_USER_CHANNEL"]:
                         content = parse_related_artist(data)
                     elif page_type == "MUSIC_PAGE_TYPE_PLAYLIST":
                         content = parse_playlist(data)
@@ -92,7 +94,7 @@
         "playlistId": nav(result, NAVIGATION_PLAYLIST_ID, True),
         "thumbnails": nav(result, THUMBNAIL_RENDERER),
     }
-    song.update(parse_song_runs(nav(result, SUBTITLE_RUNS)))
+    song.update(parse_song_runs(nav(result, SUBTITLE_RUNS), 
skip_type_spec=True))
     return song
 
 
@@ -101,17 +103,18 @@
     song = {
         "title": nav(columns[0], TEXT_RUN_TEXT),
         "videoId": nav(columns[0], TEXT_RUN + NAVIGATION_VIDEO_ID, True),
-        "artists": parse_song_artists(data, 1),
         "thumbnails": nav(data, THUMBNAILS),
         "isExplicit": nav(data, BADGE_LABEL, True) is not None,
     }
+
+    runs = nav(columns[1], TEXT_RUNS)
+    song.update(parse_song_runs(runs, skip_type_spec=True))
+
     if len(columns) > 2 and columns[2] is not None and "navigationEndpoint" in 
nav(columns[2], TEXT_RUN):
         song["album"] = {
             "name": nav(columns[2], TEXT_RUN_TEXT),
             "id": nav(columns[2], TEXT_RUN + NAVIGATION_BROWSE_ID),
         }
-    else:
-        song["views"] = nav(columns[1], ["text", "runs", -1, "text"]).split(" 
")[0]
 
     return song
 
@@ -138,7 +141,11 @@
 
 def parse_playlist(data: JsonDict) -> JsonDict:
     playlist = {
-        "title": nav(data, TITLE_TEXT),
+        "title": nav(
+            data,
+            TITLE_TEXT,
+            none_if_absent=True,  # rare but possible for playlist title to be 
missing
+        ),
         "playlistId": nav(data, TITLE + NAVIGATION_BROWSE_ID)[2:],
         "thumbnails": nav(data, THUMBNAIL_RENDERER),
     }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/ytmusicapi/parsers/constants.py 
new/ytmusicapi-1.11.1/ytmusicapi/parsers/constants.py
--- old/ytmusicapi-1.11.0/ytmusicapi/parsers/constants.py       1970-01-01 
01:00:00.000000000 +0100
+++ new/ytmusicapi-1.11.1/ytmusicapi/parsers/constants.py       2025-08-30 
21:33:06.000000000 +0200
@@ -0,0 +1 @@
+DOT_SEPARATOR_RUN = {"text": " • "}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/ytmusicapi/parsers/explore.py 
new/ytmusicapi-1.11.1/ytmusicapi/parsers/explore.py
--- old/ytmusicapi-1.11.0/ytmusicapi/parsers/explore.py 2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/ytmusicapi/parsers/explore.py 2025-08-30 
21:33:06.000000000 +0200
@@ -7,7 +7,7 @@
 
 
 def parse_chart_song(data: JsonDict) -> JsonDict:
-    parsed = parse_trending_song(data)
+    parsed = parse_song_flat(data)
     parsed.update(parse_ranking(data))
     return parsed
 
@@ -43,18 +43,6 @@
     return parsed
 
 
-def parse_trending_song(data: JsonDict) -> JsonDict:
-    flex_0 = get_flex_column_item(data, 0)
-    return {
-        "title": nav(flex_0, TEXT_RUN_TEXT),
-        "videoId": nav(flex_0, TEXT_RUN + NAVIGATION_VIDEO_ID, True),
-        **(parse_song_runs(nav(get_flex_column_item(data, 1), TEXT_RUNS))),
-        "playlistId": nav(flex_0, TEXT_RUN + NAVIGATION_PLAYLIST_ID, True),
-        "thumbnails": nav(data, THUMBNAILS),
-        "isExplicit": nav(data, BADGE_LABEL, True) is not None,
-    }
-
-
 def parse_ranking(data: JsonDict) -> JsonDict:
     return {
         "rank": nav(data, ["customIndexColumn", 
"musicCustomIndexColumnRenderer", *TEXT_RUN_TEXT]),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/ytmusicapi/parsers/playlists.py 
new/ytmusicapi-1.11.1/ytmusicapi/parsers/playlists.py
--- old/ytmusicapi-1.11.0/ytmusicapi/parsers/playlists.py       2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/ytmusicapi/parsers/playlists.py       2025-08-30 
21:33:06.000000000 +0200
@@ -26,15 +26,8 @@
     playlist.update(parse_playlist_header_meta(header))
     if playlist["thumbnails"] is None:
         playlist["thumbnails"] = nav(header, THUMBNAIL_CROPPED, True)
-    playlist["description"] = nav(header, DESCRIPTION, True)
-    run_count = len(nav(header, SUBTITLE_RUNS))
-    if run_count > 1:
-        playlist["author"] = {
-            "name": nav(header, SUBTITLE2),
-            "id": nav(header, [*SUBTITLE_RUNS, 2, *NAVIGATION_BROWSE_ID], 
True),
-        }
-        if run_count == 5:
-            playlist["year"] = nav(header, SUBTITLE3)
+    playlist["description"] = nav(header, ["description", *DESCRIPTION_SHELF, 
*DESCRIPTION], True)
+    playlist["year"] = nav(header, SUBTITLE2)
 
     return playlist
 
@@ -47,6 +40,24 @@
         "title": "".join([run["text"] for run in header.get("title", 
{}).get("runs", [])]),
         "thumbnails": nav(header, THUMBNAILS),
     }
+    if "facepile" in header:
+        playlist_meta["author"] = {
+            "name": nav(header, ["facepile", "avatarStackViewModel", "text", 
"content"]),
+            "id": nav(
+                header,
+                [
+                    "facepile",
+                    "avatarStackViewModel",
+                    "rendererContext",
+                    "commandContext",
+                    "onTap",
+                    "innertubeCommand",
+                    "browseEndpoint",
+                    "browseId",
+                ],
+                True,
+            ),
+        }
     if "runs" in header["secondSubtitle"]:
         second_subtitle_runs = header["secondSubtitle"]["runs"]
         has_views = (len(second_subtitle_runs) > 3) * 2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/ytmusicapi/parsers/search.py 
new/ytmusicapi-1.11.1/ytmusicapi/parsers/search.py
--- old/ytmusicapi-1.11.0/ytmusicapi/parsers/search.py  2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/ytmusicapi/parsers/search.py  2025-08-30 
21:33:06.000000000 +0200
@@ -34,7 +34,9 @@
 
 def parse_top_result(data: JsonDict, search_result_types: list[str]) -> 
JsonDict:
     result_type = get_search_result_type(nav(data, SUBTITLE), 
search_result_types)
-    search_result = {"category": nav(data, CARD_SHELF_TITLE), "resultType": 
result_type}
+    # header element is missing in some edge cases (#799)
+    category = nav(data, CARD_SHELF_TITLE, True) or "Top result"
+    search_result = {"category": category, "resultType": result_type}
     if result_type == "artist":
         subscribers = nav(data, SUBTITLE2, True)
         if subscribers:
@@ -79,9 +81,7 @@
     return search_result
 
 
-def parse_search_result(
-    data: JsonDict, api_search_result_types: list[str], result_type: str | 
None, category: str | None
-) -> JsonDict:
+def parse_search_result(data: JsonDict, result_type: str | None, category: str 
| None) -> JsonDict:
     default_offset = (not result_type or result_type == "album") * 2
     search_result: JsonDict = {"category": category}
     video_type = nav(data, [*PLAY_BUTTON, "playNavigationEndpoint", 
*NAVIGATION_VIDEO_TYPE], True)
@@ -189,9 +189,7 @@
         runs = flex_item["text"]["runs"]
         if flex_item2 := get_flex_column_item(data, 2):
             runs.extend([{"text": ""}, *flex_item2["text"]["runs"]])  # first 
item is a dummy separator
-        # ignore the first run if it is a type specifier (like "Single" or 
"Album")
-        runs_offset = (len(runs[0]) == 1 and runs[0]["text"].lower() in 
api_search_result_types) * 2
-        song_info = parse_song_runs(runs[runs_offset:])
+        song_info = parse_song_runs(runs, skip_type_spec=True)
         search_result.update(song_info)
 
     if result_type in ["artist", "album", "playlist", "profile", "podcast"]:
@@ -217,14 +215,10 @@
 
 def parse_search_results(
     results: JsonList,
-    api_search_result_types: list[str],
     resultType: str | None = None,
     category: str | None = None,
 ) -> JsonList:
-    return [
-        parse_search_result(result[MRLIR], api_search_result_types, 
resultType, category)
-        for result in results
-    ]
+    return [parse_search_result(result[MRLIR], resultType, category) for 
result in results]
 
 
 def get_search_params(filter: str | None, scope: str | None, ignore_spelling: 
bool) -> str | None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/ytmusicapi/parsers/songs.py 
new/ytmusicapi-1.11.1/ytmusicapi/parsers/songs.py
--- old/ytmusicapi-1.11.0/ytmusicapi/parsers/songs.py   2025-07-31 
20:52:13.000000000 +0200
+++ new/ytmusicapi-1.11.1/ytmusicapi/parsers/songs.py   2025-08-30 
21:33:06.000000000 +0200
@@ -3,6 +3,7 @@
 from ytmusicapi.type_alias import JsonDict, JsonList
 
 from ._utils import *
+from .constants import DOT_SEPARATOR_RUN
 
 
 def parse_song_artists(data: JsonDict, index: int) -> JsonList:
@@ -21,34 +22,68 @@
     return artists
 
 
-def parse_song_runs(runs: JsonList) -> JsonDict:
-    parsed: JsonDict = {"artists": []}
-    for i, run in enumerate(runs):
+def parse_song_run(run: JsonDict) -> JsonDict:
+    text = run["text"]
+
+    if "navigationEndpoint" in run:  # artist or album
+        item = {"name": text, "id": nav(run, NAVIGATION_BROWSE_ID, True)}
+
+        if item["id"] and (item["id"].startswith("MPRE") or "release_detail" 
in item["id"]):  # album
+            return {"type": "album", "data": item}
+        else:  # artist
+            return {"type": "artist", "data": item}
+    else:
+        # note: YT uses non-breaking space \xa0 to separate number and 
magnitude
+        if re.match(r"^\d([^ ])* [^ ]*$", text):
+            return {"type": "views", "data": text.split(" ")[0]}
+
+        elif re.match(r"^(\d+:)*\d+:\d+$", text):
+            return {"type": "duration", "data": text}
+
+        elif re.match(r"^\d{4}$", text):
+            return {"type": "year", "data": text}
+
+        else:  # artist without id
+            return {"type": "artist", "data": {"name": text, "id": None}}
+
+
+def parse_song_runs(runs: JsonList, skip_type_spec: bool = False) -> JsonDict:
+    """
+    :param skip_type_spec: if true, skip the type specifier (like "Song", 
"Single", or "Album") that may appear before artists ("Song • Eminem"). 
Otherwise, that text item is parsed as an artist with no ID.
+    """
+
+    parsed: JsonDict = {}
+
+    # prevent type specifier from being parsed as an artist
+    # it's the first run, separated from the actual artists by " • "
+    if (
+        skip_type_spec
+        and len(runs) > 2
+        and parse_song_run(runs[0])["type"] == "artist"
+        and runs[1] == DOT_SEPARATOR_RUN
+        and parse_song_run(runs[2])["type"] == "artist"
+    ):
+        runs = runs[2:]
+
+    for i, run in list(enumerate(runs)):
         if i % 2:  # uneven items are always separators
             continue
-        text = run["text"]
-        if "navigationEndpoint" in run:  # artist or album
-            item = {"name": text, "id": nav(run, NAVIGATION_BROWSE_ID, True)}
-
-            if item["id"] and (item["id"].startswith("MPRE") or 
"release_detail" in item["id"]):  # album
-                parsed["album"] = item
-            else:  # artist
-                parsed["artists"].append(item)
-
-        else:
-            # note: YT uses non-breaking space \xa0 to separate number and 
magnitude
-            if re.match(r"^\d([^ ])* [^ ]*$", text) and i > 0:
-                parsed["views"] = text.split(" ")[0]
-
-            elif re.match(r"^(\d+:)*\d+:\d+$", text):
-                parsed["duration"] = text
-                parsed["duration_seconds"] = parse_duration(text)
-
-            elif re.match(r"^\d{4}$", text):
-                parsed["year"] = text
 
-            else:  # artist without id
-                parsed["artists"].append({"name": text, "id": None})
+        parsed_run = parse_song_run(run)
+        data = parsed_run["data"]
+        match parsed_run["type"]:
+            case "album":
+                parsed["album"] = data
+            case "artist":
+                parsed["artists"] = parsed.get("artists", [])
+                parsed["artists"].append(data)
+            case "views":
+                parsed["views"] = data
+            case "duration":
+                parsed["duration"] = data
+                parsed["duration_seconds"] = parse_duration(data)
+            case "year":
+                parsed["year"] = data
 
     return parsed
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/ytmusicapi.egg-info/PKG-INFO 
new/ytmusicapi-1.11.1/ytmusicapi.egg-info/PKG-INFO
--- old/ytmusicapi-1.11.0/ytmusicapi.egg-info/PKG-INFO  2025-07-31 
20:52:19.000000000 +0200
+++ new/ytmusicapi-1.11.1/ytmusicapi.egg-info/PKG-INFO  2025-08-30 
21:33:14.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: ytmusicapi
-Version: 1.11.0
+Version: 1.11.1
 Summary: Unofficial API for YouTube Music
 Author-email: sigma67 <[email protected]>
 License: MIT License
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ytmusicapi-1.11.0/ytmusicapi.egg-info/SOURCES.txt 
new/ytmusicapi-1.11.1/ytmusicapi.egg-info/SOURCES.txt
--- old/ytmusicapi-1.11.0/ytmusicapi.egg-info/SOURCES.txt       2025-07-31 
20:52:19.000000000 +0200
+++ new/ytmusicapi-1.11.1/ytmusicapi.egg-info/SOURCES.txt       2025-08-30 
21:33:15.000000000 +0200
@@ -100,6 +100,8 @@
 ytmusicapi/locales/update_po.sh
 ytmusicapi/locales/ar/LC_MESSAGES/base.mo
 ytmusicapi/locales/ar/LC_MESSAGES/base.po
+ytmusicapi/locales/cs/LC_MESSAGES/base.mo
+ytmusicapi/locales/cs/LC_MESSAGES/base.po
 ytmusicapi/locales/de/LC_MESSAGES/base.mo
 ytmusicapi/locales/de/LC_MESSAGES/base.po
 ytmusicapi/locales/en/LC_MESSAGES/base.mo
@@ -150,6 +152,7 @@
 ytmusicapi/parsers/_utils.py
 ytmusicapi/parsers/albums.py
 ytmusicapi/parsers/browsing.py
+ytmusicapi/parsers/constants.py
 ytmusicapi/parsers/explore.py
 ytmusicapi/parsers/i18n.py
 ytmusicapi/parsers/library.py

Reply via email to