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