2015-12-13 04:42:28 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
2018-08-19 15:26:34 +00:00
|
|
|
use App\Console\Commands\SyncMediaCommand;
|
2016-02-04 15:48:15 +00:00
|
|
|
use App\Events\LibraryChanged;
|
2016-02-04 15:04:53 +00:00
|
|
|
use App\Libraries\WatchRecord\WatchRecordInterface;
|
2016-04-17 15:38:06 +00:00
|
|
|
use App\Models\Album;
|
|
|
|
use App\Models\Artist;
|
2016-03-22 08:22:39 +00:00
|
|
|
use App\Models\File;
|
2015-12-13 04:42:28 +00:00
|
|
|
use App\Models\Setting;
|
|
|
|
use App\Models\Song;
|
2017-12-09 18:34:27 +00:00
|
|
|
use Exception;
|
2015-12-13 04:42:28 +00:00
|
|
|
use getID3;
|
2018-08-19 09:05:33 +00:00
|
|
|
use getid3_exception;
|
|
|
|
use Log;
|
2016-02-04 15:48:15 +00:00
|
|
|
use Symfony\Component\Finder\Finder;
|
2015-12-13 04:42:28 +00:00
|
|
|
|
2018-08-19 15:26:34 +00:00
|
|
|
class MediaSyncService
|
2015-12-13 04:42:28 +00:00
|
|
|
{
|
|
|
|
/**
|
2016-03-22 08:22:39 +00: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
|
|
|
|
*/
|
2016-04-17 15:38:06 +00:00
|
|
|
protected $allTags = [
|
|
|
|
'artist',
|
|
|
|
'album',
|
|
|
|
'title',
|
|
|
|
'length',
|
|
|
|
'track',
|
2017-12-03 10:02:31 +00:00
|
|
|
'disc',
|
2016-04-17 15:38:06 +00:00
|
|
|
'lyrics',
|
|
|
|
'cover',
|
|
|
|
'mtime',
|
2016-06-16 10:17:16 +00:00
|
|
|
'compilation',
|
2016-04-17 15:38:06 +00:00
|
|
|
];
|
2016-03-22 08:22:39 +00:00
|
|
|
|
2018-08-19 09:05:33 +00:00
|
|
|
private $mediaMetadataService;
|
|
|
|
|
|
|
|
public function __construct(MediaMetadataService $mediaMetadataService)
|
|
|
|
{
|
|
|
|
$this->mediaMetadataService = $mediaMetadataService;
|
|
|
|
}
|
|
|
|
|
2016-03-22 08:22:39 +00:00
|
|
|
/**
|
|
|
|
* Tags to be synced.
|
|
|
|
*
|
|
|
|
* @var array
|
2015-12-13 04:42:28 +00:00
|
|
|
*/
|
2016-03-22 08:22:39 +00:00
|
|
|
protected $tags = [];
|
2015-12-13 04:42:28 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Sync the media. Oh sync the media.
|
|
|
|
*
|
2018-08-19 15:26:34 +00:00
|
|
|
* @param string|null $mediaPath
|
|
|
|
* @param array $tags The tags to sync.
|
2018-08-19 15:26:52 +00:00
|
|
|
* Only taken into account for existing records.
|
|
|
|
* New records will have all tags synced in regardless.
|
2018-08-19 15:26:34 +00: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.
|
2017-12-09 18:34:27 +00:00
|
|
|
*
|
|
|
|
* @throws Exception
|
2015-12-13 04:42:28 +00:00
|
|
|
*/
|
2018-08-19 15:26:34 +00:00
|
|
|
public function sync($mediaPath = null, $tags = [], $force = false, SyncMediaCommand $syncCommand = null)
|
2015-12-13 04:42:28 +00:00
|
|
|
{
|
2016-01-15 01:53:31 +00:00
|
|
|
if (!app()->runningInConsole()) {
|
2016-08-21 15:19:03 +00:00
|
|
|
set_time_limit(config('koel.sync.timeout'));
|
2016-01-15 01:53:31 +00:00
|
|
|
}
|
2015-12-13 04:42:28 +00:00
|
|
|
|
2017-08-21 20:45:01 +00:00
|
|
|
if (config('koel.memory_limit')) {
|
|
|
|
ini_set('memory_limit', config('koel.memory_limit').'M');
|
|
|
|
}
|
|
|
|
|
2017-06-03 23:21:50 +00:00
|
|
|
$mediaPath = $mediaPath ?: Setting::get('media_path');
|
2016-03-22 08:22:39 +00:00
|
|
|
$this->setTags($tags);
|
2015-12-13 04:42:28 +00:00
|
|
|
|
|
|
|
$results = [
|
2017-06-03 23:21:50 +00:00
|
|
|
'success' => [],
|
|
|
|
'bad_files' => [],
|
|
|
|
'unmodified' => [],
|
2015-12-13 04:42:28 +00:00
|
|
|
];
|
|
|
|
|
2016-03-22 08:22:39 +00:00
|
|
|
$getID3 = new getID3();
|
2017-06-03 23:21:50 +00:00
|
|
|
$songPaths = $this->gatherFiles($mediaPath);
|
|
|
|
$syncCommand && $syncCommand->createProgressBar(count($songPaths));
|
|
|
|
|
|
|
|
foreach ($songPaths as $path) {
|
2018-08-19 09:05:33 +00:00
|
|
|
$file = new File($path, $getID3, $this->mediaMetadataService);
|
2017-06-03 23:21:50 +00:00
|
|
|
|
|
|
|
switch ($result = $file->sync($this->tags, $force)) {
|
|
|
|
case File::SYNC_RESULT_SUCCESS:
|
|
|
|
$results['success'][] = $file;
|
|
|
|
break;
|
|
|
|
case File::SYNC_RESULT_UNMODIFIED:
|
|
|
|
$results['unmodified'][] = $file;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
$results['bad_files'][] = $file;
|
|
|
|
break;
|
2015-12-13 04:42:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($syncCommand) {
|
2016-08-17 14:26:07 +00:00
|
|
|
$syncCommand->updateProgressBar();
|
2017-06-03 23:21:50 +00:00
|
|
|
$syncCommand->logToConsole($file->getPath(), $result, $file->getSyncError());
|
2015-12-13 04:42:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete non-existing songs.
|
2017-06-03 23:21:50 +00:00
|
|
|
$hashes = array_map(function (File $file) {
|
2018-08-19 15:26:34 +00:00
|
|
|
return self::getFileHash($file->getPath());
|
2017-06-03 23:21:50 +00:00
|
|
|
}, array_merge($results['unmodified'], $results['success']));
|
2015-12-13 04:42:28 +00:00
|
|
|
|
2016-09-26 07:32:16 +00:00
|
|
|
Song::deleteWhereIDsNotIn($hashes);
|
2015-12-13 04:42:28 +00:00
|
|
|
|
2016-02-02 07:47:00 +00:00
|
|
|
// Trigger LibraryChanged, so that TidyLibrary handler is fired to, erm, tidy our library.
|
|
|
|
event(new LibraryChanged());
|
|
|
|
}
|
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
|
|
|
|
*
|
|
|
|
* @return array An array of SplFileInfo objects
|
|
|
|
*/
|
|
|
|
public function gatherFiles($path)
|
|
|
|
{
|
2017-04-23 16:01:02 +00:00
|
|
|
return iterator_to_array(
|
|
|
|
Finder::create()
|
|
|
|
->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 04:42:28 +00:00
|
|
|
}
|
|
|
|
|
2016-02-02 07:47:00 +00:00
|
|
|
/**
|
2016-02-04 15:04:53 +00:00
|
|
|
* Sync media using a watch record.
|
2016-02-02 07:47:00 +00:00
|
|
|
*
|
2017-06-03 16:48:13 +00:00
|
|
|
* @param WatchRecordInterface $record The watch record.
|
2017-12-09 18:34:27 +00:00
|
|
|
*
|
|
|
|
* @throws Exception
|
2016-02-02 07:47:00 +00:00
|
|
|
*/
|
2017-06-03 16:35:08 +00:00
|
|
|
public function syncByWatchRecord(WatchRecordInterface $record)
|
2016-02-02 07:47:00 +00:00
|
|
|
{
|
2016-02-04 15:47:02 +00:00
|
|
|
Log::info("New watch record received: '$record'");
|
2017-06-03 23:21:50 +00:00
|
|
|
$record->isFile() ? $this->syncFileRecord($record) : $this->syncDirectoryRecord($record);
|
|
|
|
}
|
2016-02-02 07:47:00 +00:00
|
|
|
|
2017-06-03 23:21:50 +00:00
|
|
|
/**
|
|
|
|
* Sync a file's watch record.
|
|
|
|
*
|
|
|
|
* @param WatchRecordInterface $record
|
2017-12-09 18:34:27 +00:00
|
|
|
*
|
|
|
|
* @throws Exception
|
2017-06-03 23:21:50 +00:00
|
|
|
*/
|
|
|
|
private function syncFileRecord(WatchRecordInterface $record)
|
|
|
|
{
|
|
|
|
$path = $record->getPath();
|
|
|
|
Log::info("'$path' is a file.");
|
2017-01-06 03:04:08 +00:00
|
|
|
|
2017-06-03 23:21:50 +00:00
|
|
|
// If the file has been deleted...
|
|
|
|
if ($record->isDeleted()) {
|
|
|
|
// ...and it has a record in our database, remove it.
|
|
|
|
if ($song = Song::byPath($path)) {
|
|
|
|
$song->delete();
|
|
|
|
Log::info("$path deleted.");
|
2017-01-06 03:04:08 +00:00
|
|
|
|
|
|
|
event(new LibraryChanged());
|
2017-06-03 23:21:50 +00:00
|
|
|
} else {
|
|
|
|
Log::info("$path doesn't exist in our database--skipping.");
|
2016-02-02 07:47:00 +00:00
|
|
|
}
|
2017-06-03 23:21:50 +00:00
|
|
|
}
|
|
|
|
// Otherwise, it's a new or changed file. Try to sync it in.
|
|
|
|
// File format etc. will be handled by File::sync().
|
|
|
|
elseif ($record->isNewOrModified()) {
|
|
|
|
$result = (new File($path))->sync($this->tags);
|
|
|
|
Log::info($result === File::SYNC_RESULT_SUCCESS ? "Synchronized $path" : "Invalid file $path");
|
2016-02-02 07:47:00 +00:00
|
|
|
|
2017-06-03 23:21:50 +00:00
|
|
|
event(new LibraryChanged());
|
2016-02-02 07:47:00 +00:00
|
|
|
}
|
2017-06-03 23:21:50 +00:00
|
|
|
}
|
2016-02-02 07:47:00 +00:00
|
|
|
|
2017-06-03 23:21:50 +00:00
|
|
|
/**
|
|
|
|
* Sync a directory's watch record.
|
|
|
|
*
|
|
|
|
* @param WatchRecordInterface $record
|
2018-08-19 09:05:33 +00:00
|
|
|
*
|
|
|
|
* @throws getid3_exception
|
2017-06-03 23:21:50 +00:00
|
|
|
*/
|
|
|
|
private function syncDirectoryRecord(WatchRecordInterface $record)
|
|
|
|
{
|
|
|
|
$path = $record->getPath();
|
2016-02-04 15:47:02 +00:00
|
|
|
Log::info("'$path' is a directory.");
|
2016-02-02 07:47:00 +00:00
|
|
|
|
2016-02-04 15:04:53 +00:00
|
|
|
if ($record->isDeleted()) {
|
|
|
|
// The directory is removed. We remove all songs in it.
|
|
|
|
if ($count = Song::inDirectory($path)->delete()) {
|
2016-02-10 15:30:48 +00:00
|
|
|
Log::info("Deleted $count song(s) under $path");
|
2017-06-03 23:21:50 +00:00
|
|
|
|
2016-02-02 07:47:00 +00:00
|
|
|
event(new LibraryChanged());
|
|
|
|
} else {
|
2016-02-04 15:04:53 +00:00
|
|
|
Log::info("$path is empty--no action needed.");
|
|
|
|
}
|
|
|
|
} elseif ($record->isNewOrModified()) {
|
|
|
|
foreach ($this->gatherFiles($path) as $file) {
|
2016-03-22 08:22:39 +00:00
|
|
|
(new File($file))->sync($this->tags);
|
2016-02-02 07:47:00 +00:00
|
|
|
}
|
2017-06-03 23:21:50 +00:00
|
|
|
Log::info("Synced all song(s) under $path");
|
2016-02-02 07:47:00 +00:00
|
|
|
|
2017-01-06 03:10:03 +00:00
|
|
|
event(new LibraryChanged());
|
2016-02-02 07:47:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-12-13 04:42:28 +00:00
|
|
|
/**
|
2016-03-22 08:22:39 +00: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 07:03:19 +00:00
|
|
|
* Otherwise, we only use the valid items in it.
|
2015-12-13 04:42:28 +00:00
|
|
|
*
|
2016-03-22 08:22:39 +00:00
|
|
|
* @param array $tags
|
2015-12-13 04:42:28 +00:00
|
|
|
*/
|
2016-03-22 08:22:39 +00:00
|
|
|
public function setTags($tags = [])
|
2015-12-13 04:42:28 +00:00
|
|
|
{
|
2016-03-22 08:22:39 +00:00
|
|
|
$this->tags = array_intersect((array) $tags, $this->allTags) ?: $this->allTags;
|
2015-12-13 04:42:28 +00:00
|
|
|
|
2016-03-22 08:22:39 +00:00
|
|
|
// We always keep track of mtime.
|
2016-08-03 10:42:11 +00:00
|
|
|
if (!in_array('mtime', $this->tags, true)) {
|
2016-03-22 08:22:39 +00:00
|
|
|
$this->tags[] = 'mtime';
|
2015-12-13 04:42:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate a unique hash for a file path.
|
|
|
|
*
|
|
|
|
* @param $path
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
2018-08-19 15:26:34 +00:00
|
|
|
public function getFileHash($path)
|
2015-12-13 04:42:28 +00:00
|
|
|
{
|
2016-03-22 08:22:39 +00:00
|
|
|
return File::getHash($path);
|
2015-12-13 04:42:28 +00:00
|
|
|
}
|
2016-04-17 15:38:06 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Tidy up the library by deleting empty albums and artists.
|
2017-12-09 20:10:55 +00:00
|
|
|
*
|
2017-12-09 18:34:27 +00:00
|
|
|
* @throws Exception
|
2016-04-17 15:38:06 +00:00
|
|
|
*/
|
|
|
|
public function tidy()
|
|
|
|
{
|
2017-04-29 02:53:33 +00:00
|
|
|
$inUseAlbums = Song::select('album_id')
|
|
|
|
->groupBy('album_id')
|
|
|
|
->get()
|
|
|
|
->pluck('album_id')
|
|
|
|
->toArray();
|
2016-04-17 15:38:06 +00:00
|
|
|
$inUseAlbums[] = Album::UNKNOWN_ID;
|
2016-09-26 07:32:16 +00:00
|
|
|
Album::deleteWhereIDsNotIn($inUseAlbums);
|
2016-04-17 15:38:06 +00:00
|
|
|
|
2017-05-02 03:23:10 +00:00
|
|
|
$inUseArtists = Song::select('artist_id')
|
2017-04-29 03:49:14 +00:00
|
|
|
->groupBy('artist_id')
|
2016-04-17 15:38:06 +00:00
|
|
|
->get()
|
2017-04-29 03:49:14 +00:00
|
|
|
->pluck('artist_id')
|
2016-04-17 15:38:06 +00:00
|
|
|
->toArray();
|
|
|
|
$inUseArtists[] = Artist::UNKNOWN_ID;
|
|
|
|
$inUseArtists[] = Artist::VARIOUS_ID;
|
2016-09-26 07:32:16 +00:00
|
|
|
Artist::deleteWhereIDsNotIn(array_filter($inUseArtists));
|
2016-04-17 15:38:06 +00:00
|
|
|
}
|
2015-12-13 04:42:28 +00:00
|
|
|
}
|