diff --git a/rule34Py/api_urls.py b/rule34Py/api_urls.py index 5c43c64..2bfc7bb 100644 --- a/rule34Py/api_urls.py +++ b/rule34Py/api_urls.py @@ -23,7 +23,7 @@ __base_url__ = "https://rule34.xxx/" __api_url__ = "https://api.rule34.xxx/" - +__autocomplete_url__ = "https://ac.rule34.xxx/" class API_URLS(str, Enum): """rule34.xxx API endpoint URLs. @@ -48,3 +48,5 @@ class API_URLS(str, Enum): POOL = f"{__base_url__}index.php?page=pool&s=show&id={{POOL_ID}}" #: The HTML toptags URL. TOPMAP = f"{__base_url__}index.php?page=toptags" + #: The tags autocomplete URL + AUTOCOMPLETE = f"{__autocomplete_url__}autocomplete.php?q={{q}}" \ No newline at end of file diff --git a/rule34Py/autocomplete_tag.py b/rule34Py/autocomplete_tag.py new file mode 100644 index 0000000..7512b04 --- /dev/null +++ b/rule34Py/autocomplete_tag.py @@ -0,0 +1,36 @@ +# rule34Py - Python api wrapper for rule34.xxx +# +# Copyright (C) 2022-2025 b3yc0d3 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Provides the AutocompleteTag class used for tag suggestions from Rule34 autocomplete.""" + +from dataclasses import dataclass + +@dataclass +class AutocompleteTag: + """Represents a tag suggestion from autocomplete. + + Parameters: + label: The full tag label including count (e.g., "hooves (95430)"). + value: The clean tag value (e.g., "hooves"). + type: The tag category (e.g., "general", "copyright"). + """ + + #: The full tag label including count (e.g., "hooves (95430)"). + label: str + #: The clean tag value without count information. + value: str + #: The category of the tag (general/copyright/other). + type: str \ No newline at end of file diff --git a/rule34Py/rule34.py b/rule34Py/rule34.py index 5b4a015..c49f378 100644 --- a/rule34Py/rule34.py +++ b/rule34Py/rule34.py @@ -40,6 +40,7 @@ from rule34Py.post import Post from rule34Py.post_comment import PostComment from rule34Py.toptag import TopTag +from rule34Py.autocomplete_tag import AutocompleteTag PROJECT_VERSION = importlib.metadata.version(__package__) @@ -82,6 +83,40 @@ def __init__(self): self.session = requests.session() self.session.mount(__base_url__, self._base_site_rate_limiter) + def autocomplete(self, tag_string: str) -> list[AutocompleteTag]: + """Retrieve tag suggestions based on partial input. + + Args: + tag_string: Partial tag input to search suggestions for. + + Returns: + A list of AutocompleteTag objects matching the search query, + ordered by popularity (descending). + + Raises: + requests.HTTPError: The backing HTTP request failed. + ValueError: If the response contains invalid data structure. + """ # noqa: DOC502 + params = [["q", tag_string]] + formatted_url = self._parseUrlParams(API_URLS.AUTOCOMPLETE.value, params) + response = self._get(formatted_url, headers = { + "Referer": "https://rule34.xxx/", + "Origin": "https://rule34.xxx", + "Accept": "*/*" + }) + response.raise_for_status() + + raw_data = response.json() + results = [ + AutocompleteTag( + label=item["label"], + value=item["value"], + type=item["type"] + ) + for item in raw_data + ] + return results + def _get(self, *args, **kwargs) -> requests.Response: """Send an HTTP GET request. diff --git a/tests/fixtures/mock34/responses.yml b/tests/fixtures/mock34/responses.yml index 8d23c61..d52c76f 100644 --- a/tests/fixtures/mock34/responses.yml +++ b/tests/fixtures/mock34/responses.yml @@ -103558,3 +103558,36 @@ responses: method: GET status: 200 url: http://example.com/ +- response: + auto_calculate_content_length: false + body: '[{"label":"nekomimi (6720)","value":"nekomimi","type":"general"},{"label":"neko + (3872)","value":"neko","type":"general"},{"label":"nekomata_okayu (2326)","value":"nekomata_okayu","type":"character"},{"label":"nekomata + (1697)","value":"nekomata","type":"general"},{"label":"nekopara (1281)","value":"nekopara","type":"copyright"},{"label":"nekomusume + (664)","value":"nekomusume","type":"character"},{"label":"neko-me (662)","value":"neko-me","type":"artist"},{"label":"neko3240 + (549)","value":"neko3240","type":"artist"},{"label":"nekomata_(disgaea) (518)","value":"nekomata_(disgaea)","type":"character"},{"label":"nekojishi + (515)","value":"nekojishi","type":"copyright"}]' + content_type: text/plain + headers: + CF-RAY: 9642e2cf8fbfe84c-DME + Transfer-Encoding: chunked + access-control-allow-origin: '*' + alt-svc: h3=":443"; ma=86400 + cf-cache-status: DYNAMIC + vary: Accept-Encoding + method: GET + status: 200 + url: https://ac.rule34.xxx/autocomplete.php?q=neko +- response: + auto_calculate_content_length: false + body: '[]' + content_type: text/plain + headers: + CF-RAY: 9642e2d1fa52e84c-DME + Transfer-Encoding: chunked + access-control-allow-origin: '*' + alt-svc: h3=":443"; ma=86400 + cf-cache-status: DYNAMIC + vary: Accept-Encoding + method: GET + status: 200 + url: https://ac.rule34.xxx/autocomplete.php?q= diff --git a/tests/unit/test_rule34Py.py b/tests/unit/test_rule34Py.py index 9b324cb..0ad4aae 100644 --- a/tests/unit/test_rule34Py.py +++ b/tests/unit/test_rule34Py.py @@ -10,11 +10,29 @@ from rule34Py.post_comment import PostComment from rule34Py.icame import ICame from rule34Py.toptag import TopTag - +from rule34Py.autocomplete_tag import AutocompleteTag TEST_POOL_ID = 28 # An arbitrary, very-old pool, that is probably stable. R34_VERSION = importlib.metadata.version("rule34Py") +def test_rule34Py_autocomplete(rule34): + """The autocomplete method should return a list of tag suggestions.""" + suggestions = rule34.autocomplete("neko") + + assert isinstance(suggestions, list) + + if suggestions: + first = suggestions[0] + assert isinstance(first, AutocompleteTag) + assert hasattr(first, 'label') + assert hasattr(first, 'value') + assert hasattr(first, 'type') + assert isinstance(first.label, str) + assert isinstance(first.value, str) + assert isinstance(first.type, str) + + empty_suggestions = rule34.autocomplete("") + assert isinstance(empty_suggestions, list) def test_rule34Py_get_comments(rule34): """The get_comments() method should fetch a list of comments from a post.