diff --git a/app/Enums/Models/Wiki/ResourceSite.php b/app/Enums/Models/Wiki/ResourceSite.php index 3976486d0..2eaf60af3 100644 --- a/app/Enums/Models/Wiki/ResourceSite.php +++ b/app/Enums/Models/Wiki/ResourceSite.php @@ -40,6 +40,14 @@ enum ResourceSite: int case APPLE_MUSIC = 12; case AMAZON_MUSIC = 13; + // Official Streaming + case CRUNCHYROLL = 14; + case HIDIVE = 15; + case NETFLIX = 16; + case DISNEY_PLUS = 17; + case HULU = 18; + case AMAZON_PRIME_VIDEO = 19; + /** * Get domain by resource site. * @@ -62,6 +70,12 @@ public static function getDomain(?int $value): ?string ResourceSite::YOUTUBE->value => 'www.youtube.com', ResourceSite::APPLE_MUSIC->value => 'music.apple.com', ResourceSite::AMAZON_MUSIC->value => 'amazon.co.jp', + ResourceSite::CRUNCHYROLL->value => 'www.crunchyroll.com', + ResourceSite::HIDIVE->value => 'www.hidive.com', + ResourceSite::NETFLIX->value => 'www.netflix.com', + ResourceSite::DISNEY_PLUS->value => 'www.disneyplus.com', + ResourceSite::HULU->value => 'www.hulu.com', + ResourceSite::AMAZON_PRIME_VIDEO->value => 'www.primevideo.com', default => null, }; } @@ -99,6 +113,7 @@ public static function parseIdFromLink(string $link): ?string ResourceSite::MAL => Str::match('/\d+/', $link), ResourceSite::ANIME_PLANET => ResourceSite::parseAnimePlanetIdFromLink($link), ResourceSite::KITSU => ResourceSite::parseKitsuIdFromLink($link), + ResourceSite::NETFLIX => Str::match('/\d+/', $link), default => null, }; } @@ -186,6 +201,12 @@ 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", + ResourceSite::HULU => "https://www.hulu.com/series/$slug", + ResourceSite::AMAZON_PRIME_VIDEO => "https://www.primevideo.com/detail/$slug", default => null, }; } diff --git a/app/Nova/Actions/Models/Wiki/AttachResourceAction.php b/app/Nova/Actions/Models/Wiki/AttachResourceAction.php index 4e0a3c53f..76374796d 100644 --- a/app/Nova/Actions/Models/Wiki/AttachResourceAction.php +++ b/app/Nova/Actions/Models/Wiki/AttachResourceAction.php @@ -27,8 +27,9 @@ abstract class AttachResourceAction extends Action * Create a new action instance. * * @param ResourceSite[] $sites + * @param string|null $actionName */ - public function __construct(protected array $sites) + public function __construct(protected array $sites, protected ?string $actionName) { } @@ -41,7 +42,7 @@ public function __construct(protected array $sites) */ public function name(): string { - return __('nova.actions.models.wiki.attach_resource.name'); + return $this->actionName ?? __('nova.actions.models.wiki.attach_resource.name'); } /** diff --git a/app/Nova/Lenses/Anime/AnimeResourceLens.php b/app/Nova/Lenses/Anime/AnimeResourceLens.php index db41aa566..dc435ebfd 100644 --- a/app/Nova/Lenses/Anime/AnimeResourceLens.php +++ b/app/Nova/Lenses/Anime/AnimeResourceLens.php @@ -59,7 +59,7 @@ public static function criteria(Builder $query): Builder public function actions(NovaRequest $request): array { return [ - (new AttachAnimeResourceAction([static::site()])) + (new AttachAnimeResourceAction([static::site()], null)) ->confirmButtonText(__('nova.actions.models.wiki.attach_resource.confirmButtonText')) ->cancelButtonText(__('nova.actions.base.cancelButtonText')) ->showInline() diff --git a/app/Nova/Lenses/Anime/AnimeStreamingResourceLens.php b/app/Nova/Lenses/Anime/AnimeStreamingResourceLens.php new file mode 100644 index 000000000..ef44f0ecc --- /dev/null +++ b/app/Nova/Lenses/Anime/AnimeStreamingResourceLens.php @@ -0,0 +1,95 @@ +whereDoesntHave(Anime::RELATION_RESOURCES, function (Builder $resourceQuery) { + $resourceQuery->where(function (Builder $query) { + foreach (static::sites() as $site) { + $query->orWhere(ExternalResource::ATTRIBUTE_SITE, $site->value); + } + }); + }); + } + + /** + * Get the actions available on the lens. + * + * @param NovaRequest $request + * @return array + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public function actions(NovaRequest $request): array + { + return [ + (new AttachAnimeResourceAction(static::sites(), __('nova.actions.models.wiki.attach_streaming_resource.name'))) + ->confirmButtonText(__('nova.actions.models.wiki.attach_resource.confirmButtonText')) + ->cancelButtonText(__('nova.actions.base.cancelButtonText')) + ->showInline() + ->canSeeWhen('create', ExternalResource::class), + ]; + } + + /** + * Get the URI key for the lens. + * + * @return string + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public function uriKey(): string + { + return 'anime-streaming-resources-lens'; + } +} diff --git a/app/Nova/Lenses/Artist/ArtistResourceLens.php b/app/Nova/Lenses/Artist/ArtistResourceLens.php index d44460a87..1583ce7e0 100644 --- a/app/Nova/Lenses/Artist/ArtistResourceLens.php +++ b/app/Nova/Lenses/Artist/ArtistResourceLens.php @@ -59,7 +59,7 @@ public static function criteria(Builder $query): Builder public function actions(NovaRequest $request): array { return [ - (new AttachArtistResourceAction([static::site()])) + (new AttachArtistResourceAction([static::site()], null)) ->confirmButtonText(__('nova.actions.models.wiki.attach_resource.confirmButtonText')) ->cancelButtonText(__('nova.actions.base.cancelButtonText')) ->showInline() diff --git a/app/Nova/Lenses/Song/SongResourceLens.php b/app/Nova/Lenses/Song/SongResourceLens.php index 8509930e3..fb4797e05 100644 --- a/app/Nova/Lenses/Song/SongResourceLens.php +++ b/app/Nova/Lenses/Song/SongResourceLens.php @@ -59,7 +59,7 @@ public static function criteria(Builder $query): Builder public function actions(NovaRequest $request): array { return [ - (new AttachSongResourceAction([static::site()])) + (new AttachSongResourceAction([static::site()], null)) ->confirmButtonText(__('nova.actions.models.wiki.attach_resource.confirmButtonText')) ->cancelButtonText(__('nova.actions.base.cancelButtonText')) ->showInline() diff --git a/app/Nova/Lenses/Studio/StudioResourceLens.php b/app/Nova/Lenses/Studio/StudioResourceLens.php index bd8cf945e..1c10bc108 100644 --- a/app/Nova/Lenses/Studio/StudioResourceLens.php +++ b/app/Nova/Lenses/Studio/StudioResourceLens.php @@ -59,7 +59,7 @@ public static function criteria(Builder $query): Builder public function actions(Request $request): array { return [ - (new AttachStudioResourceAction([static::site()])) + (new AttachStudioResourceAction([static::site()], null)) ->confirmButtonText(__('nova.actions.models.wiki.attach_resource.confirmButtonText')) ->cancelButtonText(__('nova.actions.base.cancelButtonText')) ->showInline() diff --git a/app/Nova/Resources/Wiki/Anime.php b/app/Nova/Resources/Wiki/Anime.php index 60a0595b0..7fe92efb6 100644 --- a/app/Nova/Resources/Wiki/Anime.php +++ b/app/Nova/Resources/Wiki/Anime.php @@ -16,6 +16,7 @@ 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; use App\Nova\Lenses\Anime\Resource\AnimeAniDbResourceLens; @@ -312,12 +313,21 @@ public function actions(NovaRequest $request): array ResourceSite::OFFICIAL_SITE, ResourceSite::TWITTER, ResourceSite::YOUTUBE, - ResourceSite::WIKI + ResourceSite::WIKI, + ]; + + $streamingResourceSites = [ + ResourceSite::CRUNCHYROLL, + ResourceSite::HIDIVE, + ResourceSite::NETFLIX, + ResourceSite::DISNEY_PLUS, + ResourceSite::HULU, + ResourceSite::AMAZON_PRIME_VIDEO, ]; $facets = [ ImageFacet::COVER_SMALL, - ImageFacet::COVER_LARGE + ImageFacet::COVER_LARGE, ]; return array_merge( @@ -337,7 +347,13 @@ public function actions(NovaRequest $request): array ->showInline() ->canSeeWhen('update', $this), - (new AttachAnimeResourceAction($resourceSites)) + (new AttachAnimeResourceAction($resourceSites, null)) + ->confirmButtonText(__('nova.actions.models.wiki.attach_resource.confirmButtonText')) + ->cancelButtonText(__('nova.actions.base.cancelButtonText')) + ->exceptOnIndex() + ->canSeeWhen('create', ExternalResourceModel::class), + + (new AttachAnimeResourceAction($streamingResourceSites, __('nova.actions.models.wiki.attach_streaming_resource.name'))) ->confirmButtonText(__('nova.actions.models.wiki.attach_resource.confirmButtonText')) ->cancelButtonText(__('nova.actions.base.cancelButtonText')) ->exceptOnIndex() @@ -391,6 +407,7 @@ public function lenses(NovaRequest $request): array new AnimeTwitterResourceLens(), new AnimeOfficialSiteResourceLens(), new AnimeYoutubeResourceLens(), + new AnimeStreamingResourceLens(), new AnimeStudioLens(), ] ); diff --git a/app/Nova/Resources/Wiki/Artist.php b/app/Nova/Resources/Wiki/Artist.php index fa6af8007..988720869 100644 --- a/app/Nova/Resources/Wiki/Artist.php +++ b/app/Nova/Resources/Wiki/Artist.php @@ -283,18 +283,18 @@ public function actions(NovaRequest $request): array ResourceSite::SPOTIFY, ResourceSite::TWITTER, ResourceSite::YOUTUBE, - ResourceSite::WIKI + ResourceSite::WIKI, ]; $facets = [ ImageFacet::COVER_SMALL, - ImageFacet::COVER_LARGE + ImageFacet::COVER_LARGE, ]; return array_merge( parent::actions($request), [ - (new AttachArtistResourceAction($resources)) + (new AttachArtistResourceAction($resources, null)) ->confirmButtonText(__('nova.actions.models.wiki.attach_resource.confirmButtonText')) ->cancelButtonText(__('nova.actions.base.cancelButtonText')) ->exceptOnIndex() diff --git a/app/Nova/Resources/Wiki/Song.php b/app/Nova/Resources/Wiki/Song.php index b4e681f8c..5f818e54a 100644 --- a/app/Nova/Resources/Wiki/Song.php +++ b/app/Nova/Resources/Wiki/Song.php @@ -239,13 +239,13 @@ public function actions(NovaRequest $request): array ResourceSite::YOUTUBE_MUSIC, ResourceSite::YOUTUBE, ResourceSite::APPLE_MUSIC, - ResourceSite::AMAZON_MUSIC + ResourceSite::AMAZON_MUSIC, ]; return array_merge( parent::actions($request), [ - (new AttachSongResourceAction($resourceSites)) + (new AttachSongResourceAction($resourceSites, null)) ->confirmButtonText(__('nova.actions.models.wiki.attach_resource.confirmButtonText')) ->cancelButtonText(__('nova.actions.base.cancelButtonText')) ->exceptOnIndex() @@ -269,7 +269,7 @@ public function lenses(NovaRequest $request): array new SongAmazonMusicResourceLens(), new SongAppleMusicResourceLens(), new SongSpotifyResourceLens(), - new SongYoutubeMusicResourceLens() + new SongYoutubeMusicResourceLens(), ] ); } diff --git a/app/Nova/Resources/Wiki/Studio.php b/app/Nova/Resources/Wiki/Studio.php index d45b20398..47b215f12 100644 --- a/app/Nova/Resources/Wiki/Studio.php +++ b/app/Nova/Resources/Wiki/Studio.php @@ -220,11 +220,11 @@ public function actions(NovaRequest $request): array ResourceSite::ANILIST, ResourceSite::ANIME_PLANET, ResourceSite::ANN, - ResourceSite::MAL + ResourceSite::MAL, ]; $facets = [ - ImageFacet::COVER_LARGE + ImageFacet::COVER_LARGE, ]; return array_merge( @@ -238,7 +238,7 @@ public function actions(NovaRequest $request): array ->showInline() ->canSeeWhen('update', $this), - (new AttachStudioResourceAction($resources)) + (new AttachStudioResourceAction($resources, null)) ->confirmButtonText(__('nova.actions.models.wiki.attach_resource.confirmButtonText')) ->cancelButtonText(__('nova.actions.base.cancelButtonText')) ->exceptOnIndex() diff --git a/app/Rules/Wiki/Resource/AnimeResourceLinkFormatRule.php b/app/Rules/Wiki/Resource/AnimeResourceLinkFormatRule.php index 3c6e7aece..929f9de34 100644 --- a/app/Rules/Wiki/Resource/AnimeResourceLinkFormatRule.php +++ b/app/Rules/Wiki/Resource/AnimeResourceLinkFormatRule.php @@ -47,6 +47,12 @@ 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::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::AMAZON_PRIME_VIDEO => '/^https:\/\/www\.primevideo\.com\/detail\/\w+$/', default => null, }; diff --git a/lang/en/enums.php b/lang/en/enums.php index 9837fee26..053a94b1d 100644 --- a/lang/en/enums.php +++ b/lang/en/enums.php @@ -59,7 +59,13 @@ ResourceSite::YOUTUBE_MUSIC->name => 'YouTube Music', ResourceSite::YOUTUBE->name => 'YouTube', ResourceSite::APPLE_MUSIC->name => 'Apple Music', - ResourceSite::AMAZON_MUSIC->name => 'Amazon Music' + ResourceSite::AMAZON_MUSIC->name => 'Amazon Music', + ResourceSite::CRUNCHYROLL->name => 'Crunchyroll', + ResourceSite::HIDIVE->name => 'HIDIVE', + ResourceSite::NETFLIX->name => 'Netflix', + ResourceSite::DISNEY_PLUS->name => 'Disney Plus', + ResourceSite::HULU->name => 'Hulu', + ResourceSite::AMAZON_PRIME_VIDEO->name => 'Amazon Prime Video', ], Service::class => [ Service::OTHER->name => 'Other', diff --git a/lang/en/nova.php b/lang/en/nova.php index bb1aa866c..185a71c50 100644 --- a/lang/en/nova.php +++ b/lang/en/nova.php @@ -207,23 +207,44 @@ 'help' => 'Ex: https://unite-up.fandom.com/wiki/Protostar', ], 'spotify' => [ - 'help' => 'Ex: https://open.spotify.com/track/5dmkAW2HpEJgFDSgkywm8N' + 'help' => 'Ex: https://open.spotify.com/track/5dmkAW2HpEJgFDSgkywm8N', ], 'youtube_music' => [ - 'help' => 'Ex: https://music.youtube.com/watch?v=dHasNhuseU8' + 'help' => 'Ex: https://music.youtube.com/watch?v=dHasNhuseU8', ], 'youtube' => [ - 'help' => 'Ex: https://www.youtube.com/@liyuuchannel' + 'help' => 'Ex: https://www.youtube.com/@liyuuchannel', ], 'apple_music' => [ - 'help' => 'Ex: https://music.apple.com/jp/album/1711324281' + 'help' => 'Ex: https://music.apple.com/jp/album/1711324281', ], 'amazon_music' => [ - 'help' => 'Ex: https://amazon.co.jp/music/player/albums/B0CKVQGSJY' - ] + 'help' => 'Ex: https://amazon.co.jp/music/player/albums/B0CKVQGSJY', + ], + 'crunchyroll' => [ + 'help' => 'Ex: https://www.crunchyroll.com/series/GRDQNQW9Y', + ], + 'hidive' => [ + 'help' => 'Ex: https://www.hidive.com/tv/the-eminence-in-shadow', + ], + 'netflix' => [ + 'help' => 'Ex: https://www.netflix.com/title/81564905', + ], + 'disney_plus' => [ + 'help' => 'https://www.disneyplus.com/series/tokyo-revengers/4HFbN55sAh0i', + ], + 'hulu' => [ + 'help' => 'Ex: https://www.hulu.com/series/the-eminence-in-shadow-66f37cf4-dba5-4511-ae26-e4092df1668b', + ], + 'amazon_prime_video' => [ + 'help' => 'Ex: https://www.primevideo.com/detail/0PXZCO5NGDNH8OWTJIDTEB8IEF', + ], ], 'name' => 'Attach Resources', ], + 'attach_streaming_resource' => [ + 'name' => 'Attach Streaming Resources', + ], 'attach_image' => [ 'confirmButtonText' => 'Upload', 'name' => 'Attach Images' @@ -778,6 +799,9 @@ 'resources' => [ 'name' => 'Anime Without :site Resource', ], + 'streaming_resources' => [ + 'name' => 'Anime Without Streaming Resources', + ], 'studios' => [ 'name' => 'Anime Without Studios', ], diff --git a/tests/Unit/Enums/Models/Wiki/ResourceSiteTest.php b/tests/Unit/Enums/Models/Wiki/ResourceSiteTest.php index 43970feaa..655c543b9 100644 --- a/tests/Unit/Enums/Models/Wiki/ResourceSiteTest.php +++ b/tests/Unit/Enums/Models/Wiki/ResourceSiteTest.php @@ -32,6 +32,7 @@ public function testParseIdFromAnimeResource(): void ResourceSite::ANILIST, ResourceSite::ANN, ResourceSite::MAL, + ResourceSite::NETFLIX, ]); $link = $site->formatAnimeResourceLink($animeId); diff --git a/tests/Unit/Rules/Wiki/Resource/AnimeResourceLinkFormatTest.php b/tests/Unit/Rules/Wiki/Resource/AnimeResourceLinkFormatTest.php index ffa70900f..9fbcd2312 100644 --- a/tests/Unit/Rules/Wiki/Resource/AnimeResourceLinkFormatTest.php +++ b/tests/Unit/Rules/Wiki/Resource/AnimeResourceLinkFormatTest.php @@ -52,6 +52,13 @@ public function testPassesForPattern(): void ResourceSite::ANN, ResourceSite::KITSU, ResourceSite::MAL, + ResourceSite::YOUTUBE, + ResourceSite::CRUNCHYROLL, + ResourceSite::HIDIVE, + ResourceSite::NETFLIX, + ResourceSite::DISNEY_PLUS, + ResourceSite::HULU, + ResourceSite::AMAZON_PRIME_VIDEO, ]); $url = $site->formatAnimeResourceLink($this->faker->randomDigitNotNull(), $this->faker->word()); @@ -82,6 +89,13 @@ public function testFailsForTrailingSlash(): void ResourceSite::ANN, ResourceSite::KITSU, ResourceSite::MAL, + ResourceSite::YOUTUBE, + ResourceSite::CRUNCHYROLL, + ResourceSite::HIDIVE, + ResourceSite::NETFLIX, + ResourceSite::DISNEY_PLUS, + ResourceSite::HULU, + ResourceSite::AMAZON_PRIME_VIDEO, ]); $url = $site->formatAnimeResourceLink($this->faker->randomDigitNotNull(), $this->faker->word());