Merge branch 'v4'

This commit is contained in:
Irfan 2022-01-02 03:13:01 +05:00
commit 8417809a44
256 changed files with 31917 additions and 4602 deletions

120
.env.dist
View File

@ -1,33 +1,127 @@
###
# App
###
APP_ENV=production
APP_DEBUG=false
APP_KEY=
APP_TIMEZONE=UTC
APP_URL=http://localhost
APP_VERSION="4.0 Beta"
CACHE_DRIVER=file
QUEUE_CONNECTION=redis
###
# Database Caching (MongoDB)
###
DB_CACHING=true
DB_CONNECTION=mongodb
DB_HOST=localhost
DB_PORT=27017
DB_DATABASE=jikan
DB_ADMIN=jikan
DB_USERNAME=
DB_PASSWORD=
CACHE_METHOD=legacy
CACHE_DEFAULT_EXPIRE=86400
CACHE_META_EXPIRE=300
CACHE_USER_EXPIRE=300
CACHE_404_EXPIRE=604800
CACHE_SEARCH_EXPIRE=432000
###
# Database query default values
###
MAX_RESULTS_PER_PAGE=30
###
# Enable MyAnimeList Heartbeat
#
# Monitor bad requests to determine whether MyAnimeList is down
#
# Fallback once the following threshold is reached
###
SOURCE=local
SOURCE_BAD_HEALTH_THRESHOLD=10
# Recheck source availability (in seconds)
SOURCE_BAD_HEALTH_RECHECK=10
# Fail count only within specified time range (in seconds)
SOURCE_BAD_HEALTH_RANGE=30
# Max Fail stores
SOURCE_BAD_HEALTH_MAX_STORE=50
# Disable failover if the score reaches the following (0.0-1.0 values ONLY)
# e.g 0.9 means 90% successful requests to MyAnimeList
SOURCE_GOOD_HEALTH_SCORE=0.9
# Max time request is allowed to take
# https://curl.haxx.se/libcurl/c/CURLOPT_TIMEOUT.html
SOURCE_TIMEOUT=10
###
# Caching (File, Redis, etc)
# Can be added over DB Caching
###
CACHING=false
CACHE_DRIVER=array
CACHE_METHOD=queue
# Caching TTL (in seconds) on specific endpoints
CACHE_DEFAULT_EXPIRE=86400 # 1 day
CACHE_META_EXPIRE=300 # 5 minutes
CACHE_USER_EXPIRE=300 # 5 minutes
CACHE_USERLIST_EXPIRE=3600 # 1 hour
CACHE_404_EXPIRE=604800 # 7 days
CACHE_SEARCH_EXPIRE=432000 # 5 days
CACHE_PRODUCERS_EXPIRE=432000 # 5 days
CACHE_MAGAZINES_EXPIRE=432000 # 5 days
###
# Redis Caching Configuration
###
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
###
# Micro Caching
# Uses CACHE_DRIVER
###
MICROCACHING=false
MICROCACHING_EXPIRE=5
###
# Queue management
# Uses QUEUE_CONNECTION as queue storage (MongoDB, Redis, etc)
###
QUEUE_CONNECTION=database
QUEUE_TABLE=jobs
QUEUE_FAILED_TABLE=jobs_failed
QUEUE_DELAY_PER_JOB=5
###
# Throttling
# Rate limiting requests
###
THROTTLE=false
THROTTLE_DECAY_MINUTES=1
THROTTLE_MAX_REQUESTS_PER_DECAY_MINUTES=60
THROTTLE_MAX_REQUESTS_PER_SECOND=2
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
###
# GitHub generate report URL on fatal errors
###
GITHUB_REPORTING=true
GITHUB_REST="jikan-me/jikan-rest"
GITHUB_API="jikan-me/jikan"
GITHUB_API="jikan-me/jikan"
###
# OpenAPI
###
SWAGGER_VERSION=3.0
###
# API call insights
###
# Enable/Disable insights API system
INSIGHTS=false #WIP
# Max requests store in seconds - default 2 days
INSIGHTS_MAX_STORE_TIME=172800
###
# Error reporting
###
REPORTING=true
REPORTING_DRIVER=sentry
SENTRY_LARAVEL_DSN="https://examplePublicKey@o0.ingest.sentry.io/0"
SENTRY_TRACES_SAMPLE_RATE=1

5
.gitignore vendored
View File

@ -1,12 +1,11 @@
/vendor
/vendor.v4
/.idea
Homestead.json
Homestead.yaml
.env.v4
.env
composer.phar
composer.lock
.php_cs.cache
.env
.env.v4
/storage/app/indexer
/storage/app/failovers.json

View File

@ -11,7 +11,10 @@ For an entire list of commands, you can run `php artisan list`
- [Remove](#cache-remove)
- [Change Cache Driver](#cache-change-cache-driver)
- [Change Cache Method](#cache-change-cache-method)
- [Indexer](#indexer)
- [Anime](#anime)
- [Manga](#manga)
## Commands
### Serve
@ -56,4 +59,53 @@ Command: `cache:method {method}`
Example: `cache:method queue`
[Read more on how it works](https://github.com/jikan-me/jikan-rest/blob/master/README.md#06-configuring-how-jikan-handles-expired-cache-optional)
[Read more on how it works](https://github.com/jikan-me/jikan-rest/blob/master/README.md#06-configuring-how-jikan-handles-expired-cache-optional)
#### Indexer: Anime
Since v4 uses MongoDB as a means to index cache on some endpoints, having a built cache is important since it
works best for endpoints like search or top.
`Indexer:Anime` uses [https://github.com/seanbreckenridge/mal-id-cache](https://github.com/seanbreckenridge/mal-id-cache) to fetch available MAL IDs and indexes them.
This function only needs to be run once. Any entry's cache updating will automatically be taken care of if it's expired, and a client makes a request for that entry.
⚠ This is strictly for performance and experience and providing better search functionality. Don't build your own anime database as that's against MyAnimeList's Terms of Service.
Command:
```
indexer:anime
{--failed : Run only entries that failed to index last time}
{--resume : Resume from the last position}
{--reverse : Start from the end of the array}
{--index=0 : Start from a specific index}
{--delay=3 : Set a delay between requests}
```
Example: `indexer:anime --reverse --delay=5 --failed`
This translates to running entries that previously failed to index or update, in reverse, with a delay of 5 seconds between each request.
#### Indexer: Manga
Since v4 uses MongoDB as a means to index cache on some endpoints, having a built cache is important since it
works best for endpoints like search or top.
`Indexer:Manga` uses [https://github.com/seanbreckenridge/mal-id-cache](https://github.com/seanbreckenridge/mal-id-cache) to fetch available MAL IDs and indexes them.
This function only needs to be run once. Any entry's cache updating will automatically be taken care of if it's expired, and a client makes a request for that entry.
⚠ This is strictly for performance and experience and providing better search functionality. Don't build your own anime database as that's against MyAnimeList's Terms of Service.
Command:
```
indexer:anime
{--failed : Run only entries that failed to index last time}
{--resume : Resume from the last position}
{--reverse : Start from the end of the array}
{--index=0 : Start from a specific index}
{--delay=3 : Set a delay between requests}
```
Example: `indexer:manga`
This simply translates to running the indexer without any additional configuration.

View File

@ -1,181 +0,0 @@
# Jikan REST Migration (v2 -> v3)
## NOTICE!
- Any key that holds an array value will be an empty array if there's nothing
- Any key that holds anything other than an array (string, int, float) will be `null` if there's nothing
## Anime
- Added `aired['string']`
- Added `trailer_url`
- Removed `airing_string`
- `link_canonical` -> `url`
- `title_synonyms` is now an array
- `producer` -> `producers`
- `licensor` -> `licensors`
- `studio` -> `studios`
- `genre` -> `genres`
- `opening_theme` -> `opening_themes`
- `ending_theme` -> `ending_themes`
## Anime : /episodes
- `episode` -> `episodes`
- `id` -> `episode_id`
- `aired` is now an array
- `episode_last_page` -> `episodes_last_page`
## Anime : /characters\_and\_staff
- `character` -> `characters`
`voice_actor` -> `voice_actors`
- `staff`
- `role` -> `positions`
- `positions` is now an array
## Anime : /news
- `news` -> `articles`
- `date` is now in ISO8601
- Added `intro`
## Anime : /pictures
- `image` -> `pictures`
- `pictures` is now an array with 2 items
- `large` - large version of the image
- `small` - small version of the image
## Anime : /videos
- `episode` -> `episodes`
## Anime : /stats
- `score_stats` -> `scores`
## Anime : /forum
- `topic` -> `topics`
- `last_post`
- `date_relative` -> `date_posted`
- `date_posted` is now in ISO8601
## Anime : /moreinfo
- `more_info` -> `moreinfo`
## Manga
- Added `published['string']`
- Removed `published_string`
- `link_canonical` -> `url`
- `title_synonyms` is now an array
- `author` -> `authors`
- `serialization` -> `serializations`
- `genre` -> `genres`
## Manga : /characters
- `character` -> `characters`
## Manga : /news
- `news` -> `articles`
- `date` is now in ISO8601
- Added `intro`
## Manga : /pictures
- `image` -> `pictures`
- `pictures` is now an array with 2 items
- `large` - large version of the image
- `small` - small version of the image
## Manga : /stats
- `score_stats` -> `scores`
## Manga : /forum
- `topic` -> `topics`
- `last_post`
- `date_relative` -> `date_posted`
- `date_posted` is now in ISO8601
## Manga : /moreinfo
- `more_info` -> `moreinfo`
## Character
- `link_canonical` -> `url`
- `nicknames` is now an array
- `voice_actor` -> `voice_actors`
## Character : /pictures
- `image` -> `pictures`
- `pictures` is now an array with 2 items
- `large` - large version of the image
- `small` - small version of the image
## Person
- `link_canonical` -> `url`
- `birthday` is now in ISO8601
- `more` -> `about`
- `voice_acting_role` -> `voice_acting_roles`
- Added `role`
- `anime_staff_position` -> `anime_staff_positions`
- `role` -> `position`
- `published_manga`
- `role` -> `position`
## Person : /pictures
- `image` -> `pictures`
- `pictures` is now an array with 2 items
- `large` - large version of the image
- `small` - small version of the image
## Search
Search query as a URL segment is now depreciated. You have to pass the query via GET key `q`.
e.g `/search/anime?q=Fate/Zero`
- `result` -> `results`
- `result_last_page` -> `last_page`
## Search : /anime
- `result`
- Added `airing` (boolean)
- `description` -> `synopsis`
- Added `start_date` (ISO8601)
- Added `end_date` (ISO8601)
- Added `rated`
## Search : /manga
- `result`
- Added `publishing` (boolean)
- `description` -> `synopsis`
- Added `start_date` (ISO8601)
- Added `end_date` (ISO8601)
- Added `chapters` (int)
## Search : /people, /person
- `result`
- `nicknames` -> `alternative_names`
- `alternative_names` is now an array
## Search : /character
- `result`
- `nicknames` -> `alternative_names`
- `alternative_names` is now an array
## Season
- `season` -> `anime`
- `producer` -> `producers`
- `genre` -> `genres`
- `licensor` -> `licensors`
- `continued` -> `continuing`
- `airing_start` is now in ISO8601
- `r18_plus` -> `r18`
## Schedule
- Added `other`
- Added `unknown`
- `producer` -> `producers`
- `genre` -> `genres`
- `licensor` -> `licensors`
- `continued` -> `continuing`
- `airing_start` is now in ISO8601
- `r18_plus` -> `r18`
## Top : Anime
- `airing_start` -> `start_date`
- `airing_end` -> `end_date`
## Top : Manga
- `publishing_start` -> `start_date`
- `publishing_end` -> `end_date`

204
README.MD
View File

@ -1,187 +1,52 @@
![Jikan](http://i.imgur.com/ctoJ3Jp.png)
# Jikan - Unofficial MyAnimeList.net REST API
[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/jikan-me/jikan-rest.svg)](http://isitmaintained.com/project/jikan-me/jikan-rest "Average time to resolve an issue") [![Percentage of issues still open](http://isitmaintained.com/badge/open/jikan-me/jikan-rest.svg)](http://isitmaintained.com/project/jikan-me/jikan-rest "Percentage of issues still open") [![stable](https://img.shields.io/badge/PHP-^%207.4-blue.svg?style=flat)]() [![Discord Server](https://img.shields.io/discord/460491088004907029.svg?style=flat&logo=discord)](http://discord.jikan.moe/)
[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/jikan-me/jikan-rest.svg)](http://isitmaintained.com/project/jikan-me/jikan-rest "Average time to resolve an issue") [![Percentage of issues still open](http://isitmaintained.com/badge/open/jikan-me/jikan-rest.svg)](http://isitmaintained.com/project/jikan-me/jikan-rest "Percentage of issues still open") [![stable](https://img.shields.io/badge/PHP-^7.2.5-blue.svg?style=flat)]() [![Discord Server](https://img.shields.io/discord/460491088004907029.svg?style=flat&logo=discord)](https://discordapp.com/invite/4tvCr36)
Jikan is a REST API for [MyAnimeList.net](https://myanimelist.net). It scrapes the website to satisfy the need for an API - which MyAnimeList lacks.
Jikan is a REST API for [MyAnimeList.net](https://myanimelist.net). It scrapes the website to satisfy the need for some API functionality - that MyAnimeList lacks.
The raison d'être of Jikan is to assist developers easily get the data they need for their apps and projects without having to depend on the lackluster official API, unstable APIs, or sidetracking their projects to develop parsers.
The raison d'être of Jikan is to assist developers easily get the data they need for their apps and projects without having to depend on unstable APIs, or sidetracking their projects to develop parsers.
The word _Jikan_ literally translates to _Time_ in Japanese (**時間**). And that's what this API saves you of. ;)
**Notice**: Jikan does not support authenticated requests. You can not update your lists.
## Index
- [Getting Started](#getting-started)
- [Requirements](#requirements)
- [🐳 Docker](#-docker)
- [Installation Prerequisites](#01-installation-prerequisites)
- [Installation](#02-installation)
- [Configuration](#03-configuration)
- [Ignition](#04-ignition)
- [Configuring Cache Driver](#05-configuring-how-jikan-caches-optional) (optional)
- [Configuring Cache Method](#06-configuring-how-jikan-handles-expired-cache-optional) (optional)
- [Configuring Supervisord](#configuring-supervisord) (optional)
- [Troubleshooting](#troubleshooting)
- [Artisan Commands](#artisan-commands)
- [Information](#information)
- [Wrappers](#wrappers)
- [Running Tests](#running-tests)
- [Backers](#backers)
- [Disclaimer](#disclaimer)
## Getting Started
### Requirements
- PHP `^7.4`
- [Composer](https://getcomposer.org/download/)
- [Redis](https://redis.io)
- [Supervisord](http://supervisord.org/) (optional)
#### 🐳 Docker
If you don't want to install it yourself, you can use the [docker image](https://github.com/jikan-me/jikan-docker)
### 01. Installation Prerequisites
#### 01A. Linux
This is specifically for Ubuntu, but other distributions should work similarly.
1. Install requirements:
- Add PHP related packages: `sudo add-apt-repository -y ppa:ondrej/php`
- If `add-apt-repository` is not installed, you can install it by doing `sudo apt install python-software-properties` or `sudo apt install software-properties-common`
- `sudo apt update && sudo apt upgrade`
- Install requirements: `sudo apt install curl git php redis unzip`
- Verify that PHP 7.4 is installed: `php -v`
- If not, install it by running `sudo apt install php7.4` and change the default PHP version with `sudo update-alternatives --set php /usr/bin/php7.4`
- Install the corresponding `php-xml` and `php-mbstring` packages for your version, e.g:
- PHP 7.4: `sudo apt install php7.4-xml php7.4-mbstring`
- Install composer: `curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin --filename=composer`
2. Start the redis server: `sudo service redis start`
#### 01B. Mac
1. Install [brew](https://brew.sh/)
2. Install requirements: `brew install php composer redis`
3. Start the redis server: `brew services start redis`
### 02. Installation
1. `git clone https://github.com/jikan-me/jikan-rest.git`
2. `cd jikan-rest`
3. `cp .env.dist .env`
5. `composer install` (Make sure Jikan's directory has write permissions)
### 03. Configuration
You're able to configure Jikan through the `.env` file.
A few kernel commands are available from the project directory by running the `artisan` file.
The first thing you need to do is generate an `APP_KEY`.
1. `php artisan key:generate`
2. [Configure how Jikan caches](https://github.com/jikan-me/jikan-rest#05-configuring-how-jikan-caches-optional)
3. [Configure how Jikan handles expired cache](https://github.com/jikan-me/jikan-rest#06-configuring-how-jikan-handles-expired-cache-optional)
### 04. Ignition
`php artisan serve --port=8080` or `php -S localhost:8000 -t public`
Jikan is now hosted on `http://localhost:8000/v3/`
**Alternatively,** host it on Apache (or Nginx)
Create a virtual host and point it to `/public`. Jikan supports Apache out of the box, you just need to create a virtual host and point it to `/public`, and enable the rewrite module for .htaccess (`sudo a2enmod rewrite`), and configure `/etc/apache/apache2.conf` by setting `AllowOverride None` to `AllowOverride All` for the `/var/www` directory.
:information_source: If you wish to configure it for Nginx or anything else, you'll have to port the rewrite rules located at `public/.htaccess`
### 05. Configuring how Jikan Caches (optional)
Jikan caches on file by default in `/storage/framework/cache`. So even if you don't change the caching method, Jikan will work out of the box.
However, you can configure Jikan to cache on Redis instead: `php artisan cache:driver redis`
Note: If you're currently running Jikan, you're required to stop it before running the above command.
### 06. Configuring how Jikan handles expired cache (optional)
Jikan handles cache in the `legacy` manner out of the box. This method was used previously to update cache.
**Notice**: Jikan does not support authenticated requests. You can not update your lists. Use the official MyAnimeList API for this.
#### 06A. Cache Method: Legacy
When a cache expires, it gets deleted. So if you make a request that has an expired cache, your request will take longer as Jikan has to fetch and parse the new data from MyAnimeList again.
## Installation
#### 06B. Cache Method: Queue
This is a newly introduced caching method to the API, it's what the public API runs on as well. It requires some further setup.
When a cache expires, it does not get deleted. Instead, if you make a request that has an expired cache, a job will be dispatched to the queue which handles updating the cache in the background. Therefore, the request will keep on providing stale cache until the job is complete and the cache is replaced with fresh data.
This method provides zero delay, and is highly recommended if you have immense traffic coming your way.
:information_source: Note: If you're currently running Jikan, you're required to stop it before running the above command. You're also required to clear any cache you've stored as well as anything on the Redis server.
1. `php artisan cache:method queue`
Next, you need to make sure that there's a service looking after the queue. This can be manually done by running a process through `php artisan queue:work --queue=high,low`. You can set the command to run on cron, nohup, etc.
But a recommended way is to install Supervisor and have it handle the queue automatically.
:information_source: Note: `--queue=high,low`; Jikan stores two types of queues; high priority and low priority. This depends on the type of request. You can check which request is considered to be high priority in the [JikanResponseHandler.php](https://github.com/jikan-me/jikan-rest/blob/master/app/Http/Middleware/JikanResponseHandler.php) middleware in the `HIGH_PRIORITY_QUEUE` array.
:information_source: Note 2: Not all requests are queuable. Some are handled the `legacy` way. You can find out which ones in the [JikanResponseHandler.php](https://github.com/jikan-me/jikan-rest/blob/master/app/Http/Middleware/JikanResponseHandler.php) middleware in the `NON_QUEUEABLE` array.
This reason for this is quite simple. User related requests such as anime/manga list can be frequently updated. They're cached by default for 5 minutes (you can change this in `.env`). But if they were to get queued for a cache update, it would take longer than 5 minutes because the update job would have to wait in line. So it skips the queue and is automatically updated on the request. This does mean a slight delay in fetching and parsing the fresh data from MyAnimeList.
:information_source: Note 3: Note 1 & Note 2 are default behavior. You can obviously change them as per your needs.
##### Configuring Supervisord
###### Linux
1. Install supervisor
- Linux: `sudo apt install supervisor`
- Mac: `brew install supervisor`
2. `sudo cp conf/supervisor/jikan-worker.conf /etc/supervisor/conf.d`
- A default supervisor configureation file is available in this repo `conf/supervisor/jikan-worker.conf`
- Be sure to update to the correct directory in `jikan-worker.conf` for `command` and `stdout_logfile` to the directory of jikan!
Example: If I install Jikan in `/var/www/jikan-is-installed-here`, you will have to adjust the following values in the `jikan-worker.conf` file.
```
...
command=php /var/www/jikan-is-installed-here/artisan queue:work --queue=high,low
...
stdout_logfile=/var/www/jikan-is-installed-here/storage/logs/worker.log
stderr_logfile=/var/www/jikan-is-installed-here/storage/logs/worker.error.log
```
3. `sudo supervisorctl reread`
4. `sudo supervisorctl update`
5. `sudo supervisorctl start jikan-worker:*`
###### Mac
1. Install Supervisor: `brew install supervisor`
2. `supervisord -c /usr/local/etc/supervisord.ini`
3. Copy `conf/supervisor/jikan-worker.conf` to `/usr/local/etc/supervisor.d/`
4. `brew services start supervisor`
5. `sudo supervisorctl update`
6. `sudo supervisorctl start jikan-worker:*`
## Troubleshooting
Please read the [troubleshooting guide](https://github.com/jikan-me/jikan-rest/blob/master/TROUBLESHOOTING.md).
### Manual installation
Please read the [manual installation guide](https://github.com/jikan-me/jikan-rest/wiki).
For any additional help, join our [Discord server](http://discord.jikan.moe/).
## Artisan Commands
Please read the [commands guide](https://github.com/jikan-me/jikan-rest/blob/master/COMMANDS.MD).
For any additional help, join our [Discord server](http://discord.jikan.moe/).
### 🐳 Docker Installation
If you don't want to install it manually, you can use the [docker image](https://github.com/jikan-me/jikan-docker)
## Information
## Public REST API
If you don't want to host your instance, there's a public API available.
- **[REST DOCUMENTATION](https://jikan.docs.apiary.io)**
- **[Apps/Projects using JikanREST](https://jikan.moe/showcase)**
- *[Apps/Projects using the REST API](https://jikan.moe/showcase)*
### Documentation
Please view the [documentation](https://docs.api.jikan.moe/).
For any additional help, join our [Discord server](http://discord.jikan.moe/).
## Wrappers
See the list of wrappers [here](https://github.com/jikan-me/jikan#wrappers)
| Language | Wrappers |
|------------|----------|
| JavaScript | [JikanJS](https://github.com/zuritor/jikanjs) by Zuritor |
| Java | [Jikan4java](https://github.com/Doomsdayrs/Jikan4java) by Doomsdayrs<br>[reactive-jikan](https://github.com/SandroHc/reactive-jikan) by Sandro Marques<br>[Jaikan](https://github.com/ShindouMihou/Jaikan) by ShindouMihou |
| Python | [JikanPy](https://github.com/abhinavk99/jikanpy) by Abhinav Kasamsetty |
| Node.js | [jikan-node](https://github.com/xy137/jikan-node) by xy137<br>[jikan-nodejs](https://github.com/ribeirogab/jikan-nodejs) by ribeirogab |
| TypeScript | [jikants](https://github.com/Julien-Broyard/jikants) by Julien Broyard<br>[jikan-client](https://github.com/javi11/jikan-client) by Javier Blanco |
| PHP | [jikan-php](https://github.com/janvernieuwe/jikan-jikanPHP) by Jan Vernieuwe |
| .NET | [Jikan.net](https://github.com/Ervie/jikan.net) by Ervie |
| Elixir | [JikanEx](https://github.com/seanbreckenridge/jikan_ex) by Sean Breckenridge |
| Go | [jikan-go](https://github.com/darenliang/jikan-go) by Daren Liang<br>[jikan2go](https://github.com/nokusukun/jikan2go) by nokusukun |
| Ruby | [Jikan.rb](https://github.com/Zerocchi/jikan.rb) by Zerocchi |
| Dart | [jikan-dart](https://github.com/charafau/jikan-dart) by Rafal Wachol |
| Kotlin | [JikanKt](https://github.com/GSculerlor/JikanKt) by Ganedra Afrasya |
[Add your wrapper here](https://github.com/jikan-me/jikan-rest/edit/master/readme.md)
## Running Tests
@ -190,19 +55,18 @@ See the list of wrappers [here](https://github.com/jikan-me/jikan#wrappers)
Note: Tests may fail due to rate limit from MyAnimeList (HTTP 429)
---
# Backers
## Backers
A huge thank you to all our Patrons! 🙏 This project wouldn't be running without your support.
We have a free [REST API service](https://jikan.moe), if you wish to support us you can [become a Patron!](https://patreon.com/jikan)
## Sugoi (すごい) Patrons
### Sugoi (すごい) Patrons
- [Jared Allard (jaredallard)](https://github.com/jaredallard)
- [hugonun (hug_onun)](https://twitter.com/hug_onun)
## Patrons
### Patrons
- Aaron Treinish
- Aika Fujiwara

View File

@ -1,809 +0,0 @@
FORMAT: 1A
HOST: https://api.jikan.moe/v3
# Jikan
[Jikan](https://jikan.moe) is an **Unofficial** MyAnimeList API. It scrapes the website to satisfy the need for an API - which MyAnimeList lacks.
The word Jikan literally translates to Time in Japanese (時間). And that's what this API saves you of. ;)
Notice: Jikan does not support authenticated requests. You can not update your lists.
⚡ Jikan is powered thanks to all its [backers](https://github.com/jikan-me/jikan#sugoi-%E3%81%99%E3%81%94%E3%81%84-backers)! 🙏 [[Become a backer]](https://patreon.com/jikan)
##
**API Path:** `https://api.jikan.moe/v3`
**API Version**: `v3.4`
[Status](https://status.jikan.moe) | [Report an Issue](https://github.com/jikan-me/jikan-rest/issues/new) | **[Discord](http://discord.jikan.moe)**
# Information
## Links
- [Jikan.moe](https://jikan.moe)
- [About](https://jikan.moe/about)
- [Stuff using Jikan](https://jikan.moe/showcase)
## Wrappers
Wrappers are available in more than 10 different languages, see the [Wrapper List](https://github.com/jikan-me/jikan#wrappers)
## Rate Limiting
Daily Limit: **Unlimited**
- **30 requests** / minute
- **2 requests** / second
**Note: Cached requests are NOT throttled**
## Bulk Requests
This API serves as a purpose for apps/projects that are user based and make a nominal amount of requests.
⚠️ If you're using the service for the sake of populating data/making your own database;
- You are breaching [MyAnimeList's Terms Of Service](https://myanimelist.net/membership/terms_of_use). **You are responsible for what you're doing.**
- **You MUST use a delay of 4 (FOUR) SECONDS between each request**
- Requesting from multiple servers/IPs is being cheeky and is **NOT** allowed
- **ABUSING THE API WILL RESULT IN GETTING BLOCKED FROM THE SERVICE**
If you're not comfortable being that restrictive, consider setting up your own Jikan REST API - It's super easy.
- [Jikan REST API - GitHub](https://github.com/jikan-me/jikan-rest)
- [Jikan REST API - Docker](https://github.com/jikan-me/jikan-docker)
## Disclaimer
- Jikan is not affiliated with MyAnimeList.net
- Jikan is a **free**, open-source API. Use it responsibly!
## JSON Notes
- Any property (except arrays) whose value does not exist or is undetermined, will be `null`
- Any array property whose value does not exist or is undetermined, will be **empty**
- Any `score` property whose value does not exist or is undetermined, will be `0`
- All dates and timestamps are returned in **ISO8601** format and in **UTC**
## Caching
By "caching", we refer to the data parsed from MyAnimeList that is cached temporarily on our servers for better performance.
All requests by default are cached for **24 hours** except for a few API endpoints which have their own unique cache expiry time.
Request | Cache TTL
:-------------- | :--------
All (Default) | 24 hours
Meta | 5 minutes
User | 5 minutes
Search | 120 hours (5 days)
The following Response Headers will detail cache information
Header | Remarks
:--------------------- | :--------
`Expires` | Expiry timestamp for the cache
`X-Request-Cached` | (boolean) Is the request cached?
`X-Request-Cache-Ttl` | (integer) Cache Time-To-Live in seconds
**FAQ: Why is `X-Request-Cache-Ttl` negative?**
If the cache expires, it queues a job in the background to update the cache.
So you're getting stale cache until the cache update completes.
## Allowed HTTP(s) requests
<pre>
GET: All requests are done via GET
</pre>
**The Jikan REST API does not provide authenticated requests for MyAnimeList.**
This means you can not use it to update your anime/manga lists.
**Reasons:**
- Why on earth would you send your credentials to a 3rd party API?
- MyAnimeList will block our IP after multiple failed login attempts
However, do not fret. This is possible via their own website. [Read the Specification](https://github.com/jikan-me/jikan-auth/blob/master/SPECIFICATION.md)
Furthermore, [JikanAuth](https://github.com/jikan-me/jikan-auth) is a PHP API which you can use to update your lists - it implements the **Specification** above. So feel free to come up with your own client-side solution.
# HTTP Response
- 200 `OK` - the request was successful.
- 304 `Not Modified` - You have the latest data
- 400 `Bad Request` - Youve made an invalid request
- 404 `Not Found` - Resource not found, i.e MyAnimeList responded with a 404
- 405 `Method Not Allowed` - requested method is not supported for resource
- 429 `Too Many Requests` - You are being rate limited or Jikan is being rate limited by MyAnimeList (either is specified in the error message)
- 500 `Internal Server Error` - Something is not working on our end, please open a Github issue by clicking on the generated `report_url`
- 503 `Service Unavailable` - Something is not working on MyAnimeLists end. MyAnimeList is either down/unavailable or is refusing to connect
# JSON Error Response
This is a typical error response
```
{
"status": 404,
"type": "BadResponseException",
"message": "Resource does not exist"
"error": "Something Happened"
}
```
Property | Remarks
:-------------- | :--------
`status` | HTTP Status returned
`type` | `Exception` generated from the PHP API
`message` | Appropriate error message from the REST API
`error` | Error response from the PHP API
`report_url` (fatal errors only) | Clicking on this would redirect you to a generated GitHub Issue
# Cache Validation
- All requests return a `ETag` header which is an md5 hash of the content.
- You can use this hash to verify if there's new or updated content by supplying it as the value for the `If-None-Match` header.
- You will get a `304` HTTP response if your value and the data on Jikan matches.
- If there's new/updated content, you'll get a normal `200` HTTP response with the updated content.
### How To Use
1. Use the `ETag` value as a header value for `If-None-Match` for future requests
2. If the content has changed, you will get a `304 - Not Modified` header response, otherwise `200 - OK`
![Cache Validation](https://i.imgur.com/925ozVn.png "Cache Validation")
## Anime [/anime/{id}/{request}/{parameter}]
A single anime object with all its details
**Endpoint Path:** `/anime/{id}(/request)`
### Requests
| Request | Parameter | Description |
| ------------- | ------------- | ------------- |
| `/` | N/A | Resource object with all it's details |
| `/characters_staff` | N/A | List of character and staff members |
| `/episodes` | Page number (integer) | List of episodes |
| `/news` | N/A | List of Related news |
| `/pictures` | N/A | List of Related pictures |
| `/videos` | N/A | List of Promotional Videos & episodes (if any) |
| `/stats` | N/A | Related statistical information |
| `/forum` | N/A | List of Related forum topics |
| `/moreinfo` | N/A | A string of more information (if any) |
| `/reviews` | Page number (integer) | List of Reviews written by users |
| `/recommendations` | N/A | List of Recommendations and their weightage made by users |
| `/userupdates` | Page number (integer) | List of the latest list updates made by users |
#### Remarks
##### `/episodes`
- The field `episodes_last_page` will tell you the last page of the paginated episodes list.
- The episodes page on MyAnimeList get paginated after 100 episodes. If there's an anime with more than 100 episodes, you'll have to use the parameter.
##### `/reviews`
- Only 20 items are shown per page for reviews
### Examples
- `/anime/1/characters_staff` - Returns the list of characters and staff
- `/anime/1/episodes` - Defaults to the 1st page
- `/anime/1/episodes/1` - Same as above
- `/anime/1/episodes/2` - Returns 2nd page if there's any
### Fetch Resource [GET]
+ Parameters
+ id (required, Number, `1`) ... MyAnimeList ID of the anime
+ request (optional, String, `episodes`) ... More details such as characters, staff, episodes
+ parameter (optional, Number, `2`) ... Anime with more than 100 episodes are paginated, hence this parameter is required.
+ Response 200 (application/json)
[
]
## Manga [/manga/{id}/{request}]
A single manga object with all its details
### Requests
| Request | Parameter | Description |
| ------------- | ------------- | ------------- |
| characters | N/A | Fetches the list of characters & staff members of the manga |
| news | N/A | News related to the item |
| pictures | N/A | Pictures related to the item |
| stats | N/A | Statistical information related to the item |
| forum | N/A | Forum topics related to the item |
| moreinfo | N/A | More info related to the item |
| reviews | Page number (integer) | Reviews written by users |
| recommendations | N/A | Recommendations and their weightage made by users |
| userupdates | Page number (integer) | Latest list updates made by users |
### Example Calls
- `/manga/1/characters` Returns the list of characters and staff
#### Remarks
- Only 20 items are shown per page for reviews
### Manga Request Example+Schema [GET]
+ Parameters
+ id (required, Number, `1`) ... Returns the Character details from that the ID
+ request (optional, String, `characters`) ... More details such as characters
+ Response 200 (application/json)
[
]
## Person [/person/{id}/{request}]
A single person object with all its details
### Requests
| Request | Parameter | Description |
| ------------- | ------------- | ------------- |
| pictures | N/A | Pictures related to the item |
### Person Request Example+Schema [GET]
+ Parameters
+ id (required, Number, `1`) ... Returns the Person details from that the ID
+ request (optional, String, `pictures`) ... Pictures related to the item
+ Response 200 (application/json)
[
]
## Character [/character/{id}/{request}]
A single character object with all its details
### Requests
| Request | Parameter | Description |
| ------------- | ------------- | ------------- |
| pictures | N/A | Pictures related to the item |
### Character Request Example+Schema [GET]
+ Parameters
+ id (required, Number, `1`) ... Returns the Character details from that the ID
+ request (optional, String, `pictures`) ... Pictures related to the item
+ Response 200 (application/json)
[
]
## Search [/search/{type}?q=Fate/Zero&page=1]
Search results for the query
**NOTE: MyAnimeList only processes queries with a minimum of 3 letters.** However, the search function can be used without `q`! Check examples below for more details.
### Parameters
| Parameter | Argument | Description |
| ------------- | ------------- | ------------- |
| type | anime, manga, person, character | Specify where to search |
| page | INTEGER | Page number of the results |
### Advanced Search Parameters (Anime & Manga)
**Note:** These are search filters which have to be passed as GET `key=value`
| Parameter | Argument | Description |
| ------------- | ------------- | ------------- |
| q | STRING | For UTF8 characters, percentage encoded and queries including back slashes |
| page | INTEGER | Page number |
| type | **See Enums Below** | Filter type of results |
| status | **See Enums Below** | Filter status of results |
| rated | **See Enums Below** | Filter age rating of results |
| genre | **See Enums Below** | Filter by genre ID(s) |
| score | FLOAT : 0.0-10.0 | Filter score of results |
| start_date | `yyyy-mm-dd` | Filter start date of results |
| end_date | `yyyy-mm-dd` | Filter end date of results |
| genre_exclude | boolean : 0/1 | To exclude/include the `genre` you added in your request |
| limit | INTEGER | Limits item results to the number specified |
| order_by | **See Enums Below** | Order results with respect to a property |
| sort | **See Enums Below** | Sort `order_by` (Default is `descending`) |
| producer | INTEGER | MAL ID of the producer |
| magazine | INTEGER | MAL ID of the magazine |
| letter | UTF8 Character | Search anime or manga by the letter/character it starts with |
#### Enums
##### `type`
| Anime Types | Manga Types |
| :------------ | :---------- |
| `tv` | `manga` |
| `ova` | `novel` |
| `movie` | `oneshot` |
| `special` | `doujin` |
| `ona` | `manhwa` |
| `music` | `manhua` |
##### `status`
| Anime Status | Manga Status |
| :------------ | :---------- |
| `airing` | `publishing` |
| `completed` | `completed` |
| `complete` (alias) | `complete` (alias) |
| `to_be_aired` | `to_be_published` |
| `tba` (alias) | `tbp` (alias) |
| `upcoming` (alias) | `upcoming` (alias) |
##### `rated`
Anime ratings are based on MyAnimeList's rating system.
Read more about them [here](https://myanimelist.net/info.php?go=mpaa)
| Anime Search | Remarks |
| :------------ | :----- |
| `g` | **G** - All Ages |
| `pg` | **PG** - Children |
| `pg13` | **PG-13** - Teens 13 or older |
| `r17` | **R** - 17+ recommended (violence & profanity) |
| `r` | **R+** - Mild Nudity (may also contain violence & profanity) |
| `rx` | **Rx** - Hentai (extreme sexual content/nudity) |
##### `order_by`
| Anime Search | Manga Search |
| :------------ | :---------- |
| `title` | `title` |
| `start_date` | `start_date` |
| `end_date` | `end_date` |
| `score` | `score` |
| `type` | `type` |
| `members` | `members` |
| `id` | `id` |
| `episodes` | `chapters` |
| `rating` | `volumes` |
##### `sort`
| Anime & Manga Sort |
| :------------ |
| `ascending` |
| `asc` (alias) |
| `descending` |
| `desc` (alias) |
##### `genre`
| Anime Genre | Manga Genre |
| :------------ | :---------- |
| **Action:** `1` | **Action:** `1` |
| **Adventure:** `2` | **Adventure:** `2` |
| **Cars:** `3` | **Cars:** `3` |
| **Comedy:** `4` | **Comedy:** `4` |
| **Avante Garde:** `5` | **Avante Garde:** `5` |
| **Demons:** `6` | **Demons:** `6` |
| **Mystery:** `7` | **Mystery:** `7` |
| **Drama:** `8` | **Drama:** `8` |
| **Ecchi:** `9` | **Ecchi:** `9` |
| **Fantasy:** `10` | **Fantasy:** `10` |
| **Game:** `11` | **Game:** `11` |
| **Hentai:** `12` | **Hentai:** `12` |
| **Historical:** `13` | **Historical:** `13` |
| **Horror:** `14` | **Horror:** `14` |
| **Kids:** `15` | **Kids:** `15` |
| **Martial Arts:** `17` | **Martial Arts:** `17` |
| **Mecha:** `18` | **Mecha:** `18` |
| **Music:** `19` | **Music:** `19` |
| **Parody:** `20` | **Parody:** `20` |
| **Samurai:** `21` | **Samurai:** `21` |
| **Romance:** `22` | **Romance:** `22` |
| **School:** `23` | **School:** `23` |
| **Sci Fi:** `24` | **Sci Fi:** `24` |
| **Shoujo:** `25` | **Shoujo:** `25` |
| **Girls Love:** `26` | **Girls Love:** `26` |
| **Shounen:** `27` | **Shounen:** `27` |
| **Boys Love:** `28` | **Boys Love**: `28` |
| **Space:** `29` | **Space:** `29` |
| **Sports:** `30` | **Sports:** `30` |
| **Super Power:** `31` | **Super Power:** `31` |
| **Vampire:** `32` | **Vampire:** `32` |
| **Harem:** `35` | **Harem:** `35` |
| **Slice Of Life:** `36` | **Slice Of Life:** `36` |
| **Supernatural:** `37` | **Supernatural:** `37` |
| **Military:** `38` | **Military:** `38` |
| **Police:** `39` | **Police:** `39` |
| **Psychological:** `40` | **Psychological:** `40` |
| **Suspense:** `41` | **Seinen:** `41` |
| **Seinen:** `42` | **Josei:** `42` |
| **Josei:** `43` | **Doujinshi:** `43` |
| | **Gender Bender:** `44` |
| | **Suspense:** `45` |
| **Award Winning**: `46` | **Award Winning**: `46` |
| **Gourmet**: `47` | **Gourmet**: `47` |
| **Work Life**: `48` | **Work Life**: `48` |
| **Erotica**: `49` | **Erotica**: `49`
#### Examples
- `/search/manga?q=Grand%20Blue&page=1` - Search manga for 'Grand Blue'
- `/search/anime?q=Fate/Zero&page=1` - Search anime for 'Fate/Zero'
- `/search/people/?q=Sawashiro&limit=3` - Search people for 'Sawashiro', limit to 3 results
- `/search/anime?q=Boku&page=1&genre=12&genre_exclude=0` - Filter out NSFW entries by using the `genre` and `genre_exclude` parameters
In many cases, if you want to filter results by a condition, you can provide an empty query (`q=`) with the `order_by` and `sort` parameters:
- `/search/anime?q=&order_by=members&sort=desc&page=1` - Search for all anime, ordered by popularity (members, descending)
- `/search/anime?q=&page=1&genre=1,10&order_by=start_date&sort=desc` - Search for all anime which have both Genre 1 and 10 (Action, Fantasy), order by most recently released
#### Remarks
- The `last_page` field is the same as what is avaiable on the MAL Search page, its often paginated to 20 pages if there are too many results.
### Search Request Example+Schema [GET]
+ Parameters
+ type (required, String, `anime`) ... Returns result from anime search
+ Response 200 (application/json)
[
]
## Season [/season/{year}/{season}]
Anime of the specified season
**Note:** Not using parameters will return the current season's anime listing
### Parameters
| Parameter | Argument | Description |
| ------------- | ------------- | ------------- |
| year | Integer: Year | Specify the year |
| season | `summer` `spring` `fall` `winter` | Specify the season |
### Season Request Example+Schema [GET]
+ Parameters
+ year (optional, Integer, `2018`) ... Returns anime of the year
+ season (optional, String, `winter`) ... Returns anime of the season
+ Response 200 (application/json)
[
]
## Season Archive [/season/archive]
All the years & their respective seasons that can be parsed from MyAnimeList
### Season Archive Request Example+Schema [GET]
+ Response 200 (application/json)
[
]
## Season Later [/season/later]
Anime that have been announced for the upcoming seasons
### Season Later Request Example+Schema [GET]
+ Response 200 (application/json)
[
]
## Schedule [/schedule/{day}]
Anime schedule of the week or specified day
**Note:** If you don't pass the `day` parameter, it'll return the schedule for **all** days of the week
### Parameters
| Parameter | Argument | Description |
| ------------- | ------------- | ------------- |
| day (optional) | `monday` `tuesday` `wednesday` `thursday` `friday` `saturday` `sunday`, `other` **(v3)**, `unknown` **(v3)** | Anime scheduled for that specific day |
### Schedule Request Example+Schema [GET]
+ Parameters
+ day (optional, String, `monday`) ... Returns scheduled anime of that specific day
+ Response 200 (application/json)
[
]
## Top [/top/{type}/{page}/{subtype}]
Top items on MyAnimeList
**Note:** `subtype` returns a filtered top list of a type `type` item. For example, the top Anime (type) movies (subtype)
**Note 2:** `subtype` is only for `anime` and `manga` types.
**Note 3:** Date properties are returned in string as they only consist of the month and year - which is not appropriate for ISO8601
### Parameters
| Parameter | Argument | Description |
| ------------- | ------------- | ------------- |
| type | `anime` `manga`, `people` (v3+), `characters` (v3+) | Top items of this type |
| page (optional) | INTEGER | The Top page on MyAnimeList is paginated offers 50 items per page |
| subtype (optional) | **Anime:** `airing` `upcoming` `tv` `movie` `ova` `special` **Manga:** `manga` `novels` `oneshots` `doujin` `manhwa` `manhua` **Both:** `bypopularity` `favorite` |
### Top Request Example+Schema [GET]
+ Parameters
+ type (required, String, `anime`) ... Returns top items of this type
+ page (optional, Integer, `1`) ... Pagination support
+ subtype (optional, String, `upcoming`) ... Returns top items of this type filtered by their subtypes
+ Response 200 (application/json)
[
]
## Genre [/genre/{type}/{genre_id}/{page}]
Anime/Manga items of the genre
**Note:** Genres with their respective IDs are listed [here]()
### Parameters
| Parameter | Argument | Description |
| ------------- | ------------- | ------------- |
| type | `anime` `manga` | Genre of this type |
| genre_id | INTEGER | Genre ID from MyAnimeList - [Genre Mapping]() |
| page (optional) | |
### Genre Request Example+Schema [GET]
+ Parameters
+ type (required, String, `anime`) ... Returns anime/manga items of this genre
+ genre_id (optional, Integer, `1`) ... Genre ID
+ page (optional, Integer, `1`) ... Pagination
+ Response 200 (application/json)
[
]
## Producer [/producer/{producer_id}/{page}]
Anime by this Producer/Studio/Licensor
### Parameters
| Parameter | Argument | Description |
| ------------- | ------------- | ------------- |
| producer_id | INTEGER | Producer ID from MyAnimeList |
| page (optional) | |
### Producer Request Example+Schema [GET]
+ Parameters
+ producer_id (optional, Integer, `1`) ... Producer ID
+ page (optional, Integer, `1`) ... Pagination
+ Response 200 (application/json)
[
]
## Magazine [/magazine/{magazine_id}/{page}]
Manga by this Magazine/Serializer/Publisher
### Parameters
| Parameter | Argument | Description |
| ------------- | ------------- | ------------- |
| magazine_id | INTEGER | Magazine ID from MyAnimeList |
| page (optional) | |
### Magazine Request Example+Schema [GET]
+ Parameters
+ magazine_id (optional, Integer, `1`) ... Magazine ID
+ page (optional, Integer, `1`) ... Pagination
+ Response 200 (application/json)
[
]
## User [/user/{username}/{request}/{argument}]
User related data
**Note:** About is returned in HTML as MyAnimeList allows custom "about" sections for users that can consist of images, formatting, etc.
**Note 2:** Anime & Manga Lists are paginated. Only 300 items are returned per page.
### Parameters
| Parameter | Argument | Description |
| ------------- | ------------- | ------------- |
| username | string | Username on MyAnimeList |
| request | `profile`, `history`, `friends`, `animelist`, `mangalist` |
| data (optional) | Additional data for the requests |
#### Data
| Data | Argument | Description |
| ------------- | ------------- | ------------- |
| history | `anime`, `manga` | Returns both combined if neither are passed |
| friends | INTEGER | Pagination support; Status 404 if there's no friends on the page |
| animelist | **See Enums Below** |
| mangalist | **See Enums Below** |
#### User List Filter
| Anime List `/animelist` | Manga List `/mangalist` |
| :------------ | :---------- |
| `/` | `/` |
| `/all` (alias) | `/all` (alias) |
| `/watching` | `/reading` |
| `/completed` | `/completed` |
| `/onhold` | `/onhold` |
| `/dropped` | `/dropped` |
| `/plantowatch` | `/plantoread` |
| `/ptw` (alias) | `/ptr` |
### Advanced User List Parameters (Anime & Manga)
**Note:** These are search filters which have to be passed as GET `key=value`
| Parameter | Argument | Description |
| ------------- | ------------- | ------------- |
| `search` | STRING | Return items in your list matching the string |
| `q` (alias) | STRING | Return items in your list matching the string |
| `page` (alias) | INTEGER | Pass page number as a `key=value` |
| `sort` | **See Enums Below** | Sort `order_by` (Default is `descending`) |
### Advanced User List Parameters (ANIME)
| Parameter | Argument | Description |
| ------------- | ------------- | ------------- |
| `order_by` | **See Enums Below** | Order items with respect to a property |
| `order_by2` | **See Enums Below** | Order items with respect to a second property |
| `aired_from` | `yyyy-mm-dd` | Filter Anime that have aired from this date |
| `aired_to` | `yyyy-mm-dd` | Filter Anime that have aired till this date |
| `producer` | Integer | Filter Anime by this Producer ID |
| `year` | Integer: Year | Filter anime from a year |
| `season` | `summer` `spring` `fall` `winter` | Filter anime from a season (require `year`) |
| `airing_status` | **See Enums Below** | Filter Anime with a status |
### Advanced User List Parameters (MANGA)
| Parameter | Argument | Description |
| ------------- | ------------- | ------------- |
| `order_by` | **See Enums Below** | Order items with respect to a property |
| `order_by2` | **See Enums Below** | Order items with respect to a second property |
| `published_from` | `yyyy-mm-dd` | Filter Manga that have published from this date |
| `published_to` | `yyyy-mm-dd` | Filter Manga that have published till this date |
| `magazine` | Integer | Filter Manga by this Magazine ID |
| `publishing_status` | **See Enums Below** | Filter Manga with a status |
Regarding `yyyy-mm-dd` dates, you can search for only the year or only the year and the month as well.
Just pass it as `yyyy-00-00` or `yyyy-mm-00`. e.g `2018-00-00`, `2018-12-00`
#### Enums
##### `order_by` & `order_by2`
| Anime List | Manga List |
| :------------ | :---------- |
| `title` | `title` |
| `finish_date` | `finish_date` |
| `start_date` | `start_date` |
| `score` | `score` |
| `last_updated` | `last_updated` |
| `type` | `type` |
| `rated` | `` |
| `rewatch` | `` |
| `rewatch_value` (alias) | `` |
| `priority` | `priority` |
| `progress` | `progress` (`chapters_read`) |
| `episodes_watched` (alias) | `chapters_read` (alias) |
| | `volumes_read` |
| `storage` | `` |
| `air_start` | `publish_start` |
| `air_end` | `publish_end` |
| `status` | `status` |
##### `sort`
| Anime & Manga Sort |
| :------------ |
| `ascending` |
| `asc` (alias) |
| `descending` |
| `desc` (alias) |
##### `airing_status` & `publishing_status`
| Anime Airing Status | Manga Publishing Status |
| :------------ | :------------ |
| `airing` | `publishing` |
| `finished` | `finished` |
| `complete` (alias) | `complete` (alias) |
| `to_be_aired` | `to_be_published` |
| `not_yet_aired` (alias) | `not_yet_published` (alias) |
| `tba` (alias) | `tbp` (alias) |
| `nya` (alias) | `nyp` (alias) |
#### Examples
- `/user/nekomata1037` - Parses Profile
- `/user/nekomata1037/profile` (alias)
- `/user/nekomata1037/history` - Parses user history (anime+manga)
- `/user/nekomata1037/history/anime` - Parses user history (anime only)
- `/user/nekomata1037/friends` - Parses user friends
The request below will return 404 because I don't have that many friends on MAL to generate a second page.
`/user/nekomata1037/friends/2` - Parses user friends (from page 2)
**Anime & Manga Lists**
Lists are paginated (300 items per page).
- `/user/nekomata1037/animelist/all` - All anime in user list
- `/user/nekomata1037/animelist/all/2` - Page 2
- `/user/nekomata1037/mangalist/reading` - Manga that I'm currently reading
### User Request Example+Schema [GET]
+ Parameters
+ username (required, string, `Nekomata1037`) ... Username on MyAnimeList
+ request (optional, string, `history`) ... Request
+ argument (optional, string, `anime`) ... Request argument
+ Response 200 (application/json)
[
]
## Club [/club/{id}/{request}]
A single club object with all its details
### Requests
| Request | Parameter | Description |
| ------------- | ------------- | ------------- |
| members | Page (INTEGER) | Fetches list of club members |
### Example Calls
- `/club/1` // Returns club information
- `/club/1/members/1` // Returns list of club members
#### Remarks
- Only 35 items are shown per page for members
### Club Request Example+Schema [GET]
+ Parameters
+ id (required, Number, `1`) ... Returns the Club details from that the ID
+ request (optional, String, `members`) ... Return club members
+ Response 200 (application/json)
[
]
## Meta [/meta/{request}/{type}/{period}]
Requests related to meta information regarding the Jikan REST Instance.
Such as the most requested endpoints for a specific period, or just status on the REST API.
### Parameters
| Parameter | Argument | Description |
| ------------- | ------------- | ------------- |
| request | `requests` `status` | |
| type | `anime` `manga` `character` `person` `search` `top` `schedule` `season` | This is only for the `requests` endpoint |
| period | `today` `weekly` `monthly` | This is only for the `requests` endpoint |
| offset | int | 1,000 requests are shown per page, you can use the offset to show more |
### Meta Request Example+Schema [GET]
+ Parameters
+ request (required, String, `requests`) ...
+ type (required, String, `anime`) ...
+ period (required, String, `today`) ...
+ Response 200 (application/json)
[
]

134
app/Anime.php Normal file
View File

@ -0,0 +1,134 @@
<?php
namespace App;
use App\Http\HttpHelper;
use Jenssegers\Mongodb\Eloquent\Model;
use Jikan\Helper\Media;
use Jikan\Helper\Parser;
use Jikan\Jikan;
use Jikan\Model\Common\YoutubeMeta;
use Jikan\Request\Anime\AnimeRequest;
class Anime extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'mal_id','url','title','title_english','title_japanese','title_synonyms', 'images', 'type','source','episodes','status','airing','aired','duration','rating','score','scored_by','rank','popularity','members','favorites','synopsis','background','premiered','broadcast','related','producers','licensors','studios','genres', 'explicit_genres', 'themes', 'demographics', 'opening_themes','ending_themes'
];
/**
* The accessors to append to the model's array form.
*
* @var array
*/
protected $appends = ['season', 'year', 'themes'];
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'anime';
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = [
'_id', 'premiered', 'opening_themes', 'ending_themes', 'request_hash', 'expiresAt'
];
public function setSeasonAttribute($value)
{
$this->attributes['season'] = $this->getSeasonAttribute();
}
public function getSeasonAttribute()
{
$premiered = $this->attributes['premiered'];
if (empty($premiered)
|| is_null($premiered)
|| !preg_match('~(Winter|Spring|Summer|Fall|)\s([\d+]{4})~', $premiered)
) {
return null;
}
$season = explode(' ', $premiered)[0];
return strtolower($season);
}
public function setYearAttribute($value)
{
$this->attributes['year'] = $this->getYearAttribute();
}
public function getYearAttribute()
{
$premiered = $this->attributes['premiered'];
if (empty($premiered)
|| is_null($premiered)
|| !preg_match('~(Winter|Spring|Summer|Fall|)\s([\d+]{4})~', $premiered)
) {
return null;
}
return (int) explode(' ', $premiered)[1];
}
public function setBroadcastAttribute($value)
{
$this->attributes['year'] = $this->getBroadcastAttribute();
}
public function getBroadcastAttribute()
{
$broadcastStr = $this->attributes['broadcast'];
if (!preg_match('~(.*) at (.*) \(~', $broadcastStr, $matches)) {
return [
'day' => null,
'time' => null,
'timezone' => null,
'string' => $broadcastStr
];
}
if (preg_match('~(.*) at (.*) \(~', $broadcastStr, $matches)) {
return [
'day' => $matches[1],
'time' => $matches[2],
'timezone' => 'Asia/Tokyo',
'string' => $broadcastStr
];
}
return [
'day' => null,
'time' => null,
'timezone' => null,
'string' => null
];
}
public static function scrape(int $id)
{
$data = app('JikanParser')->getAnime(new AnimeRequest($id));
return HttpHelper::serializeEmptyObjectsControllerLevel(
json_decode(
app('SerializerV4')
->serialize($data, 'json'),
true
)
);
}
}

63
app/Character.php Normal file
View File

@ -0,0 +1,63 @@
<?php
namespace App;
use App\Http\HttpHelper;
use Jenssegers\Mongodb\Eloquent\Model;
use Jikan\Helper\Media;
use Jikan\Helper\Parser;
use Jikan\Jikan;
use Jikan\Model\Common\YoutubeMeta;
use Jikan\Request\Anime\AnimeRequest;
use Jikan\Request\Character\CharacterRequest;
class Character extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'mal_id', 'url', 'name', 'name_kanji', 'nicknames', 'about', 'member_favorites', 'images', 'animeography', 'mangaography', 'voice_actors'
];
/**
* The accessors to append to the model's array form.
*
* @var array
*/
protected $appends = ['images', 'favorites'];
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'characters';
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = [
'_id', 'trailer_url', 'premiered', 'opening_themes', 'ending_themes', 'images', 'member_favorites'
];
public function getFavoritesAttribute()
{
return $this->attributes['member_favorites'];
}
public static function scrape(int $id)
{
$data = app('JikanParser')->getCharacter(new CharacterRequest($id));
return json_decode(
app('SerializerV4')
->serialize($data, 'json'),
true
);
}
}

55
app/Club.php Normal file
View File

@ -0,0 +1,55 @@
<?php
namespace App;
use App\Http\HttpHelper;
use Jenssegers\Mongodb\Eloquent\Model;
use Jikan\Request\Anime\AnimeRequest;
use Jikan\Request\Club\ClubRequest;
class Club extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'mal_id', 'url', 'images', 'title', 'members_count', 'pictures_count', 'category', 'created', 'type', 'staff', 'anime_relations', 'manga_relations', 'character_relations'
];
/**
* The accessors to append to the model's array form.
*
* @var array
*/
protected $appends = ['images'];
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'clubs';
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = [
'_id', 'request_hash', 'expiresAt', 'images'
];
public static function scrape(int $id)
{
$data = app('JikanParser')->getClub(new ClubRequest($id));
return json_decode(
app('SerializerV4')
->serialize($data, 'json'),
true
);
}
}

View File

@ -0,0 +1,172 @@
<?php
namespace App\Console\Commands\Indexer;
use App\Exceptions\Console\CommandAlreadyRunningException;
use App\Exceptions\Console\FileNotFoundException;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
/**
* Class AnimeIndexer
* @package App\Console\Commands\Indexer
*/
class AnimeIndexer extends Command
{
/**
* The name and signature of the console command.
*`
* @var string
*/
protected $signature = 'indexer:anime
{--failed : Run only entries that failed to index last time}
{--resume : Resume from the last position}
{--reverse : Start from the end of the array}
{--index=0 : Start from a specific index}
{--delay=3 : Set a delay between requests}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Index all anime';
/**
* @var array
*/
private array $ids;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return void
* @throws FileNotFoundException
*/
public function handle()
{
$failed = $this->option('failed') ?? false;
$resume = $this->option('resume') ?? false;
$reverse = $this->option('reverse') ?? false;
$delay = $this->option('delay') ?? 3;
$index = $this->option('index') ?? 0;
$index = (int)$index;
$delay = (int)$delay;
$this->info("Info: AnimeIndexer uses seanbreckenridge/mal-id-cache fetch available MAL IDs and updates/indexes them\n\n");
if ($failed && Storage::exists('indexer/indexer_anime.save')) {
$this->ids = $this->loadFailedMalIds();
}
if (!$failed) {
$this->ids = $this->fetchMalIds();
}
// start from the end
if ($reverse) {
$this->ids = array_reverse($this->ids);
}
// Resume
if ($resume && Storage::exists('indexer/indexer_anime.save')) {
$index = (int)Storage::get('indexer/indexer_anime.save');
$this->info("Resuming from index: {$index}");
}
// check if index even exists
if ($index > 0 && !isset($this->ids[$index])) {
$index = 0;
$this->warn('Invalid index; set back to 0');
}
// initialize and index
Storage::put('indexer/indexer_anime.save', 0);
echo "Loading MAL IDs\n";
$count = count($this->ids);
$failedIds = [];
$success = [];
echo "{$count} entries available\n";
for ($i = $index; $i <= ($count - 1); $i++) {
$id = $this->ids[$i];
$url = env('APP_URL') . "/v4/anime/{$id}";
echo "Indexing/Updating " . ($i + 1) . "/{$count} {$url} [MAL ID: {$id}] \n";
try {
$response = json_decode(file_get_contents($url), true);
if (isset($response['error']) && $response['status'] != 404) {
echo "[SKIPPED] Failed to fetch {$url} - {$response['error']}\n";
$failedIds[] = $id;
Storage::put('indexer/indexer_anime.failed', json_encode($failedIds));
}
sleep($delay);
} catch (\Exception $e) {
echo "[SKIPPED] Failed to fetch {$url}\n";
$failedIds[] = $id;
Storage::put('indexer/indexer_anime.failed', json_encode($failedIds));
}
$success[] = $id;
Storage::put('indexer/indexer_anime.save', $i);
}
Storage::delete('indexer/indexer_anime.save');
echo "---------\nIndexing complete\n";
echo count($success) . " entries indexed or updated\n";
echo count($failedIds) . " entries failed to index or update. Re-run with --failed to requeue failed entries only\n";
}
/**
* @return array
* @url https://github.com/seanbreckenridge/mal-id-cache
*/
private function fetchMalIds() : array
{
$this->info("Fetching MAL ID Cache https://raw.githubusercontent.com/seanbreckenridge/mal-id-cache/master/cache/anime_cache.json...\n");
$ids = json_decode(
file_get_contents('https://raw.githubusercontent.com/seanbreckenridge/mal-id-cache/master/cache/anime_cache.json'),
true
);
$this->ids = $ids['sfw'] + $ids['nsfw']; // merge
Storage::put('indexer/anime_mal_id.json', json_encode($this->ids));
return json_decode(Storage::get('indexer/anime_mal_id.json'));
}
/**
* @return array
* @throws FileNotFoundException
*/
private function loadFailedMalIds() : array
{
if (!Storage::exists('indexer/indexer_anime.failed')) {
throw new FileNotFoundException('"indexer/indexer_anime.failed" does not exist');
}
return json_decode(Storage::get('indexer/indexer_anime.failed'));
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Console\Commands\Indexer;
use App\Http\HttpHelper;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Jikan\Request\Schedule\ScheduleRequest;
class AnimeScheduleIndexer extends Command
{
/**
* The name and signature of the console command.
*`
* @var string
*/
protected $signature = 'indexer:anime-schedule';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Index anime schedule';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
echo "Note: AnimeScheduleIndexer makes sure anime currently airing are upto update so the schedules endpoint returns fresh information\n\n";
/**
* Schedule
*/
echo "Fetching Schedule...\n";
$results = \json_decode(
app('SerializerV4')->serialize(
app('JikanParser')
->getSchedule(new ScheduleRequest()),
'json'
),
true
);
if (HttpHelper::hasError($results)) {
echo "FAILED: {$results->original['error']}\n";
return;
}
$anime = [];
foreach ($results as $day) {
foreach ($day as $entry) {
$anime[] = $entry;
}
}
$i = 1;
$itemCount = count($anime);
echo "Anime currently airing: {$itemCount} entries\n";
foreach ($anime as $entry) {
$url = env('APP_URL') . "/v4/anime/{$entry['mal_id']}";
file_get_contents($url);
sleep(3); // prevent rate-limit
echo "Updating {$i}/{$itemCount} \r";
try {
} catch (\Exception $e) {
echo "[SKIPPED] Failed to fetch {$url}";
}
$i++;
}
echo str_pad("Indexing complete", 10).PHP_EOL;
}
}

View File

@ -0,0 +1,188 @@
<?php
namespace App\Console\Commands\Indexer;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Jikan\Request\Genre\AnimeGenresRequest;
use Jikan\Request\Genre\MangaGenresRequest;
use Jikan\Request\Magazine\MagazinesRequest;
use Jikan\Request\Producer\ProducersRequest;
use Jikan\Request\SeasonList\SeasonListRequest;
class CommonIndexer extends Command
{
/**
* The name and signature of the console command.
*`
* @var string
*/
protected $signature = 'indexer:common';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Index common endpoints: Producers, Magazines, Anime & Manga Genres';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
echo "Note: If an entry already exists, it will be updated instead.\n\n";
/**
* Producers
*/
echo "Indexing Producers...\n";
$results = \json_decode(
app('SerializerV4')->serialize(
app('JikanParser')
->getProducers(new ProducersRequest()),
'json'
),
true
)['producers'];
if (HttpHelper::hasError($results)) {
echo "FAILED: {$results->original['error']}\n";
return;
}
$itemCount = count($results);
echo "Parsed {$itemCount} producers\n";
foreach ($results as $i => $item) {
$result = DB::table('producers')
->where('mal_id', $item['mal_id'])
->updateOrInsert(['request_hash'=>'request:producers:'.sha1($item['mal_id'].$item['name'])]+$item);
echo "Indexing {$i}/{$itemCount} \r";
}
/**
* Magazines
*/
echo "Indexing Magazines...\n";
$results = \json_decode(
app('SerializerV4')->serialize(
app('JikanParser')
->getMagazines(new MagazinesRequest()),
'json'
),
true
)['magazines'];
if (HttpHelper::hasError($results)) {
echo "FAILED: {$results->original['error']}\n";
return;
}
$itemCount = count($results);
echo "Parsed {$itemCount} magazines\n";
foreach ($results as $i => $item) {
$result = DB::table('magazines')
->where('mal_id', $item['mal_id'])
->updateOrInsert(['request_hash'=>'request:magazines:'.sha1($item['mal_id'].$item['name'])]+$item);
echo "Indexing {$i}/{$itemCount} \r";
}
/**
* Anime Genres
*/
echo "Indexing Anime Genres...\n";
$results = \json_decode(
app('SerializerV4')->serialize(
app('JikanParser')
->getAnimeGenres(new AnimeGenresRequest()),
'json'
),
true
);
if (HttpHelper::hasError($results)) {
echo "FAILED: {$results->original['error']}\n";
return;
}
$itemCount = count($results['genres']);
echo "Parsed {$itemCount} anime genres\n";
foreach ($results['genres'] as $i => $item) {
$result = DB::table('genres_anime')
->where('mal_id', $item['mal_id'])
->updateOrInsert(['request_hash'=>'request:anime_genres:'.sha1($item['mal_id'].$item['name'])]+$item);
echo "Indexing {$i}/{$itemCount} \r";
}
$itemCount = count($results['explicit_genres']);
echo "Parsed {$itemCount} anime explicit_genres\n";
foreach ($results['explicit_genres'] as $i => $item) {
$result = DB::table('explicit_genres_anime')
->where('mal_id', $item['mal_id'])
->updateOrInsert(['request_hash'=>'request:anime_explicit_genres:'.sha1($item['mal_id'].$item['name'])]+$item);
echo "Indexing {$i}/{$itemCount} \r";
}
$itemCount = count($results['themes']);
echo "Parsed {$itemCount} anime themes\n";
foreach ($results['themes'] as $i => $item) {
$result = DB::table('themes_anime')
->where('mal_id', $item['mal_id'])
->updateOrInsert(['request_hash'=>'request:anime_themes:'.sha1($item['mal_id'].$item['name'])]+$item);
echo "Indexing {$i}/{$itemCount} \r";
}
$itemCount = count($results['demographics']);
echo "Parsed {$itemCount} anime demographics\n";
foreach ($results['demographics'] as $i => $item) {
$result = DB::table('demographics_anime')
->where('mal_id', $item['mal_id'])
->updateOrInsert(['request_hash'=>'request:anime_demographics:'.sha1($item['mal_id'].$item['name'])]+$item);
echo "Indexing {$i}/{$itemCount} \r";
}
/**
* Manga Genres
*/
echo "Indexing Manga Genres...\n";
$results = \json_decode(
app('SerializerV4')->serialize(
app('JikanParser')
->getMangaGenres(new MangaGenresRequest()),
'json'
),
true
)['genres'];
if (HttpHelper::hasError($results)) {
echo "FAILED: {$results->original['error']}\n";
return;
}
$itemCount = count($results);
echo "Parsed {$itemCount} manga genres\n";
foreach ($results as $i => $item) {
$result = DB::table('genres_manga')
->where('mal_id', $item['mal_id'])
->updateOrInsert(['request_hash'=>'request:manga_genres:'.sha1($item['mal_id'].$item['name'])]+$item);
echo "Indexing {$i}/{$itemCount} \r";
}
echo str_pad("Indexing complete", 10).PHP_EOL;
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace App\Console\Commands\Indexer;
use App\Http\HttpHelper;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Jikan\Request\Schedule\ScheduleRequest;
use Jikan\Request\Seasonal\SeasonalRequest;
class CurrentSeasonIndexer extends Command
{
/**
* The name and signature of the console command.
*`
* @var string
*/
protected $signature = 'indexer:anime-current-season';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Index anime in current season';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
echo "Note: CurrentSeasonIndexer makes sure anime in current season are upto update so the /seasons/now endpoint returns fresh information\n\n";
/**
* Current Season
*/
echo "Fetching Current Season...\n";
$results = \json_decode(
app('SerializerV4')->serialize(
app('JikanParser')
->getSeasonal(new SeasonalRequest()),
'json'
),
true
);
if (HttpHelper::hasError($results)) {
echo "FAILED: {$results->original['error']}\n";
return;
}
$anime = $results['anime'];
$itemCount = count($anime);
echo "Anime in current season: {$itemCount} entries\n";
foreach ($anime as $i => $entry) {
$url = env('APP_URL') . "/v4/anime/{$entry['mal_id']}";
file_get_contents($url);
sleep(3); // prevent rate-limit
echo "Updating {$i}/{$itemCount} {$url} [{$entry['mal_id']} - {$entry['title']}] \n";
try {
} catch (\Exception $e) {
echo "[SKIPPED] Failed to fetch {$url}\n";
}
}
echo str_pad("Indexing complete", 100).PHP_EOL;
}
}

View File

@ -0,0 +1,240 @@
<?php
namespace App\Console\Commands\Indexer;
use App\Http\HttpHelper;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Jikan\Request\Genre\AnimeGenresRequest;
use Jikan\Request\Genre\MangaGenresRequest;
class GenreIndexer extends Command
{
/**
* The name and signature of the console command.
*`
* @var string
*/
protected $signature = 'indexer:genres';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Index Anime & Manga Genres';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
echo "Note: If an entry already exists, it will be updated instead.\n\n";
/**
* Anime Genres
*/
echo "Indexing Anime Genres...\n";
$results = \json_decode(
app('SerializerV4')->serialize(
app('JikanParser')
->getAnimeGenres(new AnimeGenresRequest()),
'json'
),
true
);
if (HttpHelper::hasError($results)) {
echo "FAILED: {$results->original['error']}\n";
return;
}
$itemCount = count($results['genres']);
echo "Parsed {$itemCount} anime genres\n";
foreach ($results['genres'] as $i => $item) {
$item['count'] = $item['count'];
$result = DB::table('genres_anime')
// ->where('mal_id', $item['mal_id'])
->updateOrInsert(
[
'mal_id' => $item['mal_id']
],
[
'mal_id' => $item['mal_id'],
'name' => $item['name'],
'url' => $item['url'],
'count' => $item['count']
]
);
echo "Indexing {$i}/{$itemCount} \r";
}
$itemCount = count($results['explicit_genres']);
echo "Parsed {$itemCount} anime explicit_genres\n";
foreach ($results['explicit_genres'] as $i => $item) {
$result = DB::table('explicit_genres_anime')
// ->where('mal_id', $item['mal_id'])
->updateOrInsert(
[
'mal_id' => $item['mal_id']
],
[
'mal_id' => $item['mal_id'],
'name' => $item['name'],
'url' => $item['url'],
'count' => $item['count']
]
);
echo "Indexing {$i}/{$itemCount} \r";
}
$itemCount = count($results['themes']);
echo "Parsed {$itemCount} anime themes\n";
foreach ($results['themes'] as $i => $item) {
$result = DB::table('themes_anime')
// ->where('mal_id', $item['mal_id'])
->updateOrInsert(
[
'mal_id' => $item['mal_id']
],
[
'mal_id' => $item['mal_id'],
'name' => $item['name'],
'url' => $item['url'],
'count' => $item['count']
]
);
echo "Indexing {$i}/{$itemCount} \r";
}
$itemCount = count($results['demographics']);
echo "Parsed {$itemCount} anime demographics\n";
foreach ($results['demographics'] as $i => $item) {
$result = DB::table('demographics_anime')
// ->where('mal_id', $item['mal_id'])
->updateOrInsert(
[
'mal_id' => $item['mal_id']
],
[
'mal_id' => $item['mal_id'],
'name' => $item['name'],
'url' => $item['url'],
'count' => $item['count']
]
);
echo "Indexing {$i}/{$itemCount} \r";
}
/**
* Manga Genres
*/
echo "Indexing Manga Genres...\n";
$results = \json_decode(
app('SerializerV4')->serialize(
app('JikanParser')
->getMangaGenres(new MangaGenresRequest()),
'json'
),
true
);
if (HttpHelper::hasError($results)) {
echo "FAILED: {$results->original['error']}\n";
return;
}
$itemCount = count($results['genres']);
echo "Parsed {$itemCount} manga genres\n";
foreach ($results['genres'] as $i => $item) {
$result = DB::table('genres_manga')
// ->where('mal_id', $item['mal_id'])
->updateOrInsert(
[
'mal_id' => $item['mal_id']
],
[
'mal_id' => $item['mal_id'],
'name' => $item['name'],
'url' => $item['url'],
'count' => $item['count']
]
);
echo "Indexing {$i}/{$itemCount} \r";
}
$itemCount = count($results['explicit_genres']);
echo "Parsed {$itemCount} manga explicit_genres\n";
foreach ($results['explicit_genres'] as $i => $item) {
$result = DB::table('explicit_genres_manga')
// ->where('mal_id', $item['mal_id'])
->updateOrInsert(
[
'mal_id' => $item['mal_id']
],
[
'mal_id' => $item['mal_id'],
'name' => $item['name'],
'url' => $item['url'],
'count' => $item['count']
]
);
echo "Indexing {$i}/{$itemCount} \r";
}
$itemCount = count($results['themes']);
echo "Parsed {$itemCount} manga themes\n";
foreach ($results['themes'] as $i => $item) {
$result = DB::table('themes_manga')
// ->where('mal_id', $item['mal_id'])
->updateOrInsert(
[
'mal_id' => $item['mal_id']
],
[
'mal_id' => $item['mal_id'],
'name' => $item['name'],
'url' => $item['url'],
'count' => $item['count']
]
);
echo "Indexing {$i}/{$itemCount} \r";
}
$itemCount = count($results['demographics']);
echo "Parsed {$itemCount} manga demographics\n";
foreach ($results['demographics'] as $i => $item) {
$result = DB::table('demographics_manga')
// ->where('mal_id', $item['mal_id'])
->updateOrInsert(
[
'mal_id' => $item['mal_id']
],
[
'mal_id' => $item['mal_id'],
'name' => $item['name'],
'url' => $item['url'],
'count' => $item['count']
]
);
echo "Indexing {$i}/{$itemCount} \r";
}
echo str_pad("Indexing complete", 10).PHP_EOL;
}
}

View File

@ -0,0 +1,172 @@
<?php
namespace App\Console\Commands\Indexer;
use App\Exceptions\Console\CommandAlreadyRunningException;
use App\Exceptions\Console\FileNotFoundException;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
/**
* Class MangaIndexer
* @package App\Console\Commands\Indexer
*/
class MangaIndexer extends Command
{
/**
* The name and signature of the console command.
*`
* @var string
*/
protected $signature = 'indexer:manga
{--failed : Run only entries that failed to index last time}
{--resume : Resume from the last position}
{--reverse : Start from the end of the array}
{--index=0 : Start from a specific index}
{--delay=3 : Set a delay between requests}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Index all manga';
/**
* @var array
*/
private array $ids;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return void
* @throws FileNotFoundException
*/
public function handle()
{
$failed = $this->option('failed') ?? false;
$resume = $this->option('resume') ?? false;
$reverse = $this->option('reverse') ?? false;
$delay = $this->option('delay') ?? 3;
$index = $this->option('index') ?? 0;
$index = (int)$index;
$delay = (int)$delay;
$this->info("Info: MangaIndexer uses seanbreckenridge/mal-id-cache fetch available MAL IDs and updates/indexes them\n\n");
if ($failed && Storage::exists('indexer/indexer_manga.save')) {
$this->ids = $this->loadFailedMalIds();
}
if (!$failed) {
$this->ids = $this->fetchMalIds();
}
// start from the end
if ($reverse) {
$this->ids = array_reverse($this->ids);
}
// Resume
if ($resume && Storage::exists('indexer/indexer_manga.save')) {
$index = (int)Storage::get('indexer/indexer_manga.save');
$this->info("Resuming from index: {$index}");
}
// check if index even exists
if ($index > 0 && !isset($this->ids[$index])) {
$index = 0;
$this->warn('Invalid index; set back to 0');
}
// initialize and index
Storage::put('indexer/indexer_manga.save', 0);
echo "Loading MAL IDs\n";
$count = count($this->ids);
$failedIds = [];
$success = [];
echo "{$count} entries available\n";
for ($i = $index; $i <= ($count - 1); $i++) {
$id = $this->ids[$i];
$url = env('APP_URL') . "/v4/manga/{$id}";
echo "Indexing/Updating " . ($i + 1) . "/{$count} {$url} [MAL ID: {$id}] \n";
try {
$response = json_decode(file_get_contents($url), true);
if (isset($response['error']) && $response['status'] != 404) {
echo "[SKIPPED] Failed to fetch {$url} - {$response['error']}\n";
$failedIds[] = $id;
Storage::put('indexer/indexer_manga.failed', json_encode($failedIds));
}
sleep($delay);
} catch (\Exception $e) {
echo "[SKIPPED] Failed to fetch {$url}\n";
$failedIds[] = $id;
Storage::put('indexer/indexer_manga.failed', json_encode($failedIds));
}
$success[] = $id;
Storage::put('indexer/indexer_manga.save', $i);
}
Storage::delete('indexer/indexer_manga.save');
echo "---------\nIndexing complete\n";
echo count($success) . " entries indexed or updated\n";
echo count($failedIds) . " entries failed to index or update. Re-run with --failed to requeue failed entries only\n";
}
/**
* @return array
* @url https://github.com/seanbreckenridge/mal-id-cache
*/
private function fetchMalIds() : array
{
$this->info("Fetching MAL ID Cache https://raw.githubusercontent.com/seanbreckenridge/mal-id-cache/master/cache/manga_cache.json...\n");
$ids = json_decode(
file_get_contents('https://raw.githubusercontent.com/seanbreckenridge/mal-id-cache/master/cache/manga_cache.json'),
true
);
$this->ids = $ids['sfw'] + $ids['nsfw']; // merge
Storage::put('indexer/manga_mal_id.json', json_encode($this->ids));
return json_decode(Storage::get('indexer/manga_mal_id.json'));
}
/**
* @return array
* @throws FileNotFoundException
*/
private function loadFailedMalIds() : array
{
if (!Storage::exists('indexer/indexer_manga.failed')) {
throw new FileNotFoundException('"indexer/indexer_manga.failed" does not exist');
}
return json_decode(Storage::get('indexer/indexer_manga.failed'));
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class ManageMicrocaching extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'microcaching:service {status}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Enable or disable microcaching';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (!\in_array($this->argument('status'), ['disable', 'enable'])) {
$this->error('Only [enable/disable] allowed');
return;
}
if (!env('CACHING') || env('CACHE_DRIVER') !== 'redis') {
$this->error('Could not enable MICROCACHING. CACHING must be set to true and CACHE_DRIVER must be redis');
}
$enabled = $this->argument('status') === 'enable';
if ($enabled === env('MICROCACHING')) {
$this->error("MICROCACHING is already '{$this->argument('status')}'");
return;
}
$path = base_path('.env');
if (!file_exists($path)) {
$this->error(".env does not exist");
return;
}
file_put_contents($path, str_replace(
'MICROCACHING='.(env('MICROCACHING') ? 'true' : 'false'), 'MICROCACHING='.($enabled ? 'true' : 'false'), file_get_contents($path)
));
$this->info("MICROCACHING: '{$this->argument('status')}'");
}
}

View File

@ -4,6 +4,13 @@ namespace App\Console;
use App\Console\Commands\ClearQueuedJobs;
use App\Console\Commands\CacheRemove;
use App\Console\Commands\Indexer\AnimeIndexer;
use App\Console\Commands\Indexer\AnimeScheduleIndexer;
use App\Console\Commands\Indexer\CommonIndexer;
use App\Console\Commands\Indexer\CurrentSeasonIndexer;
use App\Console\Commands\Indexer\GenreIndexer;
use App\Console\Commands\Indexer\MangaIndexer;
use App\Console\Commands\ManageMicrocaching;
use App\Console\Commands\ModifyCacheDriver;
use App\Console\Commands\ModifyCacheMethod;
use Illuminate\Console\Scheduling\Schedule;
@ -21,6 +28,13 @@ class Kernel extends ConsoleKernel
ModifyCacheDriver::class,
ClearQueuedJobs::class,
CacheRemove::class,
CommonIndexer::class,
AnimeScheduleIndexer::class,
CurrentSeasonIndexer::class,
ManageMicrocaching::class,
AnimeIndexer::class,
MangaIndexer::class,
GenreIndexer::class
];
/**
@ -31,6 +45,20 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
//
// Update Scheduled Anime and current season data daily
// since they're airing, they're more prone to
// have their information updated
$schedule->command('indexer:anime-schedule')
->daily();
$schedule->command('indexer:anime-current-season')
->daily();
// Update common indexes daily
$schedule->command('indexer:common')
->daily();
$schedule->command('indexer:genres')
->daily();
}
}

47
app/Episode.php Normal file
View File

@ -0,0 +1,47 @@
<?php
namespace App;
use App\Http\HttpHelper;
use Jenssegers\Mongodb\Eloquent\Model;
use Jikan\Helper\Media;
use Jikan\Helper\Parser;
use Jikan\Jikan;
use Jikan\Model\Common\YoutubeMeta;
use Jikan\Request\Anime\AnimeRequest;
class Episode extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'mal_id', 'title', 'title_japanese', 'title_romanji', 'aired', 'filler', 'recap', 'video_url', 'forum_url', 'synopsis'
];
/**
* The accessors to append to the model's array form.
*
* @var array
*/
protected $appends = [];
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'anime_episodes';
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = [
];
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Events;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
/**
* Class SourceHeartbeatEvent
* @package App\Events
*/
class SourceHeartbeatEvent extends Event
{
public const BAD_HEALTH = 1;
public const GOOD_HEALTH = 0;
public $health;
public $status;
/**
* SourceHeartbeatEvent constructor.
* @param int $health
* @param int $status
*/
public function __construct(?int $health, ?int $status)
{
$this->health = $health ?? self::BAD_HEALTH;
$this->status = $status ?? 0;
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Exceptions\Console;
class CommandAlreadyRunningException extends \Exception
{
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Exceptions\Console;
class FileNotFoundException extends \Exception
{
}

View File

@ -69,7 +69,7 @@ class GithubReport
* @param Request $request
* @return string
*/
public static function make(\Exception $exception, Request $request, ?string $repo = null) : self
public static function make(\Throwable $exception, Request $request, ?string $repo = null) : self
{
$report = new self;
$report->name = \get_class($exception);
@ -82,10 +82,12 @@ class GithubReport
$report->jikanVersion = Versions::getVersion('jikan-me/jikan');
$report->phpVersion = PHP_VERSION;
try {
$report->redisRunning = trim(app('redis')->ping()) === 'PONG' ? "Connected" : "Disconnected";
} catch (ConnectionException $e) {
$report->redisRunning = false;
if (env('CACHING') && env('CACHE_DRIVER') === 'redis') {
try {
$report->redisRunning = trim(app('redis')->ping()) === 'PONG' ? "Connected" : "Disconnected";
} catch (ConnectionException $e) {
$report->redisRunning = false;
}
}
$report->instanceType = 'UNKNOWN';

View File

@ -2,10 +2,11 @@
namespace App\Exceptions;
use App\Events\SourceHeartbeatEvent;
use App\Http\HttpHelper;
use Bugsnag\BugsnagLaravel\Facades\Bugsnag;
use Exception;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
@ -15,6 +16,7 @@ use Jikan\Exception\ParserException;
use Laravel\Lumen\Exceptions\Handler as ExceptionHandler;
use Predis\Connection\ConnectionException;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\HttpClient\Exception\TimeoutException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Support\Facades\Cache;
@ -38,25 +40,27 @@ class Handler extends ExceptionHandler
];
/**
* @param Exception $e
* @param \Throwable $e
* @throws Exception
*/
public function report(Exception $e)
public function report(\Throwable $e)
{
parent::report($e);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $e
* @return \Illuminate\Http\Response
* @param Request $request
* @param \Throwable $e
* @return \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function render($request, Exception $e)
public function render($request, \Throwable $e)
{
$githubReport = GithubReport::make($e, $request);
if (app()->bound('sentry') && $this->shouldReport($e)) {
app('sentry')->captureException($e);
}
// ConnectionException from Redis server
if ($e instanceof ConnectionException) {
/*
@ -77,6 +81,18 @@ class Handler extends ExceptionHandler
], 500);
}
if ($e instanceof ConnectException) {
event(new SourceHeartbeatEvent(SourceHeartbeatEvent::BAD_HEALTH, $e->getCode()));
return response()
->json([
'status' => $e->getCode(),
'type' => 'BadResponseException',
'message' => 'Jikan failed to connect to MyAnimeList. MyAnimeList may be down/unavailable or refuses to connect',
'error' => $e->getMessage()
], 503);
}
// ParserException from Jikan PHP API
if ($e instanceof ParserException) {
$githubReport->setRepo(env('GITHUB_API', 'jikan-me/jikan'));
@ -95,7 +111,7 @@ class Handler extends ExceptionHandler
if ($e instanceof BadResponseException || $e instanceof ClientException) {
switch ($e->getCode()) {
case 404:
$this->set404Cache($request, $e);
// $this->set404Cache($request, $e);
return response()
->json([
@ -112,15 +128,20 @@ class Handler extends ExceptionHandler
'message' => 'Jikan is being rate limited by MyAnimeList',
'error' => $e->getMessage()
], $e->getCode());
case 403:
case 500:
case 501:
case 502:
case 503:
case 504:
// Dispatch Bad source health event to prompt database fallback if enabled
event(new SourceHeartbeatEvent(SourceHeartbeatEvent::BAD_HEALTH, $e->getCode()));
return response()
->json([
'status' => $e->getCode(),
'type' => 'BadResponseException',
'message' => 'Jikan could not connect to MyAnimeList',
'message' => 'Jikan failed to connect to MyAnimeList. MyAnimeList may be down/unavailable or refuses to connect',
'error' => $e->getMessage()
], 503);
default:
@ -145,6 +166,16 @@ class Handler extends ExceptionHandler
], 400);
}
if ($e instanceof TimeoutException) {
return response()
->json([
'status' => 408,
'type' => 'TimeoutException',
'message' => 'Request to MyAnimeList.net timed out (' .env('SOURCE_TIMEOUT', 5) . ' seconds)',
'error' => $e->getMessage()
], 408);
}
if ($e instanceof Exception) {
return response()
->json([
@ -158,8 +189,16 @@ class Handler extends ExceptionHandler
}
}
/**
* @param Request $request
* @param BadResponseException $e
*/
private function set404Cache(Request $request, BadResponseException $e)
{
if (!env('CACHING') || env('MICROCACHING')) {
return;
}
$fingerprint = "request:404:".sha1(env('APP_URL') . $request->getRequestUri());
if (Cache::has($fingerprint)) {

54
app/GenreAnime.php Normal file
View File

@ -0,0 +1,54 @@
<?php
namespace App;
use Jenssegers\Mongodb\Eloquent\Model;
use Jikan\Request\Genre\AnimeGenresRequest;
/**
* Class Magazine
* @package App
*/
class GenreAnime extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'mal_id', 'name', 'url', 'count'
];
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'genres_anime';
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = [
'_id', 'expiresAt'
];
/**
* @return array
*/
public static function scrape() : array
{
$data = app('JikanParser')->getAnimeGenres(new AnimeGenresRequest());
return json_decode(
app('SerializerV4')
->serialize($data, 'json'),
true
);
}
}

53
app/GenreManga.php Normal file
View File

@ -0,0 +1,53 @@
<?php
namespace App;
use Jenssegers\Mongodb\Eloquent\Model;
use Jikan\Request\Genre\AnimeGenresRequest;
/**
* Class Magazine
* @package App
*/
class GenreManga extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'mal_id', 'name', 'url', 'count'
];
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'genres_manga';
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = [
'_id', 'expiresAt'
];
/**
* @return array
*/
public static function scrape() : array
{
$data = app('JikanParser')->getAnimeGenres(new AnimeGenresRequest());
return json_decode(
app('SerializerV4')
->serialize($data, 'json'),
true
);
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace App\Http\Controllers\V3;
use Jikan\Request\Club\ClubRequest;
use Jikan\Request\Club\UserListRequest;
class ClubController extends Controller
{
public function main(int $id)
{
$club = $this->jikan->getClub(new ClubRequest($id));
return response($this->serializer->serialize($club, 'json'));
}
public function members(int $id, int $page = 1)
{
$club = ['members' => $this->jikan->getClubUsers(new UserListRequest($id, $page))];
return response($this->serializer->serialize($club, 'json'));
}
}

View File

@ -1,18 +0,0 @@
<?php
namespace App\Http\Controllers;
class ExampleController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
//
}
//
}

View File

@ -1,14 +0,0 @@
<?php
namespace App\Http\Controllers\V3;
use Jikan\Request\Magazine\MagazineRequest;
class MagazineController extends Controller
{
public function main(int $id, int $page = 1)
{
$magazine = $this->jikan->getMagazine(new MagazineRequest($id, $page));
return response($this->serializer->serialize($magazine, 'json'));
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Http\Controllers\V3;
use Jikan\Exception\BadResponseException;
use Jikan\Request\Person\PersonRequest;
use Jikan\Request\Person\PersonPicturesRequest;
class PersonController extends Controller
{
public function main(int $id)
{
if ($id < 1) { // MAL INCONSISTENCY: doesn't return 404, it returns an error message with HTTP 200 instead
throw new BadResponseException(null, 404);
}
$person = $this->jikan->getPerson(new PersonRequest($id));
return response($this->serializer->serialize($person, 'json'));
}
public function pictures(int $id)
{
if ($id < 1) { // MAL INCONSISTENCY: doesn't return 404, it returns an error message with HTTP 200 instead
throw new BadResponseException(null, 404);
}
$person = ['pictures' => $this->jikan->getPersonPictures(new PersonPicturesRequest($id))];
return response($this->serializer->serialize($person, 'json'));
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace App\Http\Controllers\V3;
use Jikan\Request\Producer\ProducerRequest;
class ProducerController extends Controller
{
public function main(int $id, int $page = 1)
{
$producer = $this->jikan->getProducer(new ProducerRequest($id, $page));
return response($this->serializer->serialize($producer, 'json'));
}
}

View File

@ -1,39 +0,0 @@
<?php
namespace App\Http\Controllers\V3;
use Jikan\Request\Schedule\ScheduleRequest;
class ScheduleController extends Controller
{
private const VALID_DAYS = [
'monday',
'tuesday',
'wednesday',
'thursday',
'friday',
'saturday',
'sunday',
'other',
'unknown',
];
public function main(?string $day = null)
{
if (null !== $day && !\in_array(strtolower($day), self::VALID_DAYS, true)) {
return response()->json([
'error' => 'Bad Request',
])->setStatusCode(400);
}
$schedule = $this->jikan->getSchedule(new ScheduleRequest());
if (null !== $day) {
$schedule = [
strtolower($day) => $schedule->{'get'.ucfirst(strtolower($day))}(),
];
}
return response($this->serializer->serialize($schedule, 'json'));
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace App\Http\Controllers\V3;
use Jikan\Request\Seasonal\SeasonalRequest;
use Jikan\Request\SeasonList\SeasonListRequest;
class SeasonController extends Controller
{
private const VALID_SEASONS = [
'summer',
'spring',
'winter',
'fall'
];
public function main(?int $year = null, ?string $season = null)
{
if (!is_null($season) && !\in_array(strtolower($season), self::VALID_SEASONS)) {
return response()->json([
'error' => 'Bad Request'
])->setStatusCode(400);
}
$season = $this->jikan->getSeasonal(new SeasonalRequest($year, $season));
return response($this->serializer->serialize($season, 'json'));
}
public function archive()
{
return response(
$this->serializer->serialize(
['archive' => $this->jikan->getSeasonList(new SeasonListRequest())],
'json'
)
);
}
public function later()
{
$season = $this->jikan->getSeasonal(new SeasonalRequest(null, null, true));
return response($this->serializer->serialize($season, 'json'));
}
}

View File

@ -1,75 +0,0 @@
<?php
namespace App\Http\Controllers\V3;
use Jikan\Request\Top\TopAnimeRequest;
use Jikan\Request\Top\TopMangaRequest;
use Jikan\Request\Top\TopCharactersRequest;
use Jikan\Request\Top\TopPeopleRequest;
use Jikan\Helper\Constants as JikanConstants;
class TopController extends Controller
{
public function anime(int $page = 1, string $type = null)
{
if (!is_null($type) && !\in_array(strtolower($type), [
JikanConstants::TOP_AIRING,
JikanConstants::TOP_UPCOMING,
JikanConstants::TOP_TV,
JikanConstants::TOP_MOVIE,
JikanConstants::TOP_OVA,
JikanConstants::TOP_SPECIAL,
JikanConstants::TOP_BY_POPULARITY,
JikanConstants::TOP_BY_FAVORITES,
])) {
return response()->json([
'error' => 'Bad Request'
])->setStatusCode(400);
}
$anime = $this->jikan->getTopAnime(new TopAnimeRequest($page, $type));
$top = ['top' => $this->jikan->getTopAnime(new TopAnimeRequest($page, $type))];
return response($this->serializer->serialize($top, 'json'));
}
public function manga(int $page = 1, string $type = null)
{
if (!is_null($type) && !\in_array(
strtolower($type),
[
JikanConstants::TOP_MANGA,
JikanConstants::TOP_NOVEL,
JikanConstants::TOP_ONE_SHOT,
JikanConstants::TOP_DOUJINSHI,
JikanConstants::TOP_MANHWA,
JikanConstants::TOP_MANHUA,
JikanConstants::TOP_BY_POPULARITY,
JikanConstants::TOP_BY_FAVORITES,
]
)) {
return response()->json([
'error' => 'Bad Request'
])->setStatusCode(400);
}
$top = ['top' => $this->jikan->getTopManga(new TopMangaRequest($page, $type))];
return response($this->serializer->serialize($top, 'json'));
}
public function people(int $page = 1)
{
$top = ['top' => $this->jikan->getTopPeople(new TopPeopleRequest($page))];
return response($this->serializer->serialize($top, 'json'));
}
public function characters(int $page = 1)
{
$top = ['top' => $this->jikan->getTopCharacters(new TopCharactersRequest($page))];
return response($this->serializer->serialize($top, 'json'));
}
}

View File

@ -1,9 +1,13 @@
<?php
namespace App\Http\Controllers\V3;
namespace App\Http\Controllers\V4;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Request\Anime\AnimeCharactersAndStaffRequest;
use Jikan\Request\Anime\AnimeEpisodeRequest;
use Jikan\Request\Anime\AnimeEpisodesRequest;
use Jikan\Request\Anime\AnimeForumRequest;
use Jikan\Request\Anime\AnimeMoreInfoRequest;
@ -15,13 +19,21 @@ use Jikan\Request\Anime\AnimeRequest;
use Jikan\Request\Anime\AnimeReviewsRequest;
use Jikan\Request\Anime\AnimeStatsRequest;
use Jikan\Request\Anime\AnimeVideosRequest;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class AnimeController extends Controller
{
public function main(int $id)
public function main(Request $request, int $id)
{
$anime = $this->jikan->getAnime(new AnimeRequest($id));
return response($this->serializer->serialize($anime, 'json'));
$animeSerialized = $this->serializer->serialize($anime, 'json');
$animeSerialized = HttpHelper::serializeEmptyObjectsControllerLevel(
json_decode($animeSerialized, true)
);
$animeSerialized = json_encode($animeSerialized);
return response($animeSerialized);
}
public function characters_staff(int $id)
@ -30,8 +42,15 @@ class AnimeController extends Controller
return response($this->serializer->serialize($anime, 'json'));
}
public function episodes(int $id, int $page = 1)
public function episode(int $id, int $episodeId)
{
$anime = $this->jikan->getAnimeEpisode(new AnimeEpisodeRequest($id, $episodeId));
return response($this->serializer->serialize($anime, 'json'));
}
public function episodes(int $id)
{
$page = $_GET['page'] ?? 1;
$anime = $this->jikan->getAnimeEpisodes(new AnimeEpisodesRequest($id, $page));
return response($this->serializer->serialize($anime, 'json'));
}
@ -42,13 +61,9 @@ class AnimeController extends Controller
return response($this->serializer->serialize($anime, 'json'));
}
public function forum(int $id, ?string $topic = null)
public function forum(int $id)
{
if ($topic === 'episodes') {
$topic = 'episode';
}
$anime = ['topics' => $this->jikan->getAnimeForum(new AnimeForumRequest($id, $topic))];
$anime = ['topics' => $this->jikan->getAnimeForum(new AnimeForumRequest($id))];
return response($this->serializer->serialize($anime, 'json'));
}

View File

@ -1,9 +1,13 @@
<?php
namespace App\Http\Controllers\V3;
namespace App\Http\Controllers\V4;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use App\Manga;
use Jikan\Request\Character\CharacterRequest;
use Jikan\Request\Character\CharacterPicturesRequest;
use MongoDB\BSON\UTCDateTime;
class CharacterController extends Controller
{

View File

@ -1,17 +1,20 @@
<?php
namespace App\Http\Controllers\V3;
namespace App\Http\Controllers\V4;
use App\Providers\SerializerFactory;
use App\Providers\SerializerServiceProdivder;
use App\Providers\SerializerServiceProviderV3;
use GuzzleHttp\Client;
use Jikan\Jikan;
use Jikan\MyAnimeList\MalClient;
use JMS\Serializer\Context;
use JMS\Serializer\Serializer;
use Laravel\Lumen\Routing\Controller as BaseController;
/**
* Class Controller
* @package App\Http\Controllers\V4
*/
class Controller extends BaseController
{
/**
@ -30,9 +33,9 @@ class Controller extends BaseController
* @param Serializer $serializer
* @param MalClient $jikan
*/
public function __construct()
public function __construct(MalClient $jikan)
{
$this->serializer = SerializerFactory::createV3();
$this->jikan = app('JikanParser');
$this->serializer = SerializerFactory::createV4();
$this->jikan = $jikan;
}
}

View File

@ -1,8 +1,9 @@
<?php
namespace App\Http\Controllers\V3;
namespace App\Http\Controllers\V4;
use Jikan\Request\Genre\AnimeGenreRequest;
use Jikan\Request\Genre\AnimeGenresRequest;
use Jikan\Request\Genre\MangaGenreRequest;
class GenreController extends Controller
@ -18,4 +19,16 @@ class GenreController extends Controller
$person = $this->jikan->getMangaGenre(new MangaGenreRequest($id, $page));
return response($this->serializer->serialize($person, 'json'));
}
public function animeListing()
{
$results = $this->jikan->getAnimeGenres(new AnimeGenresRequest());
return response($this->serializer->serialize($results, 'json'));
}
public function mangaListing()
{
$results = $this->jikan->getAnimeGenres(new AnimeGenresRequest());
return response($this->serializer->serialize($results, 'json'));
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\V4;
use Jikan\Request\Magazine\MagazineRequest;
use Jikan\Request\Magazine\MagazinesRequest;
class MagazineController extends Controller
{
public function main()
{
$results = $this->jikan->getMagazines(new MagazinesRequest());
return response($this->serializer->serialize($results, 'json'));
}
public function resource(int $id, int $page = 1)
{
$magazine = $this->jikan->getMagazine(new MagazineRequest($id, $page));
return response($this->serializer->serialize($magazine, 'json'));
}
}

View File

@ -1,7 +1,8 @@
<?php
namespace App\Http\Controllers\V3;
namespace App\Http\Controllers\V4;
use App\Http\HttpHelper;
use Jikan\Request\Manga\MangaCharactersRequest;
use Jikan\Request\Manga\MangaForumRequest;
use Jikan\Request\Manga\MangaMoreInfoRequest;
@ -18,7 +19,13 @@ class MangaController extends Controller
public function main(int $id)
{
$manga = $this->jikan->getManga(new MangaRequest($id));
return response($this->serializer->serialize($manga, 'json'));
$mangaSerialized = $this->serializer->serialize($manga, 'json');
$mangaSerialized = HttpHelper::serializeEmptyObjectsControllerLevel(
json_decode($mangaSerialized, true)
);
return response($this->serializer->serialize($mangaSerialized, 'json'));
}
public function characters(int $id)
@ -33,14 +40,9 @@ class MangaController extends Controller
return response($this->serializer->serialize($manga, 'json'));
}
public function forum(int $id, ?string $topic = null)
public function forum(int $id)
{
// safely bypass MAL's naming schemes
if ($topic === 'chapters') {
$topic = 'episode';
}
$manga = ['topics' => $this->jikan->getMangaForum(new MangaForumRequest($id, $topic))];
$manga = ['topics' => $this->jikan->getMangaForum(new MangaForumRequest($id))];
return response($this->serializer->serialize($manga, 'json'));
}
@ -59,24 +61,24 @@ class MangaController extends Controller
public function moreInfo(int $id)
{
$manga = ['moreinfo' => $this->jikan->getMangaMoreInfo(new MangaMoreInfoRequest($id))];
return response(json_encode($manga));
return response($this->serializer->serialize($manga, 'json'));
}
public function recommendations(int $id)
{
$anime = ['recommendations' => $this->jikan->getMangaRecommendations(new MangaRecommendationsRequest($id))];
return response($this->serializer->serialize($anime, 'json'));
$manga = ['recommendations' => $this->jikan->getMangaRecommendations(new MangaRecommendationsRequest($id))];
return response($this->serializer->serialize($manga, 'json'));
}
public function userupdates(int $id, int $page = 1)
{
$anime = ['users' => $this->jikan->getMangaRecentlyUpdatedByUsers(new MangaRecentlyUpdatedByUsersRequest($id, $page))];
return response($this->serializer->serialize($anime, 'json'));
$manga = ['users' => $this->jikan->getMangaRecentlyUpdatedByUsers(new MangaRecentlyUpdatedByUsersRequest($id, $page))];
return response($this->serializer->serialize($manga, 'json'));
}
public function reviews(int $id, int $page = 1)
{
$anime = ['reviews' => $this->jikan->getMangaReviews(new MangaReviewsRequest($id, $page))];
return response($this->serializer->serialize($anime, 'json'));
$manga = ['reviews' => $this->jikan->getMangaReviews(new MangaReviewsRequest($id, $page))];
return response($this->serializer->serialize($manga, 'json'));
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\V4;
use Jikan\Request\Producer\ProducerRequest;
use Jikan\Request\Producer\ProducersRequest;
class ProducerController extends Controller
{
public function main()
{
$results = $this->jikan->getProducers(new ProducersRequest());
return response($this->serializer->serialize($results, 'json'));
}
public function resource(int $id, int $page = 1)
{
$producer = $this->jikan->getProducer(new ProducerRequest($id, $page));
return response($this->serializer->serialize($producer, 'json'));
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers\V4;
use Jikan\Helper\Constants;
use Jikan\Request\Recommendations\RecentRecommendationsRequest;
class RecommendationsController extends Controller
{
public function anime()
{
$page = $_GET['page'] ?? 1;
$results = [
'recommendations' => $this->jikan->getRecentRecommendations(
new RecentRecommendationsRequest(Constants::RECENT_RECOMMENDATION_ANIME, $page)
)
];
return response($this->serializer->serialize($results, 'json'));
}
public function manga()
{
$page = $_GET['page'] ?? 1;
$results = [
'recommendations' => $this->jikan->getRecentRecommendations(
new RecentRecommendationsRequest(Constants::RECENT_RECOMMENDATION_MANGA, $page)
)
];
return response($this->serializer->serialize($results, 'json'));
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\V4;
use Jikan\Helper\Constants;
use Jikan\Request\Reviews\RecentReviewsRequest;
class ReviewsController extends Controller
{
public function bestVoted()
{
$page = $_GET['page'] ?? 1;
$results = $this->jikan->getRecentReviews(
new RecentReviewsRequest(Constants::RECENT_REVIEW_BEST_VOTED, $page)
);
return response($this->serializer->serialize($results, 'json'));
}
public function anime()
{
$page = $_GET['page'] ?? 1;
$results = $this->jikan->getRecentReviews(
new RecentReviewsRequest(Constants::RECENT_REVIEW_ANIME, $page)
);
return response($this->serializer->serialize($results, 'json'));
}
public function manga()
{
$page = $_GET['page'] ?? 1;
$results = $this->jikan->getRecentReviews(
new RecentReviewsRequest(Constants::RECENT_REVIEW_MANGA, $page)
);
return response($this->serializer->serialize($results, 'json'));
}
}

View File

@ -1,8 +1,7 @@
<?php
namespace App\Http\Controllers\V3;
namespace App\Http\Controllers\V4;
use Illuminate\Http\Request;
use Jikan\Jikan;
use Jikan\MyAnimeList\MalClient;
use Jikan\Request\Search\AnimeSearchRequest;
@ -11,16 +10,17 @@ use Jikan\Request\Search\CharacterSearchRequest;
use Jikan\Request\Search\PersonSearchRequest;
use Jikan\Helper\Constants as JikanConstants;
use App\Providers\SearchQueryBuilder;
use Jikan\Request\Search\UserSearchRequest;
use Jikan\Request\User\UsernameByIdRequest;
use JMS\Serializer\Serializer;
use phpDocumentor\Reflection\Types\Object_;
class SearchController extends Controller
{
public function anime(Request $request, int $page = 1)
public function anime(int $page = 1)
{
$search = $this->jikan->getAnimeSearch(
SearchQueryBuilder::create(
$request,
(new AnimeSearchRequest())->setPage($page)
)
);
@ -28,22 +28,20 @@ class SearchController extends Controller
return response($this->filter($search));
}
public function manga(Request $request, int $page = 1)
public function manga(int $page = 1)
{
$search = $this->jikan->getMangaSearch(
SearchQueryBuilder::create(
$request,
(new MangaSearchRequest())->setPage($page)
)
);
return response($this->filter($search));
}
public function people(Request $request, int $page = 1)
public function people(int $page = 1)
{
$search = $this->jikan->getPersonSearch(
SearchQueryBuilder::create(
$request,
(new PersonSearchRequest())->setPage($page)
)
);
@ -51,11 +49,10 @@ class SearchController extends Controller
return response($this->filter($search));
}
public function character(Request $request, int $page = 1)
public function character(int $page = 1)
{
$search = $this->jikan->getCharacterSearch(
SearchQueryBuilder::create(
$request,
(new CharacterSearchRequest())->setPage($page)
)
);
@ -63,6 +60,26 @@ class SearchController extends Controller
return response($this->filter($search));
}
public function users()
{
$search = $this->jikan->getUserSearch(
SearchQueryBuilder::create(
new UserSearchRequest()
)
);
return response($this->filter($search));
}
public function userById(int $id)
{
$search = $this->jikan->getUsernameById(
new UsernameByIdRequest($id)
);
return response($this->filter($search));
}
private function filter($object)
{

View File

@ -1,21 +1,23 @@
<?php
namespace App\Http\Controllers\V3;
namespace App\Http\Controllers\V4;
use App\Providers\UserListQueryBuilder;
use Illuminate\Http\Request;
use Jikan\Request\User\RecentlyOnlineUsersRequest;
use Jikan\Request\User\UserAnimeListRequest;
use Jikan\Request\User\UserClubsRequest;
use Jikan\Request\User\UserMangaListRequest;
use Jikan\Request\User\UserProfileRequest;
use Jikan\Request\User\UserFriendsRequest;
use Jikan\Request\User\UserHistoryRequest;
use Jikan\Request\User\UserRecommendationsRequest;
use Jikan\Request\User\UserReviewsRequest;
class UserController extends Controller
{
public function profile(string $username)
{
$person = $this->jikan->getUserProfile(new UserProfileRequest($username));
return response($this->serializer->serialize($person, 'json'));
$user = $this->jikan->getUserProfile(new UserProfileRequest($username));
return response($this->serializer->serialize($user, 'json'));
}
public function history(string $username, ?string $type = null)
@ -37,7 +39,7 @@ class UserController extends Controller
return response($this->serializer->serialize($person, 'json'));
}
public function animelist(Request $request, string $username, ?string $status = null, int $page = 1)
public function animelist(string $username, ?string $status = null, int $page = 1)
{
if (!is_null($status)) {
$status = strtolower($status);
@ -54,12 +56,7 @@ class UserController extends Controller
$this->serializer->serialize(
[
'anime' => $this->jikan->getUserAnimeList(
UserListQueryBuilder::create(
$request,
(new UserAnimeListRequest($username))
->setPage($page)
->setStatus($status)
)
new UserAnimeListRequest($username, $page, $status)
)
],
'json'
@ -67,7 +64,7 @@ class UserController extends Controller
);
}
public function mangalist(Request $request, string $username, ?string $status = null, int $page = 1)
public function mangalist(string $username, ?string $status = null, int $page = 1)
{
if (!is_null($status)) {
$status = strtolower($status);
@ -84,12 +81,7 @@ class UserController extends Controller
$this->serializer->serialize(
[
'manga' => $this->jikan->getUserMangaList(
UserListQueryBuilder::create(
$request,
(new UserMangaListRequest($username))
->setPage($page)
->setStatus($status)
)
new UserMangaListRequest($username, $page, $status)
)
],
'json'
@ -97,6 +89,47 @@ class UserController extends Controller
);
}
public function reviews(string $username)
{
$page = $_GET['page'] ?? 1;
$results = $this->jikan->getUserReviews(
new UserReviewsRequest($username, $page)
);
return response($this->serializer->serialize($results, 'json'));
}
public function recommendations(string $username)
{
$page = $_GET['page'] ?? 1;
$results = $this->jikan->getUserRecommendations(
new UserRecommendationsRequest($username, $page)
);
return response($this->serializer->serialize($results, 'json'));
}
public function clubs(string $username)
{
$results = [
'clubs' => $this->jikan->getUserClubs(
new UserClubsRequest($username)
)
];
return response($this->serializer->serialize($results, 'json'));
}
public function recentlyOnline()
{
$results = [
'users' => $this->jikan->getRecentOnlineUsers(
new RecentlyOnlineUsersRequest()
)
];
return response($this->serializer->serialize($results, 'json'));
}
private function listStatusToId(?string $status) : int
{

View File

@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\V4;
use Jikan\Helper\Constants;
use Jikan\Request\Watch\PopularEpisodesRequest;
use Jikan\Request\Watch\PopularPromotionalVideosRequest;
use Jikan\Request\Watch\RecentEpisodesRequest;
use Jikan\Request\Watch\RecentPromotionalVideosRequest;
class WatchController extends Controller
{
public function recentEpisodes()
{
$results = $this->jikan->getRecentEpisodes(
new RecentEpisodesRequest()
);
return response($this->serializer->serialize($results, 'json'));
}
public function popularEpisodes()
{
$results = $this->jikan->getPopularEpisodes(
new PopularEpisodesRequest()
);
return response($this->serializer->serialize($results, 'json'));
}
public function recentPromos()
{
$page = $_GET['page'] ?? 1;
$results = $this->jikan->getRecentPromotionalVideos(
new RecentPromotionalVideosRequest($page)
);
return response($this->serializer->serialize($results, 'json'));
}
public function popularPromos()
{
$results = $this->jikan->getPopularPromotionalVideos(
new PopularPromotionalVideosRequest()
);
return response($this->serializer->serialize($results, 'json'));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,400 @@
<?php
namespace App\Http\Controllers\V4DB;
use App\Anime;
use App\Character;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use App\Http\Resources\V4\CharacterAnimeCollection;
use App\Http\Resources\V4\CharacterMangaCollection;
use App\Http\Resources\V4\CharacterMangaResource;
use App\Http\Resources\V4\CharacterSeiyuuCollection;
use App\Http\Resources\V4\PersonMangaCollection;
use App\Http\Resources\V4\PicturesResource;
use App\Person;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Request\Anime\AnimePicturesRequest;
use Jikan\Request\Character\CharacterRequest;
use Jikan\Request\Character\CharacterPicturesRequest;
use MongoDB\BSON\UTCDateTime;
class CharacterController extends Controller
{
/**
* @OA\Get(
* path="/characters/{id}",
* operationId="getCharacterById",
* tags={"characters"},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response="200",
* description="Returns character resource",
* @OA\JsonContent(
* ref="#/components/schemas/character"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function main(Request $request, int $id)
{
$results = Character::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Character::scrape($id);
if (HttpHelper::hasError($response)) {
return HttpResponse::notFound($request);
}
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Character::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Character::query()
->where('mal_id', $id)
->update($response);
}
$results = Character::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new \App\Http\Resources\V4\CharacterResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/characters/{id}/anime",
* operationId="getCharacterAnime",
* tags={"characters"},
*
* @OA\Response(
* response="200",
* description="Returns anime that character is in",
* @OA\JsonContent(ref="#/components/schemas/character anime")
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function anime(Request $request, int $id)
{
$results = Character::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Character::scrape($id);
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Character::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Character::query()
->where('mal_id', $id)
->update($response);
}
$results = Character::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new CharacterAnimeCollection(
$results->first()['animeography']
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/characters/{id}/manga",
* operationId="getCharacterManga",
* tags={"characters"},
*
* @OA\Response(
* response="200",
* description="Returns manga that character is in",
* @OA\JsonContent(ref="#/components/schemas/character manga")
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function manga(Request $request, int $id)
{
$results = Character::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Character::scrape($id);
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Character::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Character::query()
->where('mal_id', $id)
->update($response);
}
$results = Character::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new CharacterMangaCollection(
$results->first()['mangaography']
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/characters/{id}/voices",
* operationId="getCharacterVoiceActors",
* tags={"characters"},
*
* @OA\Response(
* response="200",
* description="Returns the character's voice actors",
* @OA\JsonContent(ref="#/components/schemas/character voice actors")
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function voices(Request $request, int $id)
{
$results = Character::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Character::scrape($id);
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Character::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Character::query()
->where('mal_id', $id)
->update($response);
}
$results = Character::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new CharacterSeiyuuCollection(
$results->first()['voice_actors']
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/characters/{id}/pictures",
* operationId="getCharacterPictures",
* tags={"characters"},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response="200",
* description="Returns pictures related to the entry",
* @OA\JsonContent(
* ref="#/components/schemas/pictures"
* )
* ),
*
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*
* @OA\Schema(
* schema="character pictures",
* description="Character Pictures",
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(
* @OA\Property(
* property="image_url",
* type="string",
* description="Default JPG Image Size URL"
* ),
* @OA\Property(
* property="large_image_url",
* type="string",
* description="Large JPG Image Size URL"
* ),
* )
* )
* )
*/
public function pictures(Request $request, int $id)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$character = ['pictures' => $this->jikan->getCharacterPictures(new CharacterPicturesRequest($id))];
$response = \json_decode($this->serializer->serialize($character, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new PicturesResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
}

View File

@ -0,0 +1,363 @@
<?php
namespace App\Http\Controllers\V4DB;
use App\Anime;
use App\Club;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use App\Http\Resources\V4\AnimeCharactersResource;
use App\Http\Resources\V4\ResultsResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Request\Anime\AnimeCharactersAndStaffRequest;
use Jikan\Request\Club\ClubRequest;
use Jikan\Request\Club\UserListRequest;
use MongoDB\BSON\UTCDateTime;
class ClubController extends Controller
{
/**
* @OA\Get(
* path="/clubs/{id}",
* operationId="getClubsById",
* tags={"clubs"},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response="200",
* description="Returns Club Resource",
* @OA\JsonContent(
* ref="#/components/schemas/club"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function main(Request $request, int $id)
{
$results = Club::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Club::scrape($id);
if (HttpHelper::hasError($response)) {
return HttpResponse::notFound($request);
}
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Club::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Club::query()
->where('mal_id', $id)
->update($response);
}
$results = Club::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new \App\Http\Resources\V4\ClubResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/clubs/{id}/members",
* operationId="getClubMembers",
* tags={"clubs"},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* @OA\Schema(type="integer")
* ),
*
* @OA\Parameter(ref="#/components/parameters/page"),
*
* @OA\Response(
* response="200",
* description="Returns Club Members Resource",
* @OA\JsonContent(
* allOf={
* @OA\Schema(ref="#/components/schemas/pagination"),
* @OA\Schema(
* ref="#/components/schemas/club member"
* )
* }
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*
* @OA\Schema(
* schema="club member",
* description="Club Member",
* @OA\Property(
* property="data",
* type="array",
* @OA\Items(
* type="object",
* @OA\Property(
* property="username",
* type="string",
* description="MyAnimeList Username"
* ),
* @OA\Property(
* property="url",
* type="string",
* description="MyAnimeList URL"
* ),
* @OA\Property(
* property="image_url",
* type="string",
* description="MyAnimeList Image URL"
* ),
* ),
* ),
* ),
*/
public function members(Request $request, int $id)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$anime = ['results' => $this->jikan->getClubUsers(new UserListRequest($id, $page))];
$response = \json_decode($this->serializer->serialize($anime, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/clubs/{id}/staff",
* operationId="getClubStaff",
* tags={"clubs"},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response="200",
* description="Returns Club Staff",
* @OA\JsonContent(
* ref="#/components/schemas/club staff"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function staff(Request $request, int $id)
{
$results = Club::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Club::scrape($id);
if (HttpHelper::hasError($response)) {
return HttpResponse::notFound($request);
}
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Club::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Club::query()
->where('mal_id', $id)
->update($response);
}
$results = Club::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new \App\Http\Resources\V4\ClubStaffResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/clubs/{id}/relations",
* operationId="getClubRelations",
* tags={"clubs"},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response="200",
* description="Returns Club Relations",
* @OA\JsonContent(
* ref="#/components/schemas/club relations"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function relations(Request $request, int $id)
{
$results = Club::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Club::scrape($id);
if (HttpHelper::hasError($response)) {
return HttpResponse::notFound($request);
}
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Club::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Club::query()
->where('mal_id', $id)
->update($response);
}
$results = Club::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new \App\Http\Resources\V4\ClubRelationsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
}

View File

@ -0,0 +1,246 @@
<?php
namespace App\Http\Controllers\V4DB;
use App\Http\HttpHelper;
use App\Providers\SerializerFactory;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\MyAnimeList\MalClient;
use JMS\Serializer\Serializer;
use Laravel\Lumen\Routing\Controller as BaseController;
use MongoDB\BSON\UTCDateTime;
/**
* Class Controller
* @package App\Http\Controllers\V4DB
*/
class Controller extends BaseController
{
/**
* @OA\OpenApi(
* @OA\Info(
* version="4.0.0",
* title="Jikan API",
* description=API_DESCRIPTION,
* termsOfService="https://jikan.moe/terms",
*
* @OA\Contact(
* name="API Support (Discord)",
* url="http://discord.jikan.moe"
* ),
* @OA\License(
* name="MIT",
* url="https://github.com/jikan-me/jikan-rest/blob/master/LICENSE"
* )
* ),
* @OA\Server(
* description="Jikan REST API Beta",
* url="https://api.jikan.moe/v4",
* ),
* @OA\ExternalDocumentation(
* description="About",
* url="https://jikan.moe"
* ),
* )
*/
/**
* @var Serializer
*/
protected $serializer;
/**
* @var MalClient
*/
protected $jikan;
/**
* @var Request
*/
private $request;
/**
* @var array
*/
private $response;
protected $expired = false;
protected $fingerprint;
/**
* AnimeController constructor.
*
* @param Serializer $serializer
* @param MalClient $jikan
*/
public function __construct(Request $request, MalClient $jikan)
{
$this->serializer = SerializerFactory::createV4();
$this->jikan = $jikan;
$this->fingerprint = HttpHelper::resolveRequestFingerprint($request);
}
protected function isExpired($request, $results) : bool
{
$lastModified = $this->getLastModified($results);
if ($lastModified === null) {
return true;
}
$routeName = HttpHelper::getRouteName($request);
$expiry = (int) config("controller.{$routeName}.ttl") + $lastModified;
if (time() > $expiry) {
return true;
}
return false;
}
protected function getExpiry($results, $request)
{
$modifiedAt = $this->getLastModified($results);
$routeName = HttpHelper::getRouteName($request);
return (int) config("controller.{$routeName}.ttl") + $modifiedAt;
}
protected function getTtl($results, $request)
{
$routeName = HttpHelper::getRouteName($request);
return (int) config("controller.{$routeName}.ttl");
}
protected function getLastModified($results) : ?int
{
if (is_array($results->first())) {
return (int) $results->first()['modifiedAt']->toDateTime()->format('U');
}
if (is_object($results->first())) {
return (int) $results->first()->modifiedAt->toDateTime()->format('U');
}
return null;
}
protected function serialize($data) : array
{
return \json_decode(
$this->serializer->serialize($data, 'json')
);
}
protected function getRouteName($request) : string
{
return HttpHelper::getRouteName($request);
}
protected function getRouteTable($request) : string
{
return config("controller.{$this->getRouteName($request)}.table_name");
}
protected function prepareResponse($response, $results, $request)
{
return $response
->header('X-Request-Fingerprint', $this->fingerprint)
->setTtl($this->getTtl($results, $request))
->setExpires(
(new \DateTimeImmutable())->setTimestamp(
$this->getExpiry($results, $request)
)
)
->setLastModified(
(new \DateTimeImmutable())->setTimestamp(
$this->getLastModified($results)
)
);
}
protected function updateCache($request, $results, $response)
{
// If resource doesn't exist, prepare meta
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
// Update `modifiedAt` meta
$meta['modifiedAt'] = new UTCDateTime();
// join meta data with response
$response = $meta + $response;
// insert cache if resource doesn't exist
if ($results->isEmpty()) {
DB::table($this->getRouteTable($request))
->insert($response);
}
// update cache if resource exists
if ($this->isExpired($request, $results)) {
DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->update($response);
}
// return results
return DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
}
/**
* @param array $response
* @return array
*/
// protected function prepareResponse(Request $request, array $response) : array
// {
// $this->request = $request;
// $this->response = $response;
//
// unset($this->response['_id']);
//
// $this->mutation();
//
// $this->response = ['data' => $this->response];
// return $this->response;
// }
/**
* @param Request $request
* @param array $response
* @return array
*/
private function mutation() : void
{
$requestType = HttpHelper::requestType($this->request);
if (($requestType === 'anime' || $requestType === 'manga')) {
// Fix JSON response for empty related object
if (isset($this->response['related']) && \count($this->response['related']) === 0) {
$this->response['related'] = new \stdClass();
}
if (isset($this->response['related']) && !is_object($this->response['related']) && !empty($this->response['related'])) {
$relation = [];
foreach ($this->response['related'] as $relationType => $related) {
$relation[] = [
'relation' => $relationType,
'entry' => $related
];
}
$this->response['related'] = $relation;
}
}
}
}

View File

@ -0,0 +1,148 @@
<?php
namespace App\Http\Controllers\V4DB;
use App\Anime;
use App\GenreAnime;
use App\GenreManga;
use App\Http\QueryBuilder\SearchQueryBuilderGenre;
use App\Http\Resources\V4\AnimeCollection;
use App\Http\Resources\V4\GenreCollection;
use App\Http\Resources\V4\MangaCollection;
use App\Manga;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Support\Facades\DB;
class GenreController extends Controller
{
/**
* @OA\Get(
* path="/genres/anime",
* operationId="getAnimeGenres",
* tags={"genres"},
*
* @OA\Parameter(ref="#/components/parameters/page"),
* @OA\Parameter(ref="#/components/parameters/limit"),
*
* @OA\Parameter(
* name="filter",
* in="query",
* @OA\Schema(ref="#/components/schemas/genre query filter")
* ),
* @OA\Response(
* response="200",
* description="Returns entry genres, explicit_genres, themes and demographics",
* @OA\JsonContent(
* ref="#/components/schemas/genres"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function anime(Request $request): GenreCollection
{
$filter = $request->get('filter') ?? null;
$explicitGenres = DB::table('explicit_genres_anime')->get();
$themes = DB::table('themes_anime')->get();
$demographics = DB::table('demographics_anime')->get();
switch ($filter) {
case 'genres':
$results = GenreAnime::query()
->get();
break;
case 'explicit_genres':
$results = $explicitGenres;
break;
case 'themes':
$results = $themes;
break;
case 'demographics':
$results = $demographics;
break;
default:
$results = GenreAnime::query()
->get()
->concat($explicitGenres->all())
->concat($themes->all())
->concat($demographics->all());
break;
}
return new GenreCollection(
$results
);
}
/**
* @OA\Get(
* path="/genres/manga",
* operationId="getMangaGenres",
* tags={"genres"},
*
* @OA\Parameter(ref="#/components/parameters/page"),
* @OA\Parameter(ref="#/components/parameters/limit"),
*
* @OA\Parameter(
* name="filter",
* in="query",
* @OA\Schema(ref="#/components/schemas/genre query filter")
* ),
* @OA\Response(
* response="200",
* description="Returns entry genres, explicit_genres, themes and demographics",
* @OA\JsonContent(
* ref="#/components/schemas/genres"
* )
* ),
*
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function manga(Request $request): GenreCollection
{
$filter = $request->get('filter') ?? null;
$explicitGenres = DB::table('explicit_genres_manga')->get();
$themes = DB::table('themes_manga')->get();
$demographics = DB::table('demographics_manga')->get();
switch ($filter) {
case 'genres':
$results = GenreManga::query()
->get();
break;
case 'explicit_genres':
$results = $explicitGenres;
break;
case 'themes':
$results = $themes;
break;
case 'demographics':
$results = $demographics;
break;
default:
$results = GenreManga::query()
->get()
->concat($explicitGenres->all())
->concat($themes->all())
->concat($demographics->all());
break;
}
return new GenreCollection(
$results
);
}
}

View File

@ -0,0 +1,155 @@
<?php
namespace App\Http\Controllers\V4DB;
use App\Http\Resources\V4\InsightsCollection;
use App\Http\Resources\V4\TrendsCollection;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\DB;
use MongoDB\BSON\Regex;
class InsightsController extends Controller
{
public function main(Request $request)
{
if (!env('INSIGHTS')) {
return response()->json([
'status' => 403,
'type' => 'InsightsRuntimeException',
'message' => 'Insights service is disabled',
'error' => null
], 403);
}
$maxResultsPerPage = (int) env('MAX_RESULTS_PER_PAGE', 25);
$page = $request->get('page') ?? 1;
$limit = $request->get('limit') ?? $maxResultsPerPage;
$limit = (int) $limit;
if ($limit <= 0) {
$limit = 1;
}
if ($limit > $maxResultsPerPage) {
$limit = $maxResultsPerPage;
}
$results = DB::table('insights')
->where('timestamp', '>', time() - env('INSIGHTS_MAX_STORE_TIME', 172800) )
->orderBy('timestamp', 'desc')
->paginate(
$limit,
['*'],
null,
$page
);
return new InsightsCollection(
$results
);
}
const TRENDS = [
'anime',
'manga',
'people',
'characters',
];
public function trends(Request $request)
{
if (!env('INSIGHTS')) {
return response()->json([
'status' => 403,
'type' => 'InsightsRuntimeException',
'message' => 'Insights service is disabled',
'error' => null
], 403);
}
$maxResultsPerPage = (int) env('MAX_RESULTS_PER_PAGE', 25);
$page = $request->get('page') ?? 1;
$limit = $request->get('limit') ?? $maxResultsPerPage;
$trend = $request->get('trend') ?? null;
$limit = (int) $limit;
if ($limit <= 0) {
$limit = 1;
}
if ($limit > $maxResultsPerPage) {
$limit = $maxResultsPerPage;
}
if (is_null($trend) || !in_array($trend, self::TRENDS)) {
return response()->json([
'status' => 400,
'type' => 'BadRequestException',
'message' => 'Trend value is invalid',
'error' => null
], 400);
}
// $results = DB::table('insights')
// ->where('url', 'regexp',"/\/v(\d)\/{$trend}\/(\d+).*/i")
// ->where('timestamp', '>', time() - env('INSIGHTS_MAX_STORE_TIME', 172800) )
// ->orderBy('timestamp', 'desc')
// ->paginate(
// $limit,
// ['*'],
// null,
// $page
// );
$results = DB::table('insights')
// ->where('url', 'regexp',"/\/v(\d)\/{$trend}\/(\d+).*/i")
->where('timestamp', '>', time() - env('INSIGHTS_MAX_STORE_TIME', 172800) )
->orderBy('timestamp', 'desc')
->raw(fn($collection) => $collection->aggregate([
[
'$group' => [
'_id' => [
'url' => '$url',
'timestamp' => '$timestamp'
],
'urlCount' => [ '$sum' => 1 ]
]
],
[
'$group' => [
'_id' => '$_id.url',
'count' => [ '$sum' => '$urlCount' ]
]
],
[
'$sort' => [ 'count' => -1 ]
],
['$skip' => ($page - 1) * $maxResultsPerPage],
['$limit' => $maxResultsPerPage],
]));
// ->raw(fn($collection) => $collection->aggregate([
// [
// '$group' => [
// '_id' => '$_id',
// 'url' => ['$first' => '$url'],
// 'count' => [ '$sum' => 1 ]
// ]
// ]
// ]));
return new TrendsCollection(
new LengthAwarePaginator(
$results, DB::table('insights')->count(), $maxResultsPerPage, $page
)
);
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers\V4DB;
use App\Http\QueryBuilder\SearchQueryBuilderMagazine;
use App\Http\Resources\V4\MagazineCollection;
use App\Http\Resources\V4\MangaCollection;
use App\Magazine;
use App\Manga;
use Illuminate\Http\Request;
class MagazineController extends Controller
{
const MAX_RESULTS_PER_PAGE = 25;
/**
* @OA\Get(
* path="/magazines",
* operationId="getMagazines",
* tags={"magazines"},
*
* @OA\Response(
* response="200",
* description="Returns magazines collection",
* @OA\JsonContent(
* ref="#/components/schemas/magazines"
* )
* ),
*
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function main(Request $request)
{
$page = $request->get('page') ?? 1;
$limit = $request->get('limit') ?? self::MAX_RESULTS_PER_PAGE;
if (!empty($limit)) {
$limit = (int) $limit;
if ($limit <= 0) {
$limit = 1;
}
if ($limit > self::MAX_RESULTS_PER_PAGE) {
$limit = self::MAX_RESULTS_PER_PAGE;
}
}
$results = SearchQueryBuilderMagazine::query(
$request,
Magazine::query()
);
$results = $results
->paginate(
$limit,
['*'],
null,
$page
);
return new MagazineCollection(
$results
);
}
public function resource(Request $request, int $id)
{
$page = $request->get('page') ?? 1;
$results = Manga::query()
->where('serializations.mal_id', $id)
->orderBy('title');
$results = $results
->paginate(
self::MAX_RESULTS_PER_PAGE,
['*'],
null,
$page
);
return new MangaCollection(
$results
);
}
}

View File

@ -0,0 +1,658 @@
<?php
namespace App\Http\Controllers\V4DB;
use App\Anime;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use App\Http\Resources\V4\AnimeCharactersResource;
use App\Http\Resources\V4\AnimeForumResource;
use App\Http\Resources\V4\MangaRelationsResource;
use App\Http\Resources\V4\ResultsResource;
use App\Http\Resources\V4\ReviewsResource;
use App\Http\Resources\V4\UserUpdatesResource;
use App\Http\Resources\V4\RecommendationsResource;
use App\Http\Resources\V4\MoreInfoResource;
use App\Http\Resources\V4\AnimeNewsResource;
use App\Http\Resources\V4\AnimeStatisticsResource;
use App\Http\Resources\V4\ForumResource;
use App\Http\Resources\V4\MangaCharactersResource;
use App\Http\Resources\V4\MangaStatisticsResource;
use App\Http\Resources\V4\NewsResource;
use App\Http\Resources\V4\PicturesResource;
use App\Manga;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Request\Anime\AnimeCharactersAndStaffRequest;
use Jikan\Request\Anime\AnimeForumRequest;
use Jikan\Request\Anime\AnimeMoreInfoRequest;
use Jikan\Request\Anime\AnimeNewsRequest;
use Jikan\Request\Anime\AnimePicturesRequest;
use Jikan\Request\Anime\AnimeRecentlyUpdatedByUsersRequest;
use Jikan\Request\Anime\AnimeRecommendationsRequest;
use Jikan\Request\Anime\AnimeReviewsRequest;
use Jikan\Request\Anime\AnimeStatsRequest;
use Jikan\Request\Manga\MangaCharactersRequest;
use Jikan\Request\Manga\MangaForumRequest;
use Jikan\Request\Manga\MangaMoreInfoRequest;
use Jikan\Request\Manga\MangaNewsRequest;
use Jikan\Request\Manga\MangaPicturesRequest;
use Jikan\Request\Manga\MangaRecentlyUpdatedByUsersRequest;
use Jikan\Request\Manga\MangaRecommendationsRequest;
use Jikan\Request\Manga\MangaRequest;
use Jikan\Request\Manga\MangaReviewsRequest;
use Jikan\Request\Manga\MangaStatsRequest;
use MongoDB\BSON\UTCDateTime;
use mysql_xdevapi\Result;
class MangaController extends Controller
{
/**
* @OA\Get(
* path="/manga/{id}",
* operationId="getMangaById",
* tags={"manga"},
*
* @OA\Response(
* response="200",
* description="Returns pictures related to the entry",
* @OA\JsonContent(
* ref="#/components/schemas/manga"
* )
* ),
*
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function main(Request $request, int $id)
{
$results = Manga::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Manga::scrape($id);
if (HttpHelper::hasError($response)) {
return HttpResponse::notFound($request);
}
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Manga::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Manga::query()
->where('mal_id', $id)
->update($response);
}
$results = Manga::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new \App\Http\Resources\V4\MangaResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/manga/{id}/characters",
* operationId="getMangaCharacters",
* tags={"manga"},
*
* @OA\Response(
* response="200",
* description="Returns manga characters resource",
* @OA\JsonContent(
* ref="#/components/schemas/manga characters"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function characters(Request $request, int $id)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$manga = ['characters' => $this->jikan->getMangaCharacters(new MangaCharactersRequest($id))];
$response = \json_decode($this->serializer->serialize($manga, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new MangaCharactersResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/manga/{id}/news",
* operationId="getMangaNews",
* tags={"manga"},
*
* @OA\Response(
* response="200",
* description="Returns a list of manga news topics",
* @OA\JsonContent(
* ref="#/components/schemas/manga news"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*
* @OA\Schema(
* schema="manga news",
* description="Manga News Resource",
*
* allOf={
* @OA\Schema(ref="#/components/schemas/pagination"),
* @OA\Schema(ref="#/components/schemas/news"),
* }
* )
*/
public function news(Request $request, int $id)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$manga = $this->jikan->getNewsList(new MangaNewsRequest($id, $page));
$response = \json_decode($this->serializer->serialize($manga, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/manga/{id}/forum",
* operationId="getMangaTopics",
* tags={"manga"},
*
* @OA\Response(
* response="200",
* description="Returns a list of manga forum topics",
* @OA\JsonContent(
* ref="#/components/schemas/forum"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function forum(Request $request, int $id)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$topic = $request->get('topic');
$manga = ['topics' => $this->jikan->getMangaForum(new MangaForumRequest($id))];
$response = \json_decode($this->serializer->serialize($manga, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ForumResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/manga/{id}/pictures",
* operationId="getMangaPictures",
* tags={"manga"},
*
* @OA\Response(
* response="200",
* description="Returns a list of manga forum topics",
* @OA\JsonContent(
* ref="#/components/schemas/pictures"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
* @OA\Schema(
* schema="manga pictures",
* description="Manga Pictures",
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(
* @OA\Property(
* property="image_url",
* type="string",
* description="Default JPG Image Size URL"
* ),
* @OA\Property(
* property="large_image_url",
* type="string",
* description="Large JPG Image Size URL"
* ),
* )
* )
* )
*/
public function pictures(Request $request, int $id)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$manga = ['pictures' => $this->jikan->getMangaPictures(new MangaPicturesRequest($id))];
$response = \json_decode($this->serializer->serialize($manga, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new PicturesResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/manga/{id}/statistics",
* operationId="getMangaStatistics",
* tags={"manga"},
*
* @OA\Response(
* response="200",
* description="Returns anime statistics",
* @OA\JsonContent(
* ref="#/components/schemas/manga statistics"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function stats(Request $request, int $id)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$manga = $this->jikan->getMangaStats(new MangaStatsRequest($id));
$response = \json_decode($this->serializer->serialize($manga, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new MangaStatisticsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/manga/{id}/moreinfo",
* operationId="getMangaMoreInfo",
* tags={"manga"},
*
* @OA\Response(
* response="200",
* description="Returns manga moreinfo",
* @OA\JsonContent(
* ref="#/components/schemas/moreinfo"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function moreInfo(Request $request, int $id)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$manga = ['moreinfo' => $this->jikan->getMangaMoreInfo(new MangaMoreInfoRequest($id))];
$response = \json_decode($this->serializer->serialize($manga, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new MoreInfoResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/manga/{id}/recommendations",
* operationId="getMangaRecommendations",
* tags={"manga"},
*
* @OA\Response(
* response="200",
* description="Returns manga recommendations",
* @OA\JsonContent(
* ref="#/components/schemas/entry recommendations"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function recommendations(Request $request, int $id)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$manga = ['recommendations' => $this->jikan->getMangaRecommendations(new MangaRecommendationsRequest($id))];
$response = \json_decode($this->serializer->serialize($manga, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new RecommendationsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/manga/{id}/userupdates",
* operationId="getMangaUserUpdates",
* tags={"manga"},
*
* @OA\Response(
* response="200",
* description="Returns manga user updates",
* @OA\JsonContent(
* ref="#/components/schemas/manga userupdates"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function userupdates(Request $request, int $id)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$manga = $this->jikan->getMangaRecentlyUpdatedByUsers(new MangaRecentlyUpdatedByUsersRequest($id, $page));
$response = \json_decode($this->serializer->serialize($manga, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/manga/{id}/reviews",
* operationId="getMangaReviews",
* tags={"manga"},
*
* @OA\Response(
* response="200",
* description="Returns manga reviews",
* @OA\JsonContent(
* ref="#/components/schemas/manga reviews"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function reviews(Request $request, int $id)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$manga = $this->jikan->getMangaReviews(new MangaReviewsRequest($id, $page));
$response = \json_decode($this->serializer->serialize($manga, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ReviewsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/manga/{id}/relations",
* operationId="getMangaRelations",
* tags={"manga"},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response="200",
* description="Returns manga relations",
* @OA\JsonContent(
* ref="#/components/schemas/relation"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function relations(Request $request, int $id)
{
$results = Manga::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Manga::scrape($id);
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Manga::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Manga::query()
->where('mal_id', $id)
->update($response);
}
$results = Manga::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new MangaRelationsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\V3;
namespace App\Http\Controllers\V4DB;
class MetaController extends Controller
{

View File

@ -0,0 +1,408 @@
<?php
namespace App\Http\Controllers\V4DB;
use App\Character;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use App\Http\Resources\V4\PersonAnimeCollection;
use App\Http\Resources\V4\PersonAnimeResource;
use App\Http\Resources\V4\PersonMangaCollection;
use App\Http\Resources\V4\PersonVoiceResource;
use App\Http\Resources\V4\PersonVoicesCollection;
use App\Http\Resources\V4\PicturesResource;
use App\Person;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Request\Character\CharacterPicturesRequest;
use Jikan\Request\Person\PersonRequest;
use Jikan\Request\Person\PersonPicturesRequest;
use MongoDB\BSON\UTCDateTime;
class PersonController extends Controller
{
/**
* @OA\Get(
* path="/people/{id}",
* operationId="getPersonById",
* tags={"people"},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response="200",
* description="Returns pictures related to the entry",
* @OA\JsonContent(
* ref="#/components/schemas/pictures variants"
* )
* ),
*
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function main(Request $request, int $id)
{
$results = Person::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Person::scrape($id);
if (HttpHelper::hasError($response)) {
return HttpResponse::notFound($request);
}
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Person::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Person::query()
->where('mal_id', $id)
->update($response);
}
$results = Person::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new \App\Http\Resources\V4\PersonResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/people/{id}/anime",
* operationId="getPersonAnime",
* tags={"people"},
*
* @OA\Response(
* response="200",
* description="Returns person's anime staff positions",
* @OA\JsonContent(ref="#/components/schemas/person anime")
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function anime(Request $request, int $id)
{
$results = Person::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Person::scrape($id);
if (HttpHelper::hasError($response)) {
return HttpResponse::notFound($request);
}
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Person::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Person::query()
->where('mal_id', $id)
->update($response);
}
$results = Person::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new PersonAnimeCollection(
$results->first()['anime_staff_positions']
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/people/{id}/voices",
* operationId="getPersonVoices",
* tags={"people"},
*
* @OA\Response(
* response="200",
* description="Returns person's voice acting roles",
* @OA\JsonContent(ref="#/components/schemas/person voice acting roles")
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function voices(Request $request, int $id)
{
$results = Person::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Person::scrape($id);
if (HttpHelper::hasError($response)) {
return HttpResponse::notFound($request);
}
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Person::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Person::query()
->where('mal_id', $id)
->update($response);
}
$results = Person::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new PersonVoicesCollection(
$results->first()['voice_acting_roles']
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/people/{id}/manga",
* operationId="getPersonManga",
* tags={"people"},
*
* @OA\Response(
* response="200",
* description="Returns person's published manga works",
* @OA\JsonContent(ref="#/components/schemas/person manga")
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function manga(Request $request, int $id)
{
$results = Person::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Person::scrape($id);
if (HttpHelper::hasError($response)) {
return HttpResponse::notFound($request);
}
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Person::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Person::query()
->where('mal_id', $id)
->update($response);
}
$results = Person::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new PersonMangaCollection(
$results->first()['published_manga']
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/people/{id}/pictures",
* operationId="getPersonPictures",
* tags={"people"},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response="200",
* description="Returns a list of pictures of the person",
* @OA\JsonContent(
* ref="#/components/schemas/person pictures"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*
* @OA\Schema(
* schema="person pictures",
* description="Character Pictures",
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(
* @OA\Property(
* property="image_url",
* type="string",
* description="Default JPG Image Size URL"
* ),
* @OA\Property(
* property="large_image_url",
* type="string",
* description="Large JPG Image Size URL"
* ),
* )
* )
* )
*/
public function pictures(Request $request, int $id)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$person = ['pictures' => $this->jikan->getPersonPictures(new PersonPicturesRequest($id))];
$response = \json_decode($this->serializer->serialize($person, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new PicturesResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace App\Http\Controllers\V4DB;
use App\Anime;
use App\Http\QueryBuilder\SearchQueryBuilderProducer;
use App\Http\Resources\V4\AnimeCollection;
use App\Http\Resources\V4\ProducerCollection;
use App\Producer;
use Illuminate\Http\Request;
class ProducerController extends Controller
{
private $request;
const MAX_RESULTS_PER_PAGE = 100;
/**
* @OA\Get(
* path="/producers",
* operationId="getProducers",
* tags={"producers"},
*
* @OA\Response(
* response="200",
* description="Returns producers collection",
* @OA\JsonContent(
* ref="#/components/schemas/producers"
* )
* ),
*
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function main(Request $request)
{
$page = $request->get('page') ?? 1;
$limit = $request->get('limit') ?? self::MAX_RESULTS_PER_PAGE;
if (!empty($limit)) {
$limit = (int) $limit;
if ($limit <= 0) {
$limit = 1;
}
if ($limit > self::MAX_RESULTS_PER_PAGE) {
$limit = self::MAX_RESULTS_PER_PAGE;
}
}
$results = SearchQueryBuilderProducer::query(
$request,
Producer::query()
);
$results = $results
->paginate(
$limit,
['*'],
null,
$page
);
return new ProducerCollection(
$results
);
}
public function resource(Request $request, int $id)
{
$this->request = $request;
$page = $this->request->get('page') ?? 1;
$results = Anime::query()
->where('producers.mal_id', $id)
->orWhere('licensors.mal_id', $id)
->orWhere('studios.mal_id', $id)
->orderBy('title');
$results = $results
->paginate(
self::MAX_RESULTS_PER_PAGE,
['*'],
null,
$page
);
return new AnimeCollection(
$results
);
}
}

View File

@ -0,0 +1,231 @@
<?php
namespace App\Http\Controllers\V4DB;
use App\Anime;
use App\Character;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use App\Http\Resources\V4\AnimeCollection;
use App\Http\Resources\V4\AnimeResource;
use App\Http\Resources\V4\CharacterCollection;
use App\Http\Resources\V4\CharacterResource;
use App\Http\Resources\V4\CommonResource;
use App\Http\Resources\V4\MangaCollection;
use App\Http\Resources\V4\MangaResource;
use App\Http\Resources\V4\PersonCollection;
use App\Http\Resources\V4\PersonResource;
use App\Http\Resources\V4\ProfileResource;
use App\Http\Resources\V4\ResultsResource;
use App\Http\Resources\V4\UserCollection;
use App\Manga;
use App\Person;
use App\Profile;
use App\User;
use Illuminate\Http\Request;
use MongoDB\BSON\UTCDateTime;
class RandomController extends Controller
{
/**
* @OA\Schema(
* schema="random",
* description="Random Resources",
*
* @OA\Property(
* property="data",
* type="array",
* @OA\Items(
* type="object",
* anyOf={
* @OA\Schema(ref="#/components/schemas/anime"),
* @OA\Schema(ref="#/components/schemas/manga"),
* @OA\Schema(ref="#/components/schemas/character"),
* @OA\Schema(ref="#/components/schemas/person"),
* }
* ),
* ),
* ),
* }
* ),
*/
/**
* @OA\Get(
* path="/random/anime",
* operationId="getRandomAnime",
* tags={"random"},
*
* @OA\Response(
* response="200",
* description="Returns a random anime resource",
* @OA\JsonContent(
* @OA\Property(
* property="data",
* ref="#/components/schemas/anime"
* )
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*/
public function anime(Request $request)
{
$sfw = $request->get('sfw');
$results = Anime::query();
if (!is_null($sfw)) {
$results = $results
->where('rating', '!=', 'Rx - Hentai');
}
$results = $results
->raw(fn($collection) => $collection->aggregate([ ['$sample' => ['size' => 1]] ]));
return new AnimeResource(
$results->first()
);
}
/**
* @OA\Get(
* path="/random/manga",
* operationId="getRandomManga",
* tags={"random"},
*
* @OA\Response(
* response="200",
* description="Returns a random manga resource",
* @OA\JsonContent(
* @OA\Property(
* property="data",
* ref="#/components/schemas/manga"
* )
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*/
public function manga(Request $request)
{
$sfw = $request->get('sfw');
$results = Manga::query();
if (!is_null($sfw)) {
$results = $results
->where('type', '!=', 'Doujinshi');
}
$results = $results
->raw(fn($collection) => $collection->aggregate([ ['$sample' => ['size' => 1]] ]));
return new MangaResource(
$results->first()
);
}
/**
* @OA\Get(
* path="/random/characters",
* operationId="getRandomCharacters",
* tags={"random"},
*
* @OA\Response(
* response="200",
* description="Returns a random character resource",
* @OA\JsonContent(
* @OA\Property(
* property="data",
* ref="#/components/schemas/character"
* )
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*/
public function characters(Request $request)
{
$results = Character::query()
->raw(fn($collection) => $collection->aggregate([ ['$sample' => ['size' => 1]] ]));
return new CharacterResource(
$results->first()
);
}
/**
* @OA\Get(
* path="/random/people",
* operationId="getRandomPeople",
* tags={"random"},
*
* @OA\Response(
* response="200",
* description="Returns a random person resource",
* @OA\JsonContent(
* @OA\Property(
* property="data",
* ref="#/components/schemas/person"
* )
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*/
public function people(Request $request)
{
$results = Person::query()
->raw(fn($collection) => $collection->aggregate([ ['$sample' => ['size' => 1]] ]));
return new PersonResource(
$results->first()
);
}
/**
* @OA\Get(
* path="/random/users",
* operationId="getRandomUsers",
* tags={"random"},
*
* @OA\Response(
* response="200",
* description="Returns a random user profile resource",
* @OA\JsonContent(
* @OA\Property(
* property="data",
* ref="#/components/schemas/user profile"
* )
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*/
public function users(Request $request)
{
$results = Profile::query()
->raw(fn($collection) => $collection->aggregate([ ['$sample' => ['size' => 1]] ]));
return new ProfileResource(
$results->first()
);
}
}

View File

@ -0,0 +1,114 @@
<?php
namespace App\Http\Controllers\V4DB;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use App\Http\Resources\V4\ResultsResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Helper\Constants;
use Jikan\Request\Recommendations\RecentRecommendationsRequest;
use Jikan\Request\Reviews\RecentReviewsRequest;
use MongoDB\BSON\UTCDateTime;
class RecommendationsController extends Controller
{
/**
* @OA\Get(
* path="/recommendations/anime",
* operationId="getRecentAnimeRecommendations",
* tags={"recommendations"},
*
* @OA\Response(
* response="200",
* description="Returns recent anime recommendations",
* @OA\JsonContent(
* ref="#/components/schemas/recommendations"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*
*/
public function anime(Request $request)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$anime = $this->jikan->getRecentRecommendations(new RecentRecommendationsRequest(Constants::RECENT_RECOMMENDATION_ANIME, $page));
$response = \json_decode($this->serializer->serialize($anime, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/recommendations/manga",
* operationId="getRecentMangaRecommendations",
* tags={"recommendations"},
*
* @OA\Response(
* response="200",
* description="Returns recent manga recommendations",
* @OA\JsonContent(
* ref="#/components/schemas/recommendations"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*
*/
public function manga(Request $request)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$anime = $this->jikan->getRecentRecommendations(new RecentRecommendationsRequest(Constants::RECENT_RECOMMENDATION_MANGA, $page));
$response = \json_decode($this->serializer->serialize($anime, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
}

View File

@ -0,0 +1,172 @@
<?php
namespace App\Http\Controllers\V4DB;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use App\Http\Resources\V4\ResultsResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Helper\Constants;
use Jikan\Request\Reviews\RecentReviewsRequest;
use MongoDB\BSON\UTCDateTime;
class ReviewsController extends Controller
{
/**
* @OA\Get(
* path="/reviews/anime",
* operationId="getRecentAnimeReviews",
* tags={"reviews"},
*
* @OA\Response(
* response="200",
* description="Returns recent anime reviews",
* @OA\JsonContent(
* @OA\Property(
* property="data",
* allOf={
* @OA\Schema(ref="#/components/schemas/pagination"),
* @OA\Schema(
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(
* allOf={
* @OA\Schema(ref="#/components/schemas/anime review"),
* @OA\Schema(
* @OA\Property(
* property="anime",
* type="object",
* ref="#/components/schemas/anime meta",
* ),
* ),
* @OA\Schema(
* @OA\Property(
* property="user",
* type="object",
* ref="#/components/schemas/user meta",
* ),
* ),
* }
* )
* ),
* )
* }
* )
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*/
public function anime(Request $request)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$anime = $this->jikan->getRecentReviews(new RecentReviewsRequest(Constants::RECENT_REVIEW_ANIME, $page));
$response = \json_decode($this->serializer->serialize($anime, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/reviews/manga",
* operationId="getRecentMangaReviews",
* tags={"reviews"},
*
* @OA\Response(
* response="200",
* description="Returns recent manga reviews",
* @OA\JsonContent(
* @OA\Property(
* property="data",
* allOf={
* @OA\Schema(ref="#/components/schemas/pagination"),
* @OA\Schema(
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(
* allOf={
* @OA\Schema(ref="#/components/schemas/manga review"),
* @OA\Schema(
* @OA\Property(
* property="manga",
* type="object",
* ref="#/components/schemas/manga meta",
* ),
* ),
* @OA\Schema(
* @OA\Property(
* property="user",
* type="object",
* ref="#/components/schemas/user meta",
* ),
* ),
* }
* )
* ),
* )
* }
* )
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*/
public function manga(Request $request)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$anime = $this->jikan->getRecentReviews(new RecentReviewsRequest(Constants::RECENT_REVIEW_MANGA, $page));
$response = \json_decode($this->serializer->serialize($anime, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
}

View File

@ -0,0 +1,146 @@
<?php
namespace App\Http\Controllers\V4DB;
use App\Anime;
use App\Http\HttpResponse;
use App\Http\Resources\V4\AnimeCollection;
use App\Http\Resources\V4\AnimeResource;
use App\Http\Resources\V4\CommonResource;
use App\Http\Resources\V4\ScheduleResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Request\Schedule\ScheduleRequest;
class ScheduleController extends Controller
{
private const VALID_FILTERS = [
'monday',
'tuesday',
'wednesday',
'thursday',
'friday',
'saturday',
'sunday',
'other',
'unknown',
];
private const VALID_DAYS = [
'monday',
'tuesday',
'wednesday',
'thursday',
'friday',
'saturday',
'sunday',
];
private $request;
private $day;
/**
* @OA\Get(
* path="/schedules",
* operationId="getSchedules",
* tags={"schedules"},
*
* @OA\Parameter(
* name="topic",
* in="path",
* required=false,
* description="Filter by day",
* @OA\Schema(type="string",enum={"monday", "tuesday", "wednesday", "thursday", "friday", "unknown", "other"})
* ),
*
* @OA\Response(
* response="200",
* description="Returns weekly schedule",
* @OA\JsonContent(
* ref="#/components/schemas/schedules",
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*
* @OA\Schema(
* schema="schedules",
* description="Anime resources currently airing",
*
* allOf={
* @OA\Schema(ref="#/components/schemas/pagination"),
* @OA\Schema(
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(
* allOf={
* @OA\Schema(ref="#/components/schemas/anime"),
* }
* )
* ),
* )
* }
* )
*/
public function main(Request $request, ?string $day = null)
{
$this->request = $request;
$page = $this->request->get('page') ?? 1;
$limit = $this->request->get('limit') ?? env('MAX_RESULTS_PER_PAGE', 25);
if (!is_null($day)) {
$this->day = strtolower($day);
}
if (null !== $this->day
&& !\in_array($this->day, self::VALID_FILTERS, true)) {
return HttpResponse::badRequest($this->request);
}
$results = Anime::query()
->orderBy('members')
->where('type', 'TV')
->where('status', 'Currently Airing');
if ($this->day !== null && in_array($day, self::VALID_DAYS)) {
$this->day = ucfirst($this->day);
$results
->where('broadcast', 'like', "{$day}%");
}
if ($this->day === 'unknown') {
$results
->where('broadcast', 'Unknown');
}
if ($this->day === 'other') {
$results
->where('broadcast', 'Not scheduled once per week');
}
$results = $results
->paginate(
intval($limit),
['*'],
null,
$page
);
$response = (new AnimeCollection(
$results
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
}

View File

@ -0,0 +1,744 @@
<?php
namespace App\Http\Controllers\V4DB;
use App\Anime;
use App\Character;
use App\Club;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use App\Http\Middleware\Throttle;
use App\Http\QueryBuilder\SearchQueryBuilderAnime;
use App\Http\QueryBuilder\SearchQueryBuilderCharacter;
use App\Http\QueryBuilder\SearchQueryBuilderClub;
use App\Http\QueryBuilder\SearchQueryBuilderManga;
use App\Http\QueryBuilder\SearchQueryBuilderPeople;
use App\Http\QueryBuilder\SearchQueryBuilderUsers;
use App\Http\Resources\V4\AnimeCharactersResource;
use App\Http\Resources\V4\AnimeCollection;
use App\Http\Resources\V4\CharacterCollection;
use App\Http\Resources\V4\ClubCollection;
use App\Http\Resources\V4\MangaCollection;
use App\Http\Resources\V4\PersonCollection;
use App\Http\Resources\V4\ResultsResource;
use App\Http\SearchQueryBuilder;
use App\Manga;
use App\Person;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Jikan;
use Jikan\MyAnimeList\MalClient;
use Jikan\Request\Anime\AnimeCharactersAndStaffRequest;
use Jikan\Request\Search\AnimeSearchRequest;
use Jikan\Request\Search\MangaSearchRequest;
use Jikan\Request\Search\CharacterSearchRequest;
use Jikan\Request\Search\PersonSearchRequest;
use Jikan\Helper\Constants as JikanConstants;
use Jikan\Request\Search\UserSearchRequest;
use Jikan\Request\User\UsernameByIdRequest;
use JMS\Serializer\Serializer;
use MongoDB\BSON\UTCDateTime;
use phpDocumentor\Reflection\Types\Object_;
class SearchController extends Controller
{
private $request;
const MAX_RESULTS_PER_PAGE = 25;
/**
* @OA\Parameter(
* name="page",
* in="query",
* @OA\Schema(type="integer")
* ),
* @OA\Parameter(
* name="limit",
* in="query",
* @OA\Schema(type="integer")
* ),
*
* @OA\Schema(
* schema="search query sort",
* description="Characters Search Query Sort",
* type="string",
* enum={"desc","asc"}
* )
*/
/**
* @OA\Get(
* path="/anime",
* operationId="getAnimeSearch",
* tags={"anime"},
*
* @OA\Parameter(ref="#/components/parameters/page"),
* @OA\Parameter(ref="#/components/parameters/limit"),
*
* @OA\Parameter(
* name="q",
* in="query",
* @OA\Schema(type="string")
* ),
*
* @OA\Parameter(
* name="type",
* in="query",
* @OA\Schema(ref="#/components/schemas/anime search query type")
* ),
*
* @OA\Parameter(
* name="score",
* in="query",
* @OA\Schema(type="number")
* ),
*
* @OA\Parameter(
* name="status",
* in="query",
* @OA\Schema(ref="#/components/schemas/anime search query status")
* ),
*
* @OA\Parameter(
* name="rating",
* in="query",
* @OA\Schema(ref="#/components/schemas/anime search query rating")
* ),
*
* @OA\Parameter(
* name="sfw",
* in="query",
* description="Filter out Adult entries",
* @OA\Schema(type="boolean")
* ),
*
* @OA\Parameter(
* name="genres",
* in="query",
* description="Filter by genre(s) IDs. Can pass multiple with a comma as a delimiter. e.g 1,2,3",
* @OA\Schema(type="string")
* ),
*
* @OA\Parameter(
* name="order_by",
* in="query",
* @OA\Schema(ref="#/components/schemas/anime search query orderby")
* ),
*
* @OA\Parameter(
* name="sort",
* in="query",
* @OA\Schema(ref="#/components/schemas/search query sort")
* ),
*
* @OA\Parameter(
* name="letter",
* in="query",
* description="Return entries starting with the given letter",
* @OA\Schema(type="string")
* ),
*
* @OA\Parameter(
* name="producer",
* in="query",
* description="Filter by producer(s) IDs. Can pass multiple with a comma as a delimiter. e.g 1,2,3",
* @OA\Schema(type="string")
* ),
*
* @OA\Response(
* response="200",
* description="Returns search results for anime",
* @OA\JsonContent(
* ref="#/components/schemas/anime search"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function anime(Request $request)
{
$this->request = $request;
$page = $this->request->get('page') ?? 1;
$limit = $this->request->get('limit') ?? self::MAX_RESULTS_PER_PAGE;
if (!empty($limit)) {
$limit = (int) $limit;
if ($limit <= 0) {
$limit = 1;
}
if ($limit > self::MAX_RESULTS_PER_PAGE) {
$limit = self::MAX_RESULTS_PER_PAGE;
}
}
$results = SearchQueryBuilderAnime::query(
$request,
Anime::query()
);
$results = $results
->paginate(
$limit,
['*'],
null,
$page
);
return new AnimeCollection(
$results
);
}
/**
* @OA\Get(
* path="/manga",
* operationId="getMangaSearch",
* tags={"manga"},
*
* @OA\Parameter(ref="#/components/parameters/page"),
* @OA\Parameter(ref="#/components/parameters/limit"),
*
* @OA\Parameter(
* name="q",
* in="query",
* @OA\Schema(type="string")
* ),
*
* @OA\Parameter(
* name="type",
* in="query",
* @OA\Schema(ref="#/components/schemas/manga search query type")
* ),
*
* @OA\Parameter(
* name="score",
* in="query",
* @OA\Schema(type="number")
* ),
*
* @OA\Parameter(
* name="status",
* in="query",
* @OA\Schema(ref="#/components/schemas/manga search query status")
* ),
*
* @OA\Parameter(
* name="sfw",
* in="query",
* description="Filter out Adult entries",
* @OA\Schema(type="boolean")
* ),
*
* @OA\Parameter(
* name="genres",
* in="query",
* description="Filter by genre(s) IDs. Can pass multiple with a comma as a delimiter. e.g 1,2,3",
* @OA\Schema(type="string")
* ),
*
* @OA\Parameter(
* name="order_by",
* in="query",
* @OA\Schema(ref="#/components/schemas/manga search query orderby")
* ),
*
* @OA\Parameter(
* name="sort",
* in="query",
* @OA\Schema(ref="#/components/schemas/search query sort")
* ),
*
* @OA\Parameter(
* name="letter",
* in="query",
* description="Return entries starting with the given letter",
* @OA\Schema(type="string")
* ),
*
* @OA\Parameter(
* name="magazine",
* in="query",
* description="Filter by producer(s) IDs. Can pass multiple with a comma as a delimiter. e.g 1,2,3",
* @OA\Schema(type="string")
* ),
*
* @OA\Response(
* response="200",
* description="Returns search results for manga",
* @OA\JsonContent(
* ref="#/components/schemas/manga search"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function manga(Request $request)
{
$this->request = $request;
$page = $this->request->get('page') ?? 1;
$limit = $this->request->get('limit') ?? self::MAX_RESULTS_PER_PAGE;
if (!empty($limit)) {
$limit = (int) $limit;
if ($limit <= 0) {
$limit = 1;
}
if ($limit > self::MAX_RESULTS_PER_PAGE) {
$limit = self::MAX_RESULTS_PER_PAGE;
}
}
$results = SearchQueryBuilderManga::query(
$request,
Manga::query()
);
$results = $results
->paginate(
$limit,
['*'],
null,
$page
);
return new MangaCollection(
$results
);
}
/**
* @OA\Get(
* path="/people",
* operationId="getPeopleSearch",
* tags={"people"},
*
* @OA\Parameter(ref="#/components/parameters/page"),
* @OA\Parameter(ref="#/components/parameters/limit"),
*
* @OA\Parameter(
* name="q",
* in="query",
* @OA\Schema(type="string")
* ),
*
* @OA\Parameter(
* name="order_by",
* in="query",
* @OA\Schema(ref="#/components/schemas/people search query orderby")
* ),
*
* @OA\Parameter(
* name="sort",
* in="query",
* @OA\Schema(ref="#/components/schemas/search query sort")
* ),
*
* @OA\Parameter(
* name="letter",
* in="query",
* description="Return entries starting with the given letter",
* @OA\Schema(type="string")
* ),
*
* @OA\Response(
* response="200",
* description="Returns search results for people",
* @OA\JsonContent(ref="#/components/schemas/people search")
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function people(Request $request)
{
$page = $request->get('page') ?? 1;
$limit = $request->get('limit') ?? self::MAX_RESULTS_PER_PAGE;
if (!empty($limit)) {
$limit = (int) $limit;
if ($limit <= 0) {
$limit = 1;
}
if ($limit > self::MAX_RESULTS_PER_PAGE) {
$limit = self::MAX_RESULTS_PER_PAGE;
}
}
$results = SearchQueryBuilderPeople::query(
$request,
Person::query()
);
$results = $results
->paginate(
$limit,
['*'],
null,
$page
);
return new PersonCollection(
$results
);
}
/**
* @OA\Get(
* path="/characters",
* operationId="getCharactersSearch",
* tags={"characters"},
*
* @OA\Parameter(ref="#/components/parameters/page"),
* @OA\Parameter(ref="#/components/parameters/limit"),
*
* @OA\Parameter(
* name="q",
* in="query",
* @OA\Schema(type="string")
* ),
*
* @OA\Parameter(
* name="order_by",
* in="query",
* @OA\Schema(ref="#/components/schemas/characters search query orderby")
* ),
*
* @OA\Parameter(
* name="sort",
* in="query",
* @OA\Schema(ref="#/components/schemas/search query sort")
* ),
*
* @OA\Parameter(
* name="letter",
* in="query",
* description="Return entries starting with the given letter",
* @OA\Schema(type="string")
* ),
*
* @OA\Response(
* response="200",
* description="Returns search results for characters",
* @OA\JsonContent(
* ref="#/components/schemas/characters search"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function character(Request $request)
{
$page = $request->get('page') ?? 1;
$limit = $request->get('limit') ?? self::MAX_RESULTS_PER_PAGE;
if (!empty($limit)) {
$limit = (int) $limit;
if ($limit <= 0) {
$limit = 1;
}
if ($limit > self::MAX_RESULTS_PER_PAGE) {
$limit = self::MAX_RESULTS_PER_PAGE;
}
}
$results = SearchQueryBuilderCharacter::query(
$request,
Character::query()
);
$results = $results
->paginate(
$limit,
['*'],
null,
$page
);
return new CharacterCollection(
$results
);
}
/**
* @OA\Get(
* path="/users",
* operationId="getUsersSearch",
* tags={"users"},
*
* @OA\Parameter(ref="#/components/parameters/page"),
* @OA\Parameter(ref="#/components/parameters/limit"),
*
* @OA\Parameter(
* name="q",
* in="query",
* @OA\Schema(type="string")
* ),
*
* @OA\Parameter(
* name="gender",
* in="query",
* @OA\Schema(ref="#/components/schemas/users search query gender")
* ),
*
* @OA\Parameter(
* name="location",
* in="query",
* @OA\Schema(type="string")
* ),
*
* @OA\Parameter(
* name="maxAge",
* in="query",
* @OA\Schema(type="integer")
* ),
*
* @OA\Parameter(
* name="minAge",
* in="query",
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response="200",
* description="Returns search results for users",
* @OA\JsonContent(
* ref="#/components/schemas/users search"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*
* @OA\Schema(
* schema="users search",
* description="User Results",
*
* allOf={
* @OA\Schema(ref="#/components/schemas/pagination"),
* @OA\Schema(
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(
* type="object",
* @OA\Schema(
* @OA\Property(
* property="url",
* type="string",
* description="MyAnimeList URL"
* ),
* @OA\Property(
* property="username",
* type="string",
* description="MyAnimeList Username"
* ),
* @OA\Property(
* ref="#/components/schemas/user images"
* ),
* @OA\Property(
* property="last_online",
* type="string",
* description="Last Online Date ISO8601"
* ),
* ),
* ),
* ),
* ),
* },
* ),
*/
public function users(Request $request)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$anime = $this->jikan->getUserSearch(
SearchQueryBuilderUsers::query(
$request
)
);
$response = \json_decode($this->serializer->serialize($anime, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/userbyid",
* operationId="getUserById",
* tags={"users"},
*
* @OA\Response(
* response="200",
* description="Returns username by ID search",
* @OA\JsonContent(
* ref="#/components/schemas/user by id"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*
*
*/
public function userById(Request $request, int $id)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$anime = ['results'=>$this->jikan->getUsernameById(new UsernameByIdRequest($id))];
$response = \json_decode($this->serializer->serialize($anime, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/clubs",
* operationId="getClubsSearch",
* tags={"clubs"},
*
* @OA\Parameter(ref="#/components/parameters/page"),
* @OA\Parameter(ref="#/components/parameters/limit"),
*
* @OA\Parameter(
* name="q",
* in="query",
* @OA\Schema(type="string")
* ),
*
* @OA\Parameter(
* name="type",
* in="query",
* @OA\Schema(ref="#/components/schemas/club search query type")
* ),
*
* @OA\Parameter(
* name="category",
* in="query",
* @OA\Schema(ref="#/components/schemas/club search query category")
* ),
*
* @OA\Parameter(
* name="order_by",
* in="query",
* @OA\Schema(ref="#/components/schemas/club search query orderby")
* ),
*
* @OA\Parameter(
* name="sort",
* in="query",
* @OA\Schema(ref="#/components/schemas/search query sort")
* ),
*
* @OA\Parameter(
* name="letter",
* in="query",
* description="Return entries starting with the given letter",
* @OA\Schema(type="string")
* ),
*
* @OA\Response(
* response="200",
* description="Returns search results for clubs",
* @OA\JsonContent(
* ref="#/components/schemas/clubs search"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function clubs(Request $request)
{
$this->request = $request;
$page = $this->request->get('page') ?? 1;
$limit = $this->request->get('limit') ?? self::MAX_RESULTS_PER_PAGE;
if (!empty($limit)) {
$limit = (int) $limit;
if ($limit <= 0) {
$limit = 1;
}
if ($limit > self::MAX_RESULTS_PER_PAGE) {
$limit = self::MAX_RESULTS_PER_PAGE;
}
}
$results = SearchQueryBuilderClub::query(
$request,
Club::query()
);
$results = $results
->paginate(
$limit,
['*'],
null,
$page
);
return new ClubCollection(
$results
);
}
}

View File

@ -0,0 +1,233 @@
<?php
namespace App\Http\Controllers\V4DB;
use App\Anime;
use App\Http\HttpResponse;
use App\Http\QueryBuilder\SearchQueryBuilderAnime;
use App\Http\Resources\V4\AnimeCollection;
use App\Http\Resources\V4\ResultsResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Request\Seasonal\SeasonalRequest;
use Jikan\Request\SeasonList\SeasonListRequest;
use Jikan\Request\Watch\PopularPromotionalVideosRequest;
class SeasonController extends Controller
{
private const VALID_SEASONS = [
'Summer',
'Spring',
'Winter',
'Fall'
];
const MAX_RESULTS_PER_PAGE = 25;
private $request;
private $season;
private $year;
/**
* @OA\Get(
* path="/seasons/{year}/{season}",
* operationId="getSeason",
* tags={"seasons"},
*
* @OA\Response(
* response="200",
* description="Returns seasonal anime",
* @OA\JsonContent(
* ref="#/components/schemas/anime search"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*/
public function main(Request $request, ?int $year = null, ?string $season = null)
{
$this->request = $request;
$page = $this->request->get('page') ?? 1;
$limit = $this->request->get('limit') ?? self::MAX_RESULTS_PER_PAGE;
if (!empty($limit)) {
$limit = (int) $limit;
if ($limit <= 0) {
$limit = 1;
}
if ($limit > self::MAX_RESULTS_PER_PAGE) {
$limit = self::MAX_RESULTS_PER_PAGE;
}
}
if (!is_null($season)) {
$this->season = ucfirst(
strtolower($season)
);
}
if (!is_null($year)) {
$this->year = (int) $year;
}
if (!is_null($this->season)
&& !\in_array($this->season, self::VALID_SEASONS)) {
return HttpResponse::badRequest($this->request);
}
if (is_null($season) && is_null($year)) {
list($this->season, $this->year) = $this->getSeasonStr();
}
$results = Anime::query()
->where('premiered', "{$this->season} $this->year")
->orderBy('members', 'desc')
->paginate(
$limit,
['*'],
null,
$page);
return new AnimeCollection(
$results
);
}
/**
* @OA\Get(
* path="/seasons",
* operationId="getSeasons",
* tags={"seasons"},
*
* @OA\Response(
* response="200",
* description="Returns available list of seasons",
* @OA\JsonContent(
* ref="#/components/schemas/seasons"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*
* @OA\Schema(
* schema="seasons",
* description="List of available seasons",
*
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(
* type="object",
* @OA\Property(
* property="year",
* type="integer",
* description="Year"
* ),
* @OA\Property(
* property="seasons",
* type="array",
* description="List of available seasons",
* @OA\Items(
* type="string"
* ),
* ),
* ),
* ),
* ),
*/
public function archive(Request $request)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$items = $this->jikan->getSeasonList(new SeasonListRequest());
$response = \json_decode($this->serializer->serialize($items, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/seasons/upcoming",
* operationId="getSeasonUpcoming",
* tags={"seasons"},
*
* @OA\Response(
* response="200",
* description="Returns upcoming season's anime",
* @OA\JsonContent(
* ref="#/components/schemas/anime search"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*/
public function later(Request $request)
{
$this->request = $request;
$nextYear = (new \DateTime(null, new \DateTimeZone('Asia/Tokyo')))
->modify('+1 year')
->format('Y');
$results = Anime::query()
->where('status', 'Not yet aired')
->where('premiered', 'like', "%{$nextYear}%")
->orderBy('members', 'desc')
->get();
$this->season = 'Later';
return new AnimeCollection(
$results
);
}
private function getSeasonStr() : array
{
$date = new \DateTime(null, new \DateTimeZone('Asia/Tokyo'));
$year = (int) $date->format('Y');
$month = (int) $date->format('n');
switch ($month) {
case \in_array($month, range(1, 3)):
return ['Winter', $year];
case \in_array($month, range(4, 6)):
return ['Spring', $year];
case \in_array($month, range(7, 9)):
return ['Summer', $year];
case \in_array($month, range(10, 12)):
return ['Fall', $year];
default: throw new \Exception('Could not generate seasonal string');
}
}
}

View File

@ -0,0 +1,366 @@
<?php
namespace App\Http\Controllers\V4DB;
use App\Anime;
use App\Character;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use App\Http\QueryBuilder\SearchQueryBuilderUsers;
use App\Http\QueryBuilder\TopQueryBuilderAnime;
use App\Http\QueryBuilder\TopQueryBuilderManga;
use App\Http\Resources\V4\AnimeCollection;
use App\Http\Resources\V4\CharacterCollection;
use App\Http\Resources\V4\MangaCollection;
use App\Http\Resources\V4\PersonCollection;
use App\Http\Resources\V4\ResultsResource;
use App\Manga;
use App\Person;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Helper\Constants;
use Jikan\Request\Reviews\RecentReviewsRequest;
use Jikan\Request\Top\TopPeopleRequest;
use MongoDB\BSON\UTCDateTime;
class TopController extends Controller
{
const MAX_RESULTS_PER_PAGE = 25;
/**
* @OA\Get(
* path="/top/anime",
* operationId="getTopAnime",
* tags={"top"},
*
* @OA\Response(
* response="200",
* description="Returns top anime",
* @OA\JsonContent(
* ref="#/components/schemas/anime search"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function anime(Request $request)
{
$page = $request->get('page') ?? 1;
$limit = $request->get('limit') ?? self::MAX_RESULTS_PER_PAGE;
if (!empty($limit)) {
$limit = (int) $limit;
if ($limit <= 0) {
$limit = 1;
}
if ($limit > self::MAX_RESULTS_PER_PAGE) {
$limit = self::MAX_RESULTS_PER_PAGE;
}
}
$results = TopQueryBuilderAnime::query(
$request,
Anime::query()
);
$results = $results
->paginate(
$limit,
['*'],
null,
$page
);
return new AnimeCollection(
$results
);
}
/**
* @OA\Get(
* path="/top/manga",
* operationId="getTopManga",
* tags={"top"},
*
* @OA\Response(
* response="200",
* description="Returns top manga",
* @OA\JsonContent(
* ref="#/components/schemas/manga search"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function manga(Request $request)
{
$page = $request->get('page') ?? 1;
$limit = $request->get('limit') ?? self::MAX_RESULTS_PER_PAGE;
if (!empty($limit)) {
$limit = (int) $limit;
if ($limit <= 0) {
$limit = 1;
}
if ($limit > self::MAX_RESULTS_PER_PAGE) {
$limit = self::MAX_RESULTS_PER_PAGE;
}
}
$results = TopQueryBuilderManga::query(
$request,
Manga::query()
);
$results = $results
->paginate(
$limit,
['*'],
null,
$page
);
return new MangaCollection(
$results
);
}
/**
* @OA\Get(
* path="/top/people",
* operationId="getTopPeople",
* tags={"top"},
*
* @OA\Response(
* response="200",
* description="Returns top people",
* @OA\JsonContent(
* ref="#/components/schemas/people search"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function people(Request $request)
{
$page = $request->get('page') ?? 1;
$limit = $request->get('limit') ?? self::MAX_RESULTS_PER_PAGE;
if (!empty($limit)) {
$limit = (int) $limit;
if ($limit <= 0) {
$limit = 1;
}
if ($limit > self::MAX_RESULTS_PER_PAGE) {
$limit = self::MAX_RESULTS_PER_PAGE;
}
}
$results = Person::query()
->whereNotNull('member_favorites')
->where('member_favorites', '>', 0)
->orderBy('member_favorites', 'desc');
$results = $results
->paginate(
$limit,
['*'],
null,
$page
);
return new PersonCollection(
$results
);
}
/**
* @OA\Get(
* path="/top/characters",
* operationId="getTopCharacters",
* tags={"top"},
*
* @OA\Response(
* response="200",
* description="Returns top characters",
* @OA\JsonContent(
* ref="#/components/schemas/characters search"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function characters(Request $request)
{
$page = $request->get('page') ?? 1;
$limit = $request->get('limit') ?? self::MAX_RESULTS_PER_PAGE;
if (!empty($limit)) {
$limit = (int) $limit;
if ($limit <= 0) {
$limit = 1;
}
if ($limit > self::MAX_RESULTS_PER_PAGE) {
$limit = self::MAX_RESULTS_PER_PAGE;
}
}
$results = Character::query()
->whereNotNull('member_favorites')
->where('member_favorites', '>', 0)
->orderBy('member_favorites', 'desc');
$results = $results
->paginate(
$limit,
['*'],
null,
$page
);
return new CharacterCollection(
$results
);
}
/**
* @OA\Get(
* path="/top/reviews",
* operationId="getTopReviews",
* tags={"top"},
*
* @OA\Response(
* response="200",
* description="Returns top reviews",
* @OA\JsonContent(
* @OA\Property(
* property="data",
* allOf={
* @OA\Schema(ref="#/components/schemas/pagination"),
* @OA\Schema(
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(
* anyOf={
* @OA\Schema(
* allOf={
* @OA\Schema(ref="#/components/schemas/anime review"),
* @OA\Schema(
* @OA\Property(
* property="anime",
* type="object",
* ref="#/components/schemas/anime meta",
* ),
* ),
* @OA\Schema(
* @OA\Property(
* property="user",
* type="object",
* ref="#/components/schemas/user meta",
* ),
* ),
* },
* ),
* @OA\Schema(
* allOf={
* @OA\Schema(ref="#/components/schemas/manga review"),
* @OA\Schema(
* @OA\Property(
* property="manga",
* type="object",
* ref="#/components/schemas/manga meta",
* ),
* ),
* @OA\Schema(
* @OA\Property(
* property="user",
* type="object",
* ref="#/components/schemas/user meta",
* ),
* ),
* },
* ),
* },
* ),
* ),
* )
* }
* )
* ),
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*
* @OA\Schema(
* schema="reviews collection",
* description="Anime & Manga Reviews Resource",
*
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(
* type="object",
* anyOf = {
* @OA\Schema(ref="#/components/schemas/anime review"),
* @OA\Schema(ref="#/components/schemas/manga review"),
* },
* ),
* ),
* ),
*/
public function reviews(Request $request)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$data = $this->jikan->getRecentReviews(
new RecentReviewsRequest(Constants::RECENT_REVIEW_BEST_VOTED, $page)
);
$response = \json_decode($this->serializer->serialize($data, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,308 @@
<?php
namespace App\Http\Controllers\V4DB;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use App\Http\Resources\V4\ResultsResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Helper\Constants;
use Jikan\Request\Anime\AnimeNewsRequest;
use Jikan\Request\Watch\PopularEpisodesRequest;
use Jikan\Request\Watch\PopularPromotionalVideosRequest;
use Jikan\Request\Watch\RecentEpisodesRequest;
use Jikan\Request\Watch\RecentPromotionalVideosRequest;
use MongoDB\BSON\UTCDateTime;
class WatchController extends Controller
{
/**
* @OA\Get(
* path="/watch/episodes",
* operationId="getWatchRecentEpisodes",
* tags={"watch"},
*
* @OA\Response(
* response="200",
* description="Returns Recently Added Episodes",
* @OA\JsonContent(
* ref="#/components/schemas/watch episodes"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*
* @OA\Schema(
* schema="watch episodes",
* description="Watch Episodes",
*
* allOf={
* @OA\Schema(ref="#/components/schemas/pagination"),
* @OA\Schema(
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(
* type="object",
*
* @OA\Property(
* property="entry",
* type="object",
* ref="#/components/schemas/anime meta"
* ),
* @OA\Property(
* property="episodes",
* type="array",
* description="Recent Episodes (max 2 listed)",
* @OA\Items(
* type="object",
* @OA\Property(
* property="mal_id",
* type="string",
* description="MyAnimeList ID",
* ),
* @OA\Property(
* property="url",
* type="string",
* description="MyAnimeList URL",
* ),
* @OA\Property(
* property="title",
* type="string",
* description="Episode Title",
* ),
* @OA\Property(
* property="premium",
* type="boolean",
* description="For MyAnimeList Premium Users",
* ),
* ),
* ),
* @OA\Property(
* property="region_locked",
* type="boolean",
* description="Region Locked Episode"
* ),
* ),
* ),
* ),
* },
* ),
*/
public function recentEpisodes(Request $request)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$items = $this->jikan->getRecentEpisodes(new RecentEpisodesRequest());
$response = \json_decode($this->serializer->serialize($items, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/watch/episodes/popular",
* operationId="getWatchPopularEpisodes",
* tags={"watch"},
*
* @OA\Response(
* response="200",
* description="Returns Popular Episodes",
* @OA\JsonContent(
* ref="#/components/schemas/watch episodes"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*/
public function popularEpisodes(Request $request)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$items = $this->jikan->getPopularEpisodes(new PopularEpisodesRequest());
$response = \json_decode($this->serializer->serialize($items, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/watch/promos",
* operationId="getWatchRecentPromos",
* tags={"watch"},
*
* @OA\Response(
* response="200",
* description="Returns Recently Added Promotional Videos",
* @OA\JsonContent(
* ref="#/components/schemas/watch promos"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*
* @OA\Schema(
* schema="watch promos",
* description="Watch Promos",
*
* allOf={
* @OA\Schema(ref="#/components/schemas/pagination"),
* @OA\Schema(
*
* allOf={
* @OA\Schema(
* @OA\Property(
* property="title",
* type="string",
* description="Promo Title"
* ),
* ),
* @OA\Schema (
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(
* type="object",
* @OA\Property(
* property="entry",
* type="object",
* ref="#/components/schemas/anime meta"
* ),
* @OA\Property(
* property="trailer",
* type="array",
* @OA\Items(
* type="object",
* ref="#/components/schemas/trailer",
* ),
* ),
* ),
* ),
* ),
* },
* ),
* },
* ),
*/
public function recentPromos(Request $request)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$page = $request->get('page') ?? 1;
$items = $this->jikan->getRecentPromotionalVideos(new RecentPromotionalVideosRequest($page));
$response = \json_decode($this->serializer->serialize($items, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/watch/promos/popular",
* operationId="getWatchPopularPromos",
* tags={"watch"},
*
* @OA\Response(
* response="200",
* description="Returns Popular Promotional Videos",
* @OA\JsonContent(
* ref="#/components/schemas/watch promos"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*/
public function popularPromos(Request $request)
{
$results = DB::table($this->getRouteTable($request))
->where('request_hash', $this->fingerprint)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$items = $this->jikan->getPopularPromotionalVideos(new PopularPromotionalVideosRequest());
$response = \json_decode($this->serializer->serialize($items, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new ResultsResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
}

View File

@ -19,7 +19,7 @@ class HttpHelper
public static function requestType(Request $request): string
{
$requestType = $request->segments()[1];
if (!\in_array($request->segments()[0], ['v1', 'v2', 'v3'])) {
if (!\in_array($request->segments()[0], ['v1', 'v2', 'v3', 'v4'])) {
$requestType = $request->segments()[0];
}
@ -54,7 +54,7 @@ class HttpHelper
foreach ($related as $relation => $items) {
$data['related'][] = [
'relation' => $relation,
'items' => $items
'entry' => $items
];
}
}
@ -75,7 +75,7 @@ class HttpHelper
foreach ($related as $relation => $items) {
$data['related'][] = [
'relation' => $relation,
'items' => $items
'entry' => $items
];
}
}
@ -83,12 +83,11 @@ class HttpHelper
return $data;
}
public static function requestControllerName(Request $request) : string
public static function getRouteName(Request $request) : string
{
$route = explode('\\', $request->route()[1]['uses']);
$route = end($route);
return explode('@', $route)[0];
return end($route);
}
public static function getRequestUriHash(Request $request) : string
@ -96,4 +95,9 @@ class HttpHelper
return sha1($request->getRequestUri());
}
public static function getRouteTable($request) : string
{
$routeName = HttpHelper::getRouteName($request);
return config("controller.{$routeName}.table_name");
}
}

36
app/Http/HttpResponse.php Normal file
View File

@ -0,0 +1,36 @@
<?php
namespace App\Http;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class HttpResponse
{
public static function notFound(Request $request) : Response
{
return response(
\json_encode([
'status' => 404,
'type' => 'BadResponseException',
'message' => 'Resource not found',
'error' => '404 on ' . $request->getUri()
]),
404
);
}
public static function badRequest(Request $request) : Response
{
return response(
\json_encode([
'status' => 400,
'type' => 'BadRequestException',
'message' => 'Invalid or incomplete request. Make sure your request is correct. https://docs.api.jikan.moe/',
'error' => null
]),
400
);
}
}

View File

@ -1,19 +1,5 @@
<?php
/**
* This middleware is the successor of JikanResponseLegacy; used for REST v3.3+
*
* It works by storing cache with no automated TTL handling by Redis
*
* If a request is past it's TTL, it queues an update instead of removing the cache followed by fetching a new one
* Update queues are automated.
*
* Therefore,
* - if MyAnimeList is down or rate-limits the response, stale cache is served
* - if cache expires, the client doesn't have to wait longer for the server to fetch+parse the new response
*/
namespace App\Http\Middleware;
use App\Http\HttpHelper;
@ -22,7 +8,7 @@ use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class JikanResponseHandler
class CacheResolver
{
private $requestUri;
private $requestUriHash;
@ -52,6 +38,11 @@ class JikanResponseHandler
public function handle(Request $request, Closure $next)
{
if (!env('CACHING')) {
return $next($request);
}
if ($request->header('auth') === env('APP_KEY')) {
return $next($request);
}

View File

@ -1,48 +0,0 @@
<?php
namespace App\Http\Middleware;
use App\Http\HttpHelper;
use Closure;
use Illuminate\Support\Facades\Cache;
class EtagMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($request->header('auth') === env('APP_KEY')) {
return $next($request);
}
if (empty($request->segments())) {
return $next($request);
}
if (!isset($request->segments()[1])) {
return $next($request);
}
if (\in_array('meta', $request->segments())) {
return $next($request);
}
$fingerprint = HttpHelper::resolveRequestFingerprint($request);
if (
$request->hasHeader('If-None-Match')
&& Cache::has($fingerprint)
&& md5(Cache::get($fingerprint)) === $request->header('If-None-Match')
) {
return response('', 304);
}
return $next($request);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Class Insights
* @package App\Http\Middleware
*/
class Insights
{
/**
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
return $next($request);
}
/**
* @param Request $request
* @param $response
* @return void
*/
public function terminate(Request $request, $response)
{
if (isset($response->original['error'])) {
return;
}
// @todo scaling: implement as scheduled event if needed
// Delete requests older than INSIGHTS_MAX_STORE
DB::table('insights')
->where('timestamp', '<', time() - env('INSIGHTS_MAX_STORE_TIME', 172800) )
->delete();
DB::table('insights')
->insert([
'timestamp' => time(),
'url' => $request->getRequestUri(),
'type' =>
]);
}
}

View File

@ -4,10 +4,20 @@ namespace App\Http\Middleware;
use App\Http\HttpHelper;
use Closure;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Cache;
use Jikan\Exception\BadResponseException;
class MicroCaching
{
private const NO_CACHING = [
'RandomController@anime',
'RandomController@manga',
'RandomController@characters',
'RandomController@people',
'RandomController@users',
'InsightsController@main'
];
/**
* Handle an incoming request.
*
@ -17,31 +27,54 @@ class MicroCaching
*/
public function handle($request, Closure $next)
{
if (isset($request->route()[1]['uses'])) {
$route = explode('\\', $request->route()[1]['uses']);
$route = end($route);
if (\in_array($route, self::NO_CACHING)) {
return $next($request);
}
}
if ($request->header('auth') === env('APP_KEY')) {
return $next($request);
}
// Microcaching should not work alongside redis caching
if (!env('MICROCACHING', false) || env('CACHE_DRIVER', 'file') === 'redis') {
if (
empty($request->segments())
|| !isset($request->segments()[1])
) {
return $next($request);
}
if (
!env('CACHING')
|| !env('MICROCACHING')
|| env('CACHE_DRIVER') !== 'redis'
) {
return $next($request);
}
$fingerprint = "microcache:".HttpHelper::resolveRequestFingerprint($request);
// if cache exists, return cache
if (app('redis')->exists($fingerprint)) {
return response()
->json(
json_decode(app('redis')->get($fingerprint), true)
\json_decode(app('redis')->get($fingerprint), true)
);
}
// set cache
app('redis')->set(
$fingerprint,
json_encode(
$next($request)->getData()
)
);
app('redis')->expire($fingerprint, env('MICROCACHING_EXPIRE', 5));
return $next($request);
}
public static function setMicroCache($fingerprint, $cache) {
$fingerprint = "microcache:".$fingerprint;
$cache = json_encode($cache);
app('redis')->set($fingerprint, $cache);
app('redis')->expire($fingerprint, env('MICROCACHING_EXPIRE', 5));
}
}

View File

@ -3,18 +3,22 @@
namespace App\Http\Middleware;
use Closure;
use App\Events\SourceHeartbeatEvent;
class ExampleMiddleware
class SourceHeartbeatMonitor
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
event(new SourceHeartbeatEvent(SourceHeartbeatEvent::GOOD_HEALTH, 200));
return $next($request);
}
}

View File

@ -15,6 +15,10 @@ class Throttle
public function handle(Request $request, Closure $next)
{
if ($request->header('auth') === env('APP_KEY')) {
return $next($request);
}
if (!env('THROTTLE', false)) {
return $next($request);
}

View File

@ -0,0 +1,317 @@
<?php
namespace App\Http\QueryBuilder;
use App\Http\HttpHelper;
use Illuminate\Http\Request;
use Jenssegers\Mongodb\Eloquent\Builder;
/**
* Class SearchQueryBuilderAnime
* @package App\Http\QueryBuilder
*/
class SearchQueryBuilderAnime implements SearchQueryBuilderInterface
{
/**
*
*/
const MAX_RESULTS_PER_PAGE = 25;
/**
* @OA\Schema(
* schema="anime search query type",
* description="Available Anime types",
* type="string",
* enum={"tv","movie","ova","special","ona","music"}
* )
*/
const MAP_TYPES = [
'tv' => 'TV',
'movie' => 'Movie',
'ova' => 'OVA',
'special' => 'Special',
'ona' => 'ONA',
'music' => 'Music'
];
/**
* @OA\Schema(
* schema="anime search query status",
* description="Available Anime statuses",
* type="string",
* enum={"airing","complete","upcoming"}
* )
*/
const MAP_STATUS = [
'airing' => 'Currently Airing',
'complete' => 'Finished Airing',
'upcoming' => 'Not yet aired',
];
/**
* @OA\Schema(
* schema="anime search query rating",
* description="Available Anime audience ratings<br><br><b>Ratings</b><br><ul><li>G - All Ages</li><li>PG - Children</li><li>PG-13 - Teens 13 or older</li><li>R - 17+ (violence & profanity)</li><li>R+ - Mild Nudity</li><li>Rx - Hentai</li></ul>",
* type="string",
* enum={"g","pg","pg13","r17","r","rx"}
* )
*/
const MAP_RATING = [
'g' => 'G - All Ages',
'pg' => 'PG - Children',
'pg13' => 'PG-13 - Teens 13 or older',
'r17' => 'R - 17+ (violence & profanity)',
'r' => 'R+ - Mild Nudity',
'rx' => 'Rx - Hentai'
];
/**
* @OA\Schema(
* schema="anime search query orderby",
* description="Available Anime order_by properties",
* type="string",
* enum={"mal_id", "title", "type", "rating", "start_date", "end_date", "episodes", "score", "scored_by", "rank", "popularity", "members", "favorites" }
* )
*/
const ORDER_BY = [
'mal_id' => 'mal_id',
'title' => 'title',
'type' => 'type',
'rating' => 'rating',
'start_date' => 'aired.from',
'end_date' => 'aired.to',
'episodes' => 'episodes',
'score' => 'score',
'scored_by' => 'scored_by',
'rank' => 'rank',
'popularity' => 'popularity',
'members' => 'members',
'favorites' => 'favorites'
];
/**
* @param Request $request
* @param Builder $results
* @return Builder
*/
public static function query(Request $request, Builder $results) : Builder
{
$requestType = HttpHelper::requestType($request);
$query = $request->get('q');
$type = self::mapType($request->get('type'));
$score = $request->get('score') ?? 0;
$status = self::mapStatus($request->get('status'));
$rating = self::mapRating($request->get('rating'));
$sfw = $request->get('sfw');
$genres = $request->get('genres');
$orderBy = self::mapOrderBy($request->get('order_by'));
$sort = self::mapSort($request->get('sort'));
$letter = $request->get('letter');
$producer = $request->get('producers');
$minScore = $request->get('min_score');
$maxScore = $request->get('max_score');
if (!empty($query) && is_null($letter)) {
$results = $results
->where('title', 'like', "%{$query}%")
->orWhere('title_english', 'like', "%{$query}%")
->orWhere('title_japanese', 'like', "%{$query}%")
->orWhere('title_synonyms', 'like', "%{$query}%");
// needs elastic search
// $results = $results
// ->whereRaw([
// '$text' => [
// '$search' => $query
// ]
// ]);
}
if (!is_null($letter)) {
$results = $results
->where('title', 'like', "{$letter}%");
}
if (empty($query) && is_null($orderBy)) {
$results = $results
->orderBy('mal_id');
}
if (!is_null($type)) {
$results = $results
->where('type', $type);
}
if ($score !== 0) {
$score = (float) $score;
$results = $results
->where('score', '>=', $score);
}
if ($minScore !== null) {
$minScore = (float) $minScore;
$results = $results
->where('score', '>=', $minScore);
}
if ($maxScore !== null) {
$maxScore = (float) $maxScore;
$results = $results
->where('score', '<=', $maxScore);
}
if (!is_null($status)) {
$results = $results
->where('status', $status);
}
if (!is_null($rating)) {
$results = $results
->where('rating', $rating);
}
if (!is_null($sfw)) {
$results = $results
->where('rating', '!=', self::MAP_RATING['rx']);
}
if (!is_null($producer)) {
$producer = (int) $producer;
$results = $results
->where('producers.mal_id', $producer)
->orWhere('licensors.mal_id', $producer)
->orWhere('studios.mal_id', $producer);
}
if (!is_null($genres)) {
$genres = explode(',', $genres);
foreach ($genres as $genre) {
if (empty($genre)) {
continue;
}
$genre = (int) $genre;
$results = $results
->where('genres.mal_id', $genre);
}
}
if (!is_null($orderBy)) {
$results = $results
->orderBy($orderBy, $sort ?? 'asc');
}
return $results;
}
/**
* @param Request $request
* @param Builder $results
* @return array
*/
public static function paginate(Request $request, Builder $results)
{
$page = $request->get('page') ?? 1;
$limit = $request->get('limit') ?? self::MAX_RESULTS_PER_PAGE;
$limit = (int) $limit;
if ($limit <= 0) {
$limit = 1;
}
if ($limit > self::MAX_RESULTS_PER_PAGE) {
$limit = self::MAX_RESULTS_PER_PAGE;
}
if ($page <= 0) {
$page = 1;
}
$paginated = $results
->paginate(
$limit,
null,
null,
$page
);
$items = $paginated->items();
foreach ($items as &$item) {
unset($item['_id']);
}
return [
'per_page' => $paginated->perPage(),
'total' => $paginated->total(),
'current_page' => $paginated->currentPage(),
'last_page' => $paginated->lastPage(),
'data' => $items
];
}
/**
* @param string|null $type
* @return string|null
*/
public static function mapType(?string $type = null) : ?string
{
$type = strtolower($type);
return self::MAP_TYPES[$type] ?? null;
}
/**
* @param string|null $status
* @return string|null
*/
public static function mapStatus(?string $status = null) : ?string
{
$status = strtolower($status);
return self::MAP_STATUS[$status] ?? null;
}
/**
* @param string|null $rating
* @return string|null
*/
public static function mapRating(?string $rating = null) : ?string
{
$rating = strtolower($rating);
return self::MAP_RATING[$rating] ?? null;
}
/**
* @param string|null $sort
* @return string|null
*/
public static function mapSort(?string $sort = null) : ?string
{
$sort = strtolower($sort);
return $sort === 'desc' ? 'desc' : 'asc';
}
/**
* @param string|null $orderBy
* @return string|null
*/
public static function mapOrderBy(?string $orderBy) : ?string
{
$orderBy = strtolower($orderBy);
return self::ORDER_BY[$orderBy] ?? null;
}
}

View File

@ -0,0 +1,102 @@
<?php
namespace App\Http\QueryBuilder;
use App\Http\HttpHelper;
use Illuminate\Http\Request;
use Jenssegers\Mongodb\Eloquent\Builder;
/**
* Class SearchQueryBuilderCharacter
* @package App\Http\QueryBuilder
*/
class SearchQueryBuilderCharacter implements SearchQueryBuilderInterface
{
/**
*
*/
const MAX_RESULTS_PER_PAGE = 25;
/**
* @OA\Schema(
* schema="characters search query orderby",
* description="Available Character order_by properties",
* type="string",
* enum={"mal_id", "name", "favorites"}
* )
*/
const ORDER_BY = [
'mal_id' => 'mal_id',
'name' => 'name',
'favorites' => 'member_favorites'
];
/**
* @param Request $request
* @param Builder $results
* @return Builder
*/
public static function query(Request $request, Builder $results) : Builder
{
$query = $request->get('q');
$orderBy = self::mapOrderBy($request->get('order_by'));
$sort = self::mapSort($request->get('sort'));
$letter = $request->get('letter');
if (!empty($query) && is_null($letter)) {
$results = $results
->where('name', 'like', "%{$query}%")
->orWhere('name_kanji', 'like', "%{$query}%")
->orWhere('nicknames', 'like', "%{$query}%");
// $results = $results
// ->whereRaw([
// '$text' => [
// '$search' => $query
// ]
// ]);
}
if (!is_null($letter)) {
$results = $results
->where('name', 'like', "{$letter}%");
}
if (empty($query) && is_null($orderBy)) {
$results = $results
->orderBy('mal_id');
}
if (!is_null($orderBy)) {
$results = $results
->orderBy($orderBy, $sort ?? 'asc');
}
return $results;
}
/**
* @param string|null $sort
* @return string|null
*/
public static function mapSort(?string $sort = null) : ?string
{
$sort = strtolower($sort);
return $sort === 'desc' ? 'desc' : 'asc';
}
/**
* @param string|null $orderBy
* @return string|null
*/
public static function mapOrderBy(?string $orderBy) : ?string
{
$orderBy = strtolower($orderBy);
return self::ORDER_BY[$orderBy] ?? null;
}
}

View File

@ -0,0 +1,160 @@
<?php
namespace App\Http\QueryBuilder;
use App\Http\HttpHelper;
use Illuminate\Http\Request;
use Jenssegers\Mongodb\Eloquent\Builder;
/**
* Class SearchQueryBuilderAnime
* @package App\Http\QueryBuilder
*/
class SearchQueryBuilderClub implements SearchQueryBuilderInterface
{
/**
*
*/
const MAX_RESULTS_PER_PAGE = 25;
/**
* @OA\Schema(
* schema="club search query type",
* description="Club Search Query Type",
* type="string",
* enum={"public","private","secret"}
* )
*/
const MAP_TYPES = [
'public' => 'public',
'private' => 'private',
'secret' => 'secret'
];
/**
* @OA\Schema(
* schema="club search query category",
* description="Club Search Query Category",
* type="string",
* enum={
* "anime","manga","actors_and_artists","characters",
* "cities_and_neighborhoods","companies","conventions","games",
* "japan","music","other","schools"
* }
* )
*/
const MAP_CATEGORY = [
'anime' => 'Anime',
'manga' => 'Manga',
'actors_and_artists' => 'Actors & Artists',
'characters' => 'Characters',
'cities_and_neighborhoods' => 'Cities & Neighborhoods',
'companies' => 'Companies',
'conventions' => 'Conventions',
'games' => 'Games',
'japan' => 'Japan',
'music' => 'Music',
'other' => 'Other',
'schools' => 'Schools'
];
/**
* @OA\Schema(
* schema="club search query orderby",
* description="Club Search Query OrderBy",
* type="string",
* enum={"mal_id","title","members_count","pictures_count","created"}
* )
*/
const ORDER_BY = [
'mal_id', 'title', 'members_count', 'pictures_count', 'created'
];
/**
* @param Request $request
* @param Builder $results
* @return Builder
*/
public static function query(Request $request, Builder $results) : Builder
{
$requestType = HttpHelper::requestType($request);
$query = $request->get('q');
$type = self::mapType($request->get('type'));
$category = self::mapCategory($request->get('category'));
$orderBy = $request->get('order_by');
$sort = self::mapSort($request->get('sort'));
$letter = $request->get('letter');
if (!empty($query) && is_null($letter)) {
$results = $results
->where('title', 'like', "%{$query}%");
}
if (!is_null($letter)) {
$results = $results
->where('title', 'like', "{$letter}%");
}
if (empty($query)) {
$results = $results
->orderBy('mal_id');
}
if (!is_null($type)) {
$results = $results
->where('type', $type);
}
if (!is_null($category)) {
$results = $results
->where('category', $category);
}
if (!is_null($orderBy)) {
$results = $results
->orderBy($orderBy, $sort ?? 'asc');
}
return $results;
}
/**
* @param string|null $type
* @return string|null
*/
public static function mapType(?string $type = null) : ?string
{
$type = strtolower($type);
if (!in_array($type, self::MAP_TYPES)) {
return null;
}
return $type;
}
/**
* @param string|null $category
* @return string|null
*/
public static function mapCategory(?string $category = null) : ?string
{
$category = strtolower($category);
return self::MAP_CATEGORY[$category] ?? null;
}
/**
* @param string|null $sort
* @return string|null
*/
public static function mapSort(?string $sort = null) : ?string
{
$sort = strtolower($sort);
return $sort === 'desc' ? 'desc' : 'asc';
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace App\Http\QueryBuilder;
use App\Http\HttpHelper;
use Illuminate\Http\Request;
use Jenssegers\Mongodb\Eloquent\Builder;
class SearchQueryBuilderGenre implements SearchQueryBuilderInterface
{
const MAX_RESULTS_PER_PAGE = 25;
const ORDER_BY = [
'mal_id', 'name', 'count'
];
public static function query(Request $request, Builder $results) : Builder
{
$query = $request->get('q');
$orderBy = $request->get('order_by');
$sort = self::mapSort($request->get('sort'));
$letter = $request->get('letter');
if (!empty($query) && is_null($letter)) {
$results = $results
->where('name', 'like', "%{$query}%");
}
if (!is_null($letter)) {
$results = $results
->where('name', 'like', "{$letter}%");
}
if (!is_null($orderBy)) {
$results = $results
->orderBy($orderBy, $sort ?? 'asc');
}
if (empty($query)) {
$results = $results
->orderBy('mal_id');
}
return $results;
}
/**
* @param string|null $sort
* @return string|null
*/
public static function mapSort(?string $sort = null) : ?string
{
if (is_null($sort)) {
return null;
}
$sort = strtolower($sort);
return $sort === 'desc' ? 'desc' : 'asc';
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Http\QueryBuilder;
use Jenssegers\Mongodb\Eloquent\Builder;
use Illuminate\Http\Request;
interface SearchQueryBuilderInterface
{
static function query(Request $request, Builder $results) : Builder;
// static function paginate(Request $request, Builder $results);
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Http\QueryBuilder;
use App\Http\HttpHelper;
use Illuminate\Http\Request;
use Jenssegers\Mongodb\Eloquent\Builder;
/**
* Class SearchQueryBuilderMagazine
* @package App\Http\QueryBuilder
*
* @OA\Schema(
* schema="magazines query orderby",
* description="Order by magazine data",
* type="string",
* enum={"mal_id", "name", "count"}
* )
*/
class SearchQueryBuilderMagazine implements SearchQueryBuilderInterface
{
const ORDER_BY = [
'mal_id', 'name', 'count'
];
public static function query(Request $request, Builder $results) : Builder
{
$query = $request->get('q');
$orderBy = $request->get('order_by');
$sort = self::mapSort($request->get('sort'));
$letter = $request->get('letter');
if (!empty($query) && is_null($letter)) {
$results = $results
->where('name', 'like', "%{$query}%");
}
if (!is_null($letter)) {
$results = $results
->where('name', 'like', "{$letter}%");
}
if (!is_null($orderBy)) {
$results = $results
->orderBy($orderBy, $sort ?? 'asc');
}
if (empty($query)) {
$results = $results
->orderBy('mal_id');
}
return $results;
}
/**
* @param string|null $sort
* @return string|null
*/
public static function mapSort(?string $sort = null) : ?string
{
if (is_null($sort)) {
return null;
}
$sort = strtolower($sort);
return $sort === 'desc' ? 'desc' : 'asc';
}
}

View File

@ -0,0 +1,264 @@
<?php
namespace App\Http\QueryBuilder;
use App\Http\HttpHelper;
use Illuminate\Http\Request;
use Jenssegers\Mongodb\Eloquent\Builder;
class SearchQueryBuilderManga implements SearchQueryBuilderInterface
{
const MAX_RESULTS_PER_PAGE = 25;
/**
* @OA\Schema(
* schema="manga search query type",
* description="Available Manga types",
* type="string",
* enum={"manga","novel", "lightnovel", "oneshot","doujin","manhwa","manhua"}
* )
*/
const MAP_TYPES = [
'manga' => 'Manga',
'novel' => 'Novel',
'lightnovel' => 'Light Novel',
'oneshot' => 'One-shot',
'doujin' => 'Doujinshi',
'manhwa' => 'Manhwa',
'manhua' => 'Manhua'
];
/**
* @OA\Schema(
* schema="manga search query status",
* description="Available Manga statuses",
* type="string",
* enum={"publishing","complete","hiatus","discontinued","upcoming"}
* )
*/
const MAP_STATUS = [
'publishing' => 'Publishing',
'complete' => 'Finished',
'hiatus' => 'On Hiatus',
'discontinued' => 'Discontinued',
'upcoming' => 'Not yet published'
];
/**
* @OA\Schema(
* schema="manga search query orderby",
* description="Available Manga order_by properties",
* type="string",
* enum={"mal_id", "title", "start_date", "end_date", "chapters", "volumes", "score", "scored_by", "rank", "popularity", "members", "favorites"}
* )
*/
const ORDER_BY = [
'mal_id' => 'mal_id',
'title' => 'title',
'start_date' => 'published.from',
'end_date' => 'published.to',
'chapters' => 'chapters',
'volumes' => 'volumes',
'score' => 'score',
'scored_by' => 'scored_by',
'rank' => 'rank',
'popularity' => 'popularity',
'members' => 'members',
'favorites' => 'favorites'
];
public static function query(Request $request, Builder $results) : Builder
{
$requestType = HttpHelper::requestType($request);
$query = $request->get('q');
$type = self::mapType($request->get('type'));
$score = $request->get('score') ?? 0;
$status = self::mapStatus($request->get('status'));
$sfw = $request->get('sfw');
$genres = $request->get('genres');
$orderBy = self::mapOrderBy($request->get('order_by'));
$sort = self::mapSort($request->get('sort'));
$letter = $request->get('letter');
$magazine = $request->get('magazines');
$minScore = $request->get('min_score');
$maxScore = $request->get('max_score');
if (!empty($query) && is_null($letter)) {
$results = $results
->where('title', 'like', "%{$query}%")
->orWhere('title_english', 'like', "%{$query}%")
->orWhere('title_japanese', 'like', "%{$query}%")
->orWhere('title_synonyms', 'like', "%{$query}%");
// $results = $results
// ->whereRaw([
// '$text' => [
// '$search' => $query
// ]
// ]);
}
if (!is_null($letter)) {
$results = $results
->where('title', 'like', "{$letter}%");
}
if (empty($query) && is_null($orderBy)) {
$results = $results
->orderBy('mal_id');
}
if (!is_null($type)) {
$results = $results
->where('type', $type);
}
if ($score !== 0) {
$score = (float) $score;
$results = $results
->where('score', '>=', $score);
}
if ($minScore !== null) {
$minScore = (float) $minScore;
$results = $results
->where('score', '>=', $minScore);
}
if ($maxScore !== null) {
$maxScore = (float) $maxScore;
$results = $results
->where('score', '<=', $maxScore);
}
if (!is_null($status)) {
$results = $results
->where('status', $status);
}
if (!is_null($sfw)) {
$results = $results
->where('type', '!=', 'Doujinshi');
}
if (!is_null($magazine)) {
$magazine = (int) $magazine;
$results = $results
->where('serializations.mal_id', $magazine);
}
if (!is_null($genres)) {
$genres = explode(',', $genres);
foreach ($genres as $genre) {
if (empty($genre)) {
continue;
}
$genre = (int) $genre;
$results = $results
->where('genres.mal_id', $genre);
}
}
if (!is_null($orderBy)) {
$results = $results
->orderBy($orderBy, $sort ?? 'asc');
}
return $results;
}
public static function paginate(Request $request, Builder $results)
{
$page = $request->get('page') ?? 1;
$limit = $request->get('limit') ?? self::MAX_RESULTS_PER_PAGE;
$limit = (int) $limit;
if ($limit <= 0) {
$limit = 1;
}
if ($limit > self::MAX_RESULTS_PER_PAGE) {
$limit = self::MAX_RESULTS_PER_PAGE;
}
if ($page <= 0) {
$page = 1;
}
$paginated = $results
->paginate(
$limit,
null,
null,
$page
);
$items = $paginated->items();
foreach ($items as &$item) {
unset($item['_id']);
}
return [
'per_page' => $paginated->perPage(),
'total' => $paginated->total(),
'current_page' => $paginated->currentPage(),
'last_page' => $paginated->lastPage(),
'data' => $items
];
}
/**
* @param string|null $type
* @return string|null
*/
public static function mapType(?string $type = null) : ?string
{
$type = strtolower($type);
return self::MAP_TYPES[$type] ?? null;
}
/**
* @param string|null $status
* @return string|null
*/
public static function mapStatus(?string $status = null) : ?string
{
$status = strtolower($status);
return self::MAP_STATUS[$status] ?? null;
}
/**
* @param string|null $sort
* @return string|null
*/
public static function mapSort(?string $sort = null) : ?string
{
$sort = strtolower($sort);
return $sort === 'desc' ? 'desc' : 'asc';
}
/**
* @param string|null $orderBy
* @return string|null
*/
public static function mapOrderBy(?string $orderBy) : ?string
{
$orderBy = strtolower($orderBy);
return self::ORDER_BY[$orderBy] ?? null;
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace App\Http\QueryBuilder;
use App\Http\HttpHelper;
use Illuminate\Http\Request;
use Jenssegers\Mongodb\Eloquent\Builder;
/**
* Class SearchQueryBuilderPeople
* @package App\Http\QueryBuilder
*/
class SearchQueryBuilderPeople implements SearchQueryBuilderInterface
{
/**
*
*/
const MAX_RESULTS_PER_PAGE = 25;
/**
* @OA\Schema(
* schema="people search query orderby",
* description="Available People order_by properties",
* type="string",
* enum={"mal_id", "name", "birthday", "favorites"}
* )
*/
const ORDER_BY = [
'mal_id' => 'mal_id',
'name' => 'name',
'birthday' => 'birthday',
'favorites' => 'member_favorites'
];
/**
* @param Request $request
* @param Builder $results
* @return Builder
*/
public static function query(Request $request, Builder $results) : Builder
{
$query = $request->get('q');
$orderBy = self::mapOrderBy($request->get('order_by'));
$sort = self::mapSort($request->get('sort'));
$letter = $request->get('letter');
if (!empty($query) && is_null($letter)) {
$results = $results
->where('name', 'like', "%{$query}%")
->orWhere('given_name', 'like', "%{$query}%")
->orWhere('family_name', 'like', "%{$query}%")
->orWhere('alternate_names', 'like', "%{$query}%");
// $results = $results
// ->whereRaw([
// '$text' => [
// '$search' => $query
// ]
// ]);
}
if (!is_null($letter)) {
$results = $results
->where('name', 'like', "{$letter}%");
}
if (empty($query) && is_null($orderBy)) {
$results = $results
->orderBy('mal_id');
}
if (!is_null($orderBy)) {
$results = $results
->orderBy($orderBy, $sort ?? 'asc');
}
return $results;
}
/**
* @param string|null $sort
* @return string|null
*/
public static function mapSort(?string $sort = null) : ?string
{
$sort = strtolower($sort);
return $sort === 'desc' ? 'desc' : 'asc';
}
/**
* @param string|null $orderBy
* @return string|null
*/
public static function mapOrderBy(?string $orderBy) : ?string
{
$orderBy = strtolower($orderBy);
return self::ORDER_BY[$orderBy] ?? null;
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace App\Http\QueryBuilder;
use App\Http\HttpHelper;
use Illuminate\Http\Request;
use Jenssegers\Mongodb\Eloquent\Builder;
/**
* Class SearchQueryBuilderProducer
* @package App\Http\QueryBuilder
*
* @OA\Schema(
* schema="producers query orderby",
* description="Order by producers data",
* type="string",
* enum={"mal_id", "name", "count"}
* )
*/
class SearchQueryBuilderProducer implements SearchQueryBuilderInterface
{
const MAX_RESULTS_PER_PAGE = 25;
const ORDER_BY = [
'mal_id', 'name', 'count'
];
public static function query(Request $request, Builder $results) : Builder
{
$query = $request->get('q');
$orderBy = $request->get('order_by');
$sort = self::mapSort($request->get('sort'));
$letter = $request->get('letter');
if (!empty($query) && is_null($letter)) {
$results = $results
->where('name', 'like', "%{$query}%");
}
if (!is_null($letter)) {
$results = $results
->where('name', 'like', "{$letter}%");
}
if (!is_null($orderBy)) {
$results = $results
->orderBy($orderBy, $sort ?? 'asc');
}
if (empty($query)) {
$results = $results
->orderBy('mal_id');
}
return $results;
}
/**
* @param string|null $sort
* @return string|null
*/
public static function mapSort(?string $sort = null) : ?string
{
if (is_null($sort)) {
return null;
}
$sort = strtolower($sort);
return $sort === 'desc' ? 'desc' : 'asc';
}
}

View File

@ -0,0 +1,104 @@
<?php
namespace App\Http\QueryBuilder;
use App\Http\HttpHelper;
use Illuminate\Http\Request;
use Jenssegers\Mongodb\Eloquent\Builder;
use Jikan\Helper\Constants as JikanConstants;
use Jikan\Request\Search\UserSearchRequest;
class SearchQueryBuilderUsers
{
const MAX_RESULTS_PER_PAGE = 25;
/**
* @OA\Schema(
* schema="users search query gender",
* description="Users Search Query Gender",
* type="string",
* enum={"any","male","female","nonbinary"}
* )
*/
private const MAP_GENDERS = [
'any' => JikanConstants::SEARCH_USER_GENDER_ANY,
'male' => JikanConstants::SEARCH_USER_GENDER_MALE,
'female' => JikanConstants::SEARCH_USER_GENDER_FEMALE,
'nonbinary' => JikanConstants::SEARCH_USER_GENDER_NONBINARY
];
public static function query(Request $request)
{
$page = $request->get('page') ?? 1;
$query = $request->get('q');
$gender = self::mapGender($request->get('gender'));
$location = $request->get('location');
$maxAge = $request->get('maxAge');
$minAge = $request->get('minAge');
return (new UserSearchRequest())
->setQuery($query)
->setGender($gender)
->setLocation($location)
->setMaxAge($maxAge)
->setMinAge($minAge)
->setPage($page);
}
public static function paginate(Request $request, Builder $results)
{
$page = $request->get('page') ?? 1;
$limit = $request->get('limit') ?? self::MAX_RESULTS_PER_PAGE;
$limit = (int) $limit;
if ($limit <= 0) {
$limit = 1;
}
if ($limit > self::MAX_RESULTS_PER_PAGE) {
$limit = self::MAX_RESULTS_PER_PAGE;
}
if ($page <= 0) {
$page = 1;
}
$paginated = $results
->paginate(
$limit,
null,
null,
$page
);
$items = $paginated->items();
foreach ($items as &$item) {
unset($item['_id']);
}
return [
'per_page' => $paginated->perPage(),
'total' => $paginated->total(),
'current_page' => $paginated->currentPage(),
'last_page' => $paginated->lastPage(),
'data' => $items
];
}
public static function mapGender(?string $type = null) : ?int
{
if (!is_null($type)) {
return null;
}
$type = strtolower($type);
return self::MAP_GENDERS[$type] ?? null;
}
}

View File

@ -0,0 +1,117 @@
<?php
namespace App\Http\QueryBuilder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jenssegers\Mongodb\Eloquent\Builder;
/**
* Class SearchQueryBuilderAnime
* @package App\Http\QueryBuilder
*/
class TopQueryBuilderAnime implements SearchQueryBuilderInterface
{
/**
*
*/
const MAX_RESULTS_PER_PAGE = 25;
/**
*
*/
const MAP_TYPES = [
'tv' => 'TV',
'movie' => 'Movie',
'ova' => 'OVA',
'special' => 'Special',
'ona' => 'ONA',
'music' => 'Music',
];
/**
*
*/
const MAP_FILTER = [
'airing', 'upcoming', 'bypopularity', 'favorites'
];
/**
* @param string|null $type
* @return string|null
*/
public static function mapType(?string $type = null) : ?string
{
if (is_null($type)) {
return null;
}
$type = strtolower($type);
return self::MAP_TYPES[$type] ?? null;
}
/**
* @param Request $request
* @param Builder $builder
* @return Builder
*/
public static function query(Request $request, Builder $results) : Builder
{
$animeType = self::mapType($request->get('type'));
$filterType = self::mapFilter($request->get('filter'));
$results = $results
->whereNotNull('rank')
->where('rank', '>', 0)
->orderBy('rank', 'asc')
->where('status', '!=', 'Not yet aired')
->where('rating', '!=', 'Rx - Hentai');
if (!is_null($animeType)) {
$results = $results
->where('type', $animeType);
}
if (!is_null($filterType) && $filterType === 'airing') {
$results = $results
->where('airing', true);
}
if (!is_null($filterType) && $filterType === 'upcoming') {
$results = $results
->where('status', 'Not yet aired');
}
if (!is_null($filterType) && $filterType === 'bypopularity') {
$results = $results
->orderBy('members', 'desc');
}
if (!is_null($filterType) && $filterType === 'favorite') {
$results = $results
->orderBy('favorites', 'desc');
}
return $results;
}
/**
* @param string|null $filter
* @return string|null
*/
public static function mapFilter(?string $filter = null) : ?string
{
$filter = strtolower($filter);
if (!\in_array($filter, self::MAP_FILTER)) {
return null;
}
return $filter;
}
}

View File

@ -0,0 +1,109 @@
<?php
namespace App\Http\QueryBuilder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jenssegers\Mongodb\Eloquent\Builder;
/**
* Class SearchQueryBuilderAnime
* @package App\Http\QueryBuilder
*/
class TopQueryBuilderManga implements SearchQueryBuilderInterface
{
/**
*
*/
const MAX_RESULTS_PER_PAGE = 25;
/**
*
*/
const MAP_TYPES = [
'manga' => 'Manga',
'novels' => 'Novel',
'oneshots' => 'One-shot',
'doujin' => 'Doujinshi',
'manhwa' => 'Manhwa',
'manhua' => 'Manhua'
];
/**
*
*/
const MAP_FILTER = [
'upcoming', 'bypopularity', 'favorites'
];
/**
* @param Request $request
* @param Builder $builder
* @return Builder
*/
public static function query(Request $request, Builder $results) : Builder
{
$mangaType = self::mapType($request->get('type'));
$filterType = self::mapFilter($request->get('filter'));
$results = $results
->whereNotNull('rank')
->where('rank', '>', 0)
->orderBy('rank', 'asc')
->where('type', '!=', 'Doujinshi');
if (!is_null($mangaType)) {
$results = $results
->where('type', $mangaType);
}
if (!is_null($filterType) && $filterType === 'publishing') {
$results = $results
->where('publishing', true);
}
if (!is_null($filterType) && $filterType === 'bypopularity') {
$results = $results
->orderBy('popularity', 'desc');
}
if (!is_null($filterType) && $filterType === 'favorite') {
$results = $results
->orderBy('favorites', 'desc');
}
return $results;
}
/**
* @param string|null $type
* @return string|null
*/
public static function mapType(?string $type = null) : ?string
{
if (is_null($type)) {
return null;
}
$type = strtolower($type);
return self::MAP_TYPES[$type] ?? null;
}
/**
* @param string|null $filter
* @return string|null
*/
public static function mapFilter(?string $filter = null) : ?string
{
$filter = strtolower($filter);
if (!\in_array($filter, self::MAP_FILTER)) {
return null;
}
return $filter;
}
}

View File

@ -1,10 +1,10 @@
<?php
namespace App\Providers;
namespace App\Http\QueryBuilder;
use Illuminate\Http\Request;
use Jikan\Request\User\UserAnimeListRequest;
use Jikan\Request\User\UserMangaListRequest;
use Illuminate\Http\Request;
use Jikan\Helper\Constants as JikanConstants;
class UserListQueryBuilder
@ -82,170 +82,187 @@ class UserListQueryBuilder
'nya' => JikanConstants::USER_MANGA_LIST_NOT_YET_PUBLISHED,
];
public static function create(Request $request, $parser)
public static function create(Request $request, $parserRequest)
{
$query = $request->get('search') ?? null;
$search = $request->get('q') ?? null;
$page = $request->get('page') ?? null;
$sort = $request->get('sort') ?? null;
$orderBy = $request->get('order_by') ?? null;
$orderBy2 = $request->get('order_by2') ?? null;
$query = $request->get('q');
$sort = $request->get('sort');
$orderBy = $request->get('order_by');
$orderBy2 = $request->get('order_by2');
$airedFrom = $request->get('aired_from');
$airedTo = $request->get('aired_to');
$producer = $request->get('producer');
$magazine = $request->get('magazine');
$season = $request->get('season');
$year = $request->get('year');
$airingStatus = $request->get('airing_status');
$publishedFrom = $request->get('published_from');
$publishedTo = $request->get('published_to');
$publishingStatus = $request->get('publishing_status');
// anime only
$airedFrom = $request->get('aired_from') ?? null;
$airedTo = $request->get('aired_to') ?? null;
$producer = $request->get('producer') ?? null;
$season = $request->get('season') ?? null;
$year = $request->get('year') ?? null;
$airingStatus = $request->get('airing_status') ?? null;
// manga only
$publishedFrom = $request->get('published_from') ?? null;
$publishedTo = $request->get('published_to') ?? null;
$magazine = $request->get('magazine') ?? null;
$publishingStatus = $request->get('publishing_status') ?? null;
// search
if ($search !== null) {
$parser->setTitle($search);
}
// bc: alias
if ($query !== null) {
$parser->setTitle($query);
// Search
if (!is_null($query)) {
$parserRequest->setTitle($query);
}
// page
if ($page !== null) {
$parser->setPage((int) $page);
// Page
$parserRequest->setPage(
(int) $request->get('page') ?? 1
);
// Sort
$sort = $request->get('sort');
if (!is_null($sort)) {
if (array_key_exists($sort, self::VALID_SORT)) {
$sort = self::VALID_SORT[$sort];
}
}
// sort
if ($sort !== null && array_key_exists($sort, self::VALID_SORT)) {
$sort = self::VALID_SORT[$sort];
}
if ($parserRequest instanceof UserAnimeListRequest) {
// Order By
if (!is_null($orderBy)) {
// animelist only queries
if ($parser instanceof UserAnimeListRequest) {
if (array_key_exists($orderBy, self::VALID_ANIME_ORDER_BY)) {
$orderBy = self::VALID_ANIME_ORDER_BY[$orderBy];
// order by
if ($orderBy !== null && array_key_exists($orderBy, self::VALID_ANIME_ORDER_BY)) {
$orderBy = self::VALID_ANIME_ORDER_BY[$orderBy];
$parser->setOrderBy($orderBy, $sort);
$parserRequest->setOrderBy($orderBy, $sort);
}
}
// order by 2
if ($orderBy2 !== null && array_key_exists($orderBy2, self::VALID_ANIME_ORDER_BY)) {
$orderBy2 = self::VALID_ANIME_ORDER_BY[$orderBy2];
// Order By 2
if (!is_null($orderBy2)) {
$parser->setOrderBy2($orderBy2, $sort);
if (array_key_exists($orderBy2, self::VALID_ANIME_ORDER_BY)) {
$orderBy2 = self::VALID_ANIME_ORDER_BY[$orderBy2];
$parserRequest->setOrderBy2($orderBy2, $sort);
}
}
// aired from
if ($airedFrom !== null && preg_match("~[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}~", $airedFrom)) {
$airedFrom = explode("-", $airedFrom);
// Aired From
if (!is_null($airedFrom)) {
$parser->setAiredFrom(
(int) $airedFrom[0],
(int) $airedFrom[1],
(int) $airedFrom[2]
if (preg_match("~[0-9]{4}-[0-9]{2}-[0-9]{2}~", $airedFrom)) {
$airedFrom = explode("-", $airedFrom);
$parserRequest->setAiredFrom(
(int) $airedFrom[2],
(int) $airedFrom[1],
(int) $airedFrom[0]
);
}
}
// Aired To
if (!is_null($airedTo)) {
if (preg_match("~[0-9]{4}-[0-9]{2}-[0-9]{2}~", $airedTo)) {
$airedTo = explode("-", $airedTo);
$parserRequest->setAiredTo(
(int) $airedTo[2],
(int) $airedTo[1],
(int) $airedTo[0]
);
}
}
// Producer
if (!is_null($producer)) {
$parserRequest->setProducer(
(int) $producer
);
}
// aired to
if ($airedTo !== null) {
if (preg_match("~[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}~", $airedTo)) {
$airedTo = explode("-", $airedTo);
// Season
if (!is_null($season)) {
$parser->setAiredTo(
(int) $airedTo[0],
(int) $airedTo[1],
(int) $airedTo[2]
);
if (\in_array($season, self::VALID_SEASONS)) {
$parserRequest->setSeason($season);
}
}
// producer
if ($producer !== null) {
$parser->setProducer((int) $producer);
// Year
if (!is_null($year)) {
$parserRequest->setSeasonYear(
(int) $year
);
}
// season
if ($season !== null && in_array($season, self::VALID_SEASONS)) {
$parser->setSeason($season);
// Airing Status
if (!is_null($airingStatus)) {
if (array_key_exists($airingStatus, self::VALID_AIRING_STATUS)) {
$airingStatus = self::VALID_AIRING_STATUS[$airingStatus];
$parserRequest->setAiringStatus($airingStatus);
}
}
// year
if ($year !== null) {
$parser->setSeasonYear($year);
}
// airing status
if ($airingStatus !== null && array_key_exists($airingStatus, self::VALID_AIRING_STATUS)) {
$airingStatus = self::VALID_AIRING_STATUS[$airingStatus];
$parser->setAiringStatus($airingStatus);
}
}
if ($parser instanceof UserMangaListRequest) {
// order by
if ($orderBy !== null && array_key_exists($orderBy, self::VALID_MANGA_ORDER_BY)) {
$orderBy = self::VALID_MANGA_ORDER_BY[$orderBy];
if ($parserRequest instanceof UserMangaListRequest) {
// Order By
if (!is_null($orderBy)) {
$parser->setOrderBy($orderBy, $sort);
if (array_key_exists($orderBy, self::VALID_MANGA_ORDER_BY)) {
$orderBy = self::VALID_MANGA_ORDER_BY[$orderBy];
$parserRequest->setOrderBy($orderBy, $sort);
}
}
// order by 2
if ($orderBy2 !== null && array_key_exists($orderBy2, self::VALID_MANGA_ORDER_BY)) {
$orderBy2 = self::VALID_MANGA_ORDER_BY[$orderBy2];
// Order By 2
if (!is_null($orderBy2)) {
$parser->setOrderBy2($orderBy2, $sort);
if (array_key_exists($orderBy2, self::VALID_MANGA_ORDER_BY)) {
$orderBy2 = self::VALID_MANGA_ORDER_BY[$orderBy2];
$parserRequest->setOrderBy2($orderBy2, $sort);
}
}
// published from
if ($publishedFrom !== null) {
if (preg_match("~[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}~", $publishedFrom)) {
// Published From
if (!is_null($publishedFrom)) {
if (preg_match("~[0-9]{4}-[0-9]{2}-[0-9]{2}~", $publishedFrom)) {
$publishedFrom = explode("-", $publishedFrom);
$parser->setPublishedFrom(
(int) $publishedFrom[0],
$parserRequest->setPublishedFrom(
(int) $publishedFrom[2],
(int) $publishedFrom[1],
(int) $publishedFrom[2]
(int) $publishedFrom[0]
);
}
}
// published to
if ($publishedTo !== null) {
if (preg_match("~[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}~", $publishedTo)) {
// Published To
if (!is_null($publishedTo)) {
if (preg_match("~[0-9]{4}-[0-9]{2}-[0-9]{2}~", $publishedTo)) {
$publishedTo = explode("-", $publishedTo);
$parser->setPublishedTo(
(int) $publishedTo[0],
$parserRequest->setPublishedTo(
(int) $publishedTo[2],
(int) $publishedTo[1],
(int) $publishedTo[2]
(int) $publishedTo[0]
);
}
}
// magazine
if ($magazine !== null) {
$parser->setMagazine((int) $magazine);
// Magazine
if (!is_null($magazine)) {
$parserRequest->setMagazine(
(int) $magazine
);
}
// airing status
if ($publishingStatus !== null && array_key_exists($publishingStatus, self::VALID_PUBLISHING_STATUS)) {
$publishingStatus = self::VALID_PUBLISHING_STATUS[$publishingStatus];
// Publishing Status
if (!is_null($publishingStatus)) {
$parser->setPublishingStatus($publishingStatus);
if (array_key_exists($publishingStatus, self::VALID_PUBLISHING_STATUS)) {
$publishingStatus = self::VALID_PUBLISHING_STATUS[$publishingStatus];
$parserRequest->setPublishingStatus($publishingStatus);
}
}
}
return $parser;
return $parserRequest;
}
}

View File

@ -0,0 +1,102 @@
<?php
namespace App\Http\Resources\V4;
use Illuminate\Http\Resources\Json\JsonResource;
class AnimeCharactersResource extends JsonResource
{
/**
* @OA\Schema(
* schema="anime characters",
* description="Anime Characters Resource",
*
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(
* type="object",
*
* @OA\Property(
* property="character",
* type="object",
* description="Character details",
*
* @OA\Property(
* property="mal_id",
* type="integer",
* description="MyAnimeList ID"
* ),
* @OA\Property(
* property="url",
* type="string",
* description="MyAnimeList URL"
* ),
* @OA\Property(
* property="images",
* type="object",
* ref="#/components/schemas/character images"
* ),
* @OA\Property(
* property="name",
* type="string",
* description="Character Name"
* ),
* ),
* @OA\Property(
* property="role",
* type="string",
* description="Character's Role"
* ),
* @OA\Property(
* property="voice_actors",
* type="array",
*
* @OA\Items(
* type="object",
*
* @OA\Property(
* property="person",
* type="object",
*
* @OA\Property(
* property="mal_id",
* type="integer",
* ),
* @OA\Property(
* property="url",
* type="string",
* ),
* @OA\Property(
* property="images",
* type="object",
* ref="#/components/schemas/people images"
* ),
* @OA\Property(
* property="name",
* type="string",
* ),
* ),
* @OA\Property(
* property="language",
* type="string",
* ),
* ),
* ),
* ),
* ),
* )
*/
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return $this['characters'];
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Http\Resources\V4;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Pagination\LengthAwarePaginator;
class AnimeCollection extends ResourceCollection
{
/**
* The resource that this resource collects.
*
* @var string
*
* @OA\Schema(
* schema="anime search",
* description="Anime Collection Resource",
*
* allOf={
* @OA\Schema(ref="#/components/schemas/pagination"),
* @OA\Schema(
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(
* ref="#/components/schemas/anime"
* )
* ),
* )
* }
* )
*/
public $collects = 'App\Http\Resources\V4\AnimeResource';
private $pagination;
public function __construct($resource)
{
$this->pagination = [
'last_visible_page' => $resource->lastPage(),
'has_next_page' => $resource->hasMorePages()
];
$this->collection = $resource->getCollection();
parent::__construct($resource);
}
/**
* Transform the resource collection into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'pagination' => $this->pagination,
'data' => $this->collection
];
}
public function withResponse($request, $response)
{
$jsonResponse = json_decode($response->getContent(), true);
unset($jsonResponse['links'],$jsonResponse['meta']);
$response->setContent(json_encode($jsonResponse));
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Http\Resources\V4;
use Illuminate\Http\Resources\Json\JsonResource;
class AnimeEpisodeResource extends JsonResource
{
/**
* @OA\Schema(
* schema="anime episode",
* description="Anime Episode Resource",
*
* @OA\Property(
* property="data",
* type="object",
* @OA\Property(
* property="mal_id",
* type="integer",
* description="MyAnimeList ID"
* ),
* @OA\Property(
* property="url",
* type="string",
* description="MyAnimeList URL"
* ),
* @OA\Property(
* property="title",
* type="string",
* description="Title"
* ),
* @OA\Property(
* property="title_japanese",
* type="string",
* description="Title Japanese"
* ),
* @OA\Property(
* property="title_romanji",
* type="string",
* description="title_romanji"
* ),
* @OA\Property(
* property="duration",
* type="integer",
* description="Episode duration in seconds"
* ),
* @OA\Property(
* property="aired",
* type="string",
* description="Aired Date ISO8601"
* ),
* @OA\Property(
* property="filler",
* type="boolean",
* description="Filler episode"
* ),
* @OA\Property(
* property="recap",
* type="boolean",
* description="Recap episode"
* ),
* @OA\Property(
* property="synopsis",
* type="string",
* description="Episode Synopsis"
* ),
* ),
* )
*/
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'mal_id' => $this['mal_id'],
'url' => $this['url'],
'title' => $this['title'],
'title_japanese' => $this['title_japanese'],
'title_romanji' => $this['title_romanji'],
'duration' => $this['duration'],
'aired' => $this['aired'],
'filler' => $this['filler'],
'recap' => $this['recap'],
'synopsis' => $this['synopsis']
];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Resources\V4;
use Illuminate\Http\Resources\Json\JsonResource;
class AnimeEpisodesResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'last_visible_page' => $this['last_visible_page'] ?? 1,
'has_next_page' => $this['has_next_page'] ?? false,
'results' => $this['results']
];
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Http\Resources\V4;
use Illuminate\Http\Resources\Json\JsonResource;
class AnimeRelationsResource extends JsonResource
{
/**
* @OA\Schema(
* schema="anime relations",
* description="Anime Relations",
*
* @OA\Property(
* property="data",
* type="array",
* @OA\Items(
* type="object",
*
* @OA\Property(
* property="relation",
* type="string",
* description="Relation type"
* ),
* @OA\Property(
* property="entry",
* type="array",
* @OA\Items(
* type="object",
* ref="#/components/schemas/mal_url"
* ),
* )
* )
* )
* )
*/
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return $this['related'];
}
}

View File

@ -0,0 +1,257 @@
<?php
namespace App\Http\Resources\V4;
use Illuminate\Http\Resources\Json\JsonResource;
class AnimeResource extends JsonResource
{
/**
* @OA\Schema(
* schema="anime",
* description="Anime Resource",
*
* @OA\Property(
* property="mal_id",
* type="integer",
* description="MyAnimeList ID"
* ),
* @OA\Property(
* property="url",
* type="string",
* description="MyAnimeList URL"
* ),
* @OA\Property(
* property="images",
* ref="#/components/schemas/anime images"
* ),
* @OA\Property(
* property="trailer",
* ref="#/components/schemas/trailer base"
* ),
* @OA\Property(
* property="title",
* type="string",
* description="Title"
* ),
* @OA\Property(
* property="title_english",
* type="string",
* description="English Title"
* ),
* @OA\Property(
* property="title_japanese",
* type="string",
* description="Japanese Title"
* ),
* @OA\Property(
* property="title_synonyms",
* type="array",
* description="Other Titles",
* @OA\Items(
* type="string"
* )
* ),
* @OA\Property(
* property="type",
* type="string",
* enum={"TV","OVA","Movie","Special","ONA","Music"},
* description="Anime Type"
* ),
* @OA\Property(
* property="source",
* type="string",
* description="Original Material/Source adapted from"
* ),
* @OA\Property(
* property="episodes",
* type="integer",
* description="Episode count"
* ),
* @OA\Property(
* property="status",
* type="string",
* enum={"Finished Airing", "Currently Airing", "Not yet aired"},
* description="Airing status"
* ),
* @OA\Property(
* property="airing",
* type="boolean",
* description="Airing boolean"
* ),
* @OA\Property(
* property="aired",
* ref="#/components/schemas/daterange"
* ),
* @OA\Property(
* property="duration",
* type="string",
* description="Parsed raw duration"
* ),
* @OA\Property(
* property="rating",
* type="string",
* enum={"G - All Ages", "PG - Children", "PG-13 - Teens 13 or older", "R - 17+ (violence & profanity)", "R+ - Mild Nudity", "Rx - Hentai" },
* description="Anime audience rating"
* ),
* @OA\Property(
* property="score",
* type="number",
* format="float",
* description="Score"
* ),
* @OA\Property(
* property="scored_by",
* type="integer",
* description="Number of users"
* ),
* @OA\Property(
* property="rank",
* type="integer",
* description="Ranking"
* ),
* @OA\Property(
* property="popularity",
* type="integer",
* description="Popularity"
* ),
* @OA\Property(
* property="members",
* type="integer",
* description="Number of users who have added this entry to their list"
* ),
* @OA\Property(
* property="favorites",
* type="integer",
* description="Number of users who have favorited this entry"
* ),
* @OA\Property(
* property="synopsis",
* type="string",
* description="Synopsis"
* ),
* @OA\Property(
* property="background",
* type="string",
* description="Background"
* ),
* @OA\Property(
* property="season",
* type="string",
* enum={"Summer", "Winter", "Spring", "Fall"},
* description="Season"
* ),
* @OA\Property(
* property="year",
* type="integer",
* description="Year"
* ),
* @OA\Property(
* property="broadcast",
* ref="#/components/schemas/broadcast"
* ),
* @OA\Property(
* property="producers",
* type="array",
* @OA\Items(
* type="object",
* ref="#/components/schemas/mal_url"
* ),
* ),
* @OA\Property(
* property="licensors",
* type="array",
* @OA\Items(
* type="object",
* ref="#/components/schemas/mal_url"
* ),
* ),
* @OA\Property(
* property="studios",
* type="array",
* @OA\Items(
* type="object",
* ref="#/components/schemas/mal_url"
* ),
* ),
* @OA\Property(
* property="genres",
* type="array",
* @OA\Items(
* type="object",
* ref="#/components/schemas/mal_url"
* ),
* ),
* @OA\Property(
* property="explicit_genres",
* type="array",
* @OA\Items(
* type="object",
* ref="#/components/schemas/mal_url"
* ),
* ),
* @OA\Property(
* property="themes",
* type="array",
* @OA\Items(
* type="object",
* ref="#/components/schemas/mal_url"
* ),
* ),
* @OA\Property(
* property="demographics",
* type="array",
* @OA\Items(
* type="object",
* ref="#/components/schemas/mal_url"
* ),
* ),
* )
*/
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'mal_id' => $this->mal_id,
'url' => $this->url,
'images' => $this->images,
'trailer' => $this->trailer,
'title' => $this->title,
'title_english' => $this->title_english,
'title_japanese' => $this->title_japanese,
'title_synonyms' => $this->title_synonyms,
'type' => $this->type,
'source' => $this->source,
'episodes' => $this->episodes,
'status' => $this->status,
'airing' => $this->airing,
'aired' => $this->aired,
'duration' => $this->duration,
'rating' => $this->rating,
'score' => $this->score,
'scored_by' => $this->scored_by,
'rank' => $this->rank,
'popularity' => $this->popularity,
'members' => $this->members,
'favorites' => $this->favorites,
'synopsis' => $this->synopsis,
'background' => $this->background,
'season' => $this->season,
'year' => $this->year,
'broadcast' => $this->broadcast,
'producers' => $this->producers,
'licensors' => $this->licensors,
'studios' => $this->studios,
'genres' => $this->genres,
'explicit_genres' => $this->explicit_genres,
'themes' => $this->themes,
'demographics' => $this->demographics,
];
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Http\Resources\V4;
use Illuminate\Http\Resources\Json\JsonResource;
class AnimeStaffResource extends JsonResource
{
/**
* @OA\Schema(
* schema="anime staff",
* description="Anime Staff Resource",
*
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(
* type="object",
*
* @OA\Property(
* property="person",
* type="object",
* description="Person details",
*
* @OA\Property(
* property="mal_id",
* type="integer",
* description="MyAnimeList ID"
* ),
* @OA\Property(
* property="url",
* type="string",
* description="MyAnimeList URL"
* ),
* @OA\Property(
* property="images",
* type="string",
* ref="#/components/schemas/people images"
* ),
* @OA\Property(
* property="name",
* type="string",
* description="Name"
* ),
* ),
* @OA\Property(
* property="positions",
* type="array",
* description="Staff Positions",
* @OA\Items(type="string")
* ),
* ),
* ),
* )
*/
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return $this['staff'];
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Http\Resources\V4;
use Illuminate\Http\Resources\Json\JsonResource;
class AnimeStatisticsResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*
* @OA\Schema(
* schema="anime statistics",
* description="Anime Statistics Resource",
*
* @OA\Property(
* property="data",
* type="object",
*
* @OA\Property(
* property="watching",
* type="integer",
* description="Number of users watching the resource"
* ),
* @OA\Property(
* property="completed",
* type="integer",
* description="Number of users who have completed the resource"
* ),
* @OA\Property(
* property="on_hold",
* type="integer",
* description="Number of users who have put the resource on hold"
* ),
* @OA\Property(
* property="dropped",
* type="integer",
* description="Number of users who have dropped the resource"
* ),
* @OA\Property(
* property="plan_to_watch",
* type="integer",
* description="Number of users who have planned to watch the resource"
* ),
* @OA\Property(
* property="total",
* type="integer",
* description="Total number of users who have the resource added to their lists"
* ),
*
* @OA\Property(
* property="scores",
* type="array",
* @OA\Items(
* type="object",
* @OA\Property(
* property="score",
* type="integer",
* description="Scoring value"
* ),
* @OA\Property(
* property="votes",
* type="integer",
* description="Number of votes for this score"
* ),
* @OA\Property(
* property="percentage",
* type="number",
* format="float",
* description="Percentage of votes for this score"
* ),
* ),
* ),
* ),
* )
*/
public function toArray($request)
{
return [
'watching' => $this['watching'],
'completed' => $this['completed'],
'on_hold' => $this['on_hold'],
'dropped' => $this['dropped'],
'plan_to_watch' => $this['plan_to_watch'],
'total' => $this['total'],
'scores' => $this['scores']
];
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Http\Resources\V4;
use Illuminate\Http\Resources\Json\JsonResource;
class AnimeThemesResource extends JsonResource
{
/**
* @OA\Schema(
* schema="anime themes",
* description="Anime Opening and Ending Themes",
*
* @OA\Property(
* property="data",
* type="object",
* @OA\Property(
* property="openings",
* type="array",
* @OA\Items(
* type="string",
* ),
* ),
* @OA\Property(
* property="endings",
* type="array",
* @OA\Items(
* type="string",
* ),
* ),
* ),
* )
*/
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'openings' => $this->opening_themes,
'endings' => $this->ending_themes
];
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Http\Resources\V4;
use Illuminate\Http\Resources\Json\JsonResource;
class AnimeVideosResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*
* @OA\Schema(
* schema="anime videos",
* description="Anime Videos Resource",
*
* @OA\Property(
* property="data",
* type="object",
*
* @OA\Property(
* property="promos",
* type="array",
* @OA\Items(
* type="object",
*
* @OA\Property(
* property="title",
* type="string",
* description="Title"
* ),
* @OA\Property(
* property="trailer",
* ref="#/components/schemas/trailer"
* ),
* ),
* ),
* @OA\Property(
* property="episodes",
* type="array",
*
* @OA\Items(
* type="object",
* @OA\Property(
* property="mal_id",
* type="integer",
* description="MyAnimeList ID"
* ),
* @OA\Property(
* property="url",
* type="string",
* description="MyAnimeList URL"
* ),
* @OA\Property(
* property="title",
* type="string",
* description="Title"
* ),
* @OA\Property(
* property="episode",
* type="string",
* description="Episode"
* ),
* @OA\Property(
* property="images",
* type="object",
* ref="#/components/schemas/common images"
* ),
* ),
* ),
* ),
* )
*/
public function toArray($request)
{
return [
'promo' => $this['promo'],
'episodes' => $this['episodes']
];
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Http\Resources\V4;
use Illuminate\Http\Resources\Json\ResourceCollection;
class CharacterAnimeCollection extends ResourceCollection
{
/**
* The resource that this resource collects.
*
* @OA\Schema(
* schema="character anime",
* description="Character casted in anime",
*
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(
* type="object",
*
* @OA\Property(
* property="role",
* type="string",
* description="Character's Role"
* ),
* @OA\Property(
* property="anime",
* type="object",
* ref="#/components/schemas/anime meta"
* ),
* ),
* ),
* )
*/
public $collects = 'App\Http\Resources\V4\CharacterAnimeResource';
/**
* Transform the resource collection into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'data' => $this->collection
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Resources\V4;
use Illuminate\Http\Resources\Json\JsonResource;
class CharacterAnimeResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'role' => $this['role'],
'anime' => [
'mal_id' => $this['anime']['mal_id'],
'url' => $this['anime']['url'],
'images' => $this['anime']['images'],
'title' => $this['anime']['title']
],
];
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Http\Resources\V4;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Pagination\LengthAwarePaginator;
class CharacterCollection extends ResourceCollection
{
/**
* The resource that this resource collects.
*
* @var string
*
* @OA\Schema(
* schema="characters search",
* description="Characters Search Resource",
*
* allOf={
* @OA\Schema(ref="#/components/schemas/pagination"),
* @OA\Schema(
* @OA\Property(
* property="data",
* type="array",
*
* @OA\Items(
* ref="#/components/schemas/character"
* )
* ),
* )
* }
* )
*/
public $collects = 'App\Http\Resources\V4\CharacterResource';
private $pagination;
public function __construct(LengthAwarePaginator $resource)
{
$this->pagination = [
'last_visible_page' => $resource->lastPage(),
'has_next_page' => $resource->hasMorePages()
];
$this->collection = $resource->getCollection();
parent::__construct($resource);
}
/**
* Transform the resource collection into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'pagination' => $this->pagination,
'data' => $this->collection
];
}
public function withResponse($request, $response)
{
$jsonResponse = json_decode($response->getContent(), true);
unset($jsonResponse['links'],$jsonResponse['meta']);
$response->setContent(json_encode($jsonResponse));
}
}

Some files were not shown because too many files have changed in this diff Show More