koel/app/Services/MediaScanner.php

191 lines
5.7 KiB
PHP
Raw Normal View History

2015-12-13 04:42:28 +00:00
<?php
namespace App\Services;
2016-02-04 15:48:15 +00:00
use App\Events\LibraryChanged;
use App\Events\MediaScanCompleted;
2015-12-13 04:42:28 +00:00
use App\Models\Song;
use App\Repositories\SettingRepository;
2018-08-29 06:15:11 +00:00
use App\Repositories\SongRepository;
use App\Values\ScanConfiguration;
use App\Values\ScanResult;
use App\Values\ScanResultCollection;
use App\Values\WatchRecord\Contracts\WatchRecordInterface;
use Psr\Log\LoggerInterface;
2018-08-24 15:27:19 +00:00
use SplFileInfo;
2016-02-04 15:48:15 +00:00
use Symfony\Component\Finder\Finder;
use Throwable;
2015-12-13 04:42:28 +00:00
class MediaScanner
2015-12-13 04:42:28 +00:00
{
2022-07-29 10:51:20 +00:00
/** @var array<array-key, callable> */
private array $events = [];
2018-08-29 06:15:11 +00:00
public function __construct(
2024-04-18 14:36:28 +00:00
private readonly SettingRepository $settingRepository,
private readonly SongRepository $songRepository,
private readonly FileScanner $fileScanner,
private readonly Finder $finder,
private readonly LoggerInterface $logger
2018-08-29 07:07:44 +00:00
) {
2018-08-19 09:05:33 +00:00
}
public function scan(ScanConfiguration $config): ScanResultCollection
{
/** @var string $mediaPath */
$mediaPath = $this->settingRepository->getByKey('media_path');
$this->setSystemRequirements();
2015-12-13 04:42:28 +00:00
$results = ScanResultCollection::create();
$songPaths = $this->gatherFiles($mediaPath);
2022-07-29 10:51:20 +00:00
if (isset($this->events['paths-gathered'])) {
$this->events['paths-gathered']($songPaths);
}
2017-06-03 23:21:50 +00:00
foreach ($songPaths as $path) {
try {
$result = $this->fileScanner->setFile($path)->scan($config);
} catch (Throwable) {
$result = ScanResult::error($path, 'Possible invalid file');
}
2022-07-29 10:51:20 +00:00
$results->add($result);
2017-06-03 23:21:50 +00:00
2022-07-29 10:51:20 +00:00
if (isset($this->events['progress'])) {
$this->events['progress']($result);
2015-12-13 04:42:28 +00:00
}
}
event(new MediaScanCompleted($results));
2015-12-13 04:42:28 +00:00
2021-12-10 15:27:06 +00:00
// Trigger LibraryChanged, so that PruneLibrary handler is fired to prune the lib.
2016-02-02 07:47:00 +00:00
event(new LibraryChanged());
2022-07-29 10:51:20 +00:00
return $results;
2016-02-02 07:47:00 +00:00
}
2015-12-13 04:42:28 +00:00
2016-02-02 07:47:00 +00:00
/**
* Gather all applicable files in a given directory.
*
* @param string $path The directory's full path
*
2020-12-22 20:11:22 +00:00
* @return array<SplFileInfo>
2016-02-02 07:47:00 +00:00
*/
private function gatherFiles(string $path): array
2016-02-02 07:47:00 +00:00
{
2017-04-23 16:01:02 +00:00
return iterator_to_array(
$this->finder->create()
2017-04-23 16:01:02 +00:00
->ignoreUnreadableDirs()
2024-04-24 20:44:47 +00:00
->ignoreDotFiles((bool) config('koel.ignore_dot_files')) // https://github.com/koel/koel/issues/450
2017-04-23 16:01:02 +00:00
->files()
->followLinks()
->name('/\.(mp3|wav|ogg|m4a|flac|opus)$/i')
2017-04-23 16:01:02 +00:00
->in($path)
);
2015-12-13 04:42:28 +00:00
}
public function scanWatchRecord(WatchRecordInterface $record, ScanConfiguration $config): void
2016-02-02 07:47:00 +00:00
{
$this->logger->info("New watch record received: '{$record->getPath()}'");
if ($record->isFile()) {
$this->scanFileRecord($record, $config);
} else {
$this->scanDirectoryRecord($record, $config);
}
2017-06-03 23:21:50 +00:00
}
2016-02-02 07:47:00 +00:00
private function scanFileRecord(WatchRecordInterface $record, ScanConfiguration $config): void
2017-06-03 23:21:50 +00:00
{
$path = $record->getPath();
2018-08-31 13:47:15 +00:00
$this->logger->info("'$path' is a file.");
2017-01-06 03:04:08 +00:00
2017-06-03 23:21:50 +00:00
if ($record->isDeleted()) {
$this->handleDeletedFileRecord($path);
2020-12-22 20:11:22 +00:00
} elseif ($record->isNewOrModified()) {
$this->handleNewOrModifiedFileRecord($path, $config);
2016-02-02 07:47:00 +00:00
}
2017-06-03 23:21:50 +00:00
}
2016-02-02 07:47:00 +00:00
private function scanDirectoryRecord(WatchRecordInterface $record, ScanConfiguration $config): void
2017-06-03 23:21:50 +00:00
{
$path = $record->getPath();
2018-08-31 13:47:15 +00:00
$this->logger->info("'$path' is a directory.");
2016-02-02 07:47:00 +00:00
2016-02-04 15:04:53 +00:00
if ($record->isDeleted()) {
$this->handleDeletedDirectoryRecord($path);
2016-02-04 15:04:53 +00:00
} elseif ($record->isNewOrModified()) {
$this->handleNewOrModifiedDirectoryRecord($path, $config);
2016-02-02 07:47:00 +00:00
}
}
private function setSystemRequirements(): void
{
if (!app()->runningInConsole()) {
set_time_limit(config('koel.sync.timeout'));
}
if (config('koel.memory_limit')) {
2020-12-22 20:11:22 +00:00
ini_set('memory_limit', config('koel.memory_limit') . 'M');
}
}
private function handleDeletedFileRecord(string $path): void
{
$song = $this->songRepository->findOneByPath($path);
2020-12-22 20:11:22 +00:00
if ($song) {
$song->delete();
2018-08-31 13:47:15 +00:00
$this->logger->info("$path deleted.");
} else {
2018-08-31 13:47:15 +00:00
$this->logger->info("$path doesn't exist in our database--skipping.");
}
}
private function handleNewOrModifiedFileRecord(string $path, ScanConfiguration $config): void
{
$result = $this->fileScanner->setFile($path)->scan($config);
2022-07-29 10:51:20 +00:00
if ($result->isSuccess()) {
$this->logger->info("Scanned $path");
} else {
$this->logger->info("Failed to scan $path. Maybe an invalid file?");
}
}
private function handleDeletedDirectoryRecord(string $path): void
{
$count = Song::query()->inDirectory($path)->delete();
2020-12-22 20:11:22 +00:00
if ($count) {
2018-08-31 13:47:15 +00:00
$this->logger->info("Deleted $count song(s) under $path");
} else {
2018-08-31 13:47:15 +00:00
$this->logger->info("$path is empty--no action needed.");
}
}
private function handleNewOrModifiedDirectoryRecord(string $path, ScanConfiguration $config): void
{
$scanResults = ScanResultCollection::create();
foreach ($this->gatherFiles($path) as $file) {
try {
$scanResults->add($this->fileScanner->setFile($file)->scan($config));
} catch (Throwable) {
$scanResults->add(ScanResult::error($file->getRealPath(), 'Possible invalid file'));
}
}
$this->logger->info("Scanned all song(s) under $path");
event(new MediaScanCompleted($scanResults));
}
2022-07-29 10:51:20 +00:00
public function on(string $event, callable $callback): void
{
$this->events[$event] = $callback;
}
2015-12-13 04:42:28 +00:00
}