Skip to content

Commit

Permalink
Added Nekos API wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
Nekidev committed Jan 7, 2023
1 parent ef12824 commit efb0e5d
Show file tree
Hide file tree
Showing 29 changed files with 617 additions and 42 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ These are the currently supported and planned to add support for APIs:
| Danbooru | [Documentation](https://danbooru.donmai.us/wiki_pages/help:api) ||
| Yandere | [Documentation](https://yande.re/help/api) ||
| Konachan | [Documentation](https://konachan.com/help/api) ||
| Waifu.im | [Documentation](https://waifu.im/) ||
| Waifu.im | [Documentation](https://waifu.im/) ||
| Catboys | [Documentation](https://catboys.com/api) ||
| Anime Character Database | [Documentation](http://wiki.animecharactersdatabase.com/index.php?title=API_Access) ||
| AniBase | Not released ||
| Nekos API | Not released | |
| Nekos API | [Documentation](https://nekos.nekidev.com/docs/rest-api/endpoints) | |


### APIs by feature
Expand All @@ -77,6 +77,11 @@ You know what you want to do, but have no idea of what API will work for you? Th

#### Images

- Nekos API:
- Thousands of anime images
- Lots of image metadata
- Actively developed (frequent new features, images, categories, and more)
- Completely free
- Animu:
- Tons of anime gifs and images
- Get reaction gifs from +60 different categories
Expand Down
10 changes: 10 additions & 0 deletions anime_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,14 @@
"http://wiki.animecharactersdatabase.com/index.php?title=API_Access",
False,
),
(
"Nekos API",
apis.NekosAPI,
"https://nekos.nekidev.com/docs/rest-api/endpoints",
True,
),
]

__version__ = '0.15.0'
__authors__ = ['Nekidev <neki@nekidev.com>']
__license__ = "MIT License"
3 changes: 2 additions & 1 deletion anime_api/apis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@
from .neko_love import NekoLoveAPI
from .nekos_moe import NekosMoeAPI
from .nekos_best import NekosBest
from .waifu_im import WaifuImAPI
from .waifu_im import WaifuImAPI
from .nekos_api import NekosAPI
52 changes: 30 additions & 22 deletions anime_api/apis/animechan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
Base module for the Animechan API. The documentation is available at
https://animechan.vercel.app/guide
"""
from urllib.parse import quote_plus

import typing
import requests

Expand All @@ -19,13 +21,27 @@ class AnimechanAPI:
def __init__(self, endpoint: typing.Optional[str] = None):
self.endpoint = endpoint or self.endpoint

def get_random_quote(self) -> Quote:
def get_random_quote(
self,
anime_title: typing.Optional[str] = None,
character_name: typing.Optional[str] = None,
) -> Quote:
"""
Get a random quote from the API.
"""
response = requests.get(f"{self.endpoint}/random")
response = requests.get(
f"{self.endpoint}/random"
+ (
"/character?name=" + quote_plus(character_name)
if character_name
else "/anime?title=" + quote_plus(anime_title)
if anime_title
else ""
),
timeout=5
)

AnimechanAPI.__check_response_code(response.status_code)
AnimechanAPI._check_response_code(response.status_code)

return Quote(**response.json())

Expand All @@ -35,23 +51,27 @@ def get_many_random_quotes(self) -> typing.List[Quote]:
"""
response = requests.get(f"{self.endpoint}/quotes")

AnimechanAPI.__check_response_code(response.status_code)
AnimechanAPI._check_response_code(response.status_code)

return [Quote(**quote) for quote in response.json()]

def search_by_anime_title(self, anime_title: str, page: int = 1) -> typing.List[Quote]:
def search_by_anime_title(
self, anime_title: str, page: int = 1
) -> typing.List[Quote]:
"""
Return a list of quotes from the given anime.
"""
response = requests.get(
f"{self.endpoint}/quotes/anime", params={"title": anime_title, "page": page}
)

AnimechanAPI.__check_response_code(response.status_code)
AnimechanAPI._check_response_code(response.status_code)

return [Quote(**quote) for quote in response.json()]

def search_by_character_name(self, character_name: str, page: int = 1) -> typing.List[Quote]:
def search_by_character_name(
self, character_name: str, page: int = 1
) -> typing.List[Quote]:
"""
Return a list of quotes from the given character.
"""
Expand All @@ -60,25 +80,13 @@ def search_by_character_name(self, character_name: str, page: int = 1) -> typing
params={"name": character_name, "page": page},
)

AnimechanAPI.__check_response_code(response.status_code)
AnimechanAPI._check_response_code(response.status_code)

return [Quote(**quote) for quote in response.json()]

def get_animes(self, page: int = 1) -> typing.List[str]:
"""
Return a list of animes.
"""
response = requests.get(f"{self.endpoint}/available/anime", params={"page": page})

AnimechanAPI.__check_response_code(response.status_code)

AnimechanAPI.__check_response_code(response.status_code)

return response.json()

@staticmethod
def __check_response_code(status_code: int, msg: str = ''):
def _check_response_code(status_code: int, msg: str = ""):
if status_code == 404:
raise exceptions.NotFound()
elif status_code != 200:
if status_code != 200:
raise exceptions.ServerError(status_code=status_code, msg=msg)
2 changes: 2 additions & 0 deletions anime_api/apis/nekobot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
Base module for the NekoBot API. Documentation can be found at
https://docs.nekobot.xyz
"""
from urllib.parse import quote_plus

import typing
import requests

Expand Down
235 changes: 235 additions & 0 deletions anime_api/apis/nekos_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
# pylint: disable=expression-not-assigned, missing-timeout
"""
Base module for Nekos API. Documentation can be found at
https://nekos.nekidev.com/docs/rest-api/endpoints
"""
from urllib.parse import quote
from datetime import datetime
from functools import wraps

import typing
import time
import re

import requests

from anime_api import exceptions
from anime_api.apis.nekos_api.objects import Image, Artist, Character, Category
from anime_api.utils import to_snake


last_request: typing.Optional[datetime] = None


def prevent_ratelimit(func):
"""
Sleeps necessary time before running a class method to prevent ratelimiting.
"""

@wraps(func)
def decorator(*args, **kwargs):
global last_request
now = datetime.now()
if last_request is None:
# No request has been made.
result = func(*args, **kwargs)
last_request = datetime.now()
return result

elapsed_time = now - last_request
wait_time = 1 - (elapsed_time.microseconds / 1000000)

time.sleep(wait_time)

result = func(*args, **kwargs)
last_request = datetime.now()
return result

return decorator


class NekosAPI:
"""
Docs: https://nekos.nekidev.com/docs/rest-api/endpoints
"""

endpoint: str = "https://nekos.nekidev.com/api"
token: typing.Optional[str] = None

def __init__(
self, endpoint: typing.Optional[str] = None, token: typing.Optional[str] = None
):
self.endpoint = endpoint or self.endpoint

if token:
NekosAPI._validate_token(token)

self.token = token

@prevent_ratelimit
def get_images(self, limit: int = 10, offset: int = 0):
"""
Returns a list of images.
* Requires a valid access token.
"""
if not self.token:
raise ValueError("You need a valid access token to use this method.")

headers = {"Authorization": "Bearer " + self.token}
params = {"limit": limit, "offset": offset}

response = requests.get(
self.endpoint + "/image", headers=headers, params=params
)

NekosAPI._check_response_code(response)

data = response.json()

return [Image.from_json(image) for image in data["data"]]

@prevent_ratelimit
def get_random_image(
self, categories: typing.Optional[typing.List[str]] = None
) -> Image:
"""
Returns a random image with the specified categories or completely
random if categories are not specified.
"""
params = {"categories": ",".join(categories)} if categories else {}

response = requests.get(self.endpoint + "/image/random", params=params)

NekosAPI._check_response_code(response)

data = response.json()

return Image.from_json(data["data"][0])

@prevent_ratelimit
def get_random_images(
self, count: int = 10, categories: typing.Optional[typing.List[str]] = None
) -> typing.List[Image]:
"""
Returns a certain amount of random images with the specified categories or completely
random if categories are not specified.
"""
params = {"limit": count}
params.update({"categories": ",".join(categories)}) if categories else None

response = requests.get(self.endpoint + "/image/random", params=params)

NekosAPI._check_response_code(response)

data = response.json()

return [Image.from_json(image) for image in data["data"]]

@prevent_ratelimit
def get_image_by_id(self, image_id: str) -> Image:
"""
Returns an image by its ID
"""
response = requests.get(self.endpoint + "/image/" + quote(image_id))

NekosAPI._check_response_code(response)

data = response.json()

return Image.from_json(data["data"])

@prevent_ratelimit
def get_artist_by_id(self, artist_id: str) -> Artist:
"""
Returns an artist by its ID
"""
response = requests.get(self.endpoint + "/artist/" + quote(artist_id))

NekosAPI._check_response_code(response)

data = response.json()

return Artist(**to_snake(data["data"]))

@prevent_ratelimit
def get_images_by_artist_id(
self, artist_id: str, limit: int = 10, offset: int = 0
) -> typing.List[Image]:
"""
Returns all artist's images.
"""
response = requests.get(
self.endpoint + "/artist/" + quote(artist_id) + "/images",
params={"limit": limit, "offset": offset},
)

NekosAPI._check_response_code(response)

data = response.json()

return [Image.from_json(image) for image in data["data"]]

@prevent_ratelimit
def get_categories(self, limit: int = 10, offset: int = 0) -> typing.List[Category]:
"""
Returns a list of all categories.
"""
response = requests.get(
self.endpoint + "/category", params={"limit": limit, "offset": offset}
)

NekosAPI._check_response_code(response)

data = response.json()

return [Category(**to_snake(category)) for category in data["data"]]

@prevent_ratelimit
def get_category_by_id(self, category_id: str) -> Category:
"""
Returns a category by it's ID.
"""
response = requests.get(self.endpoint + "/category/" + quote(category_id))

NekosAPI._check_response_code(response)

data = response.json()

return Category(**to_snake(data["data"]))

@prevent_ratelimit
def get_character_by_id(self, character_id: str) -> Character:
"""
Returns a character by it's ID.
"""
response = requests.get(self.endpoint + "/character/" + quote(character_id))

NekosAPI._check_response_code(response)

data = response.json()

return Character(**to_snake(data["data"]))

@staticmethod
def _check_response_code(res) -> None:
"""
Check if the request was successful.
"""
data = res.json()
success = data["success"]
if res.status_code not in range(200, 300) or not success:
raise exceptions.ServerError(
res.status_code,
f"An error occurred while fetching the data from the server{'. ' + data['message'] if 'message' in data else ''}",
)

@staticmethod
def _validate_token(token) -> None:
"""
Validate the API token.
"""
exp = r"^[0-9a-zA-Z]{100}$"
if len(re.findall(exp, token)) == 0:
raise ValueError(
"The token is invalid. It should be 100 characters long and contain numbers and lowercase/uppercase characters."
)
Loading

0 comments on commit efb0e5d

Please sign in to comment.