koel/app/Services/Media.php

247 lines
7.3 KiB
PHP
Raw Normal View History

2015-12-13 04:42:28 +00:00
<?php
namespace App\Services;
2015-12-14 13:22:39 +00:00
use App\Console\Commands\SyncMedia;
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;
use getID3;
use Illuminate\Support\Facades\Log;
2016-02-04 15:48:15 +00:00
use Symfony\Component\Finder\Finder;
2015-12-13 04:42:28 +00:00
class Media
{
/**
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',
'lyrics',
'cover',
'mtime',
'compilation',
2016-04-17 15:38:06 +00:00
];
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
public function __construct()
{
}
/**
* Sync the media. Oh sync the media.
*
* @param string|null $path
2016-03-22 08:22:39 +00:00
* @param array $tags The tags to sync.
* 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
2015-12-13 04:42:28 +00:00
* @param SyncMedia $syncCommand The SyncMedia command object, to log to console if executed by artisan.
*/
2016-03-22 08:22:39 +00:00
public function sync($path = null, $tags = [], $force = false, SyncMedia $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
$path = $path ?: Setting::get('media_path');
2016-03-22 08:22:39 +00:00
$this->setTags($tags);
2015-12-13 04:42:28 +00:00
$results = [
'good' => [], // Updated or added files
'bad' => [], // Bad files
'ugly' => [], // Unmodified files
];
2016-03-22 08:22:39 +00:00
$getID3 = new getID3();
2016-08-17 14:26:07 +00:00
$files = $this->gatherFiles($path);
if ($syncCommand) {
$syncCommand->createProgressBar(count($files));
}
foreach ($files as $file) {
2016-03-22 08:22:39 +00:00
$file = new File($file, $getID3);
$song = $file->sync($this->tags, $force);
2015-12-13 04:42:28 +00:00
if ($song === true) {
$results['ugly'][] = $file;
} elseif ($song === false) {
$results['bad'][] = $file;
} else {
$results['good'][] = $file;
}
if ($syncCommand) {
2016-08-17 14:26:07 +00:00
$syncCommand->updateProgressBar();
$syncCommand->logToConsole($file->getPath(), $song, $file->getSyncError());
2015-12-13 04:42:28 +00:00
}
}
// Delete non-existing songs.
$hashes = array_map(function ($f) {
2016-03-22 08:22:39 +00:00
return self::getHash($f->getPath());
2015-12-13 04:42:28 +00:00
}, array_merge($results['ugly'], $results['good']));
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
*
2016-02-04 15:04:53 +00:00
* @param WatchRecordInterface $record The watch record.
2016-02-02 07:47:00 +00:00
* @param SyncMedia|null $syncCommand The SyncMedia command object, to log to console if executed by artisan.
*/
2016-02-04 15:04:53 +00:00
public function syncByWatchRecord(WatchRecordInterface $record, SyncMedia $syncCommand = null)
2016-02-02 07:47:00 +00:00
{
2016-02-04 15:47:02 +00:00
Log::info("New watch record received: '$record'");
2016-02-02 07:47:00 +00:00
$path = $record->getPath();
if ($record->isFile()) {
2016-02-04 15:47:02 +00:00
Log::info("'$path' is a file.");
2016-02-04 15:04:53 +00:00
2016-02-02 07:47:00 +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();
2016-02-04 15:04:53 +00:00
Log::info("$path deleted.");
2016-02-02 07:47:00 +00:00
event(new LibraryChanged());
2016-02-04 15:04:53 +00:00
} else {
Log::info("$path doesn't exist in our database--skipping.");
2016-02-02 07:47:00 +00:00
}
}
// Otherwise, it's a new or changed file. Try to sync it in.
2016-03-26 01:44:55 +00:00
// File format etc. will be handled by File::sync().
2016-02-04 15:04:53 +00:00
elseif ($record->isNewOrModified()) {
2016-03-22 08:22:39 +00:00
$result = (new File($path))->sync($this->tags);
2017-01-06 03:04:08 +00:00
2016-03-22 08:22:39 +00:00
Log::info($result instanceof Song ? "Synchronized $path" : "Invalid file $path");
2017-01-06 03:04:08 +00:00
event(new LibraryChanged());
2016-02-02 07:47:00 +00:00
}
return;
}
2016-02-04 15:04:53 +00:00
// Record is a directory.
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");
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
}
event(new LibraryChanged());
2016-02-04 15:04:53 +00:00
Log::info("Synced all song(s) under $path");
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
*/
public function getHash($path)
{
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.
*/
public function tidy()
{
2016-09-26 06:30:00 +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
2016-09-26 06:30:00 +00:00
$inUseArtists = Album::select('artist_id')->groupBy('artist_id')->get()->pluck('artist_id')->toArray();
2016-04-17 15:38:06 +00:00
$contributingArtists = Song::distinct()
->select('contributing_artist_id')
->groupBy('contributing_artist_id')
->get()
2016-09-26 06:30:00 +00:00
->pluck('contributing_artist_id')
2016-04-17 15:38:06 +00:00
->toArray();
$inUseArtists = array_merge($inUseArtists, $contributingArtists);
$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
}