diff --git a/app/Actions/Models/Wiki/Anime/BackfillAnimeExternalLinksAnilistResourceAction.php b/app/Actions/Models/Wiki/Anime/BackfillAnimeExternalLinksAnilistResourceAction.php index de4a7825f..b99c4f46f 100644 --- a/app/Actions/Models/Wiki/Anime/BackfillAnimeExternalLinksAnilistResourceAction.php +++ b/app/Actions/Models/Wiki/Anime/BackfillAnimeExternalLinksAnilistResourceAction.php @@ -80,18 +80,19 @@ protected function getAvailableSites(): array protected function getOrCreateResource(mixed $externalLink): ExternalResource { $availableSites = $this->getAvailableSites(); - $url = $externalLink['url']; + /** @var ResourceSite $resourceSite */ $resourceSite = $availableSites[$externalLink['site']]; - $urlPattern = $this->getUrlPattern($resourceSite->value); + $url = $externalLink['url']; + $urlPattern = $resourceSite->getUrlPattern(); if (preg_match($urlPattern, $url, $matches)) { - $url = $resourceSite->formatAnimeResourceLink(intval($matches[1]), $matches[1]); + $url = $resourceSite->formatAnimeResourceLink(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 . "/") + ->orWhere(ExternalResource::ATTRIBUTE_LINK, $url . '/') ->first(); if ($resource === null) { @@ -101,7 +102,7 @@ protected function getOrCreateResource(mixed $externalLink): ExternalResource $resource = ExternalResource::query()->create([ ExternalResource::ATTRIBUTE_LINK => $url, ExternalResource::ATTRIBUTE_SITE => $resourceSite->value, - ExternalResource::ATTRIBUTE_EXTERNAL_ID => $resourceSite->parseIdFromLink($url) ?? null, + ExternalResource::ATTRIBUTE_EXTERNAL_ID => $resourceSite->parseIdFromLink($url), ]); } @@ -141,6 +142,11 @@ protected function getAnilistResource(): ?ExternalResource return null; } + /** + * Get the external links by AniList API. + * + * @return array|null + */ protected function getExternalLinksByAnilistResource(): ?array { $anilistResource = $this->getAnilistResource(); @@ -176,19 +182,4 @@ protected function getExternalLinksByAnilistResource(): ?array return null; } - - protected function getUrlPattern(int $resourceSite): string - { - $matches = [ - ResourceSite::TWITTER->value => '/^https:\/\/twitter\.com\/(\w+)/', - ResourceSite::CRUNCHYROLL->value => '/^https:\/\/www\.crunchyroll\.com\/series\/(\w+)/', - ResourceSite::HIDIVE->value => '/^https:\/\/www\.hidive\.com\/tv\/([\w-]+)/', - ResourceSite::NETFLIX->value => '/^https:\/\/www\.netflix\.com\/title\/(\d+)/', - ResourceSite::DISNEY_PLUS->value => '/^https:\/\/www\.disneyplus\.com\/series\/([\w-]+\/\w+)/', - ResourceSite::HULU->value => '/^https:\/\/www\.hulu\.com\/series\/([\w-]+)/', - ResourceSite::AMAZON_PRIME_VIDEO->value => '/^https:\/\/www\.primevideo\.com\/detail\/(\w+)/', - ]; - - return $matches[$resourceSite] ?? '/^$/'; - } } diff --git a/app/Actions/Models/Wiki/BackfillExternalLinksAnilistResourceAction.php b/app/Actions/Models/Wiki/BackfillExternalLinksAnilistResourceAction.php index a1c3e7f5f..bad5087be 100644 --- a/app/Actions/Models/Wiki/BackfillExternalLinksAnilistResourceAction.php +++ b/app/Actions/Models/Wiki/BackfillExternalLinksAnilistResourceAction.php @@ -46,7 +46,7 @@ public function handle(): ActionResult $language = $externalLink['language']; if (!in_array($site, array_keys($availableSites))) continue; - if (in_array($site, ['Official Site', 'Twitter']) && $language !== 'Japanese') 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(); diff --git a/app/Enums/Models/Wiki/ResourceSite.php b/app/Enums/Models/Wiki/ResourceSite.php index d80ada77e..f520308ef 100644 --- a/app/Enums/Models/Wiki/ResourceSite.php +++ b/app/Enums/Models/Wiki/ResourceSite.php @@ -146,7 +146,7 @@ protected static function parseAnimePlanetIdFromLink(string $link): ?string return null; } - + /** * Attempt to parse Kitsu ID from link. * @@ -188,9 +188,10 @@ protected static function parseKitsuIdFromLink(string $link): ?string * * @param int $id * @param string|null $slug + * @param string|null $type * @return string|null */ - public function formatAnimeResourceLink(int $id, ?string $slug = null): ?string + public function formatAnimeResourceLink(int $id, ?string $slug = null, ?string $type = null): ?string { return match ($this) { ResourceSite::TWITTER => "https://twitter.com/$slug", @@ -201,11 +202,11 @@ public function formatAnimeResourceLink(int $id, ?string $slug = null): ?string ResourceSite::KITSU => "https://kitsu.io/anime/$slug", ResourceSite::MAL => "https://myanimelist.net/anime/$id", ResourceSite::YOUTUBE => "https://www.youtube.com/@$slug", - ResourceSite::CRUNCHYROLL => "https://www.crunchyroll.com/series/$slug", - ResourceSite::HIDIVE => "https://www.hidive.com/tv/$slug", - ResourceSite::NETFLIX => "https://www.netflix.com/title/$id", - ResourceSite::DISNEY_PLUS => "https://www.disneyplus.com/series/$slug/$id", - ResourceSite::HULU => "https://www.hulu.com/series/$slug", + ResourceSite::CRUNCHYROLL => "https://www.crunchyroll.com/$type/$slug", + ResourceSite::HIDIVE => "https://www.hidive.com/$type/$slug", + ResourceSite::NETFLIX => "https://www.netflix.com/$type/$id", + ResourceSite::DISNEY_PLUS => "https://www.disneyplus.com/$type/$slug/$id", + ResourceSite::HULU => "https://www.hulu.com/$type/$slug", ResourceSite::AMAZON_PRIME_VIDEO => "https://www.primevideo.com/detail/$slug", default => null, }; @@ -271,4 +272,23 @@ public function formatStudioResourceLink(int $id, ?string $slug = null): ?string default => null, }; } + + /** + * Get the URL pattern of the resource site. + * + * @return string + */ + public function getUrlPattern(): string + { + return match ($this) { + ResourceSite::TWITTER => '/^https?:\/\/(twitter)\.com\/(\w+)/', + ResourceSite::CRUNCHYROLL => '/^https?:\/\/www\.crunchyroll\.com\/(series|watch)\/(\w+)/', + ResourceSite::HIDIVE => '/^https?:\/\/www\.hidive\.com\/(tv|movies)\/([\w-]+)/', + ResourceSite::NETFLIX => '/^https?:\/\/www\.netflix\.com\/(title|watch)\/(\d+)/', + ResourceSite::DISNEY_PLUS => '/^https?:\/\/www\.disneyplus\.com\/(series|movies)\/([\w-]+\/\w+)/', + ResourceSite::HULU => '/^https?:\/\/www\.hulu\.com\/(series|watch|movie)\/([\w-]+)/', + ResourceSite::AMAZON_PRIME_VIDEO => '/^https?:\/\/www\.primevideo\.com\/(detail)\/(\w+)/', + default => '/^$/', + }; + } } diff --git a/app/Rules/Wiki/Resource/AnimeResourceLinkFormatRule.php b/app/Rules/Wiki/Resource/AnimeResourceLinkFormatRule.php index 929f9de34..9a845b2e3 100644 --- a/app/Rules/Wiki/Resource/AnimeResourceLinkFormatRule.php +++ b/app/Rules/Wiki/Resource/AnimeResourceLinkFormatRule.php @@ -47,11 +47,11 @@ public function validate(string $attribute, mixed $value, Closure $fail): void ResourceSite::YOUTUBE => '/^https:\/\/www\.youtube\.com\/\@\w+$/', ResourceSite::APPLE_MUSIC => '/$.^/', ResourceSite::AMAZON_MUSIC => '/$.^/', - ResourceSite::CRUNCHYROLL => '/^https:\/\/www\.crunchyroll\.com\/series\/\w+$/', - ResourceSite::HIDIVE => '/^https:\/\/www\.hidive\.com\/tv\/[\w-]+$/', + ResourceSite::CRUNCHYROLL => '/^https:\/\/www\.crunchyroll\.com\/(series|watch)\/\w+$/', + ResourceSite::HIDIVE => '/^https:\/\/www\.hidive\.com\/(tv|movies)\/[\w-]+$/', ResourceSite::NETFLIX => '/^https:\/\/www\.netflix\.com\/title\/\d+$/', - ResourceSite::DISNEY_PLUS => '/^https:\/\/www\.disneyplus\.com\/series\/[\w-]+\/\w+$/', - ResourceSite::HULU => '/^https:\/\/www\.hulu\.com\/series\/[\w-]+$/', + ResourceSite::DISNEY_PLUS => '/^https:\/\/www\.disneyplus\.com\/(series|movies)\/[\w-]+\/\w+$/', + ResourceSite::HULU => '/^https:\/\/www\.hulu\.com\/(series|watch|movie)\/[\w-]+$/', ResourceSite::AMAZON_PRIME_VIDEO => '/^https:\/\/www\.primevideo\.com\/detail\/\w+$/', default => null, }; diff --git a/database/seeders/Wiki/Anime/BackfillExternalResourcesSeeder.php b/database/seeders/Wiki/Anime/BackfillExternalResourcesSeeder.php index 57cd79206..ce7b24757 100644 --- a/database/seeders/Wiki/Anime/BackfillExternalResourcesSeeder.php +++ b/database/seeders/Wiki/Anime/BackfillExternalResourcesSeeder.php @@ -47,7 +47,7 @@ public function run(): void $language = $externalLink['language']; if (!in_array($site, array_keys($availableSites))) continue; - if (in_array($site, ['Official Site', 'Twitter']) && $language !== 'Japanese') continue; + if (in_array($site, ['Official Site', 'Twitter']) && !in_array($language, ['Japanese', null])) continue; if ($this->relation($anime)->getQuery()->where(ExternalResource::ATTRIBUTE_SITE, $availableSites[$site]->value)->exists()) { $nameLocalized = $availableSites[$site]->localize(); @@ -155,12 +155,13 @@ protected function attachResource(ExternalResource $resource, Anime $anime): voi protected function getOrCreateResource(mixed $externalLink, Anime $anime): ExternalResource { $availableSites = $this->getAvailableSites(); - $url = $externalLink['url']; + /** @var ResourceSite $resourceSite */ $resourceSite = $availableSites[$externalLink['site']]; - $urlPattern = $this->getUrlPattern($resourceSite->value); + $url = $externalLink['url']; + $urlPattern = $resourceSite->getUrlPattern(); if (preg_match($urlPattern, $url, $matches)) { - $url = $resourceSite->formatAnimeResourceLink(intval($matches[1]), $matches[1]); + $url = $resourceSite->formatAnimeResourceLink(intval($matches[2]), $matches[2], $matches[1]); } $resource = ExternalResource::query() @@ -177,7 +178,7 @@ protected function getOrCreateResource(mixed $externalLink, Anime $anime): Exter $resource = ExternalResource::query()->create([ ExternalResource::ATTRIBUTE_LINK => $url, ExternalResource::ATTRIBUTE_SITE => $resourceSite->value, - ExternalResource::ATTRIBUTE_EXTERNAL_ID => $resourceSite->parseIdFromLink($url) ?? null, + ExternalResource::ATTRIBUTE_EXTERNAL_ID => $resourceSite->parseIdFromLink($url), ]); } @@ -203,19 +204,4 @@ protected function getAvailableSites(): array 'Disney Plus' => ResourceSite::DISNEY_PLUS, ]; } - - protected function getUrlPattern(int $resourceSite): string - { - $matches = [ - ResourceSite::TWITTER->value => '/^https:\/\/twitter\.com\/(\w+)/', - ResourceSite::CRUNCHYROLL->value => '/^https:\/\/www\.crunchyroll\.com\/series\/(\w+)/', - ResourceSite::HIDIVE->value => '/^https:\/\/www\.hidive\.com\/tv\/([\w-]+)/', - ResourceSite::NETFLIX->value => '/^https:\/\/www\.netflix\.com\/title\/(\d+)/', - ResourceSite::DISNEY_PLUS->value => '/^https:\/\/www\.disneyplus\.com\/series\/([\w-]+\/\w+)/', - ResourceSite::HULU->value => '/^https:\/\/www\.hulu\.com\/series\/([\w-]+)/', - ResourceSite::AMAZON_PRIME_VIDEO->value => '/^https:\/\/www\.primevideo\.com\/detail\/(\w+)/', - ]; - - return $matches[$resourceSite] ?? '/^$/'; - } } diff --git a/database/seeders/Wiki/Anime/FixExternalResourcesSeeder.php b/database/seeders/Wiki/Anime/FixExternalResourcesSeeder.php new file mode 100644 index 000000000..12a24288e --- /dev/null +++ b/database/seeders/Wiki/Anime/FixExternalResourcesSeeder.php @@ -0,0 +1,72 @@ +getAvailableSites(); + $resourceSites = array_values($availableSites); + + $externalResources = ExternalResource::query(); + + foreach ($resourceSites as $resourceSite) { + $externalResources = $externalResources->orWhere(ExternalResource::ATTRIBUTE_SITE, $resourceSite->value); + } + + $externalResources = $externalResources->get(); + + foreach ($externalResources as $externalResource) { + $url = Arr::get($externalResource, ExternalResource::ATTRIBUTE_LINK); + $site = Arr::get($externalResource, ExternalResource::ATTRIBUTE_SITE); + $urlPattern = $site->getUrlPattern(); + + if (preg_match($urlPattern, $url, $matches)) { + $url = $site->formatAnimeResourceLink(intval($matches[2]), $matches[2], $matches[1]); + + $externalResource->update([ + ExternalResource::ATTRIBUTE_LINK => $url, + ]); + } + } + + DB::commit(); + + } catch (Exception $e) { + echo 'error ' . $e->getMessage(); + echo "\n"; + + DB::rollBack(); + + throw $e; + } + } + + protected function getAvailableSites(): array + { + /** Key name in Anilist API => @var ResourceSite */ + return [ + 'Netflix' => ResourceSite::NETFLIX, + 'Crunchyroll' => ResourceSite::CRUNCHYROLL, + 'HIDIVE' => ResourceSite::HIDIVE, + 'Hulu' => ResourceSite::HULU, + 'Disney Plus' => ResourceSite::DISNEY_PLUS, + ]; + } +} diff --git a/tests/Unit/Models/Wiki/ExternalResourceTest.php b/tests/Unit/Models/Wiki/ExternalResourceTest.php index 1e11ae208..54ba115fe 100644 --- a/tests/Unit/Models/Wiki/ExternalResourceTest.php +++ b/tests/Unit/Models/Wiki/ExternalResourceTest.php @@ -8,9 +8,11 @@ use App\Models\Wiki\Anime; use App\Models\Wiki\Artist; use App\Models\Wiki\ExternalResource; +use App\Models\Wiki\Song; use App\Models\Wiki\Studio; use App\Pivots\Wiki\AnimeResource; use App\Pivots\Wiki\ArtistResource; +use App\Pivots\Wiki\SongResource; use App\Pivots\Wiki\StudioResource; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Foundation\Testing\WithFaker; @@ -87,6 +89,23 @@ public function testArtists(): void static::assertEquals(ArtistResource::class, $resource->artists()->getPivotClass()); } + /** + * Resource shall have a many-to-many relationship with the type Song. + */ + public function testSong(): void + { + $songCount = $this->faker->randomDigitNotNull(); + + $resource = ExternalResource::factory() + ->has(Song::factory()->count($songCount)) + ->createOne(); + + static::assertInstanceOf(BelongsToMany::class, $resource->song()); + static::assertEquals($songCount, $resource->song()->count()); + static::assertInstanceOf(Song::class, $resource->song()->first()); + static::assertEquals(SongResource::class, $resource->song()->getPivotClass()); + } + /** * Resource shall have a many-to-many relationship with the type Studio. * diff --git a/tests/Unit/Rules/Wiki/Resource/AnimeResourceLinkFormatTest.php b/tests/Unit/Rules/Wiki/Resource/AnimeResourceLinkFormatTest.php index 9fbcd2312..86717a19b 100644 --- a/tests/Unit/Rules/Wiki/Resource/AnimeResourceLinkFormatTest.php +++ b/tests/Unit/Rules/Wiki/Resource/AnimeResourceLinkFormatTest.php @@ -61,7 +61,7 @@ public function testPassesForPattern(): void ResourceSite::AMAZON_PRIME_VIDEO, ]); - $url = $site->formatAnimeResourceLink($this->faker->randomDigitNotNull(), $this->faker->word()); + $url = $site->formatAnimeResourceLink($this->faker->randomDigitNotNull(), $this->faker->word(), $this->faker->word()); $attribute = $this->faker->word();