mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat: rename (alias) koel:sync to koel:scan and add owner/private options
This commit is contained in:
parent
e0ca8e8bd5
commit
9d94df50a8
42 changed files with 406 additions and 334 deletions
|
@ -11,6 +11,7 @@ use Illuminate\Contracts\Console\Kernel as Artisan;
|
|||
use Illuminate\Contracts\Hashing\Hasher as Hash;
|
||||
use Illuminate\Database\DatabaseManager as DB;
|
||||
use Illuminate\Encryption\Encrypter;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
use Jackiedo\DotenvEditor\DotenvEditor;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
@ -344,7 +345,7 @@ class InitCommand extends Command
|
|||
|
||||
private static function isValidMediaPath(string $path): bool
|
||||
{
|
||||
return is_dir($path) && is_readable($path);
|
||||
return File::isDirectory($path) && File::isReadable($path);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,68 +4,91 @@ namespace App\Console\Commands;
|
|||
|
||||
use App\Libraries\WatchRecord\InotifyWatchRecord;
|
||||
use App\Models\Setting;
|
||||
use App\Services\MediaSyncService;
|
||||
use App\Values\SyncResult;
|
||||
use App\Models\User;
|
||||
use App\Services\MediaScanner;
|
||||
use App\Values\ScanConfiguration;
|
||||
use App\Values\ScanResult;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
|
||||
class SyncCommand extends Command
|
||||
class ScanCommand extends Command
|
||||
{
|
||||
protected $signature = 'koel:sync
|
||||
protected $signature = 'koel:scan
|
||||
{record? : A single watch record. Consult Wiki for more info.}
|
||||
{--ignore=* : The comma-separated tags to ignore (exclude) from syncing}
|
||||
{--force : Force re-syncing even unchanged files}';
|
||||
{--O|owner= : The ID of the user who should own the newly scanned songs. Defaults to the first admin user.}
|
||||
{--P|private : Whether to make the newly scanned songs private to the user.}
|
||||
{--V|verbose : Show more details about the scanning process
|
||||
{--I|ignore=* : The comma-separated tags to ignore (exclude) from scanning}
|
||||
{--F|force : Force re-scanning even unchanged files}';
|
||||
|
||||
protected $description = 'Sync songs found in configured directory against the database.';
|
||||
protected $description = 'Scan for songs in the configured directory.';
|
||||
|
||||
private ?string $mediaPath;
|
||||
private ?User $owner;
|
||||
private ProgressBar $progressBar;
|
||||
|
||||
public function __construct(private MediaSyncService $mediaSyncService)
|
||||
public function __construct(private MediaScanner $mediaScanner)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->mediaSyncService->on('paths-gathered', function (array $paths): void {
|
||||
$this->mediaScanner->on('paths-gathered', function (array $paths): void {
|
||||
$this->progressBar = new ProgressBar($this->output, count($paths));
|
||||
});
|
||||
|
||||
$this->mediaSyncService->on('progress', [$this, 'onSyncProgress']);
|
||||
$this->mediaScanner->on('progress', [$this, 'onScanProgress']);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setAliases(['koel:sync']);
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->owner = $this->getOwner();
|
||||
$this->mediaPath = $this->getMediaPath();
|
||||
|
||||
$record = $this->argument('record');
|
||||
|
||||
if ($record) {
|
||||
$this->syncSingleRecord($record);
|
||||
$this->scanSingleRecord($record);
|
||||
} else {
|
||||
$this->syncAll();
|
||||
$this->scanMediaPath();
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync all files in the configured media path.
|
||||
* Scan all files in the configured media path.
|
||||
*/
|
||||
private function syncAll(): void
|
||||
private function scanMediaPath(): void
|
||||
{
|
||||
$this->components->info('Scanning ' . $this->mediaPath);
|
||||
|
||||
// The tags to ignore from syncing.
|
||||
// The tags to ignore from scanning.
|
||||
// Notice that this is only meaningful for existing records.
|
||||
// New records will have every applicable field synced in.
|
||||
// New records will have every applicable field scanned.
|
||||
$ignores = collect($this->option('ignore'))->sort()->values()->all();
|
||||
|
||||
if ($ignores) {
|
||||
$this->components->info('Ignoring tag(s): ' . implode(', ', $ignores));
|
||||
}
|
||||
|
||||
$results = $this->mediaSyncService->sync($ignores, $this->option('force'));
|
||||
$config = ScanConfiguration::make(
|
||||
owner: $this->owner,
|
||||
// When scanning via CLI, the songs should be public by default, unless explicitly specified otherwise.
|
||||
makePublic: !$this->option('private'),
|
||||
ignores: $ignores,
|
||||
force: $this->option('force')
|
||||
);
|
||||
|
||||
$results = $this->mediaScanner->scan($config);
|
||||
|
||||
$this->newLine(2);
|
||||
$this->components->info('Scanning completed!');
|
||||
|
@ -87,12 +110,12 @@ class SyncCommand extends Command
|
|||
*
|
||||
* @see http://man7.org/linux/man-pages/man1/inotifywait.1.html
|
||||
*/
|
||||
private function syncSingleRecord(string $record): void
|
||||
private function scanSingleRecord(string $record): void
|
||||
{
|
||||
$this->mediaSyncService->syncByWatchRecord(new InotifyWatchRecord($record));
|
||||
$this->mediaScanner->scanWatchRecord(new InotifyWatchRecord($record));
|
||||
}
|
||||
|
||||
public function onSyncProgress(SyncResult $result): void
|
||||
public function onScanProgress(ScanResult $result): void
|
||||
{
|
||||
if (!$this->option('verbose')) {
|
||||
$this->progressBar->advance();
|
||||
|
@ -106,7 +129,7 @@ class SyncCommand extends Command
|
|||
$result->isSuccess() => "<fg=green>OK</>",
|
||||
$result->isSkipped() => "<fg=yellow>SKIPPED</>",
|
||||
$result->isError() => "<fg=red>ERROR</>",
|
||||
default => throw new RuntimeException("Unknown sync result type: {$result->type}")
|
||||
default => throw new RuntimeException("Unknown scan result type: {$result->type}")
|
||||
});
|
||||
|
||||
if ($result->isError()) {
|
||||
|
@ -127,7 +150,7 @@ class SyncCommand extends Command
|
|||
while (true) {
|
||||
$path = $this->ask('Absolute path to your media directory');
|
||||
|
||||
if (is_dir($path) && is_readable($path)) {
|
||||
if (File::isDirectory($path) && File::isReadable($path)) {
|
||||
Setting::set('media_path', $path);
|
||||
break;
|
||||
}
|
||||
|
@ -137,4 +160,29 @@ class SyncCommand extends Command
|
|||
|
||||
return $path;
|
||||
}
|
||||
|
||||
private function getOwner(): User
|
||||
{
|
||||
$specifiedOwner = $this->option('owner');
|
||||
|
||||
if ($specifiedOwner) {
|
||||
/** @var User $user */
|
||||
$user = User::findOr($specifiedOwner, function () use ($specifiedOwner): void {
|
||||
$this->components->error("User with ID $specifiedOwner does not exist.");
|
||||
exit(self::INVALID);
|
||||
});
|
||||
|
||||
$this->components->info("Setting owner to $user->name (ID $user->id).");
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = User::where('is_admin', true)->oldest()->firstOrFail();
|
||||
$this->components->warn(
|
||||
"No song owner specified. Setting the first admin ($user->name, ID $user->id) as owner."
|
||||
);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
15
app/Events/MediaScanCompleted.php
Normal file
15
app/Events/MediaScanCompleted.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Values\ScanResultCollection;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class MediaScanCompleted extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(public ScanResultCollection $results)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Values\SyncResultCollection;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class MediaSyncCompleted extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(public SyncResultCollection $results)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class OwnerNotSetPriorToScanException extends Exception
|
||||
{
|
||||
private function __construct($message = '', $code = 0, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new static('An owner must be set prior to scanning, as a song must be owned by a user.');
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\File as FileFacade;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
|
@ -38,7 +39,7 @@ function artist_image_url(?string $fileName): ?string
|
|||
|
||||
function koel_version(): string
|
||||
{
|
||||
return trim(file_get_contents(base_path('.version')));
|
||||
return trim(FileFacade::get(base_path('.version')));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Throwable;
|
||||
|
||||
class FetchDemoCreditController extends Controller
|
||||
|
@ -10,7 +11,7 @@ class FetchDemoCreditController extends Controller
|
|||
public function __invoke()
|
||||
{
|
||||
try {
|
||||
return response()->json(json_decode(file_get_contents(resource_path('demo-credits.json')), true));
|
||||
return response()->json(json_decode(File::get(resource_path('demo-credits.json')), true));
|
||||
} catch (Throwable) {
|
||||
return response()->json();
|
||||
}
|
||||
|
|
|
@ -6,11 +6,13 @@ use App\Http\Controllers\Controller;
|
|||
use App\Http\Requests\API\SettingRequest;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Services\MediaSyncService;
|
||||
use App\Services\MediaScanner;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class SettingController extends Controller
|
||||
{
|
||||
public function __construct(private MediaSyncService $mediaSyncService)
|
||||
/** @param User $user */
|
||||
public function __construct(private MediaScanner $mediaSyncService, private ?Authenticatable $user)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -19,7 +21,8 @@ class SettingController extends Controller
|
|||
$this->authorize('admin', User::class);
|
||||
|
||||
Setting::set('media_path', rtrim(trim($request->media_path), '/'));
|
||||
$this->mediaSyncService->sync();
|
||||
|
||||
$this->mediaSyncService->scan($this->user, makePublic: true);
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\MediaSyncCompleted;
|
||||
use App\Events\MediaScanCompleted;
|
||||
use App\Models\Song;
|
||||
use App\Repositories\SongRepository;
|
||||
use App\Values\SyncResult;
|
||||
use App\Values\ScanResult;
|
||||
|
||||
class DeleteNonExistingRecordsPostSync
|
||||
{
|
||||
|
@ -13,11 +13,11 @@ class DeleteNonExistingRecordsPostSync
|
|||
{
|
||||
}
|
||||
|
||||
public function handle(MediaSyncCompleted $event): void
|
||||
public function handle(MediaScanCompleted $event): void
|
||||
{
|
||||
$paths = $event->results
|
||||
->valid()
|
||||
->map(static fn (SyncResult $result) => $result->path)
|
||||
->map(static fn (ScanResult $result) => $result->path)
|
||||
->merge($this->songRepository->getAllHostedOnS3()->pluck('path'))
|
||||
->toArray();
|
||||
|
||||
|
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\MediaSyncCompleted;
|
||||
use App\Values\SyncResult;
|
||||
use App\Events\MediaScanCompleted;
|
||||
use App\Values\ScanResult;
|
||||
use Illuminate\Support\Collection;
|
||||
use Throwable;
|
||||
|
||||
class WriteSyncLog
|
||||
{
|
||||
public function handle(MediaSyncCompleted $event): void
|
||||
public function handle(MediaScanCompleted $event): void
|
||||
{
|
||||
$transformer = static fn (SyncResult $entry) => (string) $entry;
|
||||
$transformer = static fn (ScanResult $entry) => (string) $entry;
|
||||
|
||||
/** @var Collection $messages */
|
||||
$messages = config('koel.sync_log_level') === 'all'
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
namespace App\Providers;
|
||||
|
||||
use App\Events\LibraryChanged;
|
||||
use App\Events\MediaSyncCompleted;
|
||||
use App\Events\MediaScanCompleted;
|
||||
use App\Events\PlaybackStarted;
|
||||
use App\Events\SongLikeToggled;
|
||||
use App\Events\SongsBatchLiked;
|
||||
|
@ -42,7 +42,7 @@ class EventServiceProvider extends BaseServiceProvider
|
|||
PruneLibrary::class,
|
||||
],
|
||||
|
||||
MediaSyncCompleted::class => [
|
||||
MediaScanCompleted::class => [
|
||||
DeleteNonExistingRecordsPostSync::class,
|
||||
WriteSyncLog::class,
|
||||
],
|
||||
|
|
|
@ -2,21 +2,20 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Exceptions\OwnerNotSetPriorToScanException;
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Repositories\SongRepository;
|
||||
use App\Values\ScanConfiguration;
|
||||
use App\Values\ScanResult;
|
||||
use App\Values\SongScanInformation;
|
||||
use App\Values\SyncResult;
|
||||
use getID3;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Illuminate\Support\Arr;
|
||||
use SplFileInfo;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class FileSynchronizer
|
||||
class FileScanner
|
||||
{
|
||||
private ?int $fileModifiedTime = null;
|
||||
private ?string $filePath = null;
|
||||
|
@ -26,8 +25,6 @@ class FileSynchronizer
|
|||
*/
|
||||
private ?Song $song;
|
||||
|
||||
private ?User $owner = null;
|
||||
|
||||
private ?string $syncError = null;
|
||||
|
||||
public function __construct(
|
||||
|
@ -51,13 +48,6 @@ class FileSynchronizer
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function setOwner(User $owner): static
|
||||
{
|
||||
$this->owner = $owner;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFileScanInformation(): ?SongScanInformation
|
||||
{
|
||||
$raw = $this->getID3->analyze($this->filePath);
|
||||
|
@ -75,54 +65,49 @@ class FileSynchronizer
|
|||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the song with all available media info into the database.
|
||||
*
|
||||
* @param array<string> $ignores The tags to ignore/exclude (only taken into account if the song already exists)
|
||||
* @param bool $force Whether to force syncing, even if the file is unchanged
|
||||
*/
|
||||
public function sync(array $ignores = [], bool $force = false): SyncResult
|
||||
public function scan(ScanConfiguration $config): ScanResult
|
||||
{
|
||||
if (!$this->owner) {
|
||||
throw OwnerNotSetPriorToScanException::create();
|
||||
}
|
||||
|
||||
if (!$this->isFileNewOrChanged() && !$force) {
|
||||
return SyncResult::skipped($this->filePath);
|
||||
if (!$this->isFileNewOrChanged() && !$config->force) {
|
||||
return ScanResult::skipped($this->filePath);
|
||||
}
|
||||
|
||||
$info = $this->getFileScanInformation()?->toArray();
|
||||
|
||||
if (!$info) {
|
||||
return SyncResult::error($this->filePath, $this->syncError);
|
||||
return ScanResult::error($this->filePath, $this->syncError);
|
||||
}
|
||||
|
||||
if (!$this->isFileNew()) {
|
||||
Arr::forget($info, $ignores);
|
||||
Arr::forget($info, $config->ignores);
|
||||
}
|
||||
|
||||
$artist = Arr::get($info, 'artist') ? Artist::getOrCreate($info['artist']) : $this->song->artist;
|
||||
$albumArtist = Arr::get($info, 'albumartist') ? Artist::getOrCreate($info['albumartist']) : $artist;
|
||||
$album = Arr::get($info, 'album') ? Album::getOrCreate($albumArtist, $info['album']) : $this->song->album;
|
||||
|
||||
if (!in_array('cover', $ignores, true) && !$album->has_cover) {
|
||||
if (!in_array('cover', $config->ignores, true) && !$album->has_cover) {
|
||||
$this->tryGenerateAlbumCover($album, Arr::get($info, 'cover', []));
|
||||
}
|
||||
|
||||
$data = Arr::except($info, ['album', 'artist', 'albumartist', 'cover']);
|
||||
$data['album_id'] = $album->id;
|
||||
$data['artist_id'] = $artist->id;
|
||||
$data['owner_id'] = $this->owner->id;
|
||||
$data['is_public'] = $config->makePublic;
|
||||
|
||||
if ($this->isFileNew()) {
|
||||
// Only set the owner if the song is new i.e. don't override the owner if the song is being updated.
|
||||
$data['owner_id'] = $config->owner->id;
|
||||
}
|
||||
|
||||
$this->song = Song::query()->updateOrCreate(['path' => $this->filePath], $data); // @phpstan-ignore-line
|
||||
|
||||
return SyncResult::success($this->filePath);
|
||||
return ScanResult::success($this->filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to generate a cover for an album based on extracted data, or use the cover file under the directory.
|
||||
*
|
||||
* @param array<mixed>|null $coverData
|
||||
* @param ?array<mixed> $coverData
|
||||
*/
|
||||
private function tryGenerateAlbumCover(Album $album, ?array $coverData): void
|
||||
{
|
|
@ -4,6 +4,7 @@ namespace App\Services;
|
|||
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MediaMetadataService
|
||||
|
@ -72,7 +73,7 @@ class MediaMetadataService
|
|||
$this->imageWriter->write($destination, $source);
|
||||
|
||||
if ($cleanUp && $artist->has_image) {
|
||||
@unlink($artist->image_path);
|
||||
File::delete($artist->image_path);
|
||||
}
|
||||
|
||||
$artist->update(['image' => basename($destination)]);
|
||||
|
@ -99,7 +100,7 @@ class MediaMetadataService
|
|||
return null;
|
||||
}
|
||||
|
||||
if (!file_exists($album->thumbnail_path)) {
|
||||
if (!File::exists($album->thumbnail_path)) {
|
||||
$this->createThumbnailForAlbum($album);
|
||||
}
|
||||
|
||||
|
@ -117,7 +118,7 @@ class MediaMetadataService
|
|||
return;
|
||||
}
|
||||
|
||||
@unlink($album->cover_path);
|
||||
@unlink($album->thumbnail_path);
|
||||
File::delete($album->cover_path);
|
||||
File::delete($album->thumbnail_path);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,19 +3,20 @@
|
|||
namespace App\Services;
|
||||
|
||||
use App\Events\LibraryChanged;
|
||||
use App\Events\MediaSyncCompleted;
|
||||
use App\Events\MediaScanCompleted;
|
||||
use App\Libraries\WatchRecord\WatchRecordInterface;
|
||||
use App\Models\Song;
|
||||
use App\Repositories\SettingRepository;
|
||||
use App\Repositories\SongRepository;
|
||||
use App\Values\SyncResult;
|
||||
use App\Values\SyncResultCollection;
|
||||
use App\Values\ScanConfiguration;
|
||||
use App\Values\ScanResult;
|
||||
use App\Values\ScanResultCollection;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use SplFileInfo;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Throwable;
|
||||
|
||||
class MediaSyncService
|
||||
class MediaScanner
|
||||
{
|
||||
/** @var array<array-key, callable> */
|
||||
private array $events = [];
|
||||
|
@ -23,26 +24,20 @@ class MediaSyncService
|
|||
public function __construct(
|
||||
private SettingRepository $settingRepository,
|
||||
private SongRepository $songRepository,
|
||||
private FileSynchronizer $fileSynchronizer,
|
||||
private FileScanner $fileScanner,
|
||||
private Finder $finder,
|
||||
private LoggerInterface $logger
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $ignores The tags to ignore.
|
||||
* Only taken into account for existing records.
|
||||
* New records will have all tags synced in regardless.
|
||||
* @param bool $force Whether to force syncing even unchanged files
|
||||
*/
|
||||
public function sync(array $ignores = [], bool $force = false): SyncResultCollection
|
||||
public function scan(ScanConfiguration $config): ScanResultCollection
|
||||
{
|
||||
/** @var string $mediaPath */
|
||||
$mediaPath = $this->settingRepository->getByKey('media_path');
|
||||
|
||||
$this->setSystemRequirements();
|
||||
|
||||
$results = SyncResultCollection::create();
|
||||
$results = ScanResultCollection::create();
|
||||
$songPaths = $this->gatherFiles($mediaPath);
|
||||
|
||||
if (isset($this->events['paths-gathered'])) {
|
||||
|
@ -51,9 +46,9 @@ class MediaSyncService
|
|||
|
||||
foreach ($songPaths as $path) {
|
||||
try {
|
||||
$result = $this->fileSynchronizer->setFile($path)->sync($ignores, $force);
|
||||
$result = $this->fileScanner->setFile($path)->scan($config);
|
||||
} catch (Throwable) {
|
||||
$result = SyncResult::error($path, 'Possible invalid file');
|
||||
$result = ScanResult::error($path, 'Possible invalid file');
|
||||
}
|
||||
|
||||
$results->add($result);
|
||||
|
@ -63,7 +58,7 @@ class MediaSyncService
|
|||
}
|
||||
}
|
||||
|
||||
event(new MediaSyncCompleted($results));
|
||||
event(new MediaScanCompleted($results));
|
||||
|
||||
// Trigger LibraryChanged, so that PruneLibrary handler is fired to prune the lib.
|
||||
event(new LibraryChanged());
|
||||
|
@ -83,7 +78,7 @@ class MediaSyncService
|
|||
return iterator_to_array(
|
||||
$this->finder->create()
|
||||
->ignoreUnreadableDirs()
|
||||
->ignoreDotFiles((bool) config('koel.ignore_dot_files')) // https://github.com/koel/koel/issues/450
|
||||
->ignoreDotFiles((bool)config('koel.ignore_dot_files')) // https://github.com/koel/koel/issues/450
|
||||
->files()
|
||||
->followLinks()
|
||||
->name('/\.(mp3|wav|ogg|m4a|flac|opus)$/i')
|
||||
|
@ -91,13 +86,18 @@ class MediaSyncService
|
|||
);
|
||||
}
|
||||
|
||||
public function syncByWatchRecord(WatchRecordInterface $record): void
|
||||
public function scanWatchRecord(WatchRecordInterface $record, ScanConfiguration $config): void
|
||||
{
|
||||
$this->logger->info("New watch record received: '{$record->getPath()}'");
|
||||
$record->isFile() ? $this->syncFileRecord($record) : $this->syncDirectoryRecord($record);
|
||||
|
||||
if ($record->isFile()) {
|
||||
$this->scanFileRecord($record, $config);
|
||||
} else {
|
||||
$this->scanDirectoryRecord($record, $config);
|
||||
}
|
||||
}
|
||||
|
||||
private function syncFileRecord(WatchRecordInterface $record): void
|
||||
private function scanFileRecord(WatchRecordInterface $record, ScanConfiguration $config): void
|
||||
{
|
||||
$path = $record->getPath();
|
||||
$this->logger->info("'$path' is a file.");
|
||||
|
@ -105,11 +105,11 @@ class MediaSyncService
|
|||
if ($record->isDeleted()) {
|
||||
$this->handleDeletedFileRecord($path);
|
||||
} elseif ($record->isNewOrModified()) {
|
||||
$this->handleNewOrModifiedFileRecord($path);
|
||||
$this->handleNewOrModifiedFileRecord($path, $config);
|
||||
}
|
||||
}
|
||||
|
||||
private function syncDirectoryRecord(WatchRecordInterface $record): void
|
||||
private function scanDirectoryRecord(WatchRecordInterface $record, ScanConfiguration $config): void
|
||||
{
|
||||
$path = $record->getPath();
|
||||
$this->logger->info("'$path' is a directory.");
|
||||
|
@ -117,7 +117,7 @@ class MediaSyncService
|
|||
if ($record->isDeleted()) {
|
||||
$this->handleDeletedDirectoryRecord($path);
|
||||
} elseif ($record->isNewOrModified()) {
|
||||
$this->handleNewOrModifiedDirectoryRecord($path);
|
||||
$this->handleNewOrModifiedDirectoryRecord($path, $config);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,14 +145,14 @@ class MediaSyncService
|
|||
}
|
||||
}
|
||||
|
||||
private function handleNewOrModifiedFileRecord(string $path): void
|
||||
private function handleNewOrModifiedFileRecord(string $path, ScanConfiguration $config): void
|
||||
{
|
||||
$result = $this->fileSynchronizer->setFile($path)->sync();
|
||||
$result = $this->fileScanner->setFile($path)->scan($config);
|
||||
|
||||
if ($result->isSuccess()) {
|
||||
$this->logger->info("Synchronized $path");
|
||||
$this->logger->info("Scanned $path");
|
||||
} else {
|
||||
$this->logger->info("Failed to synchronized $path. Maybe an invalid file?");
|
||||
$this->logger->info("Failed to scan $path. Maybe an invalid file?");
|
||||
}
|
||||
|
||||
event(new LibraryChanged());
|
||||
|
@ -171,21 +171,21 @@ class MediaSyncService
|
|||
}
|
||||
}
|
||||
|
||||
private function handleNewOrModifiedDirectoryRecord(string $path): void
|
||||
private function handleNewOrModifiedDirectoryRecord(string $path, ScanConfiguration $config): void
|
||||
{
|
||||
$syncResults = SyncResultCollection::create();
|
||||
$scanResults = ScanResultCollection::create();
|
||||
|
||||
foreach ($this->gatherFiles($path) as $file) {
|
||||
try {
|
||||
$syncResults->add($this->fileSynchronizer->setFile($file)->sync());
|
||||
$scanResults->add($this->fileScanner->setFile($file)->scan($config));
|
||||
} catch (Throwable) {
|
||||
$syncResults->add(SyncResult::error($file->getRealPath(), 'Possible invalid file'));
|
||||
$scanResults->add(ScanResult::error($file->getRealPath(), 'Possible invalid file'));
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->info("Synced all song(s) under $path");
|
||||
$this->logger->info("Scanned all song(s) under $path");
|
||||
|
||||
event(new MediaSyncCompleted($syncResults));
|
||||
event(new MediaScanCompleted($scanResults));
|
||||
event(new LibraryChanged());
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ class QueueService
|
|||
|
||||
$currentSong = $state->current_song_id ? $this->songRepository->findOne($state->current_song_id, $user) : null;
|
||||
|
||||
return QueueStateDTO::create(
|
||||
return QueueStateDTO::make(
|
||||
$this->songRepository->getMany(ids: $state->song_ids, inThatOrder: true, scopedUser: $user),
|
||||
$currentSong,
|
||||
$state->playback_position ?? 0
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Throwable;
|
||||
|
||||
class SimpleLrcReader
|
||||
|
@ -11,7 +12,7 @@ class SimpleLrcReader
|
|||
$lrcFilePath = self::getLrcFilePath($mediaFilePath);
|
||||
|
||||
try {
|
||||
return $lrcFilePath ? trim(file_get_contents($lrcFilePath)) : '';
|
||||
return $lrcFilePath ? trim(File::get($lrcFilePath)) : '';
|
||||
} catch (Throwable) {
|
||||
return '';
|
||||
}
|
||||
|
@ -22,7 +23,7 @@ class SimpleLrcReader
|
|||
foreach (['.lrc', '.LRC'] as $extension) {
|
||||
$lrcFilePath = preg_replace('/\.[^.]+$/', $extension, $mediaFilePath);
|
||||
|
||||
if (is_file($lrcFilePath) && is_readable($lrcFilePath)) {
|
||||
if (File::isFile($lrcFilePath) && File::isReadable($lrcFilePath)) {
|
||||
return $lrcFilePath;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,14 +7,16 @@ use App\Exceptions\SongUploadFailedException;
|
|||
use App\Models\Setting;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Values\ScanConfiguration;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Throwable;
|
||||
|
||||
use function Functional\memoize;
|
||||
|
||||
class UploadService
|
||||
{
|
||||
public function __construct(private FileSynchronizer $fileSynchronizer)
|
||||
public function __construct(private FileScanner $scanner)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -27,21 +29,18 @@ class UploadService
|
|||
$targetPathName = $uploadDirectory . $targetFileName;
|
||||
|
||||
try {
|
||||
$result = $this->fileSynchronizer
|
||||
->setOwner($uploader)
|
||||
->setFile($targetPathName)
|
||||
->sync();
|
||||
$result = $this->scanner->setFile($targetPathName)->scan(ScanConfiguration::make(owner: $uploader));
|
||||
} catch (Throwable) {
|
||||
@unlink($targetPathName);
|
||||
File::delete($targetPathName);
|
||||
throw new SongUploadFailedException('Unknown error');
|
||||
}
|
||||
|
||||
if ($result->isError()) {
|
||||
@unlink($targetPathName);
|
||||
File::delete($targetPathName);
|
||||
throw new SongUploadFailedException($result->error);
|
||||
}
|
||||
|
||||
return $this->fileSynchronizer->getSong();
|
||||
return $this->scanner->getSong();
|
||||
}
|
||||
|
||||
private function getUploadDirectory(User $uploader): string
|
||||
|
@ -59,9 +58,7 @@ class UploadService
|
|||
DIRECTORY_SEPARATOR
|
||||
);
|
||||
|
||||
if (!file_exists($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
File::ensureDirectoryExists($dir);
|
||||
|
||||
return $dir;
|
||||
});
|
||||
|
|
|
@ -5,14 +5,14 @@ namespace App\Values;
|
|||
use App\Models\Song;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class QueueState
|
||||
final class QueueState
|
||||
{
|
||||
private function __construct(public Collection $songs, public ?Song $currentSong, public ?int $playbackPosition)
|
||||
{
|
||||
}
|
||||
|
||||
public static function create(Collection $songs, ?Song $currentSong = null, ?int $playbackPosition = 0): static
|
||||
public static function make(Collection $songs, ?Song $currentSong = null, ?int $playbackPosition = 0): self
|
||||
{
|
||||
return new static($songs, $currentSong, $playbackPosition);
|
||||
return new self($songs, $currentSong, $playbackPosition);
|
||||
}
|
||||
}
|
||||
|
|
27
app/Values/ScanConfiguration.php
Normal file
27
app/Values/ScanConfiguration.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace App\Values;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
final class ScanConfiguration
|
||||
{
|
||||
/**
|
||||
* @param User $owner The user who owns the song
|
||||
* @param bool $makePublic Whether to make the song public
|
||||
* @param array<string> $ignores The tags to ignore/exclude (only taken into account if the song already exists)
|
||||
* @param bool $force Whether to force syncing, even if the file is unchanged
|
||||
*/
|
||||
private function __construct(public User $owner, public bool $makePublic, public array $ignores, public bool $force)
|
||||
{
|
||||
}
|
||||
|
||||
public static function make(
|
||||
User $owner,
|
||||
bool $makePublic = false,
|
||||
array $ignores = [],
|
||||
bool $force = false
|
||||
): self {
|
||||
return new self($owner, $makePublic, $ignores, $force);
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ namespace App\Values;
|
|||
use Exception;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final class SyncResult
|
||||
final class ScanResult
|
||||
{
|
||||
public const TYPE_SUCCESS = 1;
|
||||
public const TYPE_ERROR = 2;
|
||||
|
@ -14,9 +14,9 @@ final class SyncResult
|
|||
private function __construct(public string $path, public int $type, public ?string $error = null)
|
||||
{
|
||||
Assert::oneOf($type, [
|
||||
SyncResult::TYPE_SUCCESS,
|
||||
SyncResult::TYPE_ERROR,
|
||||
SyncResult::TYPE_SKIPPED,
|
||||
ScanResult::TYPE_SUCCESS,
|
||||
ScanResult::TYPE_ERROR,
|
||||
ScanResult::TYPE_SKIPPED,
|
||||
]);
|
||||
}
|
||||
|
37
app/Values/ScanResultCollection.php
Normal file
37
app/Values/ScanResultCollection.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Values;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
final class ScanResultCollection extends Collection
|
||||
{
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/** @return Collection|array<array-key, ScanResult> */
|
||||
public function valid(): Collection
|
||||
{
|
||||
return $this->filter(static fn (ScanResult $result): bool => $result->isValid());
|
||||
}
|
||||
|
||||
/** @return Collection|array<array-key, ScanResult> */
|
||||
public function success(): Collection
|
||||
{
|
||||
return $this->filter(static fn (ScanResult $result): bool => $result->isSuccess());
|
||||
}
|
||||
|
||||
/** @return Collection|array<array-key, ScanResult> */
|
||||
public function skipped(): Collection
|
||||
{
|
||||
return $this->filter(static fn (ScanResult $result): bool => $result->isSkipped());
|
||||
}
|
||||
|
||||
/** @return Collection|array<array-key, ScanResult> */
|
||||
public function error(): Collection
|
||||
{
|
||||
return $this->filter(static fn (ScanResult $result): bool => $result->isError());
|
||||
}
|
||||
}
|
|
@ -91,7 +91,7 @@ final class SmartPlaylistRule implements Arrayable
|
|||
Assert::countBetween($config['value'], 1, 2);
|
||||
}
|
||||
|
||||
public static function create(array $config): self
|
||||
public static function make(array $config): self
|
||||
{
|
||||
return new self($config);
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ final class SmartPlaylistRule implements Arrayable
|
|||
public function equals(array|self $rule): bool
|
||||
{
|
||||
if (is_array($rule)) {
|
||||
$rule = self::create($rule);
|
||||
$rule = self::make($rule);
|
||||
}
|
||||
|
||||
return $this->operator === $rule->operator
|
||||
|
|
|
@ -14,11 +14,11 @@ final class SmartPlaylistRuleGroup implements Arrayable
|
|||
Assert::uuid($id);
|
||||
}
|
||||
|
||||
public static function create(array $array): self
|
||||
public static function make(array $array): self
|
||||
{
|
||||
return new self(
|
||||
id: Arr::get($array, 'id'),
|
||||
rules: collect(Arr::get($array, 'rules', []))->transform([SmartPlaylistRule::class, 'create']),
|
||||
rules: collect(Arr::get($array, 'rules', []))->transform([SmartPlaylistRule::class, 'make']),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,6 @@ final class SmartPlaylistRuleGroupCollection extends Collection
|
|||
{
|
||||
public static function create(array $array): self
|
||||
{
|
||||
return new self(collect($array)->transform([SmartPlaylistRuleGroup::class, 'create']));
|
||||
return new self(collect($array)->transform([SmartPlaylistRuleGroup::class, 'make']));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Values;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
final class SyncResultCollection extends Collection
|
||||
{
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/** @return Collection|array<array-key, SyncResult> */
|
||||
public function valid(): Collection
|
||||
{
|
||||
return $this->filter(static fn (SyncResult $result): bool => $result->isValid());
|
||||
}
|
||||
|
||||
/** @return Collection|array<array-key, SyncResult> */
|
||||
public function success(): Collection
|
||||
{
|
||||
return $this->filter(static fn (SyncResult $result): bool => $result->isSuccess());
|
||||
}
|
||||
|
||||
/** @return Collection|array<array-key, SyncResult> */
|
||||
public function skipped(): Collection
|
||||
{
|
||||
return $this->filter(static fn (SyncResult $result): bool => $result->isSkipped());
|
||||
}
|
||||
|
||||
/** @return Collection|array<array-key, SyncResult> */
|
||||
public function error(): Collection
|
||||
{
|
||||
return $this->filter(static fn (SyncResult $result): bool => $result->isError());
|
||||
}
|
||||
}
|
|
@ -61,7 +61,7 @@ class PlaylistTest extends TestCase
|
|||
/** @var User $user */
|
||||
$user = User::factory()->create();
|
||||
|
||||
$rule = SmartPlaylistRule::create([
|
||||
$rule = SmartPlaylistRule::make([
|
||||
'model' => 'artist.name',
|
||||
'operator' => SmartPlaylistRule::OPERATOR_IS,
|
||||
'value' => ['Bob Dylan'],
|
||||
|
@ -96,7 +96,7 @@ class PlaylistTest extends TestCase
|
|||
[
|
||||
'id' => '2a4548cd-c67f-44d4-8fec-34ff75c8a026',
|
||||
'rules' => [
|
||||
SmartPlaylistRule::create([
|
||||
SmartPlaylistRule::make([
|
||||
'model' => 'artist.name',
|
||||
'operator' => SmartPlaylistRule::OPERATOR_IS,
|
||||
'value' => ['Bob Dylan'],
|
||||
|
|
|
@ -4,19 +4,19 @@ namespace Tests\Feature;
|
|||
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Services\MediaSyncService;
|
||||
use App\Values\SyncResultCollection;
|
||||
use App\Services\MediaScanner;
|
||||
use App\Values\ScanResultCollection;
|
||||
use Mockery\MockInterface;
|
||||
|
||||
class SettingTest extends TestCase
|
||||
{
|
||||
private MediaSyncService|MockInterface $mediaSyncService;
|
||||
private MediaScanner|MockInterface $mediaSyncService;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->mediaSyncService = self::mock(MediaSyncService::class);
|
||||
$this->mediaSyncService = self::mock(MediaScanner::class);
|
||||
}
|
||||
|
||||
public function testSaveSettings(): void
|
||||
|
@ -25,7 +25,7 @@ class SettingTest extends TestCase
|
|||
$admin = User::factory()->admin()->create();
|
||||
|
||||
$this->mediaSyncService->shouldReceive('sync')->once()
|
||||
->andReturn(SyncResultCollection::create());
|
||||
->andReturn(ScanResultCollection::create());
|
||||
|
||||
$this->putAs('/api/settings', ['media_path' => __DIR__], $admin)
|
||||
->assertSuccessful();
|
||||
|
|
|
@ -6,34 +6,25 @@ use App\Events\LibraryChanged;
|
|||
use App\Exceptions\MediaPathNotSetException;
|
||||
use App\Exceptions\SongUploadFailedException;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Services\UploadService;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Mockery\MockInterface;
|
||||
|
||||
class UploadTest extends TestCase
|
||||
{
|
||||
private UploadService|MockInterface $uploadService;
|
||||
private UploadedFile $file;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->uploadService = self::mock(UploadService::class);
|
||||
$this->file = UploadedFile::fromFile(__DIR__ . '/../songs/full.mp3', 'song.mp3');
|
||||
}
|
||||
|
||||
public function testUnauthorizedPost(): void
|
||||
{
|
||||
Setting::set('media_path', '/media/koel');
|
||||
|
||||
$this->uploadService
|
||||
->shouldReceive('handleUploadedFile')
|
||||
->never();
|
||||
Setting::set('media_path');
|
||||
|
||||
$this->postAs('/api/upload', ['file' => $this->file])->assertForbidden();
|
||||
}
|
||||
|
@ -47,38 +38,24 @@ class UploadTest extends TestCase
|
|||
];
|
||||
}
|
||||
|
||||
/** @dataProvider provideUploadExceptions */
|
||||
public function testPostShouldFail(string $exceptionClass, int $statusCode): void
|
||||
public function testUploadFailsIfMediaPathIsNotSet(): void
|
||||
{
|
||||
Setting::set('media_path');
|
||||
|
||||
/** @var User $admin */
|
||||
$admin = User::factory()->admin()->create();
|
||||
|
||||
$this->uploadService
|
||||
->shouldReceive('handleUploadedFile')
|
||||
->once()
|
||||
->with($this->file)
|
||||
->andThrow($exceptionClass);
|
||||
|
||||
$this->postAs('/api/upload', ['file' => $this->file], $admin)->assertStatus($statusCode);
|
||||
$this->postAs('/api/upload', ['file' => $this->file], $admin)->assertForbidden();
|
||||
}
|
||||
|
||||
public function testPost(): void
|
||||
public function testUploadSuccessful(): void
|
||||
{
|
||||
Event::fake(LibraryChanged::class);
|
||||
Setting::set('media_path', '/media/koel');
|
||||
|
||||
/** @var Song $song */
|
||||
$song = Song::factory()->create();
|
||||
Setting::set('media_path', public_path('sandbox/media'));
|
||||
|
||||
/** @var User $admin */
|
||||
$admin = User::factory()->admin()->create();
|
||||
|
||||
$this->uploadService
|
||||
->shouldReceive('handleUploadedFile')
|
||||
->once()
|
||||
->with($this->file)
|
||||
->andReturn($song);
|
||||
|
||||
$this->postAs('/api/upload', ['file' => $this->file], $admin)->assertJsonStructure(['song', 'album']);
|
||||
Event::assertDispatched(LibraryChanged::class);
|
||||
}
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
namespace Tests\Integration\Listeners;
|
||||
|
||||
use App\Events\MediaSyncCompleted;
|
||||
use App\Events\MediaScanCompleted;
|
||||
use App\Listeners\DeleteNonExistingRecordsPostSync;
|
||||
use App\Models\Song;
|
||||
use App\Values\SyncResult;
|
||||
use App\Values\SyncResultCollection;
|
||||
use App\Values\ScanResult;
|
||||
use App\Values\ScanResultCollection;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
@ -24,7 +24,7 @@ class DeleteNonExistingRecordsPostSyncTest extends TestCase
|
|||
public function testHandleDoesNotDeleteS3Entries(): void
|
||||
{
|
||||
$song = Song::factory()->create(['path' => 's3://do-not/delete-me.mp3']);
|
||||
$this->listener->handle(new MediaSyncCompleted(SyncResultCollection::create()));
|
||||
$this->listener->handle(new MediaScanCompleted(ScanResultCollection::create()));
|
||||
|
||||
self::assertModelExists($song);
|
||||
}
|
||||
|
@ -36,11 +36,11 @@ class DeleteNonExistingRecordsPostSyncTest extends TestCase
|
|||
|
||||
self::assertCount(4, Song::all());
|
||||
|
||||
$syncResult = SyncResultCollection::create();
|
||||
$syncResult->add(SyncResult::success($songs[0]->path));
|
||||
$syncResult->add(SyncResult::skipped($songs[3]->path));
|
||||
$syncResult = ScanResultCollection::create();
|
||||
$syncResult->add(ScanResult::success($songs[0]->path));
|
||||
$syncResult->add(ScanResult::skipped($songs[3]->path));
|
||||
|
||||
$this->listener->handle(new MediaSyncCompleted($syncResult));
|
||||
$this->listener->handle(new MediaScanCompleted($syncResult));
|
||||
|
||||
self::assertModelExists($songs[0]);
|
||||
self::assertModelExists($songs[3]);
|
||||
|
|
|
@ -8,6 +8,7 @@ use GuzzleHttp\Handler\MockHandler;
|
|||
use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ApplicationInformationServiceTest extends TestCase
|
||||
|
@ -17,7 +18,7 @@ class ApplicationInformationServiceTest extends TestCase
|
|||
$latestVersion = 'v1.1.2';
|
||||
|
||||
$mock = new MockHandler([
|
||||
new Response(200, [], file_get_contents(__DIR__ . '../../../blobs/github-tags.json')),
|
||||
new Response(200, [], File::get(__DIR__ . '../../../blobs/github-tags.json')),
|
||||
]);
|
||||
|
||||
$client = new Client(['handler' => HandlerStack::create($mock)]);
|
||||
|
|
|
@ -2,24 +2,25 @@
|
|||
|
||||
namespace Tests\Integration\Services;
|
||||
|
||||
use App\Services\FileSynchronizer;
|
||||
use App\Services\FileScanner;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FileSynchronizerTest extends TestCase
|
||||
class FileScannerTest extends TestCase
|
||||
{
|
||||
private FileSynchronizer $fileSynchronizer;
|
||||
private FileScanner $scanner;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->fileSynchronizer = app(FileSynchronizer::class);
|
||||
$this->scanner = app(FileScanner::class);
|
||||
}
|
||||
|
||||
public function testGetFileInfo(): void
|
||||
{
|
||||
$info = $this->fileSynchronizer->setFile(__DIR__ . '/../../songs/full.mp3')->getFileScanInformation();
|
||||
$info = $this->scanner->setFile(__DIR__ . '/../../songs/full.mp3')->getFileScanInformation();
|
||||
|
||||
$expectedData = [
|
||||
'artist' => 'Koel',
|
||||
|
@ -29,7 +30,7 @@ class FileSynchronizerTest extends TestCase
|
|||
'disc' => 3,
|
||||
'lyrics' => "Foo\rbar",
|
||||
'cover' => [
|
||||
'data' => file_get_contents(__DIR__ . '/../../blobs/cover.png'),
|
||||
'data' => File::get(__DIR__ . '/../../blobs/cover.png'),
|
||||
'image_mime' => 'image/png',
|
||||
'image_width' => 512,
|
||||
'image_height' => 512,
|
||||
|
@ -50,7 +51,7 @@ class FileSynchronizerTest extends TestCase
|
|||
public function testGetFileInfoVorbisCommentsFlac(): void
|
||||
{
|
||||
$flacPath = __DIR__ . '/../../songs/full-vorbis-comments.flac';
|
||||
$info = $this->fileSynchronizer->setFile($flacPath)->getFileScanInformation();
|
||||
$info = $this->scanner->setFile($flacPath)->getFileScanInformation();
|
||||
|
||||
$expectedData = [
|
||||
'artist' => 'Koel',
|
||||
|
@ -61,7 +62,7 @@ class FileSynchronizerTest extends TestCase
|
|||
'disc' => 3,
|
||||
'lyrics' => "Foo\r\nbar",
|
||||
'cover' => [
|
||||
'data' => file_get_contents(__DIR__ . '/../../blobs/cover.png'),
|
||||
'data' => File::get(__DIR__ . '/../../blobs/cover.png'),
|
||||
'image_mime' => 'image/png',
|
||||
'image_width' => 512,
|
||||
'image_height' => 512,
|
||||
|
@ -78,9 +79,9 @@ class FileSynchronizerTest extends TestCase
|
|||
|
||||
public function testSongWithoutTitleHasFileNameAsTitle(): void
|
||||
{
|
||||
$this->fileSynchronizer->setFile(__DIR__ . '/../../songs/blank.mp3');
|
||||
$this->scanner->setFile(__DIR__ . '/../../songs/blank.mp3');
|
||||
|
||||
self::assertSame('blank', $this->fileSynchronizer->getFileScanInformation()->title);
|
||||
self::assertSame('blank', $this->scanner->getFileScanInformation()->title);
|
||||
}
|
||||
|
||||
public function testIgnoreLrcFileIfEmbeddedLyricsAvailable(): void
|
||||
|
@ -91,7 +92,7 @@ class FileSynchronizerTest extends TestCase
|
|||
copy(__DIR__ . '/../../songs/full.mp3', $mediaFile);
|
||||
copy(__DIR__ . '/../../blobs/simple.lrc', $lrcFile);
|
||||
|
||||
self::assertSame("Foo\rbar", $this->fileSynchronizer->setFile($mediaFile)->getFileScanInformation()->lyrics);
|
||||
self::assertSame("Foo\rbar", $this->scanner->setFile($mediaFile)->getFileScanInformation()->lyrics);
|
||||
}
|
||||
|
||||
public function testReadLrcFileIfEmbeddedLyricsNotAvailable(): void
|
||||
|
@ -102,7 +103,7 @@ class FileSynchronizerTest extends TestCase
|
|||
copy(__DIR__ . '/../../songs/blank.mp3', $mediaFile);
|
||||
copy(__DIR__ . '/../../blobs/simple.lrc', $lrcFile);
|
||||
|
||||
$info = $this->fileSynchronizer->setFile($mediaFile)->getFileScanInformation();
|
||||
$info = $this->scanner->setFile($mediaFile)->getFileScanInformation();
|
||||
|
||||
self::assertSame("Line 1\nLine 2\nLine 3", $info->lyrics);
|
||||
}
|
|
@ -4,6 +4,7 @@ namespace Tests\Integration\Services;
|
|||
|
||||
use App\Models\Album;
|
||||
use App\Services\MediaMetadataService;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MediaMetadataServiceTest extends TestCase
|
||||
|
@ -17,7 +18,7 @@ class MediaMetadataServiceTest extends TestCase
|
|||
|
||||
public function testGetAlbumThumbnailUrl(): void
|
||||
{
|
||||
copy(__DIR__ . '/../../blobs/cover.png', album_cover_path('album-cover-for-thumbnail-test.jpg'));
|
||||
File::copy(__DIR__ . '/../../blobs/cover.png', album_cover_path('album-cover-for-thumbnail-test.jpg'));
|
||||
|
||||
/** @var Album $album */
|
||||
$album = Album::factory()->create(['cover' => 'album-cover-for-thumbnail-test.jpg']);
|
||||
|
@ -39,8 +40,8 @@ class MediaMetadataServiceTest extends TestCase
|
|||
|
||||
private function cleanUp(): void
|
||||
{
|
||||
@unlink(album_cover_path('album-cover-for-thumbnail-test.jpg'));
|
||||
@unlink(album_cover_path('album-cover-for-thumbnail-test_thumb.jpg'));
|
||||
File::delete(album_cover_path('album-cover-for-thumbnail-test.jpg'));
|
||||
File::delete(album_cover_path('album-cover-for-thumbnail-test_thumb.jpg'));
|
||||
|
||||
self::assertFileDoesNotExist(album_cover_path('album-cover-for-thumbnail-test.jpg'));
|
||||
self::assertFileDoesNotExist(album_cover_path('album-cover-for-thumbnail-test_thumb.jpg'));
|
||||
|
|
|
@ -3,29 +3,31 @@
|
|||
namespace Tests\Integration\Services;
|
||||
|
||||
use App\Events\LibraryChanged;
|
||||
use App\Events\MediaSyncCompleted;
|
||||
use App\Events\MediaScanCompleted;
|
||||
use App\Libraries\WatchRecord\InotifyWatchRecord;
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Song;
|
||||
use App\Services\FileSynchronizer;
|
||||
use App\Services\MediaSyncService;
|
||||
use App\Models\User;
|
||||
use App\Services\FileScanner;
|
||||
use App\Services\MediaScanner;
|
||||
use App\Values\ScanConfiguration;
|
||||
use getID3;
|
||||
use Illuminate\Support\Arr;
|
||||
use Mockery;
|
||||
use Tests\Feature\TestCase;
|
||||
|
||||
class MediaSyncServiceTest extends TestCase
|
||||
class MediaScannerTest extends TestCase
|
||||
{
|
||||
private MediaSyncService $mediaService;
|
||||
private MediaScanner $scanner;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Setting::set('media_path', realpath($this->mediaPath));
|
||||
$this->mediaService = app(MediaSyncService::class);
|
||||
$this->scanner = app(MediaScanner::class);
|
||||
}
|
||||
|
||||
private function path($subPath): string
|
||||
|
@ -33,20 +35,27 @@ class MediaSyncServiceTest extends TestCase
|
|||
return realpath($this->mediaPath . $subPath);
|
||||
}
|
||||
|
||||
public function testSync(): void
|
||||
public function testScan(): void
|
||||
{
|
||||
$this->expectsEvents(MediaSyncCompleted::class);
|
||||
/** @var User $owner */
|
||||
$owner = User::factory()->admin()->create();
|
||||
|
||||
$this->mediaService->sync();
|
||||
$this->expectsEvents(MediaScanCompleted::class);
|
||||
|
||||
$this->scanner->scan(ScanConfiguration::make(owner: $owner));
|
||||
|
||||
// Standard mp3 files under root path should be recognized
|
||||
self::assertDatabaseHas(Song::class, [
|
||||
'path' => $this->path('/full.mp3'),
|
||||
'track' => 5,
|
||||
'owner_id' => $owner->id,
|
||||
]);
|
||||
|
||||
// Ogg files and audio files in subdirectories should be recognized
|
||||
self::assertDatabaseHas(Song::class, ['path' => $this->path('/subdir/back-in-black.ogg')]);
|
||||
self::assertDatabaseHas(Song::class, [
|
||||
'path' => $this->path('/subdir/back-in-black.ogg'),
|
||||
'owner_id' => $owner->id,
|
||||
]);
|
||||
|
||||
// GitHub issue #380. folder.png should be copied and used as the cover for files
|
||||
// under subdir/
|
||||
|
@ -83,26 +92,30 @@ class MediaSyncServiceTest extends TestCase
|
|||
self::assertSame('Cuckoo', $song->artist->name);
|
||||
}
|
||||
|
||||
public function testModifiedFileIsResynced(): void
|
||||
public function testModifiedFileIsRescanned(): void
|
||||
{
|
||||
$this->expectsEvents(MediaSyncCompleted::class);
|
||||
$config = ScanConfiguration::make(owner: User::factory()->admin()->create());
|
||||
|
||||
$this->mediaService->sync();
|
||||
$this->expectsEvents(MediaScanCompleted::class);
|
||||
|
||||
$this->scanner->scan($config);
|
||||
|
||||
/** @var Song $song */
|
||||
$song = Song::query()->first();
|
||||
|
||||
touch($song->path, $time = time() + 1000);
|
||||
$this->mediaService->sync();
|
||||
$this->scanner->scan($config);
|
||||
|
||||
self::assertSame($time, $song->refresh()->mtime);
|
||||
}
|
||||
|
||||
public function testResyncWithoutForceDoesNotResetData(): void
|
||||
public function testRescanWithoutForceDoesNotResetData(): void
|
||||
{
|
||||
$this->expectsEvents(MediaSyncCompleted::class);
|
||||
$config = ScanConfiguration::make(owner: User::factory()->admin()->create());
|
||||
|
||||
$this->mediaService->sync();
|
||||
$this->expectsEvents(MediaScanCompleted::class);
|
||||
|
||||
$this->scanner->scan($config);
|
||||
|
||||
/** @var Song $song */
|
||||
$song = Song::query()->first();
|
||||
|
@ -112,18 +125,21 @@ class MediaSyncServiceTest extends TestCase
|
|||
'lyrics' => 'Booom Wroooom',
|
||||
]);
|
||||
|
||||
$this->mediaService->sync();
|
||||
$this->scanner->scan($config);
|
||||
|
||||
$song->refresh();
|
||||
self::assertSame("It's John Cena!", $song->title);
|
||||
self::assertSame('Booom Wroooom', $song->lyrics);
|
||||
}
|
||||
|
||||
public function testForceSyncResetsData(): void
|
||||
public function testForceScanResetsData(): void
|
||||
{
|
||||
$this->expectsEvents(MediaSyncCompleted::class);
|
||||
/** @var User $owner */
|
||||
$owner = User::factory()->admin()->create();
|
||||
|
||||
$this->mediaService->sync();
|
||||
$this->expectsEvents(MediaScanCompleted::class);
|
||||
|
||||
$this->scanner->scan(ScanConfiguration::make(owner: $owner));
|
||||
|
||||
/** @var Song $song */
|
||||
$song = Song::query()->first();
|
||||
|
@ -133,19 +149,24 @@ class MediaSyncServiceTest extends TestCase
|
|||
'lyrics' => 'Booom Wroooom',
|
||||
]);
|
||||
|
||||
$this->mediaService->sync(force: true);
|
||||
$this->scanner->scan(ScanConfiguration::make(owner: User::factory()->admin()->create(), force: true));
|
||||
|
||||
$song->refresh();
|
||||
|
||||
self::assertNotSame("It's John Cena!", $song->title);
|
||||
self::assertNotSame('Booom Wroooom', $song->lyrics);
|
||||
// make sure the user is not changed
|
||||
self::assertSame($owner->id, $song->owner_id);
|
||||
}
|
||||
|
||||
public function testSyncWithIgnoredTags(): void
|
||||
public function testScanWithIgnoredTags(): void
|
||||
{
|
||||
$this->expectsEvents(MediaSyncCompleted::class);
|
||||
/** @var User $owner */
|
||||
$owner = User::factory()->admin()->create();
|
||||
|
||||
$this->mediaService->sync();
|
||||
$this->expectsEvents(MediaScanCompleted::class);
|
||||
|
||||
$this->scanner->scan(ScanConfiguration::make(owner: $owner));
|
||||
|
||||
/** @var Song $song */
|
||||
$song = Song::query()->first();
|
||||
|
@ -155,7 +176,7 @@ class MediaSyncServiceTest extends TestCase
|
|||
'lyrics' => 'Booom Wroooom',
|
||||
]);
|
||||
|
||||
$this->mediaService->sync(ignores: ['title'], force: true);
|
||||
$this->scanner->scan(ScanConfiguration::make(owner: $owner, ignores: ['title'], force: true));
|
||||
|
||||
$song->refresh();
|
||||
|
||||
|
@ -163,17 +184,24 @@ class MediaSyncServiceTest extends TestCase
|
|||
self::assertNotSame('Booom Wroooom', $song->lyrics);
|
||||
}
|
||||
|
||||
public function testSyncAllTagsForNewFilesRegardlessOfIgnoredOption(): void
|
||||
public function testScanAllTagsForNewFilesRegardlessOfIgnoredOption(): void
|
||||
{
|
||||
$this->expectsEvents(MediaSyncCompleted::class);
|
||||
$this->mediaService->sync();
|
||||
/** @var User $owner */
|
||||
$owner = User::factory()->admin()->create();
|
||||
|
||||
$this->expectsEvents(MediaScanCompleted::class);
|
||||
$this->scanner->scan(ScanConfiguration::make(owner: $owner));
|
||||
|
||||
/** @var Song $song */
|
||||
$song = Song::query()->first();
|
||||
|
||||
$song->delete();
|
||||
|
||||
$this->mediaService->sync(ignores: ['title', 'disc', 'track'], force: true);
|
||||
$this->scanner->scan(ScanConfiguration::make(
|
||||
owner: $owner,
|
||||
ignores: ['title', 'disc', 'track'],
|
||||
force: true
|
||||
));
|
||||
|
||||
// Song should be added back with all info
|
||||
self::assertEquals(
|
||||
|
@ -182,17 +210,21 @@ class MediaSyncServiceTest extends TestCase
|
|||
);
|
||||
}
|
||||
|
||||
public function testSyncAddedSongViaWatch(): void
|
||||
public function testScanAddedSongViaWatch(): void
|
||||
{
|
||||
$this->expectsEvents(LibraryChanged::class);
|
||||
|
||||
$path = $this->path('/blank.mp3');
|
||||
$this->mediaService->syncByWatchRecord(new InotifyWatchRecord("CLOSE_WRITE,CLOSE $path"));
|
||||
|
||||
$this->scanner->scanWatchRecord(
|
||||
new InotifyWatchRecord("CLOSE_WRITE,CLOSE $path"),
|
||||
ScanConfiguration::make(owner: User::factory()->admin()->create())
|
||||
);
|
||||
|
||||
self::assertDatabaseHas(Song::class, ['path' => $path]);
|
||||
}
|
||||
|
||||
public function testSyncDeletedSongViaWatch(): void
|
||||
public function testScanDeletedSongViaWatch(): void
|
||||
{
|
||||
$this->expectsEvents(LibraryChanged::class);
|
||||
|
||||
|
@ -201,18 +233,22 @@ class MediaSyncServiceTest extends TestCase
|
|||
/** @var Song $song */
|
||||
$song = Song::query()->first();
|
||||
|
||||
$this->mediaService->syncByWatchRecord(new InotifyWatchRecord("DELETE $song->path"));
|
||||
$this->scanner->scanWatchRecord(
|
||||
new InotifyWatchRecord("DELETE $song->path"),
|
||||
ScanConfiguration::make(owner: User::factory()->admin()->create())
|
||||
);
|
||||
|
||||
self::assertModelMissing($song);
|
||||
}
|
||||
|
||||
public function testSyncDeletedDirectoryViaWatch(): void
|
||||
public function testScanDeletedDirectoryViaWatch(): void
|
||||
{
|
||||
$this->expectsEvents(LibraryChanged::class, MediaSyncCompleted::class);
|
||||
$config = ScanConfiguration::make(owner: User::factory()->admin()->create());
|
||||
|
||||
$this->mediaService->sync();
|
||||
$this->expectsEvents(LibraryChanged::class, MediaScanCompleted::class);
|
||||
|
||||
$this->mediaService->syncByWatchRecord(new InotifyWatchRecord("MOVED_FROM,ISDIR $this->mediaPath/subdir"));
|
||||
$this->scanner->scan($config);
|
||||
$this->scanner->scanWatchRecord(new InotifyWatchRecord("MOVED_FROM,ISDIR $this->mediaPath/subdir"), $config);
|
||||
|
||||
self::assertDatabaseMissing(Song::class, ['path' => $this->path('/subdir/sic.mp3')]);
|
||||
self::assertDatabaseMissing(Song::class, ['path' => $this->path('/subdir/no-name.mp3')]);
|
||||
|
@ -243,9 +279,9 @@ class MediaSyncServiceTest extends TestCase
|
|||
])
|
||||
);
|
||||
|
||||
/** @var FileSynchronizer $fileSynchronizer */
|
||||
$fileSynchronizer = app(FileSynchronizer::class);
|
||||
$info = $fileSynchronizer->setFile($path)->getFileScanInformation();
|
||||
/** @var FileScanner $fileScanner */
|
||||
$fileScanner = app(FileScanner::class);
|
||||
$info = $fileScanner->setFile($path)->getFileScanInformation();
|
||||
|
||||
self::assertSame('佐倉綾音 Unknown', $info->artistName);
|
||||
self::assertSame('小岩井こ Random', $info->albumName);
|
||||
|
@ -254,12 +290,14 @@ class MediaSyncServiceTest extends TestCase
|
|||
|
||||
public function testOptionallyIgnoreHiddenFiles(): void
|
||||
{
|
||||
$config = ScanConfiguration::make(owner: User::factory()->admin()->create());
|
||||
|
||||
config(['koel.ignore_dot_files' => false]);
|
||||
$this->mediaService->sync();
|
||||
$this->scanner->scan($config);
|
||||
self::assertDatabaseHas(Album::class, ['name' => 'Hidden Album']);
|
||||
|
||||
config(['koel.ignore_dot_files' => true]);
|
||||
$this->mediaService->sync();
|
||||
$this->scanner->scan($config);
|
||||
self::assertDatabaseMissing(Album::class, ['name' => 'Hidden Album']);
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
|
|||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Testing\TestResponse;
|
||||
use ReflectionClass;
|
||||
use Tests\Traits\CreatesApplication;
|
||||
|
@ -31,13 +32,13 @@ abstract class TestCase extends BaseTestCase
|
|||
|
||||
TestResponse::macro('log', function (string $file = 'test-response.json'): TestResponse {
|
||||
/** @var TestResponse $this */
|
||||
file_put_contents(storage_path('logs/' . $file), $this->getContent());
|
||||
File::put(storage_path('logs/' . $file), $this->getContent());
|
||||
|
||||
return $this;
|
||||
});
|
||||
|
||||
UploadedFile::macro('fromFile', static function (string $path, ?string $name = null): UploadedFile {
|
||||
return UploadedFile::fake()->createWithContent($name ?? basename($path), file_get_contents($path));
|
||||
return UploadedFile::fake()->createWithContent($name ?? basename($path), File::get($path));
|
||||
});
|
||||
|
||||
self::createSandbox();
|
||||
|
|
|
@ -11,9 +11,9 @@ trait SandboxesTests
|
|||
config(['koel.album_cover_dir' => 'sandbox/img/covers/']);
|
||||
config(['koel.artist_image_dir' => 'sandbox/img/artists/']);
|
||||
|
||||
@mkdir(public_path(config('koel.album_cover_dir')), 0755, true);
|
||||
@mkdir(public_path(config('koel.artist_image_dir')), 0755, true);
|
||||
@mkdir(public_path('sandbox/media/'), 0755, true);
|
||||
File::ensureDirectoryExists(public_path(config('koel.album_cover_dir')));
|
||||
File::ensureDirectoryExists(public_path(config('koel.artist_image_dir')));
|
||||
File::ensureDirectoryExists(public_path('sandbox/media/'));
|
||||
}
|
||||
|
||||
private static function destroySandbox(): void
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
|
||||
namespace Tests\Unit\Listeners;
|
||||
|
||||
use App\Events\MediaSyncCompleted;
|
||||
use App\Events\MediaScanCompleted;
|
||||
use App\Listeners\WriteSyncLog;
|
||||
use App\Values\SyncResult;
|
||||
use App\Values\SyncResultCollection;
|
||||
use App\Values\ScanResult;
|
||||
use App\Values\ScanResultCollection;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Tests\TestCase;
|
||||
|
||||
class WriteSyncLogTest extends TestCase
|
||||
|
@ -25,7 +26,7 @@ class WriteSyncLogTest extends TestCase
|
|||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
@unlink(storage_path('logs/sync-20210102-123456.log'));
|
||||
File::delete(storage_path('logs/sync-20210102-123456.log'));
|
||||
config(['koel.sync_log_level' => $this->originalLogLevel]);
|
||||
|
||||
parent::tearDown();
|
||||
|
@ -55,14 +56,14 @@ class WriteSyncLogTest extends TestCase
|
|||
);
|
||||
}
|
||||
|
||||
private static function createSyncCompleteEvent(): MediaSyncCompleted
|
||||
private static function createSyncCompleteEvent(): MediaScanCompleted
|
||||
{
|
||||
$resultCollection = SyncResultCollection::create()
|
||||
->add(SyncResult::success('/media/foo.mp3'))
|
||||
->add(SyncResult::error('/media/baz.mp3', 'Something went wrong'))
|
||||
->add(SyncResult::error('/media/qux.mp3', 'Something went horribly wrong'))
|
||||
->add(SyncResult::skipped('/media/bar.mp3'));
|
||||
$resultCollection = ScanResultCollection::create()
|
||||
->add(ScanResult::success('/media/foo.mp3'))
|
||||
->add(ScanResult::error('/media/baz.mp3', 'Something went wrong'))
|
||||
->add(ScanResult::error('/media/qux.mp3', 'Something went horribly wrong'))
|
||||
->add(ScanResult::skipped('/media/bar.mp3'));
|
||||
|
||||
return new MediaSyncCompleted($resultCollection);
|
||||
return new MediaScanCompleted($resultCollection);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace Tests\Unit\Models;
|
||||
|
||||
use App\Models\Artist;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ArtistTest extends TestCase
|
||||
|
@ -40,7 +41,7 @@ class ArtistTest extends TestCase
|
|||
|
||||
public function testArtistsWithNameInUtf16EncodingAreRetrievedCorrectly(): void
|
||||
{
|
||||
$name = file_get_contents(__DIR__ . '../../../blobs/utf16');
|
||||
$name = File::get(__DIR__ . '../../../blobs/utf16');
|
||||
$artist = Artist::getOrCreate($name);
|
||||
|
||||
self::assertTrue(Artist::getOrCreate($name)->is($artist));
|
||||
|
|
|
@ -7,6 +7,7 @@ use GuzzleHttp\Client as GuzzleHttpClient;
|
|||
use GuzzleHttp\Handler\MockHandler;
|
||||
use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Tests\TestCase;
|
||||
|
||||
class LastfmClientTest extends TestCase
|
||||
|
@ -14,7 +15,7 @@ class LastfmClientTest extends TestCase
|
|||
public function testGetSessionKey(): void
|
||||
{
|
||||
$mock = new MockHandler([
|
||||
new Response(200, [], file_get_contents(__DIR__ . '/../../../blobs/lastfm/session-key.json')),
|
||||
new Response(200, [], File::get(__DIR__ . '/../../../blobs/lastfm/session-key.json')),
|
||||
]);
|
||||
|
||||
$client = new LastfmClient(new GuzzleHttpClient(['handler' => HandlerStack::create($mock)]));
|
||||
|
|
|
@ -8,6 +8,7 @@ use App\Models\Song;
|
|||
use App\Models\User;
|
||||
use App\Services\ApiClients\LastfmClient;
|
||||
use App\Services\LastfmService;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Mockery;
|
||||
use Mockery\LegacyMockInterface;
|
||||
use Mockery\MockInterface;
|
||||
|
@ -39,7 +40,7 @@ class LastfmServiceTest extends TestCase
|
|||
$this->client->shouldReceive('get')
|
||||
->with('?method=artist.getInfo&autocorrect=1&artist=foo&format=json')
|
||||
->once()
|
||||
->andReturn(json_decode(file_get_contents(__DIR__ . '/../../blobs/lastfm/artist.json')));
|
||||
->andReturn(json_decode(File::get(__DIR__ . '/../../blobs/lastfm/artist.json')));
|
||||
|
||||
$info = $this->service->getArtistInformation($artist);
|
||||
|
||||
|
@ -61,7 +62,7 @@ class LastfmServiceTest extends TestCase
|
|||
$this->client->shouldReceive('get')
|
||||
->with('?method=artist.getInfo&autocorrect=1&artist=bar&format=json')
|
||||
->once()
|
||||
->andReturn(json_decode(file_get_contents(__DIR__ . '/../../blobs/lastfm/artist-notfound.json')));
|
||||
->andReturn(json_decode(File::get(__DIR__ . '/../../blobs/lastfm/artist-notfound.json')));
|
||||
|
||||
self::assertNull($this->service->getArtistInformation($artist));
|
||||
}
|
||||
|
@ -74,7 +75,7 @@ class LastfmServiceTest extends TestCase
|
|||
$this->client->shouldReceive('get')
|
||||
->with('?method=album.getInfo&autocorrect=1&album=foo&artist=bar&format=json')
|
||||
->once()
|
||||
->andReturn(json_decode(file_get_contents(__DIR__ . '/../../blobs/lastfm/album.json')));
|
||||
->andReturn(json_decode(File::get(__DIR__ . '/../../blobs/lastfm/album.json')));
|
||||
|
||||
$info = $this->service->getAlbumInformation($album);
|
||||
|
||||
|
@ -108,7 +109,7 @@ class LastfmServiceTest extends TestCase
|
|||
$this->client->shouldReceive('get')
|
||||
->with('?method=album.getInfo&autocorrect=1&album=foo&artist=bar&format=json')
|
||||
->once()
|
||||
->andReturn(json_decode(file_get_contents(__DIR__ . '/../../blobs/lastfm/album-notfound.json')));
|
||||
->andReturn(json_decode(File::get(__DIR__ . '/../../blobs/lastfm/album-notfound.json')));
|
||||
|
||||
self::assertNull($this->service->getAlbumInformation($album));
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace Tests\Unit\Services;
|
||||
|
||||
use App\Services\SimpleLrcReader;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
@ -22,9 +23,9 @@ class SimpleLrcReaderTest extends TestCase
|
|||
$base = sys_get_temp_dir() . '/' . Str::uuid();
|
||||
$lrcFile = $base . '.lrc';
|
||||
|
||||
copy(__DIR__ . '/../../blobs/simple.lrc', $lrcFile);
|
||||
File::copy(__DIR__ . '/../../blobs/simple.lrc', $lrcFile);
|
||||
|
||||
self::assertSame("Line 1\nLine 2\nLine 3", $this->reader->tryReadForMediaFile($base . '.mp3'));
|
||||
@unlink($lrcFile);
|
||||
File::delete($lrcFile);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ use App\Models\Album;
|
|||
use App\Models\Artist;
|
||||
use App\Services\ApiClients\SpotifyClient;
|
||||
use App\Services\SpotifyService;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Mockery;
|
||||
use Mockery\LegacyMockInterface;
|
||||
use Mockery\MockInterface;
|
||||
|
@ -76,7 +77,7 @@ class SpotifyServiceTest extends TestCase
|
|||
/** @return array<mixed> */
|
||||
private static function parseFixture(string $name): array
|
||||
{
|
||||
return json_decode(file_get_contents(__DIR__ . '/../../blobs/spotify/' . $name), true);
|
||||
return json_decode(File::get(__DIR__ . '/../../blobs/spotify/' . $name), true);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
|
|
|
@ -7,6 +7,7 @@ use App\Models\Song;
|
|||
use App\Services\ApiClients\YouTubeClient;
|
||||
use App\Services\YouTubeService;
|
||||
use Illuminate\Cache\Repository;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
@ -20,7 +21,7 @@ class YouTubeServiceTest extends TestCase
|
|||
|
||||
$client->shouldReceive('get')
|
||||
->with('search?part=snippet&type=video&maxResults=10&pageToken=my-token&q=Foo+Bar')
|
||||
->andReturn(json_decode(file_get_contents(__DIR__ . '/../../blobs/youtube/search.json')));
|
||||
->andReturn(json_decode(File::get(__DIR__ . '/../../blobs/youtube/search.json')));
|
||||
|
||||
$service = new YouTubeService($client, app(Repository::class));
|
||||
$response = $service->searchVideosRelatedToSong($song, 'my-token');
|
||||
|
|
Loading…
Reference in a new issue