chore: some cleanups

This commit is contained in:
Phan An 2024-06-04 15:35:00 +02:00
parent 4a10aa9915
commit 9adfabb651
23 changed files with 112 additions and 159 deletions

View file

@ -140,3 +140,11 @@ function collect_sso_providers(): array
return $providers; return $providers;
} }
function get_mtime(string|SplFileInfo $file): int
{
$file = is_string($file) ? new SplFileInfo($file) : $file;
// Workaround for #344, where getMTime() fails for certain files with Unicode names on Windows.
return attempt(static fn () => $file->getMTime()) ?? time();
}

View file

@ -19,7 +19,7 @@ class SearchVideosRequest extends Request
return '/search'; return '/search';
} }
/** @return array<mixed> */ /** @inheritdoc */
protected function defaultQuery(): array protected function defaultQuery(): array
{ {
$q = $this->song->title; $q = $this->song->title;

View file

@ -205,7 +205,7 @@ class Song extends Model
return "s3://$bucket/$key"; return "s3://$bucket/$key";
} }
/** @return array<mixed> */ /** @inheritdoc */
public function toSearchableArray(): array public function toSearchableArray(): array
{ {
$array = [ $array = [

View file

@ -68,7 +68,7 @@ abstract class Repository implements RepositoryInterface
} }
/** @return T|null */ /** @return T|null */
public function getFirstWhere(...$params): ?Model public function findFirstWhere(...$params): ?Model
{ {
return $this->model::query()->firstWhere(...$params); return $this->model::query()->firstWhere(...$params);
} }

View file

@ -14,11 +14,7 @@ class AllPlayablesAreAccessibleBy implements ValidationRule
{ {
} }
/** /** @param array<string> $value */
* Run the validation rule.
*
* @param array<string> $value
*/
public function validate(string $attribute, mixed $value, Closure $fail): void public function validate(string $attribute, mixed $value, Closure $fail): void
{ {
$ids = array_unique(Arr::wrap($value)); $ids = array_unique(Arr::wrap($value));

View file

@ -3,11 +3,11 @@
namespace App\Services; namespace App\Services;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Support\Facades\Cache;
class ApplicationInformationService class ApplicationInformationService
{ {
public function __construct(private readonly Client $client, private readonly Cache $cache) public function __construct(private readonly Client $client)
{ {
} }
@ -17,7 +17,7 @@ class ApplicationInformationService
public function getLatestVersionNumber(): string public function getLatestVersionNumber(): string
{ {
return attempt(function () { return attempt(function () {
return $this->cache->remember('latestKoelVersion', now()->addDay(), function (): string { return Cache::remember('latestKoelVersion', now()->addDay(), function (): string {
return json_decode($this->client->get('https://api.github.com/repos/koel/koel/tags')->getBody())[0] return json_decode($this->client->get('https://api.github.com/repos/koel/koel/tags')->getBody())[0]
->name; ->name;
}); });

View file

@ -8,8 +8,8 @@ use App\Repositories\UserRepository;
use App\Values\CompositeToken; use App\Values\CompositeToken;
use Illuminate\Auth\Events\PasswordReset; use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Auth\Passwords\PasswordBroker; use Illuminate\Auth\Passwords\PasswordBroker;
use Illuminate\Hashing\HashManager;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Password;
class AuthenticationService class AuthenticationService
@ -17,21 +17,20 @@ class AuthenticationService
public function __construct( public function __construct(
private readonly UserRepository $userRepository, private readonly UserRepository $userRepository,
private readonly TokenManager $tokenManager, private readonly TokenManager $tokenManager,
private readonly HashManager $hash,
private readonly PasswordBroker $passwordBroker private readonly PasswordBroker $passwordBroker
) { ) {
} }
public function login(string $email, string $password): CompositeToken public function login(string $email, string $password): CompositeToken
{ {
$user = $this->userRepository->getFirstWhere('email', $email); $user = $this->userRepository->findFirstWhere('email', $email);
if (!$user || !$this->hash->check($password, $user->password)) { if (!$user || !Hash::check($password, $user->password)) {
throw new InvalidCredentialsException(); throw new InvalidCredentialsException();
} }
if ($this->hash->needsRehash($user->password)) { if (Hash::needsRehash($user->password)) {
$user->password = $this->hash->make($password); $user->password = Hash::make($password);
$user->save(); $user->save();
} }
@ -62,8 +61,8 @@ class AuthenticationService
'token' => $token, 'token' => $token,
]; ];
$status = $this->passwordBroker->reset($credentials, function (User $user, string $password): void { $status = $this->passwordBroker->reset($credentials, static function (User $user, string $password): void {
$user->password = $this->hash->make($password); $user->password = Hash::make($password);
$user->save(); $user->save();
event(new PasswordReset($user)); event(new PasswordReset($user));
}); });

View file

@ -10,8 +10,8 @@ use App\Values\ScanConfiguration;
use App\Values\ScanResult; use App\Values\ScanResult;
use App\Values\SongScanInformation; use App\Values\SongScanInformation;
use getID3; use getID3;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use SplFileInfo; use SplFileInfo;
use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Finder;
@ -32,7 +32,6 @@ class FileScanner
private readonly MediaMetadataService $mediaMetadataService, private readonly MediaMetadataService $mediaMetadataService,
private readonly SongRepository $songRepository, private readonly SongRepository $songRepository,
private readonly SimpleLrcReader $lrcReader, private readonly SimpleLrcReader $lrcReader,
private readonly Cache $cache,
private readonly Finder $finder private readonly Finder $finder
) { ) {
} }
@ -43,7 +42,7 @@ class FileScanner
$this->filePath = $file->getRealPath(); $this->filePath = $file->getRealPath();
$this->song = $this->songRepository->findOneByPath($this->filePath); $this->song = $this->songRepository->findOneByPath($this->filePath);
$this->fileModifiedTime = Helper::getModifiedTime($file); $this->fileModifiedTime = get_mtime($file);
return $this; return $this;
} }
@ -137,7 +136,7 @@ class FileScanner
private function getCoverFileUnderSameDirectory(): ?string private function getCoverFileUnderSameDirectory(): ?string
{ {
// As directory scanning can be expensive, we cache and reuse the result. // As directory scanning can be expensive, we cache and reuse the result.
return $this->cache->remember(md5($this->filePath . '_cover'), now()->addDay(), function (): ?string { return Cache::remember(md5($this->filePath . '_cover'), now()->addDay(), function (): ?string {
$matches = array_keys( $matches = array_keys(
iterator_to_array( iterator_to_array(
$this->finder->create() $this->finder->create()

View file

@ -1,16 +0,0 @@
<?php
namespace App\Services;
use SplFileInfo;
class Helper
{
public static function getModifiedTime(string|SplFileInfo $file): int
{
$file = is_string($file) ? new SplFileInfo($file) : $file;
// Workaround for #344, where getMTime() fails for certain files with Unicode names on Windows.
return attempt(static fn () => $file->getMTime()) ?? time();
}
}

View file

@ -5,11 +5,11 @@ namespace App\Services;
use App\Http\Integrations\iTunes\ITunesConnector; use App\Http\Integrations\iTunes\ITunesConnector;
use App\Http\Integrations\iTunes\Requests\GetTrackRequest; use App\Http\Integrations\iTunes\Requests\GetTrackRequest;
use App\Models\Album; use App\Models\Album;
use Illuminate\Cache\Repository as Cache; use Illuminate\Support\Facades\Cache;
class ITunesService class ITunesService
{ {
public function __construct(private readonly ITunesConnector $connector, private readonly Cache $cache) public function __construct(private readonly ITunesConnector $connector)
{ {
} }
@ -24,7 +24,7 @@ class ITunesService
$request = new GetTrackRequest($trackName, $album); $request = new GetTrackRequest($trackName, $album);
$hash = md5(serialize($request->query())); $hash = md5(serialize($request->query()));
return $this->cache->remember( return Cache::remember(
"itunes.track.$hash", "itunes.track.$hash",
now()->addWeek(), now()->addWeek(),
function () use ($request): ?string { function () use ($request): ?string {

View file

@ -6,16 +6,16 @@ use App\Events\MultipleSongsLiked;
use App\Events\MultipleSongsUnliked; use App\Events\MultipleSongsUnliked;
use App\Events\SongLikeToggled; use App\Events\SongLikeToggled;
use App\Models\Interaction; use App\Models\Interaction;
use App\Models\Song; use App\Models\Song as Playable;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
class InteractionService class InteractionService
{ {
public function increasePlayCount(Song $song, User $user): Interaction public function increasePlayCount(Playable $playable, User $user): Interaction
{ {
return tap(Interaction::query()->firstOrCreate([ return tap(Interaction::query()->firstOrCreate([
'song_id' => $song->id, 'song_id' => $playable->id,
'user_id' => $user->id, 'user_id' => $user->id,
]), static function (Interaction $interaction): void { ]), static function (Interaction $interaction): void {
if (!$interaction->exists) { if (!$interaction->exists) {
@ -30,14 +30,14 @@ class InteractionService
} }
/** /**
* Like or unlike a song as a user. * Like or unlike a song/episode as a user.
* *
* @return Interaction The affected Interaction object * @return Interaction The affected Interaction object
*/ */
public function toggleLike(Song $song, User $user): Interaction public function toggleLike(Playable $playable, User $user): Interaction
{ {
return tap(Interaction::query()->firstOrCreate([ return tap(Interaction::query()->firstOrCreate([
'song_id' => $song->id, 'song_id' => $playable->id,
'user_id' => $user->id, 'user_id' => $user->id,
]), static function (Interaction $interaction): void { ]), static function (Interaction $interaction): void {
$interaction->liked = !$interaction->liked; $interaction->liked = !$interaction->liked;
@ -48,17 +48,17 @@ class InteractionService
} }
/** /**
* Like several songs at once as a user. * Like several songs/episodes at once as a user.
* *
* @param Collection<array-key, Song> $songs * @param Collection<array-key, Playable> $playables
* *
* @return Collection<array-key, Interaction> The array of Interaction objects * @return Collection<array-key, Interaction> The array of Interaction objects
*/ */
public function likeMany(Collection $songs, User $user): Collection public function likeMany(Collection $playables, User $user): Collection
{ {
$interactions = $songs->map(static function (Song $song) use ($user): Interaction { $interactions = $playables->map(static function (Playable $playable) use ($user): Interaction {
return tap(Interaction::query()->firstOrCreate([ return tap(Interaction::query()->firstOrCreate([
'song_id' => $song->id, 'song_id' => $playable->id,
'user_id' => $user->id, 'user_id' => $user->id,
]), static function (Interaction $interaction): void { ]), static function (Interaction $interaction): void {
$interaction->play_count ??= 0; $interaction->play_count ??= 0;
@ -67,23 +67,23 @@ class InteractionService
}); });
}); });
event(new MultipleSongsLiked($songs, $user)); event(new MultipleSongsLiked($playables, $user));
return $interactions; return $interactions;
} }
/** /**
* Unlike several songs at once. * Unlike several songs/episodes at once.
* *
* @param array<array-key, Song>|Collection $songs * @param array<array-key, Playable>|Collection $playables
*/ */
public function unlikeMany(Collection $songs, User $user): void public function unlikeMany(Collection $playables, User $user): void
{ {
Interaction::query() Interaction::query()
->whereIn('song_id', $songs->pluck('id')->all()) ->whereIn('song_id', $playables->pluck('id')->all())
->where('user_id', $user->id) ->where('user_id', $user->id)
->update(['liked' => false]); ->update(['liked' => false]);
event(new MultipleSongsUnliked($songs, $user)); event(new MultipleSongsUnliked($playables, $user));
} }
} }

View file

@ -17,12 +17,12 @@ use App\Services\Contracts\MusicEncyclopedia;
use App\Values\AlbumInformation; use App\Values\AlbumInformation;
use App\Values\ArtistInformation; use App\Values\ArtistInformation;
use Generator; use Generator;
use Illuminate\Cache\Repository as Cache;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
class LastfmService implements MusicEncyclopedia class LastfmService implements MusicEncyclopedia
{ {
public function __construct(private readonly LastfmConnector $connector, private readonly Cache $cache) public function __construct(private readonly LastfmConnector $connector)
{ {
} }
@ -49,7 +49,7 @@ class LastfmService implements MusicEncyclopedia
} }
return attempt_if(static::enabled(), function () use ($artist): ?ArtistInformation { return attempt_if(static::enabled(), function () use ($artist): ?ArtistInformation {
return $this->cache->remember( return Cache::remember(
"lastfm.artist.$artist->id", "lastfm.artist.$artist->id",
now()->addWeek(), now()->addWeek(),
fn () => $this->connector->send(new GetArtistInfoRequest($artist))->dto() fn () => $this->connector->send(new GetArtistInfoRequest($artist))->dto()
@ -64,7 +64,7 @@ class LastfmService implements MusicEncyclopedia
} }
return attempt_if(static::enabled(), function () use ($album): ?AlbumInformation { return attempt_if(static::enabled(), function () use ($album): ?AlbumInformation {
return $this->cache->remember( return Cache::remember(
"lastfm.album.$album->id", "lastfm.album.$album->id",
now()->addWeek(), now()->addWeek(),
fn () => $this->connector->send(new GetAlbumInfoRequest($album))->dto() fn () => $this->connector->send(new GetAlbumInfoRequest($album))->dto()

View file

@ -7,14 +7,13 @@ use App\Models\Artist;
use App\Services\Contracts\MusicEncyclopedia; use App\Services\Contracts\MusicEncyclopedia;
use App\Values\AlbumInformation; use App\Values\AlbumInformation;
use App\Values\ArtistInformation; use App\Values\ArtistInformation;
use Illuminate\Cache\Repository as Cache; use Illuminate\Support\Facades\Cache;
class MediaInformationService class MediaInformationService
{ {
public function __construct( public function __construct(
private readonly MusicEncyclopedia $encyclopedia, private readonly MusicEncyclopedia $encyclopedia,
private readonly MediaMetadataService $mediaMetadataService, private readonly MediaMetadataService $mediaMetadataService
private readonly Cache $cache
) { ) {
} }
@ -24,20 +23,16 @@ class MediaInformationService
return null; return null;
} }
if ($this->cache->has('album.info.' . $album->id)) { return Cache::remember("album.info.$album->id", now()->addWeek(), function () use ($album): AlbumInformation {
return $this->cache->get('album.info.' . $album->id); $info = $this->encyclopedia->getAlbumInformation($album) ?: AlbumInformation::make();
}
$info = $this->encyclopedia->getAlbumInformation($album) ?: AlbumInformation::make(); attempt_unless($album->has_cover, function () use ($info, $album): void {
$this->mediaMetadataService->tryDownloadAlbumCover($album);
$info->cover = $album->cover;
});
attempt_unless($album->has_cover, function () use ($info, $album): void { return $info;
$this->mediaMetadataService->tryDownloadAlbumCover($album);
$info->cover = $album->cover;
}); });
$this->cache->put('album.info.' . $album->id, $info, now()->addWeek());
return $info;
} }
public function getArtistInformation(Artist $artist): ?ArtistInformation public function getArtistInformation(Artist $artist): ?ArtistInformation
@ -46,19 +41,19 @@ class MediaInformationService
return null; return null;
} }
if ($this->cache->has('artist.info.' . $artist->id)) { return Cache::remember(
return $this->cache->get('artist.info.' . $artist->id); "artist.info.$artist->id",
} now()->addWeek(),
function () use ($artist): ArtistInformation {
$info = $this->encyclopedia->getArtistInformation($artist) ?: ArtistInformation::make();
$info = $this->encyclopedia->getArtistInformation($artist) ?: ArtistInformation::make(); attempt_unless($artist->has_image, function () use ($artist, $info): void {
$this->mediaMetadataService->tryDownloadArtistImage($artist);
$info->image = $artist->image;
});
attempt_unless($artist->has_image, function () use ($artist, $info): void { return $info;
$this->mediaMetadataService->tryDownloadArtistImage($artist); }
$info->image = $artist->image; );
});
$this->cache->put('artist.info.' . $artist->id, $info, now()->addWeek());
return $info;
} }
} }

View file

@ -11,7 +11,7 @@ use App\Values\ScanConfiguration;
use App\Values\ScanResult; use App\Values\ScanResult;
use App\Values\ScanResultCollection; use App\Values\ScanResultCollection;
use App\Values\WatchRecord\Contracts\WatchRecordInterface; use App\Values\WatchRecord\Contracts\WatchRecordInterface;
use Psr\Log\LoggerInterface; use Illuminate\Support\Facades\Log;
use SplFileInfo; use SplFileInfo;
use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Finder;
use Throwable; use Throwable;
@ -25,8 +25,7 @@ class MediaScanner
private readonly SettingRepository $settingRepository, private readonly SettingRepository $settingRepository,
private readonly SongRepository $songRepository, private readonly SongRepository $songRepository,
private readonly FileScanner $fileScanner, private readonly FileScanner $fileScanner,
private readonly Finder $finder, private readonly Finder $finder
private readonly LoggerInterface $logger
) { ) {
} }
@ -88,7 +87,7 @@ class MediaScanner
public function scanWatchRecord(WatchRecordInterface $record, ScanConfiguration $config): void public function scanWatchRecord(WatchRecordInterface $record, ScanConfiguration $config): void
{ {
$this->logger->info("New watch record received: '{$record->getPath()}'"); Log::info("New watch record received: '{$record->getPath()}'");
if ($record->isFile()) { if ($record->isFile()) {
$this->scanFileRecord($record, $config); $this->scanFileRecord($record, $config);
@ -100,7 +99,7 @@ class MediaScanner
private function scanFileRecord(WatchRecordInterface $record, ScanConfiguration $config): void private function scanFileRecord(WatchRecordInterface $record, ScanConfiguration $config): void
{ {
$path = $record->getPath(); $path = $record->getPath();
$this->logger->info("'$path' is a file."); Log::info("'$path' is a file.");
if ($record->isDeleted()) { if ($record->isDeleted()) {
$this->handleDeletedFileRecord($path); $this->handleDeletedFileRecord($path);
@ -112,7 +111,7 @@ class MediaScanner
private function scanDirectoryRecord(WatchRecordInterface $record, ScanConfiguration $config): void private function scanDirectoryRecord(WatchRecordInterface $record, ScanConfiguration $config): void
{ {
$path = $record->getPath(); $path = $record->getPath();
$this->logger->info("'$path' is a directory."); Log::info("'$path' is a directory.");
if ($record->isDeleted()) { if ($record->isDeleted()) {
$this->handleDeletedDirectoryRecord($path); $this->handleDeletedDirectoryRecord($path);
@ -138,9 +137,9 @@ class MediaScanner
if ($song) { if ($song) {
$song->delete(); $song->delete();
$this->logger->info("$path deleted."); Log::info("$path deleted.");
} else { } else {
$this->logger->info("$path doesn't exist in our database--skipping."); Log::info("$path doesn't exist in our database--skipping.");
} }
} }
@ -149,9 +148,9 @@ class MediaScanner
$result = $this->fileScanner->setFile($path)->scan($config); $result = $this->fileScanner->setFile($path)->scan($config);
if ($result->isSuccess()) { if ($result->isSuccess()) {
$this->logger->info("Scanned $path"); Log::info("Scanned $path");
} else { } else {
$this->logger->info("Failed to scan $path. Maybe an invalid file?"); Log::info("Failed to scan $path. Maybe an invalid file?");
} }
} }
@ -160,9 +159,9 @@ class MediaScanner
$count = Song::query()->inDirectory($path)->delete(); $count = Song::query()->inDirectory($path)->delete();
if ($count) { if ($count) {
$this->logger->info("Deleted $count song(s) under $path"); Log::info("Deleted $count song(s) under $path");
} else { } else {
$this->logger->info("$path is empty--no action needed."); Log::info("$path is empty--no action needed.");
} }
} }
@ -178,7 +177,7 @@ class MediaScanner
} }
} }
$this->logger->info("Scanned all song(s) under $path"); Log::info("Scanned all song(s) under $path");
event(new MediaScanCompleted($scanResults)); event(new MediaScanCompleted($scanResults));
} }

View file

@ -12,15 +12,14 @@ use App\Values\SongUpdateData;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Psr\Log\LoggerInterface; use Illuminate\Support\Facades\Log;
use Throwable; use Throwable;
class SongService class SongService
{ {
public function __construct( public function __construct(
private readonly SongRepository $songRepository, private readonly SongRepository $songRepository,
private readonly SongStorage $songStorage, private readonly SongStorage $songStorage
private readonly LoggerInterface $logger
) { ) {
} }
@ -40,12 +39,10 @@ class SongService
return DB::transaction(function () use ($ids, $data): Collection { return DB::transaction(function () use ($ids, $data): Collection {
return collect($ids)->reduce(function (Collection $updated, string $id) use ($data): Collection { return collect($ids)->reduce(function (Collection $updated, string $id) use ($data): Collection {
/** @var Song|null $song */ optional(
$song = Song::with('album', 'album.artist', 'artist')->find($id); Song::query()->with('album.artist')->find($id),
fn (Song $song) => $updated->push($this->updateSong($song, clone $data))
if ($song) { );
$updated->push($this->updateSong($song, clone $data));
}
return $updated; return $updated;
}, collect()); }, collect());
@ -95,9 +92,9 @@ class SongService
public function markSongsAsPrivate(Collection $songs): array public function markSongsAsPrivate(Collection $songs): array
{ {
if (License::isPlus()) { if (License::isPlus()) {
// Songs that are in collaborative playlists can't be marked as private.
/** /**
* @var Collection<array-key, Song> $collaborativeSongs * @var Collection<array-key, Song> $collaborativeSongs
* Songs that are in collaborative playlists and can't be marked as private as a result
*/ */
$collaborativeSongs = Song::query() $collaborativeSongs = Song::query()
->whereIn('songs.id', $songs->pluck('id')) ->whereIn('songs.id', $songs->pluck('id'))
@ -124,11 +121,9 @@ class SongService
public function deleteSongs(array|string $ids): void public function deleteSongs(array|string $ids): void
{ {
$ids = Arr::wrap($ids); $ids = Arr::wrap($ids);
$shouldBackUp = config('koel.backup_on_delete');
DB::transaction(function () use ($ids): void { DB::transaction(function () use ($ids, $shouldBackUp): void {
$shouldBackUp = config('koel.backup_on_delete');
/** @var Collection<array-key, Song> $songs */
$songs = Song::query()->findMany($ids); $songs = Song::query()->findMany($ids);
Song::destroy($ids); Song::destroy($ids);
@ -137,7 +132,7 @@ class SongService
try { try {
$this->songStorage->delete($song, $shouldBackUp); $this->songStorage->delete($song, $shouldBackUp);
} catch (Throwable $e) { } catch (Throwable $e) {
$this->logger->error('Failed to remove song file', [ Log::error('Failed to remove song file', [
'path' => $song->path, 'path' => $song->path,
'exception' => $e, 'exception' => $e,
]); ]);

View file

@ -4,16 +4,12 @@ namespace App\Services;
use App\Models\User; use App\Models\User;
use App\Values\CompositeToken; use App\Values\CompositeToken;
use Illuminate\Cache\Repository as Cache; use Illuminate\Support\Facades\Cache;
use Laravel\Sanctum\NewAccessToken; use Laravel\Sanctum\NewAccessToken;
use Laravel\Sanctum\PersonalAccessToken; use Laravel\Sanctum\PersonalAccessToken;
class TokenManager class TokenManager
{ {
public function __construct(private readonly Cache $cache)
{
}
public function createToken(User $user, array $abilities = ['*']): NewAccessToken public function createToken(User $user, array $abilities = ['*']): NewAccessToken
{ {
return $user->createToken(config('app.name'), $abilities); return $user->createToken(config('app.name'), $abilities);
@ -26,7 +22,7 @@ class TokenManager
audio: $this->createToken($user, ['audio']) audio: $this->createToken($user, ['audio'])
); );
$this->cache->rememberForever("app.composite-tokens.$token->apiToken", static fn () => $token->audioToken); Cache::forever("app.composite-tokens.$token->apiToken", $token->audioToken);
return $token; return $token;
} }
@ -34,11 +30,11 @@ class TokenManager
public function deleteCompositionToken(string $plainTextApiToken): void public function deleteCompositionToken(string $plainTextApiToken): void
{ {
/** @var string $audioToken */ /** @var string $audioToken */
$audioToken = $this->cache->get("app.composite-tokens.$plainTextApiToken"); $audioToken = Cache::get("app.composite-tokens.$plainTextApiToken");
if ($audioToken) { if ($audioToken) {
self::deleteTokenByPlainTextToken($audioToken); self::deleteTokenByPlainTextToken($audioToken);
$this->cache->forget("app.composite-tokens.$plainTextApiToken"); Cache::forget("app.composite-tokens.$plainTextApiToken");
} }
self::deleteTokenByPlainTextToken($plainTextApiToken); self::deleteTokenByPlainTextToken($plainTextApiToken);

View file

@ -6,15 +6,15 @@ use App\Exceptions\InvitationNotFoundException;
use App\Mail\UserInvite; use App\Mail\UserInvite;
use App\Models\User; use App\Models\User;
use App\Repositories\UserRepository; use App\Repositories\UserRepository;
use Illuminate\Contracts\Hashing\Hasher as Hash;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class UserInvitationService class UserInvitationService
{ {
public function __construct(private readonly Hash $hash, private readonly UserRepository $userRepository) public function __construct(private readonly UserRepository $userRepository)
{ {
} }
@ -26,15 +26,13 @@ class UserInvitationService
}); });
} }
/** @throws InvitationNotFoundException */
public function getUserProspectByToken(string $token): User public function getUserProspectByToken(string $token): User
{ {
return User::query()->where('invitation_token', $token)->firstOr(static function (): void { return User::query()->where('invitation_token', $token)->firstOr(static function (): never {
throw new InvitationNotFoundException(); throw new InvitationNotFoundException();
}); });
} }
/** @throws InvitationNotFoundException */
public function revokeByEmail(string $email): void public function revokeByEmail(string $email): void
{ {
$user = $this->userRepository->findOneByEmail($email); $user = $this->userRepository->findOneByEmail($email);
@ -59,14 +57,13 @@ class UserInvitationService
return $invitee; return $invitee;
} }
/** @throws InvitationNotFoundException */
public function accept(string $token, string $name, string $password): User public function accept(string $token, string $name, string $password): User
{ {
$user = $this->getUserProspectByToken($token); $user = $this->getUserProspectByToken($token);
$user->update([ $user->update(attributes: [
'name' => $name, 'name' => $name,
'password' => $this->hash->make($password), 'password' => Hash::make($password),
'invitation_token' => null, 'invitation_token' => null,
'invitation_accepted_at' => now(), 'invitation_accepted_at' => now(),
]); ]);

View file

@ -7,20 +7,16 @@ use App\Facades\License;
use App\Models\User; use App\Models\User;
use App\Repositories\UserRepository; use App\Repositories\UserRepository;
use App\Values\SSOUser; use App\Values\SSOUser;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class UserService class UserService
{ {
public function __construct( public function __construct(private readonly UserRepository $repository, private readonly ImageWriter $imageWriter)
private readonly UserRepository $repository, {
private readonly Hasher $hash,
private readonly ImageWriter $imageWriter
) {
} }
/** @noinspection PhpIncompatibleReturnTypeInspection */
public function createUser( public function createUser(
string $name, string $name,
string $email, string $email,
@ -38,7 +34,7 @@ class UserService
return User::query()->create([ return User::query()->create([
'name' => $name, 'name' => $name,
'email' => $email, 'email' => $email,
'password' => $plainTextPassword ? $this->hash->make($plainTextPassword) : '', 'password' => $plainTextPassword ? Hash::make($plainTextPassword) : '',
'is_admin' => $isAdmin, 'is_admin' => $isAdmin,
'sso_id' => $ssoId, 'sso_id' => $ssoId,
'sso_provider' => $ssoProvider, 'sso_provider' => $ssoProvider,
@ -94,7 +90,7 @@ class UserService
$user->update([ $user->update([
'name' => $name, 'name' => $name,
'email' => $email, 'email' => $email,
'password' => $password ? $this->hash->make($password) : $user->password, 'password' => $password ? Hash::make($password) : $user->password,
'is_admin' => $isAdmin ?? $user->is_admin, 'is_admin' => $isAdmin ?? $user->is_admin,
'avatar' => $avatar ? $this->createNewAvatar($avatar, $user) : null, 'avatar' => $avatar ? $this->createNewAvatar($avatar, $user) : null,
]); ]);

View file

@ -5,12 +5,12 @@ namespace App\Services;
use App\Http\Integrations\YouTube\Requests\SearchVideosRequest; use App\Http\Integrations\YouTube\Requests\SearchVideosRequest;
use App\Http\Integrations\YouTube\YouTubeConnector; use App\Http\Integrations\YouTube\YouTubeConnector;
use App\Models\Song; use App\Models\Song;
use Illuminate\Cache\Repository as Cache; use Illuminate\Support\Facades\Cache;
use Throwable; use Throwable;
class YouTubeService class YouTubeService
{ {
public function __construct(private readonly YouTubeConnector $connector, private readonly Cache $cache) public function __construct(private readonly YouTubeConnector $connector)
{ {
} }
@ -29,7 +29,7 @@ class YouTubeService
$hash = md5(serialize($request->query()->all())); $hash = md5(serialize($request->query()->all()));
try { try {
return $this->cache->remember( return Cache::remember(
"youtube.$hash", "youtube.$hash",
now()->addWeek(), now()->addWeek(),
fn () => $this->connector->send($request)->object() fn () => $this->connector->send($request)->object()

View file

@ -4,7 +4,6 @@ namespace App\Values;
use App\Models\Album; use App\Models\Album;
use App\Models\Artist; use App\Models\Artist;
use App\Services\Helper;
use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
@ -71,7 +70,7 @@ final class SongScanInformation implements Arrayable
length: (float) Arr::get($info, 'playtime_seconds'), length: (float) Arr::get($info, 'playtime_seconds'),
cover: $cover, cover: $cover,
path: $path, path: $path,
mTime: Helper::getModifiedTime($path) mTime: get_mtime($path)
); );
} }

View file

@ -7,7 +7,6 @@ use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack; use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Tests\TestCase; use Tests\TestCase;
@ -24,7 +23,7 @@ class ApplicationInformationServiceTest extends TestCase
]); ]);
$client = new Client(['handler' => HandlerStack::create($mock)]); $client = new Client(['handler' => HandlerStack::create($mock)]);
$service = new ApplicationInformationService($client, app(Cache::class)); $service = new ApplicationInformationService($client);
self::assertSame($latestVersion, $service->getLatestVersionNumber()); self::assertSame($latestVersion, $service->getLatestVersionNumber());
self::assertSame($latestVersion, cache()->get('latestKoelVersion')); self::assertSame($latestVersion, cache()->get('latestKoelVersion'));

View file

@ -6,7 +6,6 @@ use App\Repositories\UserRepository;
use App\Services\AuthenticationService; use App\Services\AuthenticationService;
use App\Services\TokenManager; use App\Services\TokenManager;
use Illuminate\Auth\Passwords\PasswordBroker; use Illuminate\Auth\Passwords\PasswordBroker;
use Illuminate\Hashing\HashManager;
use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Password;
use Mockery\MockInterface; use Mockery\MockInterface;
use Tests\TestCase; use Tests\TestCase;
@ -15,7 +14,6 @@ class AuthenticationServiceTest extends TestCase
{ {
private UserRepository|MockInterface $userRepository; private UserRepository|MockInterface $userRepository;
private TokenManager|MockInterface $tokenManager; private TokenManager|MockInterface $tokenManager;
private HashManager|MockInterface $hash;
private PasswordBroker|MockInterface $passwordBroker; private PasswordBroker|MockInterface $passwordBroker;
private AuthenticationService $service; private AuthenticationService $service;
@ -25,13 +23,11 @@ class AuthenticationServiceTest extends TestCase
$this->userRepository = $this->mock(UserRepository::class); $this->userRepository = $this->mock(UserRepository::class);
$this->tokenManager = $this->mock(TokenManager::class); $this->tokenManager = $this->mock(TokenManager::class);
$this->hash = $this->mock(HashManager::class);
$this->passwordBroker = $this->mock(PasswordBroker::class); $this->passwordBroker = $this->mock(PasswordBroker::class);
$this->service = new AuthenticationService( $this->service = new AuthenticationService(
$this->userRepository, $this->userRepository,
$this->tokenManager, $this->tokenManager,
$this->hash,
$this->passwordBroker $this->passwordBroker
); );
} }

View file

@ -10,7 +10,6 @@ use App\Services\MediaInformationService;
use App\Services\MediaMetadataService; use App\Services\MediaMetadataService;
use App\Values\AlbumInformation; use App\Values\AlbumInformation;
use App\Values\ArtistInformation; use App\Values\ArtistInformation;
use Illuminate\Cache\Repository as Cache;
use Mockery; use Mockery;
use Mockery\LegacyMockInterface; use Mockery\LegacyMockInterface;
use Mockery\MockInterface; use Mockery\MockInterface;
@ -29,11 +28,7 @@ class MediaInformationServiceTest extends TestCase
$this->encyclopedia = Mockery::mock(LastfmService::class); $this->encyclopedia = Mockery::mock(LastfmService::class);
$this->mediaMetadataService = Mockery::mock(MediaMetadataService::class); $this->mediaMetadataService = Mockery::mock(MediaMetadataService::class);
$this->mediaInformationService = new MediaInformationService( $this->mediaInformationService = new MediaInformationService($this->encyclopedia, $this->mediaMetadataService);
$this->encyclopedia,
$this->mediaMetadataService,
app(Cache::class)
);
} }
public function testGetAlbumInformation(): void public function testGetAlbumInformation(): void