mirror of
https://github.com/koel/koel
synced 2024-12-01 00:09:17 +00:00
Rewrite the file synchronization handling logic
This commit is contained in:
parent
96bbbee4a7
commit
1558062428
13 changed files with 325 additions and 351 deletions
|
@ -4,6 +4,7 @@ namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Repositories\SettingRepository;
|
||||||
use App\Services\MediaCacheService;
|
use App\Services\MediaCacheService;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
@ -22,9 +23,11 @@ class InitCommand extends Command
|
||||||
private $dotenvEditor;
|
private $dotenvEditor;
|
||||||
private $hash;
|
private $hash;
|
||||||
private $db;
|
private $db;
|
||||||
|
private $settingRepository;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
MediaCacheService $mediaCacheService,
|
MediaCacheService $mediaCacheService,
|
||||||
|
SettingRepository $settingRepository,
|
||||||
Artisan $artisan,
|
Artisan $artisan,
|
||||||
Hash $hash,
|
Hash $hash,
|
||||||
DotenvEditor $dotenvEditor,
|
DotenvEditor $dotenvEditor,
|
||||||
|
@ -37,6 +40,7 @@ class InitCommand extends Command
|
||||||
$this->dotenvEditor = $dotenvEditor;
|
$this->dotenvEditor = $dotenvEditor;
|
||||||
$this->hash = $hash;
|
$this->hash = $hash;
|
||||||
$this->db = $db;
|
$this->db = $db;
|
||||||
|
$this->settingRepository = $settingRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
|
@ -55,7 +59,7 @@ class InitCommand extends Command
|
||||||
|
|
||||||
$this->comment(PHP_EOL.'🎆 Success! Koel can now be run from localhost with `php artisan serve`.');
|
$this->comment(PHP_EOL.'🎆 Success! Koel can now be run from localhost with `php artisan serve`.');
|
||||||
|
|
||||||
if (Setting::get('media_path')) {
|
if ($this->settingRepository->getMediaPath()) {
|
||||||
$this->comment('You can also scan for media with `php artisan koel:sync`.');
|
$this->comment('You can also scan for media with `php artisan koel:sync`.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,7 +150,7 @@ class InitCommand extends Command
|
||||||
|
|
||||||
private function maybeSetMediaPath(): void
|
private function maybeSetMediaPath(): void
|
||||||
{
|
{
|
||||||
if (!Setting::get('media_path')) {
|
if ($this->settingRepository->getMediaPath()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Libraries\WatchRecord\InotifyWatchRecord;
|
use App\Libraries\WatchRecord\InotifyWatchRecord;
|
||||||
use App\Models\File;
|
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
|
use App\Repositories\SettingRepository;
|
||||||
|
use App\Services\FileSynchronizer;
|
||||||
use App\Services\MediaSyncService;
|
use App\Services\MediaSyncService;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
@ -18,21 +19,22 @@ class SyncMediaCommand extends Command
|
||||||
{--force : Force re-syncing even unchanged files}';
|
{--force : Force re-syncing even unchanged files}';
|
||||||
|
|
||||||
protected $description = 'Sync songs found in configured directory against the database.';
|
protected $description = 'Sync songs found in configured directory against the database.';
|
||||||
|
|
||||||
private $ignored = 0;
|
private $ignored = 0;
|
||||||
private $invalid = 0;
|
private $invalid = 0;
|
||||||
private $synced = 0;
|
private $synced = 0;
|
||||||
private $mediaSyncService;
|
private $mediaSyncService;
|
||||||
|
private $settingRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ProgressBar
|
* @var ProgressBar
|
||||||
*/
|
*/
|
||||||
private $progressBar;
|
private $progressBar;
|
||||||
|
|
||||||
public function __construct(MediaSyncService $mediaSyncService)
|
public function __construct(MediaSyncService $mediaSyncService, SettingRepository $settingRepository)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->mediaSyncService = $mediaSyncService;
|
$this->mediaSyncService = $mediaSyncService;
|
||||||
|
$this->settingRepository = $settingRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,19 +42,7 @@ class SyncMediaCommand extends Command
|
||||||
*/
|
*/
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
if (!Setting::get('media_path')) {
|
$this->ensureMediaPath();
|
||||||
$this->warn("Media path hasn't been configured. Let's set it up.");
|
|
||||||
while (true) {
|
|
||||||
$path = $this->ask('Absolute path to your media directory:');
|
|
||||||
|
|
||||||
if (is_dir($path) && is_readable($path)) {
|
|
||||||
Setting::set('media_path', $path);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->error('The path does not exist or not readable. Try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$record = $this->argument('record')) {
|
if (!$record = $this->argument('record')) {
|
||||||
$this->syncAll();
|
$this->syncAll();
|
||||||
|
@ -113,23 +103,15 @@ class SyncMediaCommand extends Command
|
||||||
{
|
{
|
||||||
$name = basename($path);
|
$name = basename($path);
|
||||||
|
|
||||||
if ($result === File::SYNC_RESULT_UNMODIFIED) {
|
if ($result === FileSynchronizer::SYNC_RESULT_UNMODIFIED) {
|
||||||
if ($this->option('verbose')) {
|
|
||||||
$this->line("$name has no changes – ignoring");
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->ignored++;
|
$this->ignored++;
|
||||||
} elseif ($result === File::SYNC_RESULT_BAD_FILE) {
|
} elseif ($result === FileSynchronizer::SYNC_RESULT_BAD_FILE) {
|
||||||
if ($this->option('verbose')) {
|
if ($this->option('verbose')) {
|
||||||
$this->error("$name is not a valid media file because: ".$reason);
|
$this->error(PHP_EOL . "'$name' is not a valid media file: ".$reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->invalid++;
|
$this->invalid++;
|
||||||
} else {
|
} else {
|
||||||
if ($this->option('verbose')) {
|
|
||||||
$this->info("$name synced");
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->synced++;
|
$this->synced++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,8 +121,28 @@ class SyncMediaCommand extends Command
|
||||||
$this->progressBar = $this->getOutput()->createProgressBar($max);
|
$this->progressBar = $this->getOutput()->createProgressBar($max);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function advanceProgressBar()
|
public function advanceProgressBar(): void
|
||||||
{
|
{
|
||||||
$this->progressBar->advance();
|
$this->progressBar->advance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function ensureMediaPath(): void
|
||||||
|
{
|
||||||
|
if ($this->settingRepository->getMediaPath()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->warn("Media path hasn't been configured. Let's set it up.");
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
$path = $this->ask('Absolute path to your media directory:');
|
||||||
|
|
||||||
|
if (is_dir($path) && is_readable($path)) {
|
||||||
|
Setting::set('media_path', $path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->error('The path does not exist or is not readable. Try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ use Illuminate\Support\Collection;
|
||||||
* @property int album_id
|
* @property int album_id
|
||||||
* @property int id
|
* @property int id
|
||||||
* @property int artist_id
|
* @property int artist_id
|
||||||
|
* @property int mtime
|
||||||
*/
|
*/
|
||||||
class Song extends Model
|
class Song extends Model
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Repositories;
|
namespace App\Repositories;
|
||||||
|
|
||||||
use App\Models\Album;
|
use App\Models\Album;
|
||||||
|
use App\Models\Song;
|
||||||
|
|
||||||
class AlbumRepository extends AbstractRepository
|
class AlbumRepository extends AbstractRepository
|
||||||
{
|
{
|
||||||
|
@ -10,4 +11,15 @@ class AlbumRepository extends AbstractRepository
|
||||||
{
|
{
|
||||||
return Album::class;
|
return Album::class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getNonEmptyAlbumIds(): array
|
||||||
|
{
|
||||||
|
$ids = Song::select('album_id')
|
||||||
|
->groupBy('album_id')
|
||||||
|
->get()
|
||||||
|
->pluck('album_id')
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
return $ids;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Repositories;
|
namespace App\Repositories;
|
||||||
|
|
||||||
use App\Models\Artist;
|
use App\Models\Artist;
|
||||||
|
use App\Models\Song;
|
||||||
|
|
||||||
class ArtistRepository extends AbstractRepository
|
class ArtistRepository extends AbstractRepository
|
||||||
{
|
{
|
||||||
|
@ -10,4 +11,13 @@ class ArtistRepository extends AbstractRepository
|
||||||
{
|
{
|
||||||
return Artist::class;
|
return Artist::class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getNonEmptyArtistIds(): array
|
||||||
|
{
|
||||||
|
return Song::select('artist_id')
|
||||||
|
->groupBy('artist_id')
|
||||||
|
->get()
|
||||||
|
->pluck('artist_id')
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,6 @@ namespace App\Services;
|
||||||
interface ApiConsumerInterface
|
interface ApiConsumerInterface
|
||||||
{
|
{
|
||||||
public function getEndpoint(): string;
|
public function getEndpoint(): string;
|
||||||
|
|
||||||
public function getKey(): ?string;
|
public function getKey(): ?string;
|
||||||
|
|
||||||
public function getSecret(): ?string;
|
public function getSecret(): ?string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,129 +1,113 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Album;
|
||||||
|
use App\Models\Artist;
|
||||||
|
use App\Models\Song;
|
||||||
use App\Repositories\SongRepository;
|
use App\Repositories\SongRepository;
|
||||||
use App\Services\HelperService;
|
|
||||||
use App\Services\MediaMetadataService;
|
|
||||||
use Cache;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use getID3;
|
use getID3;
|
||||||
use getid3_exception;
|
|
||||||
use getid3_lib;
|
use getid3_lib;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||||
use SplFileInfo;
|
use SplFileInfo;
|
||||||
use Symfony\Component\Finder\Finder;
|
use Symfony\Component\Finder\Finder;
|
||||||
|
|
||||||
class File
|
class FileSynchronizer
|
||||||
{
|
{
|
||||||
const SYNC_RESULT_SUCCESS = 1;
|
const SYNC_RESULT_SUCCESS = 1;
|
||||||
const SYNC_RESULT_BAD_FILE = 2;
|
const SYNC_RESULT_BAD_FILE = 2;
|
||||||
const SYNC_RESULT_UNMODIFIED = 3;
|
const SYNC_RESULT_UNMODIFIED = 3;
|
||||||
|
|
||||||
|
private $getID3;
|
||||||
|
private $mediaMetadataService;
|
||||||
|
private $helperService;
|
||||||
|
private $songRepository;
|
||||||
|
private $cache;
|
||||||
|
private $finder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A MD5 hash of the file's path.
|
* @var SplFileInfo
|
||||||
|
*/
|
||||||
|
private $splFileInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $fileModifiedTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $filePath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A (MD5) hash of the file's path.
|
||||||
* This value is unique, and can be used to query a Song record.
|
* This value is unique, and can be used to query a Song record.
|
||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $hash;
|
private $fileHash;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The file's last modified time.
|
* The song model that's associated with the current file.
|
||||||
*
|
*
|
||||||
* @var int
|
* @var Song|null
|
||||||
*/
|
*/
|
||||||
protected $mtime;
|
private $song;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The file's path.
|
* @var string|null
|
||||||
*/
|
*/
|
||||||
protected $path;
|
private $syncError;
|
||||||
|
|
||||||
/**
|
|
||||||
* The getID3 object, for ID3 tag reading.
|
|
||||||
*/
|
|
||||||
protected $getID3;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var MediaMetadataService
|
|
||||||
*/
|
|
||||||
private $mediaMetadataService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The SplFileInfo object of the file.
|
|
||||||
*
|
|
||||||
* @var SplFileInfo
|
|
||||||
*/
|
|
||||||
protected $splFileInfo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The song model that's associated with this file.
|
|
||||||
*
|
|
||||||
* @var Song
|
|
||||||
*/
|
|
||||||
protected $song;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The last parsing error text, if any.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $syncError;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var HelperService
|
|
||||||
*/
|
|
||||||
private $helperService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var SongRepository
|
|
||||||
*/
|
|
||||||
private $songRepository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct our File object.
|
|
||||||
* Upon construction, we'll set the path, hash, and associated Song object (if any).
|
|
||||||
*
|
|
||||||
* @param string|SplFileInfo $path Either the file's path, or a SplFileInfo object
|
|
||||||
*
|
|
||||||
* @throws getid3_exception
|
|
||||||
*
|
|
||||||
* @todo Refactor this bloated, anti-pattern monster.
|
|
||||||
*/
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
$path,
|
getID3 $getID3,
|
||||||
?getID3 $getID3 = null,
|
MediaMetadataService $mediaMetadataService,
|
||||||
?MediaMetadataService $mediaMetadataService = null,
|
HelperService $helperService,
|
||||||
?HelperService $helperService = null,
|
SongRepository $songRepository,
|
||||||
?SongRepository $songRepository = null
|
Cache $cache,
|
||||||
) {
|
Finder $finder
|
||||||
|
)
|
||||||
|
{
|
||||||
|
$this->getID3 = $getID3;
|
||||||
|
$this->mediaMetadataService = $mediaMetadataService;
|
||||||
|
$this->helperService = $helperService;
|
||||||
|
$this->songRepository = $songRepository;
|
||||||
|
$this->cache = $cache;
|
||||||
|
$this->finder = $finder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|SplFileInfo $path
|
||||||
|
*/
|
||||||
|
public function setFile($path): self
|
||||||
|
{
|
||||||
$this->splFileInfo = $path instanceof SplFileInfo ? $path : new SplFileInfo($path);
|
$this->splFileInfo = $path instanceof SplFileInfo ? $path : new SplFileInfo($path);
|
||||||
$this->setGetID3($getID3);
|
|
||||||
$this->setMediaMetadataService($mediaMetadataService);
|
|
||||||
$this->setHelperService($helperService);
|
|
||||||
$this->setSongRepository($songRepository);
|
|
||||||
|
|
||||||
// Workaround for #344, where getMTime() fails for certain files with Unicode names on Windows.
|
// Workaround for #344, where getMTime() fails for certain files with Unicode names on Windows.
|
||||||
try {
|
try {
|
||||||
$this->mtime = $this->splFileInfo->getMTime();
|
$this->fileModifiedTime = $this->splFileInfo->getMTime();
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
// Not worth logging the error. Just use current stamp for mtime.
|
// Not worth logging the error. Just use current stamp for mtime.
|
||||||
$this->mtime = time();
|
$this->fileModifiedTime = time();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->path = $this->splFileInfo->getPathname();
|
$this->filePath = $this->splFileInfo->getPathname();
|
||||||
$this->hash = $this->helperService->getFileHash($this->path);
|
$this->fileHash = $this->helperService->getFileHash($this->filePath);
|
||||||
$this->song = $this->songRepository->getOneById($this->hash);
|
$this->song = $this->songRepository->getOneById($this->fileHash);
|
||||||
$this->syncError = null;
|
$this->syncError = null;
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all applicable ID3 info from the file.
|
* Get all applicable ID3 info from the file.
|
||||||
*/
|
*/
|
||||||
public function getInfo(): array
|
public function getFileInfo(): array
|
||||||
{
|
{
|
||||||
$info = $this->getID3->analyze($this->path);
|
$info = $this->getID3->analyze($this->filePath);
|
||||||
|
|
||||||
if (isset($info['error']) || !isset($info['playtime_seconds'])) {
|
if (isset($info['error']) || !isset($info['playtime_seconds'])) {
|
||||||
$this->syncError = isset($info['error']) ? $info['error'][0] : 'No playtime found';
|
$this->syncError = isset($info['error']) ? $info['error'][0] : 'No playtime found';
|
||||||
|
@ -154,14 +138,14 @@ class File
|
||||||
'artist' => '',
|
'artist' => '',
|
||||||
'album' => '',
|
'album' => '',
|
||||||
'compilation' => false,
|
'compilation' => false,
|
||||||
'title' => basename($this->path, '.'.pathinfo($this->path, PATHINFO_EXTENSION)), // default to be file name
|
'title' => basename($this->filePath, '.'.pathinfo($this->filePath, PATHINFO_EXTENSION)), // default to be file name
|
||||||
'length' => $info['playtime_seconds'],
|
'length' => $info['playtime_seconds'],
|
||||||
'track' => (int) $track,
|
'track' => (int) $track,
|
||||||
'disc' => (int) array_get($info, 'comments.part_of_a_set.0', 1),
|
'disc' => (int) array_get($info, 'comments.part_of_a_set.0', 1),
|
||||||
'lyrics' => '',
|
'lyrics' => '',
|
||||||
'cover' => array_get($info, 'comments.picture', [null])[0],
|
'cover' => array_get($info, 'comments.picture', [null])[0],
|
||||||
'path' => $this->path,
|
'path' => $this->filePath,
|
||||||
'mtime' => $this->mtime,
|
'mtime' => $this->fileModifiedTime,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!$comments = array_get($info, 'comments_html')) {
|
if (!$comments = array_get($info, 'comments_html')) {
|
||||||
|
@ -195,7 +179,6 @@ class File
|
||||||
|
|
||||||
return $props;
|
return $props;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync the song with all available media info against the database.
|
* Sync the song with all available media info against the database.
|
||||||
*
|
*
|
||||||
|
@ -209,21 +192,21 @@ class File
|
||||||
public function sync(array $tags, bool $force = false)
|
public function sync(array $tags, bool $force = false)
|
||||||
{
|
{
|
||||||
// If the file is not new or changed and we're not forcing update, don't do anything.
|
// If the file is not new or changed and we're not forcing update, don't do anything.
|
||||||
if (!$this->isNewOrChanged() && !$force) {
|
if (!$this->isFileNewOrChanged() && !$force) {
|
||||||
return self::SYNC_RESULT_UNMODIFIED;
|
return self::SYNC_RESULT_UNMODIFIED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the file is invalid, don't do anything.
|
// If the file is invalid, don't do anything.
|
||||||
if (!$info = $this->getInfo()) {
|
if (!$info = $this->getFileInfo()) {
|
||||||
return self::SYNC_RESULT_BAD_FILE;
|
return self::SYNC_RESULT_BAD_FILE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fixes #366. If the file is new, we use all tags by simply setting $force to false.
|
// Fixes #366. If the file is new, we use all tags by simply setting $force to false.
|
||||||
if ($this->isNew()) {
|
if ($this->isFileNew()) {
|
||||||
$force = false;
|
$force = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->isChanged() || $force) {
|
if ($this->isFileChanged() || $force) {
|
||||||
// This is a changed file, or the user is forcing updates.
|
// This is a changed file, or the user is forcing updates.
|
||||||
// In such a case, the user must have specified a list of tags to sync.
|
// In such a case, the user must have specified a list of tags to sync.
|
||||||
// A sample command could be: ./artisan koel:sync --force --tags=artist,album,lyrics
|
// A sample command could be: ./artisan koel:sync --force --tags=artist,album,lyrics
|
||||||
|
@ -268,7 +251,7 @@ class File
|
||||||
$data = array_except($info, ['artist', 'albumartist', 'album', 'cover', 'compilation']);
|
$data = array_except($info, ['artist', 'albumartist', 'album', 'cover', 'compilation']);
|
||||||
$data['album_id'] = $album->id;
|
$data['album_id'] = $album->id;
|
||||||
$data['artist_id'] = $artist->id;
|
$data['artist_id'] = $artist->id;
|
||||||
$this->song = Song::updateOrCreate(['id' => $this->hash], $data);
|
$this->song = Song::updateOrCreate(['id' => $this->fileHash], $data);
|
||||||
|
|
||||||
return self::SYNC_RESULT_SUCCESS;
|
return self::SYNC_RESULT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
@ -278,7 +261,7 @@ class File
|
||||||
*
|
*
|
||||||
* @param mixed[]|null $coverData
|
* @param mixed[]|null $coverData
|
||||||
*/
|
*/
|
||||||
private function generateAlbumCover(Album $album, ?array $coverData): void
|
private function generateAlbumCover(Album $album, ?array $coverData)
|
||||||
{
|
{
|
||||||
// If the album has no cover, we try to get the cover image from existing tag data
|
// If the album has no cover, we try to get the cover image from existing tag data
|
||||||
if ($coverData) {
|
if ($coverData) {
|
||||||
|
@ -296,56 +279,6 @@ class File
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if the file is new (its Song record can't be found in the database).
|
|
||||||
*/
|
|
||||||
public function isNew(): bool
|
|
||||||
{
|
|
||||||
return !$this->song;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if the file is changed (its Song record is found, but the timestamp is different).
|
|
||||||
*/
|
|
||||||
public function isChanged(): bool
|
|
||||||
{
|
|
||||||
return !$this->isNew() && $this->song->mtime !== $this->mtime;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if the file is new or changed.
|
|
||||||
*/
|
|
||||||
public function isNewOrChanged(): bool
|
|
||||||
{
|
|
||||||
return $this->isNew() || $this->isChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getGetID3(): getID3
|
|
||||||
{
|
|
||||||
return $this->getID3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the last parsing error's text.
|
|
||||||
*/
|
|
||||||
public function getSyncError(): ?string
|
|
||||||
{
|
|
||||||
return $this->syncError;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws getid3_exception
|
|
||||||
*/
|
|
||||||
public function setGetID3(?getID3 $getID3 = null): void
|
|
||||||
{
|
|
||||||
$this->getID3 = $getID3 ?: new getID3();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPath(): string
|
|
||||||
{
|
|
||||||
return $this->path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Issue #380.
|
* Issue #380.
|
||||||
* Some albums have its own cover image under the same directory as cover|folder.jpg/png.
|
* Some albums have its own cover image under the same directory as cover|folder.jpg/png.
|
||||||
|
@ -356,15 +289,15 @@ class File
|
||||||
private function getCoverFileUnderSameDirectory(): ?string
|
private function getCoverFileUnderSameDirectory(): ?string
|
||||||
{
|
{
|
||||||
// As directory scanning can be expensive, we cache and reuse the result.
|
// As directory scanning can be expensive, we cache and reuse the result.
|
||||||
return Cache::remember(md5($this->path.'_cover'), 24 * 60, function () {
|
return $this->cache->remember(md5($this->filePath . '_cover'), 24 * 60, function (): ?string {
|
||||||
$matches = array_keys(iterator_to_array(
|
$matches = array_keys(iterator_to_array(
|
||||||
Finder::create()
|
$this->finder->create()
|
||||||
->depth(0)
|
->depth(0)
|
||||||
->ignoreUnreadableDirs()
|
->ignoreUnreadableDirs()
|
||||||
->files()
|
->files()
|
||||||
->followLinks()
|
->followLinks()
|
||||||
->name('/(cov|fold)er\.(jpe?g|png)$/i')
|
->name('/(cov|fold)er\.(jpe?g|png)$/i')
|
||||||
->in(dirname($this->path))
|
->in(dirname($this->filePath))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -379,18 +312,30 @@ class File
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private function setMediaMetadataService(?MediaMetadataService $mediaMetadataService = null): void
|
|
||||||
|
/**
|
||||||
|
* Determine if the file is new (its Song record can't be found in the database).
|
||||||
|
*/
|
||||||
|
public function isFileNew(): bool
|
||||||
{
|
{
|
||||||
$this->mediaMetadataService = $mediaMetadataService ?: app(MediaMetadataService::class);
|
return !$this->song;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function setHelperService(?HelperService $helperService = null): void
|
/**
|
||||||
|
* Determine if the file is changed (its Song record is found, but the timestamp is different).
|
||||||
|
*/
|
||||||
|
public function isFileChanged(): bool
|
||||||
{
|
{
|
||||||
$this->helperService = $helperService ?: app(HelperService::class);
|
return !$this->isFileNew() && $this->song->mtime !== $this->fileModifiedTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setSongRepository(?SongRepository $songRepository = null): void
|
public function isFileNewOrChanged(): bool
|
||||||
{
|
{
|
||||||
$this->songRepository = $songRepository ?: app(SongRepository::class);
|
return $this->isFileNew() || $this->isFileChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSyncError(): ?string
|
||||||
|
{
|
||||||
|
return $this->syncError;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -7,13 +7,13 @@ use App\Events\LibraryChanged;
|
||||||
use App\Libraries\WatchRecord\WatchRecordInterface;
|
use App\Libraries\WatchRecord\WatchRecordInterface;
|
||||||
use App\Models\Album;
|
use App\Models\Album;
|
||||||
use App\Models\Artist;
|
use App\Models\Artist;
|
||||||
use App\Models\File;
|
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Models\Song;
|
use App\Models\Song;
|
||||||
|
use App\Repositories\AlbumRepository;
|
||||||
|
use App\Repositories\ArtistRepository;
|
||||||
|
use App\Repositories\SettingRepository;
|
||||||
use App\Repositories\SongRepository;
|
use App\Repositories\SongRepository;
|
||||||
use Exception;
|
use Exception;
|
||||||
use getID3;
|
|
||||||
use getid3_exception;
|
|
||||||
use Log;
|
use Log;
|
||||||
use SplFileInfo;
|
use SplFileInfo;
|
||||||
use Symfony\Component\Finder\Finder;
|
use Symfony\Component\Finder\Finder;
|
||||||
|
@ -42,15 +42,30 @@ class MediaSyncService
|
||||||
private $mediaMetadataService;
|
private $mediaMetadataService;
|
||||||
private $songRepository;
|
private $songRepository;
|
||||||
private $helperService;
|
private $helperService;
|
||||||
|
private $fileSynchronizer;
|
||||||
|
private $finder;
|
||||||
|
private $artistRepository;
|
||||||
|
private $albumRepository;
|
||||||
|
private $settingRepository;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
MediaMetadataService $mediaMetadataService,
|
MediaMetadataService $mediaMetadataService,
|
||||||
SongRepository $songRepository,
|
SongRepository $songRepository,
|
||||||
HelperService $helperService
|
ArtistRepository $artistRepository,
|
||||||
|
AlbumRepository $albumRepository,
|
||||||
|
SettingRepository $settingRepository,
|
||||||
|
HelperService $helperService,
|
||||||
|
FileSynchronizer $fileSynchronizer,
|
||||||
|
Finder $finder
|
||||||
) {
|
) {
|
||||||
$this->mediaMetadataService = $mediaMetadataService;
|
$this->mediaMetadataService = $mediaMetadataService;
|
||||||
$this->songRepository = $songRepository;
|
$this->songRepository = $songRepository;
|
||||||
$this->helperService = $helperService;
|
$this->helperService = $helperService;
|
||||||
|
$this->fileSynchronizer = $fileSynchronizer;
|
||||||
|
$this->finder = $finder;
|
||||||
|
$this->artistRepository = $artistRepository;
|
||||||
|
$this->albumRepository = $albumRepository;
|
||||||
|
$this->settingRepository = $settingRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -75,17 +90,9 @@ class MediaSyncService
|
||||||
?string $mediaPath = null,
|
?string $mediaPath = null,
|
||||||
array $tags = [],
|
array $tags = [],
|
||||||
bool $force = false,
|
bool $force = false,
|
||||||
SyncMediaCommand $syncCommand = null
|
?SyncMediaCommand $syncCommand = null
|
||||||
): void {
|
): void {
|
||||||
if (!app()->runningInConsole()) {
|
$this->setSystemRequirements();
|
||||||
set_time_limit(config('koel.sync.timeout'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config('koel.memory_limit')) {
|
|
||||||
ini_set('memory_limit', config('koel.memory_limit').'M');
|
|
||||||
}
|
|
||||||
|
|
||||||
$mediaPath = $mediaPath ?: Setting::get('media_path');
|
|
||||||
$this->setTags($tags);
|
$this->setTags($tags);
|
||||||
|
|
||||||
$results = [
|
$results = [
|
||||||
|
@ -94,34 +101,33 @@ class MediaSyncService
|
||||||
'unmodified' => [],
|
'unmodified' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
$getID3 = new getID3();
|
$songPaths = $this->gatherFiles($mediaPath ?: $this->settingRepository->getMediaPath());
|
||||||
$songPaths = $this->gatherFiles($mediaPath);
|
|
||||||
$syncCommand && $syncCommand->createProgressBar(count($songPaths));
|
$syncCommand && $syncCommand->createProgressBar(count($songPaths));
|
||||||
|
|
||||||
foreach ($songPaths as $path) {
|
foreach ($songPaths as $path) {
|
||||||
$file = new File($path, $getID3, $this->mediaMetadataService);
|
$result = $this->fileSynchronizer->setFile($path)->sync($this->tags, $force);
|
||||||
|
|
||||||
switch ($result = $file->sync($this->tags, $force)) {
|
switch ($result) {
|
||||||
case File::SYNC_RESULT_SUCCESS:
|
case FileSynchronizer::SYNC_RESULT_SUCCESS:
|
||||||
$results['success'][] = $file;
|
$results['success'][] = $path;
|
||||||
break;
|
break;
|
||||||
case File::SYNC_RESULT_UNMODIFIED:
|
case FileSynchronizer::SYNC_RESULT_UNMODIFIED:
|
||||||
$results['unmodified'][] = $file;
|
$results['unmodified'][] = $path;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$results['bad_files'][] = $file;
|
$results['bad_files'][] = $path;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($syncCommand) {
|
if ($syncCommand) {
|
||||||
$syncCommand->advanceProgressBar();
|
$syncCommand->advanceProgressBar();
|
||||||
$syncCommand->logSyncStatusToConsole($file->getPath(), $result, $file->getSyncError());
|
$syncCommand->logSyncStatusToConsole($path, $result, $this->fileSynchronizer->getSyncError());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete non-existing songs.
|
// Delete non-existing songs.
|
||||||
$hashes = array_map(function (File $file): string {
|
$hashes = array_map(function (string $path): string {
|
||||||
return $this->helperService->getFileHash($file->getPath());
|
return $this->helperService->getFileHash($path);
|
||||||
}, array_merge($results['unmodified'], $results['success']));
|
}, array_merge($results['unmodified'], $results['success']));
|
||||||
|
|
||||||
Song::deleteWhereIDsNotIn($hashes);
|
Song::deleteWhereIDsNotIn($hashes);
|
||||||
|
@ -140,7 +146,7 @@ class MediaSyncService
|
||||||
public function gatherFiles(string $path): array
|
public function gatherFiles(string $path): array
|
||||||
{
|
{
|
||||||
return iterator_to_array(
|
return iterator_to_array(
|
||||||
Finder::create()
|
$this->finder->create()
|
||||||
->ignoreUnreadableDirs()
|
->ignoreUnreadableDirs()
|
||||||
->ignoreDotFiles((bool) config('koel.ignore_dot_files')) // https://github.com/phanan/koel/issues/450
|
->ignoreDotFiles((bool) config('koel.ignore_dot_files')) // https://github.com/phanan/koel/issues/450
|
||||||
->files()
|
->files()
|
||||||
|
@ -173,30 +179,16 @@ class MediaSyncService
|
||||||
|
|
||||||
// If the file has been deleted...
|
// If the file has been deleted...
|
||||||
if ($record->isDeleted()) {
|
if ($record->isDeleted()) {
|
||||||
// ...and it has a record in our database, remove it.
|
$this->handleDeletedFileRecord($path);
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Otherwise, it's a new or changed file. Try to sync it in.
|
// 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()) {
|
elseif ($record->isNewOrModified()) {
|
||||||
$result = (new File($path))->sync($this->tags);
|
$this->handleNewOrModifiedFileRecord($path);
|
||||||
Log::info($result === File::SYNC_RESULT_SUCCESS ? "Synchronized $path" : "Invalid file $path");
|
|
||||||
|
|
||||||
event(new LibraryChanged());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync a directory's watch record.
|
* Sync a directory's watch record.
|
||||||
*
|
|
||||||
* @throws getid3_exception
|
|
||||||
*/
|
*/
|
||||||
private function syncDirectoryRecord(WatchRecordInterface $record): void
|
private function syncDirectoryRecord(WatchRecordInterface $record): void
|
||||||
{
|
{
|
||||||
|
@ -204,21 +196,9 @@ class MediaSyncService
|
||||||
Log::info("'$path' is a directory.");
|
Log::info("'$path' is a directory.");
|
||||||
|
|
||||||
if ($record->isDeleted()) {
|
if ($record->isDeleted()) {
|
||||||
// The directory is removed. We remove all songs in it.
|
$this->handleDeletedDirectoryRecord($path);
|
||||||
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.");
|
|
||||||
}
|
|
||||||
} elseif ($record->isNewOrModified()) {
|
} elseif ($record->isNewOrModified()) {
|
||||||
foreach ($this->gatherFiles($path) as $file) {
|
$this->handleNewOrModifiedDirectoryRecord($path);
|
||||||
(new File($file))->sync($this->tags);
|
|
||||||
}
|
|
||||||
Log::info("Synced all song(s) under $path");
|
|
||||||
|
|
||||||
event(new LibraryChanged());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,14 +219,6 @@ class MediaSyncService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a unique hash for a file path.
|
|
||||||
*/
|
|
||||||
public function getFileHash(string $path): string
|
|
||||||
{
|
|
||||||
return File::getHash($path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tidy up the library by deleting empty albums and artists.
|
* Tidy up the library by deleting empty albums and artists.
|
||||||
*
|
*
|
||||||
|
@ -254,21 +226,74 @@ class MediaSyncService
|
||||||
*/
|
*/
|
||||||
public function tidy(): void
|
public function tidy(): void
|
||||||
{
|
{
|
||||||
$inUseAlbums = Song::select('album_id')
|
$inUseAlbums = $this->albumRepository->getNonEmptyAlbumIds();
|
||||||
->groupBy('album_id')
|
|
||||||
->get()
|
|
||||||
->pluck('album_id')
|
|
||||||
->toArray();
|
|
||||||
$inUseAlbums[] = Album::UNKNOWN_ID;
|
$inUseAlbums[] = Album::UNKNOWN_ID;
|
||||||
Album::deleteWhereIDsNotIn($inUseAlbums);
|
Album::deleteWhereIDsNotIn($inUseAlbums);
|
||||||
|
|
||||||
$inUseArtists = Song::select('artist_id')
|
$inUseArtists = $this->artistRepository->getNonEmptyArtistIds();
|
||||||
->groupBy('artist_id')
|
|
||||||
->get()
|
|
||||||
->pluck('artist_id')
|
|
||||||
->toArray();
|
|
||||||
$inUseArtists[] = Artist::UNKNOWN_ID;
|
$inUseArtists[] = Artist::UNKNOWN_ID;
|
||||||
$inUseArtists[] = Artist::VARIOUS_ID;
|
$inUseArtists[] = Artist::VARIOUS_ID;
|
||||||
Artist::deleteWhereIDsNotIn(array_filter($inUseArtists));
|
Artist::deleteWhereIDsNotIn(array_filter($inUseArtists));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,12 @@ use App\Events\LibraryChanged;
|
||||||
use App\Libraries\WatchRecord\InotifyWatchRecord;
|
use App\Libraries\WatchRecord\InotifyWatchRecord;
|
||||||
use App\Models\Album;
|
use App\Models\Album;
|
||||||
use App\Models\Artist;
|
use App\Models\Artist;
|
||||||
use App\Models\File;
|
|
||||||
use App\Models\Song;
|
use App\Models\Song;
|
||||||
|
use App\Services\FileSynchronizer;
|
||||||
use App\Services\MediaSyncService;
|
use App\Services\MediaSyncService;
|
||||||
use Exception;
|
use Exception;
|
||||||
use getID3;
|
use getID3;
|
||||||
use getid3_exception;
|
|
||||||
use Illuminate\Foundation\Testing\WithoutMiddleware;
|
use Illuminate\Foundation\Testing\WithoutMiddleware;
|
||||||
use Mockery as m;
|
|
||||||
|
|
||||||
class MediaSyncTest extends TestCase
|
class MediaSyncTest extends TestCase
|
||||||
{
|
{
|
||||||
|
@ -25,16 +23,9 @@ class MediaSyncTest extends TestCase
|
||||||
public function setUp()
|
public function setUp()
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
$this->mediaService = app(MediaSyncService::class);
|
$this->mediaService = app(MediaSyncService::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function tearDown()
|
|
||||||
{
|
|
||||||
m::close();
|
|
||||||
parent::tearDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @test
|
* @test
|
||||||
*
|
*
|
||||||
|
@ -244,14 +235,10 @@ class MediaSyncTest extends TestCase
|
||||||
$this->notSeeInDatabase('songs', ['path' => $this->mediaPath.'/subdir/back-in-black.mp3']);
|
$this->notSeeInDatabase('songs', ['path' => $this->mediaPath.'/subdir/back-in-black.mp3']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @test */
|
||||||
* @test
|
|
||||||
*
|
|
||||||
* @throws getid3_exception
|
|
||||||
*/
|
|
||||||
public function html_entities_in_tags_are_recognized_and_saved_properly()
|
public function html_entities_in_tags_are_recognized_and_saved_properly()
|
||||||
{
|
{
|
||||||
$getID3 = m::mock(getID3::class, [
|
$this->mockIocDependency(getID3::class, [
|
||||||
'analyze' => [
|
'analyze' => [
|
||||||
'tags' => [
|
'tags' => [
|
||||||
'id3v2' => [
|
'id3v2' => [
|
||||||
|
@ -265,11 +252,13 @@ class MediaSyncTest extends TestCase
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$info = (new File(__DIR__.'/songs/blank.mp3', $getID3))->getInfo();
|
/** @var FileSynchronizer $fileSynchronizer */
|
||||||
|
$fileSynchronizer = app(FileSynchronizer::class);
|
||||||
|
$info = $fileSynchronizer->setFile(__DIR__.'/songs/blank.mp3')->getFileInfo();
|
||||||
|
|
||||||
$this->assertEquals('佐倉綾音 Unknown', $info['artist']);
|
self::assertEquals('佐倉綾音 Unknown', $info['artist']);
|
||||||
$this->assertEquals('小岩井こ Random', $info['album']);
|
self::assertEquals('小岩井こ Random', $info['album']);
|
||||||
$this->assertEquals('水谷広実', $info['title']);
|
self::assertEquals('水谷広実', $info['title']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Integration\Models;
|
|
||||||
|
|
||||||
use App\Models\File;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class FileTest extends TestCase
|
|
||||||
{
|
|
||||||
/** @test */
|
|
||||||
public function file_info_is_retrieved_correctly()
|
|
||||||
{
|
|
||||||
$file = new File(__DIR__.'/../../songs/full.mp3');
|
|
||||||
$info = $file->getInfo();
|
|
||||||
|
|
||||||
$expectedData = [
|
|
||||||
'artist' => 'Koel',
|
|
||||||
'album' => 'Koel Testing Vol. 1',
|
|
||||||
'compilation' => false,
|
|
||||||
'title' => 'Amet',
|
|
||||||
'track' => 5,
|
|
||||||
'disc' => 3,
|
|
||||||
'lyrics' => "Foo\rbar",
|
|
||||||
'cover' => [
|
|
||||||
'data' => file_get_contents(__DIR__.'/../../blobs/cover.png'),
|
|
||||||
'image_mime' => 'image/png',
|
|
||||||
'image_width' => 512,
|
|
||||||
'image_height' => 512,
|
|
||||||
'imagetype' => 'PNG',
|
|
||||||
'picturetype' => 'Other',
|
|
||||||
'description' => '',
|
|
||||||
'datalength' => 7627,
|
|
||||||
],
|
|
||||||
'path' => __DIR__.'/../../songs/full.mp3',
|
|
||||||
'mtime' => filemtime(__DIR__.'/../../songs/full.mp3'),
|
|
||||||
'albumartist' => '',
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->assertArraySubset($expectedData, $info);
|
|
||||||
$this->assertEquals(10.083, $info['length'], '', 0.001);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @test */
|
|
||||||
public function song_without_a_title_tag_has_file_name_as_the_title()
|
|
||||||
{
|
|
||||||
$file = new File(__DIR__.'/../../songs/blank.mp3');
|
|
||||||
$this->assertSame('blank', $file->getInfo()['title']);
|
|
||||||
}
|
|
||||||
}
|
|
56
tests/Integration/Services/FileSynchronizerTest.php
Normal file
56
tests/Integration/Services/FileSynchronizerTest.php
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Integration\Services;
|
||||||
|
|
||||||
|
use App\Services\FileSynchronizer;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class FileSynchronizerTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @var FileSynchronizer */
|
||||||
|
private $fileSynchronizer;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->fileSynchronizer = app(FileSynchronizer::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetFileInfo()
|
||||||
|
{
|
||||||
|
$info = $this->fileSynchronizer->setFile(__DIR__ . '/../../songs/full.mp3')->getFileInfo();
|
||||||
|
|
||||||
|
$expectedData = [
|
||||||
|
'artist' => 'Koel',
|
||||||
|
'album' => 'Koel Testing Vol. 1',
|
||||||
|
'compilation' => false,
|
||||||
|
'title' => 'Amet',
|
||||||
|
'track' => 5,
|
||||||
|
'disc' => 3,
|
||||||
|
'lyrics' => "Foo\rbar",
|
||||||
|
'cover' => [
|
||||||
|
'data' => file_get_contents(__DIR__ . '/../../blobs/cover.png'),
|
||||||
|
'image_mime' => 'image/png',
|
||||||
|
'image_width' => 512,
|
||||||
|
'image_height' => 512,
|
||||||
|
'imagetype' => 'PNG',
|
||||||
|
'picturetype' => 'Other',
|
||||||
|
'description' => '',
|
||||||
|
'datalength' => 7627,
|
||||||
|
],
|
||||||
|
'path' => __DIR__ . '/../../songs/full.mp3',
|
||||||
|
'mtime' => filemtime(__DIR__ . '/../../songs/full.mp3'),
|
||||||
|
'albumartist' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
self::assertArraySubset($expectedData, $info);
|
||||||
|
self::assertEquals(10.083, $info['length'], '', 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function song_without_a_title_tag_has_file_name_as_the_title()
|
||||||
|
{
|
||||||
|
$this->fileSynchronizer->setFile(__DIR__ . '/../../songs/blank.mp3');
|
||||||
|
self::assertSame('blank', $this->fileSynchronizer->getFileInfo()['title']);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Unit\Models;
|
|
||||||
|
|
||||||
use App\Models\File;
|
|
||||||
use SplFileInfo;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class FileTest extends TestCase
|
|
||||||
{
|
|
||||||
/** @test */
|
|
||||||
public function it_can_be_instantiated()
|
|
||||||
{
|
|
||||||
$file = new File(__DIR__.'/../songs/full.mp3');
|
|
||||||
$this->assertInstanceOf(File::class, $file);
|
|
||||||
$file = new File(new SplFileInfo(__DIR__.'/../songs/full.mp3'));
|
|
||||||
$this->assertInstanceOf(File::class, $file);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue