From 3628e59a2057004f268ab1262f7be993c98fc83e Mon Sep 17 00:00:00 2001 From: Marvin Schenkel Date: Fri, 21 Aug 2026 11:17:06 +0200 Subject: [PATCH 1/2] Parse isAvailable for song/video search results --- tests/parsers/test_search.py | 39 ++++++++++++++++++++++++++++++++++++ ytmusicapi/mixins/search.py | 9 ++++++--- ytmusicapi/parsers/search.py | 8 ++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 tests/parsers/test_search.py diff --git a/tests/parsers/test_search.py b/tests/parsers/test_search.py new file mode 100644 index 00000000..df8226d8 --- /dev/null +++ b/tests/parsers/test_search.py @@ -0,0 +1,39 @@ +from ytmusicapi.parsers.search import parse_search_result + + +def _flex_result(**extra: str) -> dict: + result = { + "flexColumns": [ + {"musicResponsiveListItemFlexColumnRenderer": {"text": {"runs": [{"text": "Song Title"}]}}}, + {"musicResponsiveListItemFlexColumnRenderer": {"text": {"runs": [{"text": "Artist"}]}}}, + ], + } + result.update(extra) + return result + + +class TestParseSearchResultIsAvailable: + def test_defaults_to_available(self): + parsed = parse_search_result(_flex_result(), "song", "Songs") + assert parsed["isAvailable"] is True + + def test_grey_out_policy_marks_unavailable(self): + parsed = parse_search_result( + _flex_result(musicItemRendererDisplayPolicy="MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT"), + "video", + "Videos", + ) + assert parsed["isAvailable"] is False + + def test_other_policy_values_remain_available(self): + parsed = parse_search_result( + _flex_result(musicItemRendererDisplayPolicy="MUSIC_ITEM_RENDERER_DISPLAY_POLICY_DEFAULT"), + "song", + "Songs", + ) + assert parsed["isAvailable"] is True + + def test_other_result_types_are_unaffected(self): + # isAvailable is only meaningful for playable song/video results + parsed = parse_search_result(_flex_result(), "album", "Albums") + assert "isAvailable" not in parsed diff --git a/ytmusicapi/mixins/search.py b/ytmusicapi/mixins/search.py index bd6ee3c3..c67800a9 100644 --- a/ytmusicapi/mixins/search.py +++ b/ytmusicapi/mixins/search.py @@ -73,7 +73,8 @@ def search( "views": "1.4M", "videoType": "MUSIC_VIDEO_TYPE_OMV", "duration": "4:38", - "duration_seconds": 278 + "duration_seconds": 278, + "isAvailable": true }, { "category": "Songs", @@ -91,7 +92,8 @@ def search( "id": "MPREb_9nqEki4ZDpp" }, "duration": "4:19", - "duration_seconds": 259 + "duration_seconds": 259, + "isAvailable": true, "isExplicit": false, "inLibrary": false, "feedbackTokens": { @@ -138,7 +140,8 @@ def search( ], "views": "386M", "duration": "4:38", - "duration_seconds": 278 + "duration_seconds": 278, + "isAvailable": true }, { "category": "Artists", diff --git a/ytmusicapi/parsers/search.py b/ytmusicapi/parsers/search.py index d31cb9f2..beeae5f5 100644 --- a/ytmusicapi/parsers/search.py +++ b/ytmusicapi/parsers/search.py @@ -182,6 +182,14 @@ def parse_search_result(data: JsonDict, result_type: str | None, category: str | ) search_result["videoType"] = video_type + if result_type in ["song", "video"]: + isAvailable = True + if "musicItemRendererDisplayPolicy" in data: + isAvailable = ( + data["musicItemRendererDisplayPolicy"] != "MUSIC_ITEM_RENDERER_DISPLAY_POLICY_GREY_OUT" + ) + search_result["isAvailable"] = isAvailable + if result_type in ["song", "video", "album"]: search_result["duration"] = None search_result["year"] = None From c258418feb581bd7dbaa86eaed3fa9717063ed96 Mon Sep 17 00:00:00 2001 From: Marvin Schenkel Date: Fri, 21 Aug 2026 11:29:50 +0200 Subject: [PATCH 2/2] Remove isAvailable from the top result docstring example --- ytmusicapi/mixins/search.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ytmusicapi/mixins/search.py b/ytmusicapi/mixins/search.py index c67800a9..657e2eb2 100644 --- a/ytmusicapi/mixins/search.py +++ b/ytmusicapi/mixins/search.py @@ -73,8 +73,7 @@ def search( "views": "1.4M", "videoType": "MUSIC_VIDEO_TYPE_OMV", "duration": "4:38", - "duration_seconds": 278, - "isAvailable": true + "duration_seconds": 278 }, { "category": "Songs",