Rewrite the file synchronization handling logic

This commit is contained in:
Phan An 2018-08-29 16:41:24 +07:00
parent 96bbbee4a7
commit 1558062428
13 changed files with 325 additions and 351 deletions

View file

@ -4,6 +4,7 @@ namespace App\Console\Commands;
use App\Models\Setting;
use App\Models\User;
use App\Repositories\SettingRepository;
use App\Services\MediaCacheService;
use Exception;
use Illuminate\Console\Command;
@ -22,9 +23,11 @@ class InitCommand extends Command
private $dotenvEditor;
private $hash;
private $db;
private $settingRepository;
public function __construct(
MediaCacheService $mediaCacheService,
SettingRepository $settingRepository,
Artisan $artisan,
Hash $hash,
DotenvEditor $dotenvEditor,
@ -37,6 +40,7 @@ class InitCommand extends Command
$this->dotenvEditor = $dotenvEditor;
$this->hash = $hash;
$this->db = $db;
$this->settingRepository = $settingRepository;
}
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`.');
if (Setting::get('media_path')) {
if ($this->settingRepository->getMediaPath()) {
$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
{
if (!Setting::get('media_path')) {
if ($this->settingRepository->getMediaPath()) {
return;
}

View file

@ -3,8 +3,9 @@
namespace App\Console\Commands;
use App\Libraries\WatchRecord\InotifyWatchRecord;
use App\Models\File;
use App\Models\Setting;
use App\Repositories\SettingRepository;
use App\Services\FileSynchronizer;
use App\Services\MediaSyncService;
use Exception;
use Illuminate\Console\Command;
@ -18,21 +19,22 @@ class SyncMediaCommand extends Command
{--force : Force re-syncing even unchanged files}';
protected $description = 'Sync songs found in configured directory against the database.';
private $ignored = 0;
private $invalid = 0;
private $synced = 0;
private $mediaSyncService;
private $settingRepository;
/**
* @var ProgressBar
*/
private $progressBar;
public function __construct(MediaSyncService $mediaSyncService)
public function __construct(MediaSyncService $mediaSyncService, SettingRepository $settingRepository)
{
parent::__construct();
$this->mediaSyncService = $mediaSyncService;
$this->settingRepository = $settingRepository;
}
/**
@ -40,19 +42,7 @@ class SyncMediaCommand extends Command
*/
public function handle(): void
{
if (!Setting::get('media_path')) {
$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.');
}
}
$this->ensureMediaPath();
if (!$record = $this->argument('record')) {
$this->syncAll();
@ -113,23 +103,15 @@ class SyncMediaCommand extends Command
{
$name = basename($path);
if ($result === File::SYNC_RESULT_UNMODIFIED) {
if ($this->option('verbose')) {
$this->line("$name has no changes  ignoring");
}
if ($result === FileSynchronizer::SYNC_RESULT_UNMODIFIED) {
$this->ignored++;
} elseif ($result === File::SYNC_RESULT_BAD_FILE) {
} elseif ($result === FileSynchronizer::SYNC_RESULT_BAD_FILE) {
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++;
} else {
if ($this->option('verbose')) {
$this->info("$name synced");
}
$this->synced++;
}
}
@ -139,8 +121,28 @@ class SyncMediaCommand extends Command
$this->progressBar = $this->getOutput()->createProgressBar($max);
}
public function advanceProgressBar()
public function advanceProgressBar(): void
{
$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.');
}
}
}

View file

@ -23,6 +23,7 @@ use Illuminate\Support\Collection;
* @property int album_id
* @property int id
* @property int artist_id
* @property int mtime
*/
class Song extends Model
{

View file

@ -3,6 +3,7 @@
namespace App\Repositories;
use App\Models\Album;
use App\Models\Song;
class AlbumRepository extends AbstractRepository
{
@ -10,4 +11,15 @@ class AlbumRepository extends AbstractRepository
{
return Album::class;
}
public function getNonEmptyAlbumIds(): array
{
$ids = Song::select('album_id')
->groupBy('album_id')
->get()
->pluck('album_id')
->toArray();
return $ids;
}
}

View file

@ -3,6 +3,7 @@
namespace App\Repositories;
use App\Models\Artist;
use App\Models\Song;
class ArtistRepository extends AbstractRepository
{
@ -10,4 +11,13 @@ class ArtistRepository extends AbstractRepository
{
return Artist::class;
}
public function getNonEmptyArtistIds(): array
{
return Song::select('artist_id')
->groupBy('artist_id')
->get()
->pluck('artist_id')
->toArray();
}
}

View file

@ -5,8 +5,6 @@ namespace App\Services;
interface ApiConsumerInterface
{
public function getEndpoint(): string;
public function getKey(): ?string;
public function getSecret(): ?string;
}

View file

@ -1,129 +1,113 @@
<?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\Services\HelperService;
use App\Services\MediaMetadataService;
use Cache;
use Exception;
use getID3;
use getid3_exception;
use getid3_lib;
use InvalidArgumentException;
use Illuminate\Contracts\Cache\Repository as Cache;
use SplFileInfo;
use Symfony\Component\Finder\Finder;
class File
class FileSynchronizer
{
const SYNC_RESULT_SUCCESS = 1;
const SYNC_RESULT_BAD_FILE = 2;
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.
*
* @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(
$path,
?getID3 $getID3 = null,
?MediaMetadataService $mediaMetadataService = null,
?HelperService $helperService = null,
?SongRepository $songRepository = null
) {
getID3 $getID3,
MediaMetadataService $mediaMetadataService,
HelperService $helperService,
SongRepository $songRepository,
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->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.
try {
$this->mtime = $this->splFileInfo->getMTime();
$this->fileModifiedTime = $this->splFileInfo->getMTime();
} catch (Exception $e) {
// Not worth logging the error. Just use current stamp for mtime.
$this->mtime = time();
$this->fileModifiedTime = time();
}
$this->path = $this->splFileInfo->getPathname();
$this->hash = $this->helperService->getFileHash($this->path);
$this->song = $this->songRepository->getOneById($this->hash);
$this->filePath = $this->splFileInfo->getPathname();
$this->fileHash = $this->helperService->getFileHash($this->filePath);
$this->song = $this->songRepository->getOneById($this->fileHash);
$this->syncError = null;
return $this;
}
/**
* 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'])) {
$this->syncError = isset($info['error']) ? $info['error'][0] : 'No playtime found';
@ -154,14 +138,14 @@ class File
'artist' => '',
'album' => '',
'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'],
'track' => (int) $track,
'disc' => (int) array_get($info, 'comments.part_of_a_set.0', 1),
'lyrics' => '',
'cover' => array_get($info, 'comments.picture', [null])[0],
'path' => $this->path,
'mtime' => $this->mtime,
'path' => $this->filePath,
'mtime' => $this->fileModifiedTime,
];
if (!$comments = array_get($info, 'comments_html')) {
@ -195,7 +179,6 @@ class File
return $props;
}
/**
* 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)
{
// 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;
}
// If the file is invalid, don't do anything.
if (!$info = $this->getInfo()) {
if (!$info = $this->getFileInfo()) {
return self::SYNC_RESULT_BAD_FILE;
}
// 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;
}
if ($this->isChanged() || $force) {
if ($this->isFileChanged() || $force) {
// 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.
// 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['album_id'] = $album->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;
}
@ -278,7 +261,7 @@ class File
*
* @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 ($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.
* 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
{
// 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(
Finder::create()
$this->finder->create()
->depth(0)
->ignoreUnreadableDirs()
->files()
->followLinks()
->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;
}
}

View file

@ -7,13 +7,13 @@ use App\Events\LibraryChanged;
use App\Libraries\WatchRecord\WatchRecordInterface;
use App\Models\Album;
use App\Models\Artist;
use App\Models\File;
use App\Models\Setting;
use App\Models\Song;
use App\Repositories\AlbumRepository;
use App\Repositories\ArtistRepository;
use App\Repositories\SettingRepository;
use App\Repositories\SongRepository;
use Exception;
use getID3;
use getid3_exception;
use Log;
use SplFileInfo;
use Symfony\Component\Finder\Finder;
@ -42,15 +42,30 @@ class MediaSyncService
private $mediaMetadataService;
private $songRepository;
private $helperService;
private $fileSynchronizer;
private $finder;
private $artistRepository;
private $albumRepository;
private $settingRepository;
public function __construct(
MediaMetadataService $mediaMetadataService,
SongRepository $songRepository,
HelperService $helperService
ArtistRepository $artistRepository,
AlbumRepository $albumRepository,
SettingRepository $settingRepository,
HelperService $helperService,
FileSynchronizer $fileSynchronizer,
Finder $finder
) {
$this->mediaMetadataService = $mediaMetadataService;
$this->songRepository = $songRepository;
$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,
array $tags = [],
bool $force = false,
SyncMediaCommand $syncCommand = null
?SyncMediaCommand $syncCommand = null
): 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');
}
$mediaPath = $mediaPath ?: Setting::get('media_path');
$this->setSystemRequirements();
$this->setTags($tags);
$results = [
@ -94,34 +101,33 @@ class MediaSyncService
'unmodified' => [],
];
$getID3 = new getID3();
$songPaths = $this->gatherFiles($mediaPath);
$songPaths = $this->gatherFiles($mediaPath ?: $this->settingRepository->getMediaPath());
$syncCommand && $syncCommand->createProgressBar(count($songPaths));
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)) {
case File::SYNC_RESULT_SUCCESS:
$results['success'][] = $file;
switch ($result) {
case FileSynchronizer::SYNC_RESULT_SUCCESS:
$results['success'][] = $path;
break;
case File::SYNC_RESULT_UNMODIFIED:
$results['unmodified'][] = $file;
case FileSynchronizer::SYNC_RESULT_UNMODIFIED:
$results['unmodified'][] = $path;
break;
default:
$results['bad_files'][] = $file;
$results['bad_files'][] = $path;
break;
}
if ($syncCommand) {
$syncCommand->advanceProgressBar();
$syncCommand->logSyncStatusToConsole($file->getPath(), $result, $file->getSyncError());
$syncCommand->logSyncStatusToConsole($path, $result, $this->fileSynchronizer->getSyncError());
}
}
// Delete non-existing songs.
$hashes = array_map(function (File $file): string {
return $this->helperService->getFileHash($file->getPath());
$hashes = array_map(function (string $path): string {
return $this->helperService->getFileHash($path);
}, array_merge($results['unmodified'], $results['success']));
Song::deleteWhereIDsNotIn($hashes);
@ -140,7 +146,7 @@ class MediaSyncService
public function gatherFiles(string $path): array
{
return iterator_to_array(
Finder::create()
$this->finder->create()
->ignoreUnreadableDirs()
->ignoreDotFiles((bool) config('koel.ignore_dot_files')) // https://github.com/phanan/koel/issues/450
->files()
@ -173,30 +179,16 @@ class MediaSyncService
// If the file has been deleted...
if ($record->isDeleted()) {
// ...and it has a record in our database, remove it.
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.");
}
$this->handleDeletedFileRecord($path);
}
// 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");
event(new LibraryChanged());
$this->handleNewOrModifiedFileRecord($path);
}
}
/**
* Sync a directory's watch record.
*
* @throws getid3_exception
*/
private function syncDirectoryRecord(WatchRecordInterface $record): void
{
@ -204,21 +196,9 @@ class MediaSyncService
Log::info("'$path' is a directory.");
if ($record->isDeleted()) {
// The directory is removed. We remove all songs in it.
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.");
}
$this->handleDeletedDirectoryRecord($path);
} elseif ($record->isNewOrModified()) {
foreach ($this->gatherFiles($path) as $file) {
(new File($file))->sync($this->tags);
}
Log::info("Synced all song(s) under $path");
event(new LibraryChanged());
$this->handleNewOrModifiedDirectoryRecord($path);
}
}
@ -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.
*
@ -254,21 +226,74 @@ class MediaSyncService
*/
public function tidy(): void
{
$inUseAlbums = Song::select('album_id')
->groupBy('album_id')
->get()
->pluck('album_id')
->toArray();
$inUseAlbums = $this->albumRepository->getNonEmptyAlbumIds();
$inUseAlbums[] = Album::UNKNOWN_ID;
Album::deleteWhereIDsNotIn($inUseAlbums);
$inUseArtists = Song::select('artist_id')
->groupBy('artist_id')
->get()
->pluck('artist_id')
->toArray();
$inUseArtists = $this->artistRepository->getNonEmptyArtistIds();
$inUseArtists[] = Artist::UNKNOWN_ID;
$inUseArtists[] = Artist::VARIOUS_ID;
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());
}
}

View file

@ -19,7 +19,7 @@ class XAccelRedirectStreamer extends Streamer implements DirectStreamerInterface
header('X-Media-Root: '.Setting::get('media_path'));
header("X-Accel-Redirect: /media/$relativePath");
header("Content-Type: {$this->contentType}");
header('Content-Disposition: inline; filename="'.basename($this->song->path).'"');
header('Content-Disposition: inline; filename="' . basename($this->song->path) . '"');
exit;
}

View file

@ -6,14 +6,12 @@ use App\Events\LibraryChanged;
use App\Libraries\WatchRecord\InotifyWatchRecord;
use App\Models\Album;
use App\Models\Artist;
use App\Models\File;
use App\Models\Song;
use App\Services\FileSynchronizer;
use App\Services\MediaSyncService;
use Exception;
use getID3;
use getid3_exception;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Mockery as m;
class MediaSyncTest extends TestCase
{
@ -25,16 +23,9 @@ class MediaSyncTest extends TestCase
public function setUp()
{
parent::setUp();
$this->mediaService = app(MediaSyncService::class);
}
protected function tearDown()
{
m::close();
parent::tearDown();
}
/**
* @test
*
@ -244,14 +235,10 @@ class MediaSyncTest extends TestCase
$this->notSeeInDatabase('songs', ['path' => $this->mediaPath.'/subdir/back-in-black.mp3']);
}
/**
* @test
*
* @throws getid3_exception
*/
/** @test */
public function html_entities_in_tags_are_recognized_and_saved_properly()
{
$getID3 = m::mock(getID3::class, [
$this->mockIocDependency(getID3::class, [
'analyze' => [
'tags' => [
'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']);
$this->assertEquals('小岩井こ Random', $info['album']);
$this->assertEquals('水谷広実', $info['title']);
self::assertEquals('佐倉綾音 Unknown', $info['artist']);
self::assertEquals('小岩井こ Random', $info['album']);
self::assertEquals('水谷広実', $info['title']);
}
/**

View file

@ -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']);
}
}

View 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']);
}
}

View file

@ -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);
}
}