From e1b25f46d42db21c0f039021e770626bd66ccd3e Mon Sep 17 00:00:00 2001 From: Kyrch Date: Sun, 28 Jul 2024 23:58:19 -0300 Subject: [PATCH] refactor: backfill actions (#716) --- app/Actions/Models/BackfillWikiAction.php | 150 +++++++ .../Anime/ApiAction/AnilistAnimeApiAction.php | 173 +++++++ .../Anime/ApiAction/JikanAnimeApiAction.php | 102 +++++ .../ApiAction/LivechartAnimeApiAction.php | 86 ++++ .../Anime/ApiAction/MalAnimeApiAction.php | 83 ++++ .../Wiki/Anime/BackfillAnimeImageAction.php | 61 --- .../BackfillAnimeOtherResourcesAction.php | 185 -------- .../Anime/BackfillAnimeResourceAction.php | 96 ---- .../Anime/BackfillAnimeSynonymsAction.php | 176 -------- .../Image/BackfillLargeCoverImageAction.php | 86 ---- .../Image/BackfillSmallCoverImageAction.php | 86 ---- .../Resource/BackfillAnidbResourceAction.php | 98 ---- .../BackfillAnilistResourceAction.php | 178 -------- .../Resource/BackfillAnnResourceAction.php | 95 ---- .../Resource/BackfillKitsuResourceAction.php | 132 ------ .../Resource/BackfillMalResourceAction.php | 176 -------- .../Studio/BackfillAnimeStudiosAction.php | 251 ----------- app/Actions/Models/Wiki/ApiAction.php | 116 +++++ .../Models/Wiki/BackfillAnimeAction.php | 171 +++++++ .../Models/Wiki/BackfillImageAction.php | 141 ------ .../Wiki/BackfillOtherResourcesAction.php | 113 ----- .../Models/Wiki/BackfillResourceAction.php | 102 ----- .../Models/Wiki/BackfillStudioAction.php | 90 ++++ .../Models/Wiki/BackfillStudiosAction.php | 151 ------- .../Studio/ApiAction/MalStudioApiAction.php | 70 +++ .../Wiki/Studio/BackfillStudioImageAction.php | 61 --- .../Image/BackfillLargeCoverImageAction.php | 58 --- .../Models/CanCreateExternalResource.php | 60 +++ app/Concerns/Models/CanCreateImageFromUrl.php | 69 +++ app/Concerns/Models/CanCreateStudio.php | 79 ++++ app/Enums/Models/Wiki/ResourceSite.php | 27 +- .../Wiki/Anime/AttachAnimeResourceAction.php | 1 + .../Models/Wiki/Anime/BackfillAnimeAction.php | 129 +++--- .../Wiki/Studio/BackfillStudioAction.php | 58 ++- app/Filament/Components/Fields/BelongsTo.php | 4 +- .../Anime/AttachAnimeResourceHeaderAction.php | 1 + .../Wiki/Anime/BackfillAnimeHeaderAction.php | 127 +++--- .../Studio/BackfillStudioHeaderAction.php | 58 ++- .../Resources/Admin/FeaturedTheme.php | 2 +- .../Resources/List/External/ExternalEntry.php | 4 +- .../Storage/Base/UploadTableAction.php | 2 +- .../Wiki/Video/UploadVideoTableAction.php | 2 +- app/Http/Api/Schema/Wiki/SeriesSchema.php | 2 + .../Models/Wiki/Anime/BackfillAnimeAction.php | 226 ---------- .../Wiki/Studio/BackfillStudioAction.php | 155 ------- app/Nova/Lenses/Anime/AnimeImageLens.php | 7 - .../Lenses/Anime/Studio/AnimeStudioLens.php | 9 +- .../Studio/Image/StudioCoverLargeLens.php | 7 - .../Resources/List/External/ExternalEntry.php | 2 +- app/Nova/Resources/Wiki/Anime.php | 9 - app/Nova/Resources/Wiki/Studio.php | 9 - lang/en/enums.php | 1 + lang/en/filament.php | 21 +- phpstan.neon | 6 + .../Image/BackfillLargeCoverImageTest.php | 199 --------- .../Image/BackfillSmallCoverImageTest.php | 199 --------- .../Resource/BackfillAnidbResourceTest.php | 191 -------- .../Resource/BackfillAnilistResourceTest.php | 344 -------------- .../Resource/BackfillAnnResourceTest.php | 195 -------- .../Resource/BackfillKitsuResourceTest.php | 210 --------- .../Resource/BackfillMalResourceTest.php | 344 -------------- .../Anime/Studio/BackfillAnimeStudiosTest.php | 422 ------------------ .../Image/BackfillLargeCoverImageTest.php | 155 ------- 63 files changed, 1509 insertions(+), 5114 deletions(-) create mode 100644 app/Actions/Models/BackfillWikiAction.php create mode 100644 app/Actions/Models/Wiki/Anime/ApiAction/AnilistAnimeApiAction.php create mode 100644 app/Actions/Models/Wiki/Anime/ApiAction/JikanAnimeApiAction.php create mode 100644 app/Actions/Models/Wiki/Anime/ApiAction/LivechartAnimeApiAction.php create mode 100644 app/Actions/Models/Wiki/Anime/ApiAction/MalAnimeApiAction.php delete mode 100644 app/Actions/Models/Wiki/Anime/BackfillAnimeImageAction.php delete mode 100644 app/Actions/Models/Wiki/Anime/BackfillAnimeOtherResourcesAction.php delete mode 100644 app/Actions/Models/Wiki/Anime/BackfillAnimeResourceAction.php delete mode 100644 app/Actions/Models/Wiki/Anime/BackfillAnimeSynonymsAction.php delete mode 100644 app/Actions/Models/Wiki/Anime/Image/BackfillLargeCoverImageAction.php delete mode 100644 app/Actions/Models/Wiki/Anime/Image/BackfillSmallCoverImageAction.php delete mode 100644 app/Actions/Models/Wiki/Anime/Resource/BackfillAnidbResourceAction.php delete mode 100644 app/Actions/Models/Wiki/Anime/Resource/BackfillAnilistResourceAction.php delete mode 100644 app/Actions/Models/Wiki/Anime/Resource/BackfillAnnResourceAction.php delete mode 100644 app/Actions/Models/Wiki/Anime/Resource/BackfillKitsuResourceAction.php delete mode 100644 app/Actions/Models/Wiki/Anime/Resource/BackfillMalResourceAction.php delete mode 100644 app/Actions/Models/Wiki/Anime/Studio/BackfillAnimeStudiosAction.php create mode 100644 app/Actions/Models/Wiki/ApiAction.php create mode 100644 app/Actions/Models/Wiki/BackfillAnimeAction.php delete mode 100644 app/Actions/Models/Wiki/BackfillImageAction.php delete mode 100644 app/Actions/Models/Wiki/BackfillOtherResourcesAction.php delete mode 100644 app/Actions/Models/Wiki/BackfillResourceAction.php create mode 100644 app/Actions/Models/Wiki/BackfillStudioAction.php delete mode 100644 app/Actions/Models/Wiki/BackfillStudiosAction.php create mode 100644 app/Actions/Models/Wiki/Studio/ApiAction/MalStudioApiAction.php delete mode 100644 app/Actions/Models/Wiki/Studio/BackfillStudioImageAction.php delete mode 100644 app/Actions/Models/Wiki/Studio/Image/BackfillLargeCoverImageAction.php create mode 100644 app/Concerns/Models/CanCreateExternalResource.php create mode 100644 app/Concerns/Models/CanCreateImageFromUrl.php create mode 100644 app/Concerns/Models/CanCreateStudio.php delete mode 100644 app/Nova/Actions/Models/Wiki/Anime/BackfillAnimeAction.php delete mode 100644 app/Nova/Actions/Models/Wiki/Studio/BackfillStudioAction.php delete mode 100644 tests/Feature/Actions/Models/Wiki/Anime/Image/BackfillLargeCoverImageTest.php delete mode 100644 tests/Feature/Actions/Models/Wiki/Anime/Image/BackfillSmallCoverImageTest.php delete mode 100644 tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillAnidbResourceTest.php delete mode 100644 tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillAnilistResourceTest.php delete mode 100644 tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillAnnResourceTest.php delete mode 100644 tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillKitsuResourceTest.php delete mode 100644 tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillMalResourceTest.php delete mode 100644 tests/Feature/Actions/Models/Wiki/Anime/Studio/BackfillAnimeStudiosTest.php delete mode 100644 tests/Feature/Actions/Models/Wiki/Studio/Image/BackfillLargeCoverImageTest.php diff --git a/app/Actions/Models/BackfillWikiAction.php b/app/Actions/Models/BackfillWikiAction.php new file mode 100644 index 000000000..3545cb313 --- /dev/null +++ b/app/Actions/Models/BackfillWikiAction.php @@ -0,0 +1,150 @@ +toBackfill[self::RESOURCES]; + + foreach ($api->getResources() as $site => $url) { + $site = ResourceSite::from($site); + + if (!in_array($site, $toBackfill)) { + Log::info("Resource {$site->localize()} should not be backfilled for {$this->label()} {$this->getModel()->getName()}"); + continue; + } + + if ($this->getModel()->resources()->getQuery()->where(ExternalResource::ATTRIBUTE_SITE, $site->value)->exists()) { + Log::info("Resource {$site->localize()} already exists for {$this->label()} {$this->getModel()->getName()}"); + continue; + } + + $resource = $this->getOrCreateResource($this->getModel()::class, $site, $url); + + Log::info("Attaching Resource '{$resource->getName()}' to {$this->label()} '{$this->getModel()->getName()}'"); + $this->getModel()->resources()->attach($resource); + + $this->backfilled($site, self::RESOURCES); + } + } + + /** + * Create the images given the response. + * + * @param ApiAction $api + * @return void + */ + protected function forImages(ApiAction $api): void + { + $toBackfill = $this->toBackfill[self::IMAGES]; + + foreach ($api->getImages() as $facet => $url) { + $facet = ImageFacet::from($facet); + + if (!in_array($facet, $toBackfill)) { + Log::info("Skipping {$facet->localize()} for {$this->label()} {$this->getModel()->getName()}"); + continue; + } + + if ($this->getModel()->images()->getQuery()->where(Image::ATTRIBUTE_FACET, $facet->value)->exists()) { + Log::info("Image {$facet->localize()} already exists for {$this->label()} {$this->getModel()->getName()}"); + continue; + } + + $image = $this->createImage($url, $facet, $this->getModel()); + + Log::info("Attaching Image '{$image->getName()}' to {$this->label()} '{$this->getModel()->getName()}'"); + $this->getModel()->images()->attach($image); + + $this->backfilled($facet, self::IMAGES); + } + } + + /** + * Remove element already backfilled. + * + * @param mixed $enum + * @param string $scope + * @return void + */ + protected function backfilled(mixed $enum, string $scope): void + { + $index = array_search($enum, $this->toBackfill[$scope]); + + if ($index !== false) { + unset($this->toBackfill[$scope][$index]); + } + } + + /** + * Get the model for the action. + * + * @return BaseModel + */ + abstract protected function getModel(): BaseModel; + + /** + * Get the human-friendly label for the underlying model. + * + * @return string + */ + protected function label(): string + { + return Str::headline(class_basename($this->getModel())); + } +} diff --git a/app/Actions/Models/Wiki/Anime/ApiAction/AnilistAnimeApiAction.php b/app/Actions/Models/Wiki/Anime/ApiAction/AnilistAnimeApiAction.php new file mode 100644 index 000000000..b2362cca6 --- /dev/null +++ b/app/Actions/Models/Wiki/Anime/ApiAction/AnilistAnimeApiAction.php @@ -0,0 +1,173 @@ + $resources + * @return static + */ + public function handle(BelongsToMany $resources): static + { + $resource = $resources->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST->value); + + if ($resource instanceof ExternalResource) { + $query = ' + query ($id: Int) { + Media (id: $id, type: ANIME) { + title { + romaji + english + native + } + coverImage { + extraLarge + medium + } + externalLinks { + url + site + language + } + } + } + '; + + $variables = [ + 'id' => $resource->external_id, + ]; + + $response = Http::post('https://graphql.anilist.co', [ + 'query' => $query, + 'variables' => $variables, + ]) + ->throw() + ->json(); + + $this->response = $response; + } + + return $this; + } + + /** + * Get the mapped resources. + * + * @return array + */ + public function getResources(): array + { + $resources = []; + + if ($response = $this->response) { + $links = Arr::get($response, 'data.Media.externalLinks'); + + foreach ($links as $link) { + $url = Arr::get($link, 'url'); + $siteAnilist = Arr::get($link, 'site'); + $language = Arr::get($link, 'language'); + + foreach ($this->getResourcesMapping() as $site => $key) { + if ($siteAnilist === $key) { + if (in_array($siteAnilist, ['Official Site', 'Twitter']) && !in_array($language, ['Japanese', null])) continue; + + $resources[$site] = $url; + } + } + } + } + + return $resources; + } + + /** + * Get the mapped synonyms. + * + * @return array + */ + public function getSynonyms(): array + { + $synonyms = []; + + if ($this->response) { + foreach ($this->getSynonymsMapping() as $type => $key) { + $synonyms[$type] = Arr::get($this->response, $key); + } + } + + return $synonyms; + } + + /** + * Get the available sites to backfill. + * + * @return array + */ + protected function getResourcesMapping(): array + { + return [ + ResourceSite::TWITTER->value => 'Twitter', + ResourceSite::OFFICIAL_SITE->value => 'Official Site', + ResourceSite::NETFLIX->value => 'Netflix', + ResourceSite::CRUNCHYROLL->value => 'Crunchyroll', + ResourceSite::HIDIVE->value => 'HIDIVE', + ResourceSite::AMAZON_PRIME_VIDEO->value => 'Amazon Prime Video', + ResourceSite::HULU->value => 'Hulu', + ResourceSite::DISNEY_PLUS->value => 'Disney Plus', + ]; + } + + /** + * Get the mapping for the images. + * + * @return array + */ + protected function getImagesMapping(): array + { + return [ + ImageFacet::COVER_SMALL->value => 'data.Media.coverImage.medium', + ImageFacet::COVER_LARGE->value => 'data.Media.coverImage.extraLarge', + ]; + } + + /** + * Get the mapping for the synonyms. + * + * @return array + */ + protected function getSynonymsMapping(): array + { + return [ + AnimeSynonymType::ENGLISH->value => 'data.Media.title.english', + AnimeSynonymType::NATIVE->value => 'data.Media.title.native', + AnimeSynonymType::OTHER->value => 'data.Media.title.romaji', + ]; + } +} diff --git a/app/Actions/Models/Wiki/Anime/ApiAction/JikanAnimeApiAction.php b/app/Actions/Models/Wiki/Anime/ApiAction/JikanAnimeApiAction.php new file mode 100644 index 000000000..1652280de --- /dev/null +++ b/app/Actions/Models/Wiki/Anime/ApiAction/JikanAnimeApiAction.php @@ -0,0 +1,102 @@ + $resources + * @return static + */ + public function handle(BelongsToMany $resources): static + { + $resource = $resources->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST->value); + + if ($resource instanceof ExternalResource) { + $id = $resource->external_id; + + $response = Http::get("https://api.jikan.moe/v4/anime/$id/external") + ->throw() + ->json(); + + $this->response = $response; + } + + return $this; + } + + /** + * Get the mapped resources. + * + * @return array + */ + public function getResources(): array + { + $resources = []; + + if ($response = $this->response) { + $links = Arr::get($response, 'data'); + + foreach ($links as $link) { + $siteMal = Arr::get($link, 'site'); + $url = Arr::get($link, 'url'); + + foreach ($this->getResourcesMapping() as $site => $key) { + if ($siteMal === $key) { + $resources[$site] = $url; + } + } + } + } + + return $resources; + } + + /** + * Get the available sites to backfill. + * + * @return array + */ + protected function getResourcesMapping(): array + { + return [ + ResourceSite::ANIDB->value => 'AniDB', + ResourceSite::ANN->value => 'ANN', + ResourceSite::OFFICIAL_SITE->value => 'Official Site', + ]; + } + + /** + * Get the mapping for the images. + * + * @return array + */ + protected function getImagesMapping(): array + { + return []; + } +} \ No newline at end of file diff --git a/app/Actions/Models/Wiki/Anime/ApiAction/LivechartAnimeApiAction.php b/app/Actions/Models/Wiki/Anime/ApiAction/LivechartAnimeApiAction.php new file mode 100644 index 000000000..f4374a53f --- /dev/null +++ b/app/Actions/Models/Wiki/Anime/ApiAction/LivechartAnimeApiAction.php @@ -0,0 +1,86 @@ + $resources + * @return static + */ + public function handle(BelongsToMany $resources): static + { + $resource = $resources->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::LIVECHART->value); + + if ($resource instanceof ExternalResource) { + $id = $resource->external_id; + + $response = Http::withHeaders([ + 'accept-encoding' => 'gzip', + 'connection' => 'Keep-Alive', + 'host' => 'www.livechart.me', + 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 OPR/109.0.0.0', + ])->withoutVerifying() + ->get("https://www.livechart.me/api/v2/anime/$id") + ->throw() + ->json(); + + $this->response = $response; + } + + return $this; + } + + /** + * Get the mapping for the resources. + * + * @return array + */ + protected function getResourcesMapping(): array + { + return [ + ResourceSite::ANIDB->value => 'anidb_url', + ResourceSite::ANILIST->value => 'anilist_url', + ResourceSite::ANIME_PLANET->value => 'anime_planet_url', + ResourceSite::ANN->value => 'ann_url', + ResourceSite::KITSU->value => 'kitsu_url', + ResourceSite::MAL->value => 'mal_url', + ResourceSite::OFFICIAL_SITE->value => 'website_url', + ResourceSite::TWITTER->value => 'twitter_url', + ]; + } + + /** + * Get the mapping for the images. + * + * @return array + */ + protected function getImagesMapping(): array + { + return []; + } +} diff --git a/app/Actions/Models/Wiki/Anime/ApiAction/MalAnimeApiAction.php b/app/Actions/Models/Wiki/Anime/ApiAction/MalAnimeApiAction.php new file mode 100644 index 000000000..6b94560f3 --- /dev/null +++ b/app/Actions/Models/Wiki/Anime/ApiAction/MalAnimeApiAction.php @@ -0,0 +1,83 @@ + $resources + * @return static + */ + public function handle(BelongsToMany $resources): static + { + $resource = $resources->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL->value); + + if ($resource instanceof ExternalResource) { + $response = Http::withHeaders(['X-MAL-CLIENT-ID' => Config::get('services.mal.client')]) + ->get("https://api.myanimelist.net/v2/anime/$resource->external_id", [ + 'fields' => 'studios', + ]) + ->throw() + ->json(); + + $this->response = $response; + } + + return $this; + } + + /** + * Get the mapping for the resources. + * + * @return array + */ + protected function getResourcesMapping(): array + { + return []; + } + + /** + * Get the mapping for the images. + * + * @return array + */ + protected function getImagesMapping(): array + { + return []; + } + + /** + * Get the mapped studios. + * + * @return array + */ + public function getStudios(): array + { + return Arr::get($this->response, 'studios', []); + } +} diff --git a/app/Actions/Models/Wiki/Anime/BackfillAnimeImageAction.php b/app/Actions/Models/Wiki/Anime/BackfillAnimeImageAction.php deleted file mode 100644 index cd8b25cca..000000000 --- a/app/Actions/Models/Wiki/Anime/BackfillAnimeImageAction.php +++ /dev/null @@ -1,61 +0,0 @@ - - */ -abstract class BackfillAnimeImageAction extends BackfillImageAction -{ - /** - * Create a new action instance. - * - * @param Anime $anime - */ - public function __construct(Anime $anime) - { - parent::__construct($anime); - } - - /** - * Get the model the action is handling. - * - * @return Anime - */ - protected function getModel(): Anime - { - return $this->model; - } - - /** - * Get the relation to images. - * - * @return BelongsToMany - */ - protected function relation(): BelongsToMany - { - return $this->getModel()->images(); - } - - /** - * Attach Image to Anime. - * - * @param Image $image - * @return void - */ - protected function attachImage(Image $image): void - { - Log::info("Attaching Image '{$image->getName()}' to {$this->label()} '{$this->getModel()->getName()}'"); - $this->relation()->attach($image); - } -} diff --git a/app/Actions/Models/Wiki/Anime/BackfillAnimeOtherResourcesAction.php b/app/Actions/Models/Wiki/Anime/BackfillAnimeOtherResourcesAction.php deleted file mode 100644 index 21adf2ae9..000000000 --- a/app/Actions/Models/Wiki/Anime/BackfillAnimeOtherResourcesAction.php +++ /dev/null @@ -1,185 +0,0 @@ - - */ -class BackfillAnimeOtherResourcesAction extends BackfillOtherResourcesAction -{ - /** - * Create a new action instance. - * - * @param Anime $anime - */ - public function __construct(Anime $anime) - { - parent::__construct($anime); - } - - /** - * Get the model the action is handling. - * - * @return Anime - */ - protected function getModel(): Anime - { - return $this->model; - } - - /** - * Get the relation to resources. - * - * @return BelongsToMany - */ - protected function relation(): BelongsToMany - { - return $this->getModel()->resources(); - } - - /** - * Get the available sites to backfill. - * - * @return array - */ - protected function getAvailableSites(): array - { - /** Key name in Anilist API => @var ResourceSite */ - return [ - 'Twitter' => ResourceSite::TWITTER, - 'Official Site' => ResourceSite::OFFICIAL_SITE, - 'Netflix' => ResourceSite::NETFLIX, - 'Crunchyroll' => ResourceSite::CRUNCHYROLL, - 'HIDIVE' => ResourceSite::HIDIVE, - 'Amazon Prime Video' => ResourceSite::AMAZON_PRIME_VIDEO, - 'Hulu' => ResourceSite::HULU, - 'Disney Plus' => ResourceSite::DISNEY_PLUS, - ]; - } - - /** - * Get or Create Resource from response. - * - * @param mixed $externalLink - * @return ExternalResource - */ - protected function getOrCreateResource(mixed $externalLink): ExternalResource - { - $availableSites = $this->getAvailableSites(); - /** @var ResourceSite $resourceSite */ - $resourceSite = $availableSites[$externalLink['site']]; - $url = $externalLink['url']; - $urlPattern = $resourceSite->getUrlCaptureGroups($this->getModel()); - - if (preg_match($urlPattern, $url, $matches)) { - $url = $resourceSite->formatResourceLink(Anime::class, intval($matches[2]), $matches[2], $matches[1]); - } - - $resource = ExternalResource::query() - ->where(ExternalResource::ATTRIBUTE_SITE, $resourceSite->value) - ->where(ExternalResource::ATTRIBUTE_LINK, $url) - ->orWhere(ExternalResource::ATTRIBUTE_LINK, $url . '/') - ->first(); - - if ($resource === null) { - $nameLocalized = $resourceSite->localize(); - Log::info("Creating {$nameLocalized} -> '{$url}'"); - - $resource = ExternalResource::query()->create([ - ExternalResource::ATTRIBUTE_LINK => $url, - ExternalResource::ATTRIBUTE_SITE => $resourceSite->value, - ExternalResource::ATTRIBUTE_EXTERNAL_ID => $resourceSite->parseIdFromLink($url), - ]); - } - - return $resource; - } - - /** - * Attach Resource to Anime. - * - * @param ExternalResource $resource - * @return void - */ - protected function attachResource(ExternalResource $resource): void - { - if (AnimeResource::query() - ->where($this->getModel()->getKeyName(), $this->getModel()->getKey()) - ->where($resource->getKeyName(), $resource->getKey()) - ->doesntExist() - ) { - Log::info("Attaching Resource '{$resource->getName()}' to {$this->label()} '{$this->getModel()->getName()}'"); - $this->relation()->attach($resource); - } - } - - /** - * Get the AniList Resource. - * - * @return ExternalResource|null - */ - protected function getAnilistResource(): ?ExternalResource - { - $anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST->value); - if ($anilistResource instanceof ExternalResource) { - return $anilistResource; - } - - return null; - } - - /** - * Get the external links by AniList API. - * - * @return array|null - */ - protected function getExternalLinksByAnilistResource(): ?array - { - $anilistResource = $this->getAnilistResource(); - - if ($anilistResource !== null) { - $query = ' - query ($id: Int) { - Media (id: $id, type: ANIME) { - externalLinks { - url - site - language - } - } - } - '; - - $variables = [ - 'id' => $anilistResource->external_id, - ]; - - $response = Http::post('https://graphql.anilist.co', [ - 'query' => $query, - 'variables' => $variables, - ]) - ->throw() - ->json(); - - $externalLinks = Arr::get($response, 'data.Media.externalLinks'); - - return $externalLinks; - } - - return null; - } -} diff --git a/app/Actions/Models/Wiki/Anime/BackfillAnimeResourceAction.php b/app/Actions/Models/Wiki/Anime/BackfillAnimeResourceAction.php deleted file mode 100644 index 38a0c6f35..000000000 --- a/app/Actions/Models/Wiki/Anime/BackfillAnimeResourceAction.php +++ /dev/null @@ -1,96 +0,0 @@ - - */ -abstract class BackfillAnimeResourceAction extends BackfillResourceAction -{ - /** - * Create a new action instance. - * - * @param Anime $anime - */ - public function __construct(Anime $anime) - { - parent::__construct($anime); - } - - /** - * Get the model the action is handling. - * - * @return Anime - */ - protected function getModel(): Anime - { - return $this->model; - } - - /** - * Get the relation to resources. - * - * @return BelongsToMany - */ - protected function relation(): BelongsToMany - { - return $this->getModel()->resources(); - } - - /** - * Get or Create Resource from response. - * - * @param int $id - * @param string|null $slug - * @return ExternalResource - */ - protected function getOrCreateResource(int $id, string $slug = null): ExternalResource - { - $resource = ExternalResource::query() - ->where(ExternalResource::ATTRIBUTE_SITE, $this->getSite()->value) - ->where(ExternalResource::ATTRIBUTE_EXTERNAL_ID, $id) - ->where(ExternalResource::ATTRIBUTE_LINK, $this->getSite()->formatResourceLink(Anime::class, $id, $slug)) - ->first(); - - if ($resource === null) { - Log::info("Creating {$this->getSite()->localize()} Resource '$id'"); - - $resource = ExternalResource::query()->create([ - ExternalResource::ATTRIBUTE_EXTERNAL_ID => $id, - ExternalResource::ATTRIBUTE_LINK => $this->getSite()->formatResourceLink(Anime::class, $id, $slug), - ExternalResource::ATTRIBUTE_SITE => $this->getSite()->value, - ]); - } - - return $resource; - } - - /** - * Attach Resource to Anime. - * - * @param ExternalResource $resource - * @return void - */ - protected function attachResource(ExternalResource $resource): void - { - if (AnimeResource::query() - ->where($this->getModel()->getKeyName(), $this->getModel()->getKey()) - ->where($resource->getKeyName(), $resource->getKey()) - ->doesntExist() - ) { - Log::info("Attaching Resource '{$resource->getName()}' to {$this->label()} '{$this->getModel()->getName()}'"); - $this->relation()->attach($resource); - } - } -} diff --git a/app/Actions/Models/Wiki/Anime/BackfillAnimeSynonymsAction.php b/app/Actions/Models/Wiki/Anime/BackfillAnimeSynonymsAction.php deleted file mode 100644 index b60bfd2c3..000000000 --- a/app/Actions/Models/Wiki/Anime/BackfillAnimeSynonymsAction.php +++ /dev/null @@ -1,176 +0,0 @@ - - */ -class BackfillAnimeSynonymsAction extends BackfillAction -{ - /** - * Create a new action instance. - * - * @param Anime $anime - */ - public function __construct(Anime $anime) - { - parent::__construct($anime); - } - - /** - * Handle action. - * - * @return ActionResult - * - * @throws Exception - */ - public function handle(): ActionResult - { - try { - DB::beginTransaction(); - - $titles = $this->getTitles(); - - if ($titles === null) { - DB::rollBack(); - return new ActionResult(ActionStatus::FAILED); - } - - foreach ($titles as $type => $text) { - if ( - $text === null - || empty($text) - || ($type === 'romaji' && $text === $this->getModel()->getName()) - ) continue; - - Log::info("Creating {$text}"); - - AnimeSynonym::query()->create([ - AnimeSynonym::ATTRIBUTE_TEXT => $text, - AnimeSynonym::ATTRIBUTE_TYPE => static::getAnilistSynonymsMap($type)->value, - AnimeSynonym::ATTRIBUTE_ANIME => $this->getModel()->getKey(), - ]); - } - - DB::commit(); - } catch (Exception $e) { - Log::error($e->getMessage()); - - DB::rollBack(); - - throw $e; - } - - return new ActionResult(ActionStatus::PASSED); - } - - /** - * Get the enum related to the array map. - * - * @param string $title - * @return AnimeSynonymType - */ - protected static function getAnilistSynonymsMap($title): AnimeSynonymType - { - return match ($title) { - 'english' => AnimeSynonymType::ENGLISH, - 'native' => AnimeSynonymType::NATIVE, - default => AnimeSynonymType::OTHER, - }; - } - - /** - * Get the Anilist Resource. - * - * @return ExternalResource|null - */ - protected function getAnilistResource(): ?ExternalResource - { - $anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST->value); - if ($anilistResource instanceof ExternalResource) { - return $anilistResource; - } - - return null; - } - - /** - * Get the titles by AniList API. - * - * @return array|null - */ - protected function getTitles(): ?array - { - $anilistResource = $this->getAnilistResource(); - - if ($anilistResource !== null) { - $query = ' - query ($id: Int) { - Media (id: $id, type: ANIME) { - title { - romaji - english - native - } - } - } - '; - - $variables = [ - 'id' => $anilistResource->external_id, - ]; - - $response = Http::post('https://graphql.anilist.co', [ - 'query' => $query, - 'variables' => $variables, - ]) - ->throw() - ->json(); - - $titles = Arr::get($response, 'data.Media.title'); - - return $titles; - } - - return null; - } - - /** - * Get the model the action is handling. - * - * @return Anime - */ - protected function getModel(): Anime - { - return $this->model; - } - - /** - * Get the relation to resources. - * - * @return HasMany - */ - protected function relation(): HasMany - { - return $this->getModel()->animesynonyms(); - } -} diff --git a/app/Actions/Models/Wiki/Anime/Image/BackfillLargeCoverImageAction.php b/app/Actions/Models/Wiki/Anime/Image/BackfillLargeCoverImageAction.php deleted file mode 100644 index ba7d6161b..000000000 --- a/app/Actions/Models/Wiki/Anime/Image/BackfillLargeCoverImageAction.php +++ /dev/null @@ -1,86 +0,0 @@ -getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST->value); - if ($anilistResource instanceof ExternalResource) { - return $this->getAnilistImage($anilistResource); - } - - return null; - } - - /** - * Query Anilist API for large cover image. - * - * @param ExternalResource $anilistResource - * @return Image|null - * - * @throws RequestException - */ - protected function getAnilistImage(ExternalResource $anilistResource): ?Image - { - $query = ' - query ($id: Int) { - Media (id: $id, type: ANIME) { - coverImage { - extraLarge - } - } - } - '; - - $variables = [ - 'id' => $anilistResource->external_id, - ]; - - $response = Http::post('https://graphql.anilist.co', [ - 'query' => $query, - 'variables' => $variables, - ]) - ->throw() - ->json(); - - $anilistCoverLarge = Arr::get($response, 'data.Media.coverImage.extraLarge'); - if ($anilistCoverLarge !== null) { - return $this->createImage($anilistCoverLarge); - } - - return null; - } -} diff --git a/app/Actions/Models/Wiki/Anime/Image/BackfillSmallCoverImageAction.php b/app/Actions/Models/Wiki/Anime/Image/BackfillSmallCoverImageAction.php deleted file mode 100644 index f797b7a3f..000000000 --- a/app/Actions/Models/Wiki/Anime/Image/BackfillSmallCoverImageAction.php +++ /dev/null @@ -1,86 +0,0 @@ -getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST->value); - if ($anilistResource instanceof ExternalResource) { - return $this->getAnilistImage($anilistResource); - } - - return null; - } - - /** - * Query Anilist API for large cover image. - * - * @param ExternalResource $anilistResource - * @return Image|null - * - * @throws RequestException - */ - protected function getAnilistImage(ExternalResource $anilistResource): ?Image - { - $query = ' - query ($id: Int) { - Media (id: $id, type: ANIME) { - coverImage { - medium - } - } - } - '; - - $variables = [ - 'id' => $anilistResource->external_id, - ]; - - $response = Http::post('https://graphql.anilist.co', [ - 'query' => $query, - 'variables' => $variables, - ]) - ->throw() - ->json(); - - $anilistCoverSmall = Arr::get($response, 'data.Media.coverImage.medium'); - if ($anilistCoverSmall !== null) { - return $this->createImage($anilistCoverSmall); - } - - return null; - } -} diff --git a/app/Actions/Models/Wiki/Anime/Resource/BackfillAnidbResourceAction.php b/app/Actions/Models/Wiki/Anime/Resource/BackfillAnidbResourceAction.php deleted file mode 100644 index ff0e20651..000000000 --- a/app/Actions/Models/Wiki/Anime/Resource/BackfillAnidbResourceAction.php +++ /dev/null @@ -1,98 +0,0 @@ -getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL->value); - if ($malResource instanceof ExternalResource) { - $anidbResource = $this->getAnidbMapping($malResource, 'myanimelist'); - if ($anidbResource !== null) { - return $anidbResource; - } - - // failed mapping, sleep before re-attempting - Sleep::for(rand(1, 3))->second(); - } - - $anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST->value); - if ($anilistResource instanceof ExternalResource) { - $anidbResource = $this->getAnidbMapping($anilistResource, 'anilist'); - if ($anidbResource !== null) { - return $anidbResource; - } - - // failed mapping, sleep before re-attempting - Sleep::for(rand(1, 3))->second(); - } - - $kitsuResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU->value); - if ($kitsuResource instanceof ExternalResource) { - return $this->getAnidbMapping($kitsuResource, 'kitsu'); - } - - return null; - } - - /** - * Query Yuna API for AniDB mapping. - * - * @param ExternalResource $resource - * @param string $source - * @return ExternalResource|null - * - * @throws RequestException - */ - protected function getAnidbMapping(ExternalResource $resource, string $source): ?ExternalResource - { - $response = Http::get('https://relations.yuna.moe/api/ids', [ - 'source' => $source, - 'id' => $resource->external_id, - ]) - ->throw() - ->json(); - - $anidbId = Arr::get($response, 'anidb'); - - // Only proceed if we have a match - if ($anidbId !== null) { - return $this->getOrCreateResource($anidbId); - } - - return null; - } -} diff --git a/app/Actions/Models/Wiki/Anime/Resource/BackfillAnilistResourceAction.php b/app/Actions/Models/Wiki/Anime/Resource/BackfillAnilistResourceAction.php deleted file mode 100644 index 61967f2eb..000000000 --- a/app/Actions/Models/Wiki/Anime/Resource/BackfillAnilistResourceAction.php +++ /dev/null @@ -1,178 +0,0 @@ -getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL->value); - if ($malResource instanceof ExternalResource) { - $anilistResource = $this->getMalAnilistMapping($malResource); - if ($anilistResource !== null) { - return $anilistResource; - } - } - - $kitsuResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU->value); - if ($kitsuResource instanceof ExternalResource) { - $anilistResource = $this->getKitsuAnilistMapping($kitsuResource); - if ($anilistResource !== null) { - return $anilistResource; - } - } - - $anidbResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB->value); - if ($anidbResource instanceof ExternalResource) { - return $this->getAnidbAnilistMapping($anidbResource); - } - - return null; - } - - /** - * Query Anilist API for MAL mapping. - * - * @param ExternalResource $malResource - * @return ExternalResource|null - * - * @throws RequestException - */ - protected function getMalAnilistMapping(ExternalResource $malResource): ?ExternalResource - { - $query = ' - query ($id: Int) { - Media (idMal: $id, type: ANIME) { - id - } - } - '; - - $variables = [ - 'id' => $malResource->external_id, - ]; - - $response = Http::post('https://graphql.anilist.co', [ - 'query' => $query, - 'variables' => $variables, - ]) - ->throw() - ->json(); - - $anilistId = Arr::get($response, 'data.Media.id'); - if ($anilistId === null) { - Log::info("Skipping null Anilist mapping for MAL id '$malResource->external_id'"); - - return null; - } - - return $this->getOrCreateResource($anilistId); - } - - /** - * Query Kitsu API for Anilist mapping. - * - * @param ExternalResource $kitsuResource - * @return ExternalResource|null - * - * @throws RequestException - */ - protected function getKitsuAnilistMapping(ExternalResource $kitsuResource): ?ExternalResource - { - $query = ' - query ($id: ID!) { - findAnimeById (id: $id) { - mappings(first:20) { - nodes { - externalId - externalSite - } - } - } - } - '; - - $variables = [ - 'id' => $kitsuResource->external_id, - ]; - - $response = Http::post('https://kitsu.io/api/graphql', [ - 'query' => $query, - 'variables' => $variables, - ]) - ->throw() - ->json(); - - $kitsuMappings = Arr::get($response, 'data.findAnimeById.mappings.nodes', []); - foreach ($kitsuMappings as $kitsuMapping) { - $externalId = Arr::get($kitsuMapping, 'externalId'); - $externalSite = Arr::get($kitsuMapping, 'externalSite'); - if ($externalSite !== 'ANILIST_ANIME' || empty($externalId)) { - Log::info("Skipping mapping of externalId '$externalId' and externalSite '$externalSite'"); - continue; - } - - return $this->getOrCreateResource(intval($externalId)); - } - - return null; - } - - /** - * Query Yuna API for Anilist mapping. - * - * @param ExternalResource $anidbResource - * @return ExternalResource|null - * - * @throws RequestException - */ - protected function getAnidbAnilistMapping(ExternalResource $anidbResource): ?ExternalResource - { - $response = Http::get('https://relations.yuna.moe/api/ids', [ - 'source' => 'anidb', - 'id' => $anidbResource->external_id, - ]) - ->throw() - ->json(); - - $anilistId = Arr::get($response, 'anilist'); - - if ($anilistId !== null) { - return $this->getOrCreateResource($anilistId); - } - - return null; - } -} diff --git a/app/Actions/Models/Wiki/Anime/Resource/BackfillAnnResourceAction.php b/app/Actions/Models/Wiki/Anime/Resource/BackfillAnnResourceAction.php deleted file mode 100644 index 11ba1a1af..000000000 --- a/app/Actions/Models/Wiki/Anime/Resource/BackfillAnnResourceAction.php +++ /dev/null @@ -1,95 +0,0 @@ -getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU->value); - if ($kitsuResource instanceof ExternalResource) { - return $this->getKitsuAnnMapping($kitsuResource); - } - - return null; - } - - /** - * Query Kitsu API for ANN mapping. - * - * @param ExternalResource $kitsuResource - * @return ExternalResource|null - * - * @throws RequestException - */ - protected function getKitsuAnnMapping(ExternalResource $kitsuResource): ?ExternalResource - { - $query = ' - query ($id: ID!) { - findAnimeById (id: $id) { - mappings(first:20) { - nodes { - externalId - externalSite - } - } - } - } - '; - - $variables = [ - 'id' => $kitsuResource->external_id, - ]; - - $response = Http::post('https://kitsu.io/api/graphql', [ - 'query' => $query, - 'variables' => $variables, - ]) - ->throw() - ->json(); - - $kitsuMappings = Arr::get($response, 'data.findAnimeById.mappings.nodes', []); - foreach ($kitsuMappings as $kitsuMapping) { - $externalId = Arr::get($kitsuMapping, 'externalId'); - $externalSite = Arr::get($kitsuMapping, 'externalSite'); - if ($externalSite !== 'ANIMENEWSNETWORK' || empty($externalId)) { - Log::info("Skipping mapping of externalId '$externalId' and externalSite '$externalSite'"); - continue; - } - - return $this->getOrCreateResource(intval($externalId)); - } - - return null; - } -} diff --git a/app/Actions/Models/Wiki/Anime/Resource/BackfillKitsuResourceAction.php b/app/Actions/Models/Wiki/Anime/Resource/BackfillKitsuResourceAction.php deleted file mode 100644 index cf864e3f1..000000000 --- a/app/Actions/Models/Wiki/Anime/Resource/BackfillKitsuResourceAction.php +++ /dev/null @@ -1,132 +0,0 @@ -getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL->value); - if ($malResource instanceof ExternalResource) { - $kitsuResource = $this->getKitsuMapping($malResource, 'MYANIMELIST_ANIME'); - if ($kitsuResource !== null) { - return $kitsuResource; - } - - // failed mapping, sleep before re-attempting - Sleep::for(rand(1, 3))->second(); - } - - $anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST->value); - if ($anilistResource instanceof ExternalResource) { - $kitsuResource = $this->getKitsuMapping($anilistResource, 'ANILIST_ANIME'); - if ($kitsuResource !== null) { - return $kitsuResource; - } - - // failed mapping, sleep before re-attempting - Sleep::for(rand(1, 3))->second(); - } - - $anidbResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB->value); - if ($anidbResource instanceof ExternalResource) { - $kitsuResource = $this->getKitsuMapping($anidbResource, 'ANIDB'); - if ($kitsuResource !== null) { - return $kitsuResource; - } - - // failed mapping, sleep before re-attempting - Sleep::for(rand(1, 3))->second(); - } - - $annResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANN->value); - if ($annResource instanceof ExternalResource) { - return $this->getKitsuMapping($annResource, 'ANIMENEWSNETWORK'); - } - - return null; - } - - /** - * Query Kitsu API for MAL mapping. - * - * @param ExternalResource $resource - * @param string $externalSite - * @return ExternalResource|null - * - * @throws RequestException - */ - protected function getKitsuMapping(ExternalResource $resource, string $externalSite): ?ExternalResource - { - $query = ' - query ($externalId: ID!, $externalSite: MappingExternalSiteEnum!) { - lookupMapping(externalId: $externalId, externalSite: $externalSite) { - ... on Anime { - id - slug - } - } - } - '; - - $variables = [ - 'externalId' => $resource->external_id, - 'externalSite' => $externalSite, - ]; - - $response = Http::post('https://kitsu.io/api/graphql', [ - 'query' => $query, - 'variables' => $variables, - ]) - ->throw() - ->json(); - - $kitsuMapping = Arr::get($response, 'data.lookupMapping'); - if ($kitsuMapping !== null) { - $id = Arr::get($kitsuMapping, 'id'); - $slug = Arr::get($kitsuMapping, 'slug'); - if (empty($id) || empty($slug)) { - Log::info("Skipping mapping of id '$id' and slug '$slug'"); - - return null; - } - - return $this->getOrCreateResource(intval($id), $slug); - } - - return null; - } -} diff --git a/app/Actions/Models/Wiki/Anime/Resource/BackfillMalResourceAction.php b/app/Actions/Models/Wiki/Anime/Resource/BackfillMalResourceAction.php deleted file mode 100644 index 4b607d46c..000000000 --- a/app/Actions/Models/Wiki/Anime/Resource/BackfillMalResourceAction.php +++ /dev/null @@ -1,176 +0,0 @@ -getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU->value); - if ($kitsuResource instanceof ExternalResource) { - $malResource = $this->getKitsuMalMapping($kitsuResource); - if ($malResource !== null) { - return $malResource; - } - } - - $anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST->value); - if ($anilistResource instanceof ExternalResource) { - $malResource = $this->getAnilistMalMapping($anilistResource); - if ($malResource !== null) { - return $malResource; - } - } - - $anidbResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB->value); - if ($anidbResource instanceof ExternalResource) { - return $this->getAnidbMalMapping($anidbResource); - } - - return null; - } - - /** - * Query Kitsu API for MAL mapping. - * - * @param ExternalResource $kitsuResource - * @return ExternalResource|null - * - * @throws RequestException - */ - protected function getKitsuMalMapping(ExternalResource $kitsuResource): ?ExternalResource - { - $query = ' - query ($id: ID!) { - findAnimeById (id: $id) { - mappings(first:20) { - nodes { - externalId - externalSite - } - } - } - } - '; - - $variables = [ - 'id' => $kitsuResource->external_id, - ]; - - $response = Http::post('https://kitsu.io/api/graphql', [ - 'query' => $query, - 'variables' => $variables, - ]) - ->throw() - ->json(); - - $kitsuMappings = Arr::get($response, 'data.findAnimeById.mappings.nodes', []); - foreach ($kitsuMappings as $kitsuMapping) { - $externalId = Arr::get($kitsuMapping, 'externalId'); - $externalSite = Arr::get($kitsuMapping, 'externalSite'); - if ($externalSite !== 'MYANIMELIST_ANIME' || empty($externalId)) { - Log::info("Skipping mapping of externalId '$externalId' and externalSite '$externalSite'"); - continue; - } - - return $this->getOrCreateResource(intval($externalId)); - } - - return null; - } - - /** - * Query Anilist API for MAL mapping. - * - * @param ExternalResource $anilistResource - * @return ExternalResource|null - * - * @throws RequestException - */ - protected function getAnilistMalMapping(ExternalResource $anilistResource): ?ExternalResource - { - $query = ' - query ($id: Int) { - Media (id: $id, type: ANIME) { - idMal - } - } - '; - - $variables = [ - 'id' => $anilistResource->external_id, - ]; - - $response = Http::post('https://graphql.anilist.co', [ - 'query' => $query, - 'variables' => $variables, - ]) - ->throw() - ->json(); - - $malId = Arr::get($response, 'data.Media.idMal'); - if ($malId === null) { - Log::info("Skipping null MAL mapping for Anilist id '$anilistResource->external_id'"); - - return null; - } - - return $this->getOrCreateResource($malId); - } - - /** - * Query Yuna API for Mal mapping. - * - * @param ExternalResource $anidbResource - * @return ExternalResource|null - * - * @throws RequestException - */ - protected function getAnidbMalMapping(ExternalResource $anidbResource): ?ExternalResource - { - $response = Http::get('https://relations.yuna.moe/api/ids', [ - 'source' => 'anidb', - 'id' => $anidbResource->external_id, - ]) - ->throw() - ->json(); - - $malId = Arr::get($response, 'myanimelist'); - - if ($malId !== null) { - return $this->getOrCreateResource($malId); - } - - return null; - } -} diff --git a/app/Actions/Models/Wiki/Anime/Studio/BackfillAnimeStudiosAction.php b/app/Actions/Models/Wiki/Anime/Studio/BackfillAnimeStudiosAction.php deleted file mode 100644 index 64321caea..000000000 --- a/app/Actions/Models/Wiki/Anime/Studio/BackfillAnimeStudiosAction.php +++ /dev/null @@ -1,251 +0,0 @@ - - */ -class BackfillAnimeStudiosAction extends BackfillStudiosAction -{ - /** - * Create a new action instance. - * - * @param Anime $anime - */ - public function __construct(Anime $anime) - { - parent::__construct($anime); - } - - /** - * Get the model the action is handling. - * - * @return Anime - */ - protected function getModel(): Anime - { - return $this->model; - } - - /** - * Get the relation to studios. - * - * @return BelongsToMany - */ - protected function relation(): BelongsToMany - { - return $this->getModel()->studios(); - } - - /** - * Query third-party API for Anime Studios. - * - * @return Studio[] - * - * @throws RequestException - */ - protected function getStudios(): array - { - $malResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL->value); - if ($malResource instanceof ExternalResource) { - $studios = $this->getMalAnimeStudios($malResource); - if (! empty($studios)) { - return $studios; - } - } - - $anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST->value); - if ($anilistResource instanceof ExternalResource) { - $studios = $this->getAnilistAnimeStudios($anilistResource); - if (! empty($studios)) { - return $studios; - } - } - - $kitsuResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU->value); - if ($kitsuResource instanceof ExternalResource) { - return $this->getKitsuAnimeStudios($kitsuResource); - } - - return []; - } - - /** - * Query MAL API for Anime Studios. - * - * @param ExternalResource $malResource - * @return Studio[] - * - * @throws RequestException - */ - protected function getMalAnimeStudios(ExternalResource $malResource): array - { - $studios = []; - - $response = Http::withHeaders(['X-MAL-CLIENT-ID' => Config::get('services.mal.client')]) - ->get("https://api.myanimelist.net/v2/anime/$malResource->external_id", [ - 'fields' => 'studios', - ]) - ->throw() - ->json(); - - $malStudios = Arr::get($response, 'studios', []); - - foreach ($malStudios as $malStudio) { - $name = Arr::get($malStudio, 'name'); - $id = Arr::get($malStudio, 'id'); - if (empty($name) || empty($id)) { - Log::info("Skipping empty studio of name '$name' and id '$id' for MAL Resource '{$malResource->getName()}'"); - continue; - } - - $studio = $this->getOrCreateStudio($name); - - $studios[] = $studio; - - $this->ensureStudioHasResource($studio, ResourceSite::MAL, $id); - } - - return $studios; - } - - /** - * Query Anilist API for Anime Studios. - * - * @param ExternalResource $anilistResource - * @return Studio[] - * - * @throws RequestException - */ - protected function getAnilistAnimeStudios(ExternalResource $anilistResource): array - { - $studios = []; - - $query = ' - query ($id: Int) { - Media (id: $id, type: ANIME) { - studios (isMain: true) { - nodes { - id - name - } - } - } - } - '; - - $variables = [ - 'id' => $anilistResource->external_id, - ]; - - $response = Http::post('https://graphql.anilist.co', [ - 'query' => $query, - 'variables' => $variables, - ]) - ->throw() - ->json(); - - $anilistStudios = Arr::get($response, 'data.Media.studios.nodes', []); - - foreach ($anilistStudios as $anilistStudio) { - $name = Arr::get($anilistStudio, 'name'); - $id = Arr::get($anilistStudio, 'id'); - if (empty($name) || empty($id)) { - Log::info("Skipping empty studio of name '$name' and id '$id' for Anilist Resource '{$anilistResource->getName()}'"); - continue; - } - - $studio = $this->getOrCreateStudio($name); - - $studios[] = $studio; - - $this->ensureStudioHasResource($studio, ResourceSite::ANILIST, $id); - } - - return $studios; - } - - /** - * Query Kitsu API for Anime Studios. - * - * @param ExternalResource $kitsuResource - * @return Studio[] - * - * @throws RequestException - */ - protected function getKitsuAnimeStudios(ExternalResource $kitsuResource): array - { - $studios = []; - - $query = ' - query ($id: ID!) { - findAnimeById(id: $id) { - productions(first:20) { - nodes { - role - company { - name - } - } - } - } - } - '; - - $variables = [ - 'id' => $kitsuResource->external_id, - ]; - - $response = Http::post('https://kitsu.io/api/graphql', [ - 'query' => $query, - 'variables' => $variables, - ]) - ->throw() - ->json(); - - $kitsuStudios = Arr::get($response, 'data.findAnimeById.productions.nodes', []); - - foreach ($kitsuStudios as $kitsuStudio) { - $role = Arr::get($kitsuStudio, 'role'); - $name = Arr::get($kitsuStudio, 'company.name'); - if ($role !== 'STUDIO' || empty($name)) { - Log::info("Skipping production company of name '$name' and role '$role' for Anilist Resource '{$kitsuResource->getName()}'"); - continue; - } - - $studio = $this->getOrCreateStudio($name); - - $studios[] = $studio; - } - - return $studios; - } - - /** - * Attach Studios. - * - * @param Studio[] $studios - * @return void - */ - protected function attachStudios(array $studios): void - { - Log::info("Attaching studios to {$this->label()} '{$this->getModel()->getName()}'"); - $this->relation()->attach(Arr::pluck($studios, Studio::ATTRIBUTE_ID)); - } -} diff --git a/app/Actions/Models/Wiki/ApiAction.php b/app/Actions/Models/Wiki/ApiAction.php new file mode 100644 index 000000000..e224c49da --- /dev/null +++ b/app/Actions/Models/Wiki/ApiAction.php @@ -0,0 +1,116 @@ + $resources + * @return static + */ + abstract public function handle(BelongsToMany $resources): static; + + /** + * Get the mapped resources. + * + * @return array + */ + public function getResources(): array + { + $resources = []; + + if ($this->response) { + foreach ($this->getResourcesMapping() as $site => $key) { + $resources[$site] = Arr::get($this->response, $key); + } + } + + return $resources; + } + + /** + * Get the mapped images. + * + * @return array + */ + public function getImages(): array + { + $images = []; + + if ($this->response) { + foreach ($this->getImagesMapping() as $facet => $key) { + $images[$facet] = Arr::get($this->response, $key); + } + } + + return $images; + } + + /** + * Get the mapped studios. + * + * @return array + */ + public function getStudios(): array + { + return []; + } + + /** + * Get the mapped synonyms. + * + * @return array + */ + public function getSynonyms(): array + { + return []; + } + + /** + * Get the mapping for the resources. + * + * @return array + */ + abstract protected function getResourcesMapping(): array; + + /** + * Get the mapping for the images. + * + * @return array + */ + abstract protected function getImagesMapping(): array; +} diff --git a/app/Actions/Models/Wiki/BackfillAnimeAction.php b/app/Actions/Models/Wiki/BackfillAnimeAction.php new file mode 100644 index 000000000..6295817f1 --- /dev/null +++ b/app/Actions/Models/Wiki/BackfillAnimeAction.php @@ -0,0 +1,171 @@ +getApis() as $api) { + DB::beginTransaction(); + + if ( + count($this->toBackfill[self::RESOURCES]) === 0 + && count($this->toBackfill[self::IMAGES]) === 0 + && !$this->toBackfill[self::STUDIOS] + && !$this->toBackfill[self::SYNONYMS] + ) { + // Don't make other requests if everything is backfilled + Log::info("Backfill action finished for Anime {$this->getModel()->getName()}"); + DB::rollBack(); + break; + } + + $response = $api->handle($this->getModel()->resources()); + + $this->forResources($response); + $this->forImages($response); + $this->forStudios($response); + $this->forSynonyms($response); + + Log::info("Commit try for api {$api->getSite()->localize()}"); + DB::commit(); + } + } catch (Exception $e) { + Log::error($e->getMessage()); + + DB::rollBack(); + } + + return new ActionResult(ActionStatus::PASSED); + } + + /** + * Get the api actions available for the backfill action. + * + * @return array + */ + protected function getApis(): array + { + return [ + new LivechartAnimeApiAction(), + new AnilistAnimeApiAction(), + new MalAnimeApiAction(), + //new JikanAnimeApiAction(), + ]; + } + + /** + * Create the studios given the response. + * + * @param ApiAction $response + * @return void + */ + protected function forStudios(ApiAction $response): void + { + $studios = $response->getStudios(); + + if (!$this->toBackfill[self::STUDIOS]) return; + + foreach ($studios as $studio) { + $id = Arr::get($studio, 'id'); + $name = Arr::get($studio, 'name'); + + if (empty($name) || empty($id)) { + Log::info("Skipping empty studio of name '$name' and id '$id''"); + continue; + } + + $studio = $this->getOrCreateStudio($name); + + Log::info("Attaching Studio of name '$name' to Anime {$this->getModel()->getName()}"); + $this->getModel()->studios()->attach($studio); + + $this->toBackfill[self::STUDIOS] = false; + + $this->ensureStudioHasResource($studio, $response->getSite(), $id); + } + } + + /** + * Create the synonyms given the response. + * + * @param ApiAction $api + * @return void + */ + protected function forSynonyms(ApiAction $api): void + { + if (!$this->toBackfill[self::SYNONYMS]) return; + + foreach ($api->getSynonyms() as $type => $text) { + if ( + $text === null + || empty($text) + || ($type === AnimeSynonymType::OTHER->value && $text === $this->getModel()->getName()) + ) continue; + + Log::info("Creating {$text}"); + + AnimeSynonym::query()->create([ + AnimeSynonym::ATTRIBUTE_TEXT => $text, + AnimeSynonym::ATTRIBUTE_TYPE => $type, + AnimeSynonym::ATTRIBUTE_ANIME => $this->getModel()->getKey(), + ]); + + $this->toBackfill[self::SYNONYMS] = false; + } + } + + /** + * Get the model for the action. + * + * @return Anime + */ + protected function getModel(): Anime + { + return $this->anime; + } +} diff --git a/app/Actions/Models/Wiki/BackfillImageAction.php b/app/Actions/Models/Wiki/BackfillImageAction.php deleted file mode 100644 index 723e0e682..000000000 --- a/app/Actions/Models/Wiki/BackfillImageAction.php +++ /dev/null @@ -1,141 +0,0 @@ - - */ -abstract class BackfillImageAction extends BackfillAction -{ - /** - * Handle action. - * - * @return ActionResult - * - * @throws Exception - */ - public function handle(): ActionResult - { - try { - DB::beginTransaction(); - - if ($this->relation()->getQuery()->where(Image::ATTRIBUTE_FACET, $this->getFacet()->value)->exists()) { - DB::rollback(); - Log::info("{$this->label()} '{$this->getModel()->getName()}' already has Image of Facet '{$this->getFacet()->value}'."); - - return new ActionResult(ActionStatus::SKIPPED); - } - - $image = $this->getImage(); - - if ($image !== null) { - $this->attachImage($image); - } - - if ($this->relation()->getQuery()->where(Image::ATTRIBUTE_FACET, $this->getFacet()->value)->doesntExist()) { - DB::rollback(); - return new ActionResult( - ActionStatus::FAILED, - "{$this->label()} '{$this->getModel()->getName()}' has no {$this->getFacet()->localize()} Image after backfilling. Please review." - ); - } - - DB::commit(); - } catch (Exception $e) { - Log::error($e->getMessage()); - - DB::rollBack(); - - throw $e; - } - - return new ActionResult(ActionStatus::PASSED); - } - - /** - * Create Image from response. - * - * @param string $url - * @return Image - * - * @throws RequestException - */ - protected function createImage(string $url): Image - { - $imageResponse = Http::get($url)->throw(); - - $image = $imageResponse->body(); - - $file = File::createWithContent(basename($url), $image); - - $fs = Storage::disk(Config::get('image.disk')); - - $fsFile = $fs->putFile($this->path(), $file); - - /** @var Image $image */ - $image = Image::query()->create([ - Image::ATTRIBUTE_FACET => $this->getFacet()->value, - Image::ATTRIBUTE_PATH => $fsFile, - ]); - - return $image; - } - - /** - * Path to storage image in filesystem. - * - * @return string - */ - protected function path(): string - { - return Str::of(Str::kebab(class_basename($this->getModel()))) - ->append(DIRECTORY_SEPARATOR) - ->append(Str::kebab($this->getFacet()->localize())) - ->__toString(); - } - - /** - * Attach Image to model. - * - * @param Image $image - * @return void - */ - abstract protected function attachImage(Image $image): void; - - /** - * Get the facet to backfill. - * - * @return ImageFacet - */ - abstract protected function getFacet(): ImageFacet; - - /** - * Query third-party APIs to find Image. - * - * @return Image|null - * - * @throws RequestException - */ - abstract protected function getImage(): ?Image; -} diff --git a/app/Actions/Models/Wiki/BackfillOtherResourcesAction.php b/app/Actions/Models/Wiki/BackfillOtherResourcesAction.php deleted file mode 100644 index 80e8715f6..000000000 --- a/app/Actions/Models/Wiki/BackfillOtherResourcesAction.php +++ /dev/null @@ -1,113 +0,0 @@ - - */ -abstract class BackfillOtherResourcesAction extends BackfillAction -{ - /** - * Handle action. - * - * @return ActionResult - * - * @throws Exception - */ - public function handle(): ActionResult - { - try { - DB::beginTransaction(); - - $externalLinks = $this->getExternalLinksByAnilistResource(); - - if ($externalLinks === null) { - DB::rollback(); - return new ActionResult(ActionStatus::FAILED); - } - - $availableSites = $this->getAvailableSites(); - - foreach ($externalLinks as $externalLink) { - $site = $externalLink['site']; - $language = $externalLink['language']; - - if (!in_array($site, array_keys($availableSites))) continue; - if (in_array($site, ['Official Site', 'Twitter']) && !in_array($language, ['Japanese', null])) continue; - - if ($this->relation()->getQuery()->where(ExternalResource::ATTRIBUTE_SITE, $availableSites[$site]->value)->exists()) { - $nameLocalized = $availableSites[$site]->localize(); - Log::info("{$nameLocalized} already exists in the model {$this->getModel()->getName()}"); - continue; - } - - $resource = $this->getOrCreateResource($externalLink); - - if ($resource !== null) { - $this->attachResource($resource); - } - } - - DB::commit(); - } catch (Exception $e) { - Log::error($e->getMessage()); - - DB::rollBack(); - - throw $e; - } - - return new ActionResult(ActionStatus::PASSED); - } - - /** - * Get or Create Resource from response. - * - * @param mixed $externalLink - * @return ExternalResource - */ - abstract protected function getOrCreateResource(mixed $externalLink): ExternalResource; - - /** - * Attach External Resource to model. - * - * @param ExternalResource $resource - * @return void - */ - abstract protected function attachResource(ExternalResource $resource): void; - - /** - * Get the sites to backfill. - * - * @return array - */ - abstract protected function getAvailableSites(): array; - - /** - * Get the Anilist Resource. - * - * @return ExternalResource|null - */ - abstract protected function getAnilistResource(): ?ExternalResource; - - /** - * Get the external links that the Anilist API provides. - * - * @return array|null - */ - abstract protected function getExternalLinksByAnilistResource(): ?array; -} diff --git a/app/Actions/Models/Wiki/BackfillResourceAction.php b/app/Actions/Models/Wiki/BackfillResourceAction.php deleted file mode 100644 index bcca1a4fa..000000000 --- a/app/Actions/Models/Wiki/BackfillResourceAction.php +++ /dev/null @@ -1,102 +0,0 @@ - - */ -abstract class BackfillResourceAction extends BackfillAction -{ - /** - * Handle action. - * - * @return ActionResult - * - * @throws Exception - */ - public function handle(): ActionResult - { - try { - DB::beginTransaction(); - - if ($this->relation()->getQuery()->where(ExternalResource::ATTRIBUTE_SITE, $this->getSite()->value)->exists()) { - DB::rollback(); - Log::info("{$this->label()} '{$this->getModel()->getName()}' already has Resource of Site '{$this->getSite()->value}'."); - return new ActionResult(ActionStatus::SKIPPED); - } - - $resource = $this->getResource(); - - if ($resource !== null) { - $this->attachResource($resource); - } - - if ($this->relation()->getQuery()->where(ExternalResource::ATTRIBUTE_SITE, $this->getSite()->value)->doesntExist()) { - DB::rollback(); - return new ActionResult( - ActionStatus::FAILED, - "{$this->label()} '{$this->getModel()->getName()}' has no {$this->getSite()->localize()} Resource after backfilling. Please review." - ); - } - - DB::commit(); - } catch (Exception $e) { - Log::error($e->getMessage()); - - DB::rollBack(); - - throw $e; - } - - return new ActionResult(ActionStatus::PASSED); - } - - /** - * Get or Create Resource from response. - * - * @param int $id - * @param string|null $slug - * @return ExternalResource - */ - abstract protected function getOrCreateResource(int $id, string $slug = null): ExternalResource; - - /** - * Attach External Resource to model. - * - * @param ExternalResource $resource - * @return void - */ - abstract protected function attachResource(ExternalResource $resource): void; - - /** - * Get the site to backfill. - * - * @return ResourceSite - */ - abstract protected function getSite(): ResourceSite; - - /** - * Query third-party APIs to find Resource mapping. - * - * @return ExternalResource|null - * - * @throws RequestException - */ - abstract protected function getResource(): ?ExternalResource; -} diff --git a/app/Actions/Models/Wiki/BackfillStudioAction.php b/app/Actions/Models/Wiki/BackfillStudioAction.php new file mode 100644 index 000000000..3f10e6c21 --- /dev/null +++ b/app/Actions/Models/Wiki/BackfillStudioAction.php @@ -0,0 +1,90 @@ +getApis() as $api) { + try { + DB::beginTransaction(); + + if ( + count($this->toBackfill[self::IMAGES]) === 0 + ) { + // Don't make other requests if everything is backfilled + Log::info("Backfill action finished for Studio {$this->getModel()->getName()}"); + DB::rollBack(); + return new ActionResult(ActionStatus::SKIPPED); + } + + $response = $api->handle($this->getModel()->resources()); + + $this->forImages($response); + + DB::commit(); + } catch (Exception $e) { + Log::error($e->getMessage()); + + DB::rollBack(); + + throw $e; + } + } + + return new ActionResult(ActionStatus::PASSED); + } + + /** + * Get the api actions available for the backfill action. + * + * @return array + */ + protected function getApis(): array + { + return [ + new MalStudioApiAction(), + ]; + } + + /** + * Get the model for the action. + * + * @return Studio + */ + protected function getModel(): Studio + { + return $this->studio; + } +} diff --git a/app/Actions/Models/Wiki/BackfillStudiosAction.php b/app/Actions/Models/Wiki/BackfillStudiosAction.php deleted file mode 100644 index e41cd684a..000000000 --- a/app/Actions/Models/Wiki/BackfillStudiosAction.php +++ /dev/null @@ -1,151 +0,0 @@ - - */ -abstract class BackfillStudiosAction extends BackfillAction -{ - /** - * Handle action. - * - * @return ActionResult - * - * @throws Exception - */ - public function handle(): ActionResult - { - try { - DB::beginTransaction(); - - if ($this->relation()->getQuery()->exists()) { - DB::rollback(); - Log::info("{$this->label()} '{$this->getModel()->getName()}' already has Studios."); - - return new ActionResult(ActionStatus::SKIPPED); - } - - $studios = $this->getStudios(); - - if (! empty($studios)) { - $this->attachStudios($studios); - } - - if ($this->relation()->getQuery()->doesntExist()) { - DB::rollback(); - return new ActionResult( - ActionStatus::FAILED, - "{$this->label()} '{$this->getModel()->getName()}' has no Studios after backfilling. Please review." - ); - } - - DB::commit(); - } catch (Exception $e) { - Log::error($e->getMessage()); - - DB::rollBack(); - - throw $e; - } - - return new ActionResult(ActionStatus::PASSED); - } - - /** - * Get or create Studio from name (case-insensitive). - * - * @param string $name - * @return Studio - */ - protected function getOrCreateStudio(string $name): Studio - { - $column = Studio::ATTRIBUTE_NAME; - $studio = Studio::query() - ->whereRaw("lower($column) = ?", Str::lower($name)) - ->first(); - - if (! $studio instanceof Studio) { - Log::info("Creating studio '$name'"); - - $studio = Studio::query()->create([ - Studio::ATTRIBUTE_NAME => $name, - Studio::ATTRIBUTE_SLUG => Str::slug($name, '_'), - ]); - } - - return $studio; - } - - /** - * Ensure Studio has Resource. - * - * @param Studio $studio - * @param ResourceSite $site - * @param int $id - * @return void - */ - protected function ensureStudioHasResource(Studio $studio, ResourceSite $site, int $id): void - { - $studioResource = ExternalResource::query() - ->where(ExternalResource::ATTRIBUTE_SITE, $site->value) - ->where(ExternalResource::ATTRIBUTE_EXTERNAL_ID, $id) - ->where(ExternalResource::ATTRIBUTE_LINK, $site->formatResourceLink(Studio::class, $id)) - ->first(); - - if (! $studioResource instanceof ExternalResource) { - Log::info("Creating studio resource with site '$site->value' and id '$id'"); - - $studioResource = ExternalResource::query()->create([ - ExternalResource::ATTRIBUTE_EXTERNAL_ID => $id, - ExternalResource::ATTRIBUTE_LINK =>$site->formatResourceLink(Studio::class, $id), - ExternalResource::ATTRIBUTE_SITE => $site->value, - ]); - } - - if (StudioResource::query() - ->where($studio->getKeyName(), $studio->getKey()) - ->where($studioResource->getKeyName(), $studioResource->getKey()) - ->doesntExist() - ) { - Log::info("Attaching resource '$studioResource->link' to studio '{$studio->getName()}'"); - $studioResource->studios()->attach($studio); - } - } - - /** - * Attach Studios. - * - * @param Studio[] $studios - * @return void - */ - abstract protected function attachStudios(array $studios): void; - - /** - * Query third-party API for Studios. - * - * @return Studio[] - * - * @throws RequestException - */ - abstract protected function getStudios(): array; -} diff --git a/app/Actions/Models/Wiki/Studio/ApiAction/MalStudioApiAction.php b/app/Actions/Models/Wiki/Studio/ApiAction/MalStudioApiAction.php new file mode 100644 index 000000000..d10f79477 --- /dev/null +++ b/app/Actions/Models/Wiki/Studio/ApiAction/MalStudioApiAction.php @@ -0,0 +1,70 @@ + $resources + * @return static + */ + public function handle(BelongsToMany $resources): static + { + $resource = $resources->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL->value); + + if ($resource instanceof ExternalResource) { + $this->response = [ + 'images' => [ + 'large' => "https://cdn.myanimelist.net/images/company/$resource->external_id.png", + ], + ]; + } + + return $this; + } + + /** + * Get the mapping for the resources. + * + * @return array + */ + protected function getResourcesMapping(): array + { + return []; + } + + /** + * Get the mapping for the images. + * + * @return array + */ + protected function getImagesMapping(): array + { + return [ + ImageFacet::COVER_LARGE->value => 'images.large', + ]; + } +} diff --git a/app/Actions/Models/Wiki/Studio/BackfillStudioImageAction.php b/app/Actions/Models/Wiki/Studio/BackfillStudioImageAction.php deleted file mode 100644 index 54667071e..000000000 --- a/app/Actions/Models/Wiki/Studio/BackfillStudioImageAction.php +++ /dev/null @@ -1,61 +0,0 @@ - - */ -abstract class BackfillStudioImageAction extends BackfillImageAction -{ - /** - * Create a new action instance. - * - * @param Studio $studio - */ - public function __construct(Studio $studio) - { - parent::__construct($studio); - } - - /** - * Get the model the action is handling. - * - * @return Studio - */ - protected function getModel(): Studio - { - return $this->model; - } - - /** - * Get the relation to images. - * - * @return BelongsToMany - */ - protected function relation(): BelongsToMany - { - return $this->getModel()->images(); - } - - /** - * Attach Image to Studio. - * - * @param Image $image - * @return void - */ - protected function attachImage(Image $image): void - { - Log::info("Attaching Image '{$image->getName()}' to {$this->label()} '{$this->getModel()->getName()}'"); - $this->relation()->attach($image); - } -} diff --git a/app/Actions/Models/Wiki/Studio/Image/BackfillLargeCoverImageAction.php b/app/Actions/Models/Wiki/Studio/Image/BackfillLargeCoverImageAction.php deleted file mode 100644 index c4ebbd48a..000000000 --- a/app/Actions/Models/Wiki/Studio/Image/BackfillLargeCoverImageAction.php +++ /dev/null @@ -1,58 +0,0 @@ -getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL->value); - if ($malResource instanceof ExternalResource) { - return $this->getMalImage($malResource); - } - - return null; - } - - /** - * Query MAL API for large cover image. - * - * @param ExternalResource $malResource - * @return Image|null - * - * @throws RequestException - */ - protected function getMalImage(ExternalResource $malResource): ?Image - { - return $this->createImage("https://cdn.myanimelist.net/images/company/$malResource->external_id.png"); - } -} diff --git a/app/Concerns/Models/CanCreateExternalResource.php b/app/Concerns/Models/CanCreateExternalResource.php new file mode 100644 index 000000000..1181878a5 --- /dev/null +++ b/app/Concerns/Models/CanCreateExternalResource.php @@ -0,0 +1,60 @@ + $model + * @param ResourceSite $site + * @param string $url + * @return ExternalResource + */ + public function getOrCreateResource(string $model, ResourceSite $site, string $url): ExternalResource + { + $urlPattern = $site->getUrlCaptureGroups(new $model); + Log::info($urlPattern); + $id = $site::parseIdFromLink($url); + Log::info($id); + + if (preg_match($urlPattern, $url, $matches)) { + $url = $site->formatResourceLink($model, intval($matches[2]), $matches[2], $matches[1]); + } + Log::info($url); + + if ($id !== null) { + $url = $site->formatResourceLink($model, intval($id), $id); + } + Log::info($url); + + $resource = ExternalResource::query() + ->where(ExternalResource::ATTRIBUTE_SITE, $site->value) + ->where(ExternalResource::ATTRIBUTE_LINK, $url) + ->orWhere(ExternalResource::ATTRIBUTE_LINK, $url . '/') + ->first(); + + if ($resource === null) { + Log::info("Creating {$site->localize()} -> '{$url}'"); + + $resource = ExternalResource::query()->create([ + ExternalResource::ATTRIBUTE_LINK => $url, + ExternalResource::ATTRIBUTE_SITE => $site->value, + ExternalResource::ATTRIBUTE_EXTERNAL_ID => $site::parseIdFromLink($url), + ]); + } + + return $resource; + } +} diff --git a/app/Concerns/Models/CanCreateImageFromUrl.php b/app/Concerns/Models/CanCreateImageFromUrl.php new file mode 100644 index 000000000..716e3ae7e --- /dev/null +++ b/app/Concerns/Models/CanCreateImageFromUrl.php @@ -0,0 +1,69 @@ +throw(); + + $image = $imageResponse->body(); + + $file = File::createWithContent(basename($url), $image); + + $fs = Storage::disk(Config::get('image.disk')); + + $fsFile = $fs->putFile($this->path($facet, $model), $file); + + Log::info("Creating Image {$fsFile}"); + /** @var Image $image */ + $image = Image::query()->create([ + Image::ATTRIBUTE_FACET => $facet->value, + Image::ATTRIBUTE_PATH => $fsFile, + ]); + + return $image; + } + + /** + * Path to storage image in filesystem. + * + * @param ImageFacet $facet + * @param BaseModel $model + * @return string + */ + protected function path(ImageFacet $facet, BaseModel $model): string + { + return Str::of(Str::kebab(class_basename($model))) + ->append(DIRECTORY_SEPARATOR) + ->append(Str::kebab($facet->localize())) + ->__toString(); + } +} diff --git a/app/Concerns/Models/CanCreateStudio.php b/app/Concerns/Models/CanCreateStudio.php new file mode 100644 index 000000000..ab48dd599 --- /dev/null +++ b/app/Concerns/Models/CanCreateStudio.php @@ -0,0 +1,79 @@ +whereRaw("lower($column) = ?", Str::lower($name)) + ->first(); + + if (! $studio instanceof Studio) { + Log::info("Creating studio '$name'"); + + $studio = Studio::query()->create([ + Studio::ATTRIBUTE_NAME => $name, + Studio::ATTRIBUTE_SLUG => Str::slug($name, '_'), + ]); + } + + return $studio; + } + + /** + * Ensure Studio has Resource. + * + * @param Studio $studio + * @param ResourceSite $site + * @param int $id + * @return void + */ + public function ensureStudioHasResource(Studio $studio, ResourceSite $site, int $id): void + { + $studioResource = ExternalResource::query() + ->where(ExternalResource::ATTRIBUTE_SITE, $site->value) + ->where(ExternalResource::ATTRIBUTE_EXTERNAL_ID, $id) + ->where(ExternalResource::ATTRIBUTE_LINK, $site->formatResourceLink(Studio::class, $id)) + ->first(); + + if (! $studioResource instanceof ExternalResource) { + Log::info("Creating studio resource with site '{$site->localize()}' and id '$id'"); + + $studioResource = ExternalResource::query()->create([ + ExternalResource::ATTRIBUTE_EXTERNAL_ID => $id, + ExternalResource::ATTRIBUTE_LINK => $site->formatResourceLink(Studio::class, $id), + ExternalResource::ATTRIBUTE_SITE => $site->value, + ]); + } + + if (StudioResource::query() + ->where($studio->getKeyName(), $studio->getKey()) + ->where($studioResource->getKeyName(), $studioResource->getKey()) + ->doesntExist() + ) { + Log::info("Attaching resource '$studioResource->link' to studio '{$studio->getName()}'"); + $studioResource->studios()->attach($studio); + } + } +} \ No newline at end of file diff --git a/app/Enums/Models/Wiki/ResourceSite.php b/app/Enums/Models/Wiki/ResourceSite.php index 4eb6505c2..181a9ac8d 100644 --- a/app/Enums/Models/Wiki/ResourceSite.php +++ b/app/Enums/Models/Wiki/ResourceSite.php @@ -34,6 +34,7 @@ enum ResourceSite: int case ANN = 5; case KITSU = 6; case MAL = 7; + case LIVECHART = 20; // Compendia case WIKI = 8; @@ -68,6 +69,7 @@ public static function getDomain(?int $value): ?string ResourceSite::ANIME_PLANET->value => 'www.anime-planet.com', ResourceSite::ANN->value => 'www.animenewsnetwork.com', ResourceSite::KITSU->value => 'kitsu.io', + ResourceSite::LIVECHART->value => 'www.livechart.me', ResourceSite::MAL->value => 'myanimelist.net', ResourceSite::WIKI->value => 'wikipedia.org', ResourceSite::SPOTIFY->value => 'open.spotify.com', @@ -117,8 +119,9 @@ public static function parseIdFromLink(string $link): ?string ResourceSite::ANN, ResourceSite::MAL, ResourceSite::NETFLIX, + ResourceSite::LIVECHART, ResourceSite::APPLE_MUSIC => Str::match('/\d+/', $link), - ResourceSite::ANIME_PLANET => ResourceSite::parseAnimePlanetIdFromLink($link), + // ResourceSite::ANIME_PLANET => ResourceSite::parseAnimePlanetIdFromLink($link), ResourceSite::KITSU => ResourceSite::parseKitsuIdFromLink($link), default => null, }; @@ -133,7 +136,7 @@ public static function parseIdFromLink(string $link): ?string protected static function parseAnimePlanetIdFromLink(string $link): ?string { // We only want to attempt to parse the ID for an anime resource - if (Str::match('/^https:\/\/www\.anime-planet\.com\/anime\/[a-zA-Z0-9-]+$/', $link) !== $link) { + if (Str::match('/^https?:\/\/www\.anime-planet\.com\/anime\/[a-zA-Z0-9-]+$/', $link) !== $link) { return null; } @@ -162,6 +165,10 @@ protected static function parseAnimePlanetIdFromLink(string $link): ?string protected static function parseKitsuIdFromLink(string $link): ?string { try { + if ($id = Str::match('/^https?:\/\/kitsu\.io\/anime\/(\d+)/', $link)) { + return $id; + } + $query = ' query ($slug: String!) { findAnimeBySlug(slug: $slug) { @@ -215,6 +222,7 @@ public static function getForModel(?string $modelClass): array ResourceSite::AMAZON_PRIME_VIDEO, ResourceSite::OFFICIAL_SITE, ResourceSite::WIKI, + ResourceSite::LIVECHART, ], Artist::class => [ ResourceSite::TWITTER, @@ -272,6 +280,7 @@ public function formatResourceLink(string $modelClass, int $id, ?string $slug = ResourceSite::ANIME_PLANET => "https://www.anime-planet.com/anime/$slug", ResourceSite::ANN => "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=$id", ResourceSite::KITSU => "https://kitsu.io/anime/$slug", + ResourceSite::LIVECHART => "https://www.livechart.me/anime/$id", ResourceSite::MAL => "https://myanimelist.net/anime/$id", ResourceSite::YOUTUBE => "https://www.youtube.com/@$slug", ResourceSite::CRUNCHYROLL => "https://www.crunchyroll.com/$type/$slug", @@ -342,6 +351,7 @@ public function getUrlCaptureGroups(?Model $model): string ResourceSite::ANIME_PLANET => '/^https:\/\/www\.anime-planet\.com\/(anime)\/([a-zA-Z0-9-]+)$/', ResourceSite::ANN => '/^https:\/\/www\.animenewsnetwork\.com\/encyclopedia\/(anime)\.php\?id=(\d+)$/', ResourceSite::KITSU => '/^https:\/\/kitsu\.io\/(anime)\/([a-zA-Z0-9-]+)$/', + ResourceSite::LIVECHART => '/^https:\/\/www\.livechart\.me\/(anime)\/(\d+)$/', ResourceSite::MAL => '/^https:\/\/myanimelist\.net\/(anime)\/(\d+)$/', ResourceSite::YOUTUBE => '/^https:\/\/www\.(youtube)\.com\/\@([\w-]+)$/', ResourceSite::ANIDB => '/^https:\/\/anidb\.net\/(anime)\/(\d+)$/', @@ -413,6 +423,7 @@ public function getPattern(?string $modelClass): ?string ResourceSite::ANIME_PLANET => '/^https:\/\/www\.anime-planet\.com\/anime\/[a-zA-Z0-9-]+$/', ResourceSite::ANN => '/^https:\/\/www\.animenewsnetwork\.com\/encyclopedia\/anime\.php\?id=\d+$/', ResourceSite::KITSU => '/^https:\/\/kitsu\.io\/anime\/[a-zA-Z0-9-]+$/', + ResourceSite::LIVECHART => '/^https:\/\/www\.livechart\.me\/anime\/\d+$/', ResourceSite::MAL => '/^https:\/\/myanimelist\.net\/anime\/\d+$/', ResourceSite::YOUTUBE => '/^https:\/\/www\.youtube\.com\/\@[\w-]+$/', ResourceSite::CRUNCHYROLL => '/^https:\/\/www\.crunchyroll\.com\/(?:series|watch|null)\/\w+$/', @@ -421,7 +432,8 @@ public function getPattern(?string $modelClass): ?string ResourceSite::DISNEY_PLUS => '/^https:\/\/www\.disneyplus\.com\/(?:series|movies|null)\/[\w-]+\/\w+$/', ResourceSite::HULU => '/^https:\/\/www\.hulu\.com\/(?:series|watch|movie|null)\/[\w-]+$/', ResourceSite::AMAZON_PRIME_VIDEO => '/^https:\/\/www\.primevideo\.com\/detail\/\w+$/', - ResourceSite::OFFICIAL_SITE, ResourceSite::WIKI => null, + ResourceSite::OFFICIAL_SITE, + ResourceSite::WIKI => null, default => '/$.^/', }; } @@ -437,7 +449,8 @@ public function getPattern(?string $modelClass): ?string ResourceSite::SPOTIFY => '/^https:\/\/open\.spotify\.com\/artist\/\w+$/', ResourceSite::YOUTUBE_MUSIC => '/^https:\/\/music\.youtube\.com\/channel\/[\w-]+/', ResourceSite::YOUTUBE => '/^https:\/\/www\.youtube\.com\/\@[\w-]+$/', - ResourceSite::OFFICIAL_SITE, ResourceSite::WIKI => null, + ResourceSite::OFFICIAL_SITE, + ResourceSite::WIKI => null, default => '/$.^/', }; } @@ -450,7 +463,8 @@ public function getPattern(?string $modelClass): ?string ResourceSite::YOUTUBE => '/^https:\/\/www\.youtube\.com\/watch\?v=[\w-]+$/', ResourceSite::APPLE_MUSIC => '/^https:\/\/music\.apple\.com\/jp\/album\/\d+$/', ResourceSite::AMAZON_MUSIC => '/^https:\/\/music\.amazon\.co\.jp\/tracks\/\w+$/', - ResourceSite::OFFICIAL_SITE, ResourceSite::WIKI => null, + ResourceSite::OFFICIAL_SITE, + ResourceSite::WIKI => null, default => '/$.^/', }; } @@ -463,7 +477,8 @@ public function getPattern(?string $modelClass): ?string ResourceSite::ANIME_PLANET => '/^https:\/\/www\.anime-planet\.com\/anime\/studios\/[a-zA-Z0-9-]+$/', ResourceSite::ANN => '/^https:\/\/www\.animenewsnetwork\.com\/encyclopedia\/company\.php\?id=\d+$/', ResourceSite::MAL => '/^https:\/\/myanimelist\.net\/anime\/producer\/\d+$/', - ResourceSite::OFFICIAL_SITE, ResourceSite::WIKI => null, + ResourceSite::OFFICIAL_SITE, + ResourceSite::WIKI => null, default => '/$.^/', }; } diff --git a/app/Filament/Actions/Models/Wiki/Anime/AttachAnimeResourceAction.php b/app/Filament/Actions/Models/Wiki/Anime/AttachAnimeResourceAction.php index 06d750377..bd979ff53 100644 --- a/app/Filament/Actions/Models/Wiki/Anime/AttachAnimeResourceAction.php +++ b/app/Filament/Actions/Models/Wiki/Anime/AttachAnimeResourceAction.php @@ -31,6 +31,7 @@ protected function setUp(): void ResourceSite::ANIME_PLANET, ResourceSite::ANN, ResourceSite::KITSU, + ResourceSite::LIVECHART, ResourceSite::MAL, ResourceSite::OFFICIAL_SITE, ResourceSite::TWITTER, diff --git a/app/Filament/Actions/Models/Wiki/Anime/BackfillAnimeAction.php b/app/Filament/Actions/Models/Wiki/Anime/BackfillAnimeAction.php index f45b9934a..08da99297 100644 --- a/app/Filament/Actions/Models/Wiki/Anime/BackfillAnimeAction.php +++ b/app/Filament/Actions/Models/Wiki/Anime/BackfillAnimeAction.php @@ -4,17 +4,7 @@ namespace App\Filament\Actions\Models\Wiki\Anime; -use App\Actions\Models\BackfillAction; -use App\Actions\Models\Wiki\Anime\BackfillAnimeOtherResourcesAction; -use App\Actions\Models\Wiki\Anime\BackfillAnimeSynonymsAction; -use App\Actions\Models\Wiki\Anime\Image\BackfillLargeCoverImageAction; -use App\Actions\Models\Wiki\Anime\Image\BackfillSmallCoverImageAction; -use App\Actions\Models\Wiki\Anime\Resource\BackfillAnidbResourceAction; -use App\Actions\Models\Wiki\Anime\Resource\BackfillAnilistResourceAction; -use App\Actions\Models\Wiki\Anime\Resource\BackfillAnnResourceAction; -use App\Actions\Models\Wiki\Anime\Resource\BackfillKitsuResourceAction; -use App\Actions\Models\Wiki\Anime\Resource\BackfillMalResourceAction; -use App\Actions\Models\Wiki\Anime\Studio\BackfillAnimeStudiosAction; +use App\Actions\Models\Wiki\BackfillAnimeAction as BackfillAnimeActionAction; use App\Enums\Models\Wiki\ImageFacet; use App\Enums\Models\Wiki\ResourceSite; use App\Filament\Actions\BaseAction; @@ -42,8 +32,14 @@ class BackfillAnimeAction extends BaseAction implements ShouldQueue use InteractsWithQueue; use Queueable; + final public const RESOURCES = BackfillAnimeActionAction::RESOURCES; + final public const IMAGES = BackfillAnimeActionAction::IMAGES; + final public const STUDIOS = BackfillAnimeActionAction::STUDIOS; + final public const SYNONYMS = BackfillAnimeActionAction::SYNONYMS; + final public const BACKFILL_ANIDB_RESOURCE = 'backfill_anidb_resource'; final public const BACKFILL_ANILIST_RESOURCE = 'backfill_anilist_resource'; + final public const BACKFILL_ANIME_PLANET_RESOURCE = 'backfill_anime_planet_resource'; final public const BACKFILL_ANN_RESOURCE = 'backfill_ann_resource'; final public const BACKFILL_KITSU_RESOURCE = 'backfill_kitsu_resource'; final public const BACKFILL_OTHER_RESOURCES = 'backfill_other_resources'; @@ -83,24 +79,22 @@ public function handle(Anime $anime, array $fields): void $this->failedLog(__('filament.actions.anime.backfill.message.resource_required_failure')); return; } - - $actions = $this->getActions($fields, $anime); + + $action = new BackfillAnimeActionAction($anime, $this->getToBackfill($fields)); try { - foreach ($actions as $action) { - $result = $action->handle(); - if ($result->hasFailed()) { - Notification::make() - ->body($result->getMessage()) - ->warning() - ->actions([ - NotificationAction::make('mark-as-read') - ->button() - ->markAsRead(), - ]) - ->sendToDatabase(Auth::user()); - } - } + $result = $action->handle(); + // if ($result->hasFailed()) { + // Notification::make() + // ->body($result->getMessage()) + // ->warning() + // ->actions([ + // NotificationAction::make('mark-as-read') + // ->button() + // ->markAsRead(), + // ]) + // ->sendToDatabase(Auth::user()); + // } } catch (Exception $e) { $this->failedLog($e); } finally { @@ -134,22 +128,27 @@ public function getForm(Form $form): ?Form ->label(__('filament.actions.anime.backfill.fields.resources.anilist.name')) ->helperText(__('filament.actions.anime.backfill.fields.resources.anilist.help')) ->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST->value)->doesntExist()), - + Checkbox::make(self::BACKFILL_MAL_RESOURCE) ->label(__('filament.actions.anime.backfill.fields.resources.mal.name')) ->helperText(__('filament.actions.anime.backfill.fields.resources.mal.help')) ->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL->value)->doesntExist()), - + Checkbox::make(self::BACKFILL_ANIDB_RESOURCE) ->label(__('filament.actions.anime.backfill.fields.resources.anidb.name')) ->helperText(__('filament.actions.anime.backfill.fields.resources.anidb.help')) ->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB->value)->doesntExist()), - + Checkbox::make(self::BACKFILL_ANN_RESOURCE) ->label(__('filament.actions.anime.backfill.fields.resources.ann.name')) ->helperText(__('filament.actions.anime.backfill.fields.resources.ann.help')) ->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANN->value)->doesntExist()), - + + Checkbox::make(self::BACKFILL_ANIME_PLANET_RESOURCE) + ->label(__('filament.actions.anime.backfill.fields.resources.anime_planet.name')) + ->helperText(__('filament.actions.anime.backfill.fields.resources.anime_planet.help')) + ->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIME_PLANET->value)->doesntExist()), + Checkbox::make(self::BACKFILL_OTHER_RESOURCES) ->label(__('filament.actions.anime.backfill.fields.resources.external_links.name')) ->helperText(__('filament.actions.anime.backfill.fields.resources.external_links.help')) @@ -162,7 +161,7 @@ public function getForm(Form $form): ?Form ->label(__('filament.actions.anime.backfill.fields.images.large_cover.name')) ->helperText(__('filament.actions.anime.backfill.fields.images.large_cover.help')) ->default(fn () => $anime instanceof Anime && $anime->images()->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_LARGE->value)->doesntExist()), - + Checkbox::make(self::BACKFILL_SMALL_COVER) ->label(__('filament.actions.anime.backfill.fields.images.small_cover.name')) ->helperText(__('filament.actions.anime.backfill.fields.images.small_cover.help')) @@ -188,44 +187,66 @@ public function getForm(Form $form): ?Form } /** - * Get the selected actions for backfilling anime. + * Get what should be backfilled. * * @param array $fields - * @param Anime $anime - * @return BackfillAction[] + * @return array */ - protected function getActions(array $fields, Anime $anime): array + protected function getToBackfill(array $fields): array { - $actions = []; + $toBackfill = []; + $toBackfill[self::RESOURCES] = []; + $toBackfill[self::IMAGES] = []; + + foreach ($this->getResourcesMapping() as $field => $sites) { + if (Arr::get($fields, $field) === true) { + $toBackfill[self::RESOURCES] = array_merge($toBackfill[self::RESOURCES], $sites); + } + } - foreach ($this->getActionMapping($anime) as $field => $action) { + foreach ($this->getImagesMapping() as $field => $facets) { if (Arr::get($fields, $field) === true) { - $actions[] = $action; + $toBackfill[self::IMAGES] = array_merge($toBackfill[self::IMAGES], $facets); } } - return $actions; + $toBackfill[self::STUDIOS] = Arr::get($fields, self::BACKFILL_STUDIOS); + $toBackfill[self::SYNONYMS] = Arr::get($fields, self::BACKFILL_SYNONYMS); + + return $toBackfill; } /** - * Get the mapping of actions to their form fields. + * Get the resources for mapping. * - * @param Anime $anime - * @return array + * @return array + */ + protected function getResourcesMapping(): array + { + return [ + self::BACKFILL_KITSU_RESOURCE => [ResourceSite::KITSU], + self::BACKFILL_ANILIST_RESOURCE => [ResourceSite::ANILIST], + self::BACKFILL_MAL_RESOURCE => [ResourceSite::MAL], + self::BACKFILL_ANIDB_RESOURCE => [ResourceSite::ANIDB], + self::BACKFILL_ANN_RESOURCE => [ResourceSite::ANN], + self::BACKFILL_ANIME_PLANET_RESOURCE => [ResourceSite::ANIME_PLANET], + self::BACKFILL_OTHER_RESOURCES => [ + ResourceSite::TWITTER, ResourceSite::OFFICIAL_SITE, ResourceSite::NETFLIX, ResourceSite::CRUNCHYROLL, + ResourceSite::HIDIVE, ResourceSite::AMAZON_PRIME_VIDEO, ResourceSite::HULU, ResourceSite::DISNEY_PLUS, + ], + ]; + } + + /** + * Get the images for mapping. + * + * @return array */ - protected function getActionMapping(Anime $anime): array + protected function getImagesMapping(): array { return [ - self::BACKFILL_KITSU_RESOURCE => new BackfillKitsuResourceAction($anime), - self::BACKFILL_ANILIST_RESOURCE => new BackfillAnilistResourceAction($anime), - self::BACKFILL_MAL_RESOURCE => new BackfillMalResourceAction($anime), - self::BACKFILL_ANIDB_RESOURCE => new BackfillAnidbResourceAction($anime), - self::BACKFILL_ANN_RESOURCE => new BackfillAnnResourceAction($anime), - self::BACKFILL_OTHER_RESOURCES => new BackfillAnimeOtherResourcesAction($anime), - self::BACKFILL_LARGE_COVER => new BackfillLargeCoverImageAction($anime), - self::BACKFILL_SMALL_COVER => new BackfillSmallCoverImageAction($anime), - self::BACKFILL_STUDIOS => new BackfillAnimeStudiosAction($anime), - self::BACKFILL_SYNONYMS => new BackfillAnimeSynonymsAction($anime), + self::BACKFILL_LARGE_COVER => [ImageFacet::COVER_LARGE], + self::BACKFILL_SMALL_COVER => [ImageFacet::COVER_SMALL], ]; } } diff --git a/app/Filament/Actions/Models/Wiki/Studio/BackfillStudioAction.php b/app/Filament/Actions/Models/Wiki/Studio/BackfillStudioAction.php index 7916a59e5..1aed84fd9 100644 --- a/app/Filament/Actions/Models/Wiki/Studio/BackfillStudioAction.php +++ b/app/Filament/Actions/Models/Wiki/Studio/BackfillStudioAction.php @@ -4,8 +4,7 @@ namespace App\Filament\Actions\Models\Wiki\Studio; -use App\Actions\Models\BackfillAction; -use App\Actions\Models\Wiki\Studio\Image\BackfillLargeCoverImageAction; +use App\Actions\Models\Wiki\BackfillStudioAction as BackfillStudioActionAction; use App\Enums\Models\Wiki\ImageFacet; use App\Filament\Actions\BaseAction; use App\Models\Wiki\Image; @@ -31,6 +30,8 @@ class BackfillStudioAction extends BaseAction implements ShouldQueue use InteractsWithQueue; use Queueable; + final public const IMAGES = BackfillStudioActionAction::IMAGES; + final public const BACKFILL_LARGE_COVER = 'backfill_large_cover'; /** @@ -63,23 +64,21 @@ public function handle(Studio $studio, array $fields): void return; } - $actions = $this->getActions($fields, $studio); + $action = new BackfillStudioActionAction($studio, $this->getToBackfill($fields)); try { - foreach ($actions as $action) { - $result = $action->handle(); - if ($result->hasFailed()) { - Notification::make() - ->body($result->getMessage()) - ->warning() - ->actions([ - NotificationAction::make('mark-as-read') - ->button() - ->markAsRead(), - ]) - ->sendToDatabase(Auth::user()); - } - } + $result = $action->handle(); + // if ($result->hasFailed()) { + // Notification::make() + // ->body($result->getMessage()) + // ->warning() + // ->actions([ + // NotificationAction::make('mark-as-read') + // ->button() + // ->markAsRead(), + // ]) + // ->sendToDatabase(Auth::user()); + // } } catch (Exception $e) { $this->failedLog($e); } finally { @@ -113,35 +112,34 @@ public function getForm(Form $form): Form } /** - * Get the selected actions for backfilling studios. + * Get what should be backfilled. * * @param array $fields - * @param Studio $studio - * @return BackfillAction[] + * @return array */ - protected function getActions(array $fields, Studio $studio): array + protected function getToBackfill(array $fields): array { - $actions = []; + $toBackfill = []; + $toBackfill[self::IMAGES] = []; - foreach ($this->getActionMapping($studio) as $field => $action) { + foreach ($this->getImagesMapping() as $field => $facets) { if (Arr::get($fields, $field) === true) { - $actions[] = $action; + $toBackfill[self::IMAGES] = array_merge($toBackfill[self::IMAGES], $facets); } } - return $actions; + return $toBackfill; } /** - * Get the mapping of actions to their form fields. + * Get the images for mapping. * - * @param Studio $studio - * @return array + * @return array */ - protected function getActionMapping(Studio $studio): array + protected function getImagesMapping(): array { return [ - self::BACKFILL_LARGE_COVER => new BackfillLargeCoverImageAction($studio), + self::BACKFILL_LARGE_COVER => [ImageFacet::COVER_LARGE], ]; } } diff --git a/app/Filament/Components/Fields/BelongsTo.php b/app/Filament/Components/Fields/BelongsTo.php index 91bf28a25..1db78aaa3 100644 --- a/app/Filament/Components/Fields/BelongsTo.php +++ b/app/Filament/Components/Fields/BelongsTo.php @@ -39,8 +39,8 @@ protected function reload(): void $this->tryScout($model); if ($this->showCreateOption) { - $this->createOptionForm(fn(Form $form) => $resource::form($form)->getComponents()); - $this->createOptionUsing(fn(array $data) => (new $model)::query()->create($data)->getKey()); + $this->createOptionForm(fn (Form $form) => $resource::form($form)->getComponents()); + $this->createOptionUsing(fn (array $data) => (new $model)::query()->create($data)->getKey()); } } } diff --git a/app/Filament/HeaderActions/Models/Wiki/Anime/AttachAnimeResourceHeaderAction.php b/app/Filament/HeaderActions/Models/Wiki/Anime/AttachAnimeResourceHeaderAction.php index 114cf6285..06470f681 100644 --- a/app/Filament/HeaderActions/Models/Wiki/Anime/AttachAnimeResourceHeaderAction.php +++ b/app/Filament/HeaderActions/Models/Wiki/Anime/AttachAnimeResourceHeaderAction.php @@ -31,6 +31,7 @@ protected function setUp(): void ResourceSite::ANIME_PLANET, ResourceSite::ANN, ResourceSite::KITSU, + ResourceSite::LIVECHART, ResourceSite::MAL, ResourceSite::OFFICIAL_SITE, ResourceSite::TWITTER, diff --git a/app/Filament/HeaderActions/Models/Wiki/Anime/BackfillAnimeHeaderAction.php b/app/Filament/HeaderActions/Models/Wiki/Anime/BackfillAnimeHeaderAction.php index 2cf21c433..7d0604425 100644 --- a/app/Filament/HeaderActions/Models/Wiki/Anime/BackfillAnimeHeaderAction.php +++ b/app/Filament/HeaderActions/Models/Wiki/Anime/BackfillAnimeHeaderAction.php @@ -4,17 +4,7 @@ namespace App\Filament\HeaderActions\Models\Wiki\Anime; -use App\Actions\Models\BackfillAction; -use App\Actions\Models\Wiki\Anime\BackfillAnimeOtherResourcesAction; -use App\Actions\Models\Wiki\Anime\BackfillAnimeSynonymsAction; -use App\Actions\Models\Wiki\Anime\Image\BackfillLargeCoverImageAction; -use App\Actions\Models\Wiki\Anime\Image\BackfillSmallCoverImageAction; -use App\Actions\Models\Wiki\Anime\Resource\BackfillAnidbResourceAction; -use App\Actions\Models\Wiki\Anime\Resource\BackfillAnilistResourceAction; -use App\Actions\Models\Wiki\Anime\Resource\BackfillAnnResourceAction; -use App\Actions\Models\Wiki\Anime\Resource\BackfillKitsuResourceAction; -use App\Actions\Models\Wiki\Anime\Resource\BackfillMalResourceAction; -use App\Actions\Models\Wiki\Anime\Studio\BackfillAnimeStudiosAction; +use App\Actions\Models\Wiki\BackfillAnimeAction; use App\Enums\Models\Wiki\ImageFacet; use App\Enums\Models\Wiki\ResourceSite; use App\Filament\HeaderActions\BaseHeaderAction; @@ -42,8 +32,14 @@ class BackfillAnimeHeaderAction extends BaseHeaderAction implements ShouldQueue use InteractsWithQueue; use Queueable; + final public const RESOURCES = BackfillAnimeAction::RESOURCES; + final public const IMAGES = BackfillAnimeAction::IMAGES; + final public const STUDIOS = BackfillAnimeAction::STUDIOS; + final public const SYNONYMS = BackfillAnimeAction::SYNONYMS; + final public const BACKFILL_ANIDB_RESOURCE = 'backfill_anidb_resource'; final public const BACKFILL_ANILIST_RESOURCE = 'backfill_anilist_resource'; + final public const BACKFILL_ANIME_PLANET_RESOURCE = 'backfill_anime_planet_resource'; final public const BACKFILL_ANN_RESOURCE = 'backfill_ann_resource'; final public const BACKFILL_KITSU_RESOURCE = 'backfill_kitsu_resource'; final public const BACKFILL_OTHER_RESOURCES = 'backfill_other_resources'; @@ -84,23 +80,21 @@ public function handle(Anime $anime, array $fields): void return; } - $actions = $this->getActions($fields, $anime); + $action = new BackfillAnimeAction($anime, $this->getToBackfill($fields)); try { - foreach ($actions as $action) { - $result = $action->handle(); - if ($result->hasFailed()) { - Notification::make() - ->body($result->getMessage()) - ->warning() - ->actions([ - NotificationAction::make('mark-as-read') - ->button() - ->markAsRead(), - ]) - ->sendToDatabase(Auth::user()); - } - } + $result = $action->handle(); + // if ($result->hasFailed()) { + // Notification::make() + // ->body($result->getMessage()) + // ->warning() + // ->actions([ + // NotificationAction::make('mark-as-read') + // ->button() + // ->markAsRead(), + // ]) + // ->sendToDatabase(Auth::user()); + // } } catch (Exception $e) { $this->failedLog($e); } finally { @@ -134,22 +128,27 @@ public function getForm(Form $form): ?Form ->label(__('filament.actions.anime.backfill.fields.resources.anilist.name')) ->helperText(__('filament.actions.anime.backfill.fields.resources.anilist.help')) ->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST->value)->doesntExist()), - + Checkbox::make(self::BACKFILL_MAL_RESOURCE) ->label(__('filament.actions.anime.backfill.fields.resources.mal.name')) ->helperText(__('filament.actions.anime.backfill.fields.resources.mal.help')) ->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL->value)->doesntExist()), - + Checkbox::make(self::BACKFILL_ANIDB_RESOURCE) ->label(__('filament.actions.anime.backfill.fields.resources.anidb.name')) ->helperText(__('filament.actions.anime.backfill.fields.resources.anidb.help')) ->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB->value)->doesntExist()), - + Checkbox::make(self::BACKFILL_ANN_RESOURCE) ->label(__('filament.actions.anime.backfill.fields.resources.ann.name')) ->helperText(__('filament.actions.anime.backfill.fields.resources.ann.help')) ->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANN->value)->doesntExist()), - + + Checkbox::make(self::BACKFILL_ANIME_PLANET_RESOURCE) + ->label(__('filament.actions.anime.backfill.fields.resources.anime_planet.name')) + ->helperText(__('filament.actions.anime.backfill.fields.resources.anime_planet.help')) + ->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIME_PLANET->value)->doesntExist()), + Checkbox::make(self::BACKFILL_OTHER_RESOURCES) ->label(__('filament.actions.anime.backfill.fields.resources.external_links.name')) ->helperText(__('filament.actions.anime.backfill.fields.resources.external_links.help')) @@ -162,7 +161,7 @@ public function getForm(Form $form): ?Form ->label(__('filament.actions.anime.backfill.fields.images.large_cover.name')) ->helperText(__('filament.actions.anime.backfill.fields.images.large_cover.help')) ->default(fn () => $anime instanceof Anime && $anime->images()->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_LARGE->value)->doesntExist()), - + Checkbox::make(self::BACKFILL_SMALL_COVER) ->label(__('filament.actions.anime.backfill.fields.images.small_cover.name')) ->helperText(__('filament.actions.anime.backfill.fields.images.small_cover.help')) @@ -188,44 +187,66 @@ public function getForm(Form $form): ?Form } /** - * Get the selected actions for backfilling anime. + * Get what should be backfilled. * * @param array $fields - * @param Anime $anime - * @return BackfillAction[] + * @return array */ - protected function getActions(array $fields, Anime $anime): array + protected function getToBackfill(array $fields): array { - $actions = []; + $toBackfill = []; + $toBackfill[self::RESOURCES] = []; + $toBackfill[self::IMAGES] = []; - foreach ($this->getActionMapping($anime) as $field => $action) { + foreach ($this->getResourcesMapping() as $field => $sites) { if (Arr::get($fields, $field) === true) { - $actions[] = $action; + $toBackfill[self::RESOURCES] = array_merge($toBackfill[self::RESOURCES], $sites); } } - return $actions; + foreach ($this->getImagesMapping() as $field => $facets) { + if (Arr::get($fields, $field) === true) { + $toBackfill[self::IMAGES] = array_merge($toBackfill[self::IMAGES], $facets); + } + } + + $toBackfill[self::STUDIOS] = Arr::get($fields, self::BACKFILL_STUDIOS); + $toBackfill[self::SYNONYMS] = Arr::get($fields, self::BACKFILL_SYNONYMS); + + return $toBackfill; } /** - * Get the mapping of actions to their form fields. + * Get the resources for mapping. * - * @param Anime $anime - * @return array + * @return array + */ + protected function getResourcesMapping(): array + { + return [ + self::BACKFILL_KITSU_RESOURCE => [ResourceSite::KITSU], + self::BACKFILL_ANILIST_RESOURCE => [ResourceSite::ANILIST], + self::BACKFILL_MAL_RESOURCE => [ResourceSite::MAL], + self::BACKFILL_ANIDB_RESOURCE => [ResourceSite::ANIDB], + self::BACKFILL_ANN_RESOURCE => [ResourceSite::ANN], + self::BACKFILL_ANIME_PLANET_RESOURCE => [ResourceSite::ANIME_PLANET], + self::BACKFILL_OTHER_RESOURCES => [ + ResourceSite::TWITTER, ResourceSite::OFFICIAL_SITE, ResourceSite::NETFLIX, ResourceSite::CRUNCHYROLL, + ResourceSite::HIDIVE, ResourceSite::AMAZON_PRIME_VIDEO, ResourceSite::HULU, ResourceSite::DISNEY_PLUS, + ], + ]; + } + + /** + * Get the images for mapping. + * + * @return array */ - protected function getActionMapping(Anime $anime): array + protected function getImagesMapping(): array { return [ - self::BACKFILL_KITSU_RESOURCE => new BackfillKitsuResourceAction($anime), - self::BACKFILL_ANILIST_RESOURCE => new BackfillAnilistResourceAction($anime), - self::BACKFILL_MAL_RESOURCE => new BackfillMalResourceAction($anime), - self::BACKFILL_ANIDB_RESOURCE => new BackfillAnidbResourceAction($anime), - self::BACKFILL_ANN_RESOURCE => new BackfillAnnResourceAction($anime), - self::BACKFILL_OTHER_RESOURCES => new BackfillAnimeOtherResourcesAction($anime), - self::BACKFILL_LARGE_COVER => new BackfillLargeCoverImageAction($anime), - self::BACKFILL_SMALL_COVER => new BackfillSmallCoverImageAction($anime), - self::BACKFILL_STUDIOS => new BackfillAnimeStudiosAction($anime), - self::BACKFILL_SYNONYMS => new BackfillAnimeSynonymsAction($anime), + self::BACKFILL_LARGE_COVER => [ImageFacet::COVER_LARGE], + self::BACKFILL_SMALL_COVER => [ImageFacet::COVER_SMALL], ]; } } diff --git a/app/Filament/HeaderActions/Models/Wiki/Studio/BackfillStudioHeaderAction.php b/app/Filament/HeaderActions/Models/Wiki/Studio/BackfillStudioHeaderAction.php index e761fd7f6..0442c42c6 100644 --- a/app/Filament/HeaderActions/Models/Wiki/Studio/BackfillStudioHeaderAction.php +++ b/app/Filament/HeaderActions/Models/Wiki/Studio/BackfillStudioHeaderAction.php @@ -4,8 +4,7 @@ namespace App\Filament\HeaderActions\Models\Wiki\Studio; -use App\Actions\Models\BackfillAction; -use App\Actions\Models\Wiki\Studio\Image\BackfillLargeCoverImageAction; +use App\Actions\Models\Wiki\BackfillStudioAction as BackfillStudioActionAction; use App\Enums\Models\Wiki\ImageFacet; use App\Filament\HeaderActions\BaseHeaderAction; use App\Models\Wiki\Image; @@ -31,6 +30,8 @@ class BackfillStudioHeaderAction extends BaseHeaderAction implements ShouldQueue use InteractsWithQueue; use Queueable; + final public const IMAGES = BackfillStudioActionAction::IMAGES; + final public const BACKFILL_LARGE_COVER = 'backfill_large_cover'; /** @@ -63,23 +64,21 @@ public function handle(Studio $studio, array $fields): void return; } - $actions = $this->getActions($fields, $studio); + $action = new BackfillStudioActionAction($studio, $this->getToBackfill($fields)); try { - foreach ($actions as $action) { - $result = $action->handle(); - if ($result->hasFailed()) { - Notification::make() - ->body($result->getMessage()) - ->warning() - ->actions([ - NotificationAction::make('mark-as-read') - ->button() - ->markAsRead(), - ]) - ->sendToDatabase(Auth::user()); - } - } + $result = $action->handle(); + // if ($result->hasFailed()) { + // Notification::make() + // ->body($result->getMessage()) + // ->warning() + // ->actions([ + // NotificationAction::make('mark-as-read') + // ->button() + // ->markAsRead(), + // ]) + // ->sendToDatabase(Auth::user()); + // } } catch (Exception $e) { $this->failedLog($e); } finally { @@ -113,35 +112,34 @@ public function getForm(Form $form): Form } /** - * Get the selected actions for backfilling studios. + * Get what should be backfilled. * * @param array $fields - * @param Studio $studio - * @return BackfillAction[] + * @return array */ - protected function getActions(array $fields, Studio $studio): array + protected function getToBackfill(array $fields): array { - $actions = []; + $toBackfill = []; + $toBackfill[self::IMAGES] = []; - foreach ($this->getActionMapping($studio) as $field => $action) { + foreach ($this->getImagesMapping() as $field => $facets) { if (Arr::get($fields, $field) === true) { - $actions[] = $action; + $toBackfill[self::IMAGES] = array_merge($toBackfill[self::IMAGES], $facets); } } - return $actions; + return $toBackfill; } /** - * Get the mapping of actions to their form fields. + * Get the images for mapping. * - * @param Studio $studio - * @return array + * @return array */ - protected function getActionMapping(Studio $studio): array + protected function getImagesMapping(): array { return [ - self::BACKFILL_LARGE_COVER => new BackfillLargeCoverImageAction($studio), + self::BACKFILL_LARGE_COVER => [ImageFacet::COVER_LARGE], ]; } } diff --git a/app/Filament/Resources/Admin/FeaturedTheme.php b/app/Filament/Resources/Admin/FeaturedTheme.php index 301ec8bbf..81194773a 100644 --- a/app/Filament/Resources/Admin/FeaturedTheme.php +++ b/app/Filament/Resources/Admin/FeaturedTheme.php @@ -124,7 +124,7 @@ public static function form(Form $form): Form ->timezone('UTC') ->displayFormat('MM/DD/YYYY') ->format(AllowedDateFormat::YMD->value) - ->formatStateUsing(fn($record) => $record !== null ? $record->start_at->format('m/d/Y') . ' - ' . $record->end_at->format('m/d/Y') : null) + ->formatStateUsing(fn ($record) => $record !== null ? $record->start_at->format('m/d/Y') . ' - ' . $record->end_at->format('m/d/Y') : null) ->required() ->rules([ 'required', diff --git a/app/Filament/Resources/List/External/ExternalEntry.php b/app/Filament/Resources/List/External/ExternalEntry.php index 240c1b16e..ba35ee249 100644 --- a/app/Filament/Resources/List/External/ExternalEntry.php +++ b/app/Filament/Resources/List/External/ExternalEntry.php @@ -181,7 +181,7 @@ public static function table(Table $table): Table TextColumn::make(ExternalEntryModel::ATTRIBUTE_WATCH_STATUS) ->label(__('filament.fields.external_entry.watch_status.name')) - ->formatStateUsing(fn(ExternalEntryWatchStatus $state) => $state->localize()) + ->formatStateUsing(fn (ExternalEntryWatchStatus $state) => $state->localize()) ->sortable() ->toggleable(), @@ -222,7 +222,7 @@ public static function infolist(Infolist $infolist): Infolist TextEntry::make(ExternalEntryModel::ATTRIBUTE_WATCH_STATUS) ->label(__('filament.fields.external_entry.watch_status.name')) - ->formatStateUsing(fn(ExternalEntryWatchStatus $state) => $state->localize()), + ->formatStateUsing(fn (ExternalEntryWatchStatus $state) => $state->localize()), TextEntry::make(ExternalEntryModel::ATTRIBUTE_ID) ->label(__('filament.fields.base.id')), diff --git a/app/Filament/TableActions/Storage/Base/UploadTableAction.php b/app/Filament/TableActions/Storage/Base/UploadTableAction.php index ae4f690b1..f4eaa2e9e 100644 --- a/app/Filament/TableActions/Storage/Base/UploadTableAction.php +++ b/app/Filament/TableActions/Storage/Base/UploadTableAction.php @@ -58,7 +58,7 @@ public function getForm(Form $form): Form ->label(__('filament.actions.storage.upload.fields.path.name')) ->helperText(__('filament.actions.storage.upload.fields.path.help')) ->rules(['doesnt_start_with:/', 'doesnt_end_with:/', 'string', new StorageDirectoryExistsRule($fs)]) - ->hidden(fn($livewire) => $livewire instanceof VideoEntryRelationManager || $livewire instanceof ScriptVideoRelationManager), + ->hidden(fn ($livewire) => $livewire instanceof VideoEntryRelationManager || $livewire instanceof ScriptVideoRelationManager), ]); } diff --git a/app/Filament/TableActions/Storage/Wiki/Video/UploadVideoTableAction.php b/app/Filament/TableActions/Storage/Wiki/Video/UploadVideoTableAction.php index ca50692aa..3f68bdf5f 100644 --- a/app/Filament/TableActions/Storage/Wiki/Video/UploadVideoTableAction.php +++ b/app/Filament/TableActions/Storage/Wiki/Video/UploadVideoTableAction.php @@ -82,7 +82,7 @@ public function getForm(Form $form): Form [ Hidden::make(AnimeThemeEntry::ATTRIBUTE_ID) ->label(__('filament.resources.singularLabel.anime_theme_entry')) - ->default(fn(BaseRelationManager|ListVideos $livewire) => $livewire instanceof VideoEntryRelationManager ? $livewire->getOwnerRecord()->getKey() : null), + ->default(fn (BaseRelationManager|ListVideos $livewire) => $livewire instanceof VideoEntryRelationManager ? $livewire->getOwnerRecord()->getKey() : null), ], parent::getForm($form)->getComponents(), [ diff --git a/app/Http/Api/Schema/Wiki/SeriesSchema.php b/app/Http/Api/Schema/Wiki/SeriesSchema.php index b378446f7..366407ef8 100644 --- a/app/Http/Api/Schema/Wiki/SeriesSchema.php +++ b/app/Http/Api/Schema/Wiki/SeriesSchema.php @@ -11,6 +11,7 @@ use App\Http\Api\Field\Wiki\Series\SeriesSlugField; use App\Http\Api\Include\AllowedInclude; use App\Http\Api\Schema\EloquentSchema; +use App\Http\Api\Schema\Wiki\Anime\SynonymSchema; use App\Http\Resources\Wiki\Resource\SeriesResource; use App\Models\Wiki\Series; @@ -44,6 +45,7 @@ public function allowedIncludes(): array new AllowedInclude(new VideoSchema(), 'anime.animethemes.animethemeentries.videos'), new AllowedInclude(new GroupSchema(), 'anime.animethemes.group'), new AllowedInclude(new SongSchema(), 'anime.animethemes.song'), + new AllowedInclude(new SynonymSchema(), 'anime.animesynonyms'), ]; } diff --git a/app/Nova/Actions/Models/Wiki/Anime/BackfillAnimeAction.php b/app/Nova/Actions/Models/Wiki/Anime/BackfillAnimeAction.php deleted file mode 100644 index dca6dd811..000000000 --- a/app/Nova/Actions/Models/Wiki/Anime/BackfillAnimeAction.php +++ /dev/null @@ -1,226 +0,0 @@ - $models - * @return Collection - */ - public function handle(ActionFields $fields, Collection $models): Collection - { - $uriKey = AnimeResource::uriKey(); - - foreach ($models as $anime) { - if ($anime->resources()->doesntExist()) { - $this->markAsFailed($anime, __('nova.actions.anime.backfill.message.resource_required_failure')); - continue; - } - - $actions = $this->getActions($fields, $anime); - - try { - foreach ($actions as $action) { - $result = $action->handle(); - if ($result->hasFailed()) { - $this->user->notify( - NovaNotification::make() - ->icon('flag') - ->message($result->getMessage()) - ->type(NovaNotification::WARNING_TYPE) - ->url("/resources/$uriKey/{$anime->getKey()}") - ); - } - } - } catch (Exception $e) { - $this->markAsFailed($anime, $e); - } finally { - // Try not to upset third-party APIs - Sleep::for(rand(3, 5))->second(); - } - } - - return $models; - } - - /** - * Get the fields available on the action. - * - * @param NovaRequest $request - * @return array - * - * @noinspection PhpMissingParentCallCommonInspection - */ - public function fields(NovaRequest $request): array - { - $anime = $request->findModelQuery()->first(); - - return [ - Heading::make(__('nova.actions.anime.backfill.fields.resources.name')), - - Boolean::make(__('nova.actions.anime.backfill.fields.resources.kitsu.name'), self::BACKFILL_KITSU_RESOURCE) - ->help(__('nova.actions.anime.backfill.fields.resources.kitsu.help')) - ->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU->value)->doesntExist()), - - Boolean::make(__('nova.actions.anime.backfill.fields.resources.anilist.name'), self::BACKFILL_ANILIST_RESOURCE) - ->help(__('nova.actions.anime.backfill.fields.resources.anilist.help')) - ->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST->value)->doesntExist()), - - Boolean::make(__('nova.actions.anime.backfill.fields.resources.mal.name'), self::BACKFILL_MAL_RESOURCE) - ->help(__('nova.actions.anime.backfill.fields.resources.mal.help')) - ->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL->value)->doesntExist()), - - Boolean::make(__('nova.actions.anime.backfill.fields.resources.anidb.name'), self::BACKFILL_ANIDB_RESOURCE) - ->help(__('nova.actions.anime.backfill.fields.resources.anidb.help')) - ->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB->value)->doesntExist()), - - Boolean::make(__('nova.actions.anime.backfill.fields.resources.ann.name'), self::BACKFILL_ANN_RESOURCE) - ->help(__('nova.actions.anime.backfill.fields.resources.ann.help')) - ->default(fn () => $anime instanceof Anime && $anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANN->value)->doesntExist()), - - Boolean::make(__('nova.actions.anime.backfill.fields.resources.external_links.name'), self::BACKFILL_OTHER_RESOURCES) - ->help(__('nova.actions.anime.backfill.fields.resources.external_links.help')) - ->default(fn () => $anime instanceof Anime), - - Heading::make(__('nova.actions.anime.backfill.fields.images.name')), - - Boolean::make(__('nova.actions.anime.backfill.fields.images.large_cover.name'), self::BACKFILL_LARGE_COVER) - ->help(__('nova.actions.anime.backfill.fields.images.large_cover.help')) - ->default(fn () => $anime instanceof Anime && $anime->images()->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_LARGE->value)->doesntExist()), - - Boolean::make(__('nova.actions.anime.backfill.fields.images.small_cover.name'), self::BACKFILL_SMALL_COVER) - ->help(__('nova.actions.anime.backfill.fields.images.small_cover.help')) - ->default(fn () => $anime instanceof Anime && $anime->images()->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_SMALL->value)->doesntExist()), - - Heading::make(__('nova.actions.anime.backfill.fields.studios.name')), - - Boolean::make(__('nova.actions.anime.backfill.fields.studios.anime.name'), self::BACKFILL_STUDIOS) - ->help(__('nova.actions.anime.backfill.fields.studios.anime.help')) - ->default(fn () => $anime instanceof Anime && $anime->studios()->doesntExist()), - - Heading::make(__('nova.actions.anime.backfill.fields.synonyms.name')), - - Boolean::make(__('nova.actions.anime.backfill.fields.synonyms.name')) - ->help(__('nova.actions.anime.backfill.fields.synonyms.help')) - ->default(fn () => $anime instanceof Anime && $anime->animesynonyms()->count() === 0), - ]; - } - - /** - * Get the selected actions for backfilling anime. - * - * @param ActionFields $fields - * @param Anime $anime - * @return BackfillAction[] - */ - protected function getActions(ActionFields $fields, Anime $anime): array - { - $actions = []; - - foreach ($this->getActionMapping($anime) as $field => $action) { - if (Arr::get($fields, $field) === true) { - $actions[] = $action; - } - } - - return $actions; - } - - /** - * Get the mapping of actions to their form fields. - * - * @param Anime $anime - * @return array - */ - protected function getActionMapping(Anime $anime): array - { - return [ - self::BACKFILL_KITSU_RESOURCE => new BackfillKitsuResourceAction($anime), - self::BACKFILL_ANILIST_RESOURCE => new BackfillAnilistResourceAction($anime), - self::BACKFILL_MAL_RESOURCE => new BackfillMalResourceAction($anime), - self::BACKFILL_ANIDB_RESOURCE => new BackfillAnidbResourceAction($anime), - self::BACKFILL_ANN_RESOURCE => new BackfillAnnResourceAction($anime), - self::BACKFILL_OTHER_RESOURCES => new BackfillAnimeOtherResourcesAction($anime), - self::BACKFILL_LARGE_COVER => new BackfillLargeCoverImageAction($anime), - self::BACKFILL_SMALL_COVER => new BackfillSmallCoverImageAction($anime), - self::BACKFILL_STUDIOS => new BackfillAnimeStudiosAction($anime), - self::BACKFILL_SYNONYMS => new BackfillAnimeSynonymsAction($anime), - ]; - } -} diff --git a/app/Nova/Actions/Models/Wiki/Studio/BackfillStudioAction.php b/app/Nova/Actions/Models/Wiki/Studio/BackfillStudioAction.php deleted file mode 100644 index 0833298b2..000000000 --- a/app/Nova/Actions/Models/Wiki/Studio/BackfillStudioAction.php +++ /dev/null @@ -1,155 +0,0 @@ - $models - * @return Collection - */ - public function handle(ActionFields $fields, Collection $models): Collection - { - $uriKey = StudioResource::uriKey(); - - foreach ($models as $studio) { - if ($studio->resources()->doesntExist()) { - $this->markAsFailed($studio, __('nova.actions.studio.backfill.message.resource_required_failure')); - continue; - } - - $actions = $this->getActions($fields, $studio); - - try { - foreach ($actions as $action) { - $result = $action->handle(); - if ($result->hasFailed()) { - $this->user->notify( - NovaNotification::make() - ->icon('flag') - ->message($result->getMessage()) - ->type(NovaNotification::WARNING_TYPE) - ->url("/resources/$uriKey/{$studio->getKey()}") - ); - } - } - } catch (Exception $e) { - $this->markAsFailed($studio, $e); - } finally { - // Try not to upset third-party APIs - Sleep::for(rand(3, 5))->second(); - } - } - - return $models; - } - - /** - * Get the fields available on the action. - * - * @param NovaRequest $request - * @return array - * - * @noinspection PhpMissingParentCallCommonInspection - */ - public function fields(NovaRequest $request): array - { - $studio = $request->findModelQuery()->first(); - - return [ - Heading::make(__('nova.actions.studio.backfill.fields.images.name')), - - Boolean::make(__('nova.actions.studio.backfill.fields.images.large_cover.name'), self::BACKFILL_LARGE_COVER) - ->help(__('nova.actions.studio.backfill.fields.images.large_cover.help')) - ->default(fn () => $studio instanceof Studio && $studio->images()->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_LARGE->value)->doesntExist()), - ]; - } - - /** - * Get the selected actions for backfilling studios. - * - * @param ActionFields $fields - * @param Studio $studio - * @return BackfillAction[] - */ - protected function getActions(ActionFields $fields, Studio $studio): array - { - $actions = []; - - foreach ($this->getActionMapping($studio) as $field => $action) { - if (Arr::get($fields, $field) === true) { - $actions[] = $action; - } - } - - return $actions; - } - - /** - * Get the mapping of actions to their form fields. - * - * @param Studio $studio - * @return array - */ - protected function getActionMapping(Studio $studio): array - { - return [ - self::BACKFILL_LARGE_COVER => new BackfillLargeCoverImageAction($studio), - ]; - } -} diff --git a/app/Nova/Lenses/Anime/AnimeImageLens.php b/app/Nova/Lenses/Anime/AnimeImageLens.php index 2a5b1b348..896a41a05 100644 --- a/app/Nova/Lenses/Anime/AnimeImageLens.php +++ b/app/Nova/Lenses/Anime/AnimeImageLens.php @@ -8,7 +8,6 @@ use App\Models\Wiki\Anime; use App\Models\Wiki\Image; use App\Nova\Actions\Models\Wiki\Anime\AttachAnimeImageAction; -use App\Nova\Actions\Models\Wiki\Anime\BackfillAnimeAction; use Illuminate\Database\Eloquent\Builder; use Laravel\Nova\Http\Requests\NovaRequest; @@ -60,12 +59,6 @@ public static function criteria(Builder $query): Builder public function actions(NovaRequest $request): array { return [ - (new BackfillAnimeAction($request->user())) - ->confirmButtonText(__('nova.actions.anime.backfill.confirmButtonText')) - ->cancelButtonText(__('nova.actions.base.cancelButtonText')) - ->showInline() - ->canSeeWhen('update', $this->resource), - (new AttachAnimeImageAction([static::facet()])) ->confirmButtonText(__('nova.actions.models.wiki.attach_image.confirmButtonText')) ->cancelButtonText(__('nova.actions.base.cancelButtonText')) diff --git a/app/Nova/Lenses/Anime/Studio/AnimeStudioLens.php b/app/Nova/Lenses/Anime/Studio/AnimeStudioLens.php index d134e7606..9db1758d0 100644 --- a/app/Nova/Lenses/Anime/Studio/AnimeStudioLens.php +++ b/app/Nova/Lenses/Anime/Studio/AnimeStudioLens.php @@ -5,7 +5,6 @@ namespace App\Nova\Lenses\Anime\Studio; use App\Models\Wiki\Anime; -use App\Nova\Actions\Models\Wiki\Anime\BackfillAnimeAction; use App\Nova\Lenses\Anime\AnimeLens; use Illuminate\Database\Eloquent\Builder; use Laravel\Nova\Http\Requests\NovaRequest; @@ -48,13 +47,7 @@ public static function criteria(Builder $query): Builder */ public function actions(NovaRequest $request): array { - return [ - (new BackfillAnimeAction($request->user())) - ->confirmButtonText(__('nova.actions.anime.backfill.confirmButtonText')) - ->cancelButtonText(__('nova.actions.base.cancelButtonText')) - ->showInline() - ->canSeeWhen('update', $this->resource), - ]; + return []; } /** diff --git a/app/Nova/Lenses/Studio/Image/StudioCoverLargeLens.php b/app/Nova/Lenses/Studio/Image/StudioCoverLargeLens.php index 659b023f5..6820e1903 100644 --- a/app/Nova/Lenses/Studio/Image/StudioCoverLargeLens.php +++ b/app/Nova/Lenses/Studio/Image/StudioCoverLargeLens.php @@ -8,7 +8,6 @@ use App\Models\Wiki\Image; use App\Models\Wiki\Studio; use App\Nova\Actions\Models\Wiki\Studio\AttachStudioImageAction; -use App\Nova\Actions\Models\Wiki\Studio\BackfillStudioAction; use App\Nova\Lenses\Studio\StudioLens; use Illuminate\Database\Eloquent\Builder; use Laravel\Nova\Http\Requests\NovaRequest; @@ -54,12 +53,6 @@ public static function criteria(Builder $query): Builder public function actions(NovaRequest $request): array { return [ - (new BackfillStudioAction($request->user())) - ->confirmButtonText(__('nova.actions.studio.backfill.confirmButtonText')) - ->cancelButtonText(__('nova.actions.base.cancelButtonText')) - ->showInline() - ->canSeeWhen('update', $this->resource), - (new AttachStudioImageAction([ImageFacet::COVER_LARGE])) ->confirmButtonText(__('nova.actions.models.wiki.attach_image.confirmButtonText')) ->cancelButtonText(__('nova.actions.base.cancelButtonText')) diff --git a/app/Nova/Resources/List/External/ExternalEntry.php b/app/Nova/Resources/List/External/ExternalEntry.php index e667de52f..97754e1b9 100644 --- a/app/Nova/Resources/List/External/ExternalEntry.php +++ b/app/Nova/Resources/List/External/ExternalEntry.php @@ -177,7 +177,7 @@ public function fields(NovaRequest $request): array Select::make(__('nova.fields.externalentry.watch_status.name'), ExternalEntryModel::ATTRIBUTE_WATCH_STATUS) ->options(ExternalEntryWatchStatus::asSelectArray()) - ->displayUsing(fn(?int $enumValue) => ExternalEntryWatchStatus::tryFrom($enumValue)?->localize()) + ->displayUsing(fn (?int $enumValue) => ExternalEntryWatchStatus::tryFrom($enumValue)?->localize()) ->sortable() ->rules(['required', new Enum(ExternalEntryWatchStatus::class)]) ->help(__('nova.fields.externalentry.watch_status.help')) diff --git a/app/Nova/Resources/Wiki/Anime.php b/app/Nova/Resources/Wiki/Anime.php index fb6e03823..6b2e4500c 100644 --- a/app/Nova/Resources/Wiki/Anime.php +++ b/app/Nova/Resources/Wiki/Anime.php @@ -15,7 +15,6 @@ use App\Nova\Actions\Discord\DiscordThreadAction; use App\Nova\Actions\Models\Wiki\Anime\AttachAnimeImageAction; use App\Nova\Actions\Models\Wiki\Anime\AttachAnimeResourceAction; -use App\Nova\Actions\Models\Wiki\Anime\BackfillAnimeAction; use App\Nova\Lenses\Anime\AnimeStreamingResourceLens; use App\Nova\Lenses\Anime\Image\AnimeCoverLargeLens; use App\Nova\Lenses\Anime\Image\AnimeCoverSmallLens; @@ -339,14 +338,6 @@ public function actions(NovaRequest $request): array ->exceptOnIndex() ->canSeeWhen('create', VideoModel::class), - (new BackfillAnimeAction($request->user())) - ->confirmButtonText(__('nova.actions.anime.backfill.confirmButtonText')) - ->cancelButtonText(__('nova.actions.base.cancelButtonText')) - ->showOnIndex() - ->showOnDetail() - ->showInline() - ->canSeeWhen('update', $this), - (new AttachAnimeResourceAction($resourceSites, null)) ->confirmButtonText(__('nova.actions.models.wiki.attach_resource.confirmButtonText')) ->cancelButtonText(__('nova.actions.base.cancelButtonText')) diff --git a/app/Nova/Resources/Wiki/Studio.php b/app/Nova/Resources/Wiki/Studio.php index 217903ead..690dd8c98 100644 --- a/app/Nova/Resources/Wiki/Studio.php +++ b/app/Nova/Resources/Wiki/Studio.php @@ -11,7 +11,6 @@ use App\Models\Wiki\Studio as StudioModel; use App\Nova\Actions\Models\Wiki\Studio\AttachStudioImageAction; use App\Nova\Actions\Models\Wiki\Studio\AttachStudioResourceAction; -use App\Nova\Actions\Models\Wiki\Studio\BackfillStudioAction; use App\Nova\Lenses\Studio\Image\StudioCoverLargeLens; use App\Nova\Lenses\Studio\Resource\StudioAniDbResourceLens; use App\Nova\Lenses\Studio\Resource\StudioAnilistResourceLens; @@ -231,14 +230,6 @@ public function actions(NovaRequest $request): array return array_merge( parent::actions($request), [ - (new BackfillStudioAction($request->user())) - ->confirmButtonText(__('nova.actions.studio.backfill.confirmButtonText')) - ->cancelButtonText(__('nova.actions.base.cancelButtonText')) - ->showOnIndex() - ->showOnDetail() - ->showInline() - ->canSeeWhen('update', $this), - (new AttachStudioResourceAction($resources, null)) ->confirmButtonText(__('nova.actions.models.wiki.attach_resource.confirmButtonText')) ->cancelButtonText(__('nova.actions.base.cancelButtonText')) diff --git a/lang/en/enums.php b/lang/en/enums.php index d3e96e46f..88076863a 100644 --- a/lang/en/enums.php +++ b/lang/en/enums.php @@ -81,6 +81,7 @@ ResourceSite::ANIME_PLANET->name => 'Anime-Planet', ResourceSite::ANN->name => 'Anime News Network', ResourceSite::KITSU->name => 'Kitsu', + ResourceSite::LIVECHART->name => 'LiveChart', ResourceSite::MAL->name => 'MyAnimeList', ResourceSite::WIKI->name => 'Wiki', ResourceSite::SPOTIFY->name => 'Spotify', diff --git a/lang/en/filament.php b/lang/en/filament.php index 0b91a4f57..92580f4cf 100644 --- a/lang/en/filament.php +++ b/lang/en/filament.php @@ -21,34 +21,38 @@ ], 'resources' => [ 'anidb' => [ - 'help' => 'Use the Manami Project Anime Offline Database hosted by yuna.moe to find an AniDB mapping from a MAL, Anilist or Kitsu Resource', + 'help' => 'Use LiveChart to find an AniDB mapping', 'name' => 'Backfill AniDB Resource', ], 'anilist' => [ - 'help' => 'Use the MAL, Kitsu or AniDB Resource to find an Anilist mapping', + 'help' => 'Use LiveChart to find an AniList mapping', 'name' => 'Backfill Anilist Resource', ], + 'anime_planet' => [ + 'help' => 'Use LiveChart to find an Anime Planet mapping', + 'name' => 'Backfill Anime Planet Resource', + ], 'ann' => [ - 'help' => 'Use the Kitsu resource to find an ANN mapping', + 'help' => 'Use LiveChart to find an Anime News Network mapping', 'name' => 'Backfill ANN Resource', ], 'kitsu' => [ - 'help' => 'Use the Kitsu API to find a mapping from a MAL, Anilist, AniDB or ANN Resource', + 'help' => 'Use LiveChart to find a Kitsu mapping', 'name' => 'Backfill Kitsu Resource', ], 'mal' => [ - 'help' => 'Use the Kitsu, Anilist or AniDB Resource to find a MAL mapping', + 'help' => 'Use LiveChart to find a MyAnimeList mapping', 'name' => 'Backfill MyAnimeList Resource', ], 'external_links' => [ - 'help' => 'Use Anilist Resource to find other resources as Official Sites and Streamings', + 'help' => 'Use Anilist Resource to find other resources like Official Sites and Streamings', 'name' => 'Backfill Other Resources' ], 'name' => 'Backfill Resources', ], 'studios' => [ 'anime' => [ - 'help' => 'Use the MAL, Anilist or Kitsu Resource to map Anime Studios', + 'help' => 'Use the MAL Resource to map Anime Studios', 'name' => 'Backfill Anime Studios', ], 'name' => 'Backfill Studios', @@ -214,6 +218,9 @@ 'kitsu' => [ 'help' => 'Ex: https://kitsu.io/anime/hibike-euphonium-2', ], + 'livechart' => [ + 'help' => 'Ex: https://www.livechart.me/anime/11873', + ], 'mal' => [ 'help' => 'Ex: https://myanimelist.net/anime/31988, https://myanimelist.net/people/11030', ], diff --git a/phpstan.neon b/phpstan.neon index 311aac22f..c343da636 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -34,3 +34,9 @@ parameters: - message: '#Call to an undefined method App\\Filament\\BulkActions\\.*::getRecord\(\).#' path: app/Concerns/Filament/Actions/HasActionLogs.php + - + message: '#Call to an undefined method App\\Models\\BaseModel::resources\(\)#' + path: app/Actions/Models/BackfillWikiAction.php + - + message: '#Call to an undefined method App\\Models\\BaseModel::images\(\)#' + path: app/Actions/Models/BackfillWikiAction.php diff --git a/tests/Feature/Actions/Models/Wiki/Anime/Image/BackfillLargeCoverImageTest.php b/tests/Feature/Actions/Models/Wiki/Anime/Image/BackfillLargeCoverImageTest.php deleted file mode 100644 index 5f9e40e79..000000000 --- a/tests/Feature/Actions/Models/Wiki/Anime/Image/BackfillLargeCoverImageTest.php +++ /dev/null @@ -1,199 +0,0 @@ -createOne([ - Image::ATTRIBUTE_FACET => ImageFacet::COVER_LARGE->value, - ]); - - $anime = Anime::factory() - ->hasAttached($image) - ->createOne(); - - $action = new BackfillLargeCoverImageAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::SKIPPED === $result->getStatus()); - static::assertDatabaseCount(Image::class, 1); - static::assertEmpty(Storage::disk(Config::get('image.disk'))->allFiles()); - Http::assertNothingSent(); - } - - /** - * The Backfill Large Cover Image Action shall fail if the Anime has no Resources. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoResource(): void - { - Storage::fake(Config::get('image.disk')); - - $anime = Anime::factory()->createOne(); - - $action = new BackfillLargeCoverImageAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(Image::class, 0); - static::assertEmpty(Storage::disk(Config::get('image.disk'))->allFiles()); - Http::assertNothingSent(); - } - - /** - * The Backfill Large Cover Image Action shall fail if the Anime has no Anilist Resource. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoAnilistResource(): void - { - Storage::fake(Config::get('image.disk')); - - $site = null; - - while ($site === null) { - $siteCandidate = Arr::random(ResourceSite::cases()); - if (ResourceSite::ANILIST !== $siteCandidate) { - $site = $siteCandidate; - } - } - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => $site->value, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillLargeCoverImageAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(Image::class, 0); - static::assertEmpty(Storage::disk(Config::get('image.disk'))->allFiles()); - Http::assertNothingSent(); - } - - /** - * The Backfill Large Cover Image Action shall fail if the Anilist request is not of the expected structure. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenBadAnilistResponse(): void - { - Storage::fake(Config::get('image.disk')); - - Http::fake([ - 'https://graphql.anilist.co' => Http::response([ - $this->faker->word() => $this->faker->word(), - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::ANILIST, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillLargeCoverImageAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(Image::class, 0); - static::assertEmpty(Storage::disk(Config::get('image.disk'))->allFiles()); - Http::assertSentCount(1); - } - - /** - * The Backfill Large Cover Image Action shall pass if the image can be retrieved. - * - * @return void - * - * @throws Exception - */ - public function testPassed(): void - { - Storage::fake(Config::get('image.disk')); - - $file = File::fake()->image($this->faker->word().'.jpg'); - - Http::fake([ - 'https://graphql.anilist.co' => Http::response([ - 'data' => [ - 'Media' => [ - 'coverImage' => [ - 'extraLarge' => "https://s4.anilist.co/file/anilistcdn/media/anime/cover/extraLarge/{$file->getBasename()}", - ], - ], - ], - ]), - 'https://s4.anilist.co/*' => Http::response($file->getContent()), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::ANILIST, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillLargeCoverImageAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::PASSED === $result->getStatus()); - static::assertDatabaseCount(Image::class, 1); - static::assertTrue($anime->images()->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_LARGE->value)->exists()); - static::assertCount(1, Storage::disk(Config::get('image.disk'))->allFiles()); - Http::assertSentCount(2); - } -} diff --git a/tests/Feature/Actions/Models/Wiki/Anime/Image/BackfillSmallCoverImageTest.php b/tests/Feature/Actions/Models/Wiki/Anime/Image/BackfillSmallCoverImageTest.php deleted file mode 100644 index 6720d4185..000000000 --- a/tests/Feature/Actions/Models/Wiki/Anime/Image/BackfillSmallCoverImageTest.php +++ /dev/null @@ -1,199 +0,0 @@ -createOne([ - Image::ATTRIBUTE_FACET => ImageFacet::COVER_SMALL->value, - ]); - - $anime = Anime::factory() - ->hasAttached($image) - ->createOne(); - - $action = new BackfillSmallCoverImageAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::SKIPPED === $result->getStatus()); - static::assertDatabaseCount(Image::class, 1); - static::assertEmpty(Storage::disk(Config::get('image.disk'))->allFiles()); - Http::assertNothingSent(); - } - - /** - * The Backfill Small Cover Image Action shall fail if the Anime has no Resources. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoResource(): void - { - Storage::fake(Config::get('image.disk')); - - $anime = Anime::factory()->createOne(); - - $action = new BackfillSmallCoverImageAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(Image::class, 0); - static::assertEmpty(Storage::disk(Config::get('image.disk'))->allFiles()); - Http::assertNothingSent(); - } - - /** - * The Backfill Small Cover Image Action shall fail if the Anime has no Anilist Resource. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoAnilistResource(): void - { - Storage::fake(Config::get('image.disk')); - - $site = null; - - while ($site === null) { - $siteCandidate = Arr::random(ResourceSite::cases()); - if (ResourceSite::ANILIST !== $siteCandidate) { - $site = $siteCandidate; - } - } - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => $site->value, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillSmallCoverImageAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(Image::class, 0); - static::assertEmpty(Storage::disk(Config::get('image.disk'))->allFiles()); - Http::assertNothingSent(); - } - - /** - * The Backfill Small Cover Image Action shall fail if the Anilist request is not of the expected structure. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenBadAnilistResponse(): void - { - Storage::fake(Config::get('image.disk')); - - Http::fake([ - 'https://graphql.anilist.co' => Http::response([ - $this->faker->word() => $this->faker->word(), - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::ANILIST, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillSmallCoverImageAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(Image::class, 0); - static::assertEmpty(Storage::disk(Config::get('image.disk'))->allFiles()); - Http::assertSentCount(1); - } - - /** - * The Backfill Small Cover Image Action shall pass if the image can be retrieved. - * - * @return void - * - * @throws Exception - */ - public function testPassed(): void - { - Storage::fake(Config::get('image.disk')); - - $file = File::fake()->image($this->faker->word().'.jpg'); - - Http::fake([ - 'https://graphql.anilist.co' => Http::response([ - 'data' => [ - 'Media' => [ - 'coverImage' => [ - 'medium' => "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/{$file->getBasename()}", - ], - ], - ], - ]), - 'https://s4.anilist.co/*' => Http::response($file->getContent()), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::ANILIST, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillSmallCoverImageAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::PASSED === $result->getStatus()); - static::assertDatabaseCount(Image::class, 1); - static::assertTrue($anime->images()->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_SMALL->value)->exists()); - static::assertCount(1, Storage::disk(Config::get('image.disk'))->allFiles()); - Http::assertSentCount(2); - } -} diff --git a/tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillAnidbResourceTest.php b/tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillAnidbResourceTest.php deleted file mode 100644 index 2c9be4336..000000000 --- a/tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillAnidbResourceTest.php +++ /dev/null @@ -1,191 +0,0 @@ -createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::ANIDB, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnidbResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::SKIPPED === $result->getStatus()); - static::assertDatabaseCount(ExternalResource::class, 1); - Http::assertNothingSent(); - } - - /** - * The Backfill AniDB Action shall fail if the Anime has no Resources. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoResource(): void - { - $anime = Anime::factory()->createOne(); - - $action = new BackfillAnidbResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(ExternalResource::class, 0); - Http::assertNothingSent(); - } - - /** - * The Backfill AniDB Action shall fail if the Yuna API does not return a match. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoMatch(): void - { - Http::fake([ - 'https://relations.yuna.moe/api/ids*' => Http::response(), - ]); - - $site = Arr::random([ - ResourceSite::MAL, - ResourceSite::ANILIST, - ResourceSite::KITSU, - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => $site, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnidbResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(ExternalResource::class, 1); - Http::assertSentCount(1); - } - - /** - * The Backfill AniDB Action shall pass if the Yuna API returns a match. - * - * @return void - * - * @throws Exception - */ - public function testPassed(): void - { - Http::fake([ - 'https://relations.yuna.moe/api/ids*' => Http::response([ - 'anidb' => $this->faker->randomDigitNotNull(), - ]), - ]); - - $site = Arr::random([ - ResourceSite::MAL, - ResourceSite::ANILIST, - ResourceSite::KITSU, - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => $site, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnidbResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::PASSED === $result->getStatus()); - static::assertDatabaseCount(ExternalResource::class, 2); - static::assertTrue($anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB)->exists()); - Http::assertSentCount(1); - } - - /** - * The Backfill AniDB Action shall get an existing resource if the site and id match. - * - * @return void - * - * @throws Exception - */ - public function testGetsExistingResource(): void - { - $anidbId = $this->faker->randomDigitNotNull(); - - ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::ANIDB, - ExternalResource::ATTRIBUTE_EXTERNAL_ID => $anidbId, - ExternalResource::ATTRIBUTE_LINK => ResourceSite::ANIDB->formatResourceLink(Anime::class, $anidbId), - - ]); - - Http::fake([ - 'https://relations.yuna.moe/api/ids*' => Http::response([ - 'anidb' => $anidbId, - ]), - ]); - - $site = Arr::random([ - ResourceSite::MAL, - ResourceSite::ANILIST, - ResourceSite::KITSU, - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => $site, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnidbResourceAction($anime); - - $action->handle(); - - static::assertDatabaseCount(ExternalResource::class, 2); - Http::assertSentCount(1); - } -} diff --git a/tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillAnilistResourceTest.php b/tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillAnilistResourceTest.php deleted file mode 100644 index a8cacc9fa..000000000 --- a/tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillAnilistResourceTest.php +++ /dev/null @@ -1,344 +0,0 @@ -createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::ANILIST, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnilistResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::SKIPPED === $result->getStatus()); - static::assertDatabaseCount(ExternalResource::class, 1); - Http::assertNothingSent(); - } - - /** - * The Backfill Anilist Action shall fail if the Anime has no Resources. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoResource(): void - { - $anime = Anime::factory()->createOne(); - - $action = new BackfillAnilistResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(ExternalResource::class, 0); - Http::assertNothingSent(); - } - - /** - * The Backfill Anilist Action shall fail if the Anilist API returns no match for a MAL ID. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoMalMatch(): void - { - Http::fake([ - 'https://graphql.anilist.co' => Http::response([ - $this->faker->word() => $this->faker->word(), - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::MAL, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnilistResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(ExternalResource::class, 1); - Http::assertSentCount(1); - } - - /** - * The Backfill AniDB Action shall pass if the Anilist API returns a match for a MAL ID. - * - * @return void - * - * @throws Exception - */ - public function testMalPassed(): void - { - Http::fake([ - 'https://graphql.anilist.co' => Http::response([ - 'data' => [ - 'Media' => [ - 'id' => $this->faker->randomDigitNotNull(), - ], - ], - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::MAL, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnilistResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::PASSED === $result->getStatus()); - static::assertDatabaseCount(ExternalResource::class, 2); - static::assertTrue($anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST)->exists()); - Http::assertSentCount(1); - } - - /** - * The Backfill Anilist Action shall fail if the Kitsu API returns no Anilist mapping. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoKitsuMatch(): void - { - Http::fake([ - 'https://kitsu.io/api/graphql' => Http::response([ - $this->faker->word() => $this->faker->word(), - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::KITSU, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnilistResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(ExternalResource::class, 1); - Http::assertSentCount(1); - } - - /** - * The Backfill Anilist Action shall pass if the Kitsu API returns an Anilist mapping. - * - * @return void - * - * @throws Exception - */ - public function testKitsuPassed(): void - { - Http::fake([ - 'https://kitsu.io/api/graphql' => Http::response([ - 'data' => [ - 'findAnimeById' => [ - 'mappings' => [ - 'nodes' => [ - [ - 'externalSite' => 'ANILIST_ANIME', - 'externalId' => $this->faker->randomDigitNotNull(), - ], - ], - ], - ], - ], - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::KITSU, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnilistResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::PASSED === $result->getStatus()); - static::assertDatabaseCount(ExternalResource::class, 2); - static::assertTrue($anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST)->exists()); - Http::assertSentCount(1); - } - - /** - * The Backfill Anilist Action shall fail if the Yuna API returns no Anilist mapping for an AniDB ID. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoAnidbMatch(): void - { - Http::fake([ - 'https://relations.yuna.moe/api/ids*' => Http::response(), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::ANIDB, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnilistResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(ExternalResource::class, 1); - Http::assertSentCount(1); - } - - /** - * The Backfill Anilist Action shall pass if the Yuna API returns an Anilist mapping for an AniDB ID. - * - * @return void - * - * @throws Exception - */ - public function testAnidbPassed(): void - { - Http::fake([ - 'https://relations.yuna.moe/api/ids*' => Http::response([ - 'anilist' => $this->faker->randomDigitNotNull(), - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::ANIDB, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnilistResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::PASSED === $result->getStatus()); - static::assertDatabaseCount(ExternalResource::class, 2); - static::assertTrue($anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST)->exists()); - Http::assertSentCount(1); - } - - /** - * The Backfill Anilist Action shall get an existing resource if the site and id match. - * - * @return void - * - * @throws Exception - */ - public function testGetsExistingResource(): void - { - $anilistId = $this->faker->randomDigitNotNull(); - - ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::ANILIST, - ExternalResource::ATTRIBUTE_EXTERNAL_ID => $anilistId, - ExternalResource::ATTRIBUTE_LINK => ResourceSite::ANILIST->formatResourceLink(Anime::class, $anilistId), - ]); - - Http::fake([ - 'https://graphql.anilist.co' => Http::response([ - 'data' => [ - 'Media' => [ - 'id' => $anilistId, - ], - ], - ]), - 'https://kitsu.io/api/graphql' => Http::response([ - 'data' => [ - 'findAnimeById' => [ - 'mappings' => [ - 'nodes' => [ - [ - 'externalSite' => 'ANILIST_ANIME', - 'externalId' => $anilistId, - ], - ], - ], - ], - ], - ]), - 'https://relations.yuna.moe/api/ids*' => Http::response([ - 'anilist' => $anilistId, - ]), - ]); - - $site = Arr::random([ - ResourceSite::MAL, - ResourceSite::KITSU, - ResourceSite::ANIDB, - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => $site, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnilistResourceAction($anime); - - $action->handle(); - - static::assertDatabaseCount(ExternalResource::class, 2); - Http::assertSentCount(1); - } -} diff --git a/tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillAnnResourceTest.php b/tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillAnnResourceTest.php deleted file mode 100644 index 4f913d837..000000000 --- a/tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillAnnResourceTest.php +++ /dev/null @@ -1,195 +0,0 @@ -createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::ANN, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnnResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::SKIPPED === $result->getStatus()); - static::assertDatabaseCount(ExternalResource::class, 1); - Http::assertNothingSent(); - } - - /** - * The Backfill ANN Action shall fail if the Anime has no Resources. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoResource(): void - { - $anime = Anime::factory()->createOne(); - - $action = new BackfillAnnResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(ExternalResource::class, 0); - Http::assertNothingSent(); - } - - /** - * The Backfill ANN Action shall fail if the Kitsu API returns no ANN mapping. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoKitsuMatch(): void - { - Http::fake([ - 'https://kitsu.io/api/graphql' => Http::response([ - $this->faker->word() => $this->faker->word(), - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::KITSU, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnnResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(ExternalResource::class, 1); - Http::assertSentCount(1); - } - - /** - * The Backfill ANN Action shall pass if the Kitsu API returns an ANN mapping. - * - * @return void - * - * @throws Exception - */ - public function testKitsuPassed(): void - { - Http::fake([ - 'https://kitsu.io/api/graphql' => Http::response([ - 'data' => [ - 'findAnimeById' => [ - 'mappings' => [ - 'nodes' => [ - [ - 'externalSite' => 'ANIMENEWSNETWORK', - 'externalId' => $this->faker->randomDigitNotNull(), - ], - ], - ], - ], - ], - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::KITSU, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnnResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::PASSED === $result->getStatus()); - static::assertDatabaseCount(ExternalResource::class, 2); - static::assertTrue($anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANN)->exists()); - Http::assertSentCount(1); - } - - /** - * The Backfill ANN Action shall get an existing resource if the site and id match. - * - * @return void - * - * @throws Exception - */ - public function testGetsExistingResource(): void - { - $annId = $this->faker->randomDigitNotNull(); - - ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::ANN, - ExternalResource::ATTRIBUTE_EXTERNAL_ID => $annId, - ExternalResource::ATTRIBUTE_LINK => ResourceSite::ANN->formatResourceLink(Anime::class, $annId), - ]); - - Http::fake([ - 'https://kitsu.io/api/graphql' => Http::response([ - 'data' => [ - 'findAnimeById' => [ - 'mappings' => [ - 'nodes' => [ - [ - 'externalSite' => 'ANIMENEWSNETWORK', - 'externalId' => $annId, - ], - ], - ], - ], - ], - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::KITSU, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnnResourceAction($anime); - - $action->handle(); - - static::assertDatabaseCount(ExternalResource::class, 2); - Http::assertSentCount(1); - } -} diff --git a/tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillKitsuResourceTest.php b/tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillKitsuResourceTest.php deleted file mode 100644 index c7caa968d..000000000 --- a/tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillKitsuResourceTest.php +++ /dev/null @@ -1,210 +0,0 @@ -createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::KITSU, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillKitsuResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::SKIPPED === $result->getStatus()); - static::assertDatabaseCount(ExternalResource::class, 1); - Http::assertNothingSent(); - } - - /** - * The Backfill Kitsu Action shall fail if the Anime has no Resources. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoResource(): void - { - $anime = Anime::factory()->createOne(); - - $action = new BackfillKitsuResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(ExternalResource::class, 0); - - Http::assertNothingSent(); - } - - /** - * The Backfill Kitsu Action shall fail if the Kitsu API does not return a match. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoMatch(): void - { - Http::fake([ - 'https://kitsu.io/api/graphql' => Http::response([ - $this->faker->word() => $this->faker->word(), - ]), - ]); - - $site = Arr::random([ - ResourceSite::MAL, - ResourceSite::ANILIST, - ResourceSite::ANIDB, - ResourceSite::ANN, - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => $site, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillKitsuResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(ExternalResource::class, 1); - Http::assertSentCount(1); - } - - /** - * The Backfill Kitsu Action shall pass if the Kitsu API returns a match. - * - * @return void - * - * @throws Exception - */ - public function testPassed(): void - { - Http::fake([ - 'https://kitsu.io/api/graphql' => Http::response([ - 'data' => [ - 'lookupMapping' => [ - 'id' => $this->faker->randomDigitNotNull(), - 'slug' => $this->faker->slug(), - ], - ], - ]), - ]); - - $site = Arr::random([ - ResourceSite::MAL, - ResourceSite::ANILIST, - ResourceSite::ANIDB, - ResourceSite::ANN, - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => $site, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillKitsuResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::PASSED === $result->getStatus()); - static::assertDatabaseCount(ExternalResource::class, 2); - static::assertTrue($anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU)->exists()); - Http::assertSentCount(1); - } - - /** - * The Backfill Kitsu Action shall get an existing resource if the site and id match. - * - * @return void - * - * @throws Exception - */ - public function testGetsExistingResource(): void - { - $kitsuId = $this->faker->randomDigitNotNull(); - $kitsuSlug = $this->faker->slug(); - - ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::KITSU, - ExternalResource::ATTRIBUTE_EXTERNAL_ID => $kitsuId, - ExternalResource::ATTRIBUTE_LINK => ResourceSite::KITSU->formatResourceLink(Anime::class, $kitsuId, $kitsuSlug), - - ]); - - Http::fake([ - 'https://kitsu.io/api/graphql' => Http::response([ - 'data' => [ - 'lookupMapping' => [ - 'id' => $kitsuId, - 'slug' => $kitsuSlug, - ], - ], - ]), - ]); - - $site = Arr::random([ - ResourceSite::MAL, - ResourceSite::ANILIST, - ResourceSite::ANIDB, - ResourceSite::ANN, - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => $site, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillKitsuResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::PASSED === $result->getStatus()); - static::assertDatabaseCount(ExternalResource::class, 2); - static::assertTrue($anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU)->exists()); - Http::assertSentCount(1); - } -} diff --git a/tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillMalResourceTest.php b/tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillMalResourceTest.php deleted file mode 100644 index d57bed7ce..000000000 --- a/tests/Feature/Actions/Models/Wiki/Anime/Resource/BackfillMalResourceTest.php +++ /dev/null @@ -1,344 +0,0 @@ -createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::MAL, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillMalResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::SKIPPED === $result->getStatus()); - static::assertDatabaseCount(ExternalResource::class, 1); - Http::assertNothingSent(); - } - - /** - * The Backfill MAL Action shall fail if the Anime has no Resources. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoResource(): void - { - $anime = Anime::factory()->createOne(); - - $action = new BackfillMalResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(ExternalResource::class, 0); - Http::assertNothingSent(); - } - - /** - * The Backfill MAL Action shall fail if the Kitsu API returns no MAL mapping. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoKitsuMatch(): void - { - Http::fake([ - 'https://kitsu.io/api/graphql' => Http::response([ - $this->faker->word() => $this->faker->word(), - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::KITSU, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillMalResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(ExternalResource::class, 1); - Http::assertSentCount(1); - } - - /** - * The Backfill MAL Action shall pass if the Kitsu API returns an MAL mapping. - * - * @return void - * - * @throws Exception - */ - public function testKitsuPassed(): void - { - Http::fake([ - 'https://kitsu.io/api/graphql' => Http::response([ - 'data' => [ - 'findAnimeById' => [ - 'mappings' => [ - 'nodes' => [ - [ - 'externalSite' => 'MYANIMELIST_ANIME', - 'externalId' => $this->faker->randomDigitNotNull(), - ], - ], - ], - ], - ], - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::KITSU, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillMalResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::PASSED === $result->getStatus()); - static::assertDatabaseCount(ExternalResource::class, 2); - static::assertTrue($anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL)->exists()); - Http::assertSentCount(1); - } - - /** - * The Backfill MAL Action shall fail if the Anilist API returns no MAL mapping. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoAnilistMatch(): void - { - Http::fake([ - 'https://graphql.anilist.co' => Http::response([ - $this->faker->word() => $this->faker->word(), - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::ANILIST, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillMalResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(ExternalResource::class, 1); - Http::assertSentCount(1); - } - - /** - * The Backfill MAL Action shall pass if the Anilist API returns a match for a MAL ID. - * - * @return void - * - * @throws Exception - */ - public function testAnilistPassed(): void - { - Http::fake([ - 'https://graphql.anilist.co' => Http::response([ - 'data' => [ - 'Media' => [ - 'idMal' => $this->faker->randomDigitNotNull(), - ], - ], - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::ANILIST, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillMalResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::PASSED === $result->getStatus()); - static::assertDatabaseCount(ExternalResource::class, 2); - static::assertTrue($anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL)->exists()); - Http::assertSentCount(1); - } - - /** - * The Backfill MAL Action shall fail if the Yuna API returns no MAL mapping for an AniDB ID. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoAnidbMatch(): void - { - Http::fake([ - 'https://relations.yuna.moe/api/ids*' => Http::response(), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::ANIDB, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillMalResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(ExternalResource::class, 1); - Http::assertSentCount(1); - } - - /** - * The Backfill MAL Action shall pass if the Yuna API returns a MAL mapping for an AniDB ID. - * - * @return void - * - * @throws Exception - */ - public function testAnidbPassed(): void - { - Http::fake([ - 'https://relations.yuna.moe/api/ids*' => Http::response([ - 'myanimelist' => $this->faker->randomDigitNotNull(), - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::ANIDB, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillMalResourceAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::PASSED === $result->getStatus()); - static::assertDatabaseCount(ExternalResource::class, 2); - static::assertTrue($anime->resources()->where(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL)->exists()); - Http::assertSentCount(1); - } - - /** - * The Backfill MAL Action shall get an existing resource if the site and id match. - * - * @return void - * - * @throws Exception - */ - public function testGetsExistingResource(): void - { - $malId = $this->faker->randomDigitNotNull(); - - ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::MAL, - ExternalResource::ATTRIBUTE_EXTERNAL_ID => $malId, - ExternalResource::ATTRIBUTE_LINK => ResourceSite::MAL->formatResourceLink(Anime::class, $malId), - ]); - - Http::fake([ - 'https://kitsu.io/api/graphql' => Http::response([ - 'data' => [ - 'findAnimeById' => [ - 'mappings' => [ - 'nodes' => [ - [ - 'externalSite' => 'MYANIMELIST_ANIME', - 'externalId' => $malId, - ], - ], - ], - ], - ], - ]), - 'https://graphql.anilist.co' => Http::response([ - 'data' => [ - 'Media' => [ - 'idMal' => $malId, - ], - ], - ]), - 'https://relations.yuna.moe/api/ids*' => Http::response([ - 'myanimelist' => $malId, - ]), - ]); - - $site = Arr::random([ - ResourceSite::KITSU, - ResourceSite::ANILIST, - ResourceSite::ANIDB, - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => $site, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillMalResourceAction($anime); - - $action->handle(); - - static::assertDatabaseCount(ExternalResource::class, 2); - Http::assertSentCount(1); - } -} diff --git a/tests/Feature/Actions/Models/Wiki/Anime/Studio/BackfillAnimeStudiosTest.php b/tests/Feature/Actions/Models/Wiki/Anime/Studio/BackfillAnimeStudiosTest.php deleted file mode 100644 index 8b5191be7..000000000 --- a/tests/Feature/Actions/Models/Wiki/Anime/Studio/BackfillAnimeStudiosTest.php +++ /dev/null @@ -1,422 +0,0 @@ -faker->randomDigitNotNull(); - - $anime = Anime::factory() - ->has(Studio::factory()->count($studiosCount)) - ->createOne(); - - $action = new BackfillAnimeStudiosAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::SKIPPED === $result->getStatus()); - static::assertDatabaseCount(Studio::class, $studiosCount); - Http::assertNothingSent(); - } - - /** - * The Backfill Studios Action shall fail if the Anime has no Resources. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoResource(): void - { - $anime = Anime::factory()->createOne(); - - $action = new BackfillAnimeStudiosAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(Studio::class, 0); - Http::assertNothingSent(); - } - - /** - * The Backfill Studios Action shall fail if the MAL API returns no studios. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoMalMatch(): void - { - Http::fake([ - 'https://api.myanimelist.net/v2/anime/*' => Http::response([ - $this->faker->word() => $this->faker->word(), - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::MAL, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnimeStudiosAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(Studio::class, 0); - Http::assertSentCount(1); - } - - /** - * The Backfill Studios Action shall pass if the MAL API returns studios. - * - * @return void - * - * @throws Exception - */ - public function testMalPassed(): void - { - $studioCount = $this->faker->randomDigitNotNull(); - - $studios = Collection::times( - $studioCount, - fn (int $time) => ['id' => $time, 'name' => $this->faker->unique()->word()] - ); - - Http::fake([ - 'https://api.myanimelist.net/v2/anime/*' => Http::response([ - 'studios' => $studios->toArray(), - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::MAL, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnimeStudiosAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::PASSED === $result->getStatus()); - static::assertDatabaseCount(Studio::class, $studioCount); - static::assertEquals($studioCount, $anime->studios()->count()); - Http::assertSentCount(1); - } - - /** - * The Backfill Studios Action shall fail if the Anilist API returns no studios. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoAnilistMatch(): void - { - Http::fake([ - 'https://graphql.anilist.co' => Http::response([ - $this->faker->word() => $this->faker->word(), - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::ANILIST, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnimeStudiosAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(Studio::class, 0); - Http::assertSentCount(1); - } - - /** - * The Backfill Studios Action shall pass if the Anilist API returns studios. - * - * @return void - * - * @throws Exception - */ - public function testAnilistPassed(): void - { - $studioCount = $this->faker->randomDigitNotNull(); - - $studios = Collection::times( - $studioCount, - fn (int $time) => ['id' => $time, 'name' => $this->faker->unique()->word()] - ); - - Http::fake([ - 'https://graphql.anilist.co' => Http::response([ - 'data' => [ - 'Media' => [ - 'studios' => [ - 'nodes' => $studios, - ], - ], - ], - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::ANILIST, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnimeStudiosAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::PASSED === $result->getStatus()); - static::assertDatabaseCount(Studio::class, $studioCount); - static::assertEquals($studioCount, $anime->studios()->count()); - Http::assertSentCount(1); - } - - /** - * The Backfill Studios Action shall fail if the Kitsu API returns no studios. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoKitsuMatch(): void - { - Http::fake([ - 'https://kitsu.io/api/graphql' => Http::response([ - $this->faker->word() => $this->faker->word(), - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::KITSU, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnimeStudiosAction($anime); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(Studio::class, 0); - Http::assertSentCount(1); - } - - /** - * The Backfill Studios Action shall pass if the Kitsu API returns studios. - * - * @return void - * - * @throws Exception - */ - public function testKitsuPassed(): void - { - $studioCount = $this->faker->randomDigitNotNull(); - - $studios = Collection::times( - $studioCount, - fn () => [ - 'role' => 'STUDIO', - 'company' => [ - 'name' => $this->faker->unique()->word(), - ], - ], - ); - - Http::fake([ - 'https://kitsu.io/api/graphql' => Http::response([ - 'data' => [ - 'findAnimeById' => [ - 'productions' => [ - 'nodes' => $studios, - ], - ], - ], - ]), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::KITSU, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnimeStudiosAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::PASSED === $result->getStatus()); - static::assertDatabaseCount(Studio::class, $studioCount); - static::assertEquals($studioCount, $anime->studios()->count()); - Http::assertSentCount(1); - } - - /** - * The Backfill Studios Action shall create a resource if the site provides an ID. - * - * @return void - * - * @throws Exception - */ - public function testCreatesStudioResource(): void - { - $studioCount = $this->faker->randomDigitNotNull(); - - $studios = Collection::times( - $studioCount, - fn (int $time) => ['id' => $time, 'name' => $this->faker->unique()->word()] - ); - - Http::fake([ - 'https://api.myanimelist.net/v2/anime/*' => Http::response([ - 'studios' => $studios->toArray(), - ]), - 'https://graphql.anilist.co' => Http::response([ - 'data' => [ - 'Media' => [ - 'studios' => [ - 'nodes' => $studios, - ], - ], - ], - ]), - ]); - - $site = Arr::random([ - ResourceSite::MAL, - ResourceSite::ANILIST, - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => $site, - ]); - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnimeStudiosAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::PASSED === $result->getStatus()); - static::assertDatabaseCount(ExternalResource::class, $studioCount + 1); - Http::assertSentCount(1); - } - - /** - * The Backfill Studios Action shall create a resource if the site provides an ID. - * - * @return void - * - * @throws Exception - */ - public function testGetsExistingResource(): void - { - $studioCount = $this->faker->randomDigitNotNull(); - - $studios = Collection::times( - $studioCount, - fn (int $time) => ['id' => $time, 'name' => $this->faker->unique()->word()] - ); - - Http::fake([ - 'https://api.myanimelist.net/v2/anime/*' => Http::response([ - 'studios' => $studios->toArray(), - ]), - 'https://graphql.anilist.co' => Http::response([ - 'data' => [ - 'Media' => [ - 'studios' => [ - 'nodes' => $studios, - ], - ], - ], - ]), - ]); - - $site = Arr::random([ - ResourceSite::MAL, - ResourceSite::ANILIST, - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => $site->value, - ]); - - foreach ($studios as $studio) { - $id = Arr::get($studio, 'id'); - $slug = Str::slug(Arr::get($studio, 'name')); - $link = $site->formatResourceLink(Studio::class, $id, $slug); - - ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => $site, - ExternalResource::ATTRIBUTE_EXTERNAL_ID => $id, - ExternalResource::ATTRIBUTE_LINK => $link, - ]); - } - - $anime = Anime::factory() - ->hasAttached($resource, [], Anime::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillAnimeStudiosAction($anime); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::PASSED === $result->getStatus()); - static::assertDatabaseCount(ExternalResource::class, $studioCount + 1); - Http::assertSentCount(1); - } -} diff --git a/tests/Feature/Actions/Models/Wiki/Studio/Image/BackfillLargeCoverImageTest.php b/tests/Feature/Actions/Models/Wiki/Studio/Image/BackfillLargeCoverImageTest.php deleted file mode 100644 index c024a92ff..000000000 --- a/tests/Feature/Actions/Models/Wiki/Studio/Image/BackfillLargeCoverImageTest.php +++ /dev/null @@ -1,155 +0,0 @@ -createOne([ - Image::ATTRIBUTE_FACET => ImageFacet::COVER_LARGE->value, - ]); - - $studio = Studio::factory() - ->hasAttached($image) - ->createOne(); - - $action = new BackfillLargeCoverImageAction($studio); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::SKIPPED === $result->getStatus()); - static::assertDatabaseCount(Image::class, 1); - static::assertEmpty(Storage::disk(Config::get('image.disk'))->allFiles()); - Http::assertNothingSent(); - } - - /** - * The Backfill Large Cover Image Action shall fail if the Studio has no Resources. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoResource(): void - { - Storage::fake(Config::get('image.disk')); - - $studio = Studio::factory()->createOne(); - - $action = new BackfillLargeCoverImageAction($studio); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(Image::class, 0); - static::assertEmpty(Storage::disk(Config::get('image.disk'))->allFiles()); - Http::assertNothingSent(); - } - - /** - * The Backfill Large Cover Image Action shall fail if the Studio has no MAL Resource. - * - * @return void - * - * @throws Exception - */ - public function testFailedWhenNoMalResource(): void - { - Storage::fake(Config::get('image.disk')); - - $site = null; - - while ($site === null) { - $siteCandidate = Arr::random(ResourceSite::cases()); - if (ResourceSite::MAL !== $siteCandidate) { - $site = $siteCandidate; - } - } - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => $site->value, - ]); - - $studio = Studio::factory() - ->hasAttached($resource, [], Studio::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillLargeCoverImageAction($studio); - - $result = $action->handle(); - - static::assertTrue($result->hasFailed()); - static::assertDatabaseCount(Image::class, 0); - static::assertEmpty(Storage::disk(Config::get('image.disk'))->allFiles()); - Http::assertNothingSent(); - } - - /** - * The Backfill Large Cover Image Action shall pass if the image can be retrieved. - * - * @return void - * - * @throws Exception - */ - public function testPassed(): void - { - Storage::fake(Config::get('image.disk')); - - $file = File::fake()->image($this->faker->word().'.jpg'); - - Http::fake([ - 'https://cdn.myanimelist.net/images/company/*' => Http::response($file->getContent()), - ]); - - $resource = ExternalResource::factory()->createOne([ - ExternalResource::ATTRIBUTE_SITE => ResourceSite::MAL, - ]); - - $studio = Studio::factory() - ->hasAttached($resource, [], Studio::RELATION_RESOURCES) - ->createOne(); - - $action = new BackfillLargeCoverImageAction($studio); - - $result = $action->handle(); - - static::assertTrue(ActionStatus::PASSED === $result->getStatus()); - static::assertDatabaseCount(Image::class, 1); - static::assertTrue($studio->images()->where(Image::ATTRIBUTE_FACET, ImageFacet::COVER_LARGE->value)->exists()); - static::assertCount(1, Storage::disk(Config::get('image.disk'))->allFiles()); - Http::assertSentCount(1); - } -}