Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

search: adds functionality to remove search suggestion #678

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions tests/mixins/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
sigma67 marked this conversation as resolved.
Show resolved Hide resolved

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
59 changes: 55 additions & 4 deletions ytmusicapi/mixins/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@


class SearchMixin(MixinProtocol):
def __init__(self):
self._latest_suggestions = None
self._latest_feedback_tokens = None
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Despite being an instance, we don't store request-specific state in the YTMusic instance between function calls.

The only state in YTMusic relates to authentication/configuration.

Please move the variables into function parameters.


def search(
self,
query: str,
Expand Down Expand Up @@ -295,7 +299,8 @@ def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[
{
"text": "d"
}
]
],
"number": 1
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer index, starting at 0

},
{
"text": "faded alan walker lyrics",
Expand All @@ -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",
Expand All @@ -319,7 +325,8 @@ def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[
{
"text": "d alan walker"
}
]
],
"number": 3
},
...
]
Expand All @@ -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:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, please rename number to index

"""
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(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please raise YTMusicError or YTMusicUserError as you see fit

"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
sigma67 marked this conversation as resolved.
Show resolved Hide resolved
19 changes: 17 additions & 2 deletions ytmusicapi/parsers/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {}
sigma67 marked this conversation as resolved.
Show resolved Hide resolved


def get_search_result_type(result_type_local, result_types_local):
Expand Down Expand Up @@ -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:
sigma67 marked this conversation as resolved.
Show resolved Hide resolved
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
Expand All @@ -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):
sigma67 marked this conversation as resolved.
Show resolved Hide resolved
return FEEDBACK_TOKENS.get(suggestion_number)
Loading