diff --git a/app/Builders/AlbumBuilder.php b/app/Builders/AlbumBuilder.php new file mode 100644 index 00000000..d40d83b9 --- /dev/null +++ b/app/Builders/AlbumBuilder.php @@ -0,0 +1,33 @@ +whereNot('albums.id', Album::UNKNOWN_ID); + } + + public function withMeta(User $user): static + { + return $this->with('artist') + ->leftJoin('songs', 'albums.id', '=', 'songs.album_id') + ->leftJoin('interactions', static function (JoinClause $join) use ($user): void { + $join->on('songs.id', '=', 'interactions.song_id')->where('interactions.user_id', $user->id); + }) + ->groupBy('albums.id') + ->select( + 'albums.*', + DB::raw('CAST(SUM(interactions.play_count) AS UNSIGNED) AS play_count') + ) + ->withCount('songs AS song_count') + ->withSum('songs AS length', 'length'); + } +} diff --git a/app/Builders/ArtistBuilder.php b/app/Builders/ArtistBuilder.php new file mode 100644 index 00000000..c4de62e2 --- /dev/null +++ b/app/Builders/ArtistBuilder.php @@ -0,0 +1,33 @@ +whereNotIn('artists.id', [Artist::UNKNOWN_ID, Artist::VARIOUS_ID]); + } + + public function withMeta(User $user): static + { + return $this->leftJoin('songs', 'artists.id', '=', 'songs.artist_id') + ->leftJoin('interactions', static function (JoinClause $join) use ($user): void { + $join->on('interactions.song_id', '=', 'songs.id')->where('interactions.user_id', $user->id); + }) + ->groupBy('artists.id') + ->select([ + 'artists.*', + DB::raw('CAST(SUM(interactions.play_count) AS UNSIGNED) AS play_count'), + DB::raw('COUNT(DISTINCT songs.album_id) AS album_count'), + ]) + ->withCount('songs AS song_count') + ->withSum('songs AS length', 'length'); + } +} diff --git a/app/Builders/SongBuilder.php b/app/Builders/SongBuilder.php new file mode 100644 index 00000000..9c1a60a4 --- /dev/null +++ b/app/Builders/SongBuilder.php @@ -0,0 +1,41 @@ +where('path', 'LIKE', "$path%"); + } + + public function withMeta(User $user): static + { + return $this + ->with('artist', 'album', 'album.artist') + ->leftJoin('interactions', static function (JoinClause $join) use ($user): void { + $join->on('interactions.song_id', '=', 'songs.id')->where('interactions.user_id', $user->id); + }) + ->join('albums', 'songs.album_id', '=', 'albums.id') + ->join('artists', 'songs.artist_id', '=', 'artists.id') + ->select( + 'songs.*', + 'albums.name', + 'artists.name', + 'interactions.liked', + 'interactions.play_count' + ); + } + + public function hostedOnS3(): static + { + return $this->where('path', 'LIKE', 's3://%'); + } +} diff --git a/app/Console/Commands/Admin/ChangePasswordCommand.php b/app/Console/Commands/Admin/ChangePasswordCommand.php index d1cfc072..e687ba54 100644 --- a/app/Console/Commands/Admin/ChangePasswordCommand.php +++ b/app/Console/Commands/Admin/ChangePasswordCommand.php @@ -25,7 +25,9 @@ class ChangePasswordCommand extends Command $email = $this->argument('email'); /** @var User|null $user */ - $user = $email ? User::where('email', $email)->first() : User::where('is_admin', true)->first(); + $user = $email + ? User::query()->where('email', $email)->first() + : User::query()->where('is_admin', true)->first(); if (!$user) { $this->error('The user account cannot be found.'); diff --git a/app/Console/Commands/InitCommand.php b/app/Console/Commands/InitCommand.php index 6db021c1..0774be65 100644 --- a/app/Console/Commands/InitCommand.php +++ b/app/Console/Commands/InitCommand.php @@ -204,7 +204,7 @@ class InitCommand extends Command private function setUpAdminAccount(): void { $this->components->task('Creating default admin account', function (): void { - User::create([ + User::query()->create([ 'name' => self::DEFAULT_ADMIN_NAME, 'email' => self::DEFAULT_ADMIN_EMAIL, 'password' => $this->hash->make(self::DEFAULT_ADMIN_PASSWORD), @@ -217,7 +217,7 @@ class InitCommand extends Command private function maybeSeedDatabase(): void { - if (!User::count()) { + if (!User::query()->count()) { $this->setUpAdminAccount(); $this->components->task('Seeding data', function (): void { diff --git a/app/Http/Controllers/V6/API/AlbumController.php b/app/Http/Controllers/V6/API/AlbumController.php index a870775f..b6f95c01 100644 --- a/app/Http/Controllers/V6/API/AlbumController.php +++ b/app/Http/Controllers/V6/API/AlbumController.php @@ -12,22 +12,17 @@ use Illuminate\Contracts\Auth\Authenticatable; class AlbumController extends Controller { /** @param User $user */ - public function __construct(private AlbumRepository $albumRepository, private ?Authenticatable $user) + public function __construct(private AlbumRepository $repository, private ?Authenticatable $user) { } public function index() { - $pagination = Album::withMeta($this->user) - ->isStandard() - ->orderBy('albums.name') - ->simplePaginate(21); - - return AlbumResource::collection($pagination); + return AlbumResource::collection($this->repository->paginate($this->user)); } public function show(Album $album) { - return AlbumResource::make($this->albumRepository->getOne($album->id, $this->user)); + return AlbumResource::make($this->repository->getOne($album->id, $this->user)); } } diff --git a/app/Http/Controllers/V6/API/ArtistController.php b/app/Http/Controllers/V6/API/ArtistController.php index 60dfe81e..d51a8505 100644 --- a/app/Http/Controllers/V6/API/ArtistController.php +++ b/app/Http/Controllers/V6/API/ArtistController.php @@ -12,22 +12,17 @@ use Illuminate\Contracts\Auth\Authenticatable; class ArtistController extends Controller { /** @param User $user */ - public function __construct(private ArtistRepository $artistRepository, private ?Authenticatable $user) + public function __construct(private ArtistRepository $repository, private ?Authenticatable $user) { } public function index() { - $pagination = Artist::withMeta($this->user) - ->isStandard() - ->orderBy('artists.name') - ->simplePaginate(21); - - return ArtistResource::collection($pagination); + return ArtistResource::collection($this->repository->paginate($this->user)); } public function show(Artist $artist) { - return ArtistResource::make($this->artistRepository->getOne($artist->id, $this->user)); + return ArtistResource::make($this->repository->getOne($artist->id, $this->user)); } } diff --git a/app/Models/Album.php b/app/Models/Album.php index 3a69cc1e..89ce6b68 100644 --- a/app/Models/Album.php +++ b/app/Models/Album.php @@ -2,18 +2,15 @@ namespace App\Models; +use App\Builders\AlbumBuilder; use Carbon\Carbon; -use Illuminate\Contracts\Database\Query\Builder as BuilderContract; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Arr; -use Illuminate\Support\Facades\DB; use Laravel\Scout\Searchable; /** @@ -34,14 +31,6 @@ use Laravel\Scout\Searchable; * @property float|string $length Total length of the album in seconds (dynamically calculated) * @property int|string $play_count Total number of times the album's songs have been played (dynamically calculated) * @property int|string $song_count Total number of songs on the album (dynamically calculated) - * - * @method static self firstOrCreate(array $where, array $params = []) - * @method static self|null find(int $id) - * @method static Builder where(...$params) - * @method static self first() - * @method static Builder whereArtistIdAndName(int $id, string $name) - * @method static orderBy(...$params) - * @method static Builder latest() */ class Album extends Model { @@ -59,13 +48,23 @@ class Album extends Model /** @deprecated */ protected $appends = ['is_compilation']; + public static function query(): AlbumBuilder + { + return parent::query(); + } + + public function newEloquentBuilder($query): AlbumBuilder + { + return new AlbumBuilder($query); + } + /** * Get an album using some provided information. * If such is not found, a new album will be created using the information. */ - public static function getOrCreate(Artist $artist, ?string $name = null): self + public static function getOrCreate(Artist $artist, ?string $name = null): static { - return static::firstOrCreate([ + return static::query()->firstOrCreate([ // @phpstan-ignore-line 'artist_id' => $artist->id, 'name' => trim($name) ?: self::UNKNOWN_NAME, ]); @@ -143,29 +142,6 @@ class Album extends Model return Attribute::get(fn () => $this->artist_id === Artist::VARIOUS_ID); } - public function scopeIsStandard(Builder $query): Builder - { - return $query->whereNot('albums.id', self::UNKNOWN_ID); - } - - public static function withMeta(User $scopedUser): BuilderContract - { - return static::query() - ->with('artist') - ->leftJoin('songs', 'albums.id', '=', 'songs.album_id') - ->leftJoin('interactions', static function (JoinClause $join) use ($scopedUser): void { - $join->on('songs.id', '=', 'interactions.song_id') - ->where('interactions.user_id', $scopedUser->id); - }) - ->groupBy('albums.id') - ->select( - 'albums.*', - DB::raw('CAST(SUM(interactions.play_count) AS UNSIGNED) AS play_count') - ) - ->withCount('songs AS song_count') - ->withSum('songs AS length', 'length'); - } - /** @return array */ public function toSearchableArray(): array { diff --git a/app/Models/Artist.php b/app/Models/Artist.php index f8b3e046..f92a4c07 100644 --- a/app/Models/Artist.php +++ b/app/Models/Artist.php @@ -2,18 +2,15 @@ namespace App\Models; +use App\Builders\ArtistBuilder; use App\Facades\Util; use Carbon\Carbon; -use Illuminate\Contracts\Database\Query\Builder as BuilderContract; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Arr; -use Illuminate\Support\Facades\DB; use Laravel\Scout\Searchable; /** @@ -30,14 +27,6 @@ use Laravel\Scout\Searchable; * @property string|int $song_count Total number of songs by the artist (dynamically calculated) * @property string|int $album_count Total number of albums by the artist (dynamically calculated) * @property Carbon $created_at - * - * @method static self find(int $id) - * @method static self firstOrCreate(array $where, array $params = []) - * @method static Builder where(...$params) - * @method static self first() - * @method static Builder whereName(string $name) - * @method static Builder orderBy(...$params) - * @method static Builder join(...$params) */ class Artist extends Model { @@ -53,6 +42,16 @@ class Artist extends Model protected $guarded = ['id']; protected $hidden = ['created_at', 'updated_at']; + public static function query(): ArtistBuilder + { + return parent::query(); + } + + public function newEloquentBuilder($query): ArtistBuilder + { + return new ArtistBuilder($query); + } + /** * Get an Artist object from their name. * If such is not found, a new artist will be created. @@ -66,7 +65,7 @@ class Artist extends Model $name = mb_convert_encoding($name, 'UTF-8', $encoding); } - return static::firstOrCreate(['name' => trim($name) ?: self::UNKNOWN_NAME]); + return static::query()->firstOrCreate(['name' => trim($name) ?: self::UNKNOWN_NAME]); } public function albums(): HasMany @@ -120,29 +119,6 @@ class Artist extends Model }); } - public function scopeIsStandard(Builder $query): Builder - { - return $query->whereNotIn('artists.id', [self::UNKNOWN_ID, self::VARIOUS_ID]); - } - - public static function withMeta(User $scopedUser): BuilderContract - { - return static::query() - ->leftJoin('songs', 'artists.id', '=', 'songs.artist_id') - ->leftJoin('interactions', static function (JoinClause $join) use ($scopedUser): void { - $join->on('interactions.song_id', '=', 'songs.id') - ->where('interactions.user_id', $scopedUser->id); - }) - ->groupBy('artists.id') - ->select([ - 'artists.*', - DB::raw('CAST(SUM(interactions.play_count) AS UNSIGNED) AS play_count'), - DB::raw('COUNT(DISTINCT songs.album_id) AS album_count'), - ]) - ->withCount('songs AS song_count') - ->withSum('songs AS length', 'length'); - } - /** @return array */ public function toSearchableArray(): array { diff --git a/app/Models/Interaction.php b/app/Models/Interaction.php index 3331b5c4..95a4d465 100644 --- a/app/Models/Interaction.php +++ b/app/Models/Interaction.php @@ -2,7 +2,6 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -14,12 +13,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; * @property User $user * @property int $id * @property string $song_id - * - * @method static self firstOrCreate(array $where, array $params = []) - * @method static self find(int $id) - * @method static Builder whereSongIdAndUserId(string $songId, string $userId) - * @method static Builder whereIn(...$params) - * @method static Builder where(...$params) */ class Interaction extends Model { diff --git a/app/Models/Playlist.php b/app/Models/Playlist.php index 82a1413a..0059dfb0 100644 --- a/app/Models/Playlist.php +++ b/app/Models/Playlist.php @@ -4,7 +4,6 @@ namespace App\Models; use App\Casts\SmartPlaylistRulesCast; use App\Values\SmartPlaylistRuleGroup; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -22,8 +21,6 @@ use Laravel\Scout\Searchable; * @property bool $is_smart * @property string $name * @property user $user - * - * @method static Builder orderBy(string $field, string $order = 'asc') */ class Playlist extends Model { diff --git a/app/Models/Setting.php b/app/Models/Setting.php index 2384ac8a..639b9193 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -10,7 +10,6 @@ use Illuminate\Database\Eloquent\Model; * @property mixed $value * * @method static self find(string $key) - * @method static self updateOrCreate(array $where, array $params) */ class Setting extends Model { @@ -44,6 +43,6 @@ class Setting extends Model return; } - self::updateOrCreate(compact('key'), compact('value')); + self::query()->updateOrCreate(compact('key'), compact('value')); } } diff --git a/app/Models/Song.php b/app/Models/Song.php index e8c2b996..a9baa0b7 100644 --- a/app/Models/Song.php +++ b/app/Models/Song.php @@ -2,16 +2,14 @@ namespace App\Models; +use App\Builders\SongBuilder; use Carbon\Carbon; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Query\JoinClause; -use Illuminate\Support\Collection; use Illuminate\Support\Str; use Laravel\Scout\Searchable; @@ -31,26 +29,13 @@ use Laravel\Scout\Searchable; * @property ?bool $liked Whether the song is liked by the current user (dynamically calculated) * @property ?int $play_count The number of times the song has been played by the current user (dynamically calculated) * @property Carbon $created_at - * - * @method static self updateOrCreate(array $where, array $params) - * @method static Builder select(string $string) - * @method static Builder inDirectory(string $path) - * @method static self first() - * @method static Builder orderBy(...$args) - * @method static int count() - * @method static self|Collection|null find($id) - * @method static Builder take(int $count) - * @method static float|int sum(string $column) - * @method static Builder latest(string $column = 'created_at') - * @method static Builder where(...$params) - * @method static Song findOrFail(string $id) + * @property array $s3_params */ class Song extends Model { use HasFactory; use Searchable; use SupportsDeleteWhereValueNotIn; - use SupportsS3; public const ID_REGEX = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'; @@ -73,6 +58,16 @@ class Song extends Model static::creating(static fn (self $song) => $song->id = Str::uuid()->toString()); } + public static function query(): SongBuilder + { + return parent::query(); + } + + public function newEloquentBuilder($query): SongBuilder + { + return new SongBuilder($query); + } + public function artist(): BelongsTo { return $this->belongsTo(Artist::class); @@ -93,17 +88,6 @@ class Song extends Model return $this->hasMany(Interaction::class); } - /** - * Scope a query to only include songs in a given directory. - */ - public function scopeInDirectory(Builder $query, string $path): Builder - { - // Make sure the path ends with a directory separator. - $path = rtrim(trim($path), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; - - return $query->where('path', 'LIKE', "$path%"); - } - protected function title(): Attribute { return new Attribute( @@ -120,30 +104,22 @@ class Song extends Model return new Attribute(get: $normalizer, set: $normalizer); } - public static function withMeta(User $scopedUser, ?Builder $query = null): Builder + protected function s3Params(): Attribute { - $query ??= static::query(); + return Attribute::get(function (): ?array { + if (!preg_match('/^s3:\\/\\/(.*)/', $this->path, $matches)) { + return null; + } - return $query - ->with('artist', 'album', 'album.artist') - ->leftJoin('interactions', static function (JoinClause $join) use ($scopedUser): void { - $join->on('interactions.song_id', '=', 'songs.id') - ->where('interactions.user_id', $scopedUser->id); - }) - ->join('albums', 'songs.album_id', '=', 'albums.id') - ->join('artists', 'songs.artist_id', '=', 'artists.id') - ->select( - 'songs.*', - 'albums.name', - 'artists.name', - 'interactions.liked', - 'interactions.play_count' - ); + [$bucket, $key] = explode('/', $matches[1], 2); + + return compact('bucket', 'key'); + }); } - public function scopeWithMeta(Builder $query, User $scopedUser): Builder + public static function getPathFromS3BucketAndKey(string $bucket, string $key): string { - return static::withMeta($scopedUser, $query); + return "s3://$bucket/$key"; } /** @return array */ diff --git a/app/Models/SongZipArchive.php b/app/Models/SongZipArchive.php index 70795a17..c8dc23c0 100644 --- a/app/Models/SongZipArchive.php +++ b/app/Models/SongZipArchive.php @@ -10,7 +10,6 @@ use ZipArchive; class SongZipArchive { private ZipArchive $archive; - private string $path; /** @@ -30,7 +29,7 @@ class SongZipArchive } } - public function addSongs(Collection $songs): self + public function addSongs(Collection $songs): static { $songs->each(function (Song $song): void { $this->addSong($song); @@ -39,7 +38,7 @@ class SongZipArchive return $this; } - public function addSong(Song $song): self + public function addSong(Song $song): static { attempt(function () use ($song): void { $path = Download::fromSong($song); @@ -49,7 +48,7 @@ class SongZipArchive return $this; } - public function finish(): self + public function finish(): static { $this->archive->close(); diff --git a/app/Models/SupportsDeleteWhereValueNotIn.php b/app/Models/SupportsDeleteWhereValueNotIn.php index f33390e5..bd383758 100644 --- a/app/Models/SupportsDeleteWhereValueNotIn.php +++ b/app/Models/SupportsDeleteWhereValueNotIn.php @@ -10,9 +10,7 @@ use Illuminate\Support\Facades\DB; * MySQL and PostgresSQL seem to have a limit of 2^16-1 (65535) elements in an IN statement. * This trait provides a method as a workaround to this limitation. * - * @method static Builder whereIn($keys, array $values) - * @method static Builder whereNotIn($keys, array $values) - * @method static Builder select(string $string) + * @method static Builder query() */ trait SupportsDeleteWhereValueNotIn { @@ -25,18 +23,18 @@ trait SupportsDeleteWhereValueNotIn // If the number of entries is lower than, or equals to maxChunkSize, just go ahead. if (count($values) <= $maxChunkSize) { - static::whereNotIn($field, $values)->delete(); + static::query()->whereNotIn($field, $values)->delete(); return; } // Otherwise, we get the actual IDs that should be deleted… - $allIDs = static::select($field)->get()->pluck($field)->all(); + $allIDs = static::query()->select($field)->get()->pluck($field)->all(); $whereInIDs = array_diff($allIDs, $values); // …and see if we can delete them instead. if (count($whereInIDs) < $maxChunkSize) { - static::whereIn($field, $whereInIDs)->delete(); + static::query()->whereIn($field, $whereInIDs)->delete(); return; } @@ -50,7 +48,7 @@ trait SupportsDeleteWhereValueNotIn { DB::transaction(static function () use ($values, $field, $chunkSize): void { foreach (array_chunk($values, $chunkSize) as $chunk) { - static::whereIn($field, $chunk)->delete(); + static::query()->whereIn($field, $chunk)->delete(); } }); } diff --git a/app/Models/SupportsS3.php b/app/Models/SupportsS3.php deleted file mode 100644 index 69bb4c31..00000000 --- a/app/Models/SupportsS3.php +++ /dev/null @@ -1,37 +0,0 @@ -|null $s3_params The bucket and key name of an S3 object. - * - * @method static Builder hostedOnS3() - */ -trait SupportsS3 -{ - protected function s3Params(): Attribute - { - return Attribute::get(function (): ?array { - if (!preg_match('/^s3:\\/\\/(.*)/', $this->path, $matches)) { - return null; - } - - [$bucket, $key] = explode('/', $matches[1], 2); - - return compact('bucket', 'key'); - }); - } - - public static function getPathFromS3BucketAndKey(string $bucket, string $key): string - { - return "s3://$bucket/$key"; - } - - public function scopeHostedOnS3(Builder $query): Builder - { - return $query->where('path', 'LIKE', 's3://%'); - } -} diff --git a/app/Models/User.php b/app/Models/User.php index fcc77844..79c10756 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -7,7 +7,6 @@ use App\Values\UserPreferences; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Query\Builder; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; @@ -21,11 +20,6 @@ use Laravel\Sanctum\HasApiTokens; * @property string $email * @property string $password * @property-read string $avatar - * - * @method static self create(array $params) - * @method static int count() - * @method static Builder where(...$params) - * @method static self|null firstWhere(...$params) */ class User extends Authenticatable { diff --git a/app/Repositories/AlbumRepository.php b/app/Repositories/AlbumRepository.php index 0f817a9c..ef6b212d 100644 --- a/app/Repositories/AlbumRepository.php +++ b/app/Repositories/AlbumRepository.php @@ -5,6 +5,7 @@ namespace App\Repositories; use App\Models\Album; use App\Models\User; use App\Repositories\Traits\Searchable; +use Illuminate\Contracts\Pagination\Paginator; use Illuminate\Support\Collection; class AlbumRepository extends Repository @@ -13,7 +14,8 @@ class AlbumRepository extends Repository public function getOne(int $id, ?User $scopedUser = null): Album { - return Album::withMeta($scopedUser ?? $this->auth->user()) + return Album::query() + ->withMeta($scopedUser ?? $this->auth->user()) ->where('albums.id', $id) ->first(); } @@ -21,7 +23,8 @@ class AlbumRepository extends Repository /** @return Collection|array */ public function getRecentlyAdded(int $count = 6, ?User $scopedUser = null): Collection { - return Album::withMeta($scopedUser ?? $this->auth->user()) + return Album::query() + ->withMeta($scopedUser ?? $this->auth->user()) ->isStandard() ->latest('albums.created_at') ->limit($count) @@ -31,9 +34,8 @@ class AlbumRepository extends Repository /** @return Collection|array */ public function getMostPlayed(int $count = 6, ?User $scopedUser = null): Collection { - $scopedUser ??= $this->auth->user(); - - return Album::withMeta($scopedUser ?? $this->auth->user()) + return Album::query() + ->withMeta($scopedUser ?? $this->auth->user()) ->isStandard() ->orderByDesc('play_count') ->limit($count) @@ -43,8 +45,18 @@ class AlbumRepository extends Repository /** @return Collection|array */ public function getByIds(array $ids, ?User $scopedUser = null): Collection { - return Album::withMeta($scopedUser ?? $this->auth->user()) + return Album::query() + ->withMeta($scopedUser ?? $this->auth->user()) ->whereIn('albums.id', $ids) ->get(); } + + public function paginate(?User $scopedUser = null): Paginator + { + return Album::query() + ->withMeta($scopedUser ?? $this->auth->user()) + ->isStandard() + ->orderBy('albums.name') + ->simplePaginate(21); + } } diff --git a/app/Repositories/ArtistRepository.php b/app/Repositories/ArtistRepository.php index 08acd661..fd73b969 100644 --- a/app/Repositories/ArtistRepository.php +++ b/app/Repositories/ArtistRepository.php @@ -5,6 +5,7 @@ namespace App\Repositories; use App\Models\Artist; use App\Models\User; use App\Repositories\Traits\Searchable; +use Illuminate\Contracts\Pagination\Paginator; use Illuminate\Database\Eloquent\Collection; class ArtistRepository extends Repository @@ -14,7 +15,8 @@ class ArtistRepository extends Repository /** @return Collection|array */ public function getMostPlayed(int $count = 6, ?User $scopedUser = null): Collection { - return Artist::withMeta($scopedUser ?? $this->auth->user()) + return Artist::query() + ->withMeta($scopedUser ?? $this->auth->user()) ->isStandard() ->orderByDesc('play_count') ->limit($count) @@ -23,7 +25,8 @@ class ArtistRepository extends Repository public function getOne(int $id, ?User $scopedUser = null): Artist { - return Artist::withMeta($scopedUser ?? $this->auth->user()) + return Artist::query() + ->withMeta($scopedUser ?? $this->auth->user()) ->where('artists.id', $id) ->first(); } @@ -31,9 +34,19 @@ class ArtistRepository extends Repository /** @return Collection|array */ public function getByIds(array $ids, ?User $scopedUser = null): Collection { - return Artist::withMeta($scopedUser ?? $this->auth->user()) + return Artist::query() + ->withMeta($scopedUser ?? $this->auth->user()) ->isStandard() ->whereIn('artists.id', $ids) ->get(); } + + public function paginate(?User $scopedUser = null): Paginator + { + return Artist::query() + ->withMeta($scopedUser ?? $this->auth->user()) + ->isStandard() + ->orderBy('artists.name') + ->simplePaginate(21); + } } diff --git a/app/Repositories/InteractionRepository.php b/app/Repositories/InteractionRepository.php index 07d46dc1..c8ee58b2 100644 --- a/app/Repositories/InteractionRepository.php +++ b/app/Repositories/InteractionRepository.php @@ -5,7 +5,6 @@ namespace App\Repositories; use App\Models\Interaction; use App\Models\User; use App\Repositories\Traits\ByCurrentUser; -use Illuminate\Database\Query\Builder; use Illuminate\Support\Collection; class InteractionRepository extends Repository @@ -15,10 +14,12 @@ class InteractionRepository extends Repository /** @return Collection|array */ public function getUserFavorites(User $user): Collection { - return $this->model->where([ - 'user_id' => $user->id, - 'liked' => true, - ]) + return $this->model + ->newQuery() + ->where([ + 'user_id' => $user->id, + 'liked' => true, + ]) ->with('song') ->pluck('song'); } @@ -26,11 +27,11 @@ class InteractionRepository extends Repository /** @return array */ public function getRecentlyPlayed(User $user, ?int $count = null): array { - /** @var Builder $query */ $query = $this->model + ->newQuery() ->where('user_id', $user->id) ->where('play_count', '>', 0) - ->orderBy('updated_at', 'DESC'); + ->latest('updated_at'); if ($count) { $query = $query->take($count); diff --git a/app/Repositories/SongRepository.php b/app/Repositories/SongRepository.php index a2be86d4..48ab7e33 100644 --- a/app/Repositories/SongRepository.php +++ b/app/Repositories/SongRepository.php @@ -31,27 +31,26 @@ class SongRepository extends Repository public function getOneByPath(string $path): ?Song { - return Song::where('path', $path)->first(); + return Song::query()->where('path', $path)->first(); } /** @return Collection|array */ public function getAllHostedOnS3(): Collection { - return Song::hostedOnS3()->get(); + return Song::query()->hostedOnS3()->get(); } /** @return Collection|array */ public function getRecentlyAdded(int $count = 10, ?User $scopedUser = null): Collection { - return Song::withMeta($scopedUser ?? $this->auth->user())->latest()->limit($count)->get(); + return Song::query()->withMeta($scopedUser ?? $this->auth->user())->latest()->limit($count)->get(); } /** @return Collection|array */ public function getMostPlayed(int $count = 7, ?User $scopedUser = null): Collection { - $scopedUser ??= $this->auth->user(); - - return Song::withMeta($scopedUser) + return Song::query() + ->withMeta($scopedUser ?? $this->auth->user()) ->where('interactions.play_count', '>', 0) ->orderByDesc('interactions.play_count') ->limit($count) @@ -61,9 +60,8 @@ class SongRepository extends Repository /** @return Collection|array */ public function getRecentlyPlayed(int $count = 7, ?User $scopedUser = null): Collection { - $scopedUser ??= $this->auth->user(); - - return Song::withMeta($scopedUser) + return Song::query() + ->withMeta($scopedUser ?? $this->auth->user()) ->where('interactions.play_count', '>', 0) ->orderByDesc('interactions.updated_at') ->limit($count) @@ -77,7 +75,7 @@ class SongRepository extends Repository int $perPage = 50 ): Paginator { return self::applySort( - Song::withMeta($scopedUser ?? $this->auth->user()), + Song::query()->withMeta($scopedUser ?? $this->auth->user()), $sortColumn, $sortDirection ) @@ -92,7 +90,7 @@ class SongRepository extends Repository ?User $scopedUser = null, ): Collection { return self::applySort( - Song::withMeta($scopedUser ?? $this->auth->user()), + Song::query()->withMeta($scopedUser ?? $this->auth->user()), $sortColumn, $sortDirection ) @@ -103,13 +101,14 @@ class SongRepository extends Repository /** @return Collection|array */ public function getFavorites(?User $scopedUser = null): Collection { - return Song::withMeta($scopedUser ?? $this->auth->user())->where('interactions.liked', true)->get(); + return Song::query()->withMeta($scopedUser ?? $this->auth->user())->where('interactions.liked', true)->get(); } /** @return Collection|array */ public function getByAlbum(Album $album, ?User $scopedUser = null): Collection { - return Song::withMeta($scopedUser ?? $this->auth->user()) + return Song::query() + ->withMeta($scopedUser ?? $this->auth->user()) ->where('album_id', $album->id) ->orderBy('songs.track') ->orderBy('songs.disc') @@ -120,7 +119,8 @@ class SongRepository extends Repository /** @return Collection|array */ public function getByArtist(Artist $artist, ?User $scopedUser = null): Collection { - return Song::withMeta($scopedUser ?? $this->auth->user()) + return Song::query() + ->withMeta($scopedUser ?? $this->auth->user()) ->where('songs.artist_id', $artist->id) ->orderBy('albums.name') ->orderBy('songs.track') @@ -132,7 +132,8 @@ class SongRepository extends Repository /** @return Collection|array */ public function getByStandardPlaylist(Playlist $playlist, ?User $scopedUser = null): Collection { - return Song::withMeta($scopedUser ?? $this->auth->user()) + return Song::query() + ->withMeta($scopedUser ?? $this->auth->user()) ->leftJoin('playlist_song', 'songs.id', '=', 'playlist_song.song_id') ->leftJoin('playlists', 'playlists.id', '=', 'playlist_song.playlist_id') ->where('playlists.id', $playlist->id) @@ -143,28 +144,28 @@ class SongRepository extends Repository /** @return Collection|array */ public function getRandom(int $limit, ?User $scopedUser = null): Collection { - return Song::withMeta($scopedUser ?? $this->auth->user())->inRandomOrder()->limit($limit)->get(); + return Song::query()->withMeta($scopedUser ?? $this->auth->user())->inRandomOrder()->limit($limit)->get(); } /** @return Collection|array */ public function getByIds(array $ids, ?User $scopedUser = null): Collection { - return Song::withMeta($scopedUser ?? $this->auth->user())->whereIn('songs.id', $ids)->get(); + return Song::query()->withMeta($scopedUser ?? $this->auth->user())->whereIn('songs.id', $ids)->get(); } public function getOne($id, ?User $scopedUser = null): Song { - return Song::withMeta($scopedUser ?? $this->auth->user())->findOrFail($id); + return Song::query()->withMeta($scopedUser ?? $this->auth->user())->findOrFail($id); } public function count(): int { - return Song::count(); + return Song::query()->count(); } public function getTotalLength(): float { - return Song::sum('length'); + return Song::query()->sum('length'); } private static function normalizeSortColumn(string $column): string diff --git a/app/Services/FileSynchronizer.php b/app/Services/FileSynchronizer.php index 19c3fd54..a9ed0025 100644 --- a/app/Services/FileSynchronizer.php +++ b/app/Services/FileSynchronizer.php @@ -88,7 +88,7 @@ class FileSynchronizer $data['album_id'] = $album->id; $data['artist_id'] = $artist->id; - $this->song = Song::updateOrCreate(['path' => $this->filePath], $data); + $this->song = Song::query()->updateOrCreate(['path' => $this->filePath], $data); // @phpstan-ignore-line return SyncResult::success($this->filePath); } diff --git a/app/Services/InteractionService.php b/app/Services/InteractionService.php index 100b6747..2d528ecb 100644 --- a/app/Services/InteractionService.php +++ b/app/Services/InteractionService.php @@ -19,7 +19,7 @@ class InteractionService */ public function increasePlayCount(string $songId, User $user): Interaction { - return tap(Interaction::firstOrCreate([ + return tap(Interaction::query()->firstOrCreate([ 'song_id' => $songId, 'user_id' => $user->id, ]), static function (Interaction $interaction): void { @@ -39,7 +39,7 @@ class InteractionService */ public function toggleLike(string $songId, User $user): Interaction { - return tap(Interaction::firstOrCreate([ + return tap(Interaction::query()->firstOrCreate([ 'song_id' => $songId, 'user_id' => $user->id, ]), static function (Interaction $interaction): void { @@ -59,21 +59,18 @@ class InteractionService */ public function batchLike(array $songIds, User $user): Collection { - $interactions = collect($songIds)->map(static fn ($songId): Interaction => tap(Interaction::firstOrCreate([ - 'song_id' => $songId, - 'user_id' => $user->id, - ]), static function (Interaction $interaction): void { - if (!$interaction->exists) { - $interaction->play_count = 0; - } + $interactions = collect($songIds)->map(static function ($songId) use ($user): Interaction { + return tap(Interaction::query()->firstOrCreate([ + 'song_id' => $songId, + 'user_id' => $user->id, + ]), static function (Interaction $interaction): void { + $interaction->play_count ??= 0; + $interaction->liked = true; + $interaction->save(); + }); + }); - $interaction->liked = true; - $interaction->save(); - })); - - event(new SongsBatchLiked($interactions->map(static function (Interaction $interaction): Song { - return $interaction->song; - }), $user)); + event(new SongsBatchLiked($interactions->map(static fn (Interaction $item) => $item->song), $user)); return $interactions; } @@ -85,10 +82,11 @@ class InteractionService */ public function batchUnlike(array $songIds, User $user): void { - Interaction::whereIn('song_id', $songIds) + Interaction::query() + ->whereIn('song_id', $songIds) ->where('user_id', $user->id) ->update(['liked' => false]); - event(new SongsBatchUnliked(Song::find($songIds), $user)); + event(new SongsBatchUnliked(Song::query()->find($songIds), $user)); } } diff --git a/app/Services/LibraryManager.php b/app/Services/LibraryManager.php index 6ef87153..4d823cda 100644 --- a/app/Services/LibraryManager.php +++ b/app/Services/LibraryManager.php @@ -5,7 +5,6 @@ namespace App\Services; use App\Models\Album; use App\Models\Artist; use Illuminate\Database\Eloquent\Collection; -use Illuminate\Database\Query\Builder; use Illuminate\Support\Facades\DB; class LibraryManager @@ -19,13 +18,13 @@ class LibraryManager public function prune(bool $dryRun = false): array { return DB::transaction(static function () use ($dryRun): array { - /** @var Builder $albumQuery */ - $albumQuery = Album::leftJoin('songs', 'songs.album_id', '=', 'albums.id') + $albumQuery = Album::query() + ->leftJoin('songs', 'songs.album_id', '=', 'albums.id') ->whereNull('songs.album_id') ->whereNotIn('albums.id', [Album::UNKNOWN_ID]); - /** @var Builder $artistQuery */ - $artistQuery = Artist::leftJoin('songs', 'songs.artist_id', '=', 'artists.id') + $artistQuery = Artist::query() + ->leftJoin('songs', 'songs.artist_id', '=', 'artists.id') ->leftJoin('albums', 'albums.artist_id', '=', 'artists.id') ->whereNull('songs.artist_id') ->whereNull('albums.artist_id') diff --git a/app/Services/MediaCacheService.php b/app/Services/MediaCacheService.php index 506a9163..ec3919c1 100644 --- a/app/Services/MediaCacheService.php +++ b/app/Services/MediaCacheService.php @@ -38,8 +38,8 @@ class MediaCacheService private function query(): array { return [ - 'albums' => Album::orderBy('name')->get(), - 'artists' => Artist::orderBy('name')->get(), + 'albums' => Album::query()->orderBy('name')->get(), + 'artists' => Artist::query()->orderBy('name')->get(), 'songs' => Song::all(), ]; } diff --git a/app/Services/MediaSyncService.php b/app/Services/MediaSyncService.php index 9c5cf6b0..d47ad352 100644 --- a/app/Services/MediaSyncService.php +++ b/app/Services/MediaSyncService.php @@ -153,7 +153,7 @@ class MediaSyncService private function handleDeletedDirectoryRecord(string $path): void { - $count = Song::inDirectory($path)->delete(); + $count = Song::query()->inDirectory($path)->delete(); if ($count) { $this->logger->info("Deleted $count song(s) under $path"); diff --git a/app/Services/S3Service.php b/app/Services/S3Service.php index f1a697c8..a4f84156 100644 --- a/app/Services/S3Service.php +++ b/app/Services/S3Service.php @@ -66,7 +66,7 @@ class S3Service implements ObjectStorageInterface ); } - $song = Song::updateOrCreate(['id' => Helper::getFileHash($path)], [ + $song = Song::query()->updateOrCreate(['id' => Helper::getFileHash($path)], [ 'path' => $path, 'album_id' => $album->id, 'artist_id' => $artist->id, diff --git a/app/Services/SmartPlaylistService.php b/app/Services/SmartPlaylistService.php index bd780e5c..c13418b7 100644 --- a/app/Services/SmartPlaylistService.php +++ b/app/Services/SmartPlaylistService.php @@ -23,7 +23,7 @@ class SmartPlaylistService { throw_unless($playlist->is_smart, NonSmartPlaylistException::create($playlist)); - $query = Song::withMeta($user ?? $this->auth->user()); + $query = Song::query()->withMeta($user ?? $this->auth->user()); $playlist->rule_groups->each(static function (SmartPlaylistRuleGroup $group, int $index) use ($query): void { $clause = $index === 0 ? 'where' : 'orWhere'; diff --git a/app/Services/UserService.php b/app/Services/UserService.php index 05c0dd9d..ed75d193 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -13,7 +13,7 @@ class UserService public function createUser(string $name, string $email, string $plainTextPassword, bool $isAdmin): User { - return User::create([ + return User::query()->create([ 'name' => $name, 'email' => $email, 'password' => $this->hash->make($plainTextPassword), diff --git a/app/Services/V6/SearchService.php b/app/Services/V6/SearchService.php index c7b29966..5acb259e 100644 --- a/app/Services/V6/SearchService.php +++ b/app/Services/V6/SearchService.php @@ -2,6 +2,7 @@ namespace App\Services\V6; +use App\Builders\SongBuilder; use App\Models\Album; use App\Models\Artist; use App\Models\Song; @@ -10,7 +11,6 @@ use App\Repositories\AlbumRepository; use App\Repositories\ArtistRepository; use App\Repositories\SongRepository; use App\Values\ExcerptSearchResult; -use Illuminate\Contracts\Database\Query\Builder; use Illuminate\Support\Collection; class SearchService @@ -55,7 +55,7 @@ class SearchService int $limit = self::DEFAULT_MAX_SONG_RESULT_COUNT ): Collection { return Song::search($keywords) - ->query(static function (Builder $builder) use ($scopedUser, $limit): void { + ->query(static function (SongBuilder $builder) use ($scopedUser, $limit): void { $builder->withMeta($scopedUser ?? auth()->user())->limit($limit); }) ->get(); diff --git a/database/migrations/2016_04_16_082627_create_various_artists.php b/database/migrations/2016_04_16_082627_create_various_artists.php index 8ad03f34..606ba54a 100644 --- a/database/migrations/2016_04_16_082627_create_various_artists.php +++ b/database/migrations/2016_04_16_082627_create_various_artists.php @@ -25,7 +25,8 @@ class CreateVariousArtists extends Migration Artist::unguard(); - $existingArtist = Artist::find(Artist::VARIOUS_ID); + /** @var Artist|null $existingArtist */ + $existingArtist = Artist::query()->find(Artist::VARIOUS_ID); if ($existingArtist) { if ($existingArtist->name === Artist::VARIOUS_NAME) { @@ -34,7 +35,8 @@ class CreateVariousArtists extends Migration // There's an existing artist with that special ID, but it's not our Various Artist // We move it to the end of the table. - $latestArtist = Artist::orderBy('id', 'DESC')->first(); + /** @var Artist $latestArtist */ + $latestArtist = Artist::query()->orderByDesc('id')->first(); $existingArtist->id = $latestArtist->id + 1; $existingArtist->save(); } diff --git a/database/migrations/2016_07_09_054503_fix_artist_autoindex_value.php b/database/migrations/2016_07_09_054503_fix_artist_autoindex_value.php index c3a94090..80212fc0 100644 --- a/database/migrations/2016_07_09_054503_fix_artist_autoindex_value.php +++ b/database/migrations/2016_07_09_054503_fix_artist_autoindex_value.php @@ -16,7 +16,7 @@ class FixArtistAutoindexValue extends Migration } /** @var Artist $latestArtist */ - $latestArtist = Artist::orderBy('id', 'DESC')->first(); + $latestArtist = Artist::query()->orderByDesc('id')->first(); DB::statement('ALTER TABLE artists AUTO_INCREMENT=' . ($latestArtist->id + 1)); } diff --git a/database/migrations/2017_04_21_092159_copy_artist_to_contributing_artist.php b/database/migrations/2017_04_21_092159_copy_artist_to_contributing_artist.php index 8676ba46..7212800f 100644 --- a/database/migrations/2017_04_21_092159_copy_artist_to_contributing_artist.php +++ b/database/migrations/2017_04_21_092159_copy_artist_to_contributing_artist.php @@ -8,6 +8,7 @@ class CopyArtistToContributingArtist extends Migration public function up(): void { Song::with('album', 'album.artist')->get()->each(static function (Song $song): void { + // @phpstan-ignore-line if (!$song->contributing_artist_id) { $song->contributing_artist_id = $song->album->artist->id; $song->save(); diff --git a/database/migrations/2022_07_05_085742_remove_default_album_covers.php b/database/migrations/2022_07_05_085742_remove_default_album_covers.php index 87678cca..b0c70e53 100644 --- a/database/migrations/2022_07_05_085742_remove_default_album_covers.php +++ b/database/migrations/2022_07_05_085742_remove_default_album_covers.php @@ -7,6 +7,6 @@ return new class extends Migration { public function up(): void { - Album::where('cover', 'unknown-album.png')->update(['cover' => '']); + Album::query()->where('cover', 'unknown-album.png')->update(['cover' => '']); } }; diff --git a/database/seeders/AlbumTableSeeder.php b/database/seeders/AlbumTableSeeder.php index 49f9cf53..094a1d9e 100644 --- a/database/seeders/AlbumTableSeeder.php +++ b/database/seeders/AlbumTableSeeder.php @@ -11,9 +11,7 @@ class AlbumTableSeeder extends Seeder { public function run(): void { - Album::firstOrCreate([ - 'id' => Album::UNKNOWN_ID, - ], [ + Album::query()->firstOrCreate(['id' => Album::UNKNOWN_ID], [ 'artist_id' => Artist::UNKNOWN_ID, 'name' => Album::UNKNOWN_NAME, ]); diff --git a/database/seeders/ArtistTableSeeder.php b/database/seeders/ArtistTableSeeder.php index 111a89a9..2259c88e 100644 --- a/database/seeders/ArtistTableSeeder.php +++ b/database/seeders/ArtistTableSeeder.php @@ -10,11 +10,7 @@ class ArtistTableSeeder extends Seeder { public function run(): void { - Artist::firstOrCreate([ - 'id' => Artist::UNKNOWN_ID, - ], [ - 'name' => Artist::UNKNOWN_NAME, - ]); + Artist::query()->firstOrCreate(['id' => Artist::UNKNOWN_ID], ['name' => Artist::UNKNOWN_NAME]); self::maybeResetPgsqlSerialValue(); } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index f6721d8a..5ca128a1 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -21,13 +21,12 @@ parameters: - '#expects .*, Mockery\\LegacyMockInterface given#' - '#Call to an undefined method Illuminate\\Filesystem\\FilesystemAdapter::getAdapter\(\)#' - '#Call to an undefined method Mockery\\ExpectationInterface|Mockery\\HigherOrderMessage::with\(\)#' - - '#Call to an undefined method Laravel\\Scout\\Builder::with\(\)#' - - '#Call to an undefined method Illuminate\\Contracts\\Database\\Query\\Builder::isStandard\(\)#' - - '#Call to an undefined method Illuminate\\Contracts\\Database\\Query\\Builder::withMeta\(\)#' - - '#should return App\\Models\\.*(\|null)? but returns Illuminate\\Database\\Eloquent\\Model(\|null)?#' + - '#Call to private method .*\(\) of parent class Illuminate\\Database\\Eloquent\\Builder#' + - '#should return App\\Models\\.*(\|null)? but returns .*Illuminate\\Database\\Eloquent\\Model(\|null)?#' # Laravel factories allow declaration of dynamic methods as "states" - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Factories\\Factory::#' - '#expects App\\Models\\User\|null, Illuminate\\Database\\Eloquent\\Collection\|Illuminate\\Database\\Eloquent\\Model given#' + - '#Method App\\Models\\.*::query\(\) should return App\\Builders\\.*Builder but returns Illuminate\\Database\\Eloquent\\Builder#' excludePaths: - ./routes/console.php diff --git a/tests/Feature/DownloadTest.php b/tests/Feature/DownloadTest.php index 6711edc8..0636d833 100644 --- a/tests/Feature/DownloadTest.php +++ b/tests/Feature/DownloadTest.php @@ -27,7 +27,8 @@ class DownloadTest extends TestCase public function testNonLoggedInUserCannotDownload(): void { - $song = Song::first(); + /** @var Song $song */ + $song = Song::query()->first(); $this->downloadService ->shouldReceive('from') @@ -39,7 +40,8 @@ class DownloadTest extends TestCase public function testDownloadOneSong(): void { - $song = Song::first(); + /** @var Song $song */ + $song = Song::query()->first(); /** @var User $user */ $user = User::factory()->create(); @@ -62,7 +64,7 @@ class DownloadTest extends TestCase $user = User::factory()->create(); /** @var array|Collection $songs */ - $songs = Song::take(2)->orderBy('id')->get(); + $songs = Song::query()->take(2)->orderBy('id')->get(); $this->downloadService ->shouldReceive('from') @@ -84,7 +86,8 @@ class DownloadTest extends TestCase public function testDownloadAlbum(): void { - $album = Album::first(); + /** @var Album $album */ + $album = Album::query()->first(); /** @var User $user */ $user = User::factory()->create(); @@ -103,7 +106,8 @@ class DownloadTest extends TestCase public function testDownloadArtist(): void { - $artist = Artist::first(); + /** @var Artist $artist */ + $artist = Artist::query()->first(); /** @var User $user */ $user = User::factory()->create(); diff --git a/tests/Feature/InteractionTest.php b/tests/Feature/InteractionTest.php index f33e1f93..f0ff85c8 100644 --- a/tests/Feature/InteractionTest.php +++ b/tests/Feature/InteractionTest.php @@ -25,7 +25,7 @@ class InteractionTest extends TestCase $user = User::factory()->create(); /** @var Song $song */ - $song = Song::orderBy('id')->first(); + $song = Song::query()->orderBy('id')->first(); $this->postAs('api/interaction/play', ['song' => $song->id], $user); self::assertDatabaseHas('interactions', [ @@ -52,7 +52,7 @@ class InteractionTest extends TestCase $user = User::factory()->create(); /** @var Song $song */ - $song = Song::orderBy('id')->first(); + $song = Song::query()->orderBy('id')->first(); $this->postAs('api/interaction/like', ['song' => $song->id], $user); self::assertDatabaseHas('interactions', [ @@ -79,7 +79,7 @@ class InteractionTest extends TestCase $user = User::factory()->create(); /** @var Collection|array $songs */ - $songs = Song::orderBy('id')->take(2)->get(); + $songs = Song::query()->orderBy('id')->take(2)->get(); $songIds = array_pluck($songs->toArray(), 'id'); $this->postAs('api/interaction/batch/like', ['songs' => $songIds], $user); diff --git a/tests/Feature/ObjectStorage/S3Test.php b/tests/Feature/ObjectStorage/S3Test.php index 0a680d4e..c99dc24c 100644 --- a/tests/Feature/ObjectStorage/S3Test.php +++ b/tests/Feature/ObjectStorage/S3Test.php @@ -34,7 +34,7 @@ class S3Test extends TestCase ]); /** @var Song $song */ - $song = Song::where('path', 's3://koel/sample.mp3')->firstOrFail(); + $song = Song::query()->where('path', 's3://koel/sample.mp3')->firstOrFail(); self::assertSame('A Koel Song', $song->title); self::assertSame('Koel Testing Vol. 1', $song->album->name); diff --git a/tests/Feature/PlaylistTest.php b/tests/Feature/PlaylistTest.php index 6d9a7337..ee4ef986 100644 --- a/tests/Feature/PlaylistTest.php +++ b/tests/Feature/PlaylistTest.php @@ -23,7 +23,7 @@ class PlaylistTest extends TestCase $user = User::factory()->create(); /** @var array|Collection $songs */ - $songs = Song::orderBy('id')->take(3)->get(); + $songs = Song::query()->orderBy('id')->take(3)->get(); $response = $this->postAs('api/playlist', [ 'name' => 'Foo Bar', @@ -34,7 +34,7 @@ class PlaylistTest extends TestCase $response->assertOk(); /** @var Playlist $playlist */ - $playlist = Playlist::orderBy('id', 'desc')->first(); + $playlist = Playlist::query()->orderByDesc('id')->first(); self::assertSame('Foo Bar', $playlist->name); self::assertTrue($playlist->user->is($user)); @@ -63,7 +63,7 @@ class PlaylistTest extends TestCase ], $user); /** @var Playlist $playlist */ - $playlist = Playlist::orderBy('id', 'desc')->first(); + $playlist = Playlist::query()->orderByDesc('id')->first(); self::assertSame('Smart Foo Bar', $playlist->name); self::assertTrue($playlist->user->is($user)); @@ -88,11 +88,11 @@ class PlaylistTest extends TestCase ], ], ], - 'songs' => Song::orderBy('id')->take(3)->get()->pluck('id')->all(), + 'songs' => Song::query()->orderBy('id')->take(3)->get()->pluck('id')->all(), ]); /** @var Playlist $playlist */ - $playlist = Playlist::orderBy('id', 'desc')->first(); + $playlist = Playlist::query()->orderByDesc('id')->first(); self::assertSame('Smart Foo Bar', $playlist->name); self::assertEmpty($playlist->songs); diff --git a/tests/Feature/SongTest.php b/tests/Feature/SongTest.php index d92cda55..f7be3213 100644 --- a/tests/Feature/SongTest.php +++ b/tests/Feature/SongTest.php @@ -21,7 +21,9 @@ class SongTest extends TestCase { /** @var User $user */ $user = User::factory()->admin()->create(); - $song = Song::first(); + + /** @var Song $song */ + $song = Song::query()->first(); $this->putAs('/api/songs', [ 'songs' => [$song->id], @@ -37,11 +39,11 @@ class SongTest extends TestCase ->assertOk(); /** @var Artist $artist */ - $artist = Artist::where('name', 'John Cena')->first(); + $artist = Artist::query()->where('name', 'John Cena')->first(); self::assertNotNull($artist); /** @var Album $album */ - $album = Album::where('name', 'One by One')->first(); + $album = Album::query()->where('name', 'One by One')->first(); self::assertNotNull($album); self::assertDatabaseHas(Song::class, [ @@ -57,7 +59,10 @@ class SongTest extends TestCase { /** @var User $user */ $user = User::factory()->admin()->create(); - $song = Song::first(); + + /** @var Song $song */ + $song = Song::query()->first(); + $originalArtistId = $song->artist->id; $this->putAs('/api/songs', [ @@ -73,17 +78,17 @@ class SongTest extends TestCase ->assertOk(); // We don't expect the song's artist to change - self::assertEquals($originalArtistId, Song::find($song->id)->artist->id); + self::assertEquals($originalArtistId, $song->refresh()->artist->id); // But we expect a new album to be created for this artist and contain this song - self::assertEquals('One by One', Song::find($song->id)->album->name); + self::assertEquals('One by One', $song->album->name); } public function testMultipleUpdateNoCompilation(): void { /** @var User $user */ $user = User::factory()->admin()->create(); - $songIds = Song::latest()->take(3)->pluck('id')->toArray(); + $songIds = Song::query()->latest()->take(3)->pluck('id')->toArray(); $this->putAs('/api/songs', [ 'songs' => $songIds, @@ -97,7 +102,7 @@ class SongTest extends TestCase ], $user) ->assertOk(); - $songs = Song::whereIn('id', $songIds)->get(); + $songs = Song::query()->whereIn('id', $songIds)->get(); // All of these songs must now belong to a new album and artist set self::assertEquals('One by One', $songs[0]->album->name); @@ -115,7 +120,7 @@ class SongTest extends TestCase $user = User::factory()->admin()->create(); /** @var array|Collection $originalSongs */ - $originalSongs = Song::latest()->take(3)->get(); + $originalSongs = Song::query()->latest()->take(3)->get(); $songIds = $originalSongs->pluck('id')->toArray(); $this->putAs('/api/songs', [ @@ -131,7 +136,7 @@ class SongTest extends TestCase ->assertOk(); /** @var array|Collection $songs */ - $songs = Song::latest()->take(3)->get(); + $songs = Song::query()->latest()->take(3)->get(); // Even though the album name doesn't change, a new artist should have been created // and thus, a new album with the same name was created as well. @@ -152,7 +157,10 @@ class SongTest extends TestCase { /** @var User $user */ $user = User::factory()->admin()->create(); - $song = Song::first(); + + /** @var Song $song */ + $song = Song::query()->first(); + $this->putAs('/api/songs', [ 'songs' => [$song->id], @@ -169,13 +177,13 @@ class SongTest extends TestCase ->assertOk(); /** @var Album $album */ - $album = Album::where('name', 'One by One')->first(); + $album = Album::query()->where('name', 'One by One')->first(); /** @var Artist $albumArtist */ - $albumArtist = Artist::whereName('John Lennon')->first(); + $albumArtist = Artist::query()->where('name', 'John Lennon')->first(); /** @var Artist $artist */ - $artist = Artist::whereName('John Cena')->first(); + $artist = Artist::query()->where('name', 'John Cena')->first(); self::assertDatabaseHas(Song::class, [ 'id' => $song->id, @@ -191,11 +199,11 @@ class SongTest extends TestCase public function testDeletingByChunk(): void { - self::assertNotEquals(0, Song::count()); - $ids = Song::select('id')->get()->pluck('id')->all(); + self::assertNotEquals(0, Song::query()->count()); + $ids = Song::query()->select('id')->get()->pluck('id')->all(); Song::deleteByChunk($ids, 'id', 1); - self::assertEquals(0, Song::count()); + self::assertEquals(0, Song::query()->count()); } } diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php index 7ea0b456..de9c9548 100644 --- a/tests/Feature/UserTest.php +++ b/tests/Feature/UserTest.php @@ -30,7 +30,8 @@ class UserTest extends TestCase ], $admin) ->assertSuccessful(); - $user = User::firstWhere('email', 'bar@baz.com'); + /** @var User $user */ + $user = User::query()->firstWhere('email', 'bar@baz.com'); self::assertTrue(Hash::check('secret', $user->password)); self::assertSame('Foo', $user->name); diff --git a/tests/Feature/V6/PlayCountTest.php b/tests/Feature/V6/PlayCountTest.php index 97ed75f5..8179dee7 100644 --- a/tests/Feature/V6/PlayCountTest.php +++ b/tests/Feature/V6/PlayCountTest.php @@ -44,6 +44,12 @@ class PlayCountTest extends TestCase 'play_count', ]); - self::assertEquals(1, Interaction::whereSongIdAndUserId($song->id, $user->id)->first()->play_count); + /** @var Interaction $interaction */ + $interaction = Interaction::query() + ->where('song_id', $song->id) + ->where('user_id', $user->id) + ->first(); + + self::assertEquals(1, $interaction->play_count); } } diff --git a/tests/Feature/YouTubeTest.php b/tests/Feature/YouTubeTest.php index e8d1bdbd..64118953 100644 --- a/tests/Feature/YouTubeTest.php +++ b/tests/Feature/YouTubeTest.php @@ -21,13 +21,13 @@ class YouTubeTest extends TestCase public function testSearchYouTubeVideos(): void { static::createSampleMediaSet(); - $song = Song::first(); + + /** @var Song $song */ + $song = Song::query()->first(); $this->youTubeService ->shouldReceive('searchVideosRelatedToSong') - ->with(Mockery::on(static function (Song $retrievedSong) use ($song) { - return $song->id === $retrievedSong->id; - }), 'foo') + ->with(Mockery::on(static fn (Song $retrievedSong) => $song->is($retrievedSong)), 'foo') ->once(); $this->getAs("/api/youtube/search/song/{$song->id}?pageToken=foo") diff --git a/tests/Integration/Services/InteractionServiceTest.php b/tests/Integration/Services/InteractionServiceTest.php index 9fb1b40f..d1c264dc 100644 --- a/tests/Integration/Services/InteractionServiceTest.php +++ b/tests/Integration/Services/InteractionServiceTest.php @@ -27,11 +27,10 @@ class InteractionServiceTest extends TestCase { /** @var Interaction $interaction */ $interaction = Interaction::factory()->create(); - + $currentCount = $interaction->play_count; $this->interactionService->increasePlayCount($interaction->song, $interaction->user); - $updatedInteraction = Interaction::find($interaction->id); - self::assertEquals($interaction->play_count + 1, $updatedInteraction->play_count); + self::assertEquals($currentCount + 1, $interaction->refresh()->play_count); } public function testToggleLike(): void @@ -40,11 +39,11 @@ class InteractionServiceTest extends TestCase /** @var Interaction $interaction */ $interaction = Interaction::factory()->create(); + $currentLiked = $interaction->liked; $this->interactionService->toggleLike($interaction->song, $interaction->user); - $updatedInteraction = Interaction::find($interaction->id); - self::assertNotSame($interaction->liked, $updatedInteraction->liked); + self::assertNotSame($currentLiked, $interaction->refresh()->liked); } public function testLikeMultipleSongs(): void @@ -60,7 +59,13 @@ class InteractionServiceTest extends TestCase $this->interactionService->batchLike($songs->pluck('id')->all(), $user); $songs->each(static function (Song $song) use ($user): void { - self::assertTrue(Interaction::whereSongIdAndUserId($song->id, $user->id)->first()->liked); + /** @var Interaction $interaction */ + $interaction = Interaction::query() + ->where('song_id', $song->id) + ->where('user_id', $user->id) + ->first(); + + self::assertTrue($interaction->liked); }); } @@ -80,7 +85,7 @@ class InteractionServiceTest extends TestCase $this->interactionService->batchUnlike($interactions->pluck('song.id')->all(), $user); $interactions->each(static function (Interaction $interaction): void { - self::assertFalse(Interaction::find($interaction->id)->liked); + self::assertFalse($interaction->refresh()->liked); }); } } diff --git a/tests/Integration/Services/MediaCacheServiceTest.php b/tests/Integration/Services/MediaCacheServiceTest.php index 03b832fd..52cf1e1c 100644 --- a/tests/Integration/Services/MediaCacheServiceTest.php +++ b/tests/Integration/Services/MediaCacheServiceTest.php @@ -28,8 +28,8 @@ class MediaCacheServiceTest extends TestCase Song::factory(5)->create(); $this->cache->shouldReceive('rememberForever')->andReturn([ - 'albums' => Album::orderBy('name')->get(), - 'artists' => Artist::orderBy('name')->get(), + 'albums' => Album::query()->orderBy('name')->get(), + 'artists' => Artist::query()->orderBy('name')->get(), 'songs' => Song::all(), ]); diff --git a/tests/Integration/Services/MediaSyncServiceTest.php b/tests/Integration/Services/MediaSyncServiceTest.php index f5476664..05592e2d 100644 --- a/tests/Integration/Services/MediaSyncServiceTest.php +++ b/tests/Integration/Services/MediaSyncServiceTest.php @@ -51,7 +51,7 @@ class MediaSyncServiceTest extends TestCase // GitHub issue #380. folder.png should be copied and used as the cover for files // under subdir/ /** @var Song $song */ - $song = Song::where('path', $this->path('/subdir/back-in-black.ogg'))->first(); + $song = Song::query()->where('path', $this->path('/subdir/back-in-black.ogg'))->first(); self::assertNotEmpty($song->album->cover); // File search shouldn't be case-sensitive. @@ -72,12 +72,12 @@ class MediaSyncServiceTest extends TestCase // Albums and artists should be correctly linked /** @var Album $album */ - $album = Album::where('name', 'Koel Testing Vol. 1')->first(); + $album = Album::query()->where('name', 'Koel Testing Vol. 1')->first(); self::assertEquals('Koel', $album->artist->name); // Compilation albums, artists and songs must be recognized /** @var Song $song */ - $song = Song::where('title', 'This song belongs to a compilation')->first(); + $song = Song::query()->where('title', 'This song belongs to a compilation')->first(); self::assertFalse($song->album->artist->is($song->artist)); self::assertSame('Koel', $song->album->artist->name); self::assertSame('Cuckoo', $song->artist->name); @@ -87,7 +87,8 @@ class MediaSyncServiceTest extends TestCase { $this->mediaService->sync(); - $song = Song::first(); + /** @var Song $song */ + $song = Song::query()->first(); touch($song->path, $time = time() + 1000); $this->mediaService->sync(); @@ -101,7 +102,8 @@ class MediaSyncServiceTest extends TestCase $this->mediaService->sync(); - $song = Song::first(); + /** @var Song $song */ + $song = Song::query()->first(); $song->update([ 'title' => "It's John Cena!", @@ -121,7 +123,8 @@ class MediaSyncServiceTest extends TestCase $this->mediaService->sync(); - $song = Song::first(); + /** @var Song $song */ + $song = Song::query()->first(); $song->update([ 'title' => "It's John Cena!", @@ -142,7 +145,8 @@ class MediaSyncServiceTest extends TestCase $this->mediaService->sync(); - $song = Song::first(); + /** @var Song $song */ + $song = Song::query()->first(); $song->update([ 'title' => "It's John Cena!", @@ -161,14 +165,16 @@ class MediaSyncServiceTest extends TestCase { $this->mediaService->sync(); - $song = Song::first(); + /** @var Song $song */ + $song = Song::query()->first(); + $song->delete(); $this->mediaService->sync(ignores: ['title', 'disc', 'track'], force: true); // Song should be added back with all info self::assertEquals( - Arr::except(Song::where('path', $song->path)->first()->toArray(), ['id', 'created_at']), + Arr::except(Song::query()->where('path', $song->path)->first()->toArray(), ['id', 'created_at']), Arr::except($song->toArray(), ['id', 'created_at']) ); } @@ -188,8 +194,9 @@ class MediaSyncServiceTest extends TestCase $this->expectsEvents(LibraryChanged::class); static::createSampleMediaSet(); - $song = Song::first(); - self::assertModelExists($song); + + /** @var Song $song */ + $song = Song::query()->first(); $this->mediaService->syncByWatchRecord(new InotifyWatchRecord("DELETE $song->path")); diff --git a/tests/Traits/CreatesApplication.php b/tests/Traits/CreatesApplication.php index e219e2ab..a2b7a4e5 100644 --- a/tests/Traits/CreatesApplication.php +++ b/tests/Traits/CreatesApplication.php @@ -30,7 +30,7 @@ trait CreatesApplication { $this->artisan->call('migrate'); - if (!User::count()) { + if (!User::query()->count()) { $this->artisan->call('db:seed'); } } diff --git a/tests/Unit/Models/AlbumTest.php b/tests/Unit/Models/AlbumTest.php index 887bd78c..1be9e0f7 100644 --- a/tests/Unit/Models/AlbumTest.php +++ b/tests/Unit/Models/AlbumTest.php @@ -22,7 +22,7 @@ class AlbumTest extends TestCase $artist = Artist::factory()->create(); $name = 'Foo'; - self::assertNull(Album::whereArtistIdAndName($artist->id, $name)->first()); + self::assertNull(Album::query()->where('artist_id', $artist->id)->where('name', $name)->first()); $album = Album::getOrCreate($artist, $name); self::assertSame('Foo', $album->name); diff --git a/tests/Unit/Models/ArtistTest.php b/tests/Unit/Models/ArtistTest.php index 95a1998d..626d4922 100644 --- a/tests/Unit/Models/ArtistTest.php +++ b/tests/Unit/Models/ArtistTest.php @@ -17,7 +17,7 @@ class ArtistTest extends TestCase public function testNewArtistIsCreatedWithName(): void { - self::assertNull(Artist::whereName('Foo')->first()); + self::assertNull(Artist::query()->where('name', 'Foo')->first()); self::assertSame('Foo', Artist::getOrCreate('Foo')->name); } diff --git a/tests/Unit/Services/MediaMetadataServiceTest.php b/tests/Unit/Services/MediaMetadataServiceTest.php index 336c6d24..e14b2063 100644 --- a/tests/Unit/Services/MediaMetadataServiceTest.php +++ b/tests/Unit/Services/MediaMetadataServiceTest.php @@ -53,7 +53,7 @@ class MediaMetadataServiceTest extends TestCase ->with('/koel/public/img/album/foo.jpg', 'dummy-src'); $this->mediaMetadataService->writeAlbumCover($album, 'dummy-src', 'jpg', $coverPath); - self::assertEquals(album_cover_url('foo.jpg'), Album::find($album->id)->cover); + self::assertEquals(album_cover_url('foo.jpg'), $album->refresh()->cover); } public function testTryDownloadArtistImage(): void @@ -81,6 +81,7 @@ class MediaMetadataServiceTest extends TestCase ->with('/koel/public/img/artist/foo.jpg', 'dummy-src'); $this->mediaMetadataService->writeArtistImage($artist, 'dummy-src', 'jpg', $imagePath); - self::assertEquals(artist_image_url('foo.jpg'), Artist::find($artist->id)->image); + + self::assertEquals(artist_image_url('foo.jpg'), $artist->refresh()->image); } }