From 2d52e047013a352f56c991a6e0799b52929d8f34 Mon Sep 17 00:00:00 2001 From: Corentin MARQUET Date: Wed, 13 Nov 2024 00:13:15 +0100 Subject: [PATCH 1/5] search: adds functionality to remove search history suggestion --- tests/mixins/test_search.py | 24 +++++++++++++++ ytmusicapi/mixins/search.py | 59 +++++++++++++++++++++++++++++++++--- ytmusicapi/parsers/search.py | 19 ++++++++++-- 3 files changed, 96 insertions(+), 6 deletions(-) diff --git a/tests/mixins/test_search.py b/tests/mixins/test_search.py index 4d5c1808..77676916 100644 --- a/tests/mixins/test_search.py +++ b/tests/mixins/test_search.py @@ -115,3 +115,27 @@ def test_search_library(self, config, yt_oauth): yt_oauth.search("beatles", filter="community_playlists", scope="library", limit=40) with pytest.raises(Exception): yt_oauth.search("beatles", filter="featured_playlists", scope="library", limit=40) + + def test_remove_suggestion_valid(self, yt_auth): + first_pass = yt_auth.search("b") + assert len(first_pass) > 0, "Search returned no results" + + results = yt_auth.get_search_suggestions("b", detailed_runs=True) + assert len(results) > 0, "No search suggestions returned" + assert any(item.get("fromHistory") for item in results), "No suggestions from history found" + + suggestion_to_remove = 1 + response = yt_auth.remove_search_suggestion(suggestion_to_remove) + assert response is True, "Failed to remove search suggestion" + + def test_remove_suggestion_invalid_number(self, yt_auth): + first_pass = yt_auth.search("a") + assert len(first_pass) > 0, "Search returned no results" + + results = yt_auth.get_search_suggestions("a", detailed_runs=True) + assert len(results) > 0, "No search suggestions returned" + assert any(item.get("fromHistory") for item in results), "No suggestions from history found" + + suggestion_to_remove = 99 + response = yt_auth.remove_search_suggestion(suggestion_to_remove) + assert response is False \ No newline at end of file diff --git a/ytmusicapi/mixins/search.py b/ytmusicapi/mixins/search.py index 96074616..7b940ce2 100644 --- a/ytmusicapi/mixins/search.py +++ b/ytmusicapi/mixins/search.py @@ -7,6 +7,10 @@ class SearchMixin(MixinProtocol): + def __init__(self): + self._latest_suggestions = None + self._latest_feedback_tokens = None + def search( self, query: str, @@ -295,7 +299,8 @@ def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[ { "text": "d" } - ] + ], + "number": 1 }, { "text": "faded alan walker lyrics", @@ -307,7 +312,8 @@ def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[ { "text": "d alan walker lyrics" } - ] + ], + "number": 2 }, { "text": "faded alan walker", @@ -319,7 +325,8 @@ def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[ { "text": "d alan walker" } - ] + ], + "number": 3 }, ... ] @@ -329,6 +336,50 @@ def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[ endpoint = "music/get_search_suggestions" response = self._send_request(endpoint, body) - search_suggestions = parse_search_suggestions(response, detailed_runs) + # Pass feedback_tokens as a dictionary to store tokens for deletion + feedback_tokens: dict[int, str] = {} + search_suggestions = parse_search_suggestions(response, detailed_runs, feedback_tokens) + + # Store the suggestions and feedback tokens for later use + self._latest_suggestions = search_suggestions + self._latest_feedback_tokens = feedback_tokens return search_suggestions + + def remove_search_suggestion(self, number: int) -> bool: + """ + Remove a search suggestion from the user search history based on the number displayed next to it. + + :param number: The number of the suggestion to be removed. + This number is displayed when the `detailed_runs` and `display_numbers` parameters are set to True + in the `get_search_suggestions` method. + :return: True if the operation was successful, False otherwise. + + Example usage: + + # Assuming you want to remove suggestion number 1 + success = ytmusic.remove_search_suggestion(number=1) + if success: + print("Suggestion removed successfully") + else: + print("Failed to remove suggestion") + """ + if self._latest_suggestions is None or self._latest_feedback_tokens is None: + raise ValueError( + "No suggestions available. Please run get_search_suggestions first to retrieve suggestions." + ) + + feedback_token = self._latest_feedback_tokens.get(number) + + if not feedback_token: + return False + + body = {"feedbackTokens": [feedback_token]} + endpoint = "feedback" + + response = self._send_request(endpoint, body) + + if "feedbackResponses" in response and response["feedbackResponses"][0].get("isProcessed", False): + return True + + return False diff --git a/ytmusicapi/parsers/search.py b/ytmusicapi/parsers/search.py index a8b87078..3a79d776 100644 --- a/ytmusicapi/parsers/search.py +++ b/ytmusicapi/parsers/search.py @@ -4,6 +4,7 @@ UNIQUE_RESULT_TYPES = ["artist", "playlist", "song", "video", "station", "profile", "podcast", "episode"] ALL_RESULT_TYPES = ["album", *UNIQUE_RESULT_TYPES] +FEEDBACK_TOKENS: dict[int, str] = {} def get_search_result_type(result_type_local, result_types_local): @@ -257,17 +258,25 @@ def _get_param2(filter): return filter_params[filter] -def parse_search_suggestions(results, detailed_runs): +def parse_search_suggestions(results, detailed_runs, feedback_tokens): if not results.get("contents", [{}])[0].get("searchSuggestionsSectionRenderer", {}).get("contents", []): return [] raw_suggestions = results["contents"][0]["searchSuggestionsSectionRenderer"]["contents"] suggestions = [] + count = 1 # Used for deleting a search suggestion for raw_suggestion in raw_suggestions: if "historySuggestionRenderer" in raw_suggestion: suggestion_content = raw_suggestion["historySuggestionRenderer"] from_history = True + feedback_token = ( + suggestion_content.get("serviceEndpoint", {}).get("feedbackEndpoint", {}).get("feedbackToken") + ) # Extract feedbackToken if present + + # Store the feedback token in the provided dictionary if it exists + if feedback_token: + feedback_tokens[count] = feedback_token else: suggestion_content = raw_suggestion["searchSuggestionRenderer"] from_history = False @@ -276,8 +285,14 @@ def parse_search_suggestions(results, detailed_runs): runs = suggestion_content["suggestion"]["runs"] if detailed_runs: - suggestions.append({"text": text, "runs": runs, "fromHistory": from_history}) + suggestions.append({"text": text, "runs": runs, "fromHistory": from_history, "number": count}) else: suggestions.append(text) + count += 1 + return suggestions + + +def get_feedback_token(suggestion_number): + return FEEDBACK_TOKENS.get(suggestion_number) From 1da196748f94b193c4628a7dd27c63c5b0f90b5e Mon Sep 17 00:00:00 2001 From: Corentin MARQUET Date: Sat, 16 Nov 2024 14:34:55 +0100 Subject: [PATCH 2/5] Minor improvements for remove search suggestions --- tests/mixins/test_search.py | 12 ++++----- ytmusicapi/mixins/search.py | 51 ++++++++++++++++-------------------- ytmusicapi/parsers/search.py | 14 +++------- 3 files changed, 31 insertions(+), 46 deletions(-) diff --git a/tests/mixins/test_search.py b/tests/mixins/test_search.py index 77676916..9a8bf4ec 100644 --- a/tests/mixins/test_search.py +++ b/tests/mixins/test_search.py @@ -117,25 +117,25 @@ def test_search_library(self, config, yt_oauth): yt_oauth.search("beatles", filter="featured_playlists", scope="library", limit=40) def test_remove_suggestion_valid(self, yt_auth): - first_pass = yt_auth.search("b") + first_pass = yt_auth.search("b") # Populate the suggestion history assert len(first_pass) > 0, "Search returned no results" - results = yt_auth.get_search_suggestions("b", detailed_runs=True) + results, feedback_tokens = yt_auth.get_search_suggestions("b", detailed_runs=True) assert len(results) > 0, "No search suggestions returned" assert any(item.get("fromHistory") for item in results), "No suggestions from history found" - suggestion_to_remove = 1 - response = yt_auth.remove_search_suggestion(suggestion_to_remove) + suggestion_to_remove = 0 + response = yt_auth.remove_search_suggestion(suggestion_to_remove, feedback_tokens) assert response is True, "Failed to remove search suggestion" def test_remove_suggestion_invalid_number(self, yt_auth): first_pass = yt_auth.search("a") assert len(first_pass) > 0, "Search returned no results" - results = yt_auth.get_search_suggestions("a", detailed_runs=True) + results, feedback_tokens = yt_auth.get_search_suggestions("a", detailed_runs=True) assert len(results) > 0, "No search suggestions returned" assert any(item.get("fromHistory") for item in results), "No suggestions from history found" suggestion_to_remove = 99 - response = yt_auth.remove_search_suggestion(suggestion_to_remove) + response = yt_auth.remove_search_suggestion(suggestion_to_remove, feedback_tokens) assert response is False \ No newline at end of file diff --git a/ytmusicapi/mixins/search.py b/ytmusicapi/mixins/search.py index 7b940ce2..8ff5d51a 100644 --- a/ytmusicapi/mixins/search.py +++ b/ytmusicapi/mixins/search.py @@ -7,10 +7,6 @@ class SearchMixin(MixinProtocol): - def __init__(self): - self._latest_suggestions = None - self._latest_feedback_tokens = None - def search( self, query: str, @@ -262,7 +258,7 @@ def parse_func(contents): return search_results - def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[str], list[dict]]: + def get_search_suggestions(self, query: str, detailed_runs=False) -> tuple[Union[list[str], list[dict]], dict[int, str]]: """ Get Search Suggestions @@ -272,7 +268,10 @@ def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[ suggestion along with the complete text (like many search services usually bold the text typed by the user). Default: False, returns the list of search suggestions in plain text. - :return: List of search suggestion results depending on ``detailed_runs`` param. + :return: A tuple containing: + - A list of search suggestions. If ``detailed_runs`` is False, it returns plain text suggestions. + If ``detailed_runs`` is True, it returns a list of dictionaries with detailed information. + - A dictionary of feedback tokens that can be used to remove suggestions later. Example response when ``query`` is 'fade' and ``detailed_runs`` is set to ``False``:: @@ -300,7 +299,7 @@ def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[ "text": "d" } ], - "number": 1 + "index": 0 }, { "text": "faded alan walker lyrics", @@ -313,7 +312,7 @@ def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[ "text": "d alan walker lyrics" } ], - "number": 2 + "index": 1 }, { "text": "faded alan walker", @@ -326,7 +325,7 @@ def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[ "text": "d alan walker" } ], - "number": 3 + "index": 2 }, ... ] @@ -340,36 +339,33 @@ def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[ feedback_tokens: dict[int, str] = {} search_suggestions = parse_search_suggestions(response, detailed_runs, feedback_tokens) - # Store the suggestions and feedback tokens for later use - self._latest_suggestions = search_suggestions - self._latest_feedback_tokens = feedback_tokens - - return search_suggestions + return search_suggestions, feedback_tokens - def remove_search_suggestion(self, number: int) -> bool: + def remove_search_suggestion(self, index: int, feedback_tokens: dict[int, str]) -> bool: """ Remove a search suggestion from the user search history based on the number displayed next to it. - :param number: The number of the suggestion to be removed. + :param index: The number of the suggestion to be removed. This number is displayed when the `detailed_runs` and `display_numbers` parameters are set to True in the `get_search_suggestions` method. + :param feedback_tokens: A dictionary containing feedback tokens for each suggestion. + This dictionary is obtained from the `get_search_suggestions` method. :return: True if the operation was successful, False otherwise. - Example usage: + Example usage:: - # Assuming you want to remove suggestion number 1 - success = ytmusic.remove_search_suggestion(number=1) + # Assuming you want to remove suggestion number 0 + suggestions, feedback_tokens = ytmusic.get_search_suggestions(query="fade", detailed_runs=True) + success = ytmusic.remove_search_suggestion(index=0, feedback_tokens=feedback_tokens) if success: print("Suggestion removed successfully") else: print("Failed to remove suggestion") """ - if self._latest_suggestions is None or self._latest_feedback_tokens is None: - raise ValueError( - "No suggestions available. Please run get_search_suggestions first to retrieve suggestions." - ) - - feedback_token = self._latest_feedback_tokens.get(number) + if not feedback_tokens: + raise YTMusicUserError("No feedback tokens provided. Please run get_search_suggestions first to retrieve suggestions.") + + feedback_token = feedback_tokens.get(index) if not feedback_token: return False @@ -379,7 +375,4 @@ def remove_search_suggestion(self, number: int) -> bool: response = self._send_request(endpoint, body) - if "feedbackResponses" in response and response["feedbackResponses"][0].get("isProcessed", False): - return True - - return False + return bool(nav(response, ["feedbackResponses", 0, "isProcessed"], none_if_absent=True)) diff --git a/ytmusicapi/parsers/search.py b/ytmusicapi/parsers/search.py index 3a79d776..14eaf879 100644 --- a/ytmusicapi/parsers/search.py +++ b/ytmusicapi/parsers/search.py @@ -4,7 +4,6 @@ UNIQUE_RESULT_TYPES = ["artist", "playlist", "song", "video", "station", "profile", "podcast", "episode"] ALL_RESULT_TYPES = ["album", *UNIQUE_RESULT_TYPES] -FEEDBACK_TOKENS: dict[int, str] = {} def get_search_result_type(result_type_local, result_types_local): @@ -265,8 +264,7 @@ def parse_search_suggestions(results, detailed_runs, feedback_tokens): raw_suggestions = results["contents"][0]["searchSuggestionsSectionRenderer"]["contents"] suggestions = [] - count = 1 # Used for deleting a search suggestion - for raw_suggestion in raw_suggestions: + for index, raw_suggestion in enumerate(raw_suggestions): if "historySuggestionRenderer" in raw_suggestion: suggestion_content = raw_suggestion["historySuggestionRenderer"] from_history = True @@ -276,7 +274,7 @@ def parse_search_suggestions(results, detailed_runs, feedback_tokens): # Store the feedback token in the provided dictionary if it exists if feedback_token: - feedback_tokens[count] = feedback_token + feedback_tokens[index] = feedback_token else: suggestion_content = raw_suggestion["searchSuggestionRenderer"] from_history = False @@ -285,14 +283,8 @@ def parse_search_suggestions(results, detailed_runs, feedback_tokens): runs = suggestion_content["suggestion"]["runs"] if detailed_runs: - suggestions.append({"text": text, "runs": runs, "fromHistory": from_history, "number": count}) + suggestions.append({"text": text, "runs": runs, "fromHistory": from_history, "index": index}) else: suggestions.append(text) - count += 1 - return suggestions - - -def get_feedback_token(suggestion_number): - return FEEDBACK_TOKENS.get(suggestion_number) From 808077ba256b006e52b091e3b77bd5aeab00a82e Mon Sep 17 00:00:00 2001 From: sigma67 Date: Tue, 17 Dec 2024 18:45:47 +0100 Subject: [PATCH 3/5] fix lint --- tests/mixins/test_search.py | 6 +++--- ytmusicapi/__init__.py | 2 +- ytmusicapi/mixins/search.py | 10 +++++++--- ytmusicapi/parsers/search.py | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/mixins/test_search.py b/tests/mixins/test_search.py index 9a8bf4ec..721d28ba 100644 --- a/tests/mixins/test_search.py +++ b/tests/mixins/test_search.py @@ -117,7 +117,7 @@ def test_search_library(self, config, yt_oauth): yt_oauth.search("beatles", filter="featured_playlists", scope="library", limit=40) def test_remove_suggestion_valid(self, yt_auth): - first_pass = yt_auth.search("b") # Populate the suggestion history + first_pass = yt_auth.search("b") # Populate the suggestion history assert len(first_pass) > 0, "Search returned no results" results, feedback_tokens = yt_auth.get_search_suggestions("b", detailed_runs=True) @@ -131,11 +131,11 @@ def test_remove_suggestion_valid(self, yt_auth): def test_remove_suggestion_invalid_number(self, yt_auth): first_pass = yt_auth.search("a") assert len(first_pass) > 0, "Search returned no results" - + results, feedback_tokens = yt_auth.get_search_suggestions("a", detailed_runs=True) assert len(results) > 0, "No search suggestions returned" assert any(item.get("fromHistory") for item in results), "No suggestions from history found" suggestion_to_remove = 99 response = yt_auth.remove_search_suggestion(suggestion_to_remove, feedback_tokens) - assert response is False \ No newline at end of file + assert response is False diff --git a/ytmusicapi/__init__.py b/ytmusicapi/__init__.py index 4f3ce510..4ed851d0 100644 --- a/ytmusicapi/__init__.py +++ b/ytmusicapi/__init__.py @@ -12,4 +12,4 @@ __copyright__ = "Copyright 2023 sigma67" __license__ = "MIT" __title__ = "ytmusicapi" -__all__ = ["YTMusic", "setup_oauth", "setup"] +__all__ = ["YTMusic", "setup", "setup_oauth"] diff --git a/ytmusicapi/mixins/search.py b/ytmusicapi/mixins/search.py index 8ff5d51a..798fe2c2 100644 --- a/ytmusicapi/mixins/search.py +++ b/ytmusicapi/mixins/search.py @@ -258,7 +258,9 @@ def parse_func(contents): return search_results - def get_search_suggestions(self, query: str, detailed_runs=False) -> tuple[Union[list[str], list[dict]], dict[int, str]]: + def get_search_suggestions( + self, query: str, detailed_runs=False + ) -> tuple[Union[list[str], list[dict]], dict[int, str]]: """ Get Search Suggestions @@ -363,8 +365,10 @@ def remove_search_suggestion(self, index: int, feedback_tokens: dict[int, str]) print("Failed to remove suggestion") """ if not feedback_tokens: - raise YTMusicUserError("No feedback tokens provided. Please run get_search_suggestions first to retrieve suggestions.") - + raise YTMusicUserError( + "No feedback tokens provided. Please run get_search_suggestions first to retrieve suggestions." + ) + feedback_token = feedback_tokens.get(index) if not feedback_token: diff --git a/ytmusicapi/parsers/search.py b/ytmusicapi/parsers/search.py index 14eaf879..db79cdef 100644 --- a/ytmusicapi/parsers/search.py +++ b/ytmusicapi/parsers/search.py @@ -271,7 +271,7 @@ def parse_search_suggestions(results, detailed_runs, feedback_tokens): feedback_token = ( suggestion_content.get("serviceEndpoint", {}).get("feedbackEndpoint", {}).get("feedbackToken") ) # Extract feedbackToken if present - + # Store the feedback token in the provided dictionary if it exists if feedback_token: feedback_tokens[index] = feedback_token From aa3a41f1d6e72458da73914b05bd285f14206438 Mon Sep 17 00:00:00 2001 From: sigma67 Date: Tue, 17 Dec 2024 19:42:26 +0100 Subject: [PATCH 4/5] some improvements: remove_search_suggestions --- docs/source/reference.rst | 2 ++ tests/mixins/test_search.py | 24 ++++++++++------ ytmusicapi/mixins/search.py | 54 ++++++++++++++++++------------------ ytmusicapi/parsers/search.py | 29 +++++++++++-------- 4 files changed, 62 insertions(+), 47 deletions(-) diff --git a/docs/source/reference.rst b/docs/source/reference.rst index 8623d398..d77018b9 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -19,6 +19,8 @@ Search ------ .. automethod:: YTMusic.search .. automethod:: YTMusic.get_search_suggestions +.. automethod:: YTMusic.remove_search_suggestions + Browsing -------- diff --git a/tests/mixins/test_search.py b/tests/mixins/test_search.py index 721d28ba..7ed4285a 100644 --- a/tests/mixins/test_search.py +++ b/tests/mixins/test_search.py @@ -1,6 +1,7 @@ import pytest from ytmusicapi import YTMusic +from ytmusicapi.exceptions import YTMusicUserError from ytmusicapi.parsers.search import ALL_RESULT_TYPES @@ -116,26 +117,31 @@ def test_search_library(self, config, yt_oauth): with pytest.raises(Exception): yt_oauth.search("beatles", filter="featured_playlists", scope="library", limit=40) - def test_remove_suggestion_valid(self, yt_auth): + def test_remove_search_suggestions_valid(self, yt_auth): first_pass = yt_auth.search("b") # Populate the suggestion history assert len(first_pass) > 0, "Search returned no results" - results, feedback_tokens = yt_auth.get_search_suggestions("b", detailed_runs=True) + results = yt_auth.get_search_suggestions("b", detailed_runs=True) assert len(results) > 0, "No search suggestions returned" assert any(item.get("fromHistory") for item in results), "No suggestions from history found" - suggestion_to_remove = 0 - response = yt_auth.remove_search_suggestion(suggestion_to_remove, feedback_tokens) + suggestion_to_remove = [0] + response = yt_auth.remove_search_suggestions(results, suggestion_to_remove) assert response is True, "Failed to remove search suggestion" - def test_remove_suggestion_invalid_number(self, yt_auth): + def test_remove_search_suggestions_errors(self, yt_auth, yt): first_pass = yt_auth.search("a") assert len(first_pass) > 0, "Search returned no results" - results, feedback_tokens = yt_auth.get_search_suggestions("a", detailed_runs=True) + results = yt_auth.get_search_suggestions("a", detailed_runs=True) assert len(results) > 0, "No search suggestions returned" assert any(item.get("fromHistory") for item in results), "No suggestions from history found" - suggestion_to_remove = 99 - response = yt_auth.remove_search_suggestion(suggestion_to_remove, feedback_tokens) - assert response is False + suggestion_to_remove = [99] + with pytest.raises(YTMusicUserError, match="Index out of range."): + yt_auth.remove_search_suggestions(results, suggestion_to_remove) + + suggestion_to_remove = [0] + with pytest.raises(YTMusicUserError, match="No search result from history provided."): + results = yt.get_search_suggestions("a", detailed_runs=True) + yt.remove_search_suggestions(results, suggestion_to_remove) diff --git a/ytmusicapi/mixins/search.py b/ytmusicapi/mixins/search.py index 798fe2c2..d1166ce0 100644 --- a/ytmusicapi/mixins/search.py +++ b/ytmusicapi/mixins/search.py @@ -258,9 +258,7 @@ def parse_func(contents): return search_results - def get_search_suggestions( - self, query: str, detailed_runs=False - ) -> tuple[Union[list[str], list[dict]], dict[int, str]]: + def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[str], list[dict]]: """ Get Search Suggestions @@ -270,10 +268,8 @@ def get_search_suggestions( suggestion along with the complete text (like many search services usually bold the text typed by the user). Default: False, returns the list of search suggestions in plain text. - :return: A tuple containing: - - A list of search suggestions. If ``detailed_runs`` is False, it returns plain text suggestions. + :return: A list of search suggestions. If ``detailed_runs`` is False, it returns plain text suggestions. If ``detailed_runs`` is True, it returns a list of dictionaries with detailed information. - - A dictionary of feedback tokens that can be used to remove suggestions later. Example response when ``query`` is 'fade' and ``detailed_runs`` is set to ``False``:: @@ -301,7 +297,8 @@ def get_search_suggestions( "text": "d" } ], - "index": 0 + "fromHistory": true, + "feedbackToken": "AEEJK..." }, { "text": "faded alan walker lyrics", @@ -314,7 +311,8 @@ def get_search_suggestions( "text": "d alan walker lyrics" } ], - "index": 1 + "fromHistory": true, + "feedbackToken": None }, { "text": "faded alan walker", @@ -327,54 +325,56 @@ def get_search_suggestions( "text": "d alan walker" } ], - "index": 2 + "fromHistory": false, + "feedbackToken": None }, ... ] """ - body = {"input": query} endpoint = "music/get_search_suggestions" response = self._send_request(endpoint, body) - # Pass feedback_tokens as a dictionary to store tokens for deletion - feedback_tokens: dict[int, str] = {} - search_suggestions = parse_search_suggestions(response, detailed_runs, feedback_tokens) - return search_suggestions, feedback_tokens + return parse_search_suggestions(response, detailed_runs) - def remove_search_suggestion(self, index: int, feedback_tokens: dict[int, str]) -> bool: + def remove_search_suggestions(self, suggestions: list[dict[str, Any]], indices: list[int]) -> bool: """ Remove a search suggestion from the user search history based on the number displayed next to it. - :param index: The number of the suggestion to be removed. - This number is displayed when the `detailed_runs` and `display_numbers` parameters are set to True - in the `get_search_suggestions` method. - :param feedback_tokens: A dictionary containing feedback tokens for each suggestion. - This dictionary is obtained from the `get_search_suggestions` method. + :param suggestions: The dictionary obtained from the :py:func:`get_search_suggestions` + (with detailed_runs=True)` + :param indices: The indices of the suggestions to be removed. :return: True if the operation was successful, False otherwise. Example usage:: # Assuming you want to remove suggestion number 0 - suggestions, feedback_tokens = ytmusic.get_search_suggestions(query="fade", detailed_runs=True) - success = ytmusic.remove_search_suggestion(index=0, feedback_tokens=feedback_tokens) + suggestions = ytmusic.get_search_suggestions(query="fade", detailed_runs=True) + success = ytmusic.remove_search_suggestion(suggestions=suggestions, index=0) if success: print("Suggestion removed successfully") else: print("Failed to remove suggestion") """ - if not feedback_tokens: + if not any(run["fromHistory"] for run in suggestions): raise YTMusicUserError( - "No feedback tokens provided. Please run get_search_suggestions first to retrieve suggestions." + "No search result from history provided. " + "Please run get_search_suggestions first to retrieve suggestions. " + "Ensure that you have searched a similar term before." ) - feedback_token = feedback_tokens.get(index) + if any(index >= len(suggestions) for index in indices): + raise YTMusicUserError("Index out of range. Index must be smaller than the length of suggestions") - if not feedback_token: + feedback_tokens = [suggestions[index]["feedbackToken"] for index in indices] + if all(feedback_token is None for feedback_token in feedback_tokens): return False - body = {"feedbackTokens": [feedback_token]} + # filter None tokens + feedback_tokens = [token for token in feedback_tokens if token is not None] + + body = {"feedbackTokens": feedback_tokens} endpoint = "feedback" response = self._send_request(endpoint, body) diff --git a/ytmusicapi/parsers/search.py b/ytmusicapi/parsers/search.py index db79cdef..df62eb1b 100644 --- a/ytmusicapi/parsers/search.py +++ b/ytmusicapi/parsers/search.py @@ -1,3 +1,5 @@ +from typing import Union + from ..helpers import to_int from ._utils import * from .songs import * @@ -257,7 +259,9 @@ def _get_param2(filter): return filter_params[filter] -def parse_search_suggestions(results, detailed_runs, feedback_tokens): +def parse_search_suggestions( + results: dict[str, Any], detailed_runs: bool +) -> Union[list[str], list[dict[str, Any]]]: if not results.get("contents", [{}])[0].get("searchSuggestionsSectionRenderer", {}).get("contents", []): return [] @@ -265,25 +269,28 @@ def parse_search_suggestions(results, detailed_runs, feedback_tokens): suggestions = [] for index, raw_suggestion in enumerate(raw_suggestions): + feedback_token = None if "historySuggestionRenderer" in raw_suggestion: suggestion_content = raw_suggestion["historySuggestionRenderer"] - from_history = True - feedback_token = ( - suggestion_content.get("serviceEndpoint", {}).get("feedbackEndpoint", {}).get("feedbackToken") - ) # Extract feedbackToken if present - - # Store the feedback token in the provided dictionary if it exists - if feedback_token: - feedback_tokens[index] = feedback_token + # Extract feedbackToken if present + feedback_token = nav( + suggestion_content, ["serviceEndpoint", "feedbackEndpoint", "feedbackToken"], True + ) else: suggestion_content = raw_suggestion["searchSuggestionRenderer"] - from_history = False text = suggestion_content["navigationEndpoint"]["searchEndpoint"]["query"] runs = suggestion_content["suggestion"]["runs"] if detailed_runs: - suggestions.append({"text": text, "runs": runs, "fromHistory": from_history, "index": index}) + suggestions.append( + { + "text": text, + "runs": runs, + "fromHistory": feedback_token is not None, + "feedbackToken": feedback_token, + } + ) else: suggestions.append(text) From 4801d3a1fa6a63d28b397325160758228b19d44e Mon Sep 17 00:00:00 2001 From: sigma67 Date: Tue, 17 Dec 2024 19:52:35 +0100 Subject: [PATCH 5/5] make indices optional --- tests/mixins/test_search.py | 5 ++--- ytmusicapi/mixins/search.py | 17 +++++++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/mixins/test_search.py b/tests/mixins/test_search.py index 7ed4285a..c52d2045 100644 --- a/tests/mixins/test_search.py +++ b/tests/mixins/test_search.py @@ -125,9 +125,8 @@ def test_remove_search_suggestions_valid(self, yt_auth): assert len(results) > 0, "No search suggestions returned" assert any(item.get("fromHistory") for item in results), "No suggestions from history found" - suggestion_to_remove = [0] - response = yt_auth.remove_search_suggestions(results, suggestion_to_remove) - assert response is True, "Failed to remove search suggestion" + response = yt_auth.remove_search_suggestions(results) + assert response is True, "Failed to remove search suggestions" def test_remove_search_suggestions_errors(self, yt_auth, yt): first_pass = yt_auth.search("a") diff --git a/ytmusicapi/mixins/search.py b/ytmusicapi/mixins/search.py index d1166ce0..08399e12 100644 --- a/ytmusicapi/mixins/search.py +++ b/ytmusicapi/mixins/search.py @@ -311,7 +311,7 @@ def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[ "text": "d alan walker lyrics" } ], - "fromHistory": true, + "fromHistory": false, "feedbackToken": None }, { @@ -338,20 +338,22 @@ def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[ return parse_search_suggestions(response, detailed_runs) - def remove_search_suggestions(self, suggestions: list[dict[str, Any]], indices: list[int]) -> bool: + def remove_search_suggestions( + self, suggestions: list[dict[str, Any]], indices: Optional[list[int]] = None + ) -> bool: """ - Remove a search suggestion from the user search history based on the number displayed next to it. + Remove search suggestion from the user search history. :param suggestions: The dictionary obtained from the :py:func:`get_search_suggestions` (with detailed_runs=True)` - :param indices: The indices of the suggestions to be removed. + :param indices: Optional. The indices of the suggestions to be removed. Default: remove all suggestions. :return: True if the operation was successful, False otherwise. Example usage:: - # Assuming you want to remove suggestion number 0 + # Removing suggestion number 0 suggestions = ytmusic.get_search_suggestions(query="fade", detailed_runs=True) - success = ytmusic.remove_search_suggestion(suggestions=suggestions, index=0) + success = ytmusic.remove_search_suggestions(suggestions=suggestions, indices=[0]) if success: print("Suggestion removed successfully") else: @@ -364,6 +366,9 @@ def remove_search_suggestions(self, suggestions: list[dict[str, Any]], indices: "Ensure that you have searched a similar term before." ) + if indices is None: + indices = list(range(len(suggestions))) + if any(index >= len(suggestions) for index in indices): raise YTMusicUserError("Index out of range. Index must be smaller than the length of suggestions")