feat: better supports for compilation when scanning

This commit is contained in:
Phan An 2022-07-05 15:47:26 +02:00
parent 5d71de93ae
commit 58659c2e30
No known key found for this signature in database
GPG key ID: A81E4477F0BB6FDC
10 changed files with 180 additions and 270 deletions

View file

@ -13,7 +13,7 @@ class SyncCommand extends Command
{ {
protected $signature = 'koel:sync protected $signature = 'koel:sync
{record? : A single watch record. Consult Wiki for more info.} {record? : A single watch record. Consult Wiki for more info.}
{--tags= : The comma-separated tags to sync into the database} {--excludes= : The comma-separated tags to excludes from syncing}
{--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.';
@ -42,7 +42,7 @@ class SyncCommand extends Command
return; return;
} }
$this->syngle($record); $this->syncSingleRecord($record);
} }
/** /**
@ -52,12 +52,12 @@ class SyncCommand extends Command
{ {
$this->info('Syncing media from ' . Setting::get('media_path') . PHP_EOL); $this->info('Syncing media from ' . Setting::get('media_path') . PHP_EOL);
// Get the tags to sync. // The excluded tags.
// Notice that this is only meaningful for existing records. // Notice that this is only meaningful for existing records.
// New records will have every applicable field sync'ed in. // New records will have every applicable field synced in.
$tags = $this->option('tags') ? explode(',', $this->option('tags')) : []; $excludes = $this->option('excludes') ? explode(',', $this->option('excludes')) : [];
$this->mediaSyncService->sync(null, $tags, $this->option('force'), $this); $this->mediaSyncService->sync(null, $excludes, $this->option('force'), $this);
$this->output->writeln( $this->output->writeln(
PHP_EOL . PHP_EOL PHP_EOL . PHP_EOL
@ -68,8 +68,6 @@ class SyncCommand extends Command
} }
/** /**
* SYNc a sinGLE file or directory. See my awesome pun?
*
* @param string $record The watch record. * @param string $record The watch record.
* As of current we only support inotifywait. * As of current we only support inotifywait.
* Some examples: * Some examples:
@ -79,7 +77,7 @@ class SyncCommand extends Command
* *
* @see http://man7.org/linux/man-pages/man1/inotifywait.1.html * @see http://man7.org/linux/man-pages/man1/inotifywait.1.html
*/ */
public function syngle(string $record): void public function syncSingleRecord(string $record): void
{ {
$this->mediaSyncService->syncByWatchRecord(new InotifyWatchRecord($record)); $this->mediaSyncService->syncByWatchRecord(new InotifyWatchRecord($record));
} }

View file

@ -44,7 +44,6 @@ class AlbumRepository extends AbstractRepository
public function getByIds(array $ids, ?User $scopedUser = null): Collection public function getByIds(array $ids, ?User $scopedUser = null): Collection
{ {
return Album::withMeta($scopedUser ?? $this->auth->user()) return Album::withMeta($scopedUser ?? $this->auth->user())
->isStandard()
->whereIn('albums.id', $ids) ->whereIn('albums.id', $ids)
->get(); ->get();
} }

View file

@ -6,10 +6,10 @@ use App\Models\Album;
use App\Models\Artist; use App\Models\Artist;
use App\Models\Song; use App\Models\Song;
use App\Repositories\SongRepository; use App\Repositories\SongRepository;
use App\Values\SongScanInformation;
use getID3; use getID3;
use getid3_lib;
use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Contracts\Cache\Repository as Cache;
use InvalidArgumentException; use Illuminate\Support\Arr;
use SplFileInfo; use SplFileInfo;
use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Finder;
use Throwable; use Throwable;
@ -20,12 +20,6 @@ class FileSynchronizer
public const SYNC_RESULT_BAD_FILE = 2; public const SYNC_RESULT_BAD_FILE = 2;
public const SYNC_RESULT_UNMODIFIED = 3; public const SYNC_RESULT_UNMODIFIED = 3;
private getID3 $getID3;
private MediaMetadataService $mediaMetadataService;
private Helper $helper;
private SongRepository $songRepository;
private Cache $cache;
private Finder $finder;
private ?int $fileModifiedTime = null; private ?int $fileModifiedTime = null;
private ?string $filePath = null; private ?string $filePath = null;
@ -43,35 +37,19 @@ class FileSynchronizer
private ?string $syncError; private ?string $syncError;
public function __construct( public function __construct(
getID3 $getID3, private getID3 $getID3,
MediaMetadataService $mediaMetadataService, private MediaMetadataService $mediaMetadataService,
Helper $helper, private Helper $helper,
SongRepository $songRepository, private SongRepository $songRepository,
Cache $cache, private Cache $cache,
Finder $finder private Finder $finder
) { ) {
$this->getID3 = $getID3;
$this->mediaMetadataService = $mediaMetadataService;
$this->helper = $helper;
$this->songRepository = $songRepository;
$this->cache = $cache;
$this->finder = $finder;
} }
/** @param string|SplFileInfo $path */ public function setFile(string|SplFileInfo $path): self
public function setFile($path): self
{ {
$splFileInfo = null;
$splFileInfo = $path instanceof SplFileInfo ? $path : new SplFileInfo($path); $splFileInfo = $path instanceof SplFileInfo ? $path : new SplFileInfo($path);
// Workaround for #344, where getMTime() fails for certain files with Unicode names on Windows.
try {
$this->fileModifiedTime = $splFileInfo->getMTime();
} catch (Throwable $e) {
// Not worth logging the error. Just use current stamp for mtime.
$this->fileModifiedTime = time();
}
$this->filePath = $splFileInfo->getPathname(); $this->filePath = $splFileInfo->getPathname();
$this->fileHash = $this->helper->getFileHash($this->filePath); $this->fileHash = $this->helper->getFileHash($this->filePath);
$this->song = $this->songRepository->getOneById($this->fileHash); // @phpstan-ignore-line $this->song = $this->songRepository->getOneById($this->fileHash); // @phpstan-ignore-line
@ -80,121 +58,55 @@ class FileSynchronizer
return $this; return $this;
} }
/** public function getFileInfo(): ?SongScanInformation
* Get all applicable info from the file.
*
* @return array<mixed>
*/
public function getFileInfo(): array
{ {
$info = $this->getID3->analyze($this->filePath); $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';
return []; return null;
} }
// Copy the available tags over to comment. return SongScanInformation::fromGetId3Info($info);
// This is a helper from getID3, though it doesn't really work well.
// We'll still prefer getting ID3v2 tags directly later.
getid3_lib::CopyTagsToComments($info);
$props = [
'artist' => '',
'album' => '',
'albumartist' => '',
'compilation' => false,
'title' => basename($this->filePath, '.' . pathinfo($this->filePath, PATHINFO_EXTENSION)),
'length' => $info['playtime_seconds'],
'track' => $this->getTrackNumberFromInfo($info),
'disc' => (int) array_get($info, 'comments.part_of_a_set.0', 1),
'lyrics' => '',
'cover' => array_get($info, 'comments.picture', [null])[0],
'path' => $this->filePath,
'mtime' => $this->fileModifiedTime,
];
$comments = array_get($info, 'comments_html');
if (!$comments) {
return $props;
}
$this->gatherPropsFromTags($info, $comments, $props);
$props['compilation'] = (bool) $props['compilation'] || $this->isCompilation($props);
return $props;
} }
/** /**
* Sync the song with all available media info against the database. * Sync the song with all available media info into the database.
* *
* @param array<string> $tags The (selective) tags to sync (if the song exists) * @param array<string> $excludes The tags to exclude (only taken into account if the song already exists)
* @param bool $force Whether to force syncing, even if the file is unchanged * @param bool $force Whether to force syncing, even if the file is unchanged
*/ */
public function sync(array $tags, bool $force = false): int public function sync(array $excludes = [], bool $force = false): int
{ {
if (!$this->isFileNewOrChanged() && !$force) { if (!$this->isFileNewOrChanged() && !$force) {
return self::SYNC_RESULT_UNMODIFIED; return self::SYNC_RESULT_UNMODIFIED;
} }
$info = $this->getFileInfo(); $info = $this->getFileInfo()?->toArray();
if (!$info) { if (!$info) {
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. if (!$this->isFileNew()) {
if ($this->isFileNew()) { foreach ($excludes as $tag) {
$force = false; unset($info[$tag]);
}
} }
if ($this->isFileChanged() || $force) { $artist = Arr::get($info, 'artist') ? Artist::getOrCreate($info['artist']) : $this->song->artist;
// This is a changed file, or the user is forcing updates. $albumArtist = Arr::get($info, 'albumartist') ? Artist::getOrCreate($info['albumartist']) : $artist;
// In such a case, the user must have specified a list of tags to sync. $album = Arr::get($info, 'album') ? Album::getOrCreate($albumArtist, $info['album']) : $this->song->album;
// A sample command could be: ./artisan koel:sync --force --tags=artist,album,lyrics
// We cater for these tags by removing those not specified.
// There's a special case with 'album' though.
// If 'compilation' tag is specified, 'album' must be counted in as well.
// But if 'album' isn't specified, we don't want to update normal albums.
// This variable is to keep track of this state.
$changeCompilationAlbumOnly = false;
if (in_array('compilation', $tags, true) && !in_array('album', $tags, true)) {
$tags[] = 'album';
$changeCompilationAlbumOnly = true;
}
$info = array_intersect_key($info, array_flip($tags));
// If the "artist" tag is specified, use it.
// Otherwise, re-use the existing model value.
$artist = isset($info['artist']) ? Artist::getOrCreate($info['artist']) : $this->song->album->artist;
// If the "album" tag is specified, use it.
// Otherwise, re-use the existing model value.
if (isset($info['album'])) {
$album = $changeCompilationAlbumOnly
? $this->song->album
: Album::getOrCreate($artist, $info['album'], array_get($info, 'compilation'));
} else {
$album = $this->song->album;
}
} else {
// The file is newly added.
$artist = Artist::getOrCreate($info['artist']);
$album = Album::getOrCreate($artist, $info['album'], array_get($info, 'compilation'));
}
if (!$album->has_cover) { if (!$album->has_cover) {
$this->generateAlbumCover($album, array_get($info, 'cover')); $this->tryGenerateAlbumCover($album, Arr::get($info, 'cover', []));
} }
$data = array_except($info, ['artist', 'albumartist', 'album', 'cover', 'compilation']); $data = Arr::except($info, ['album', 'artist', 'albumartist', 'cover']);
$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->fileHash], $data); $this->song = Song::updateOrCreate(['id' => $this->fileHash], $data);
return self::SYNC_RESULT_SUCCESS; return self::SYNC_RESULT_SUCCESS;
@ -205,24 +117,27 @@ class FileSynchronizer
* *
* @param array<mixed>|null $coverData * @param array<mixed>|null $coverData
*/ */
private function generateAlbumCover(Album $album, ?array $coverData): void private function tryGenerateAlbumCover(Album $album, ?array $coverData): void
{ {
// If the album has no cover, we try to get the cover image from existing tag data try {
if ($coverData) { // If the album has no cover, we try to get the cover image from existing tag data
$extension = explode('/', $coverData['image_mime']); if ($coverData) {
$extension = $extension[1] ?? 'png'; $extension = explode('/', $coverData['image_mime']);
$extension = $extension[1] ?? 'png';
$this->mediaMetadataService->writeAlbumCover($album, $coverData['data'], $extension); $this->mediaMetadataService->writeAlbumCover($album, $coverData['data'], $extension);
return; return;
} }
// Or, if there's a cover image under the same directory, use it. // Or, if there's a cover image under the same directory, use it.
$cover = $this->getCoverFileUnderSameDirectory(); $cover = $this->getCoverFileUnderSameDirectory();
if ($cover) { if ($cover) {
$extension = pathinfo($cover, PATHINFO_EXTENSION); $extension = pathinfo($cover, PATHINFO_EXTENSION);
$this->mediaMetadataService->writeAlbumCover($album, file_get_contents($cover), $extension); $this->mediaMetadataService->writeAlbumCover($album, file_get_contents($cover), $extension);
}
} catch (Throwable) {
} }
} }
@ -230,8 +145,6 @@ class FileSynchronizer
* 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.
* We'll check if such a cover file is found, and use it if positive. * We'll check if such a cover file is found, and use it if positive.
*
* @throws InvalidArgumentException
*/ */
private function getCoverFileUnderSameDirectory(): ?string private function getCoverFileUnderSameDirectory(): ?string
{ {
@ -251,15 +164,15 @@ class FileSynchronizer
$cover = $matches ? $matches[0] : null; $cover = $matches ? $matches[0] : null;
return $cover && $this->isImage($cover) ? $cover : null; return $cover && static::isImage($cover) ? $cover : null;
}); });
} }
private function isImage(string $path): bool private static function isImage(string $path): bool
{ {
try { try {
return (bool) exif_imagetype($path); return (bool) exif_imagetype($path);
} catch (Throwable $e) { } catch (Throwable) {
return false; return false;
} }
} }
@ -290,60 +203,6 @@ class FileSynchronizer
return $this->syncError; return $this->syncError;
} }
private function getTrackNumberFromInfo(array $info): int
{
$track = 0;
// Apparently track numbers can be stored with different indices as the following.
$trackIndices = [
'comments.track',
'comments.tracknumber',
'comments.track_number',
];
for ($i = 0; $i < count($trackIndices) && $track === 0; ++$i) {
$track = (int) array_get($info, $trackIndices[$i], [0])[0];
}
return $track;
}
private function gatherPropsFromTags(array $info, array $comments, array &$props): void
{
$propertyMap = [
'artist' => 'artist',
'albumartist' => 'band',
'album' => 'album',
'title' => 'title',
'lyrics' => ['unsychronised_lyric', 'unsynchronised_lyric'],
'compilation' => 'part_of_a_compilation',
];
foreach ($propertyMap as $name => $tags) {
foreach ((array) $tags as $tag) {
$value = array_get($info, "tags.id3v2.$tag", [null])[0] ?: array_get($comments, $tag, [''])[0];
if ($value) {
$props[$name] = $value;
}
}
// Fixes #323, where tag names can be htmlentities()'ed
if (is_string($props[$name]) && $props[$name]) {
$props[$name] = trim(html_entity_decode($props[$name]));
}
}
}
private function isCompilation(array $props): bool
{
// A "compilation" property can be determined by:
// - "part_of_a_compilation" tag (used by iTunes), or
// - "albumartist" (used by non-retarded applications).
// Also, the latter is only valid if the value is NOT the same as "artist".
return $props['albumartist'] && $props['artist'] !== $props['albumartist'];
}
public function getSong(): ?Song public function getSong(): ?Song
{ {
return $this->song; return $this->song;

View file

@ -16,23 +16,6 @@ use Symfony\Component\Finder\Finder;
class MediaSyncService class MediaSyncService
{ {
/**
* All applicable tags in a media file that we cater for.
* Note that each isn't necessarily a valid ID3 tag name.
*/
public const APPLICABLE_TAGS = [
'artist',
'album',
'title',
'length',
'track',
'disc',
'lyrics',
'cover',
'mtime',
'compilation',
];
public function __construct( public function __construct(
private SongRepository $songRepository, private SongRepository $songRepository,
private FileSynchronizer $fileSynchronizer, private FileSynchronizer $fileSynchronizer,
@ -42,14 +25,7 @@ class MediaSyncService
} }
/** /**
* Tags to be synced. * @param array<string> $excludes The tags to exclude.
*/
protected array $tags = [];
/**
* Sync the media. Oh sync the media.
*
* @param array<string> $tags The tags to sync.
* Only taken into account for existing records. * Only taken into account for existing records.
* New records will have all tags synced in regardless. * New records will have all tags synced in regardless.
* @param bool $force Whether to force syncing even unchanged files * @param bool $force Whether to force syncing even unchanged files
@ -57,23 +33,18 @@ class MediaSyncService
*/ */
public function sync( public function sync(
?string $mediaPath = null, ?string $mediaPath = null,
array $tags = [], array $excludes = [],
bool $force = false, bool $force = false,
?SyncCommand $syncCommand = null ?SyncCommand $syncCommand = null
): void { ): void {
$this->setSystemRequirements(); $this->setSystemRequirements();
$this->setTags($tags);
$syncResult = SyncResult::init(); $syncResult = SyncResult::init();
$songPaths = $this->gatherFiles($mediaPath ?: Setting::get('media_path')); $songPaths = $this->gatherFiles($mediaPath ?: Setting::get('media_path'));
$syncCommand?->createProgressBar(count($songPaths));
if ($syncCommand) {
$syncCommand->createProgressBar(count($songPaths));
}
foreach ($songPaths as $path) { foreach ($songPaths as $path) {
$result = $this->fileSynchronizer->setFile($path)->sync($this->tags, $force); $result = $this->fileSynchronizer->setFile($path)->sync($excludes, $force);
switch ($result) { switch ($result) {
case FileSynchronizer::SYNC_RESULT_SUCCESS: case FileSynchronizer::SYNC_RESULT_SUCCESS:
@ -113,7 +84,7 @@ class MediaSyncService
return iterator_to_array( return iterator_to_array(
$this->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/koel/koel/issues/450
->files() ->files()
->followLinks() ->followLinks()
->name('/\.(mp3|ogg|m4a|flac)$/i') ->name('/\.(mp3|ogg|m4a|flac)$/i')
@ -151,23 +122,6 @@ class MediaSyncService
} }
} }
/**
* 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.
* Otherwise, we only use the valid items in it.
*
* @param array<string> $tags
*/
public function setTags(array $tags = []): void
{
$this->tags = array_intersect($tags, self::APPLICABLE_TAGS) ?: self::APPLICABLE_TAGS;
// We always keep track of mtime.
if (!in_array('mtime', $this->tags, true)) {
$this->tags[] = 'mtime';
}
}
private function setSystemRequirements(): void private function setSystemRequirements(): void
{ {
if (!app()->runningInConsole()) { if (!app()->runningInConsole()) {
@ -194,7 +148,7 @@ class MediaSyncService
private function handleNewOrModifiedFileRecord(string $path): void private function handleNewOrModifiedFileRecord(string $path): void
{ {
$result = $this->fileSynchronizer->setFile($path)->sync($this->tags); $result = $this->fileSynchronizer->setFile($path)->sync();
if ($result === FileSynchronizer::SYNC_RESULT_SUCCESS) { if ($result === FileSynchronizer::SYNC_RESULT_SUCCESS) {
$this->logger->info("Synchronized $path"); $this->logger->info("Synchronized $path");
@ -221,7 +175,7 @@ class MediaSyncService
private function handleNewOrModifiedDirectoryRecord(string $path): void private function handleNewOrModifiedDirectoryRecord(string $path): void
{ {
foreach ($this->gatherFiles($path) as $file) { foreach ($this->gatherFiles($path) as $file) {
$this->fileSynchronizer->setFile($file)->sync($this->tags); $this->fileSynchronizer->setFile($file)->sync();
} }
$this->logger->info("Synced all song(s) under $path"); $this->logger->info("Synced all song(s) under $path");

View file

@ -24,8 +24,7 @@ class UploadService
$file->move($this->getUploadDirectory(), $targetFileName); $file->move($this->getUploadDirectory(), $targetFileName);
$targetPathName = $this->getUploadDirectory() . $targetFileName; $targetPathName = $this->getUploadDirectory() . $targetFileName;
$this->fileSynchronizer->setFile($targetPathName); $result = $this->fileSynchronizer->setFile($targetPathName)->sync();
$result = $this->fileSynchronizer->sync(MediaSyncService::APPLICABLE_TAGS);
if ($result !== FileSynchronizer::SYNC_RESULT_SUCCESS) { if ($result !== FileSynchronizer::SYNC_RESULT_SUCCESS) {
@unlink($targetPathName); @unlink($targetPathName);

View file

@ -0,0 +1,114 @@
<?php
namespace App\Values;
use App\Models\Album;
use App\Models\Artist;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr;
use SplFileInfo;
use Throwable;
final class SongScanInformation implements Arrayable
{
private function __construct(
public ?string $title,
public ?string $albumName,
public ?string $artistName,
public ?string $albumArtistName,
public ?int $track,
public ?int $disc,
public ?string $lyrics,
public ?int $length,
public ?array $cover,
public ?string $path,
public ?int $mTime,
) {
}
public static function fromGetId3Info(array $info): self
{
// We prefer ID3v2 tags over ID3v1 tags.
$tags = array_merge(Arr::get($info, 'tags.id3v1', []), Arr::get($info, 'tags.id3v2', []));
$comments = Arr::get($info, 'comments', []);
$title = self::getTag($tags, 'title', 'Untitled');
$albumName = self::getTag($tags, 'album', Album::UNKNOWN_NAME);
$artistName = self::getTag($tags, 'artist', Artist::UNKNOWN_NAME);
$albumArtistName = self::getTag($tags, ['albumartist', 'album_artist', 'band']);
// If the song is explicitly marked as a compilation but there's no album artist name, use the umbrella
// "Various Artists" artist.
if (self::getTag($tags, 'part_of_a_compilation') && !$albumArtistName) {
$albumArtistName = Artist::VARIOUS_NAME;
}
$track = (int) self::getTag($tags, ['track', 'tracknumber', 'track_number']);
$disc = (int) self::getTag($tags, 'part_of_a_set', 1);
$lyrics = self::getTag($tags, ['unsynchronised_lyric', 'unsychronised_lyric']);
$path = Arr::get($info, 'filenamepath');
$length = (float) Arr::get($info, 'playtime_seconds');
$cover = self::getTag($comments, 'picture', []);
$mTime = self::getMTime($path);
return new self(
title: $title,
albumName: $albumName,
artistName: $artistName,
albumArtistName: $albumArtistName,
track: $track,
disc: $disc,
lyrics: $lyrics,
length: $length,
cover: $cover,
path: $path,
mTime: $mTime,
);
}
private static function getTag(array $arr, string | array $keys, $default = ''): mixed
{
$keys = Arr::wrap($keys);
for ($i = 0; $i < count($keys); ++$i) {
$value = Arr::get($arr, $keys[$i] . '.0');
if ($value) {
break;
}
}
return $value ?? $default;
}
private static function getMTime(mixed $path): int
{
$splFileInfo = new SplFileInfo($path);
// Workaround for #344, where getMTime() fails for certain files with Unicode names on Windows.
try {
return $splFileInfo->getMTime();
} catch (Throwable) {
// Just use current stamp for mtime.
return time();
}
}
/** @return array<mixed> */
public function toArray(): array
{
return [
'title' => $this->title,
'album' => $this->albumName,
'artist' => $this->artistName,
'albumartist' => $this->albumArtistName,
'track' => $this->track,
'disc' => $this->disc,
'lyrics' => $this->lyrics,
'length' => $this->length,
'cover' => $this->cover,
'path' => $this->path,
'mtime' => $this->mTime,
];
}
}

View file

@ -6,20 +6,8 @@ use Illuminate\Support\Collection;
final class SyncResult final class SyncResult
{ {
/** @var Collection|array<string> */ private function __construct(public Collection $success, public Collection $bad, public Collection $unmodified)
public Collection $success;
/** @var Collection|array<string> */
public Collection $bad;
/** @var Collection|array<string> */
public Collection $unmodified;
private function __construct(Collection $success, Collection $bad, Collection $unmodified)
{ {
$this->success = $success;
$this->bad = $bad;
$this->unmodified = $unmodified;
} }
public static function init(): self public static function init(): self

6
public/.gitignore vendored
View file

@ -3,9 +3,9 @@ fonts
# Ignore all (generated) images under img, but keep the folder structure # Ignore all (generated) images under img, but keep the folder structure
img/* img/*
!img/albums !img/covers
img/albums/* img/covers/*
!img/albums/.gitkeep !img/covers/.gitkeep
!img/artists !img/artists
img/artists/* img/artists/*
!img/artists/.gitkeep !img/artists/.gitkeep

View file

@ -7,7 +7,6 @@ use App\Exceptions\SongUploadFailedException;
use App\Models\Setting; use App\Models\Setting;
use App\Models\Song; use App\Models\Song;
use App\Services\FileSynchronizer; use App\Services\FileSynchronizer;
use App\Services\MediaSyncService;
use App\Services\UploadService; use App\Services\UploadService;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Mockery; use Mockery;
@ -56,7 +55,7 @@ class UploadServiceTest extends TestCase
$this->fileSynchronizer $this->fileSynchronizer
->shouldReceive('sync') ->shouldReceive('sync')
->once() ->once()
->with(MediaSyncService::APPLICABLE_TAGS) ->with()
->andReturn(FileSynchronizer::SYNC_RESULT_BAD_FILE); ->andReturn(FileSynchronizer::SYNC_RESULT_BAD_FILE);
$this->fileSynchronizer $this->fileSynchronizer
@ -91,7 +90,7 @@ class UploadServiceTest extends TestCase
$this->fileSynchronizer $this->fileSynchronizer
->shouldReceive('sync') ->shouldReceive('sync')
->once() ->once()
->with(MediaSyncService::APPLICABLE_TAGS) ->with()
->andReturn(FileSynchronizer::SYNC_RESULT_SUCCESS); ->andReturn(FileSynchronizer::SYNC_RESULT_SUCCESS);
$song = new Song(); $song = new Song();