Skip to content

Commit

Permalink
some improvements: remove_search_suggestions
Browse files Browse the repository at this point in the history
  • Loading branch information
sigma67 committed Dec 17, 2024
1 parent 9d65260 commit aa3a41f
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 47 deletions.
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
24 changes: 15 additions & 9 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 @@ -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)
54 changes: 27 additions & 27 deletions ytmusicapi/mixins/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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``::
Expand Down Expand Up @@ -301,7 +297,8 @@ def get_search_suggestions(
"text": "d"
}
],
"index": 0
"fromHistory": true,
"feedbackToken": "AEEJK..."
},
{
"text": "faded alan walker lyrics",
Expand All @@ -314,7 +311,8 @@ def get_search_suggestions(
"text": "d alan walker lyrics"
}
],
"index": 1
"fromHistory": true,
"feedbackToken": None
},
{
"text": "faded alan walker",
Expand All @@ -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)
Expand Down
29 changes: 18 additions & 11 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,33 +259,38 @@ 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 []

raw_suggestions = results["contents"][0]["searchSuggestionsSectionRenderer"]["contents"]
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)

Expand Down

0 comments on commit aa3a41f

Please sign in to comment.