koel/app/Services/MediaSyncService.php

300 lines
9 KiB
PHP
Raw Normal View History

2015-12-13 12:42:28 +08:00
<?php
namespace App\Services;
2018-08-19 17:26:34 +02:00
use App\Console\Commands\SyncMediaCommand;
2016-02-04 10:48:15 -05:00
use App\Events\LibraryChanged;
2016-02-04 23:04:53 +08:00
use App\Libraries\WatchRecord\WatchRecordInterface;
2016-04-17 23:38:06 +08:00
use App\Models\Album;
use App\Models\Artist;
2015-12-13 12:42:28 +08:00
use App\Models\Setting;
use App\Models\Song;
use App\Repositories\AlbumRepository;
use App\Repositories\ArtistRepository;
use App\Repositories\SettingRepository;
2018-08-29 13:15:11 +07:00
use App\Repositories\SongRepository;
use Exception;
2018-08-19 11:05:33 +02:00
use Log;
2018-08-24 17:27:19 +02:00
use SplFileInfo;
2016-02-04 10:48:15 -05:00
use Symfony\Component\Finder\Finder;
2015-12-13 12:42:28 +08:00
2018-08-19 17:26:34 +02:00
class MediaSyncService
2015-12-13 12:42:28 +08:00
{
/**
2016-03-22 16:22:39 +08:00
* All applicable tags in a media file that we cater for.
* Note that each isn't necessarily a valid ID3 tag name.
*
* @var array
*/
2018-08-24 17:27:19 +02:00
private const APPLICABLE_TAGS = [
2016-04-17 23:38:06 +08:00
'artist',
'album',
'title',
'length',
'track',
'disc',
2016-04-17 23:38:06 +08:00
'lyrics',
'cover',
'mtime',
'compilation',
2016-04-17 23:38:06 +08:00
];
2016-03-22 16:22:39 +08:00
2018-08-19 11:05:33 +02:00
private $mediaMetadataService;
2018-08-29 13:15:11 +07:00
private $songRepository;
private $helperService;
private $fileSynchronizer;
private $finder;
private $artistRepository;
private $albumRepository;
private $settingRepository;
2018-08-29 13:15:11 +07:00
public function __construct(
MediaMetadataService $mediaMetadataService,
SongRepository $songRepository,
ArtistRepository $artistRepository,
AlbumRepository $albumRepository,
SettingRepository $settingRepository,
HelperService $helperService,
FileSynchronizer $fileSynchronizer,
Finder $finder
2018-08-29 09:07:44 +02:00
) {
2018-08-19 11:05:33 +02:00
$this->mediaMetadataService = $mediaMetadataService;
2018-08-29 13:15:11 +07:00
$this->songRepository = $songRepository;
$this->helperService = $helperService;
$this->fileSynchronizer = $fileSynchronizer;
$this->finder = $finder;
$this->artistRepository = $artistRepository;
$this->albumRepository = $albumRepository;
$this->settingRepository = $settingRepository;
2018-08-19 11:05:33 +02:00
}
2016-03-22 16:22:39 +08:00
/**
* Tags to be synced.
*
* @var array
2015-12-13 12:42:28 +08:00
*/
2016-03-22 16:22:39 +08:00
protected $tags = [];
2015-12-13 12:42:28 +08:00
/**
* Sync the media. Oh sync the media.
*
2018-08-24 17:27:19 +02:00
* @param string[] $tags The tags to sync.
2018-08-19 17:26:52 +02:00
* Only taken into account for existing records.
* New records will have all tags synced in regardless.
2018-08-19 17:26:34 +02:00
* @param bool $force Whether to force syncing even unchanged files
* @param SyncMediaCommand $syncCommand The SyncMedia command object, to log to console if executed by artisan.
*
* @throws Exception
2015-12-13 12:42:28 +08:00
*/
2018-08-24 17:27:19 +02:00
public function sync(
?string $mediaPath = null,
array $tags = [],
bool $force = false,
?SyncMediaCommand $syncCommand = null
2018-08-24 17:27:19 +02:00
): void {
$this->setSystemRequirements();
2016-03-22 16:22:39 +08:00
$this->setTags($tags);
2015-12-13 12:42:28 +08:00
$results = [
2017-06-04 00:21:50 +01:00
'success' => [],
'bad_files' => [],
'unmodified' => [],
2015-12-13 12:42:28 +08:00
];
$songPaths = $this->gatherFiles($mediaPath ?: $this->settingRepository->getMediaPath());
2017-06-04 00:21:50 +01:00
$syncCommand && $syncCommand->createProgressBar(count($songPaths));
foreach ($songPaths as $path) {
$result = $this->fileSynchronizer->setFile($path)->sync($this->tags, $force);
2017-06-04 00:21:50 +01:00
switch ($result) {
case FileSynchronizer::SYNC_RESULT_SUCCESS:
$results['success'][] = $path;
2017-06-04 00:21:50 +01:00
break;
case FileSynchronizer::SYNC_RESULT_UNMODIFIED:
$results['unmodified'][] = $path;
2017-06-04 00:21:50 +01:00
break;
default:
$results['bad_files'][] = $path;
2017-06-04 00:21:50 +01:00
break;
2015-12-13 12:42:28 +08:00
}
if ($syncCommand) {
2018-08-24 17:27:19 +02:00
$syncCommand->advanceProgressBar();
$syncCommand->logSyncStatusToConsole($path, $result, $this->fileSynchronizer->getSyncError());
2015-12-13 12:42:28 +08:00
}
}
// Delete non-existing songs.
$hashes = array_map(function (string $path): string {
return $this->helperService->getFileHash($path);
2017-06-04 00:21:50 +01:00
}, array_merge($results['unmodified'], $results['success']));
2015-12-13 12:42:28 +08:00
2016-09-26 15:32:16 +08:00
Song::deleteWhereIDsNotIn($hashes);
2015-12-13 12:42:28 +08:00
2016-02-02 15:47:00 +08:00
// Trigger LibraryChanged, so that TidyLibrary handler is fired to, erm, tidy our library.
event(new LibraryChanged());
}
2015-12-13 12:42:28 +08:00
2016-02-02 15:47:00 +08:00
/**
* Gather all applicable files in a given directory.
*
* @param string $path The directory's full path
*
2018-08-24 17:27:19 +02:00
* @return SplFileInfo[]
2016-02-02 15:47:00 +08:00
*/
2018-08-24 17:27:19 +02:00
public function gatherFiles(string $path): array
2016-02-02 15:47:00 +08:00
{
2017-04-24 00:01:02 +08:00
return iterator_to_array(
$this->finder->create()
2017-04-24 00:01:02 +08:00
->ignoreUnreadableDirs()
->ignoreDotFiles((bool) config('koel.ignore_dot_files')) // https://github.com/phanan/koel/issues/450
->files()
->followLinks()
->name('/\.(mp3|ogg|m4a|flac)$/i')
->in($path)
);
2015-12-13 12:42:28 +08:00
}
2016-02-02 15:47:00 +08:00
/**
2016-02-04 23:04:53 +08:00
* Sync media using a watch record.
2016-02-02 15:47:00 +08:00
*
* @throws Exception
2016-02-02 15:47:00 +08:00
*/
2018-08-24 17:27:19 +02:00
public function syncByWatchRecord(WatchRecordInterface $record): void
2016-02-02 15:47:00 +08:00
{
2016-02-04 23:47:02 +08:00
Log::info("New watch record received: '$record'");
2017-06-04 00:21:50 +01:00
$record->isFile() ? $this->syncFileRecord($record) : $this->syncDirectoryRecord($record);
}
2016-02-02 15:47:00 +08:00
2017-06-04 00:21:50 +01:00
/**
* Sync a file's watch record.
*
* @throws Exception
2017-06-04 00:21:50 +01:00
*/
2018-08-24 17:27:19 +02:00
private function syncFileRecord(WatchRecordInterface $record): void
2017-06-04 00:21:50 +01:00
{
$path = $record->getPath();
Log::info("'$path' is a file.");
2017-01-06 11:04:08 +08:00
2017-06-04 00:21:50 +01:00
// If the file has been deleted...
if ($record->isDeleted()) {
$this->handleDeletedFileRecord($path);
2017-06-04 00:21:50 +01:00
}
// Otherwise, it's a new or changed file. Try to sync it in.
elseif ($record->isNewOrModified()) {
$this->handleNewOrModifiedFileRecord($path);
2016-02-02 15:47:00 +08:00
}
2017-06-04 00:21:50 +01:00
}
2016-02-02 15:47:00 +08:00
2017-06-04 00:21:50 +01:00
/**
* Sync a directory's watch record.
*/
2018-08-24 17:27:19 +02:00
private function syncDirectoryRecord(WatchRecordInterface $record): void
2017-06-04 00:21:50 +01:00
{
$path = $record->getPath();
2016-02-04 23:47:02 +08:00
Log::info("'$path' is a directory.");
2016-02-02 15:47:00 +08:00
2016-02-04 23:04:53 +08:00
if ($record->isDeleted()) {
$this->handleDeletedDirectoryRecord($path);
2016-02-04 23:04:53 +08:00
} elseif ($record->isNewOrModified()) {
$this->handleNewOrModifiedDirectoryRecord($path);
2016-02-02 15:47:00 +08:00
}
}
2015-12-13 12:42:28 +08:00
/**
2016-03-22 16:22:39 +08:00
* Construct an array of tags to be synced into the database from an input array of tags.
* If the input array is empty or contains only invalid items, we use all tags.
2016-08-02 03:03:19 -04:00
* Otherwise, we only use the valid items in it.
2015-12-13 12:42:28 +08:00
*
2018-08-24 17:27:19 +02:00
* @param string[] $tags
2015-12-13 12:42:28 +08:00
*/
2018-08-24 17:27:19 +02:00
public function setTags(array $tags = []): void
2015-12-13 12:42:28 +08:00
{
2018-08-24 17:27:19 +02:00
$this->tags = array_intersect((array) $tags, self::APPLICABLE_TAGS) ?: self::APPLICABLE_TAGS;
2015-12-13 12:42:28 +08:00
2016-03-22 16:22:39 +08:00
// We always keep track of mtime.
2016-08-03 18:42:11 +08:00
if (!in_array('mtime', $this->tags, true)) {
2016-03-22 16:22:39 +08:00
$this->tags[] = 'mtime';
2015-12-13 12:42:28 +08:00
}
}
2016-04-17 23:38:06 +08:00
/**
* Tidy up the library by deleting empty albums and artists.
2017-12-09 21:10:55 +01:00
*
* @throws Exception
2016-04-17 23:38:06 +08:00
*/
2018-08-24 17:27:19 +02:00
public function tidy(): void
2016-04-17 23:38:06 +08:00
{
$inUseAlbums = $this->albumRepository->getNonEmptyAlbumIds();
2016-04-17 23:38:06 +08:00
$inUseAlbums[] = Album::UNKNOWN_ID;
2016-09-26 15:32:16 +08:00
Album::deleteWhereIDsNotIn($inUseAlbums);
2016-04-17 23:38:06 +08:00
$inUseArtists = $this->artistRepository->getNonEmptyArtistIds();
2016-04-17 23:38:06 +08:00
$inUseArtists[] = Artist::UNKNOWN_ID;
$inUseArtists[] = Artist::VARIOUS_ID;
2016-09-26 15:32:16 +08:00
Artist::deleteWhereIDsNotIn(array_filter($inUseArtists));
2016-04-17 23:38:06 +08:00
}
private function setSystemRequirements(): void
{
if (!app()->runningInConsole()) {
set_time_limit(config('koel.sync.timeout'));
}
if (config('koel.memory_limit')) {
ini_set('memory_limit', config('koel.memory_limit') . 'M');
}
}
/**
* @throws Exception
*/
private function handleDeletedFileRecord(string $path): void
{
if ($song = $this->songRepository->getOneByPath($path)) {
$song->delete();
Log::info("$path deleted.");
event(new LibraryChanged());
} else {
Log::info("$path doesn't exist in our database--skipping.");
}
}
private function handleNewOrModifiedFileRecord(string $path): void
{
$result = $this->fileSynchronizer->setFile($path)->sync($this->tags);
if ($result === FileSynchronizer::SYNC_RESULT_SUCCESS) {
Log::info("Synchronized $path");
} else {
Log::info("Failed to synchronized $path. Maybe an invalid file?");
}
event(new LibraryChanged());
}
private function handleDeletedDirectoryRecord(string $path): void
{
if ($count = Song::inDirectory($path)->delete()) {
Log::info("Deleted $count song(s) under $path");
event(new LibraryChanged());
} else {
Log::info("$path is empty--no action needed.");
}
}
private function handleNewOrModifiedDirectoryRecord(string $path): void
{
foreach ($this->gatherFiles($path) as $file) {
$this->fileSynchronizer->setFile($file)->sync($this->tags);
}
Log::info("Synced all song(s) under $path");
event(new LibraryChanged());
}
2015-12-13 12:42:28 +08:00
}