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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [unreleased] - 2025-08-24

### Added
- Added a `rule34.autocomplete` method.
- Added `AutocompleteTag` class.

### Changed
- Updated API wrapper to support website’s new authentication system.
- The underlying website API now **requires authentication** (`api_key` and `user_id`) for all requests.

## [3.0.0] - 2025-06-09

### Added
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ $(wheels) &: $(dist_files)


$(sdist) : $(dist_files)
$(POETRY) --output $(builddir) --format=sdist
$(POETRY) build --output $(builddir) --format=sdist


# PHONY TARGETS #
#################

# Build all binary targets necessary for installation.
# Does not build documentation or source distributions.
all : $(wheels)
all : $(wheels) $(sdist)
.PHONY : all


Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,30 @@ See the [Developer Guide](https://b3yc0d3.github.io/rule34Py/dev/developer-guide

```python
from rule34Py import rule34Py
r34Py = rule34Py()
client = rule34Py()
client.api_key = "YOUR_API_KEY"
client.user_id = "YOUR_USER_ID"

# Get comments of an post.
r34Py.get_comments(4153825)
client.get_comments(4153825)

# Get post by its id.
r34Py.get_post(4153825)
client.get_post(4153825)

# Get top 100 icame.
r34Py.icame()
client.icame()

# Search for posts by tag(s).
r34Py.search(["neko"], page_id=2, limit=50)
client.search(["neko"], page_id=2, limit=50)

# Get pool by id.
r34Py.get_pool(28)
client.get_pool(28)

# Get a random post.
random = r34Py.random_post()
random = client.random_post()

# Get just a random post ID.
random_id = r34Py.random_post_id()
random_id = client.random_post_id()
```


Expand Down
Binary file added docs/_static/api-credentials.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

# sphinx.ext.autodoc configuration #
autodoc_default_options = {
"private-members": False
}
autodoc_typehints = "both" # Show typehints in the signature and as content of the function or method

Expand Down
44 changes: 44 additions & 0 deletions docs/guides/api-credentials.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
=================================
How to set Rule34 api credentials
=================================

Since Aug 19, 2025 the `api.rule34.xxx <https://rule34.xxx/index.php?page=help&topic=dapi>`_ REST API requires credentials for all requests made with the REST API. With that changes also came a rate limit of 60 request per 60 seconds.

In order to be able to make requests with `rule34Py <https://pypi.org/project/rule34Py/>`_, you now have to set the api key and your user id.

Reference the :doc:`rule34Py.rule34Py <../../api/rule34Py/rule34>` class documentation for an indication of which workflows have these limits.

Note that as of now, you need an `rule34.xxx <https://rule34.xxx>`_ account.


Setting api credentials
=======================

A `rule34.xxx <https://rule34.xxx>`_ account is required to receive REST API credentials.

#. Use any reasonable browser with javascript enabled to open any URL to the https://rule34.xxx site.

#. Login without account

#. Navigate to https://rule34.xxx/index.php?page=account&s=options

#. Scroll down to **API Access Credentials**, there you will find a long string similar to the one bellow. If you don't see a text similar to the one below, click the checkbox *Generate New Key?* and then *Save* at the bottom, revisit the site.

.. image:: /_static/api-credentials.png

- The `api_key` is highlighted in yellow (the long block of text with 128 letters and numbers)
- The `user_id` is highlighted in green (the short block of seven digits)

.. important::

Do not share those information with anyone online!

#. Set the in the previous step retrieved credentials like the following.

.. code-block:: python

import rule34Py as r34
client = r34.rule34Py()

client.api_key="00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
client.user_id="0000000"
1 change: 1 addition & 0 deletions docs/guides/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ This section contains how-to guides and special topics of interest to module use
:maxdepth: 1

captcha-clearance
api-credentials
package-metadata
2 changes: 2 additions & 0 deletions docs/tutorials/downloader.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from rule34Py import rule34Py

client = rule34Py()
client.api_key = "YOUR_API_KEY"
client.user_id = "YOUR_USER_ID"

TAGS = ["neko", "sort:score", "-video"]

Expand Down
80 changes: 51 additions & 29 deletions rule34Py/rule34.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
SEARCH_RESULT_MAX: int = 1000


class rule34Py():
class rule34Py:
"""The rule34.xxx API client.

This class is the primary broker for interactions with the real Rule34 servers.
Expand All @@ -62,6 +62,9 @@ class rule34Py():
.. code-block:: python

client = rule34Py()
client.api_key="API_KEY"
client.user_id="USER_ID"

post = client.get_post(1234)
"""

Expand All @@ -77,9 +80,21 @@ class rule34Py():
#: Defaults to either the value of the ``R34_USER_AGENT`` environment variable; or the ``rule34Py.rule34.DEFAULT_USER_AGENT``, if not asserted.
#: Can be overridden by the user at runtime to change User-Agents.
user_agent: str = os.environ.get("R34_USER_AGENT", DEFAULT_USER_AGENT)
#: User id required for requests by `rule34.xxx <https://api.rule34.xxx/>`_
user_id: str = None
#: Api key required for requests by `rule34.xxx <https://api.rule34.xxx/>`_
api_key: str = None

def __init__(self):
"""Initialize a new rule34 API client instance."""
"""Initialize a new rule34 API client instance.

Args:
api_key: Api key from rule34.xxx

user_id: User id from rule34.xxx account

The api key and the user id can both be found at <https://rule34.xxx/index.php?page=account&s=options>.
"""
self.session = requests.session()
self.session.mount(__base_url__, self._base_site_rate_limiter)

Expand All @@ -99,20 +114,19 @@ def autocomplete(self, tag_string: str) -> list[AutocompleteTag]:
""" # 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 = 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"]
)
AutocompleteTag(label=item["label"], value=item["value"], type=item["type"])
for item in raw_data
]
return results
Expand All @@ -124,11 +138,24 @@ def _get(self, *args, **kwargs) -> requests.Response:

Returns:
The Response object from the GET request.

Raises:
ValueError: API credentials aer not supplied.
"""
is_api_request = args[0].startswith(__api_url__) == True

# check if api credentials are set
if is_api_request and self.user_id == None and self.api_key == None:
raise ValueError("API credentials must be supplied, api_key and user_id can not be None!\nSee https://api.rule34.xxx/ for more information.")

# headers
kwargs.setdefault("headers", {})
kwargs["headers"].setdefault("User-Agent", self.user_agent)

# api authentication
if is_api_request:
kwargs["params"] = {"api_key": self.api_key, "user_id": self.user_id}

# cookies
kwargs.setdefault("cookies", {})
if self.captcha_clearance is not None:
Expand All @@ -152,9 +179,7 @@ def get_comments(self, post_id: int) -> list[PostComment]:
Raises:
requests.HTTPError: The backing HTTP GET operation failed.
""" # noqa: DOC502
params = [
["POST_ID", str(post_id)]
]
params = [["POST_ID", str(post_id)]]
formatted_url = self._parseUrlParams(API_URLS.COMMENTS, params)
response = self._get(formatted_url)
response.raise_for_status()
Expand All @@ -163,11 +188,11 @@ def get_comments(self, post_id: int) -> list[PostComment]:
comment_soup = BeautifulSoup(response.content.decode("utf-8"), features="xml")
for e_comment in comment_soup.find_all("comment"):
comment = PostComment(
id = e_comment["id"],
owner_id = e_comment["creator_id"],
body = e_comment["body"],
post_id = e_comment["post_id"],
creation = e_comment["created_at"],
id=e_comment["id"],
owner_id=e_comment["creator_id"],
body=e_comment["body"],
post_id=e_comment["post_id"],
creation=e_comment["created_at"],
)
comments.append(comment)

Expand All @@ -188,9 +213,7 @@ def get_pool(self, pool_id: int) -> Pool:
Raises:
requests.HTTPError: The backing HTTP GET operation failed.
""" # noqa: DOC502
params = [
["POOL_ID", str(pool_id)]
]
params = [["POOL_ID", str(pool_id)]]
response = self._get(self._parseUrlParams(API_URLS.POOL.value, params))
response.raise_for_status()
return PoolPage.pool_from_html(response.text)
Expand All @@ -207,9 +230,7 @@ def get_post(self, post_id: int) -> Union[Post, None]:
Raises:
requests.HTTPError: The backing HTTP GET operation failed.
""" # noqa: DOC502
params = [
["POST_ID", str(post_id)]
]
params = [["POST_ID", str(post_id)]]
formatted_url = self._parseUrlParams(API_URLS.GET_POST.value, params)
response = self._get(formatted_url)
response.raise_for_status()
Expand Down Expand Up @@ -311,7 +332,7 @@ def random_post(self) -> Post:

Returns:
A random Post.

Raises:
requests.HTTPError: The backing HTTP GET operation failed.
""" # noqa: DOC502
Expand All @@ -335,9 +356,10 @@ def random_post_id(self) -> int:
response = self._get(API_URLS.RANDOM_POST.value)
response.raise_for_status()
parsed = urlparse.urlparse(response.url)
return int(parse_qs(parsed.query)['id'][0])
return int(parse_qs(parsed.query)["id"][0])

def search(self,
def search(
self,
tags: list[str] = [],
page_id: Union[int, None] = None,
limit: int = SEARCH_RESULT_MAX,
Expand Down
Loading
Loading