Skip to content

Commit 614999c

Browse files
authored
search: fix sigma67#569 (sigma67#581)
* search: fix sigma67#569 * fix an issue with localized instances * add test for localized search
1 parent 94c67a8 commit 614999c

File tree

3 files changed

+61
-27
lines changed

3 files changed

+61
-27
lines changed

tests/mixins/test_search.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import pytest
22

3+
from ytmusicapi import YTMusic
4+
from ytmusicapi.parsers.search import ALL_RESULT_TYPES
5+
36

47
class TestSearch:
58
def test_search_exceptions(self, yt_auth):
@@ -14,13 +17,30 @@ def test_search_queries(self, yt, yt_brand, query: str) -> None:
1417
results = yt_brand.search(query)
1518
assert ["resultType" in r for r in results] == [True] * len(results)
1619
assert len(results) >= 10
20+
assert not any(
21+
artist["name"].lower() in ALL_RESULT_TYPES
22+
for result in results
23+
if "artists" in result
24+
for artist in result["artists"]
25+
)
1726
results = yt.search(query)
1827
assert len(results) >= 10
28+
assert not any(
29+
artist["name"].lower() in ALL_RESULT_TYPES
30+
for result in results
31+
if "artists" in result
32+
for artist in result["artists"]
33+
)
1934

2035
def test_search_ignore_spelling(self, yt_auth):
2136
results = yt_auth.search("Martin Stig Andersen - Deteriation", ignore_spelling=True)
2237
assert len(results) > 0
2338

39+
def test_search_localized(self):
40+
yt_local = YTMusic(language="it")
41+
results = yt_local.search("ABBA")
42+
assert all(result["resultType"] in ALL_RESULT_TYPES for result in results)
43+
2444
def test_search_filters(self, yt_auth):
2545
query = "hip hop playlist"
2646
results = yt_auth.search(query, filter="songs")

ytmusicapi/mixins/search.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,10 @@ def search(
192192
else:
193193
results = response["contents"]
194194

195-
results = nav(results, SECTION_LIST)
195+
section_list = nav(results, SECTION_LIST)
196196

197197
# no results
198-
if len(results) == 1 and "itemSectionRenderer" in results:
198+
if len(section_list) == 1 and "itemSectionRenderer" in section_list:
199199
return search_results
200200

201201
# set filter for parser
@@ -204,35 +204,31 @@ def search(
204204
elif scope == scopes[1]:
205205
filter = scopes[1]
206206

207-
for res in results:
207+
for res in section_list:
208208
if "musicCardShelfRenderer" in res:
209209
top_result = parse_top_result(
210210
res["musicCardShelfRenderer"], self.parser.get_search_result_types()
211211
)
212212
search_results.append(top_result)
213-
if results := nav(res, ["musicCardShelfRenderer", "contents"], True):
214-
category = None
215-
# category "more from youtube" is missing sometimes
216-
if "messageRenderer" in results[0]:
217-
category = nav(results.pop(0), ["messageRenderer", *TEXT_RUN_TEXT])
218-
type = None
219-
else:
213+
if not (shelf_contents := nav(res, ["musicCardShelfRenderer", "contents"], True)):
220214
continue
215+
type = category = None
216+
# if "more from youtube" is present, remove it - it's not parseable
217+
if "messageRenderer" in shelf_contents[0]:
218+
category = nav(shelf_contents.pop(0), ["messageRenderer", *TEXT_RUN_TEXT])
221219

222220
elif "musicShelfRenderer" in res:
223-
results = res["musicShelfRenderer"]["contents"]
224-
type_filter = filter
221+
shelf_contents = res["musicShelfRenderer"]["contents"]
225222
category = nav(res, MUSIC_SHELF + TITLE_TEXT, True)
226-
if not type_filter and scope == scopes[0]:
227-
type_filter = category
223+
type_filter = filter or category
228224

229225
type = type_filter[:-1].lower() if type_filter else None
230226

231227
else:
232228
continue
233229

234230
search_result_types = self.parser.get_search_result_types()
235-
search_results.extend(parse_search_results(results, search_result_types, type, category))
231+
search_results.extend(parse_search_results(shelf_contents, search_result_types, type, category))
236232

237233
if filter: # if filter is set, there are continuations
238234

ytmusicapi/parsers/search.py

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
from ._utils import *
22
from .songs import *
33

4+
UNIQUE_RESULT_TYPES = ["artist", "playlist", "song", "video", "station", "profile", "podcast", "episode"]
5+
ALL_RESULT_TYPES = ["album", *UNIQUE_RESULT_TYPES]
6+
47

58
def get_search_result_type(result_type_local, result_types_local):
69
if not result_type_local:
710
return None
8-
result_types = ["artist", "playlist", "song", "video", "station", "profile", "podcast", "episode"]
911
result_type_local = result_type_local.lower()
1012
# default to album since it's labeled with multiple values ('Single', 'EP', etc.)
1113
if result_type_local not in result_types_local:
1214
result_type = "album"
1315
else:
14-
result_type = result_types[result_types_local.index(result_type_local)]
16+
result_type = UNIQUE_RESULT_TYPES[result_types_local.index(result_type_local)]
1517

1618
return result_type
1719

@@ -39,7 +41,7 @@ def parse_top_result(data, search_result_types):
3941

4042
search_result["title"] = nav(data, TITLE_TEXT)
4143
runs = nav(data, ["subtitle", "runs"])
42-
song_info = parse_song_runs(runs)
44+
song_info = parse_song_runs(runs[2:])
4345
search_result.update(song_info)
4446

4547
if result_type in ["album"]:
@@ -58,14 +60,29 @@ def parse_search_result(data, search_result_types, result_type, category):
5860
default_offset = (not result_type or result_type == "album") * 2
5961
search_result = {"category": category}
6062
video_type = nav(data, [*PLAY_BUTTON, "playNavigationEndpoint", *NAVIGATION_VIDEO_TYPE], True)
61-
if not result_type and video_type:
62-
result_type = "song" if video_type == "MUSIC_VIDEO_TYPE_ATV" else "video"
63-
64-
result_type = (
65-
get_search_result_type(get_item_text(data, 1), search_result_types)
66-
if not result_type
67-
else result_type
68-
)
63+
64+
# try to determine the result type based on the first run
65+
if result_type not in ALL_RESULT_TYPES: # i.e. localized result_type
66+
result_type = get_search_result_type(get_item_text(data, 1), search_result_types)
67+
68+
# determine result type based on browseId
69+
# if there was no category title (i.e. for extra results in Top Result)
70+
if not result_type:
71+
if browse_id := nav(data, NAVIGATION_BROWSE_ID, True):
72+
mapping = {
73+
"VMPL": "playlist",
74+
"RD": "playlist",
75+
"MPLA": "artist",
76+
"MPRE": "album",
77+
"MPSP": "podcast",
78+
"MPED": "episode",
79+
}
80+
result_type = next(
81+
iter(type for prefix, type in mapping.items() if browse_id.startswith(prefix)), None
82+
)
83+
else:
84+
result_type = "song" if video_type == "MUSIC_VIDEO_TYPE_ATV" else "video"
85+
6986
search_result["resultType"] = result_type
7087

7188
if result_type != "artist":
@@ -134,7 +151,8 @@ def parse_search_result(data, search_result_types, result_type, category):
134151
search_result["year"] = None
135152
flex_item = get_flex_column_item(data, 1)
136153
runs = flex_item["text"]["runs"]
137-
song_info = parse_song_runs(runs)
154+
runs_offset = (len(runs[0]) == 1) * 2 # ignore the first run if it is a type specifier (like "Song")
155+
song_info = parse_song_runs(runs[runs_offset:])
138156
search_result.update(song_info)
139157

140158
if result_type in ["artist", "album", "playlist", "profile", "podcast"]:

0 commit comments

Comments
 (0)