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

Merged
merged 6 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions docs/source/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Search
------
.. automethod:: YTMusic.search
.. automethod:: YTMusic.get_search_suggestions
.. automethod:: YTMusic.remove_search_suggestions


Browsing
--------
Expand Down
29 changes: 29 additions & 0 deletions tests/mixins/test_search.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest

from ytmusicapi import YTMusic
from ytmusicapi.exceptions import YTMusicUserError
from ytmusicapi.parsers.search import ALL_RESULT_TYPES


Expand Down Expand Up @@ -115,3 +116,31 @@ 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_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"
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"

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")
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]
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)
67 changes: 60 additions & 7 deletions ytmusicapi/mixins/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,8 @@ 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 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.

Example response when ``query`` is 'fade' and ``detailed_runs`` is set to ``False``::

Expand All @@ -295,7 +296,9 @@ def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[
{
"text": "d"
}
]
],
"fromHistory": true,
"feedbackToken": "AEEJK..."
},
{
"text": "faded alan walker lyrics",
Expand All @@ -307,7 +310,9 @@ def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[
{
"text": "d alan walker lyrics"
}
]
],
"fromHistory": false,
"feedbackToken": None
},
{
"text": "faded alan walker",
Expand All @@ -319,16 +324,64 @@ def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[
{
"text": "d alan walker"
}
]
],
"fromHistory": false,
"feedbackToken": None
},
...
]
"""

body = {"input": query}
endpoint = "music/get_search_suggestions"

response = self._send_request(endpoint, body)
search_suggestions = parse_search_suggestions(response, detailed_runs)

return search_suggestions
return parse_search_suggestions(response, detailed_runs)

def remove_search_suggestions(
self, suggestions: list[dict[str, Any]], indices: Optional[list[int]] = None
) -> bool:
"""
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: Optional. The indices of the suggestions to be removed. Default: remove all suggestions.
:return: True if the operation was successful, False otherwise.

Example usage::

# Removing suggestion number 0
suggestions = ytmusic.get_search_suggestions(query="fade", detailed_runs=True)
success = ytmusic.remove_search_suggestions(suggestions=suggestions, indices=[0])
if success:
print("Suggestion removed successfully")
else:
print("Failed to remove suggestion")
"""
if not any(run["fromHistory"] for run in suggestions):
raise YTMusicUserError(
"No search result from history provided. "
"Please run get_search_suggestions first to retrieve suggestions. "
"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")

feedback_tokens = [suggestions[index]["feedbackToken"] for index in indices]
if all(feedback_token is None for feedback_token in feedback_tokens):
return False

# 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)

return bool(nav(response, ["feedbackResponses", 0, "isProcessed"], none_if_absent=True))
24 changes: 19 additions & 5 deletions ytmusicapi/parsers/search.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Union

from ..helpers import to_int
from ._utils import *
from .songs import *
Expand Down Expand Up @@ -257,26 +259,38 @@ def _get_param2(filter):
return filter_params[filter]


def parse_search_suggestions(results, detailed_runs):
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 []

raw_suggestions = results["contents"][0]["searchSuggestionsSectionRenderer"]["contents"]
suggestions = []

for raw_suggestion in raw_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
# 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})
suggestions.append(
{
"text": text,
"runs": runs,
"fromHistory": feedback_token is not None,
"feedbackToken": feedback_token,
}
)
else:
suggestions.append(text)

Expand Down
Loading