Skip to content

Commit

Permalink
feat: revamp sync and sync commands
Browse files Browse the repository at this point in the history
  • Loading branch information
phanan committed Jul 29, 2022
1 parent b12e0c1 commit 686c5f7
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 79 deletions.
1 change: 0 additions & 1 deletion app/Console/Commands/PruneLibraryCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ public function __construct(private LibraryManager $libraryManager)
public function handle(): int
{
$this->libraryManager->prune();

$this->info('Empty artists and albums removed.');

return self::SUCCESS;
Expand Down
81 changes: 41 additions & 40 deletions app/Console/Commands/SyncCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

use App\Libraries\WatchRecord\InotifyWatchRecord;
use App\Models\Setting;
use App\Services\FileSynchronizer;
use App\Services\MediaSyncService;
use App\Values\SyncResult;
use Illuminate\Console\Command;
use RuntimeException;
use Symfony\Component\Console\Helper\ProgressBar;

class SyncCommand extends Command
Expand All @@ -17,15 +18,18 @@ class SyncCommand extends Command
{--force : Force re-syncing even unchanged files}';

protected $description = 'Sync songs found in configured directory against the database.';
private int $skippedCount = 0;
private int $invalidCount = 0;
private int $syncedCount = 0;

private ?ProgressBar $progressBar = null;
private ProgressBar $progressBar;

public function __construct(private MediaSyncService $mediaSyncService)
{
parent::__construct();

$this->mediaSyncService->on('paths-gathered', function (array $paths): void {
$this->progressBar = new ProgressBar($this->output, count($paths));
});

$this->mediaSyncService->on('progress', [$this, 'onSyncProgress']);
}

public function handle(): int
Expand All @@ -46,24 +50,27 @@ public function handle(): int
/**
* Sync all files in the configured media path.
*/
protected function syncAll(): void
private function syncAll(): void
{
$path = Setting::get('media_path');
$this->info('Syncing media from ' . $path . PHP_EOL);

// The excluded tags.
$this->components->info('Scanning ' . $path);

// The tags to ignore from syncing.
// Notice that this is only meaningful for existing records.
// New records will have every applicable field synced in.
$excludes = $this->option('excludes') ? explode(',', $this->option('excludes')) : [];
$ignores = $this->option('ignore') ? explode(',', $this->option('ignore')) : [];

$results = $this->mediaSyncService->sync($ignores, $this->option('force'));

$this->mediaSyncService->sync($excludes, $this->option('force'), $this);
$this->newLine(2);
$this->components->info('Scanning completed!');

$this->output->writeln(
PHP_EOL . PHP_EOL
. "<info>Completed! $this->syncedCount new or updated song(s)</info>, "
. "$this->skippedCount unchanged song(s), "
. "and <comment>$this->invalidCount invalid file(s)</comment>."
);
$this->components->bulletList([
"<fg=green>{$results->success()->count()}</> new or updated song(s)",
"<fg=yellow>{$results->skipped()->count()}</> unchanged song(s)",
"<fg=red>{$results->error()->count()}</> invalid file(s)",
]);
}

/**
Expand All @@ -76,39 +83,33 @@ protected function syncAll(): void
*
* @see http://man7.org/linux/man-pages/man1/inotifywait.1.html
*/
public function syncSingleRecord(string $record): void
private function syncSingleRecord(string $record): void
{
$this->mediaSyncService->syncByWatchRecord(new InotifyWatchRecord($record));
}

/**
* Log a song's sync status to console.
*/
public function logSyncStatusToConsole(string $path, int $result, ?string $reason = null): void
public function onSyncProgress(SyncResult $result): void
{
$name = basename($path);

if ($result === FileSynchronizer::SYNC_RESULT_UNMODIFIED) {
++$this->skippedCount;
} elseif ($result === FileSynchronizer::SYNC_RESULT_BAD_FILE) {
if ($this->option('verbose')) {
$this->error(PHP_EOL . "'$name' is not a valid media file: $reason");
}
if (!$this->option('verbose')) {
$this->progressBar->advance();

++$this->invalidCount;
} else {
++$this->syncedCount;
return;
}
}

public function createProgressBar(int $max): void
{
$this->progressBar = $this->getOutput()->createProgressBar($max);
}
$path = dirname($result->path);
$file = basename($result->path);
$sep = DIRECTORY_SEPARATOR;

public function advanceProgressBar(): void
{
$this->progressBar->advance();
$this->components->twoColumnDetail("<fg=gray>$path$sep</>$file", match (true) {
$result->isSuccess() => "<fg=green>OK</>",
$result->isSkipped() => "<fg=yellow>SKIPPED</>",
$result->isError() => "<fg=red>ERROR</>",
default => throw new RuntimeException("Unknown sync result type: {$result->type}")
});

if ($result->isError()) {
$this->output->writeln("<fg=red>$result->error</>");
}
}

private function ensureMediaPath(): void
Expand Down
4 changes: 2 additions & 2 deletions app/Events/MediaSyncCompleted.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

namespace App\Events;

use App\Values\SyncResult;
use App\Values\SyncResultCollection;
use Illuminate\Queue\SerializesModels;

class MediaSyncCompleted extends Event
{
use SerializesModels;

public function __construct(public SyncResult $result)
public function __construct(public SyncResultCollection $results)
{
}
}
7 changes: 4 additions & 3 deletions app/Listeners/DeleteNonExistingRecordsPostSync.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use App\Models\Song;
use App\Repositories\SongRepository;
use App\Services\Helper;
use App\Values\SyncResult;

class DeleteNonExistingRecordsPostSync
{
Expand All @@ -15,9 +16,9 @@ public function __construct(private SongRepository $songRepository)

public function handle(MediaSyncCompleted $event): void
{
$hashes = $event->result
->validEntries()
->map(static fn (string $path): string => Helper::getFileHash($path))
$hashes = $event->results
->valid()
->map(static fn (SyncResult $result) => Helper::getFileHash($result->path))
->merge($this->songRepository->getAllHostedOnS3()->pluck('id'))
->toArray();

Expand Down
9 changes: 5 additions & 4 deletions app/Services/FileSynchronizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Models\Song;
use App\Repositories\SongRepository;
use App\Values\SongScanInformation;
use App\Values\SyncResult;
use getID3;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Support\Arr;
Expand Down Expand Up @@ -72,16 +73,16 @@ public function getFileScanInformation(): ?SongScanInformation
* @param array<string> $ignores The tags to ignore/exclude (only taken into account if the song already exists)
* @param bool $force Whether to force syncing, even if the file is unchanged
*/
public function sync(array $ignores = [], bool $force = false): int
public function sync(array $ignores = [], bool $force = false): SyncResult
{
if (!$this->isFileNewOrChanged() && !$force) {
return self::SYNC_RESULT_UNMODIFIED;
return SyncResult::skipped($this->filePath);
}

$info = $this->getFileScanInformation()?->toArray();

if (!$info) {
return self::SYNC_RESULT_BAD_FILE;
return SyncResult::error($this->filePath, $this->syncError);
}

if (!$this->isFileNew()) {
Expand All @@ -102,7 +103,7 @@ public function sync(array $ignores = [], bool $force = false): int

$this->song = Song::updateOrCreate(['id' => $this->fileHash], $data);

return self::SYNC_RESULT_SUCCESS;
return SyncResult::success($this->filePath);
}

/**
Expand Down
47 changes: 22 additions & 25 deletions app/Services/MediaSyncService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@

namespace App\Services;

use App\Console\Commands\SyncCommand;
use App\Events\LibraryChanged;
use App\Events\MediaSyncCompleted;
use App\Libraries\WatchRecord\WatchRecordInterface;
use App\Models\Song;
use App\Repositories\SettingRepository;
use App\Repositories\SongRepository;
use App\Values\SyncResult;
use App\Values\SyncResultCollection;
use Psr\Log\LoggerInterface;
use SplFileInfo;
use Symfony\Component\Finder\Finder;

class MediaSyncService
{
/** @var array<array-key, callable> */
private array $events = [];

public function __construct(
private SettingRepository $settingRepository,
private SongRepository $songRepository,
Expand All @@ -30,46 +32,36 @@ public function __construct(
* Only taken into account for existing records.
* New records will have all tags synced in regardless.
* @param bool $force Whether to force syncing even unchanged files
* @param SyncCommand $syncCommand The SyncMedia command object, to log to console if executed by artisan
*/
public function sync(array $ignores = [], bool $force = false, ?SyncCommand $syncCommand = null): void
public function sync(array $ignores = [], bool $force = false): SyncResultCollection
{
/** @var string $mediaPath */
$mediaPath = $this->settingRepository->getByKey('media_path');

$this->setSystemRequirements();

$syncResult = SyncResult::init();
$results = SyncResultCollection::create();
$songPaths = $this->gatherFiles($mediaPath);
$syncCommand?->createProgressBar(count($songPaths));

if (isset($this->events['paths-gathered'])) {
$this->events['paths-gathered']($songPaths);
}

foreach ($songPaths as $path) {
$result = $this->fileSynchronizer->setFile($path)->sync($ignores, $force);
$results->add($result);

switch ($result) {
case FileSynchronizer::SYNC_RESULT_SUCCESS:
$syncResult->success->add($path);
break;

case FileSynchronizer::SYNC_RESULT_UNMODIFIED:
$syncResult->unmodified->add($path);
break;

default:
$syncResult->bad->add($path);
break;
}

if ($syncCommand) {
$syncCommand->advanceProgressBar();
$syncCommand->logSyncStatusToConsole($path, $result, $this->fileSynchronizer->getSyncError());
if (isset($this->events['progress'])) {
$this->events['progress']($result);
}
}

event(new MediaSyncCompleted($syncResult));
event(new MediaSyncCompleted($results));

// Trigger LibraryChanged, so that PruneLibrary handler is fired to prune the lib.
event(new LibraryChanged());

return $results;
}

/**
Expand Down Expand Up @@ -150,7 +142,7 @@ private function handleNewOrModifiedFileRecord(string $path): void
{
$result = $this->fileSynchronizer->setFile($path)->sync();

if ($result === FileSynchronizer::SYNC_RESULT_SUCCESS) {
if ($result->isSuccess()) {
$this->logger->info("Synchronized $path");
} else {
$this->logger->info("Failed to synchronized $path. Maybe an invalid file?");
Expand Down Expand Up @@ -182,4 +174,9 @@ private function handleNewOrModifiedDirectoryRecord(string $path): void

event(new LibraryChanged());
}

public function on(string $event, callable $callback): void
{
$this->events[$event] = $callback;
}
}
56 changes: 56 additions & 0 deletions app/Values/SyncResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace App\Values;

use Webmozart\Assert\Assert;

final class SyncResult
{
public const TYPE_SUCCESS = 1;
public const TYPE_ERROR = 2;
public const TYPE_SKIPPED = 3;

private function __construct(public string $path, public int $type, public ?string $error)
{
Assert::oneOf($type, [
SyncResult::TYPE_SUCCESS,
SyncResult::TYPE_ERROR,
SyncResult::TYPE_SKIPPED,
]);
}

public static function success(string $path): self
{
return new self($path, self::TYPE_SUCCESS, null);
}

public static function skipped(string $path): self
{
return new self($path, self::TYPE_SKIPPED, null);
}

public static function error(string $path, ?string $error): self
{
return new self($path, self::TYPE_ERROR, $error);
}

public function isSuccess(): bool
{
return $this->type === self::TYPE_SUCCESS;
}

public function isSkipped(): bool
{
return $this->type === self::TYPE_SKIPPED;
}

public function isError(): bool
{
return $this->type === self::TYPE_ERROR;
}

public function isValid(): bool
{
return $this->isSuccess() || $this->isSkipped();
}
}
Loading

0 comments on commit 686c5f7

Please sign in to comment.