feat: anime theme entry likeable (#993)

This commit is contained in:
Kyrch
2025-11-06 05:49:20 -03:00
committed by GitHub
parent c0ffa7083d
commit 8dd25de1a4
34 changed files with 112 additions and 138 deletions
+7
View File
@@ -26,6 +26,13 @@ jobs:
run: |
mkdir -p database
touch database/database.sqlite
- name: Install static FFmpeg
run: |
curl -L -o ffmpeg.tar.xz https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
tar -xf ffmpeg.tar.xz
sudo mv ffmpeg-*-amd64-static/ffmpeg /usr/local/bin/
sudo mv ffmpeg-*-amd64-static/ffprobe /usr/local/bin/
ffmpeg -encoders | grep av1 || true
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: sqlite
@@ -72,7 +72,7 @@ class OptimizeImageAction
);
[$command, $imagePath] = match ($this->extension) {
'avif' => static::getAvifCommand(),
'avif' => static::getAvifCommand($this->image),
default => throw new Exception("Unsupported image extension: {$this->extension}"),
};
@@ -106,17 +106,17 @@ class OptimizeImageAction
/**
* @return array{0:array<int, string>, 1:string}
*/
protected function getAvifCommand(): array
public static function getAvifCommand(Image $image): array
{
$imagePath = Storage::disk('local')->path(
Str::replaceLast(File::extension($this->image->path), 'avif', $this->image->path)
Str::replaceLast(File::extension($image->path), 'avif', $image->path)
);
return [
[
'ffmpeg',
'-i',
Storage::disk('local')->path($this->image->path),
Storage::disk('local')->path($image->path),
'-c:v',
'libaom-av1',
'-crf',
@@ -9,8 +9,6 @@ use Illuminate\Database\Eloquent\Relations\MorphOne;
trait AggregatesLike
{
final public const RELATION_LIKE_AGGREGATE = 'likeAggregate';
/**
* @return MorphOne<LikeAggregate, $this>
*/
@@ -12,5 +12,7 @@ use Illuminate\Database\Eloquent\Relations\MorphOne;
*/
interface HasAggregateLikes
{
public const RELATION_LIKE_AGGREGATE = 'likeAggregate';
public function likeAggregate(): MorphOne;
}
+1 -1
View File
@@ -12,7 +12,7 @@ class AllowAudioStreams
{
public function resolve(?User $user): bool
{
if (filled($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value))) {
if ($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value)) {
return true;
}
+1 -1
View File
@@ -12,7 +12,7 @@ class AllowDumpDownloading
{
public function resolve(?User $user): bool
{
if (filled($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value))) {
if ($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value)) {
return true;
}
@@ -12,7 +12,7 @@ class AllowExternalProfileManagement
{
public function resolve(?User $user): bool
{
if (filled($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value))) {
if ($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value)) {
return true;
}
+1 -1
View File
@@ -12,7 +12,7 @@ class AllowPlaylistManagement
{
public function resolve(?User $user): bool
{
if (filled($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value))) {
if ($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value)) {
return true;
}
+1 -1
View File
@@ -12,7 +12,7 @@ class AllowReport
{
public function resolve(?User $user): bool
{
if (filled($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value))) {
if ($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value)) {
return true;
}
+1 -1
View File
@@ -12,7 +12,7 @@ class AllowScriptDownloading
{
public function resolve(?User $user): bool
{
if (filled($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value))) {
if ($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value)) {
return true;
}
+1 -1
View File
@@ -12,7 +12,7 @@ class AllowVideoStreams
{
public function resolve(?User $user): bool
{
if (filled($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value))) {
if ($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value)) {
return true;
}
@@ -35,6 +35,7 @@ abstract class BaseController
* Get the attributes and values that were validated.
*
* @param class-string<BaseMutation> $mutation
* @return array<string, mixed>
*
* @throws ValidationException
*/
+14 -30
View File
@@ -4,46 +4,37 @@ declare(strict_types=1);
namespace App\GraphQL\Controllers\User;
use App\Contracts\Models\Likeable;
use App\Exceptions\GraphQL\ClientValidationException;
use App\GraphQL\Controllers\BaseController;
use App\GraphQL\Schema\Mutations\Models\User\LikeMutation;
use App\GraphQL\Schema\Mutations\Models\User\UnlikeMutation;
use App\Models\List\Playlist;
use App\Models\User\Like;
use App\Models\Wiki\Video;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
/**
* @extends BaseController<Like>
*/
class LikeController extends BaseController
{
final public const string ATTRIBUTE_ENTRY = 'entry';
final public const string ATTRIBUTE_PLAYLIST = 'playlist';
final public const string ATTRIBUTE_VIDEO = 'video';
/**
* @param array<string, mixed> $args
*
* @throws ClientValidationException
*/
public function store($root, array $args): Model
public function store($root, array $args): Model&Likeable
{
$validated = $this->validated($args, LikeMutation::class);
$playlist = Arr::get($validated, self::ATTRIBUTE_PLAYLIST);
$video = Arr::get($validated, self::ATTRIBUTE_VIDEO);
foreach ($validated as $likeable) {
if ($likeable instanceof Model && $likeable instanceof Likeable) {
$likeable->like();
if ($playlist instanceof Playlist) {
$playlist->like();
return $playlist;
}
if ($video instanceof Video) {
$video->like();
return $video;
return $likeable;
}
}
throw new ClientValidationException('One resource is required to like.');
@@ -54,23 +45,16 @@ class LikeController extends BaseController
*
* @throws ClientValidationException
*/
public function destroy($root, array $args): Model
public function destroy($root, array $args): Model&Likeable
{
$validated = $this->validated($args, UnlikeMutation::class);
$playlist = Arr::get($validated, self::ATTRIBUTE_PLAYLIST);
$video = Arr::get($validated, self::ATTRIBUTE_VIDEO);
foreach ($validated as $likeable) {
if ($likeable instanceof Model && $likeable instanceof Likeable) {
$likeable->unlike();
if ($playlist instanceof Playlist) {
$playlist->unlike();
return $playlist;
}
if ($video instanceof Video) {
$video->unlike();
return $video;
return $likeable;
}
}
throw new ClientValidationException('One resource is required to unlike.');
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\Base;
use App\Contracts\Models\HasAggregateLikes;
class LikesCountField extends CountAggregateField
{
public function __construct()
{
parent::__construct(HasAggregateLikes::RELATION_LIKE_AGGREGATE, 'likesCount');
}
public function description(): string
{
return 'The number of likes recorded for the resource';
}
}
@@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\List\Playlist;
use App\GraphQL\Schema\Fields\Base\CountAggregateField;
use App\Models\List\Playlist;
class PlaylistLikesCountField extends CountAggregateField
{
public function __construct()
{
parent::__construct(Playlist::RELATION_LIKE_AGGREGATE, 'likesCount');
}
public function description(): string
{
return 'The number of likes recorded for the resource';
}
}
@@ -9,21 +9,21 @@ use App\Contracts\GraphQL\Fields\CreatableField;
use App\Contracts\GraphQL\Fields\DeletableField;
use App\GraphQL\Controllers\User\LikeController;
use App\GraphQL\Schema\Fields\Field;
use App\Models\Wiki\Video;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use GraphQL\Type\Definition\Type;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class LikeVideoField extends Field implements BindableField, CreatableField, DeletableField
class LikeAnimeThemeEntryField extends Field implements BindableField, CreatableField, DeletableField
{
public function __construct()
{
parent::__construct('video');
parent::__construct('entry');
}
public function description(): string
{
return 'The id of the video to like';
return 'The id of the entry to like';
}
public function baseType(): Type
@@ -34,10 +34,10 @@ class LikeVideoField extends Field implements BindableField, CreatableField, Del
/**
* The resolver to cast the model.
*/
public function bindResolver(array $args): Video
public function bindResolver(array $args): AnimeThemeEntry
{
return Video::query()
->where(Video::ATTRIBUTE_ID, Arr::get($args, $this->getName()))
return AnimeThemeEntry::query()
->where(AnimeThemeEntry::ATTRIBUTE_ID, Arr::get($args, $this->getName()))
->firstOrFail();
}
@@ -47,7 +47,7 @@ class LikePlaylistField extends Field implements BindableField, CreatableField,
public function getCreationRules(array $args): array
{
return [
Str::of('prohibits:')->append(LikeController::ATTRIBUTE_VIDEO)->__toString(),
Str::of('prohibits:')->append(LikeController::ATTRIBUTE_ENTRY)->__toString(),
];
}
@@ -57,7 +57,7 @@ class LikePlaylistField extends Field implements BindableField, CreatableField,
public function getDeleteRules(array $args): array
{
return [
Str::of('prohibits:')->append(LikeController::ATTRIBUTE_VIDEO)->__toString(),
Str::of('prohibits:')->append(LikeController::ATTRIBUTE_ENTRY)->__toString(),
];
}
}
@@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\Wiki\Video;
use App\GraphQL\Schema\Fields\Base\CountAggregateField;
use App\Models\Wiki\Video;
class VideoLikesCountField extends CountAggregateField
{
public function __construct()
{
parent::__construct(Video::RELATION_LIKE_AGGREGATE, 'likesCount');
}
public function description(): string
{
return 'The number of likes recorded for the resource';
}
}
@@ -11,7 +11,6 @@ use App\GraphQL\Schema\Mutations\BaseMutation;
use App\GraphQL\Schema\Types\User\LikeType;
use App\GraphQL\Schema\Unions\LikedUnion;
use App\GraphQL\Support\Argument\Argument;
use App\Models\User\Like;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use Illuminate\Support\Facades\App;
@@ -16,7 +16,7 @@ use App\GraphQL\Schema\Types\Auth\PermissionType;
use App\GraphQL\Schema\Types\Auth\RoleType;
use App\GraphQL\Schema\Types\EloquentType;
use App\GraphQL\Schema\Types\List\PlaylistType;
use App\GraphQL\Schema\Types\Wiki\VideoType;
use App\GraphQL\Schema\Types\Wiki\Anime\Theme\AnimeThemeEntryType;
use App\GraphQL\Schema\Unions\NotificationUnion;
use App\GraphQL\Support\Relations\BelongsToManyRelation;
use App\GraphQL\Support\Relations\HasManyRelation;
@@ -43,8 +43,8 @@ class MeType extends EloquentType
new HasManyRelation(new PlaylistType(), User::RELATION_PLAYLISTS),
new BelongsToManyRelation($this, RoleType::class, User::RELATION_ROLES),
new BelongsToManyRelation($this, PermissionType::class, User::RELATION_PERMISSIONS),
new BelongsToManyRelation($this, AnimeThemeEntryType::class, 'likedentries'),
new BelongsToManyRelation($this, PlaylistType::class, 'likedplaylists'),
new BelongsToManyRelation($this, VideoType::class, 'likedvideos'),
];
}
@@ -5,11 +5,11 @@ declare(strict_types=1);
namespace App\GraphQL\Schema\Types\List;
use App\GraphQL\Schema\Fields\Base\CreatedAtField;
use App\GraphQL\Schema\Fields\Base\LikesCountField;
use App\GraphQL\Schema\Fields\Base\UpdatedAtField;
use App\GraphQL\Schema\Fields\Field;
use App\GraphQL\Schema\Fields\List\Playlist\PlaylistDescriptionField;
use App\GraphQL\Schema\Fields\List\Playlist\PlaylistIdField;
use App\GraphQL\Schema\Fields\List\Playlist\PlaylistLikesCountField;
use App\GraphQL\Schema\Fields\List\Playlist\PlaylistNameField;
use App\GraphQL\Schema\Fields\List\Playlist\PlaylistTracksCountField;
use App\GraphQL\Schema\Fields\List\Playlist\PlaylistTracksExistsField;
@@ -65,7 +65,7 @@ class PlaylistType extends EloquentType
new LocalizedEnumField(new PlaylistVisibilityField()),
new PlaylistTracksCountField(),
new PlaylistTracksExistsField(),
new PlaylistLikesCountField(),
new LikesCountField(),
new CreatedAtField(),
new UpdatedAtField(),
];
+2 -2
View File
@@ -5,8 +5,8 @@ declare(strict_types=1);
namespace App\GraphQL\Schema\Types\User;
use App\GraphQL\Schema\Fields\Field;
use App\GraphQL\Schema\Fields\User\Like\LikeAnimeThemeEntryField;
use App\GraphQL\Schema\Fields\User\Like\LikePlaylistField;
use App\GraphQL\Schema\Fields\User\Like\LikeVideoField;
use App\GraphQL\Schema\Types\Auth\UserType;
use App\GraphQL\Schema\Types\EloquentType;
use App\GraphQL\Schema\Unions\LikedUnion;
@@ -44,8 +44,8 @@ class LikeType extends EloquentType
public function fieldClasses(): array
{
return [
new LikeAnimeThemeEntryField(),
new LikePlaylistField(),
new LikeVideoField(),
];
}
}
@@ -8,6 +8,7 @@ use App\Contracts\GraphQL\Types\ReportableType;
use App\GraphQL\Schema\Fields\Base\CreatedAtField;
use App\GraphQL\Schema\Fields\Base\DeletedAtField;
use App\GraphQL\Schema\Fields\Base\IdField;
use App\GraphQL\Schema\Fields\Base\LikesCountField;
use App\GraphQL\Schema\Fields\Base\UpdatedAtField;
use App\GraphQL\Schema\Fields\Field;
use App\GraphQL\Schema\Fields\Wiki\Anime\Theme\Entry\AnimeThemeEntryEpisodesField;
@@ -64,6 +65,7 @@ class AnimeThemeEntryType extends EloquentType implements ReportableType
new AnimeThemeEntryNsfwField(),
new AnimeThemeEntrySpoilerField(),
new AnimeThemeEntryVersionField(),
new LikesCountField(),
new AnimeThemeEntryTracksCountField(),
new CreatedAtField(),
new UpdatedAtField(),
@@ -13,7 +13,6 @@ use App\GraphQL\Schema\Fields\Field;
use App\GraphQL\Schema\Fields\LocalizedEnumField;
use App\GraphQL\Schema\Fields\Wiki\Video\VideoBasenameField;
use App\GraphQL\Schema\Fields\Wiki\Video\VideoFilenameField;
use App\GraphQL\Schema\Fields\Wiki\Video\VideoLikesCountField;
use App\GraphQL\Schema\Fields\Wiki\Video\VideoLinkField;
use App\GraphQL\Schema\Fields\Wiki\Video\VideoLyricsField;
use App\GraphQL\Schema\Fields\Wiki\Video\VideoMimetypeField;
@@ -83,7 +82,6 @@ class VideoType extends EloquentType implements ReportableType
new VideoUncenField(),
new VideoTagsField(),
new VideoLinkField(),
new VideoLikesCountField(),
new VideoViewsCountField(),
new CreatedAtField(),
new UpdatedAtField(),
+2 -2
View File
@@ -6,7 +6,7 @@ namespace App\GraphQL\Schema\Unions;
use App\GraphQL\Schema\Types\BaseType;
use App\GraphQL\Schema\Types\List\PlaylistType;
use App\GraphQL\Schema\Types\Wiki\VideoType;
use App\GraphQL\Schema\Types\Wiki\Anime\Theme\AnimeThemeEntryType;
class LikedUnion extends BaseUnion
{
@@ -32,8 +32,8 @@ class LikedUnion extends BaseUnion
public function baseTypes(): array
{
return [
new AnimeThemeEntryType(),
new PlaylistType(),
new VideoType(),
];
}
}
@@ -28,7 +28,7 @@ class PlaylistExceedsTrackLimit
$user = $request->user('sanctum');
abort_if(intval($playlist?->tracks()?->count()) >= $trackLimit
&& blank($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value)), 403, "Playlists cannot contain more than '$trackLimit' tracks.");
&& $user?->cannot(SpecialPermission::BYPASS_FEATURE_FLAGS->value), 403, "Playlists cannot contain more than '$trackLimit' tracks.");
return $next($request);
}
@@ -24,7 +24,7 @@ class UserExceedsExternalProfileLimit
$user = $request->user('sanctum');
abort_if(intval($user?->externalprofiles()?->count()) >= $profileLimit
&& blank($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value)), 403, "User cannot have more than '$profileLimit' external profiles.");
&& $user?->cannot(SpecialPermission::BYPASS_FEATURE_FLAGS->value), 403, "User cannot have more than '$profileLimit' external profiles.");
return $next($request);
}
@@ -24,7 +24,7 @@ class UserExceedsPlaylistLimit
$user = $request->user('sanctum');
abort_if(intval($user?->playlists()?->count()) >= $playlistLimit
&& blank($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS->value)), 403, "User cannot have more than '$playlistLimit' playlists.");
&& $user?->cannot(SpecialPermission::BYPASS_FEATURE_FLAGS->value), 403, "User cannot have more than '$playlistLimit' playlists.");
return $next($request);
}
+20 -20
View File
@@ -20,7 +20,7 @@ use App\Models\List\Playlist;
use App\Models\User\Like;
use App\Models\User\Notification;
use App\Models\User\Report;
use App\Models\Wiki\Video;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use Database\Factories\Auth\UserFactory;
use Filament\Facades\Filament;
use Filament\Models\Contracts\FilamentUser;
@@ -204,6 +204,25 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasSubtit
return $this->hasMany(ExternalProfile::class, ExternalProfile::ATTRIBUTE_USER);
}
/**
* Get the liked entries of the user.
*
* @return BelongsToMany<AnimeThemeEntry, $this>
*/
public function likedentries(): BelongsToMany
{
return $this->belongsToMany(
AnimeThemeEntry::class,
Like::TABLE,
Like::ATTRIBUTE_USER,
Like::ATTRIBUTE_LIKEABLE_ID,
null,
AnimeThemeEntry::ATTRIBUTE_ID
)
->wherePivot(Like::ATTRIBUTE_LIKEABLE_TYPE, Relation::getMorphAlias(AnimeThemeEntry::class))
->withTimestamps();
}
/**
* Get the liked playlists of the user.
*
@@ -223,25 +242,6 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasSubtit
->withTimestamps();
}
/**
* Get the liked videos of the user.
*
* @return BelongsToMany<Video, $this>
*/
public function likedvideos(): BelongsToMany
{
return $this->belongsToMany(
Video::class,
Like::TABLE,
Like::ATTRIBUTE_USER,
Like::ATTRIBUTE_LIKEABLE_ID,
null,
Video::ATTRIBUTE_ID
)
->wherePivot(Like::ATTRIBUTE_LIKEABLE_TYPE, Relation::getMorphAlias(Video::class))
->withTimestamps();
}
/**
* Get the submissions that the user made.
*/
@@ -4,10 +4,14 @@ declare(strict_types=1);
namespace App\Models\Wiki\Anime\Theme;
use App\Concerns\Models\Aggregate\AggregatesLike;
use App\Concerns\Models\InteractsWithLikes;
use App\Concerns\Models\Reportable;
use App\Concerns\Models\SoftDeletes;
use App\Contracts\Http\Api\InteractsWithSchema;
use App\Contracts\Models\HasAggregateLikes;
use App\Contracts\Models\HasResources;
use App\Contracts\Models\Likeable;
use App\Contracts\Models\SoftDeletable;
use App\Enums\Models\Wiki\ThemeType;
use App\Events\Wiki\Anime\Theme\Entry\EntryCreated;
@@ -57,9 +61,11 @@ use Znck\Eloquent\Traits\BelongsToThrough as ZnckBelongsToThrough;
*
* @method static AnimeThemeEntryFactory factory(...$parameters)
*/
class AnimeThemeEntry extends BaseModel implements HasResources, InteractsWithSchema, SoftDeletable
class AnimeThemeEntry extends BaseModel implements HasAggregateLikes, HasResources, InteractsWithSchema, Likeable, SoftDeletable
{
use AggregatesLike;
use HasFactory;
use InteractsWithLikes;
use Reportable;
use Searchable;
use SoftDeletes;
+1 -7
View File
@@ -4,14 +4,10 @@ declare(strict_types=1);
namespace App\Models\Wiki;
use App\Concerns\Models\Aggregate\AggregatesLike;
use App\Concerns\Models\Aggregate\AggregatesView;
use App\Concerns\Models\InteractsWithLikes;
use App\Concerns\Models\Reportable;
use App\Concerns\Models\SoftDeletes;
use App\Contracts\Models\HasAggregateLikes;
use App\Contracts\Models\HasAggregateViews;
use App\Contracts\Models\Likeable;
use App\Contracts\Models\SoftDeletable;
use App\Contracts\Models\Streamable;
use App\Enums\Models\List\PlaylistVisibility;
@@ -63,12 +59,10 @@ use Illuminate\Support\Collection;
*
* @method static VideoFactory factory(...$parameters)
*/
class Video extends BaseModel implements HasAggregateLikes, HasAggregateViews, Likeable, SoftDeletable, Streamable
class Video extends BaseModel implements HasAggregateViews, SoftDeletable, Streamable
{
use AggregatesLike;
use AggregatesView;
use HasFactory;
use InteractsWithLikes;
use Reportable;
use Searchable;
use SoftDeletes;
+1 -1
View File
@@ -49,7 +49,7 @@ class ModerationRule implements ValidationRule
$flagged = Arr::get($response, 'results.0.flagged');
if (filled($flagged)) {
if ($flagged === true) {
$fail(__('validation.moderation', ['attribute' => $attribute]));
}
} catch (Exception $e) {
@@ -26,7 +26,7 @@ test('converts to avif', function () {
$result = $action->handle();
$this->assertTrue(Str::endsWith(($image->path), '.avif'));
$this->assertTrue(Str::endsWith(($image->refresh()->path), '.avif'));
$this->assertTrue($result->getStatus() === ActionStatus::PASSED);
$this->assertDatabaseCount(Image::class, 1);
$this->assertTrue($image->exists());
@@ -15,6 +15,7 @@ use App\Models\Wiki\Artist;
use App\Models\Wiki\Song;
use App\Models\Wiki\Song\Performance as PerformanceModel;
use Filament\Actions\Testing\TestAction;
use Illuminate\Support\Facades\DB;
use Livewire\Livewire;
use function Pest\Laravel\actingAs;
@@ -77,6 +78,10 @@ test('mount create action', function () {
});
test('mount edit action', function () {
if (DB::getDriverName() === 'sqlite') {
$this->markTestSkipped('SQLite does not support this test properly.');
}
$user = User::factory()
->withPermissions(
SpecialPermission::VIEW_FILAMENT->value,