Skip to content
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
4 changes: 3 additions & 1 deletion rule34Py/api_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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}}"
36 changes: 36 additions & 0 deletions rule34Py/autocomplete_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# rule34Py - Python api wrapper for rule34.xxx
#
# Copyright (C) 2022-2025 b3yc0d3 <b3yc0d3@gmail.com>
#
# 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 <https://www.gnu.org/licenses/>.
"""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
35 changes: 35 additions & 0 deletions rule34Py/rule34.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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.

Expand Down
33 changes: 33 additions & 0 deletions tests/fixtures/mock34/responses.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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=
20 changes: 19 additions & 1 deletion tests/unit/test_rule34Py.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading