Merge branch 'master' into irfan-dahir-patch-1

This commit is contained in:
Irfan (Nekomata) 2024-11-13 21:39:56 +05:00 committed by GitHub
commit 9d572e4b8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 458 additions and 84 deletions

View File

@ -69,7 +69,9 @@ jobs:
- name: Build and push by digest
id: build
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
env:
DOCKER_BUILD_NO_SUMMARY: true
with:
context: .
platforms: ${{ matrix.platform }}

View File

@ -14,6 +14,7 @@ For an entire list of commands, you can run `php artisan list`
- [Indexer](#indexer)
- [Anime](#indexer-anime)
- [Manga](#indexer-manga)
- [Incremental](#indexer-incremental)
## Commands
@ -66,7 +67,7 @@ Example: `cache:method queue`
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.
`Indexer:Anime` uses [https://github.com/purarue/mal-id-cache](https://github.com/purarue/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.
@ -90,7 +91,7 @@ This translates to running entries that previously failed to index or update, in
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.
`Indexer:Manga` uses [https://github.com/purarue/mal-id-cache](https://github.com/purarue/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.
@ -98,7 +99,7 @@ This function only needs to be run once. Any entry's cache updating will automat
Command:
```
indexer:anime
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}
@ -109,3 +110,16 @@ indexer:anime
Example: `indexer:manga`
This simply translates to running the indexer without any additional configuration.
#### Indexer: Incremental
Incrementally indexes media entries from MAL.
This command will compare the latest version of MAL ids from the [mal_id_cache](https://github.com/purarue/mal-id-cache)
github repository and compares them with the downloaded ids from the previous run. If no ids found from the previous run, a full indexing session is started.
Command:
```
indexer:incremental {mediaType*}
{--failed : Run only entries that failed to index last time}
{--resume : Resume from the last position}
{--delay=3 : Set a delay between requests}
```

View File

@ -52,7 +52,6 @@ For any additional help, join our [Discord server](http://discord.jikan.moe/).
| TypeScript | [jikants](https://github.com/Julien-Broyard/jikants) by Julien Broyard<br>[jikan-client](https://github.com/javi11/jikan-client) by Javier Blanco<br>🆕 **(v4)** [jikan-ts](https://github.com/tutkli/jikan-ts) by Clara Castillo |
| 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 | 🆕 **(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 |

View File

@ -2,7 +2,6 @@
namespace App\Console\Commands\Indexer;
use App\Exceptions\Console\CommandAlreadyRunningException;
use App\Exceptions\Console\FileNotFoundException;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
@ -67,7 +66,7 @@ class AnimeIndexer extends Command
$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");
$this->info("Info: AnimeIndexer uses purarue/mal-id-cache fetch available MAL IDs and updates/indexes them\n\n");
if ($failed && Storage::exists('indexer/indexer_anime.failed')) {
$this->ids = $this->loadFailedMalIds();
@ -140,14 +139,14 @@ class AnimeIndexer extends Command
/**
* @return array
* @url https://github.com/seanbreckenridge/mal-id-cache
* @url https://github.com/purarue/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");
$this->info("Fetching MAL ID Cache https://raw.githubusercontent.com/purarue/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'),
file_get_contents('https://raw.githubusercontent.com/purarue/mal-id-cache/master/cache/anime_cache.json'),
true
);

View File

@ -74,14 +74,14 @@ class AnimeSweepIndexer extends Command
/**
* @return array
* @url https://github.com/seanbreckenridge/mal-id-cache
* @url https://github.com/purarue/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");
$this->info("Fetching MAL ID Cache https://raw.githubusercontent.com/purarue/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'),
file_get_contents('https://raw.githubusercontent.com/purarue/mal-id-cache/master/cache/anime_cache.json'),
true
);

View File

@ -0,0 +1,228 @@
<?php
namespace App\Console\Commands\Indexer;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
class IncrementalIndexer extends Command
{
/**
* @var bool
*/
private bool $cancelled = false;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'indexer:incremental {mediaType*}
{--delay=3 : Set a delay between requests}
{--resume : Resume from the last position}
{--failed : Run only entries that failed to index last time}';
protected function promptForMissingArgumentsUsing(): array
{
return [
'mediaType' => ['The media type to index.', 'Valid values: anime, manga']
];
}
private function getExistingIds(string $mediaType): array
{
$existingIdsHash = "";
$existingIdsRaw = "";
if (Storage::exists("indexer/incremental/$mediaType.json"))
{
$existingIdsRaw = Storage::get("indexer/incremental/$mediaType.json");
$existingIdsHash = sha1($existingIdsRaw);
}
return [$existingIdsHash, $existingIdsRaw];
}
private function getIdsToFetch(string $mediaType): array
{
$idsToFetch = [];
[$existingIdsHash, $existingIdsRaw] = $this->getExistingIds($mediaType);
if ($this->cancelled)
{
return [];
}
$newIdsRaw = file_get_contents("https://raw.githubusercontent.com/purarue/mal-id-cache/master/cache/${mediaType}_cache.json");
$newIdsHash = sha1($newIdsRaw);
/** @noinspection PhpConditionAlreadyCheckedInspection */
if ($this->cancelled)
{
return [];
}
if ($newIdsHash !== $existingIdsHash)
{
$newIds = json_decode($newIdsRaw, true);
$existingIds = json_decode($existingIdsRaw, true);
if (is_null($existingIds) || count($existingIds) === 0)
{
$idsToFetch = $newIds;
}
else
{
foreach (["sfw", "nsfw"] as $t)
{
$idsToFetch[$t] = array_diff($existingIds[$t], $newIds[$t]);
}
}
Storage::put("indexer/incremental/$mediaType.json.tmp", $newIdsRaw);
}
return $idsToFetch;
}
private function getFailedIdsToFetch(string $mediaType): array
{
return json_decode(Storage::get("indexer/incremental/{$mediaType}_failed.json"));
}
private function fetchIds(string $mediaType, array $idsToFetch, bool $resume): void
{
$index = 0;
$success = [];
$failedIds = [];
$idCount = count($idsToFetch);
if ($resume && Storage::exists("indexer/incremental/{$mediaType}_resume.save"))
{
$index = (int)Storage::get("indexer/incremental/{$mediaType}_resume.save");
$this->info("Resuming from index: $index");
}
$ids = array_merge($idsToFetch['sfw'], $idsToFetch['nsfw']);
if ($index > 0 && !isset($ids[$index]))
{
$index = 0;
$this->warn('Invalid index; set back to 0');
}
Storage::put("indexer/incremental/{$mediaType}_resume.save", 0);
$this->info("$idCount $mediaType entries available");
for ($i = $index; $i <= ($idCount - 1); $i++)
{
if ($this->cancelled)
{
return;
}
$id = $ids[$index];
$url = env('APP_URL') . "/v4/$mediaType/$id";
$this->info("Indexing/Updating " . ($i + 1) . "/$idCount $url [MAL ID: $id]");
try
{
$response = json_decode(file_get_contents($url), true);
if (!isset($response['error']) || $response['status'] == 404)
{
continue;
}
$this->error("[SKIPPED] Failed to fetch $url - {$response['error']}");
}
catch (\Exception)
{
$this->warn("[SKIPPED] Failed to fetch $url");
$failedIds[] = $id;
Storage::put("indexer/incremental/$mediaType.failed", json_encode($failedIds));
}
$success[] = $id;
Storage::put("indexer/incremental/{$mediaType}_resume.save", $index);
}
Storage::delete("indexer/incremental/{$mediaType}_resume.save");
$this->info("--- Indexing of $mediaType is complete.");
$this->info(count($success) . ' entries indexed or updated.');
if (count($failedIds) > 0)
{
$this->info(count($failedIds) . ' entries failed to index or update. Re-run with --failed to requeue failed entries only.');
}
// finalize the latest state
Storage::move("indexer/incremental/$mediaType.json.tmp", "indexer/incremental/$mediaType.json");
}
public function handle(): int
{
// validate inputs
$validator = Validator::make(
[
'mediaType' => $this->argument('mediaType'),
'delay' => $this->option('delay'),
'resume' => $this->option('resume') ?? false,
'failed' => $this->option('failed') ?? false
],
[
'mediaType' => 'required|in:anime,manga',
'delay' => 'integer|min:1',
'resume' => 'bool|prohibited_with:failed',
'failed' => 'bool|prohibited_with:resume'
]
);
if ($validator->fails()) {
$this->error($validator->errors()->toJson());
return 1;
}
// we want to handle signals from the OS
$this->trap([SIGTERM, SIGQUIT, SIGINT], fn () => $this->cancelled = true);
$resume = $this->option('resume') ?? false;
$onlyFailed = $this->option('failed') ?? false;
/**
* @var $mediaTypes array
*/
$mediaTypes = $this->argument("mediaType");
foreach ($mediaTypes as $mediaType)
{
$idsToFetch = [];
// if "--failed" option is specified just run the failed ones
if ($onlyFailed && Storage::exists("indexer/incremental/{$mediaType}_failed.json"))
{
$idsToFetch["sfw"] = $this->getFailedIdsToFetch($mediaType);
}
else
{
$idsToFetch = $this->getIdsToFetch($mediaType);
}
if ($this->cancelled)
{
return 127;
}
$idCount = count($idsToFetch);
if ($idCount === 0)
{
continue;
}
$this->fetchIds($mediaType, $idsToFetch, $resume);
}
return 0;
}
}

View File

@ -67,7 +67,7 @@ class MangaIndexer extends Command
$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");
$this->info("Info: MangaIndexer uses purarue/mal-id-cache fetch available MAL IDs and updates/indexes them\n\n");
if ($failed && Storage::exists('indexer/indexer_manga.failed')) {
$this->ids = $this->loadFailedMalIds();
@ -140,14 +140,14 @@ class MangaIndexer extends Command
/**
* @return array
* @url https://github.com/seanbreckenridge/mal-id-cache
* @url https://github.com/purarue/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");
$this->info("Fetching MAL ID Cache https://raw.githubusercontent.com/purarue/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'),
file_get_contents('https://raw.githubusercontent.com/purarue/mal-id-cache/master/cache/manga_cache.json'),
true
);

View File

@ -71,14 +71,14 @@ class MangaSweepIndexer extends Command
/**
* @return array
* @url https://github.com/seanbreckenridge/mal-id-cache
* @url https://github.com/purarue/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");
$this->info("Fetching MAL ID Cache https://raw.githubusercontent.com/purarue/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'),
file_get_contents('https://raw.githubusercontent.com/purarue/mal-id-cache/master/cache/manga_cache.json'),
true
);

View File

@ -24,7 +24,8 @@ class Kernel extends ConsoleKernel
Indexer\GenreIndexer::class,
Indexer\ProducersIndexer::class,
Indexer\AnimeSweepIndexer::class,
Indexer\MangaSweepIndexer::class
Indexer\MangaSweepIndexer::class,
Indexer\IncrementalIndexer::class
];
/**

View File

@ -10,7 +10,7 @@ use Spatie\LaravelData\Optional;
/**
* @OA\Parameter(
* name="spoiler",
* name="spoilers",
* in="query",
* required=false,
* description="Any reviews that are tagged as a spoiler. Spoiler reviews are not returned by default. e.g usage: `?spoiler=true`",

View File

@ -4,7 +4,6 @@ namespace App\Dto;
use App\Concerns\HasRequestFingerprint;
use App\Contracts\DataRequest;
use App\DataPipes\MapRouteParametersDataPipe;
use App\Dto\Concerns\MapsRouteParameters;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\Json\ResourceCollection;
@ -13,12 +12,6 @@ use Spatie\LaravelData\Attributes\Validation\Min;
use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Attributes\Validation\Required;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\DataPipeline;
use Spatie\LaravelData\DataPipes\AuthorizedDataPipe;
use Spatie\LaravelData\DataPipes\CastPropertiesDataPipe;
use Spatie\LaravelData\DataPipes\DefaultValuesDataPipe;
use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe;
use Spatie\LaravelData\DataPipes\ValidatePropertiesDataPipe;
/**
* Base class for all requests/commands which are for looking up things by id.

View File

@ -2,14 +2,12 @@
namespace App\Dto;
use App\Casts\ContextualBooleanCast;
use App\Casts\EnumCast;
use App\Concerns\HasRequestFingerprint;
use App\Contracts\DataRequest;
use App\Dto\Concerns\HasPreliminaryParameter;
use App\Dto\Concerns\HasSpoilersParameter;
use App\Dto\Concerns\PreparesData;
use App\Enums\TopAnimeFilterEnum;
use App\Enums\TopReviewsTypeEnum;
use App\Rules\Attributes\EnumValidation;
use Illuminate\Http\JsonResponse;
@ -23,6 +21,6 @@ final class QueryTopReviewsCommand extends QueryTopItemsCommand implements DataR
{
use HasRequestFingerprint, HasPreliminaryParameter, HasSpoilersParameter, PreparesData;
#[WithCast(EnumCast::class, TopAnimeFilterEnum::class), EnumValidation(TopReviewsTypeEnum::class)]
#[WithCast(EnumCast::class, TopReviewsTypeEnum::class), EnumValidation(TopReviewsTypeEnum::class)]
public TopReviewsTypeEnum|Optional $type;
}

View File

@ -3,8 +3,6 @@
namespace App\Features;
use App\Dto\QuerySpecificAnimeSeasonCommand;
use App\Enums\AnimeSeasonEnum;
use App\Enums\AnimeStatusEnum;
use App\Enums\AnimeTypeEnum;
use Illuminate\Contracts\Database\Query\Builder;
use Illuminate\Support\Carbon;

View File

@ -7,7 +7,6 @@ use App\Enums\TopReviewsTypeEnum;
use App\Support\CachedData;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use Jikan\Helper\Constants;
use Jikan\MyAnimeList\MalClient;
use Jikan\Request\Reviews\ReviewsRequest;
@ -31,7 +30,7 @@ final class QueryTopReviewsHandler extends RequestHandlerWithScraperCache
$preliminary = $requestParams->get("preliminary", true);
return $this->scraperService->findList(
$requestFingerPrint,
fn (MalClient $jikan, ?int $page = null) => $jikan->getReviews(new ReviewsRequest($type, $page, $spoilers, $preliminary)),
fn (MalClient $jikan, ?int $page = null) => $jikan->getReviews(new ReviewsRequest(ensureEnumPrimitiveValue($type), $page, $spoilers, $preliminary)),
$requestParams->get("page"));
}
}

View File

@ -22,7 +22,7 @@ use App\Dto\AnimeThemesLookupCommand;
use App\Dto\AnimeUserUpdatesLookupCommand;
use App\Dto\AnimeVideosEpisodesLookupCommand;
use App\Dto\AnimeVideosLookupCommand;
use Illuminate\Http\Request;
use OpenApi\Annotations as OA;
class AnimeController extends Controller
{
@ -225,15 +225,17 @@ class AnimeController extends Controller
* nullable=true
* ),
* @OA\Property(
* property="duration",
* type="integer",
* description="Episode duration in seconds",
* nullable=true
* ),
* @OA\Property(
* property="aired",
* type="string",
* description="Aired Date ISO8601",
* nullable=true
* ),
* @OA\Property(
* property="score",
* type="float",
* description="Aggregated episode score (1.00 - 5.00) based on MyAnimeList user voting",
* minimum="1",
* maximum="5",
* nullable=true
* ),
* @OA\Property(
@ -669,7 +671,7 @@ class AnimeController extends Controller
*
* @OA\Parameter(ref="#/components/parameters/page"),
* @OA\Parameter(ref="#/components/parameters/preliminary"),
* @OA\Parameter(ref="#/components/parameters/spoiler"),
* @OA\Parameter(ref="#/components/parameters/spoilers"),
*
* @OA\Response(
* response="200",

View File

@ -15,7 +15,7 @@ use App\Dto\MangaRelationsLookupCommand;
use App\Dto\MangaReviewsLookupCommand;
use App\Dto\MangaStatsLookupCommand;
use App\Dto\MangaUserUpdatesLookupCommand;
use Illuminate\Http\Request;
use OpenApi\Annotations as OA;
class MangaController extends Controller
{
@ -387,7 +387,7 @@ class MangaController extends Controller
*
* @OA\Parameter(ref="#/components/parameters/page"),
* @OA\Parameter(ref="#/components/parameters/preliminary"),
* @OA\Parameter(ref="#/components/parameters/spoiler"),
* @OA\Parameter(ref="#/components/parameters/spoilers"),
*
* @OA\Response(
* response="200",

View File

@ -4,18 +4,20 @@ namespace App\Http\Controllers\V4DB;
use App\Dto\QueryAnimeReviewsCommand;
use App\Dto\QueryMangaReviewsCommand;
use OpenApi\Annotations as OA;
class ReviewsController extends Controller
{
/**
* @OA\Get(
* @OA\
* Get(
* path="/reviews/anime",
* operationId="getRecentAnimeReviews",
* tags={"reviews"},
*
* @OA\Parameter(ref="#/components/parameters/page"),
* @OA\Parameter(ref="#/components/parameters/preliminary"),
* @OA\Parameter(ref="#/components/parameters/spoiler"),
* @OA\Parameter(ref="#/components/parameters/spoilers"),
*
*
* @OA\Response(
@ -70,7 +72,7 @@ class ReviewsController extends Controller
*
* @OA\Parameter(ref="#/components/parameters/page"),
* @OA\Parameter(ref="#/components/parameters/preliminary"),
* @OA\Parameter(ref="#/components/parameters/spoiler"),
* @OA\Parameter(ref="#/components/parameters/spoilers"),
*
* @OA\Response(
* response="200",

View File

@ -3,6 +3,7 @@
namespace App\Http\Resources\V4;
use Illuminate\Http\Resources\Json\JsonResource;
use OpenApi\Annotations as OA;
class ReviewsResource extends JsonResource
{

View File

@ -72,6 +72,33 @@ class Producers extends JikanApiSearchableModel
];
}
public function getCollectionSchema(): array
{
return [
'name' => $this->searchableAs(),
'fields' => [
[
'name' => '.*',
'type' => 'auto',
],
[
'name' => 'titles',
'type' => 'string',
'optional' => false,
'infix' => true,
'sort' => true
],
[
'name' => 'url',
'type' => 'string',
'optional' => false,
'infix' => true,
'sort' => true
],
]
];
}
public function typesenseQueryBy(): array
{
return [

View File

@ -167,19 +167,39 @@ final class DefaultAnimeRepository extends DatabaseRepository implements AnimeRe
$finalFilter['$or'][] = [
// note: this expression only works with mongodb version 5.0.0 or higher
'$expr' => [
'$lte' => [
'$and' => [
[
'$dateDiff' => [
'startDate' => [
'$dateFromString' => [
'dateString' => '$aired.from'
'$lte' => [
[
'$dateDiff' => [
'startDate' => [
'$dateFromString' => [
'dateString' => '$aired.from'
]
],
'endDate' => new UTCDateTime($from),
'unit' => 'month'
]
],
'endDate' => new UTCDateTime($from),
'unit' => 'month'
]
3 // there are 3 months in a season, so anything that started in 3 months or less will be included
],
],
3 // there are 3 months in a season, so anything that started in 3 months or less will be included
[
'$gt' => [
[
'$dateDiff' => [
'startDate' => [
'$dateFromString' => [
'dateString' => '$aired.from'
]
],
'endDate' => new UTCDateTime($from),
'unit' => 'month'
]
],
0
]
]
]
],
'aired.to' => null,

View File

@ -114,3 +114,12 @@ if (! function_exists('cache')) {
return app('cache')->put(key($arguments[0]), reset($arguments[0]), $arguments[1] ?? null);
}
}
if (!function_exists("ensureEnumPrimitiveValue")) {
function ensureEnumPrimitiveValue(int|string|bool|float|null|\Spatie\Enum\Laravel\Enum $value): mixed {
if ($value instanceof \Spatie\Enum\Laravel\Enum) {
return $value->value;
}
return $value;
}
}

View File

@ -14,6 +14,7 @@
"php": "^8.1",
"ext-json": "*",
"ext-mongodb": "*",
"ext-pcntl": "*",
"amphp/http-client": "^4.6",
"danielmewes/php-rql": "dev-master",
"darkaonline/swagger-lume": "^9.0",

14
composer.lock generated
View File

@ -4554,16 +4554,16 @@
},
{
"name": "jikan-me/jikan",
"version": "v4.0.11",
"version": "v4.0.12",
"source": {
"type": "git",
"url": "https://github.com/jikan-me/jikan.git",
"reference": "fcc8d20817ce29332b496a21652b9965c6c8196c"
"reference": "dcb47237a9407473f484bd28a5e44479cdd916fc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jikan-me/jikan/zipball/fcc8d20817ce29332b496a21652b9965c6c8196c",
"reference": "fcc8d20817ce29332b496a21652b9965c6c8196c",
"url": "https://api.github.com/repos/jikan-me/jikan/zipball/dcb47237a9407473f484bd28a5e44479cdd916fc",
"reference": "dcb47237a9407473f484bd28a5e44479cdd916fc",
"shasum": ""
},
"require": {
@ -4602,7 +4602,7 @@
"description": "Jikan is an unofficial MyAnimeList API",
"support": {
"issues": "https://github.com/jikan-me/jikan/issues",
"source": "https://github.com/jikan-me/jikan/tree/v4.0.11"
"source": "https://github.com/jikan-me/jikan/tree/v4.0.12"
},
"funding": [
{
@ -4610,7 +4610,7 @@
"type": "patreon"
}
],
"time": "2024-05-30T08:15:50+00:00"
"time": "2024-09-20T22:15:42+00:00"
},
{
"name": "jms/metadata",
@ -13591,5 +13591,5 @@
"ext-mongodb": "*"
},
"platform-dev": [],
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.3.0"
}

View File

@ -260,6 +260,7 @@ return [
| `BadRequestException` | `405 - Method Not Allowed` | Requested Method is not supported for resource. Only `GET` requests are allowed |
| `RateLimitException` | `429 - Too Many Request` | You are being rate limited by Jikan or MyAnimeList is rate-limiting our servers (specified in the error response) |
| `UpstreamException`,`ParserException`,etc. | `500 - Internal Server Error` | Something didn't work. Try again later. If you see an error response with a `report_url` URL, please click on it to open an auto-generated GitHub issue |
| `ServiceUnavailableException` | `503 - Service Unavailable` | In most cases this is intentionally done if the service is down for maintenance. |
## JSON Error Response

View File

@ -34,6 +34,7 @@ display_help() {
echo "stop Stop Jikan API"
echo "validate-prereqs Validate pre-reqs installed (docker, docker-compose)"
echo "execute-indexers Execute the indexers, which will scrape and index data from MAL. (Notice: This can take days)"
echo "index-incrementally Executes the incremental indexers for each media type. (anime, manga)"
echo ""
}
@ -168,6 +169,11 @@ case "$1" in
$DOCKER_COMPOSE_CMD -p "$DOCKER_COMPOSE_PROJECT_NAME" exec jikan_rest php /app/artisan indexer:producers
echo "Indexing done!"
;;
"index-incrementally")
echo "Indexing..."
$DOCKER_COMPOSE_CMD -p "$DOCKER_COMPOSE_PROJECT_NAME" exec jikan_rest php /app/artisan indexer:incremental anime manga
echo "Indexing done!"
;;
*)
echo "No command specified, displaying help"
display_help

View File

@ -16,6 +16,9 @@ This will:
> **Note**: The script supports both `docker` and `podman`. In case of `podman` please bare in mind that sometimes the container name resolution doesn't work on the container network.
> In those cases you might have to install `aardvark-dns` package. On `Arch Linux` podman uses `netavark` network by default (in 2023) so you will need to install the before mentioned package.
> **Note 2**: The script will start the jikan API, but if you start it for the first time, it won't have any data in it!
> You will have to run the indexers through artisan to have data. See ["Running the indexer with the script"](#running-the-indexer-with-the-script) section.
The script has the following prerequisites and will notify you if these are not present:
- git
@ -36,6 +39,7 @@ start Start Jikan API (mongodb, typesense, redis, jikan-api wor
stop Stop Jikan API
validate-prereqs Validate pre-reqs installed (docker, docker-compose)
execute-indexers Execute the indexers, which will scrape and index data from MAL. (Notice: This can take days)
index-incrementally Executes the incremental indexers for each media type. (anime, manga)
```
### Running the indexer with the script

View File

@ -23,7 +23,7 @@ secrets:
services:
jikan_rest:
image: "jikanme/jikan-rest:${_JIKAN_API_VERSION:-latest}"
image: "docker.io/jikanme/jikan-rest:${_JIKAN_API_VERSION:-latest}"
user: "${APP_UID:-10001}:${APP_GID:-10001}"
networks:
- jikan_network
@ -116,7 +116,3 @@ services:
- typesense-data:/data
ports:
- "8108/tcp"
healthcheck:
test: [ 'CMD-SHELL', '{ ! [ -f "curl_created" ] && apt -qq update -y && apt -qq install -y curl && touch curl_created && curl -s -f http://localhost:8108/health; } || { curl -s -f http://localhost:8108/health; }' ]
interval: 5s
timeout: 2s

View File

@ -2,7 +2,7 @@
"openapi": "3.0.0",
"info": {
"title": "Jikan API",
"description": "[Jikan](https://jikan.moe) is an **Unofficial** MyAnimeList API.\nIt scrapes the website to satisfy the need for a complete API - which MyAnimeList lacks.\n\n# Information\n\n⚡ Jikan is powered by its awesome backers - 🙏 [Become a backer](https://www.patreon.com/jikan)\n\n## Rate Limiting\n\n| Duration | Requests |\n|----|----|\n| Daily | **Unlimited** |\n| Per Minute | 60 requests |\n| Per Second | 3 requests |\n\nNote: It's still possible to get rate limited from MyAnimeList.net instead.\n\n\n## JSON Notes\n- Any property (except arrays or objects) whose value does not exist or is undetermined, will be `null`.\n- Any array or object property whose value does not exist or is undetermined, will be empty.\n- Any `score` property whose value does not exist or is undetermined, will be `0`.\n- All dates and timestamps are returned in [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) format and in UTC timezone\n\n## Caching\nBy **CACHING**, we refer to the data parsed from MyAnimeList which is stored temporarily on our servers to provide better API performance.\n\nAll requests are cached for **24 hours**.\n\nThe following response headers will detail cache information.\n\n| Header | Remarks |\n| ---- | ---- |\n| `Expires` | Cache expiry date |\n| `Last-Modified` | Cache set date |\n| `X-Request-Fingerprint` | Unique request fingerprint (only for cachable requests, not queries) |\n\n\nNote: `X-Request-Fingerprint` will only be available on single resource requests and their child endpoints. e.g `/anime/1`, `/anime/1/relations`.\nThey won't be available on pages which perform queries, like /anime, or /top/anime, etc.\n\n## Allowed HTTP(s) requests\n\n**Jikan REST API does not provide authenticated requests for MyAnimeList.** This means you can not use it to update your anime/manga list.\nOnly GET requests are supported which return READ-ONLY data.\n\n## HTTP Responses\n\nAll error responses are accompanied by a JSON Error response.\n\n| Exception | HTTP Status | Remarks |\n| ---- | ---- | ---- |\n| N/A | `200 - OK` | The request was successful |\n| N/A | `304 - Not Modified` | You have the latest data (Cache Validation response) |\n| `BadRequestException`,`ValidationException` | `400 - Bad Request` | You've made an invalid request. Recheck documentation |\n| `BadResponseException` | `404 - Not Found` | The resource was not found or MyAnimeList responded with a `404` |\n| `BadRequestException` | `405 - Method Not Allowed` | Requested Method is not supported for resource. Only `GET` requests are allowed |\n| `RateLimitException` | `429 - Too Many Request` | You are being rate limited by Jikan or MyAnimeList is rate-limiting our servers (specified in the error response) |\n| `UpstreamException`,`ParserException`,etc. | `500 - Internal Server Error` | Something didn't work. Try again later. If you see an error response with a `report_url` URL, please click on it to open an auto-generated GitHub issue |\n\n## JSON Error Response\n\n```json\n {\n \"status\": 500,\n \"type\": \"InternalException\",\n \"message\": \"Exception Message\",\n \"error\": \"Exception Trace\",\n \"report_url\": \"https://github.com...\"\n }\n```\n\n| Property | Remarks |\n| ---- | ---- |\n| `status` | Returned HTTP Status Code |\n| `type` | Thrown Exception |\n| `message` | Human-readable error message |\n| `error` | Error response and trace from the API |\n| `report_url` | Clicking this would redirect you to a generated GitHub issue |\n\n\n## Cache Validation\n\n- All requests return a `ETag` header which is an MD5 hash of the response\n- You can use this hash to verify if there's new or updated content by suppliying it as the value for the `If-None-Match` in your next request header\n- You will get a HTTP `304 - Not Modified` response if the content has not changed\n- If the content has changed, you'll get a HTTP `200 - OK` response with the updated JSON response\n\n![Cache Validation](https://i.imgur.com/925ozVn.png 'Cache Validation')\n\n## Disclaimer\n\n- Jikan is not affiliated with MyAnimeList.net.\n- Jikan is a free, open-source API. Please use it responsibly.\n\n----\n\nBy using the API, you are agreeing to Jikan's [terms of use](https://jikan.moe/terms) policy.\n\n[v3 Documentation](https://jikan.docs.apiary.io/) - [Wrappers/SDKs](https://github.com/jikan-me/jikan#wrappers) - [Report an issue](https://github.com/jikan-me/jikan-rest/issues/new) - [Host your own server](https://github.com/jikan-me/jikan-rest)",
"description": "[Jikan](https://jikan.moe) is an **Unofficial** MyAnimeList API.\nIt scrapes the website to satisfy the need for a complete API - which MyAnimeList lacks.\n\n# Information\n\n⚡ Jikan is powered by its awesome backers - 🙏 [Become a backer](https://www.patreon.com/jikan)\n\n## Rate Limiting\n\n| Duration | Requests |\n|----|----|\n| Daily | **Unlimited** |\n| Per Minute | 60 requests |\n| Per Second | 3 requests |\n\nNote: It's still possible to get rate limited from MyAnimeList.net instead.\n\n\n## JSON Notes\n- Any property (except arrays or objects) whose value does not exist or is undetermined, will be `null`.\n- Any array or object property whose value does not exist or is undetermined, will be empty.\n- Any `score` property whose value does not exist or is undetermined, will be `0`.\n- All dates and timestamps are returned in [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) format and in UTC timezone\n\n## Caching\nBy **CACHING**, we refer to the data parsed from MyAnimeList which is stored temporarily on our servers to provide better API performance.\n\nAll requests are cached for **24 hours**.\n\nThe following response headers will detail cache information.\n\n| Header | Remarks |\n| ---- | ---- |\n| `Expires` | Cache expiry date |\n| `Last-Modified` | Cache set date |\n| `X-Request-Fingerprint` | Unique request fingerprint (only for cachable requests, not queries) |\n\n\nNote: `X-Request-Fingerprint` will only be available on single resource requests and their child endpoints. e.g `/anime/1`, `/anime/1/relations`.\nThey won't be available on pages which perform queries, like /anime, or /top/anime, etc.\n\n## Allowed HTTP(s) requests\n\n**Jikan REST API does not provide authenticated requests for MyAnimeList.** This means you can not use it to update your anime/manga list.\nOnly GET requests are supported which return READ-ONLY data.\n\n## HTTP Responses\n\nAll error responses are accompanied by a JSON Error response.\n\n| Exception | HTTP Status | Remarks |\n| ---- | ---- | ---- |\n| N/A | `200 - OK` | The request was successful |\n| N/A | `304 - Not Modified` | You have the latest data (Cache Validation response) |\n| `BadRequestException`,`ValidationException` | `400 - Bad Request` | You've made an invalid request. Recheck documentation |\n| `BadResponseException` | `404 - Not Found` | The resource was not found or MyAnimeList responded with a `404` |\n| `BadRequestException` | `405 - Method Not Allowed` | Requested Method is not supported for resource. Only `GET` requests are allowed |\n| `RateLimitException` | `429 - Too Many Request` | You are being rate limited by Jikan or MyAnimeList is rate-limiting our servers (specified in the error response) |\n| `UpstreamException`,`ParserException`,etc. | `500 - Internal Server Error` | Something didn't work. Try again later. If you see an error response with a `report_url` URL, please click on it to open an auto-generated GitHub issue |\n| `ServiceUnavailableException` | `503 - Service Unavailable` | In most cases this is intentionally done if the service is down for maintenance. |\n\n## JSON Error Response\n\n```json\n {\n \"status\": 500,\n \"type\": \"InternalException\",\n \"message\": \"Exception Message\",\n \"error\": \"Exception Trace\",\n \"report_url\": \"https://github.com...\"\n }\n```\n\n| Property | Remarks |\n| ---- | ---- |\n| `status` | Returned HTTP Status Code |\n| `type` | Thrown Exception |\n| `message` | Human-readable error message |\n| `error` | Error response and trace from the API |\n| `report_url` | Clicking this would redirect you to a generated GitHub issue |\n\n\n## Cache Validation\n\n- All requests return a `ETag` header which is an MD5 hash of the response\n- You can use this hash to verify if there's new or updated content by suppliying it as the value for the `If-None-Match` in your next request header\n- You will get a HTTP `304 - Not Modified` response if the content has not changed\n- If the content has changed, you'll get a HTTP `200 - OK` response with the updated JSON response\n\n![Cache Validation](https://i.imgur.com/925ozVn.png 'Cache Validation')\n\n## Disclaimer\n\n- Jikan is not affiliated with MyAnimeList.net.\n- Jikan is a free, open-source API. Please use it responsibly.\n\n----\n\nBy using the API, you are agreeing to Jikan's [terms of use](https://jikan.moe/terms) policy.\n\n[v3 Documentation](https://jikan.docs.apiary.io/) - [Wrappers/SDKs](https://github.com/jikan-me/jikan#wrappers) - [Report an issue](https://github.com/jikan-me/jikan-rest/issues/new) - [Host your own server](https://github.com/jikan-me/jikan-rest)",
"termsOfService": "https://jikan.moe/terms",
"contact": {
"name": "API Support (Discord)",
@ -587,7 +587,7 @@
"$ref": "#/components/parameters/preliminary"
},
{
"$ref": "#/components/parameters/spoiler"
"$ref": "#/components/parameters/spoilers"
}
],
"responses": {
@ -1605,7 +1605,7 @@
"$ref": "#/components/parameters/preliminary"
},
{
"$ref": "#/components/parameters/spoiler"
"$ref": "#/components/parameters/spoilers"
}
],
"responses": {
@ -2226,7 +2226,7 @@
"$ref": "#/components/parameters/preliminary"
},
{
"$ref": "#/components/parameters/spoiler"
"$ref": "#/components/parameters/spoilers"
}
],
"responses": {
@ -2258,7 +2258,7 @@
"$ref": "#/components/parameters/preliminary"
},
{
"$ref": "#/components/parameters/spoiler"
"$ref": "#/components/parameters/spoilers"
}
],
"responses": {
@ -4480,16 +4480,18 @@
"type": "string",
"nullable": true
},
"duration": {
"description": "Episode duration in seconds",
"type": "integer",
"nullable": true
},
"aired": {
"description": "Aired Date ISO8601",
"type": "string",
"nullable": true
},
"score": {
"description": "Aggregated episode score (1.00 - 5.00) based on MyAnimeList user voting",
"type": "float",
"maximum": "5",
"minimum": "1",
"nullable": true
},
"filler": {
"description": "Filler episode",
"type": "boolean"
@ -9086,8 +9088,8 @@
"type": "boolean"
}
},
"spoiler": {
"name": "spoiler",
"spoilers": {
"name": "spoilers",
"in": "query",
"description": "Any reviews that are tagged as a spoiler. Spoiler reviews are not returned by default. e.g usage: `?spoiler=true`",
"required": false,

View File

@ -190,4 +190,39 @@ class SeasonControllerTest extends TestCase
$this->assertIsArray($content["data"]);
$this->assertCount(2, $content["data"]);
}
public function testShouldNotIncludeNewlyStartedSeasonOfAnimeInPreviousSeasons()
{
Carbon::setTestNow(Carbon::parse("2024-10-26"));
$f = Anime::factory(1);
$startDate = "2024-10-02";
$carbonStartDate = Carbon::parse($startDate);
$state = $f->serializeStateDefinition([
"aired" => new CarbonDateRange($carbonStartDate, null)
]);
$state["aired"]["string"] = "Oct 2, 2024 to ?";
$state["airing"] = true;
$state["status"] = "Currently Airing";
$state["premiered"] = "Fall 2024";
$state["mal_id"] = 54857;
$state["title"] = "Re:Zero kara Hajimeru Isekai Seikatsu 3rd Season";
$state["episodes"] = 16;
$state["type"] = "TV";
$state["duration"] = "23 min per ep";
$state["score"] = 8.9;
$f->create($state);
$content = $this->getJsonResponse([], "/v4/seasons/now?filter=tv&continuing&page=1");
$this->seeStatusCode(200);
$this->assertCount(1, $content["data"]);
$content = $this->getJsonResponse([], "/v4/seasons/2024/summer?filter=tv&continuing&page=1");
$this->seeStatusCode(200);
$this->assertCount(0, $content["data"]);
$content = $this->getJsonResponse([], "/v4/seasons/2024/spring?filter=tv&continuing&page=1");
$this->seeStatusCode(200);
$this->assertCount(0, $content["data"]);
Carbon::setTestNow();
}
}

View File

@ -7,6 +7,11 @@ use App\Person;
use App\Testing\ScoutFlush;
use App\Testing\SyntheticMongoDbTransaction;
use Illuminate\Database\Eloquent\Factories\Sequence;
use Jikan\Exception\BadResponseException;
use Jikan\Exception\ParserException;
use Jikan\Model\Reviews\Reviews;
use Jikan\MyAnimeList\MalClient;
use Jikan\Parser\Reviews\ReviewsParser;
use Tests\TestCase;
class TopControllerTest extends TestCase
@ -14,6 +19,15 @@ class TopControllerTest extends TestCase
use SyntheticMongoDbTransaction;
use ScoutFlush;
public function topReviewTypeParametersProvider(): array
{
return [
"empty query string" => [[]],
"query string = `?type=anime`" => [["type" => "anime"]],
"query string = `?type=manga`" => [["type" => "manga"]],
];
}
public function testTopAnime()
{
Anime::factory(3)->state(new Sequence(
@ -290,4 +304,27 @@ class TopControllerTest extends TestCase
$this->get('/v4/top/anime/999')
->seeStatusCode(404);
}
/**
* @dataProvider topReviewTypeParametersProvider
* @param $params
* @return void
* @throws BadResponseException
* @throws ParserException
*/
public function testTopReviews($params)
{
$jikanParser = \Mockery::mock(MalClient::class)->makePartial();
$reviewsParser = \Mockery::mock(ReviewsParser::class)->makePartial();
$reviewsParser->allows()->getReviews()->andReturn([]);
$reviewsParser->allows()->hasNextPage()->andReturn(false);
$reviewsFacade = Reviews::fromParser($reviewsParser);
/** @noinspection PhpParamsInspection */
$jikanParser->allows()->getReviews(\Mockery::any())->andReturn($reviewsFacade);
$this->app->instance('JikanParser', $jikanParser);
$this->getJsonResponse($params,"/v4/top/reviews");
$this->seeStatusCode(200);
}
}