diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index 9a3f81c..ae40d38 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -5,6 +5,7 @@ use App\Enum\Language; use App\Enum\Setting; use App\Exception\ExitException; +use App\Exception\InvalidValueException; use App\Exception\TooManyRetriesException; use App\Exception\UnreadableFileException; use App\Service\DownloadManager; @@ -240,7 +241,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $progress->finish(); $io->newLine(); - }, $input->getOption('retry'), $input->getOption('retry-delay')); + }, maxRetries: $input->getOption('retry'), retryDelay: $input->getOption('retry-delay'), ignoreExceptions: [InvalidValueException::class]); } catch (TooManyRetriesException $e) { if (!$input->getOption('skip-errors')) { throw $e; diff --git a/src/DTO/GameDetail.php b/src/DTO/GameDetail.php index 03dfd33..218a04c 100644 --- a/src/DTO/GameDetail.php +++ b/src/DTO/GameDetail.php @@ -15,6 +15,7 @@ public function __construct( public string $cdKey, #[ArrayType(type: DownloadDescription::class)] public array $downloads, + public string $slug, ) { } } diff --git a/src/DTO/GameInfo.php b/src/DTO/GameInfo.php index f144da6..f1d578f 100644 --- a/src/DTO/GameInfo.php +++ b/src/DTO/GameInfo.php @@ -16,6 +16,8 @@ final class GameInfo implements OwnedItemInfo public readonly bool $isNew; + public readonly ?string $slug; + public function getId(): int { return $this->id; @@ -35,4 +37,9 @@ public function hasUpdates(): bool { return $this->hasUpdates || $this->isNew; } + + public function getSlug(): string + { + return $this->slug ?? ''; + } } diff --git a/src/DTO/MovieInfo.php b/src/DTO/MovieInfo.php index 41f31a0..3965448 100644 --- a/src/DTO/MovieInfo.php +++ b/src/DTO/MovieInfo.php @@ -29,4 +29,9 @@ public function hasUpdates(): bool { return false; } + + public function getSlug(): string + { + return ''; + } } diff --git a/src/DTO/OwnedItemInfo.php b/src/DTO/OwnedItemInfo.php index a85750f..03a8657 100644 --- a/src/DTO/OwnedItemInfo.php +++ b/src/DTO/OwnedItemInfo.php @@ -13,4 +13,6 @@ public function getTitle(): string; public function getType(): MediaType; public function hasUpdates(): bool; + + public function getSlug(): string; } diff --git a/src/Enum/NamingConvention.php b/src/Enum/NamingConvention.php new file mode 100644 index 0000000..a90dc09 --- /dev/null +++ b/src/Enum/NamingConvention.php @@ -0,0 +1,9 @@ +exec('create table settings (id integer primary key autoincrement, setting text, value text)'); + $pdo->prepare("insert into settings (setting, value) values (?, ?)")->execute([ + Setting::NamingConvention->value, + json_encode(NamingConvention::GogSlug->value), + ]); } public function getVersion(): int diff --git a/src/Migration/Migration4.php b/src/Migration/Migration4.php new file mode 100644 index 0000000..4040ac8 --- /dev/null +++ b/src/Migration/Migration4.php @@ -0,0 +1,36 @@ +exec('alter table games add column slug text default null'); + // delete duplicate settings + $pdo->exec('delete from settings where id in (select max(id) from settings group by setting having count(setting) > 1)'); + $pdo->exec("alter table settings rename to old_settings"); + $pdo->exec('create table settings (id integer primary key autoincrement, setting text, value text, constraint setting_name unique (setting))'); + $pdo->exec('insert into settings (setting, value) select setting, value from old_settings'); + $pdo->exec('drop table old_settings'); + + // A new default value has been retroactively added to Migration1. + // The original naming convention is being set here if there isn't one. + // This effectively means that all new installations have the new value while everyone who used the app before + // has the original one. + $pdo->prepare('insert into settings (setting, value) values (?, ?) on conflict(setting) do nothing')->execute([ + Setting::NamingConvention->value, + json_encode(NamingConvention::Custom->value), + ]); + } + + public function getVersion(): int + { + return 4; + } +} diff --git a/src/Service/OwnedItemsManager.php b/src/Service/OwnedItemsManager.php index c063171..cbe7d79 100644 --- a/src/Service/OwnedItemsManager.php +++ b/src/Service/OwnedItemsManager.php @@ -264,6 +264,7 @@ private function getGameDetail(OwnedItemInfo $item, int $httpTimeout): GameDetai $detail = $this->serializer->deserialize($response->getContent(), GameDetail::class, [ 'id' => $item->getId(), + 'slug' => $item->getSlug(), ]); foreach ($detail->downloads as $download) { $this->setMd5($download, $detail, $httpTimeout); diff --git a/src/Service/Persistence/PersistenceManagerSqlite.php b/src/Service/Persistence/PersistenceManagerSqlite.php index ac6aa43..a5476c6 100644 --- a/src/Service/Persistence/PersistenceManagerSqlite.php +++ b/src/Service/Persistence/PersistenceManagerSqlite.php @@ -101,6 +101,7 @@ public function getLocalGameData(): ?array 'title' => $next['title'], 'cdKey' => $next['cd_key'] ?? '', 'downloads' => $downloads, + 'slug' => $next['slug'] ?? '', ], GameDetail::class); } @@ -113,16 +114,18 @@ public function storeSingleGameDetail(GameDetail $detail): void $this->migrationManager->apply($pdo); $pdo->prepare( - 'insert into games (title, cd_key, game_id) - VALUES (?, ?, ?) + 'insert into games (title, cd_key, game_id, slug) + VALUES (?, ?, ?, ?) ON CONFLICT DO UPDATE SET title = excluded.title, cd_key = excluded.cd_key, - game_id = excluded.game_id + game_id = excluded.game_id, + slug = excluded.slug ' )->execute([ $detail->title, $detail->cdKey ?: null, $detail->id, + $detail->slug, ]); $query = $pdo->prepare('select id from games where game_id = ?'); diff --git a/src/Service/RetryService.php b/src/Service/RetryService.php index 60961eb..7f438c8 100644 --- a/src/Service/RetryService.php +++ b/src/Service/RetryService.php @@ -16,7 +16,7 @@ public function __construct( * @throws TooManyRetriesException * @throws Throwable */ - public function retry(callable $callable, int $maxRetries, int $retryDelay, ?array $exceptions = null): void + public function retry(callable $callable, int $maxRetries, int $retryDelay, ?array $exceptions = null, ?array $ignoreExceptions = null): void { $retries = 0; $thrown = []; @@ -33,6 +33,9 @@ public function retry(callable $callable, int $maxRetries, int $retryDelay, ?arr if (!$this->matches($e, $exceptions)) { throw $e; } + if ($ignoreExceptions && $this->matches($e, $ignoreExceptions)) { + throw $e; + } sleep($retryDelay); } } while ($retries < $maxRetries); diff --git a/src/Trait/FilteredGamesResolverTrait.php b/src/Trait/FilteredGamesResolverTrait.php index 2c68e71..01b466d 100644 --- a/src/Trait/FilteredGamesResolverTrait.php +++ b/src/Trait/FilteredGamesResolverTrait.php @@ -141,6 +141,7 @@ function (GameDetail $game) use ($englishFallback, $languages) { title: $game->title, cdKey: $game->cdKey, downloads: $downloads, + slug: $game->slug, ); }, $iterable, @@ -163,6 +164,7 @@ function (GameDetail $game) use ($operatingSystems) { title: $game->title, cdKey: $game->cdKey, downloads: $downloads, + slug: $game->slug, ); }, $iterable, diff --git a/src/Trait/TargetDirectoryTrait.php b/src/Trait/TargetDirectoryTrait.php index 379c120..9051dbb 100644 --- a/src/Trait/TargetDirectoryTrait.php +++ b/src/Trait/TargetDirectoryTrait.php @@ -3,6 +3,10 @@ namespace App\Trait; use App\DTO\GameDetail; +use App\Enum\NamingConvention; +use App\Enum\Setting; +use App\Exception\InvalidValueException; +use LogicException; use Symfony\Component\Console\Input\InputInterface; trait TargetDirectoryTrait @@ -20,9 +24,23 @@ private function getTargetDir(InputInterface $input, GameDetail $game, ?string $ } } - $title = preg_replace('@[^a-zA-Z-_0-9.]@', '_', $game->title); - $title = preg_replace('@_{2,}@', '_', $title); - $title = trim($title, '.'); + $namingScheme = NamingConvention::tryFrom($this->persistence->getSetting(Setting::NamingConvention)) ?? NamingConvention::GogSlug; + + switch ($namingScheme) { + case NamingConvention::GogSlug: + if (!$game->slug) { + throw new InvalidValueException("GOG Downloader is configured to use the GOG slug naming scheme, but the game '{$game->title}' does not have a slug. If you migrated from the previous naming scheme, please run the update command first."); + } + $title = $game->slug; + break; + case NamingConvention::Custom: + $title = preg_replace('@[^a-zA-Z-_0-9.]@', '_', $game->title); + $title = preg_replace('@_{2,}@', '_', $title); + $title = trim($title, '.'); + break; + default: + throw new LogicException('Unimplemented naming scheme: ' . $namingScheme->value); + } $dir = "{$dir}/{$title}"; if ($subdirectory !== null) {