Merge branch 'master' into feature/queue-resource-updater

This commit is contained in:
Irfan 2022-10-05 01:59:14 +05:00 committed by GitHub
commit 951099dcca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 2086 additions and 617 deletions

1
.gitignore vendored
View File

@ -4,7 +4,6 @@ Homestead.json
Homestead.yaml
.env
composer.phar
composer.lock
.php_cs.cache
.phpunit.result.cache
.env.v4

View File

@ -41,14 +41,14 @@ For any additional help, join our [Discord server](http://discord.jikan.moe/).
| Language | Wrappers |
|------------|----------|
| JavaScript | [JikanJS](https://github.com/zuritor/jikanjs) by Zuritor<br>🆕 **(v4)** [JikanJS](https://github.com/mateoaranda/jikanjs) by Mateo Aranda |
| 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 |
| Java | [Jikan4java](https://github.com/Doomsdayrs/Jikan4java) by Doomsdayrs<br>🆕 **(v4)** [reactive-jikan](https://github.com/SandroHc/reactive-jikan) by Sandro Marques<br>🆕 **(v4)** [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<br>🆕 **(v4)** [Jikan4JS](https://github.com/rizzzigit/jikan4.js) by RizzziGit<br>🆕 **(v4)** [jikan-api.js](https://github.com/OpenianDevelopment/jikan-api.js) by OpenianDev |
| 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 | 🆕 **(v4)** [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 |
| Go | 🆕 **(v4)** [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 |

View File

@ -19,7 +19,7 @@ class AnimeIndexer extends Command
*`
* @var string
*/
protected $signature = 'indexer:anime
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}
@ -150,7 +150,7 @@ class AnimeIndexer extends Command
true
);
$this->ids = $ids['sfw'] + $ids['nsfw']; // merge
$ids = array_merge($ids['sfw'], $ids['nsfw']);
Storage::put('indexer/anime_mal_id.json', json_encode($this->ids));
return json_decode(Storage::get('indexer/anime_mal_id.json'));

View File

@ -0,0 +1,93 @@
<?php
namespace App\Console\Commands\Indexer;
use App\Exceptions\Console\CommandAlreadyRunningException;
use App\Exceptions\Console\FileNotFoundException;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
/**
* Class AnimeSweepIndexer
*
* @package App\Console\Commands\Indexer
*/
class AnimeSweepIndexer extends Command
{
/**
* The name and signature of the console command.
*`
*
* @var string
*/
protected $signature = 'indexer:anime-sweep';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete all removed anime';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return void
* @throws FileNotFoundException
*/
public function handle()
{
$this->info("Info: Delete removed MAL IDs\n\n");
echo "Loading MAL IDs\n";
$malIds = array_fill_keys($this->fetchMalIds(), null);
echo "Loading MAL IDs from local DB\n";
$results = DB::table('anime')->select('mal_id', '_id')->get();
echo "Compare MAL IDs\n";
$remove = [];
foreach ($results as $result) {
if (!array_key_exists($result['mal_id'], $malIds)) {
echo "Removing https://myanimelist.net/anime/".$result['mal_id']."\n";
$remove[] = $result['_id'];
}
}
echo "\n\n".count($remove);
echo "Delete removed MAL IDs\n";
DB::table('anime')->whereIn('_id', $remove)->delete();
}
/**
* @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
);
$ids = array_merge($ids['sfw'], $ids['nsfw']);
Storage::put('indexer/anime_mal_id_sweep.json', json_encode($ids));
return json_decode(Storage::get('indexer/anime_mal_id_sweep.json'));
}
}

View File

@ -48,42 +48,6 @@ class CommonIndexer extends Command
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')
->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";
}
/**
* Magazines
*/

View File

@ -19,7 +19,7 @@ class MangaIndexer extends Command
*`
* @var string
*/
protected $signature = 'indexer:manga
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}
@ -150,7 +150,7 @@ class MangaIndexer extends Command
true
);
$this->ids = $ids['sfw'] + $ids['nsfw']; // merge
$ids = array_merge($ids['sfw'], $ids['nsfw']);
Storage::put('indexer/manga_mal_id.json', json_encode($this->ids));
return json_decode(Storage::get('indexer/manga_mal_id.json'));

View File

@ -0,0 +1,90 @@
<?php
namespace App\Console\Commands\Indexer;
use App\Exceptions\Console\CommandAlreadyRunningException;
use App\Exceptions\Console\FileNotFoundException;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
/**
* Class MangaSweepIndexer
*
* @package App\Console\Commands\Indexer
*/
class MangaSweepIndexer extends Command
{
/**
* The name and signature of the console command.
*`
*
* @var string
*/
protected $signature = 'indexer:manga-sweep';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete all removed manga';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return void
* @throws FileNotFoundException
*/
public function handle()
{
$this->info("Info: Delete removed MAL IDs\n\n");
echo "Loading MAL IDs\n";
$malIds = array_fill_keys($this->fetchMalIds(), null);
echo "Loading MAL IDs from local DB\n";
$results = DB::table('manga')->select('mal_id', '_id')->get();
echo "Compare MAL IDs\n";
$remove = [];
foreach ($results as $result) {
if (!array_key_exists($result['mal_id'], $malIds)) {
$remove[] = $result['_id'];
}
}
echo "Delete removed MAL IDs\n";
DB::table('manga')->whereIn('_id', $remove)->delete();
}
/**
* @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
);
$ids = array_merge($ids['sfw'], $ids['nsfw']);
Storage::put('indexer/manga_mal_id_sweep.json', json_encode($ids));
return json_decode(Storage::get('indexer/manga_mal_id_sweep.json'));
}
}

View File

@ -0,0 +1,178 @@
<?php
namespace App\Console\Commands\Indexer;
use App\Exceptions\Console\FileNotFoundException;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use Jikan\Request\Producer\ProducersRequest;
/**
* Class ProducersIndexer
* @package App\Console\Commands\Indexer
*/
class ProducersIndexer extends Command
{
/**
* The name and signature of the console command.
*`
* @var string
*/
protected $signature = 'indexer:producers
{--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 producers';
/**
* @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: ProducersIndexer scrapes available MAL IDs and updates/indexes them\n\n");
if ($failed && Storage::exists('indexer/indexer_producers.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_producers.save')) {
$index = (int)Storage::get('indexer/indexer_producers.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_producers.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/producers/{$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_producers.failed', json_encode($failedIds));
}
sleep($delay);
} catch (\Exception $e) {
echo "[SKIPPED] Failed to fetch {$url}\n";
$failedIds[] = $id;
Storage::put('indexer/indexer_producers.failed', json_encode($failedIds));
}
$success[] = $id;
Storage::put('indexer/indexer_producers.save', $i);
}
Storage::delete('indexer/indexer_producers.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
*/
private function fetchMalIds() : array
{
$this->info("Scraping Producer MAL IDs from https://myanimelist.net/anime/producer...\n");
$producers = \json_decode(
app('SerializerV4')->serialize(
app('JikanParser')
->getProducers(new ProducersRequest()),
'json'
),
true
)['producers'];
foreach ($producers as $producer) {
$this->ids[] = $producer['mal_id'];
}
Storage::put('indexer/producers_mal_id.json', json_encode($this->ids));
return json_decode(Storage::get('indexer/producers_mal_id.json'));
}
/**
* @return array
* @throws FileNotFoundException
*/
private function loadFailedMalIds() : array
{
if (!Storage::exists('indexer/indexer_producers.failed')) {
throw new FileNotFoundException('"indexer/indexer_producers.failed" does not exist');
}
return json_decode(Storage::get('indexer/indexer_producers.failed'));
}
}

11
app/Console/Kernel.php Executable file → Normal file
View File

@ -5,11 +5,14 @@ namespace App\Console;
use App\Console\Commands\ClearQueuedJobs;
use App\Console\Commands\CacheRemove;
use App\Console\Commands\Indexer\AnimeIndexer;
use App\Console\Commands\Indexer\AnimeSweepIndexer;
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\Indexer\MangaSweepIndexer;
use App\Console\Commands\Indexer\ProducersIndexer;
use App\Console\Commands\ManageMicrocaching;
use App\Console\Commands\ModifyCacheDriver;
use App\Console\Commands\ModifyCacheMethod;
@ -34,7 +37,10 @@ class Kernel extends ConsoleKernel
ManageMicrocaching::class,
AnimeIndexer::class,
MangaIndexer::class,
GenreIndexer::class
GenreIndexer::class,
ProducersIndexer::class,
AnimeSweepIndexer::class,
MangaSweepIndexer::class,
];
/**
@ -60,5 +66,8 @@ class Kernel extends ConsoleKernel
$schedule->command('indexer:genres')
->daily();
$schedule->command('indexer:producers')
->daily();
}
}

View File

@ -45,6 +45,7 @@ use Jikan\Request\Anime\AnimeRecommendationsRequest;
use Jikan\Request\Anime\AnimeRequest;
use Jikan\Request\Anime\AnimeReviewsRequest;
use Jikan\Request\Anime\AnimeStatsRequest;
use Jikan\Request\Anime\AnimeVideosEpisodesRequest;
use Jikan\Request\Anime\AnimeVideosRequest;
use Laravel\Lumen\Http\ResponseFactory;
use MongoDB\BSON\UTCDateTime;
@ -700,6 +701,105 @@ class AnimeController extends Controller
);
}
/**
* @OA\Get(
* path="/anime/{id}/videos/episodes",
* operationId="getAnimeVideosEpisodes",
* tags={"anime"},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* @OA\Schema(type="integer")
* ),
*
* @OA\Parameter(ref="#/components/parameters/page"),
*
* @OA\Response(
* response="200",
* description="Returns episode videos related to the entry",
* @OA\JsonContent(
* ref="#/components/schemas/anime_videos_episodes"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* ),
*
*
* @OA\Schema(
* schema="anime_videos_episodes",
* description="Anime Videos Episodes Resource",
*
* allOf={
* @OA\Schema(ref="#/components/schemas/pagination"),
* @OA\Schema(
* @OA\Property(
* property="data",
* type="array",
* @OA\Items(
* type="object",
* @OA\Property(
* property="mal_id",
* type="integer",
* description="MyAnimeList ID or Episode Number"
* ),
* @OA\Property(
* property="title",
* type="string",
* description="Episode Title"
* ),
* @OA\Property(
* property="episode",
* type="string",
* description="Episode Subtitle"
* ),
* @OA\Property(
* property="url",
* type="string",
* description="Episode Page URL",
* ),
* @OA\Property(
* property="images",
* ref="#/components/schemas/common_images"
* ),
* ),
* ),
* ),
* }
* )
*/
public function videosEpisodes(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 = $this->jikan->getAnimeVideosEpisodes(new AnimeVideosEpisodesRequest($id, $page));
$response = \json_decode($this->serializer->serialize($anime, 'json'), true);
$results = $this->updateCache($request, $results, $response);
}
$response = (new AnimeEpisodesResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/anime/{id}/pictures",

View File

@ -11,9 +11,6 @@ use Illuminate\Http\Request;
class MagazineController extends Controller
{
const MAX_RESULTS_PER_PAGE = 25;
/**
* @OA\Get(
* path="/magazines",
@ -21,6 +18,32 @@ class MagazineController extends Controller
* tags={"magazines"},
*
* @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/magazines_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",
@ -36,21 +59,21 @@ class MagazineController extends Controller
* ),
* )
*/
public function main(Request $request)
public function main(Request $request): MagazineCollection
{
$maxResultsPerPage = (int) env('MAX_RESULTS_PER_PAGE', 25);
$page = $request->get('page') ?? 1;
$limit = $request->get('limit') ?? self::MAX_RESULTS_PER_PAGE;
$limit = $request->get('limit') ?? $maxResultsPerPage;
if (!empty($limit)) {
$limit = (int) $limit;
$limit = (int) $limit;
if ($limit <= 0) {
$limit = 1;
}
if ($limit <= 0) {
$limit = 1;
}
if ($limit > self::MAX_RESULTS_PER_PAGE) {
$limit = self::MAX_RESULTS_PER_PAGE;
}
if ($limit > $maxResultsPerPage) {
$limit = $maxResultsPerPage;
}
$results = SearchQueryBuilderMagazine::query(

View File

@ -3,11 +3,16 @@
namespace App\Http\Controllers\V4DB;
use App\Anime;
use App\Http\Resources\V4\ExternalLinksResource;
use App\Producers;
use App\Http\HttpHelper;
use App\Http\HttpResponse;
use App\Http\QueryBuilder\SearchQueryBuilderProducer;
use App\Http\Resources\V4\AnimeCollection;
use App\Http\Resources\V4\ProducerCollection;
use App\Producer;
use Illuminate\Http\Request;
use Jikan\Model\Producer\Producer;
use MongoDB\BSON\UTCDateTime;
class ProducerController extends Controller
{
@ -17,58 +22,259 @@ class ProducerController extends Controller
/**
* @OA\Get(
* path="/producers",
* operationId="getProducers",
* path="/producers/{id}",
* operationId="getProducerById",
* tags={"producers"},
*
* @OA\Parameter(ref="#/components/parameters/page"),
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response="200",
* description="Returns producers collection",
* description="Returns producer resource",
* @OA\JsonContent(
* ref="#/components/schemas/producers"
* @OA\Property(
* property="data",
* ref="#/components/schemas/producer"
* )
* )
* ),
*
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function main(Request $request)
public function main(Request $request, int $id)
{
$page = $request->get('page') ?? 1;
$limit = $request->get('limit') ?? self::MAX_RESULTS_PER_PAGE;
$results = Producers::query()
->where('mal_id', $id)
->get();
if (!empty($limit)) {
$limit = (int) $limit;
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Producers::scrape($id);
if ($limit <= 0) {
$limit = 1;
if (HttpHelper::hasError($response)) {
return HttpResponse::notFound($request);
}
if ($limit > self::MAX_RESULTS_PER_PAGE) {
$limit = self::MAX_RESULTS_PER_PAGE;
if ($results->isEmpty()) {
$meta = [
'createdAt' => new UTCDateTime(),
'modifiedAt' => new UTCDateTime(),
'request_hash' => $this->fingerprint
];
}
$meta['modifiedAt'] = new UTCDateTime();
$response = $meta + $response;
if ($results->isEmpty()) {
Producers::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Producers::query()
->where('mal_id', $id)
->update($response);
}
$results = Producers::query()
->where('mal_id', $id)
->get();
}
$results = SearchQueryBuilderProducer::query(
$request,
Producer::query()
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new \App\Http\Resources\V4\ProducerResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
$results = $results
->paginate(
$limit,
['*'],
null,
$page
);
/**
* @OA\Get(
* path="/producers/{id}/full",
* operationId="getProducerFullById",
* tags={"producers"},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response="200",
* description="Returns producer resource",
* @OA\JsonContent(
* @OA\Property(
* property="data",
* ref="#/components/schemas/producer_full"
* )
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function full(Request $request, int $id)
{
$results = Producers::query()
->where('mal_id', $id)
->get();
return new ProducerCollection(
$results
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Producers::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()) {
Producers::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Producers::query()
->where('mal_id', $id)
->update($response);
}
$results = Producers::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new \App\Http\Resources\V4\ProducerFullResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
/**
* @OA\Get(
* path="/producers/{id}/external",
* operationId="getProducerExternal",
* tags={"producers"},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response="200",
* description="Returns producer's external links",
* @OA\JsonContent(
* ref="#/components/schemas/external_links"
* )
* ),
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function external(Request $request, int $id)
{
$results = Producers::query()
->where('mal_id', $id)
->get();
if (
$results->isEmpty()
|| $this->isExpired($request, $results)
) {
$response = Producers::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()) {
Producers::query()
->insert($response);
}
if ($this->isExpired($request, $results)) {
Producers::query()
->where('mal_id', $id)
->update($response);
}
$results = Producers::query()
->where('mal_id', $id)
->get();
}
if ($results->isEmpty()) {
return HttpResponse::notFound($request);
}
$response = (new ExternalLinksResource(
$results->first()
))->response();
return $this->prepareResponse(
$response,
$results,
$request
);
}
}

View File

@ -13,6 +13,7 @@ use App\Http\QueryBuilder\SearchQueryBuilderCharacter;
use App\Http\QueryBuilder\SearchQueryBuilderClub;
use App\Http\QueryBuilder\SearchQueryBuilderManga;
use App\Http\QueryBuilder\SearchQueryBuilderPeople;
use App\Http\QueryBuilder\SearchQueryBuilderProducer;
use App\Http\QueryBuilder\SearchQueryBuilderUsers;
use App\Http\Resources\V4\AnimeCharactersResource;
use App\Http\Resources\V4\AnimeCollection;
@ -20,10 +21,12 @@ 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\ProducerCollection;
use App\Http\Resources\V4\ResultsResource;
use App\Http\SearchQueryBuilder;
use App\Manga;
use App\Person;
use App\Producers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Jikan\Jikan;
@ -821,4 +824,87 @@ class SearchController extends Controller
$results
);
}
/**
* @OA\Get(
* path="/producers",
* operationId="getProducers",
* tags={"producers"},
*
* @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/producers_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 producers collection",
* @OA\JsonContent(
* ref="#/components/schemas/producers"
* )
* ),
*
* @OA\Response(
* response="400",
* description="Error: Bad request. When required parameters were not supplied.",
* ),
* )
*/
public function producers(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,
Producers::query()
);
$results = $results
->paginate(
$limit,
['*'],
null,
$page
);
return new ProducerCollection(
$results
);
}
}

View File

@ -14,14 +14,14 @@ use Jenssegers\Mongodb\Eloquent\Builder;
* schema="producers_query_orderby",
* description="Order by producers data",
* type="string",
* enum={"mal_id", "name", "count"}
* enum={"mal_id", "name", "count", "favorites", "established"}
* )
*/
class SearchQueryBuilderProducer implements SearchQueryBuilderInterface
{
const ORDER_BY = [
'mal_id', 'name', 'count'
'mal_id', 'name', 'count', 'favorites', 'established'
];
public static function query(Request $request, Builder $results) : Builder
@ -70,4 +70,4 @@ class SearchQueryBuilderProducer implements SearchQueryBuilderInterface
return $sort === 'desc' ? 'desc' : 'asc';
}
}
}

View File

@ -1,38 +1,39 @@
<?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 MAP_TYPES = [
'manga' => 'Manga',
'novels' => 'Novel',
'lightnovels' => 'Light Novel',
'oneshots' => 'One-shot',
'doujin' => 'Doujinshi',
'manhwa' => 'Manhwa',
'manhua' => 'Manhua'
];
/**
*
*/
const MAP_FILTER = [
'publishing', 'upcoming', 'bypopularity', 'favorite'
];
/**
* @param Request $request
* @param Builder $builder
@ -42,15 +43,15 @@
{
$mangaType = self::mapType($request->get('type'));
$filterType = self::mapFilter($request->get('filter'));
$results = $results
->where('type', '!=', 'Doujinshi');
if (!is_null($mangaType)) {
$results = $results
->where('type', $mangaType);
}
if (!is_null($filterType) && $filterType === 'publishing') {
$results = $results
->where('publishing', true)
@ -60,14 +61,14 @@
return $results;
}
if (!is_null($filterType) && $filterType === 'bypopularity') {
$results = $results
->orderBy('members', 'desc');
return $results;
}
if (!is_null($filterType) && $filterType === 'favorite') {
$results = $results
->orderBy('favorites', 'desc');
@ -82,7 +83,7 @@
return $results;
}
/**
* @param string|null $type
* @return string|null
@ -92,12 +93,12 @@
if (is_null($type)) {
return null;
}
$type = strtolower($type);
return self::MAP_TYPES[$type] ?? null;
}
/**
* @param string|null $filter
* @return string|null
@ -105,11 +106,11 @@
public static function mapFilter(?string $filter = null) : ?string
{
$filter = strtolower($filter);
if (!\in_array($filter, self::MAP_FILTER)) {
return null;
}
return $filter;
}
}
}

View File

@ -15,9 +15,11 @@ class AnimeEpisodesResource extends JsonResource
public function toArray($request)
{
return [
'last_visible_page' => $this['last_visible_page'] ?? 1,
'has_next_page' => $this['has_next_page'] ?? false,
'results' => $this['results']
'pagination' => [
'last_visible_page' => $this['last_visible_page'] ?? 1,
'has_next_page' => $this['has_next_page'] ?? false,
],
'data' => $this['results'],
];
}
}
}

View File

@ -39,8 +39,9 @@ class AnimeFullResource extends JsonResource
* type="array",
* description="All titles",
* @OA\Items(
* type="string"
* )
* type="object",
* ref="#/components/schemas/title",
* ),
* ),
* @OA\Property(
* property="title",

View File

@ -39,8 +39,9 @@ class AnimeResource extends JsonResource
* type="array",
* description="All titles",
* @OA\Items(
* type="string"
* )
* type="object",
* ref="#/components/schemas/title",
* ),
* ),
* @OA\Property(
* property="title",

View File

@ -70,6 +70,38 @@ class AnimeVideosResource extends JsonResource
* ),
* ),
* ),
* @OA\Property(
* property="music_videos",
* type="array",
* @OA\Items(
* type="object",
*
* @OA\Property(
* property="title",
* type="string",
* description="Title"
* ),
* @OA\Property(
* property="video",
* ref="#/components/schemas/trailer"
* ),
* @OA\Property (
* type="object",
* property="meta",
*
* @OA\Property (
* property="title",
* type="string",
* nullable=true
* ),
* @OA\Property (
* property="author",
* type="string",
* nullable=true
* ),
* ),
* ),
* ),
* ),
* )
*/
@ -77,7 +109,8 @@ class AnimeVideosResource extends JsonResource
{
return [
'promo' => $this['promo'],
'episodes' => $this['episodes']
'episodes' => $this['episodes'],
'music_videos' => $this['music_videos'],
];
}
}
}

View File

@ -11,13 +11,13 @@ class CommonResource extends JsonResource
* schema="trailer",
* type="object",
* description="Youtube Details",
*
*
* allOf={
* @OA\Schema(ref="#/components/schemas/trailer_base"),
* @OA\Schema(ref="#/components/schemas/trailer_images"),
* }
* }
* ),
*
*
* @OA\Schema(
* schema="trailer_base",
* type="object",
@ -42,7 +42,7 @@ class CommonResource extends JsonResource
* nullable=true
* ),
* ),
*
*
* @OA\Schema(
* schema="trailer_images",
* type="object",
@ -684,6 +684,21 @@ class CommonResource extends JsonResource
* ),
* ),
*
* @OA\Schema(
* schema="title",
* type="object",
* @OA\Property(
* property="type",
* type="string",
* description="Title type",
* ),
* @OA\Property(
* property="title",
* type="string",
* description="Title value",
* ),
* ),
*
*
*/
}
}

View File

@ -26,34 +26,6 @@ class MagazineCollection extends ResourceCollection
* schema="magazines",
* description="Magazine Collection Resource",
*
* @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/magazines_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")
* ),
*
* allOf={
* @OA\Schema(ref="#/components/schemas/pagination"),
* @OA\Schema(
@ -76,7 +48,13 @@ class MagazineCollection extends ResourceCollection
{
$this->pagination = [
'last_visible_page' => $resource->lastPage(),
'has_next_page' => $resource->hasMorePages()
'has_next_page' => $resource->hasMorePages(),
'current_page' => $resource->currentPage(),
'items' => [
'count' => $resource->count(),
'total' => $resource->total(),
'per_page' => $resource->perPage(),
],
];
$this->collection = $resource->getCollection();
@ -104,4 +82,4 @@ class MagazineCollection extends ResourceCollection
unset($jsonResponse['links'],$jsonResponse['meta']);
$response->setContent(json_encode($jsonResponse));
}
}
}

View File

@ -35,8 +35,9 @@ class MangaFullResource extends JsonResource
* type="array",
* description="All Titles",
* @OA\Items(
* type="string"
* )
* type="object",
* ref="#/components/schemas/title",
* ),
* ),
* @OA\Property(
* property="title",
@ -70,7 +71,7 @@ class MangaFullResource extends JsonResource
* @OA\Property(
* property="type",
* type="string",
* enum={"Manga", "Novel", "One-shot", "Doujinshi", "Manhua", "Manhwa", "OEL"},
* enum={"Manga", "Novel", "Light Novel", "One-shot", "Doujinshi", "Manhua", "Manhwa", "OEL"},
* description="Manga Type",
* nullable=true
* ),
@ -105,12 +106,14 @@ class MangaFullResource extends JsonResource
* property="score",
* type="number",
* format="float",
* description="Score"
* description="Score",
* nullable=true
* ),
* @OA\Property(
* property="scored_by",
* type="integer",
* description="Number of users"
* description="Number of users",
* nullable=true
* ),
* @OA\Property(
* property="rank",

View File

@ -35,8 +35,9 @@ class MangaResource extends JsonResource
* type="array",
* description="All Titles",
* @OA\Items(
* type="string"
* )
* type="object",
* ref="#/components/schemas/title",
* ),
* ),
* @OA\Property(
* property="title",
@ -61,7 +62,7 @@ class MangaResource extends JsonResource
* @OA\Property(
* property="type",
* type="string",
* enum={"Manga", "Novel", "One-shot", "Doujinshi", "Manhua", "Manhwa", "OEL"},
* enum={"Manga", "Novel", "Light Novel", "One-shot", "Doujinshi", "Manhua", "Manhwa", "OEL"},
* description="Manga Type",
* nullable=true
* ),
@ -96,12 +97,14 @@ class MangaResource extends JsonResource
* property="score",
* type="number",
* format="float",
* description="Score"
* description="Score",
* nullable=true
* ),
* @OA\Property(
* property="scored_by",
* type="integer",
* description="Number of users"
* description="Number of users",
* nullable=true
* ),
* @OA\Property(
* property="rank",

View File

@ -24,35 +24,7 @@ class ProducerCollection extends ResourceCollection
*
* @OA\Schema(
* schema="producers",
* description="Producer Collection Resource",
*
* @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/producers_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")
* ),
* description="Producers Collection Resource",
*
* allOf={
* @OA\Schema(ref="#/components/schemas/pagination"),
@ -75,7 +47,13 @@ class ProducerCollection extends ResourceCollection
{
$this->pagination = [
'last_visible_page' => $resource->lastPage(),
'has_next_page' => $resource->hasMorePages()
'has_next_page' => $resource->hasMorePages(),
'current_page' => $resource->currentPage(),
'items' => [
'count' => $resource->count(),
'total' => $resource->total(),
'per_page' => $resource->perPage(),
],
];
$this->collection = $resource->getCollection();
@ -103,4 +81,4 @@ class ProducerCollection extends ResourceCollection
unset($jsonResponse['links'],$jsonResponse['meta']);
$response->setContent(json_encode($jsonResponse));
}
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace App\Http\Resources\V4;
use Illuminate\Http\Resources\Json\JsonResource;
class ProducerFullResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*
* @OA\Schema(
* schema="producer_full",
* description="Producers Resource",
* @OA\Property(
* property="mal_id",
* type="integer",
* description="MyAnimeList ID"
* ),
* @OA\Property(
* property="url",
* type="string",
* description="MyAnimeList URL"
* ),
* @OA\Property(
* property="titles",
* type="array",
* description="All titles",
* @OA\Items(
* type="object",
* ref="#/components/schemas/title",
* ),
* ),
* @OA\Property(
* property="images",
* type="object",
* ref="#/components/schemas/common_images",
* ),
* @OA\Property(
* property="favorites",
* type="integer",
* description="Producers's member favorites count"
* ),
* @OA\Property(
* property="count",
* type="integer",
* description="Producers's anime count"
* ),
* @OA\Property(
* property="established",
* type="string",
* description="Established Date ISO8601",
* nullable=true
* ),
* @OA\Property(
* property="about",
* type="string",
* description="About the Producer",
* nullable=true
* ),
*
* @OA\Property(
* property="external",
* type="array",
*
* @OA\Items(
* type="object",
*
* @OA\Property(
* property="name",
* type="string",
* ),
* @OA\Property(
* property="url",
* type="string",
* ),
* ),
* ),
* ),
*/
public function toArray($request)
{
return [
'mal_id' => $this->mal_id,
'url' => $this->url,
'titles' => $this->titles,
'images' => $this->images,
'favorites' => $this->favorites ?? null,
'established' => $this->established ?? null,
'about' => $this->about ?? null,
'count' => $this->count ?? null,
'external' => $this->external_links ?? null
];
}
}

View File

@ -14,36 +14,66 @@ class ProducerResource extends JsonResource
*
* @OA\Schema(
* schema="producer",
* description="Producer Resource",
* description="Producers Resource",
* @OA\Property(
* property="mal_id",
* type="integer",
* description="MyAnimeList ID"
* ),
* @OA\Property(
* property="name",
* type="string",
* description="Producer Name"
* ),
* @OA\Property(
* property="url",
* type="string",
* description="MyAnimeList URL"
* ),
* @OA\Property(
* property="titles",
* type="array",
* description="All titles",
* @OA\Items(
* type="object",
* ref="#/components/schemas/title",
* ),
* ),
* @OA\Property(
* property="images",
* type="object",
* ref="#/components/schemas/common_images",
* ),
* @OA\Property(
* property="favorites",
* type="integer",
* description="Producers's member favorites count"
* ),
* @OA\Property(
* property="count",
* type="integer",
* description="Producer's anime count"
* description="Producers's anime count"
* ),
* @OA\Property(
* property="established",
* type="string",
* description="Established Date ISO8601",
* nullable=true
* ),
* @OA\Property(
* property="about",
* type="string",
* description="About the Producer",
* nullable=true
* ),
* ),
*/
public function toArray($request)
{
return [
'mal_id' => $this->mal_id,
'name' => $this->name,
'url' => $this->url,
'count' => $this->count
'titles' => $this->titles,
'images' => $this->images,
'favorites' => $this->favorites ?? null,
'established' => $this->established ?? null,
'about' => $this->about ?? null,
'count' => $this->count ?? null
];
}
}
}

View File

@ -85,7 +85,8 @@ class UserUpdatesResource extends JsonResource
* @OA\Property(
* property="score",
* type="integer",
* description="User Score"
* description="User Score",
* nullable=true
* ),
* @OA\Property(
* property="status",
@ -127,4 +128,4 @@ class UserUpdatesResource extends JsonResource
{
return $this['users'];
}
}
}

View File

@ -3,13 +3,9 @@
namespace App;
use Jenssegers\Mongodb\Eloquent\Model;
use Jikan\Request\Producer\ProducersRequest;
use Jikan\Request\Producer\ProducerRequest;
/**
* Class Magazine
* @package App
*/
class Producer extends Model
class Producers extends Model
{
/**
@ -18,7 +14,7 @@ class Producer extends Model
* @var array
*/
protected $fillable = [
'mal_id', 'name', 'url', 'count'
'mal_id', 'url', 'images', 'titles', 'established', 'favorites', 'about', 'external', 'count'
];
/**
@ -28,27 +24,26 @@ class Producer extends Model
*/
protected $table = 'producers';
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = [
'_id', 'expiresAt'
'_id', 'request_hash', 'expiresAt'
];
/**
* @return array
*/
public static function scrape() : array
public static function scrape(int $id)
{
$data = app('JikanParser')->getProducers(new ProducersRequest());
$data = app('JikanParser')->getProducer(new ProducerRequest($id));
return json_decode(
$data = json_decode(
app('SerializerV4')
->serialize($data, 'json'),
true
);
unset($data['results'], $data['has_next_page'], $data['last_visible_page']);
return $data;
}
}
}

903
composer.lock generated Executable file → Normal file

File diff suppressed because it is too large Load Diff

View File

@ -41,6 +41,10 @@ return [
'table_name' => 'anime_videos',
'ttl' => env('CACHE_DEFAULT_EXPIRE')
],
'AnimeController@videosEpisodes' => [
'table_name' => 'anime_videos_episodes',
'ttl' => env('CACHE_DEFAULT_EXPIRE')
],
'AnimeController@pictures' => [
'table_name' => 'anime_pictures',
'ttl' => env('CACHE_DEFAULT_EXPIRE')
@ -186,11 +190,7 @@ return [
* Producers
*/
'ProducerController@main' => [
'table_name' => 'common',
'ttl' => env('CACHE_PRODUCERS_EXPIRE')
],
'ProducerController@resource' => [
'table_name' => 'common',
'table_name' => 'producers',
'ttl' => env('CACHE_DEFAULT_EXPIRE')
],
@ -333,6 +333,10 @@ return [
'table_name' => 'common',
'ttl' => env('CACHE_SEARCH_EXPIRE')
],
'SearchController@producers' => [
'table_name' => 'common',
'ttl' => env('CACHE_SEARCH_EXPIRE')
],
'ClubController@main' => [
'table_name' => 'clubs',
@ -378,4 +382,4 @@ return [
'ttl' => env('CACHE_DEFAULT_EXPIRE')
],
];
];

View File

@ -17,6 +17,9 @@ class CreateProducersTable extends Migration
$table->unique(['mal_id' => 1], 'mal_id');
$table->index('count', 'count');
$table->index('name', 'name');
$table->index('titles', 'titles');
$table->index('established', 'established');
$table->index('favorites', 'favorites');
$table->index(
[

View File

@ -67,6 +67,10 @@ $router->group(
'uses' => 'AnimeController@videos'
]);
$router->get('/videos/episodes', [
'uses' => 'AnimeController@videosEpisodes'
]);
$router->get('/pictures', [
'uses' => 'AnimeController@pictures'
]);
@ -275,7 +279,34 @@ $router->group(
],
function() use ($router) {
$router->get('/', [
'uses' => 'ProducerController@main',
'uses' => 'SearchController@producers',
]);
$router->get('/{id:[0-9]+}', [
'uses' => 'ProducerController@main'
]);
}
);
$router->get('/producers', [
'uses' => 'SearchController@producers'
]);
$router->group(
[
'prefix' => 'producers/{id:[0-9]+}'
],
function () use ($router) {
$router->get('/', [
'uses' => 'ProducerController@main'
]);
$router->get('/full', [
'uses' => 'ProducerController@full'
]);
$router->get('/external', [
'uses' => 'ProducerController@external'
]);
}
);

View File

@ -361,6 +361,42 @@
}
}
},
"/anime/{id}/videos/episodes": {
"get": {
"tags": [
"anime"
],
"operationId": "getAnimeVideosEpisodes",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
},
{
"$ref": "#/components/parameters/page"
}
],
"responses": {
"200": {
"description": "Returns episode videos related to the entry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/anime_videos_episodes"
}
}
}
},
"400": {
"description": "Error: Bad request. When required parameters were not supplied."
}
}
}
},
"/anime/{id}/pictures": {
"get": {
"tags": [
@ -1130,6 +1166,38 @@
"parameters": [
{
"$ref": "#/components/parameters/page"
},
{
"$ref": "#/components/parameters/limit"
},
{
"name": "q",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "order_by",
"in": "query",
"schema": {
"$ref": "#/components/schemas/magazines_query_orderby"
}
},
{
"name": "sort",
"in": "query",
"schema": {
"$ref": "#/components/schemas/search_query_sort"
}
},
{
"name": "letter",
"in": "query",
"description": "Return entries starting with the given letter",
"schema": {
"type": "string"
}
}
],
"responses": {
@ -1827,24 +1895,105 @@
}
}
},
"/producers": {
"/producers/{id}": {
"get": {
"tags": [
"producers"
],
"operationId": "getProducers",
"operationId": "getProducerById",
"parameters": [
{
"$ref": "#/components/parameters/page"
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "Returns producers collection",
"description": "Returns producer resource",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/producers"
"properties": {
"data": {
"$ref": "#/components/schemas/producer"
}
},
"type": "object"
}
}
}
},
"400": {
"description": "Error: Bad request. When required parameters were not supplied."
}
}
}
},
"/producers/{id}/full": {
"get": {
"tags": [
"producers"
],
"operationId": "getProducerFullById",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "Returns producer resource",
"content": {
"application/json": {
"schema": {
"properties": {
"data": {
"$ref": "#/components/schemas/producer_full"
}
},
"type": "object"
}
}
}
},
"400": {
"description": "Error: Bad request. When required parameters were not supplied."
}
}
}
},
"/producers/{id}/external": {
"get": {
"tags": [
"producers"
],
"operationId": "getProducerExternal",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "Returns producer's external links",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/external_links"
}
}
}
@ -2773,6 +2922,66 @@
}
}
},
"/producers": {
"get": {
"tags": [
"producers"
],
"operationId": "getProducers",
"parameters": [
{
"$ref": "#/components/parameters/page"
},
{
"$ref": "#/components/parameters/limit"
},
{
"name": "q",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "order_by",
"in": "query",
"schema": {
"$ref": "#/components/schemas/producers_query_orderby"
}
},
{
"name": "sort",
"in": "query",
"schema": {
"$ref": "#/components/schemas/search_query_sort"
}
},
{
"name": "letter",
"in": "query",
"description": "Return entries starting with the given letter",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Returns producers collection",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/producers"
}
}
}
},
"400": {
"description": "Error: Bad request. When required parameters were not supplied."
}
}
}
},
"/seasons/{year}/{season}": {
"get": {
"tags": [
@ -3914,6 +4123,46 @@
}
]
},
"anime_videos_episodes": {
"description": "Anime Videos Episodes Resource",
"allOf": [
{
"properties": {
"data": {
"type": "array",
"items": {
"properties": {
"mal_id": {
"description": "MyAnimeList ID or Episode Number",
"type": "integer"
},
"title": {
"description": "Episode Title",
"type": "string"
},
"episode": {
"description": "Episode Subtitle",
"type": "string"
},
"url": {
"description": "Episode Page URL",
"type": "string"
},
"images": {
"$ref": "#/components/schemas/common_images"
}
},
"type": "object"
}
}
},
"type": "object"
},
{
"$ref": "#/components/schemas/pagination"
}
]
},
"character_pictures": {
"description": "Character Pictures",
"properties": {
@ -4456,7 +4705,9 @@
"enum": [
"mal_id",
"name",
"count"
"count",
"favorites",
"established"
]
},
"users_search_query_gender": {
@ -4631,7 +4882,7 @@
"description": "All titles",
"type": "array",
"items": {
"type": "string"
"$ref": "#/components/schemas/title"
}
},
"title": {
@ -4934,7 +5185,7 @@
"description": "All titles",
"type": "array",
"items": {
"type": "string"
"$ref": "#/components/schemas/title"
}
},
"title": {
@ -5292,6 +5543,34 @@
},
"type": "object"
}
},
"music_videos": {
"type": "array",
"items": {
"properties": {
"title": {
"description": "Title",
"type": "string"
},
"video": {
"$ref": "#/components/schemas/trailer"
},
"meta": {
"properties": {
"title": {
"type": "string",
"nullable": true
},
"author": {
"type": "string",
"nullable": true
}
},
"type": "object"
}
},
"type": "object"
}
}
},
"type": "object"
@ -6229,6 +6508,19 @@
},
"type": "object"
},
"title": {
"properties": {
"type": {
"description": "Title type",
"type": "string"
},
"title": {
"description": "Title value",
"type": "string"
}
},
"type": "object"
},
"external_links": {
"description": "External links",
"properties": {
@ -6461,7 +6753,7 @@
"description": "All Titles",
"type": "array",
"items": {
"type": "string"
"$ref": "#/components/schemas/title"
}
},
"title": {
@ -6495,6 +6787,7 @@
"enum": [
"Manga",
"Novel",
"Light Novel",
"One-shot",
"Doujinshi",
"Manhua",
@ -6534,11 +6827,13 @@
"score": {
"description": "Score",
"type": "number",
"format": "float"
"format": "float",
"nullable": true
},
"scored_by": {
"description": "Number of users",
"type": "integer"
"type": "integer",
"nullable": true
},
"rank": {
"description": "Ranking",
@ -6663,7 +6958,7 @@
"description": "All Titles",
"type": "array",
"items": {
"type": "string"
"$ref": "#/components/schemas/title"
}
},
"title": {
@ -6689,6 +6984,7 @@
"enum": [
"Manga",
"Novel",
"Light Novel",
"One-shot",
"Doujinshi",
"Manhua",
@ -6728,11 +7024,13 @@
"score": {
"description": "Score",
"type": "number",
"format": "float"
"format": "float",
"nullable": true
},
"scored_by": {
"description": "Number of users",
"type": "integer"
"type": "integer",
"nullable": true
},
"rank": {
"description": "Ranking",
@ -7213,7 +7511,7 @@
"type": "object"
},
"producers": {
"description": "Producer Collection Resource",
"description": "Producers Collection Resource",
"allOf": [
{
"properties": {
@ -7231,24 +7529,100 @@
}
]
},
"producer": {
"description": "Producer Resource",
"producer_full": {
"description": "Producers Resource",
"properties": {
"mal_id": {
"description": "MyAnimeList ID",
"type": "integer"
},
"name": {
"description": "Producer Name",
"url": {
"description": "MyAnimeList URL",
"type": "string"
},
"titles": {
"description": "All titles",
"type": "array",
"items": {
"$ref": "#/components/schemas/title"
}
},
"images": {
"$ref": "#/components/schemas/common_images"
},
"favorites": {
"description": "Producers's member favorites count",
"type": "integer"
},
"count": {
"description": "Producers's anime count",
"type": "integer"
},
"established": {
"description": "Established Date ISO8601",
"type": "string",
"nullable": true
},
"about": {
"description": "About the Producer",
"type": "string",
"nullable": true
},
"external": {
"type": "array",
"items": {
"properties": {
"name": {
"type": "string"
},
"url": {
"type": "string"
}
},
"type": "object"
}
}
},
"type": "object"
},
"producer": {
"description": "Producers Resource",
"properties": {
"mal_id": {
"description": "MyAnimeList ID",
"type": "integer"
},
"url": {
"description": "MyAnimeList URL",
"type": "string"
},
"count": {
"description": "Producer's anime count",
"titles": {
"description": "All titles",
"type": "array",
"items": {
"$ref": "#/components/schemas/title"
}
},
"images": {
"$ref": "#/components/schemas/common_images"
},
"favorites": {
"description": "Producers's member favorites count",
"type": "integer"
},
"count": {
"description": "Producers's anime count",
"type": "integer"
},
"established": {
"description": "Established Date ISO8601",
"type": "string",
"nullable": true
},
"about": {
"description": "About the Producer",
"type": "string",
"nullable": true
}
},
"type": "object"
@ -8353,7 +8727,8 @@
},
"score": {
"description": "User Score",
"type": "integer"
"type": "integer",
"nullable": true
},
"status": {
"description": "User list status",