diff --git a/app/Concerns/HasRequestFingerprint.php b/app/Concerns/HasRequestFingerprint.php index 425eaf2..5a73c3d 100644 --- a/app/Concerns/HasRequestFingerprint.php +++ b/app/Concerns/HasRequestFingerprint.php @@ -4,6 +4,7 @@ namespace App\Concerns; use App\Http\HttpHelper; use Illuminate\Http\Request; +use Spatie\LaravelData\Resolvers\DataFromSomethingResolver; /** * Helper trait for data transfer objects @@ -16,7 +17,8 @@ trait HasRequestFingerprint public static function fromRequest(Request $request): ?static { - $result = new self(); + $result = app(DataFromSomethingResolver::class) + ->withoutMagicalCreation()->execute(self::class, $request); $result->fingerprint = HttpHelper::resolveRequestFingerprint($request); return $result; } diff --git a/app/Concerns/ScraperCacheTtl.php b/app/Concerns/ScraperCacheTtl.php index 7d61f56..e7b244a 100644 --- a/app/Concerns/ScraperCacheTtl.php +++ b/app/Concerns/ScraperCacheTtl.php @@ -2,10 +2,12 @@ namespace App\Concerns; +use Illuminate\Support\Env; + trait ScraperCacheTtl { - protected function cacheTtl(): int + protected static function cacheTtl(): int { - return (int) env('CACHE_DEFAULT_EXPIRE'); + return (int) Env::get('CACHE_DEFAULT_EXPIRE'); } } diff --git a/app/Contracts/AnimeRepository.php b/app/Contracts/AnimeRepository.php index e09c0b8..4867712 100644 --- a/app/Contracts/AnimeRepository.php +++ b/app/Contracts/AnimeRepository.php @@ -3,7 +3,8 @@ namespace App\Contracts; use App\Anime; -use \Illuminate\Database\Eloquent\Builder as EloquentBuilder; +use App\Enums\AnimeScheduleFilterEnum; +use Illuminate\Contracts\Database\Query\Builder as EloquentBuilder; use \Laravel\Scout\Builder as ScoutBuilder; /** @@ -22,4 +23,10 @@ interface AnimeRepository extends Repository public function orderByFavoriteCount(): EloquentBuilder|ScoutBuilder; public function orderByRank(): EloquentBuilder|ScoutBuilder; + + public function getCurrentlyAiring( + ?AnimeScheduleFilterEnum $filter = null, + bool $kids = false, + bool $sfw = false + ): EloquentBuilder; } diff --git a/app/Contracts/CachedScraperService.php b/app/Contracts/CachedScraperService.php index 4dc24bb..f9227bc 100644 --- a/app/Contracts/CachedScraperService.php +++ b/app/Contracts/CachedScraperService.php @@ -33,6 +33,4 @@ interface CachedScraperService public function findByKey(string $key, mixed $val, string $cacheKey): CachedData; public function get(string $cacheKey): CachedData; - - public function augmentResponse(JsonResponse|Response $response, string $cacheKey, CachedData $scraperResults): JsonResponse|Response; } diff --git a/app/Contracts/MangaRepository.php b/app/Contracts/MangaRepository.php index e5c5a09..28b60ad 100644 --- a/app/Contracts/MangaRepository.php +++ b/app/Contracts/MangaRepository.php @@ -3,7 +3,7 @@ namespace App\Contracts; use App\Manga; -use Illuminate\Database\Eloquent\Builder as EloquentBuilder; +use Illuminate\Contracts\Database\Query\Builder as EloquentBuilder; use Laravel\Scout\Builder as ScoutBuilder; /** @@ -20,4 +20,6 @@ interface MangaRepository extends Repository public function orderByFavoriteCount(): EloquentBuilder|ScoutBuilder; public function orderByRank(): EloquentBuilder|ScoutBuilder; + + public function exceptItemsWithAdultRating(): EloquentBuilder|ScoutBuilder; } diff --git a/app/Contracts/Repository.php b/app/Contracts/Repository.php index 82963c5..cc2fe8f 100644 --- a/app/Contracts/Repository.php +++ b/app/Contracts/Repository.php @@ -33,4 +33,6 @@ interface Repository extends RepositoryQuery public function scrape(int|string $id): array; public function insert(array $attributes): bool; + + public function random(int $numberOfRandomItems = 1): Collection; } diff --git a/app/Dto/AnimeEpisodesLookupCommand.php b/app/Dto/AnimeEpisodesLookupCommand.php index 2d25198..7bb5f3a 100644 --- a/app/Dto/AnimeEpisodesLookupCommand.php +++ b/app/Dto/AnimeEpisodesLookupCommand.php @@ -3,6 +3,7 @@ namespace App\Dto; use Illuminate\Http\JsonResponse; +use Spatie\LaravelData\Attributes\Validation\Min; use Spatie\LaravelData\Attributes\Validation\Numeric; use Spatie\LaravelData\Optional; @@ -11,6 +12,6 @@ use Spatie\LaravelData\Optional; */ final class AnimeEpisodesLookupCommand extends LookupDataCommand { - #[Numeric] + #[Numeric, Min(1)] public int|Optional $page; } diff --git a/app/Dto/AnimeNewsLookupCommand.php b/app/Dto/AnimeNewsLookupCommand.php index 70fb837..56df393 100644 --- a/app/Dto/AnimeNewsLookupCommand.php +++ b/app/Dto/AnimeNewsLookupCommand.php @@ -3,6 +3,7 @@ namespace App\Dto; use Illuminate\Http\JsonResponse; +use Spatie\LaravelData\Attributes\Validation\Min; use Spatie\LaravelData\Attributes\Validation\Numeric; use Spatie\LaravelData\Optional; @@ -11,6 +12,6 @@ use Spatie\LaravelData\Optional; */ final class AnimeNewsLookupCommand extends LookupDataCommand { - #[Numeric] + #[Numeric, Min(1)] public int|Optional $page; } diff --git a/app/Dto/AnimeReviewsLookupCommand.php b/app/Dto/AnimeReviewsLookupCommand.php index 5202705..0355917 100644 --- a/app/Dto/AnimeReviewsLookupCommand.php +++ b/app/Dto/AnimeReviewsLookupCommand.php @@ -7,6 +7,7 @@ use App\Enums\MediaReviewsSortEnum; use Illuminate\Http\JsonResponse; use Spatie\Enum\Laravel\Rules\EnumRule; use Spatie\LaravelData\Attributes\Validation\BooleanType; +use Spatie\LaravelData\Attributes\Validation\Min; use Spatie\LaravelData\Attributes\Validation\Numeric; use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Optional; @@ -16,7 +17,7 @@ use Spatie\LaravelData\Optional; */ final class AnimeReviewsLookupCommand extends LookupDataCommand { - #[Numeric] + #[Numeric, Min(1)] public int|Optional $page; #[WithCast(EnumCast::class, MediaReviewsSortEnum::class)] diff --git a/app/Dto/AnimeUserUpdatesLookupCommand.php b/app/Dto/AnimeUserUpdatesLookupCommand.php index 299e110..a0563ea 100644 --- a/app/Dto/AnimeUserUpdatesLookupCommand.php +++ b/app/Dto/AnimeUserUpdatesLookupCommand.php @@ -3,6 +3,7 @@ namespace App\Dto; use Illuminate\Http\JsonResponse; +use Spatie\LaravelData\Attributes\Validation\Min; use Spatie\LaravelData\Attributes\Validation\Numeric; use Spatie\LaravelData\Optional; @@ -11,6 +12,6 @@ use Spatie\LaravelData\Optional; */ final class AnimeUserUpdatesLookupCommand extends LookupDataCommand { - #[Numeric] + #[Numeric, Min(1)] public int|Optional $page; } diff --git a/app/Dto/AnimeVideosEpisodesLookupCommand.php b/app/Dto/AnimeVideosEpisodesLookupCommand.php index d3432d0..1b9bd7f 100644 --- a/app/Dto/AnimeVideosEpisodesLookupCommand.php +++ b/app/Dto/AnimeVideosEpisodesLookupCommand.php @@ -4,6 +4,7 @@ namespace App\Dto; use Illuminate\Http\JsonResponse; use Illuminate\Support\Optional; +use Spatie\LaravelData\Attributes\Validation\Min; use Spatie\LaravelData\Attributes\Validation\Numeric; /** @@ -11,6 +12,6 @@ use Spatie\LaravelData\Attributes\Validation\Numeric; */ final class AnimeVideosEpisodesLookupCommand extends LookupDataCommand { - #[Numeric] + #[Numeric, Min(1)] public int|Optional $page; } diff --git a/app/Dto/ClubMembersLookupCommand.php b/app/Dto/ClubMembersLookupCommand.php index 7beff30..38b10e5 100644 --- a/app/Dto/ClubMembersLookupCommand.php +++ b/app/Dto/ClubMembersLookupCommand.php @@ -3,6 +3,7 @@ namespace App\Dto; use Illuminate\Http\JsonResponse; +use Spatie\LaravelData\Attributes\Validation\Min; use Spatie\LaravelData\Attributes\Validation\Numeric; use Spatie\LaravelData\Optional; @@ -11,6 +12,6 @@ use Spatie\LaravelData\Optional; */ final class ClubMembersLookupCommand extends LookupDataCommand { - #[Numeric] + #[Numeric, Min(1)] public int|Optional $page; } diff --git a/app/Dto/MangaNewsLookupCommand.php b/app/Dto/MangaNewsLookupCommand.php index 2ccb6cb..985a682 100644 --- a/app/Dto/MangaNewsLookupCommand.php +++ b/app/Dto/MangaNewsLookupCommand.php @@ -4,6 +4,7 @@ namespace App\Dto; use Illuminate\Http\JsonResponse; +use Spatie\LaravelData\Attributes\Validation\Min; use Spatie\LaravelData\Attributes\Validation\Numeric; use Spatie\LaravelData\Optional; @@ -12,6 +13,6 @@ use Spatie\LaravelData\Optional; */ final class MangaNewsLookupCommand extends LookupDataCommand { - #[Numeric] + #[Numeric, Min(1)] public int|Optional $page; } diff --git a/app/Dto/MangaReviewsLookupCommand.php b/app/Dto/MangaReviewsLookupCommand.php index 23618da..570f458 100644 --- a/app/Dto/MangaReviewsLookupCommand.php +++ b/app/Dto/MangaReviewsLookupCommand.php @@ -8,6 +8,7 @@ use App\Enums\MediaReviewsSortEnum; use Illuminate\Http\JsonResponse; use Spatie\Enum\Laravel\Rules\EnumRule; use Spatie\LaravelData\Attributes\Validation\BooleanType; +use Spatie\LaravelData\Attributes\Validation\Min; use Spatie\LaravelData\Attributes\Validation\Numeric; use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Optional; @@ -17,7 +18,7 @@ use Spatie\LaravelData\Optional; */ final class MangaReviewsLookupCommand extends LookupDataCommand { - #[Numeric] + #[Numeric, Min(1)] public int|Optional $page; #[WithCast(EnumCast::class, MediaReviewsSortEnum::class)] diff --git a/app/Dto/MangaUserUpdatesLookupCommand.php b/app/Dto/MangaUserUpdatesLookupCommand.php index 0ba2193..c5e40f2 100644 --- a/app/Dto/MangaUserUpdatesLookupCommand.php +++ b/app/Dto/MangaUserUpdatesLookupCommand.php @@ -4,6 +4,7 @@ namespace App\Dto; use Illuminate\Http\JsonResponse; +use Spatie\LaravelData\Attributes\Validation\Min; use Spatie\LaravelData\Attributes\Validation\Numeric; use Spatie\LaravelData\Optional; @@ -12,6 +13,6 @@ use Spatie\LaravelData\Optional; */ final class MangaUserUpdatesLookupCommand extends LookupDataCommand { - #[Numeric] + #[Numeric, Min(1)] public int|Optional $page; } diff --git a/app/Dto/PersonAnimeLookupCommand.php b/app/Dto/PersonAnimeLookupCommand.php new file mode 100644 index 0000000..12ea971 --- /dev/null +++ b/app/Dto/PersonAnimeLookupCommand.php @@ -0,0 +1,13 @@ + + */ +final class PersonAnimeLookupCommand extends LookupDataCommand +{ +} diff --git a/app/Dto/PersonFullLookupCommand.php b/app/Dto/PersonFullLookupCommand.php new file mode 100644 index 0000000..fed959a --- /dev/null +++ b/app/Dto/PersonFullLookupCommand.php @@ -0,0 +1,13 @@ + + */ +final class PersonFullLookupCommand extends LookupDataCommand +{ +} diff --git a/app/Dto/PersonLookupCommand.php b/app/Dto/PersonLookupCommand.php new file mode 100644 index 0000000..fcd350b --- /dev/null +++ b/app/Dto/PersonLookupCommand.php @@ -0,0 +1,13 @@ + + */ +final class PersonLookupCommand extends LookupDataCommand +{ +} diff --git a/app/Dto/PersonMangaLookupCommand.php b/app/Dto/PersonMangaLookupCommand.php new file mode 100644 index 0000000..571c451 --- /dev/null +++ b/app/Dto/PersonMangaLookupCommand.php @@ -0,0 +1,13 @@ + + */ +final class PersonMangaLookupCommand extends LookupDataCommand +{ +} diff --git a/app/Dto/PersonPicturesLookupCommand.php b/app/Dto/PersonPicturesLookupCommand.php new file mode 100644 index 0000000..217abaf --- /dev/null +++ b/app/Dto/PersonPicturesLookupCommand.php @@ -0,0 +1,13 @@ + + */ +final class PersonPicturesLookupCommand extends LookupDataCommand +{ +} diff --git a/app/Dto/PersonVoicesLookupCommand.php b/app/Dto/PersonVoicesLookupCommand.php new file mode 100644 index 0000000..da6fc67 --- /dev/null +++ b/app/Dto/PersonVoicesLookupCommand.php @@ -0,0 +1,13 @@ + + */ +final class PersonVoicesLookupCommand extends LookupDataCommand +{ +} diff --git a/app/Dto/ProducerExternalLookupCommand.php b/app/Dto/ProducerExternalLookupCommand.php new file mode 100644 index 0000000..eed51e5 --- /dev/null +++ b/app/Dto/ProducerExternalLookupCommand.php @@ -0,0 +1,13 @@ + + */ +final class ProducerExternalLookupCommand extends LookupDataCommand +{ +} diff --git a/app/Dto/ProducerFullLookupCommand.php b/app/Dto/ProducerFullLookupCommand.php new file mode 100644 index 0000000..119670a --- /dev/null +++ b/app/Dto/ProducerFullLookupCommand.php @@ -0,0 +1,13 @@ + + */ +final class ProducerFullLookupCommand extends LookupDataCommand +{ +} diff --git a/app/Dto/ProducerLookupCommand.php b/app/Dto/ProducerLookupCommand.php new file mode 100644 index 0000000..10c98d1 --- /dev/null +++ b/app/Dto/ProducerLookupCommand.php @@ -0,0 +1,13 @@ + + */ +final class ProducerLookupCommand extends LookupDataCommand +{ +} diff --git a/app/Dto/QueryAnimeRecommendationsCommand.php b/app/Dto/QueryAnimeRecommendationsCommand.php new file mode 100644 index 0000000..529d309 --- /dev/null +++ b/app/Dto/QueryAnimeRecommendationsCommand.php @@ -0,0 +1,17 @@ + + */ +final class QueryAnimeRecommendationsCommand extends Data implements DataRequest +{ + use HasRequestFingerprint; +} diff --git a/app/Dto/QueryAnimeReviewsCommand.php b/app/Dto/QueryAnimeReviewsCommand.php new file mode 100644 index 0000000..55ae1b6 --- /dev/null +++ b/app/Dto/QueryAnimeReviewsCommand.php @@ -0,0 +1,7 @@ + + */ +final class QueryAnimeSchedulesCommand extends Data implements DataRequest +{ + use HasRequestFingerprint; + + #[Numeric, Min(1)] + public int $page = 1; + + #[IntegerType, Min(1), Nullable] + public ?int $limit; + + #[BooleanType] + public bool $kids = false; + + #[BooleanType] + public bool $sfw = false; + + #[WithCast(EnumCast::class, AnimeScheduleFilterEnum::class)] + public ?AnimeScheduleFilterEnum $filter; + + public static function rules(...$args): array + { + return [ + "filter" => [new EnumRule(AnimeScheduleFilterEnum::class), new Nullable()] + ]; + } + + /** @noinspection PhpUnused */ + public static function fromRequestAndDay(Request $request, ?string $day): self + { + /** + * @var QueryAnimeSchedulesCommand $data + */ + $data = self::fromRequest($request); + + if (!is_null($day)) { + $data->filter = AnimeScheduleFilterEnum::from($day); + } + + return $data; + } +} diff --git a/app/Dto/QueryMangaRecommendationsCommand.php b/app/Dto/QueryMangaRecommendationsCommand.php new file mode 100644 index 0000000..84247e0 --- /dev/null +++ b/app/Dto/QueryMangaRecommendationsCommand.php @@ -0,0 +1,17 @@ + + */ +final class QueryMangaRecommendationsCommand extends Data implements DataRequest +{ + use HasRequestFingerprint; +} diff --git a/app/Dto/QueryMangaReviewsCommand.php b/app/Dto/QueryMangaReviewsCommand.php new file mode 100644 index 0000000..1d9de00 --- /dev/null +++ b/app/Dto/QueryMangaReviewsCommand.php @@ -0,0 +1,7 @@ + + */ +final class QueryRandomAnimeCommand extends Data implements DataRequest +{ + #[BooleanType] + public bool|Optional $sfw; +} diff --git a/app/Dto/QueryRandomCharacterCommand.php b/app/Dto/QueryRandomCharacterCommand.php new file mode 100644 index 0000000..ff71e1d --- /dev/null +++ b/app/Dto/QueryRandomCharacterCommand.php @@ -0,0 +1,14 @@ + + */ +final class QueryRandomCharacterCommand extends Data implements DataRequest +{ +} diff --git a/app/Dto/QueryRandomMangaCommand.php b/app/Dto/QueryRandomMangaCommand.php new file mode 100644 index 0000000..ccf8b52 --- /dev/null +++ b/app/Dto/QueryRandomMangaCommand.php @@ -0,0 +1,18 @@ + + */ +final class QueryRandomMangaCommand extends Data implements DataRequest +{ + #[BooleanType] + public bool|Optional $sfw; +} diff --git a/app/Dto/QueryRandomPersonCommand.php b/app/Dto/QueryRandomPersonCommand.php new file mode 100644 index 0000000..10e07fd --- /dev/null +++ b/app/Dto/QueryRandomPersonCommand.php @@ -0,0 +1,14 @@ + + */ +final class QueryRandomPersonCommand extends Data implements DataRequest +{ +} diff --git a/app/Dto/QueryRandomUserCommand.php b/app/Dto/QueryRandomUserCommand.php new file mode 100644 index 0000000..7a39bcf --- /dev/null +++ b/app/Dto/QueryRandomUserCommand.php @@ -0,0 +1,14 @@ + + */ +final class QueryRandomUserCommand extends Data implements DataRequest +{ +} diff --git a/app/Dto/QueryReviewsCommand.php b/app/Dto/QueryReviewsCommand.php new file mode 100644 index 0000000..c26658d --- /dev/null +++ b/app/Dto/QueryReviewsCommand.php @@ -0,0 +1,44 @@ + + */ +abstract class QueryReviewsCommand extends Data implements DataRequest +{ + use HasRequestFingerprint; + + #[Numeric, Min(1)] + public int|Optional $page; + + #[WithCast(EnumCast::class, MediaReviewsSortEnum::class)] + public MediaReviewsSortEnum|Optional $sort; + + #[BooleanType] + public bool|Optional $spoilers; + + #[BooleanType] + public bool|Optional $preliminary; + + public static function rules(): array + { + return [ + "sort" => [new EnumRule(MediaReviewsSortEnum::class)] + ]; + } +} diff --git a/app/Dto/QueryTopItemsCommand.php b/app/Dto/QueryTopItemsCommand.php index db5de07..d4f6dc5 100644 --- a/app/Dto/QueryTopItemsCommand.php +++ b/app/Dto/QueryTopItemsCommand.php @@ -3,11 +3,15 @@ namespace App\Dto; use Illuminate\Support\Optional; +use Spatie\LaravelData\Attributes\Validation\Min; +use Spatie\LaravelData\Attributes\Validation\Numeric; use Spatie\LaravelData\Data; abstract class QueryTopItemsCommand extends Data { + #[Numeric, Min(1)] public int|Optional $page; + #[Numeric, Min(1)] public int|Optional $limit; } diff --git a/app/Enums/AnimeScheduleFilterEnum.php b/app/Enums/AnimeScheduleFilterEnum.php new file mode 100644 index 0000000..550e165 --- /dev/null +++ b/app/Enums/AnimeScheduleFilterEnum.php @@ -0,0 +1,34 @@ +value !== self::other()->value && $this->value !== self::unknown()->value; + } + + protected static function labels(): array + { + return [ + ...collect(self::values())->map(fn ($x) => ucfirst($x))->toArray(), + "other" => "Not scheduled once per week", + "unknown" => "Unknown" + ]; + } +} diff --git a/app/Enums/ReviewTypeEnum.php b/app/Enums/ReviewTypeEnum.php new file mode 100644 index 0000000..9156663 --- /dev/null +++ b/app/Enums/ReviewTypeEnum.php @@ -0,0 +1,14 @@ +scraperService->find($request->id, $requestFingerprint); $resource = $this->resource($results->collect()); - return $this->scraperService->augmentResponse($resource->response(), $requestFingerprint, $results); + return $resource->response()->addJikanCacheFlags($requestFingerprint, $results); } protected abstract function resource(Collection $results): JsonResource; diff --git a/app/Features/PersonAnimeLookupHandler.php b/app/Features/PersonAnimeLookupHandler.php new file mode 100644 index 0000000..852fcfe --- /dev/null +++ b/app/Features/PersonAnimeLookupHandler.php @@ -0,0 +1,24 @@ + + */ +final class PersonAnimeLookupHandler extends ItemLookupHandler +{ + public function requestClass(): string + { + return PersonAnimeLookupCommand::class; + } + + protected function resource(Collection $results): PersonAnimeCollection + { + return new PersonAnimeCollection($results->offsetGetFirst("anime_staff_positions")); + } +} diff --git a/app/Features/PersonFullLookupHandler.php b/app/Features/PersonFullLookupHandler.php new file mode 100644 index 0000000..fd920ec --- /dev/null +++ b/app/Features/PersonFullLookupHandler.php @@ -0,0 +1,25 @@ + + */ +final class PersonFullLookupHandler extends ItemLookupHandler +{ + public function requestClass(): string + { + return PersonFullLookupCommand::class; + } + + protected function resource(Collection $results): JsonResource + { + return new PersonFullResource($results->first()); + } +} diff --git a/app/Features/PersonLookupHandler.php b/app/Features/PersonLookupHandler.php new file mode 100644 index 0000000..cb03d0d --- /dev/null +++ b/app/Features/PersonLookupHandler.php @@ -0,0 +1,25 @@ + + */ +final class PersonLookupHandler extends ItemLookupHandler +{ + public function requestClass(): string + { + return PersonLookupCommand::class; + } + + protected function resource(Collection $results): JsonResource + { + return new PersonResource($results->first()); + } +} diff --git a/app/Features/PersonMangaLookupHandler.php b/app/Features/PersonMangaLookupHandler.php new file mode 100644 index 0000000..838699e --- /dev/null +++ b/app/Features/PersonMangaLookupHandler.php @@ -0,0 +1,25 @@ + + */ +final class PersonMangaLookupHandler extends ItemLookupHandler +{ + public function requestClass(): string + { + return PersonMangaLookupCommand::class; + } + + protected function resource(Collection $results): JsonResource + { + return new PersonMangaCollection($results->offsetGetFirst("published_manga")); + } +} diff --git a/app/Features/PersonPicturesLookupHandler.php b/app/Features/PersonPicturesLookupHandler.php new file mode 100644 index 0000000..117074e --- /dev/null +++ b/app/Features/PersonPicturesLookupHandler.php @@ -0,0 +1,39 @@ + + */ +final class PersonPicturesLookupHandler extends RequestHandlerWithScraperCache +{ + public function requestClass(): string + { + return PersonPicturesLookupCommand::class; + } + + protected function resource(Collection $results): JsonResource + { + return new PicturesResource($results->first()); + } + + protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData + { + $id = $requestParams->get("id"); + return $this->scraperService->findList( + $requestFingerPrint, + fn(MalClient $jikan, ?int $page = null) => collect( + ["pictures" => $jikan->getPersonPictures(new PersonPicturesRequest($id))] + ) + ); + } +} diff --git a/app/Features/PersonVoicesLookupHandler.php b/app/Features/PersonVoicesLookupHandler.php new file mode 100644 index 0000000..c5bb2d1 --- /dev/null +++ b/app/Features/PersonVoicesLookupHandler.php @@ -0,0 +1,25 @@ + + */ +final class PersonVoicesLookupHandler extends ItemLookupHandler +{ + public function requestClass(): string + { + return PersonVoicesLookupCommand::class; + } + + protected function resource(Collection $results): JsonResource + { + return new PersonVoicesCollection($results->offsetGetFirst("voice_acting_roles")); + } +} diff --git a/app/Features/ProducerExternalLookupHandler.php b/app/Features/ProducerExternalLookupHandler.php new file mode 100644 index 0000000..9270938 --- /dev/null +++ b/app/Features/ProducerExternalLookupHandler.php @@ -0,0 +1,25 @@ + + */ +final class ProducerExternalLookupHandler extends ItemLookupHandler +{ + public function requestClass(): string + { + return ProducerExternalLookupCommand::class; + } + + protected function resource(Collection $results): JsonResource + { + return new ExternalLinksResource($results->first()); + } +} diff --git a/app/Features/ProducerFullLookupHandler.php b/app/Features/ProducerFullLookupHandler.php new file mode 100644 index 0000000..33147ee --- /dev/null +++ b/app/Features/ProducerFullLookupHandler.php @@ -0,0 +1,25 @@ + + */ +final class ProducerFullLookupHandler extends ItemLookupHandler +{ + public function requestClass(): string + { + return ProducerFullLookupCommand::class; + } + + protected function resource(Collection $results): JsonResource + { + return new ProducerFullResource($results->first()); + } +} diff --git a/app/Features/ProducerLookupHandler.php b/app/Features/ProducerLookupHandler.php new file mode 100644 index 0000000..a69ada8 --- /dev/null +++ b/app/Features/ProducerLookupHandler.php @@ -0,0 +1,25 @@ + + */ +final class ProducerLookupHandler extends ItemLookupHandler +{ + public function requestClass(): string + { + return ProducerLookupCommand::class; + } + + protected function resource(Collection $results): JsonResource + { + return new ProducerResource($results->first()); + } +} diff --git a/app/Features/QueryAnimeRecommendationsHandler.php b/app/Features/QueryAnimeRecommendationsHandler.php new file mode 100644 index 0000000..d08b37a --- /dev/null +++ b/app/Features/QueryAnimeRecommendationsHandler.php @@ -0,0 +1,22 @@ + + */ +final class QueryAnimeRecommendationsHandler extends QueryRecommendationsHandler +{ + public function requestClass(): string + { + return QueryAnimeRecommendationsCommand::class; + } + + protected function recommendationType(): string + { + return Constants::RECENT_RECOMMENDATION_ANIME; + } +} diff --git a/app/Features/QueryAnimeReviewsHandler.php b/app/Features/QueryAnimeReviewsHandler.php new file mode 100644 index 0000000..3b0dfd2 --- /dev/null +++ b/app/Features/QueryAnimeReviewsHandler.php @@ -0,0 +1,25 @@ + + */ +final class QueryAnimeReviewsHandler extends QueryReviewsHandler +{ + protected function reviewType(): ReviewTypeEnum + { + return ReviewTypeEnum::anime(); + } + + /** + * @inheritDoc + */ + public function requestClass(): string + { + return QueryAnimeReviewsCommand::class; + } +} diff --git a/app/Features/QueryAnimeSchedulesHandler.php b/app/Features/QueryAnimeSchedulesHandler.php new file mode 100644 index 0000000..fd17126 --- /dev/null +++ b/app/Features/QueryAnimeSchedulesHandler.php @@ -0,0 +1,50 @@ + + */ +final class QueryAnimeSchedulesHandler implements RequestHandler +{ + public function __construct(private readonly AnimeRepository $repository) + { + } + + /** + * @inheritDoc + */ + public function handle($request) + { + $limit = intval($request->limit ?? Env::get("MAX_RESULTS_PER_PAGE", 25)); + $results = $this->repository->getCurrentlyAiring($request->filter, $request->kids, $request->sfw); + $results = $results->paginate( + $limit, + ["*"], + null, + $request->page + ); + + $animeCollection = new AnimeCollection( + $results + ); + $response = $animeCollection->response(); + + return $response->addJikanCacheFlags($request->getFingerPrint(), CachedData::from($animeCollection->collection)); + } + + /** + * @inheritDoc + */ + public function requestClass(): string + { + return QueryAnimeSchedulesCommand::class; + } +} diff --git a/app/Features/QueryMangaRecommendationsHandler.php b/app/Features/QueryMangaRecommendationsHandler.php new file mode 100644 index 0000000..a3c8509 --- /dev/null +++ b/app/Features/QueryMangaRecommendationsHandler.php @@ -0,0 +1,25 @@ + + */ +final class QueryMangaRecommendationsHandler extends QueryRecommendationsHandler +{ + protected function recommendationType(): string + { + return Constants::RECENT_RECOMMENDATION_MANGA; + } + + /** + * @inheritDoc + */ + public function requestClass(): string + { + return QueryMangaRecommendationsCommand::class; + } +} diff --git a/app/Features/QueryMangaReviewsHandler.php b/app/Features/QueryMangaReviewsHandler.php new file mode 100644 index 0000000..c1731a7 --- /dev/null +++ b/app/Features/QueryMangaReviewsHandler.php @@ -0,0 +1,25 @@ + + */ +final class QueryMangaReviewsHandler extends QueryReviewsHandler +{ + protected function reviewType(): ReviewTypeEnum + { + return ReviewTypeEnum::manga(); + } + + /** + * @inheritDoc + */ + public function requestClass(): string + { + return QueryMangaReviewsCommand::class; + } +} diff --git a/app/Features/QueryRandomAnimeHandler.php b/app/Features/QueryRandomAnimeHandler.php new file mode 100644 index 0000000..57a20d8 --- /dev/null +++ b/app/Features/QueryRandomAnimeHandler.php @@ -0,0 +1,51 @@ + + */ +final class QueryRandomAnimeHandler implements RequestHandler +{ + public function __construct( + private readonly AnimeRepository $repository + ) + { + } + + /** + * @inheritDoc + */ + public function handle($request): AnimeResource + { + $sfw = Optional::create() !== $request->sfw ? $request->sfw : null; + + /** + * @var Collection $results; + */ + if ($sfw) { + $results = $this->repository->exceptItemsWithAdultRating()->random(); + } else { + $results = $this->repository->random(); + } + + return new AnimeResource( + $results->first() + ); + } + + /** + * @inheritDoc + */ + public function requestClass(): string + { + return QueryRandomAnimeCommand::class; + } +} diff --git a/app/Features/QueryRandomCharacterHandler.php b/app/Features/QueryRandomCharacterHandler.php new file mode 100644 index 0000000..c66a2cd --- /dev/null +++ b/app/Features/QueryRandomCharacterHandler.php @@ -0,0 +1,33 @@ + + */ +final class QueryRandomCharacterHandler extends QueryRandomItemHandler +{ + public function __construct(CharacterRepository $repository) + { + parent::__construct($repository); + } + + /** + * @inheritDoc + */ + public function requestClass(): string + { + return QueryRandomCharacterCommand::class; + } + + protected function resource(Collection $results): JsonResource + { + return new CharacterResource($results->first()); + } +} diff --git a/app/Features/QueryRandomItemHandler.php b/app/Features/QueryRandomItemHandler.php new file mode 100644 index 0000000..6db9e1f --- /dev/null +++ b/app/Features/QueryRandomItemHandler.php @@ -0,0 +1,34 @@ + + */ +abstract class QueryRandomItemHandler implements RequestHandler +{ + protected function __construct(protected readonly Repository $repository) + { + } + + /** + * @inheritDoc + */ + public function handle($request) + { + $results = $this->repository->random(); + return $this->resource($results); + } + + protected abstract function resource(Collection $results): JsonResource; +} diff --git a/app/Features/QueryRandomMangaHandler.php b/app/Features/QueryRandomMangaHandler.php new file mode 100644 index 0000000..99955ff --- /dev/null +++ b/app/Features/QueryRandomMangaHandler.php @@ -0,0 +1,51 @@ + + */ +final class QueryRandomMangaHandler implements RequestHandler +{ + public function __construct( + private readonly MangaRepository $repository + ) + { + } + + /** + * @inheritDoc + */ + public function handle($request) + { + $sfw = Optional::create() !== $request->sfw ? $request->sfw : null; + + /** + * @var Collection $results; + */ + if ($sfw) { + $results = $this->repository->exceptItemsWithAdultRating()->random(); + } else { + $results = $this->repository->random(); + } + + return new MangaResource( + $results->first() + ); + } + + /** + * @inheritDoc + */ + public function requestClass(): string + { + return QueryRandomMangaCommand::class; + } +} diff --git a/app/Features/QueryRandomPersonHandler.php b/app/Features/QueryRandomPersonHandler.php new file mode 100644 index 0000000..fcc1560 --- /dev/null +++ b/app/Features/QueryRandomPersonHandler.php @@ -0,0 +1,35 @@ + + */ +final class QueryRandomPersonHandler extends QueryRandomItemHandler +{ + public function __construct(PeopleRepository $repository) + { + parent::__construct($repository); + } + + protected function resource(Collection $results): JsonResource + { + return new PersonResource( + $results->first() + ); + } + + /** + * @inheritDoc + */ + public function requestClass(): string + { + return QueryRandomPersonCommand::class; + } +} diff --git a/app/Features/QueryRandomUserHandler.php b/app/Features/QueryRandomUserHandler.php new file mode 100644 index 0000000..2e29c95 --- /dev/null +++ b/app/Features/QueryRandomUserHandler.php @@ -0,0 +1,35 @@ + + */ +final class QueryRandomUserHandler extends QueryRandomItemHandler +{ + public function __construct(UserRepository $repository) + { + parent::__construct($repository); + } + + protected function resource(Collection $results): JsonResource + { + return new ProfileResource( + $results->first() + ); + } + + /** + * @inheritDoc + */ + public function requestClass(): string + { + return QueryRandomUserCommand::class; + } +} diff --git a/app/Features/QueryRecommendationsHandler.php b/app/Features/QueryRecommendationsHandler.php new file mode 100644 index 0000000..f085dfd --- /dev/null +++ b/app/Features/QueryRecommendationsHandler.php @@ -0,0 +1,30 @@ + + * @extends RequestHandlerWithScraperCache + */ +abstract class QueryRecommendationsHandler extends RequestHandlerWithScraperCache +{ + protected abstract function recommendationType(): string; + + protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData + { + return $this->scraperService->findList( + $requestFingerPrint, + fn(MalClient $jikan, ?int $page = null) => $jikan->getRecentRecommendations(new RecentRecommendationsRequest( + $this->recommendationType(), $page + )), + $requestParams->get("page", 1) + ); + } +} diff --git a/app/Features/QueryReviewsHandler.php b/app/Features/QueryReviewsHandler.php new file mode 100644 index 0000000..6496dac --- /dev/null +++ b/app/Features/QueryReviewsHandler.php @@ -0,0 +1,42 @@ + + * @extends RequestHandlerWithScraperCache + */ +abstract class QueryReviewsHandler extends RequestHandlerWithScraperCache +{ + use ResolvesMediaReviewParams; + + protected abstract function reviewType(): ReviewTypeEnum; + + protected function getScraperData(string $requestFingerPrint, Collection $requestParams): CachedData + { + $reviewRequestParams = $this->getReviewRequestParams($requestParams); + extract($reviewRequestParams); + + return $this->scraperService->findList( + $requestFingerPrint, + fn(MalClient $jikan, ?int $page = null) => $jikan->getReviews( + new ReviewsRequest( + $this->reviewType()->value, + $page, + $sort, + $spoilers, + $preliminary + ) + ) + ); + } +} diff --git a/app/Features/RequestHandlerWithScraperCache.php b/app/Features/RequestHandlerWithScraperCache.php index e15599e..ceb825a 100644 --- a/app/Features/RequestHandlerWithScraperCache.php +++ b/app/Features/RequestHandlerWithScraperCache.php @@ -53,6 +53,6 @@ abstract class RequestHandlerWithScraperCache implements RequestHandler { $finalResults = $results->collect(); $response = $this->resource($finalResults)->response(); - return $this->scraperService->augmentResponse($response, $requestFingerPrint, $results); + return $response->addJikanCacheFlags($requestFingerPrint, $results); } } diff --git a/app/Http/Controllers/V4DB/PersonController.php b/app/Http/Controllers/V4DB/PersonController.php index 95f54d1..39e9af7 100644 --- a/app/Http/Controllers/V4DB/PersonController.php +++ b/app/Http/Controllers/V4DB/PersonController.php @@ -2,22 +2,13 @@ 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 App\Dto\PersonAnimeLookupCommand; +use App\Dto\PersonFullLookupCommand; +use App\Dto\PersonLookupCommand; +use App\Dto\PersonMangaLookupCommand; +use App\Dto\PersonPicturesLookupCommand; +use App\Dto\PersonVoicesLookupCommand; 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 { @@ -53,59 +44,8 @@ class PersonController extends Controller */ public function full(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::create($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\PersonFullResource( - $results->first() - ))->response(); - - return $this->prepareResponse( - $response, - $results, - $request - ); + $command = PersonFullLookupCommand::from($request, $id); + return $this->mediator->send($command); } /** @@ -140,59 +80,8 @@ class PersonController extends Controller */ 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::create($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 - ); + $command = PersonLookupCommand::from($request, $id); + return $this->mediator->send($command); } /** @@ -221,59 +110,8 @@ class PersonController extends Controller */ 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::create($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 - ); + $command = PersonAnimeLookupCommand::from($request, $id); + return $this->mediator->send($command); } /** @@ -302,59 +140,8 @@ class PersonController extends Controller */ 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::create($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 - ); + $command = PersonVoicesLookupCommand::from($request, $id); + return $this->mediator->send($command); } /** @@ -383,59 +170,8 @@ class PersonController extends Controller */ 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::create($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 - ); + $command = PersonMangaLookupCommand::from($request, $id); + return $this->mediator->send($command); } /** @@ -480,28 +216,7 @@ class PersonController extends Controller */ 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 - ); + $command = PersonPicturesLookupCommand::from($request, $id); + return $this->mediator->send($command); } } diff --git a/app/Http/Controllers/V4DB/ProducerController.php b/app/Http/Controllers/V4DB/ProducerController.php index 352a9b4..7ebe6f9 100644 --- a/app/Http/Controllers/V4DB/ProducerController.php +++ b/app/Http/Controllers/V4DB/ProducerController.php @@ -2,19 +2,12 @@ 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\Dto\ProducerExternalLookupCommand; +use App\Dto\ProducerFullLookupCommand; +use App\Dto\ProducerLookupCommand; use Illuminate\Http\Request; -use Jikan\Model\Producer\Producer; -use MongoDB\BSON\UTCDateTime; -class ProducerController extends ControllerWithQueryBuilderProvider +class ProducerController extends Controller { /** * @OA\Get( @@ -47,60 +40,8 @@ class ProducerController extends ControllerWithQueryBuilderProvider */ public function main(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 \App\Http\Resources\V4\ProducerResource( - $results->first() - ))->response(); - - return $this->prepareResponse( - $response, - $results, - $request - ); + $command = ProducerLookupCommand::from($request, $id); + return $this->mediator->send($command); } /** @@ -134,60 +75,8 @@ class ProducerController extends ControllerWithQueryBuilderProvider */ public function full(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 \App\Http\Resources\V4\ProducerFullResource( - $results->first() - ))->response(); - - return $this->prepareResponse( - $response, - $results, - $request - ); + $command = ProducerFullLookupCommand::from($request, $id); + return $this->mediator->send($command); } /** @@ -218,59 +107,7 @@ class ProducerController extends ControllerWithQueryBuilderProvider */ 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 - ); + $command = ProducerExternalLookupCommand::from($request, $id); + return $this->mediator->send($command); } } diff --git a/app/Http/Controllers/V4DB/RandomController.php b/app/Http/Controllers/V4DB/RandomController.php index 9fa98ea..a2cc9e1 100644 --- a/app/Http/Controllers/V4DB/RandomController.php +++ b/app/Http/Controllers/V4DB/RandomController.php @@ -4,6 +4,11 @@ namespace App\Http\Controllers\V4DB; use App\Anime; use App\Character; +use App\Dto\QueryRandomAnimeCommand; +use App\Dto\QueryRandomCharacterCommand; +use App\Dto\QueryRandomMangaCommand; +use App\Dto\QueryRandomPersonCommand; +use App\Dto\QueryRandomUserCommand; use App\Http\HttpHelper; use App\Http\HttpResponse; use App\Http\Resources\V4\AnimeCollection; @@ -72,23 +77,9 @@ class RandomController extends Controller * ), * ), */ - public function anime(Request $request) + public function anime(QueryRandomAnimeCommand $command) { - $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() - ); + return $this->mediator->send($command); } /** @@ -113,24 +104,9 @@ class RandomController extends Controller * ), * ), */ - public function manga(Request $request) + public function manga(QueryRandomMangaCommand $command) { - $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() - ); + return $this->mediator->send($command); } /** @@ -155,14 +131,9 @@ class RandomController extends Controller * ), * ), */ - public function characters(Request $request) + public function characters(QueryRandomCharacterCommand $command) { - $results = Character::query() - ->raw(fn($collection) => $collection->aggregate([ ['$sample' => ['size' => 1]] ])); - - return new CharacterResource( - $results->first() - ); + return $this->mediator->send($command); } /** @@ -187,14 +158,9 @@ class RandomController extends Controller * ), * ), */ - public function people(Request $request) + public function people(QueryRandomPersonCommand $command) { - $results = Person::query() - ->raw(fn($collection) => $collection->aggregate([ ['$sample' => ['size' => 1]] ])); - - return new PersonResource( - $results->first() - ); + return $this->mediator->send($command); } /** @@ -219,13 +185,8 @@ class RandomController extends Controller * ), * ), */ - public function users(Request $request) + public function users(QueryRandomUserCommand $command) { - $results = Profile::query() - ->raw(fn($collection) => $collection->aggregate([ ['$sample' => ['size' => 1]] ])); - - return new ProfileResource( - $results->first() - ); + return $this->mediator->send($command); } } diff --git a/app/Http/Controllers/V4DB/RecommendationsController.php b/app/Http/Controllers/V4DB/RecommendationsController.php index bc64aa8..6f0119e 100644 --- a/app/Http/Controllers/V4DB/RecommendationsController.php +++ b/app/Http/Controllers/V4DB/RecommendationsController.php @@ -2,15 +2,8 @@ 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; +use App\Dto\QueryAnimeRecommendationsCommand; +use App\Dto\QueryMangaRecommendationsCommand; class RecommendationsController extends Controller { @@ -37,33 +30,9 @@ class RecommendationsController extends Controller * ), * */ - public function anime(Request $request) + public function anime(QueryAnimeRecommendationsCommand $command) { - $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 - ); + return $this->mediator->send($command); } /** @@ -88,31 +57,8 @@ class RecommendationsController extends Controller * ), * */ - public function manga(Request $request) + public function manga(QueryMangaRecommendationsCommand $command) { - $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 - ); + return $this->mediator->send($command); } } diff --git a/app/Http/Controllers/V4DB/ReviewsController.php b/app/Http/Controllers/V4DB/ReviewsController.php index 34a214f..f02f6f3 100644 --- a/app/Http/Controllers/V4DB/ReviewsController.php +++ b/app/Http/Controllers/V4DB/ReviewsController.php @@ -2,16 +2,8 @@ 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 Jikan\Request\Reviews\ReviewsRequest; -use MongoDB\BSON\UTCDateTime; -use Symfony\Component\HttpFoundation\Exception\BadRequestException; +use App\Dto\QueryAnimeReviewsCommand; +use App\Dto\QueryMangaReviewsCommand; class ReviewsController extends Controller { @@ -63,52 +55,9 @@ class ReviewsController extends Controller * ), * ), */ - public function anime(Request $request) + public function anime(QueryAnimeReviewsCommand $command) { - $results = DB::table($this->getRouteTable($request)) - ->where('request_hash', $this->fingerprint) - ->get(); - - - if ( - $results->isEmpty() - || $this->isExpired($request, $results) - ) { - $page = $request->get('page') ?? 1; - $sort = $request->get('sort') ?? Constants::REVIEWS_SORT_MOST_VOTED; - - if (!in_array($sort, [Constants::REVIEWS_SORT_MOST_VOTED, Constants::REVIEWS_SORT_NEWEST, Constants::REVIEWS_SORT_OLDEST])) { - throw new BadRequestException('Invalid sort for reviews. Please refer to the documentation: https://docs.api.jikan.moe/'); - } - - $spoilers = $request->get('spoilers') ?? false; - $preliminary = $request->get('preliminary') ?? false; - - $anime = $this->jikan - ->getReviews( - new ReviewsRequest( - Constants::ANIME, - $page, - $sort, - $spoilers, - $preliminary - ) - ); - - $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 - ); + return $this->mediator->send($command); } /** @@ -159,49 +108,8 @@ class ReviewsController extends Controller * ), * ), */ - public function manga(Request $request) + public function manga(QueryMangaReviewsCommand $command) { - $results = DB::table($this->getRouteTable($request)) - ->where('request_hash', $this->fingerprint) - ->get(); - - if ( - $results->isEmpty() - || $this->isExpired($request, $results) - ) { - $page = $request->get('page') ?? 1; - $sort = $request->get('sort') ?? Constants::REVIEWS_SORT_MOST_VOTED; - - if (!in_array($sort, [Constants::REVIEWS_SORT_MOST_VOTED, Constants::REVIEWS_SORT_NEWEST, Constants::REVIEWS_SORT_OLDEST])) { - throw new BadRequestException('Invalid sort for reviews. Please refer to the documentation: https://docs.api.jikan.moe/'); - } - - $spoilers = $request->get('spoilers') ?? false; - $preliminary = $request->get('preliminary') ?? false; - - $anime = $this->jikan - ->getReviews( - new ReviewsRequest( - Constants::MANGA, - $page, - $sort, - $spoilers, - $preliminary - ) - ); - - $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 - ); + return $this->mediator->send($command); } } diff --git a/app/Http/Controllers/V4DB/ScheduleController.php b/app/Http/Controllers/V4DB/ScheduleController.php index ce8825f..b96d11e 100644 --- a/app/Http/Controllers/V4DB/ScheduleController.php +++ b/app/Http/Controllers/V4DB/ScheduleController.php @@ -2,44 +2,11 @@ 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 App\Dto\QueryAnimeSchedulesCommand; use Illuminate\Http\Request; -use Illuminate\Support\Facades\DB; -use Jikan\Helper\Constants; -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", @@ -107,92 +74,9 @@ class ScheduleController extends Controller * } * ) */ - /* - * all have status as currently airing - * all have premiered but they're not necesarily the current season or year - * all have aired date but they're not necessarily the current date/season - * - */ 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); - $filter = $this->request->get('filter') ?? null; - $kids = $this->request->get('kids') ?? false; - $sfw = $this->request->get('sfw') ?? false; - - if (!is_null($day)) { - $this->day = strtolower($day); - } - - if (!is_null($filter) && is_null($day)) { - $this->day = strtolower($filter); - } - - 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 ($kids) { - $results = $results - ->orWhere('demographics.mal_id', '!=', Constants::GENRE_ANIME_KIDS); - } - - if ($sfw) { - $results = $results - ->orWhere('demographics.mal_id', '!=', Constants::GENRE_ANIME_HENTAI); - } - -// if (is_null($sfw)) { -// $results = $results -// ->orWhere('genres.mal_id', '!=', Constants::GENRE_ANIME_HENTAI) -// ->orWhere('genres.mal_id', '!=', Constants::GENRE_ANIME_BOYS_LOVE) -// ->orWhere('genres.mal_id', '!=', Constants::GENRE_ANIME_GIRLS_LOVE) -// ; -// } - - if (\in_array($this->day, self::VALID_DAYS)) { - $this->day = ucfirst($this->day); - - $results - ->where('broadcast', 'like', "{$this->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 - ); + $command = QueryAnimeSchedulesCommand::from($request, $day); + return $this->mediator->send($command); } } diff --git a/app/JikanApiModel.php b/app/JikanApiModel.php index b540e10..8bf99b2 100644 --- a/app/JikanApiModel.php +++ b/app/JikanApiModel.php @@ -3,6 +3,8 @@ namespace App; use App\Filters\FilterQueryString; +use Illuminate\Support\Collection; +use Jenssegers\Mongodb\Eloquent\Builder; class JikanApiModel extends \Jenssegers\Mongodb\Eloquent\Model { @@ -15,4 +17,12 @@ class JikanApiModel extends \Jenssegers\Mongodb\Eloquent\Model * @var string[] */ protected array $filters = []; + + /** @noinspection PhpUnused */ + public function scopeRandom(Builder $query, int $numberOfRandomItems = 1): Collection + { + return $query->raw(fn(\MongoDB\Collection $collection) => $collection->aggregate([ + ['$sample' => ['size' => $numberOfRandomItems]] + ])); + } } diff --git a/app/Macros/ResponseJikanCacheFlags.php b/app/Macros/ResponseJikanCacheFlags.php new file mode 100644 index 0000000..03c20c2 --- /dev/null +++ b/app/Macros/ResponseJikanCacheFlags.php @@ -0,0 +1,30 @@ +header("X-Request-Fingerprint", $cacheKey) + ->setTtl(self::cacheTtl()) + ->setExpires(Carbon::createFromTimestamp($scraperResults->expiry())) + ->setLastModified(Carbon::createFromTimestamp($scraperResults->lastModified())); + }; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 53141bb..5deb636 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -26,6 +26,7 @@ use App\Http\QueryBuilder\MangaSearchQueryBuilder; use App\Http\QueryBuilder\TopAnimeQueryBuilder; use App\Http\QueryBuilder\TopMangaQueryBuilder; use App\Macros\CollectionOffsetGetFirst; +use App\Macros\ResponseJikanCacheFlags; use App\Macros\To2dArrayWithDottedKeys; use App\Magazine; use App\Mixins\ScoutBuilderMixin; @@ -54,6 +55,7 @@ use App\Support\DefaultMediator; use App\Support\JikanUnitOfWork; use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\Foundation\Application; +use Illuminate\Http\Response; use Illuminate\Support\Env; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Collection; @@ -307,7 +309,20 @@ class AppServiceProvider extends ServiceProvider Features\MangaLookupHandler::class => $unitOfWorkInstance->manga(), Features\MangaFullLookupHandler::class => $unitOfWorkInstance->manga(), Features\MangaRelationsLookupHandler::class => $unitOfWorkInstance->manga(), - Features\MangaExternalLookupHandler::class => $unitOfWorkInstance->manga() + Features\MangaExternalLookupHandler::class => $unitOfWorkInstance->manga(), + Features\PersonLookupHandler::class => $unitOfWorkInstance->people(), + Features\PersonAnimeLookupHandler::class => $unitOfWorkInstance->people(), + Features\PersonFullLookupHandler::class => $unitOfWorkInstance->people(), + Features\PersonMangaLookupHandler::class => $unitOfWorkInstance->people(), + Features\PersonVoicesLookupHandler::class => $unitOfWorkInstance->people(), + Features\PersonPicturesLookupHandler::class => $unitOfWorkInstance->documents("people_pictures"), + Features\ProducerLookupHandler::class => $unitOfWorkInstance->producers(), + Features\ProducerFullLookupHandler::class => $unitOfWorkInstance->producers(), + Features\ProducerExternalLookupHandler::class => $unitOfWorkInstance->producers(), + Features\QueryAnimeRecommendationsHandler::class => $unitOfWorkInstance->documents("recommendations"), + Features\QueryMangaRecommendationsHandler::class => $unitOfWorkInstance->documents("recommendations"), + Features\QueryAnimeReviewsHandler::class => $unitOfWorkInstance->documents("reviews"), + Features\QueryMangaReviewsHandler::class => $unitOfWorkInstance->documents("reviews") ]; foreach ($requestHandlersWithScraperService as $handlerClass => $repositoryInstance) { @@ -319,7 +334,14 @@ class AppServiceProvider extends ServiceProvider ]); } + // automatically resolvable dependencies or no dependencies at all $requestHandlersWithNoDependencies = [ + Features\QueryRandomAnimeHandler::class, + Features\QueryRandomMangaHandler::class, + Features\QueryRandomCharacterHandler::class, + Features\QueryRandomPersonHandler::class, + Features\QueryRandomUserHandler::class, + Features\QueryAnimeSchedulesHandler::class ]; foreach ($requestHandlersWithNoDependencies as $handlerClass) { @@ -372,6 +394,8 @@ class AppServiceProvider extends ServiceProvider ->reject(fn ($class, $macro) => Collection::hasMacro($macro)) ->each(fn ($class, $macro) => Collection::macro($macro, app($class)())); + Response::macro("addJikanCacheFlags", app(ResponseJikanCacheFlags::class)()); + ScoutBuilder::mixin(new ScoutBuilderMixin()); } diff --git a/app/Repositories/DatabaseRepository.php b/app/Repositories/DatabaseRepository.php index 5685da4..903ba90 100644 --- a/app/Repositories/DatabaseRepository.php +++ b/app/Repositories/DatabaseRepository.php @@ -55,4 +55,9 @@ class DatabaseRepository extends RepositoryQuery implements Repository { return $this->queryable(true)->insert($attributes); } + + public function random(int $numberOfRandomItems = 1): Collection + { + return $this->queryable(true)->random($numberOfRandomItems); + } } diff --git a/app/Repositories/DefaultAnimeRepository.php b/app/Repositories/DefaultAnimeRepository.php index 51acecd..5b924c2 100644 --- a/app/Repositories/DefaultAnimeRepository.php +++ b/app/Repositories/DefaultAnimeRepository.php @@ -6,8 +6,11 @@ use App\Anime; use App\Contracts\AnimeRepository; use App\Contracts\Repository; use App\Enums\AnimeRatingEnum; +use App\Enums\AnimeScheduleFilterEnum; use App\Enums\AnimeStatusEnum; -use Illuminate\Database\Eloquent\Builder as EloquentBuilder; +use App\Enums\AnimeTypeEnum; +use Illuminate\Contracts\Database\Query\Builder as EloquentBuilder; +use Jikan\Helper\Constants; use Laravel\Scout\Builder as ScoutBuilder; /** @@ -60,4 +63,42 @@ final class DefaultAnimeRepository extends DatabaseRepository implements AnimeRe ->where("rank", ">", 0) ->orderBy("rank"); } + + public function getCurrentlyAiring( + ?AnimeScheduleFilterEnum $filter = null, + bool $kids = false, + bool $sfw = false): EloquentBuilder + { + /* + * all have status as currently airing + * all have premiered, but they're not necessarily the current season or year + * all have aired date, but they're not necessarily the current date/season + */ + $queryable = $this->queryable(true) + ->orderBy("members") + ->where("type", AnimeTypeEnum::tv()->label) + ->where("status", AnimeStatusEnum::airing()->label); + + if ($kids) { + $queryable = $queryable->where("demographics.mal_id", Constants::GENRE_ANIME_KIDS); + } + else { + $queryable = $queryable->where("demographics.mal_id", "!=", Constants::GENRE_ANIME_KIDS); + } + + if ($sfw) { + $queryable = $queryable->where("demographics.mal_id", "!=", Constants::GENRE_ANIME_HENTAI); + } + + if (!is_null($filter)) { + if ($filter->isWeekDay()) { + $queryable = $queryable->where("broadcast", "like", "{$filter->label}%"); + } + else { + $queryable = $queryable->where("broadcast", $filter->label); + } + } + + return $queryable; + } } diff --git a/app/Repositories/DefaultMangaRepository.php b/app/Repositories/DefaultMangaRepository.php index 2737d43..acd5d84 100644 --- a/app/Repositories/DefaultMangaRepository.php +++ b/app/Repositories/DefaultMangaRepository.php @@ -4,8 +4,9 @@ namespace App\Repositories; use App\Contracts\MangaRepository; use App\Enums\MangaStatusEnum; +use App\Enums\MangaTypeEnum; use App\Manga; -use Illuminate\Database\Eloquent\Builder as EloquentBuilder; +use Illuminate\Contracts\Database\Query\Builder as EloquentBuilder; use Laravel\Scout\Builder as ScoutBuilder; class DefaultMangaRepository extends DatabaseRepository implements MangaRepository @@ -44,4 +45,9 @@ class DefaultMangaRepository extends DatabaseRepository implements MangaReposito ->where("rank", ">", 0) ->orderBy("rank"); } + + public function exceptItemsWithAdultRating(): EloquentBuilder|ScoutBuilder + { + return $this->queryable()->where("type", "!=", MangaTypeEnum::doujin()->label); + } } diff --git a/app/Services/DefaultCachedScraperService.php b/app/Services/DefaultCachedScraperService.php index 58ecb6d..cc9c3f5 100644 --- a/app/Services/DefaultCachedScraperService.php +++ b/app/Services/DefaultCachedScraperService.php @@ -8,9 +8,6 @@ use App\Contracts\Repository; use App\Http\HttpHelper; use App\Support\CachedData; use Illuminate\Contracts\Database\Query\Builder; -use Illuminate\Http\Response; -use Illuminate\Http\JsonResponse; -use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Jikan\MyAnimeList\MalClient; use MongoDB\BSON\UTCDateTime; @@ -110,15 +107,6 @@ final class DefaultCachedScraperService implements CachedScraperService return CachedData::from($this->getByCacheKey($cacheKey)); } - public function augmentResponse(JsonResponse|Response $response, string $cacheKey, CachedData $scraperResults): JsonResponse|Response - { - return $response - ->header("X-Request-Fingerprint", $cacheKey) - ->setTtl($this->cacheTtl()) - ->setExpires(Carbon::createFromTimestamp($scraperResults->expiry())) - ->setLastModified(Carbon::createFromTimestamp($scraperResults->lastModified())); - } - private function raiseNotFoundIfEmpty(CachedData $results) { if ($results->isEmpty()) { diff --git a/app/Support/CachedData.php b/app/Support/CachedData.php index b1c5455..d4ead43 100644 --- a/app/Support/CachedData.php +++ b/app/Support/CachedData.php @@ -73,11 +73,11 @@ final class CachedData $result = $this->scraperResult->first(); - if (is_array($result)) { + if (is_array($result) && array_key_exists("modifiedAt", $result)) { return (int) $result["modifiedAt"]->toDateTime()->format("U"); } - if (is_object($result)) { + if (is_object($result) && property_exists($result, "modifiedAt")) { return $result->modifiedAt->toDateTime()->format("U"); }