From cda94a87594f1edb5f3e98e07162c8c204e3ab8b Mon Sep 17 00:00:00 2001 From: "Abdulmhsen B. A. A" Date: Sun, 8 May 2022 04:40:09 +0300 Subject: [PATCH] Initial code changes to support new db redesign. --- README.md | 12 ++ config/config.php | 4 +- config/services.php | 11 - .../sqlite_1644418046_create_state_table.sql | 47 ++-- migrations/sqlite_1651934866_move_fields.sql | 122 ----------- src/Commands/Database/ListCommand.php | 76 ++++--- src/Commands/State/ImportCommand.php | 14 +- src/Libs/Entity/StateEntity.php | 201 +++++++++--------- src/Libs/Entity/StateInterface.php | 33 ++- src/Libs/Mappers/Export/ExportMapper.php | 51 +---- src/Libs/Mappers/ExportInterface.php | 9 - src/Libs/Mappers/Import/DirectMapper.php | 177 --------------- src/Libs/Mappers/Import/MemoryMapper.php | 100 +++------ src/Libs/Servers/EmbyServer.php | 2 +- src/Libs/Servers/JellyfinServer.php | 111 ++++------ src/Libs/Servers/PlexServer.php | 114 ++++------ src/Libs/Storage/PDO/PDOAdapter.php | 141 ++++++------ src/Libs/helpers.php | 2 +- tests/Fixtures/EpisodeEntity.php | 38 ++-- tests/Fixtures/MovieEntity.php | 28 ++- tests/Mappers/Import/DirectMapperTest.php | 163 -------------- tests/Mappers/Import/MemoryMapperTest.php | 21 +- tests/Storage/PDOAdapterTest.php | 14 +- 23 files changed, 468 insertions(+), 1023 deletions(-) delete mode 100644 migrations/sqlite_1651934866_move_fields.sql delete mode 100644 src/Libs/Mappers/Import/DirectMapper.php delete mode 100644 tests/Mappers/Import/DirectMapperTest.php diff --git a/README.md b/README.md index ba5dafff..2a3a8d1e 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,18 @@ WatchState is a CLI based tool to sync your watch state between different media services, like trakt.tv, This tool support `Plex Media Server`, `Emby` and `Jellyfin` out of the box currently, with plans for future expansion for other media servers. +# Breaking Change + +If you are using old version of the tool i.e. before (2022-05-08) you need to run manual import to populate the new +database. We had massive code/db changes. The new database name should be `watchstate_v0.db`, if you don't have a +database named `watchstate.db` then there is nothing to do. + +to manually import the run this command with --force-full flag to get all previous records. + +```bash +$ docker exec -ti watchstate console state:import --force-full -vvrm +``` + # Install create your `docker-compose.yaml` file: diff --git a/config/config.php b/config/config.php index 5b816fb6..79ff8385 100644 --- a/config/config.php +++ b/config/config.php @@ -28,10 +28,10 @@ return (function () { ], ]; - $config['tmpDir'] = fixPath(env('WS_TMP_DIR', fn() => ag($config, 'path'))); + $config['tmpDir'] = fixPath(env('WS_TMP_DIR', $config['path'])); $config['storage'] = [ - 'dsn' => 'sqlite:' . ag($config, 'path') . '/db/watchstate.db', + 'dsn' => 'sqlite:' . ag($config, 'path') . '/db/watchstate_v0.db', 'username' => null, 'password' => null, 'options' => [ diff --git a/config/services.php b/config/services.php index e86a1f4f..2ed1b1a4 100644 --- a/config/services.php +++ b/config/services.php @@ -7,7 +7,6 @@ use App\Libs\Entity\StateEntity; use App\Libs\Entity\StateInterface; use App\Libs\Mappers\Export\ExportMapper; use App\Libs\Mappers\ExportInterface; -use App\Libs\Mappers\Import\DirectMapper; use App\Libs\Mappers\Import\MemoryMapper; use App\Libs\Mappers\ImportInterface; use App\Libs\Storage\PDO\PDOAdapter; @@ -89,16 +88,6 @@ return (function (): array { ], ], - DirectMapper::class => [ - 'class' => function (LoggerInterface $logger, StorageInterface $storage): ImportInterface { - return (new DirectMapper($logger, $storage))->setUp(Config::get('mapper.import.opts', [])); - }, - 'args' => [ - LoggerInterface::class, - StorageInterface::class, - ], - ], - ImportInterface::class => [ 'class' => function (ImportInterface $mapper): ImportInterface { return $mapper; diff --git a/migrations/sqlite_1644418046_create_state_table.sql b/migrations/sqlite_1644418046_create_state_table.sql index af600f27..cbcb4271 100644 --- a/migrations/sqlite_1644418046_create_state_table.sql +++ b/migrations/sqlite_1644418046_create_state_table.sql @@ -1,31 +1,32 @@ -- # migrate_up -CREATE TABLE IF NOT EXISTS "state" +CREATE TABLE "state" ( - "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, - "type" text NOT NULL, - "updated" integer NOT NULL, - "watched" integer NOT NULL DEFAULT 0, - "meta" text NULL, - "guid_plex" text NULL, - "guid_imdb" text NULL, - "guid_tvdb" text NULL, - "guid_tmdb" text NULL, - "guid_tvmaze" text NULL, - "guid_tvrage" text NULL, - "guid_anidb" text NULL + "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, + "type" text NOT NULL, + "updated" integer NOT NULL, + "watched" integer NOT NULL DEFAULT '0', + "via" text NOT NULL, + "title" text NOT NULL, + "year" integer NULL, + "season" integer NULL, + "episode" integer NULL, + "parent" text NULL, + "guids" text NULL, + "extra" text NULL ); -CREATE INDEX IF NOT EXISTS "state_type" ON "state" ("type"); -CREATE INDEX IF NOT EXISTS "state_watched" ON "state" ("watched"); -CREATE INDEX IF NOT EXISTS "state_updated" ON "state" ("updated"); -CREATE INDEX IF NOT EXISTS "state_meta" ON "state" ("meta"); -CREATE INDEX IF NOT EXISTS "state_guid_plex" ON "state" ("guid_plex"); -CREATE INDEX IF NOT EXISTS "state_guid_imdb" ON "state" ("guid_imdb"); -CREATE INDEX IF NOT EXISTS "state_guid_tvdb" ON "state" ("guid_tvdb"); -CREATE INDEX IF NOT EXISTS "state_guid_tvmaze" ON "state" ("guid_tvmaze"); -CREATE INDEX IF NOT EXISTS "state_guid_tvrage" ON "state" ("guid_tvrage"); -CREATE INDEX IF NOT EXISTS "state_guid_anidb" ON "state" ("guid_anidb"); +CREATE INDEX "state_type" ON "state" ("type"); +CREATE INDEX "state_updated" ON "state" ("updated"); +CREATE INDEX "state_watched" ON "state" ("watched"); +CREATE INDEX "state_via" ON "state" ("via"); +CREATE INDEX "state_title" ON "state" ("title"); +CREATE INDEX "state_year" ON "state" ("year"); +CREATE INDEX "state_season" ON "state" ("season"); +CREATE INDEX "state_episode" ON "state" ("episode"); +CREATE INDEX "state_parent" ON "state" ("parent"); +CREATE INDEX "state_guids" ON "state" ("guids"); +CREATE INDEX "state_extra" ON "state" ("extra"); -- # migrate_down diff --git a/migrations/sqlite_1651934866_move_fields.sql b/migrations/sqlite_1651934866_move_fields.sql deleted file mode 100644 index 06c73240..00000000 --- a/migrations/sqlite_1651934866_move_fields.sql +++ /dev/null @@ -1,122 +0,0 @@ --- # migrate_up -ALTER TABLE "state" - RENAME TO "old_state"; - -CREATE TABLE "state" -( - "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, - "type" text NOT NULL, - "updated" integer NOT NULL, - "watched" integer NOT NULL DEFAULT '0', - "via" text NULL, - "title" text NULL, - "year" integer NULL, - "season" integer NULL, - "episode" integer NULL, - "parent" text NULL, - "guids" text NULL, - "meta" text NULL, - "guid_plex" text NULL, - "guid_imdb" text NULL, - "guid_tvdb" text NULL, - "guid_tmdb" text NULL, - "guid_tvmaze" text NULL, - "guid_tvrage" text NULL, - "guid_anidb" text NULL -); - -INSERT INTO "state" ("id", "type", "updated", "watched", "meta", "guid_plex", "guid_imdb", "guid_tvdb", - "guid_tmdb", "guid_tvmaze", "guid_tvrage", "guid_anidb") -SELECT "id", - "type", - "updated", - "watched", - "meta", - "guid_plex", - "guid_imdb", - "guid_tvdb", - "guid_tmdb", - "guid_tvmaze", - "guid_tvrage", - "guid_anidb" -FROM "old_state"; - -UPDATE sqlite_sequence -SET "seq" = (SELECT MAX("id") FROM "state") -WHERE "name" = 'state'; - -DROP TABLE "old_state"; - -CREATE INDEX "state_type" ON "state" ("type"); -CREATE INDEX "state_updated" ON "state" ("updated"); -CREATE INDEX "state_watched" ON "state" ("watched"); -CREATE INDEX "state_via" ON "state" ("via"); -CREATE INDEX "state_title" ON "state" ("title"); -CREATE INDEX "state_year" ON "state" ("year"); -CREATE INDEX "state_season" ON "state" ("season"); -CREATE INDEX "state_episode" ON "state" ("episode"); -CREATE INDEX "state_parent" ON "state" ("parent"); -CREATE INDEX "state_guids" ON "state" ("guids"); -CREATE INDEX "state_meta" ON "state" ("meta"); -CREATE INDEX "state_guid_plex" ON "state" ("guid_plex"); -CREATE INDEX "state_guid_imdb" ON "state" ("guid_imdb"); -CREATE INDEX "state_guid_tvdb" ON "state" ("guid_tvdb"); -CREATE INDEX "state_guid_tvmaze" ON "state" ("guid_tvmaze"); -CREATE INDEX "state_guid_tvrage" ON "state" ("guid_tvrage"); -CREATE INDEX "state_guid_anidb" ON "state" ("guid_anidb"); - --- # migrate_down - -ALTER TABLE "state" - RENAME TO "old_state"; - -CREATE TABLE "state" -( - "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, - "type" text NOT NULL, - "updated" integer NOT NULL, - "watched" integer NOT NULL DEFAULT '0', - "meta" text NULL, - "guid_plex" text NULL, - "guid_imdb" text NULL, - "guid_tvdb" text NULL, - "guid_tmdb" text NULL, - "guid_tvmaze" text NULL, - "guid_tvrage" text NULL, - "guid_anidb" text NULL -); - -INSERT INTO "state" ("id", "type", "updated", "watched", "meta", "guid_plex", "guid_imdb", "guid_tvdb", - "guid_tmdb", "guid_tvmaze", "guid_tvrage", "guid_anidb") -SELECT "id", - "type", - "updated", - "watched", - "meta", - "guid_plex", - "guid_imdb", - "guid_tvdb", - "guid_tmdb", - "guid_tvmaze", - "guid_tvrage", - "guid_anidb" -FROM "old_state"; - -UPDATE sqlite_sequence -SET "seq" = (SELECT MAX("id") FROM "state") -WHERE "name" = 'state'; - -DROP TABLE "old_state"; - -CREATE INDEX "state_type" ON "state" ("type"); -CREATE INDEX "state_updated" ON "state" ("updated"); -CREATE INDEX "state_watched" ON "state" ("watched"); -CREATE INDEX "state_meta" ON "state" ("meta"); -CREATE INDEX "state_guid_plex" ON "state" ("guid_plex"); -CREATE INDEX "state_guid_imdb" ON "state" ("guid_imdb"); -CREATE INDEX "state_guid_tvdb" ON "state" ("guid_tvdb"); -CREATE INDEX "state_guid_tvmaze" ON "state" ("guid_tvmaze"); -CREATE INDEX "state_guid_tvrage" ON "state" ("guid_tvrage"); -CREATE INDEX "state_guid_anidb" ON "state" ("guid_anidb"); - --- put your downgrade database commands here. diff --git a/src/Commands/Database/ListCommand.php b/src/Commands/Database/ListCommand.php index 98f7a6a2..376e1d1a 100644 --- a/src/Commands/Database/ListCommand.php +++ b/src/Commands/Database/ListCommand.php @@ -39,13 +39,19 @@ final class ListCommand extends Command 'Limit results to this specified server. This filter is not reliable. and changes based on last server query.' ) ->addOption('output', null, InputOption::VALUE_REQUIRED, 'Display output as [json, yaml, table]', 'table') - ->addOption('series', null, InputOption::VALUE_REQUIRED, 'Limit results to this specified series.') - ->addOption('movie', null, InputOption::VALUE_REQUIRED, 'Limit results to this specified movie.') - ->addOption('parent', null, InputOption::VALUE_NONE, 'If set it will search parent GUIDs instead.') + ->addOption( + 'type', + null, + InputOption::VALUE_REQUIRED, + 'Limit results to this specified type can be [movie or episode].' + ) + ->addOption('title', null, InputOption::VALUE_REQUIRED, 'Limit results to this specified tv show.') ->addOption('season', null, InputOption::VALUE_REQUIRED, 'Select season number') ->addOption('episode', null, InputOption::VALUE_REQUIRED, 'Select episode number') ->addOption('id', null, InputOption::VALUE_REQUIRED, 'Select db record number') ->addOption('sort', null, InputOption::VALUE_REQUIRED, 'sort order by [id, updated]', 'updated') + ->addOption('asc', null, InputOption::VALUE_NONE, 'Sort records in ascending order.') + ->addOption('desc', null, InputOption::VALUE_NONE, 'Sort records in descending order. (Default)') ->setDescription('List Database entries.'); foreach (array_keys(Guid::SUPPORTED) as $guid) { @@ -57,6 +63,8 @@ final class ListCommand extends Command 'Search Using ' . ucfirst($guid) . ' id.' ); } + + $this->addOption('parent', null, InputOption::VALUE_NONE, 'If set it will search parent GUIDs instead.'); } /** @@ -89,32 +97,37 @@ final class ListCommand extends Command $sql = "SELECT * FROM state "; - if ($input->getOption('via')) { - $where[] = "json_extract(meta,'$.via') = :via"; - $params['via'] = $input->getOption('via'); - } - if ($input->getOption('id')) { $where[] = "id = :id"; $params['id'] = $input->getOption('id'); } - if ($input->getOption('series')) { - $where[] = "json_extract(meta,'$.series') = :series"; - $params['series'] = $input->getOption('series'); + if ($input->getOption('via')) { + $where[] = "via = :via"; + $params['via'] = $input->getOption('via'); } - if ($input->getOption('movie')) { - $where[] = "json_extract(meta,'$.title') = :movie"; - $params['movie'] = $input->getOption('movie'); + if ($input->getOption('type')) { + $where[] = "type = :type"; + $params['type'] = match ($input->getOption('type')) { + StateInterface::TYPE_MOVIE => StateInterface::TYPE_MOVIE, + default => StateInterface::TYPE_EPISODE, + }; + } + + if ($input->getOption('title')) { + $where[] = "title LIKE '%' || :title || '%'"; + $params['title'] = $input->getOption('title'); } if (null !== $input->getOption('season')) { - $where[] = "json_extract(meta,'$.season') = " . (int)$input->getOption('season'); + $where[] = "season = :season"; + $params['season'] = $input->getOption('season'); } if (null !== $input->getOption('episode')) { - $where[] = "json_extract(meta,'$.episode') = " . (int)$input->getOption('episode'); + $where[] = "episode = :episode"; + $params['episode'] = $input->getOption('episode'); } if ($input->getOption('parent')) { @@ -122,7 +135,7 @@ final class ListCommand extends Command if (null === ($val = $input->getOption(afterLast($guid, 'guid_')))) { continue; } - $where[] = "json_extract(meta,'$.parent.{$guid}') = :{$guid}"; + $where[] = "json_extract(parent,'$.{$guid}') = :{$guid}"; $params[$guid] = $val; } } else { @@ -130,7 +143,7 @@ final class ListCommand extends Command if (null === ($val = $input->getOption(afterLast($guid, 'guid_')))) { continue; } - $where[] = "{$guid} LIKE '%' || :{$guid} || '%'"; + $where[] = "json_extract(guids,'$.{$guid}') = :{$guid}"; $params[$guid] = $val; } } @@ -139,8 +152,17 @@ final class ListCommand extends Command $sql .= 'WHERE ' . implode(' AND ', $where); } - $sort = $input->getOption('sort') === 'id' ? 'id' : 'updated'; - $sql .= " ORDER BY {$sort} DESC LIMIT :limit"; + $sort = match ($input->getOption('sort')) { + 'id' => 'id', + 'season' => 'season', + 'episode' => 'episode', + 'type' => 'type', + default => 'updated', + }; + + $sortOrder = ($input->getOption('asc')) ? 'ASC' : 'DESC'; + + $sql .= " ORDER BY {$sort} {$sortOrder} LIMIT :limit"; $stmt = $this->pdo->prepare($sql); $stmt->execute($params); @@ -200,27 +222,27 @@ final class ListCommand extends Command $type = strtolower($row['type'] ?? '??'); - $meta = json_decode(ag($row, 'meta', '{}'), true); + $extra = json_decode(ag($row, 'extra', '{}'), true); $episode = null; if (StateInterface::TYPE_EPISODE === $type) { $episode = sprintf( '%sx%s', - str_pad((string)($meta['season'] ?? 0), 2, '0', STR_PAD_LEFT), - str_pad((string)($meta['episode'] ?? 0), 2, '0', STR_PAD_LEFT), + str_pad((string)($row['season'] ?? 0), 2, '0', STR_PAD_LEFT), + str_pad((string)($row['episode'] ?? 0), 3, '0', STR_PAD_LEFT), ); } $list[] = [ $row['id'], ucfirst($row['type'] ?? '??'), - $meta['via'] ?? '??', - $meta['series'] ?? $meta['title'] ?? '??', - $meta['year'] ?? '0000', + $row['via'] ?? '??', + $row['title'] ?? '??', + $row['year'] ?? '0000', $episode ?? '-', makeDate($row['updated']), true === (bool)$row['watched'] ? 'Yes' : 'No', - $meta['webhook']['event'] ?? '-', + $extra['webhook']['event'] ?? '-', ]; if ($x < $rowCount) { diff --git a/src/Commands/State/ImportCommand.php b/src/Commands/State/ImportCommand.php index 15a01666..b9b4be77 100644 --- a/src/Commands/State/ImportCommand.php +++ b/src/Commands/State/ImportCommand.php @@ -6,11 +6,9 @@ namespace App\Commands\State; use App\Command; use App\Libs\Config; -use App\Libs\Container; use App\Libs\Data; use App\Libs\Entity\StateInterface; use App\Libs\Extends\CliLogger; -use App\Libs\Mappers\Import\DirectMapper; use App\Libs\Mappers\ImportInterface; use App\Libs\Storage\PDO\PDOAdapter; use App\Libs\Storage\StorageInterface; @@ -86,12 +84,6 @@ class ImportCommand extends Command 'Filter final status output e.g. (servername.key)', null ) - ->addOption( - 'mapper-direct', - null, - InputOption::VALUE_NONE, - 'Uses less memory. However, it\'s significantly slower then default mapper.' - ) ->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use Alternative config file.'); } @@ -114,10 +106,6 @@ class ImportCommand extends Command $config = Config::get('path') . '/config/servers.yaml'; } - if ($input->getOption('mapper-direct')) { - $this->mapper = Container::get(DirectMapper::class); - } - $list = []; $serversFilter = (string)$input->getOption('servers-filter'); $selected = explode(',', $serversFilter); @@ -183,7 +171,7 @@ class ImportCommand extends Command /** @var array $queue */ $queue = []; - if (count($list) >= 1 && !$input->getOption('mapper-direct')) { + if (count($list) >= 1) { $this->logger->info('Preloading all mapper data.'); $this->mapper->loadData(); $this->logger->info('Finished preloading mapper data.'); diff --git a/src/Libs/Entity/StateEntity.php b/src/Libs/Entity/StateEntity.php index 7eb0e93a..22073fea 100644 --- a/src/Libs/Entity/StateEntity.php +++ b/src/Libs/Entity/StateEntity.php @@ -12,21 +12,21 @@ final class StateEntity implements StateInterface private array $data = []; private bool $tainted = false; - /** - * User Addressable Variables. - */ public null|string|int $id = null; public string $type = ''; public int $updated = 0; public int $watched = 0; - public array $meta = []; - public string|null $guid_plex = null; - public string|null $guid_imdb = null; - public string|null $guid_tvdb = null; - public string|null $guid_tmdb = null; - public string|null $guid_tvmaze = null; - public string|null $guid_tvrage = null; - public string|null $guid_anidb = null; + + public string $via = ''; + public string $title = ''; + + public int|null $year = null; + public int|null $season = null; + public int|null $episode = null; + + public array $parent = []; + public array $guids = []; + public array $extra = []; public function __construct(array $data) { @@ -46,8 +46,16 @@ final class StateEntity implements StateInterface ); } - if ('meta' === $key && is_string($val)) { - if (null === ($val = json_decode($val, true))) { + foreach (StateInterface::ENTITY_ARRAY_KEYS as $subKey) { + if ($subKey !== $key) { + continue; + } + + if (true === is_array($val)) { + continue; + } + + if (null === ($val = json_decode($val ?? '{}', true))) { $val = []; } } @@ -63,20 +71,12 @@ final class StateEntity implements StateInterface return new self($data); } - public function diff(): array + public function diff(bool $all = false): array { $changed = []; foreach ($this->getAll() as $key => $value) { - /** - * We ignore meta on purpose as it changes frequently. - * from one server to another. - */ - if ('meta' === $key && !$this->isEpisode()) { - continue; - } - - if ('meta' === $key && ($value['parent'] ?? []) === ($this->data['parent'] ?? [])) { + if (false === $all && true === in_array($key, StateInterface::ENTITY_IGNORE_DIFF_CHANGES)) { continue; } @@ -84,25 +84,21 @@ final class StateEntity implements StateInterface continue; } - if ('meta' === $key) { - $getChanged = array_diff_assoc_recursive($this->data['meta'] ?? [], $this->meta); - - foreach ($getChanged as $metaKey => $_) { - $changed['new'][$key][$metaKey] = $this->meta[$metaKey] ?? 'None'; - $changed['old'][$key][$metaKey] = $this->data[$key][$metaKey] ?? 'None'; + if (true === in_array($key, StateInterface::ENTITY_ARRAY_KEYS)) { + $changes = array_diff_assoc_recursive($this->data[$key] ?? [], $value ?? []); + if (!empty($changes)) { + foreach (array_keys($changes) as $subKey) { + $changed[$key][$subKey] = [ + 'old' => $this->data[$key][$subKey] ?? 'None', + 'new' => $value[$subKey] ?? 'None' + ]; + } } } else { - $changed['new'][$key] = $value ?? 'None'; - $changed['old'][$key] = $this->data[$key] ?? 'None'; - } - } - - if (!empty($changed) && !array_key_exists('meta', $changed['new'] ?? $changed['old'] ?? [])) { - $getChanged = array_diff_assoc_recursive($this->data['meta'] ?? [], $this->meta); - - foreach ($getChanged as $key => $_) { - $changed['new']['meta'][$key] = $this->meta[$key] ?? 'None'; - $changed['old']['meta'][$key] = $this->data['meta'][$key] ?? 'None'; + $changed[$key] = [ + 'old' => $this->data[$key] ?? 'None', + 'new' => $value ?? 'None' + ]; } } @@ -112,21 +108,16 @@ final class StateEntity implements StateInterface public function getName(): string { if ($this->isMovie()) { - return sprintf( - '%s (%d) - @%s', - $this->meta['title'] ?? $this->data['meta']['title'] ?? '??', - $this->meta['year'] ?? $this->data['meta']['year'] ?? '??', - $this->meta['via'] ?? $this->data['meta']['via'] ?? '??', - ); + return sprintf('%s (%d) - @%s', $this->title ?? '??', $this->year ?? 0000, $this->via ?? '??'); } return sprintf( - '%s (%d) - %dx%d - @%s', - $this->meta['series'] ?? $this->data['meta']['series'] ?? '??', - $this->meta['year'] ?? $this->data['meta']['year'] ?? '??', - $this->meta['season'] ?? $this->data['meta']['season'] ?? 00, - $this->meta['episode'] ?? $this->data['meta']['episode'] ?? 00, - $this->meta['via'] ?? $this->data['meta']['via'] ?? '??', + '%s (%s) - %sx%s - @%s', + $this->title ?? '??', + $this->year ?? 0000, + str_pad((string)($this->season ?? 0), 2, '0', STR_PAD_LEFT), + str_pad((string)($this->episode ?? 0), 3, '0', STR_PAD_LEFT), + $this->via ?? '??', ); } @@ -137,41 +128,35 @@ final class StateEntity implements StateInterface 'type' => $this->type, 'updated' => $this->updated, 'watched' => $this->watched, - 'meta' => $this->meta, - 'guid_plex' => $this->guid_plex, - 'guid_imdb' => $this->guid_imdb, - 'guid_tvdb' => $this->guid_tvdb, - 'guid_tmdb' => $this->guid_tmdb, - 'guid_tvmaze' => $this->guid_tvmaze, - 'guid_tvrage' => $this->guid_tvrage, - 'guid_anidb' => $this->guid_anidb, + 'via' => $this->via, + 'title' => $this->title, + 'year' => $this->year, + 'season' => $this->season, + 'episode' => $this->episode, + 'parent' => $this->parent, + 'guids' => $this->guids, + 'extra' => $this->extra, ]; } public function isChanged(): bool { - return count($this->diff()) >= 1; + return count($this->diff(all: false)) >= 1; } public function hasGuids(): bool { - foreach (array_keys(Guid::SUPPORTED) as $key) { - if (null !== $this->{$key}) { - return true; - } - } - - return false; + return count($this->guids) >= 1; } public function hasParentGuid(): bool { - return count($this->getParentGuids()) >= 1; + return count($this->parent) >= 1; } public function getParentGuids(): array { - return (array)ag($this->meta, 'parent', []); + return $this->parent; } public function isMovie(): bool @@ -186,27 +171,19 @@ final class StateEntity implements StateInterface public function hasRelativeGuid(): bool { - $parents = ag($this->meta, 'parent', []); - $season = ag($this->meta, 'season', null); - $episode = ag($this->meta, 'episode', null); - - return !(null === $season || null === $episode || 0 === $episode || empty($parents)); + return $this->isEpisode() && !empty($this->parent) && null !== $this->season && null !== $this->episode; } public function getRelativeGuids(): array { - $parents = ag($this->meta, 'parent', []); - $season = ag($this->meta, 'season', null); - $episode = ag($this->meta, 'episode', null); - - if (null === $season || null === $episode || 0 === $episode || empty($parents)) { + if (!$this->isEpisode()) { return []; } $list = []; - foreach ($parents as $key => $val) { - $list[$key] = $val . '/' . $season . '/' . $episode; + foreach ($this->parent as $key => $val) { + $list[$key] = $val . '/' . $this->season . '/' . $this->episode; } return array_intersect_key($list, Guid::SUPPORTED); @@ -214,20 +191,40 @@ final class StateEntity implements StateInterface public function getRelativePointers(): array { - return Guid::fromArray($this->getRelativeGuids())->getPointers(); + if (!$this->isEpisode()) { + return []; + } + + $list = Guid::fromArray($this->getRelativeGuids())->getPointers(); + + $rPointers = []; + + foreach ($list as $val) { + $rPointers[] = 'r' . $val; + } + + return $rPointers; } public function apply(StateInterface $entity, bool $guidOnly = false): self { + if (true === $guidOnly) { + if ($this->guids !== $entity->guids) { + $this->updateValue('guids', $entity); + } + + if ($this->parent !== $entity->parent) { + $this->updateValue('parent', $entity); + } + + return $this; + } + if ($this->isEqual($entity)) { return $this; } foreach ($entity->getAll() as $key => $val) { - if (true === $guidOnly && !str_starts_with($key, 'guid_')) { - continue; - } - $this->updateValue($key, $entity); } @@ -245,9 +242,17 @@ final class StateEntity implements StateInterface return $this->data; } - public function getPointers(): array + public function getPointers(array|null $guids = null): array { - return Guid::fromArray(array_intersect_key((array)$this, Guid::SUPPORTED))->getPointers(); + $list = array_intersect_key($this->guids, Guid::SUPPORTED); + + if ($this->isEpisode()) { + foreach ($list as $key => $val) { + $list[$key] = $val . '/' . $this->season . '/' . $this->episode; + } + } + + return Guid::fromArray($list)->getPointers(); } public function setIsTainted(bool $isTainted): StateInterface @@ -275,7 +280,7 @@ final class StateEntity implements StateInterface private function isEqualValue(string $key, StateInterface $entity): bool { - if ($key === 'updated' || $key === 'watched') { + if ('updated' === $key || 'watched' === $key) { return !($entity->updated > $this->updated && $entity->watched !== $this->watched); } @@ -288,7 +293,7 @@ final class StateEntity implements StateInterface private function updateValue(string $key, StateInterface $entity): void { - if ($key === 'updated' || $key === 'watched') { + if ('updated' === $key || 'watched' === $key) { if ($entity->updated > $this->updated && $entity->watched !== $this->watched) { $this->updated = $entity->updated; $this->watched = $entity->watched; @@ -296,12 +301,14 @@ final class StateEntity implements StateInterface return; } - if (null !== ($entity->{$key} ?? null) && $this->{$key} !== $entity->{$key}) { - if ('meta' === $key) { - $this->{$key} = array_replace_recursive($this->{$key} ?? [], $entity->{$key} ?? []); - } else { - $this->{$key} = $entity->{$key}; - } + if ('id' === $key) { + return; + } + + if (true === in_array($key, StateInterface::ENTITY_ARRAY_KEYS)) { + $this->{$key} = array_replace_recursive($this->{$key} ?? [], $entity->{$key} ?? []); + } else { + $this->{$key} = $entity->{$key}; } } } diff --git a/src/Libs/Entity/StateInterface.php b/src/Libs/Entity/StateInterface.php index 5adef224..7836cbda 100644 --- a/src/Libs/Entity/StateInterface.php +++ b/src/Libs/Entity/StateInterface.php @@ -9,19 +9,32 @@ interface StateInterface public const TYPE_MOVIE = 'movie'; public const TYPE_EPISODE = 'episode'; + public const ENTITY_IGNORE_DIFF_CHANGES = [ + 'via', + 'extra', + 'title', + 'year', + ]; + + public const ENTITY_ARRAY_KEYS = [ + 'parent', + 'guids', + 'extra' + ]; + public const ENTITY_KEYS = [ 'id', 'type', 'updated', 'watched', - 'meta', - 'guid_plex', - 'guid_imdb', - 'guid_tvdb', - 'guid_tmdb', - 'guid_tvmaze', - 'guid_tvrage', - 'guid_anidb', + 'via', + 'title', + 'year', + 'season', + 'episode', + 'parent', + 'guids', + 'extra', ]; /** @@ -36,9 +49,11 @@ interface StateInterface /** * Return An array of changed items. * + * @param bool $all check all keys. including ignored keys. + * * @return array */ - public function diff(): array; + public function diff(bool $all = false): array; /** * Get All Entity keys. diff --git a/src/Libs/Mappers/Export/ExportMapper.php b/src/Libs/Mappers/Export/ExportMapper.php index 2b4212a7..b510f0a2 100644 --- a/src/Libs/Mappers/Export/ExportMapper.php +++ b/src/Libs/Mappers/Export/ExportMapper.php @@ -4,9 +4,7 @@ declare(strict_types=1); namespace App\Libs\Mappers\Export; -use App\Libs\Container; use App\Libs\Entity\StateInterface; -use App\Libs\Guid; use App\Libs\Mappers\ExportInterface; use App\Libs\Storage\StorageInterface; use DateTimeInterface; @@ -73,40 +71,13 @@ final class ExportMapper implements ExportInterface return $this->objects[$entity->id]; } - if ($entity->hasGuids()) { - foreach ($entity->getPointers() as $key) { - if (null !== ($this->guids[$key] ?? null)) { - return $this->objects[$this->guids[$key]]; - } + foreach ($entity->getRelativePointers() as $key) { + if (null !== ($this->guids[$key] ?? null)) { + return $this->objects[$this->guids[$key]]; } } - if ($entity->isEpisode() && $entity->hasRelativeGuid()) { - foreach ($entity->getRelativePointers() as $key) { - if (null !== ($this->guids[$key] ?? null)) { - return $this->objects[$this->guids[$key]]; - } - } - } - - if (true === $this->fullyLoaded) { - return null; - } - - if (null !== ($lazyEntity = $this->storage->get($entity))) { - $this->objects[$lazyEntity->id] = $lazyEntity; - $this->addGuids($this->objects[$lazyEntity->id], $lazyEntity->id); - return $this->objects[$lazyEntity->id]; - } - - return null; - } - - public function findByIds(array $ids): null|StateInterface - { - $pointers = Guid::fromArray($ids)->getPointers(); - - foreach ($pointers as $key) { + foreach ($entity->getPointers() as $key) { if (null !== ($this->guids[$key] ?? null)) { return $this->objects[$this->guids[$key]]; } @@ -116,8 +87,6 @@ final class ExportMapper implements ExportInterface return null; } - $entity = Container::get(StateInterface::class)::fromArray($ids); - if (null !== ($lazyEntity = $this->storage->get($entity))) { $this->objects[$lazyEntity->id] = $lazyEntity; $this->addGuids($this->objects[$lazyEntity->id], $lazyEntity->id); @@ -169,16 +138,12 @@ final class ExportMapper implements ExportInterface private function addGuids(StateInterface $entity, int|string $pointer): void { - if ($entity->hasGuids()) { - foreach ($entity->getPointers() as $key) { - $this->guids[$key] = $pointer; - } + foreach ($entity->getPointers() as $key) { + $this->guids[$key] = $pointer; } - if ($entity->isEpisode() && $entity->hasRelativeGuid()) { - foreach ($entity->getRelativePointers() as $key) { - $this->guids[$key] = $pointer; - } + foreach ($entity->getRelativePointers() as $key) { + $this->guids[$key] = $pointer; } } } diff --git a/src/Libs/Mappers/ExportInterface.php b/src/Libs/Mappers/ExportInterface.php index f9ec4e2d..55a2f7fb 100644 --- a/src/Libs/Mappers/ExportInterface.php +++ b/src/Libs/Mappers/ExportInterface.php @@ -48,15 +48,6 @@ interface ExportInterface */ public function get(StateInterface $entity): null|StateInterface; - /** - * Find Entity By Ids. - * - * @param array $ids - * - * @return StateInterface|null - */ - public function findByIds(array $ids): null|StateInterface; - /** * Has Entity. * diff --git a/src/Libs/Mappers/Import/DirectMapper.php b/src/Libs/Mappers/Import/DirectMapper.php deleted file mode 100644 index 776f0b28..00000000 --- a/src/Libs/Mappers/Import/DirectMapper.php +++ /dev/null @@ -1,177 +0,0 @@ - ['added' => 0, 'updated' => 0, 'failed' => 0], - StateInterface::TYPE_EPISODE => ['added' => 0, 'updated' => 0, 'failed' => 0], - ]; - - private int $changed = 0; - - public function __construct(private LoggerInterface $logger, private StorageInterface $storage) - { - } - - public function setUp(array $opts): ImportInterface - { - return $this; - } - - public function loadData(DateTimeInterface|null $date = null): ImportInterface - { - return $this; - } - - public function add(string $bucket, string $name, StateInterface $entity, array $opts = []): self - { - if (!$entity->hasGuids() && $entity->hasRelativeGuid()) { - $this->logger->info(sprintf('Ignoring %s. No valid GUIDs.', $name)); - Data::increment($bucket, $entity->type . '_failed_no_guid'); - return $this; - } - - $item = $this->get($entity); - - if (null === $entity->id && null === $item) { - try { - $this->storage->insert($entity); - } catch (Throwable $e) { - $this->operations[$entity->type]['failed']++; - Data::append($bucket, 'storage_error', $e->getMessage()); - return $this; - } - - $this->changed++; - Data::increment($bucket, $entity->type . '_added'); - $this->operations[$entity->type]['added']++; - $this->logger->debug(sprintf('Adding %s. As new Item.', $name)); - return $this; - } - - // -- Ignore old item. - if (null !== ($opts['after'] ?? null) && ($opts['after'] instanceof DateTimeInterface)) { - if ($opts['after']->getTimestamp() >= $entity->updated) { - // -- check for updated GUIDs. - if ($item->apply($entity, guidOnly: true)->isChanged()) { - try { - $this->changed++; - if (!empty($entity->meta)) { - $item->meta = $entity->meta; - } - $this->storage->update($item); - $this->operations[$entity->type]['updated']++; - $this->logger->debug(sprintf('Updating %s. GUIDs.', $name), $item->diff()); - return $this; - } catch (Throwable $e) { - $this->operations[$entity->type]['failed']++; - Data::append($bucket, 'storage_error', $e->getMessage()); - return $this; - } - } - - $this->logger->debug(sprintf('Ignoring %s. No change since last sync.', $name)); - Data::increment($bucket, $entity->type . '_ignored_not_played_since_last_sync'); - return $this; - } - } - - $item = $item->apply($entity); - - if ($item->isChanged()) { - try { - $this->storage->update($item); - } catch (Throwable $e) { - $this->operations[$entity->type]['failed']++; - Data::append($bucket, 'storage_error', $e->getMessage()); - return $this; - } - - $this->changed++; - Data::increment($bucket, $entity->type . '_updated'); - $this->operations[$entity->type]['updated']++; - } else { - Data::increment($bucket, $entity->type . '_ignored_no_change'); - } - - return $this; - } - - public function get(StateInterface $entity): null|StateInterface - { - return $this->storage->get($entity); - } - - public function remove(StateInterface $entity): bool - { - return $this->storage->remove($entity); - } - - public function commit(): mixed - { - $op = $this->operations; - - $this->reset(); - - return $op; - } - - public function has(StateInterface $entity): bool - { - return null !== $this->storage->get($entity); - } - - public function reset(): self - { - $this->changed = 0; - - $this->operations[StateInterface::TYPE_EPISODE]['added'] = 0; - $this->operations[StateInterface::TYPE_EPISODE]['updated'] = 0; - $this->operations[StateInterface::TYPE_EPISODE]['failed'] = 0; - $this->operations[StateInterface::TYPE_MOVIE]['added'] = 0; - $this->operations[StateInterface::TYPE_MOVIE]['updated'] = 0; - $this->operations[StateInterface::TYPE_MOVIE]['failed'] = 0; - - return $this; - } - - public function getObjects(array $opts = []): array - { - return []; - } - - public function getObjectsCount(): int - { - return 0; - } - - public function setLogger(LoggerInterface $logger): self - { - $this->logger = $logger; - $this->storage->setLogger($logger); - return $this; - } - - public function setStorage(StorageInterface $storage): self - { - $this->storage = $storage; - return $this; - } - - public function count(): int - { - return $this->changed; - } -} diff --git a/src/Libs/Mappers/Import/MemoryMapper.php b/src/Libs/Mappers/Import/MemoryMapper.php index c5a72a37..b968bb6a 100644 --- a/src/Libs/Mappers/Import/MemoryMapper.php +++ b/src/Libs/Mappers/Import/MemoryMapper.php @@ -52,7 +52,7 @@ final class MemoryMapper implements ImportInterface continue; } $this->objects[$entity->id] = $entity; - $this->addGuids($this->objects[$entity->id], $entity->id); + $this->addPointers($this->objects[$entity->id], $entity->id); } return $this; @@ -73,7 +73,7 @@ final class MemoryMapper implements ImportInterface $this->changed[$pointer] = $pointer; Data::increment($bucket, $entity->type . '_added'); - $this->addGuids($this->objects[$pointer], $pointer); + $this->addPointers($this->objects[$pointer], $pointer); $this->logger->debug(sprintf('Adding %s. As new Item.', $name)); return $this; @@ -82,15 +82,17 @@ final class MemoryMapper implements ImportInterface // -- Ignore old item. if (null !== ($opts['after'] ?? null) && ($opts['after'] instanceof DateTimeInterface)) { if ($opts['after']->getTimestamp() >= $entity->updated) { + $cloned = clone $this->objects[$pointer]; // -- check for updated GUIDs. if ($this->objects[$pointer]->apply($entity, guidOnly: true)->isChanged()) { $this->changed[$pointer] = $pointer; - if (!empty($entity->meta)) { - $this->objects[$pointer]->meta = $entity->meta; - } Data::increment($bucket, $entity->type . '_updated'); - $this->addGuids($this->objects[$pointer], $pointer); - $this->logger->debug(sprintf('Updating %s. GUIDs.', $name), $this->objects[$pointer]->diff()); + $this->removePointers($cloned); + $this->addPointers($this->objects[$pointer], $pointer); + $this->logger->debug(sprintf('Updating %s. Parent & Entity GUIDs.', $name), [ + 'changes' => $this->objects[$pointer]->diff(), + ]); + return $this; } @@ -102,11 +104,15 @@ final class MemoryMapper implements ImportInterface $this->objects[$pointer] = $this->objects[$pointer]->apply($entity); - if ($this->objects[$pointer]->isChanged()) { + $cloned = clone $this->objects[$pointer]; + if (true === $this->objects[$pointer]->isChanged()) { Data::increment($bucket, $entity->type . '_updated'); $this->changed[$pointer] = $pointer; - $this->addGuids($this->objects[$pointer], $pointer); - $this->logger->debug(sprintf('Updating %s. State changed.', $name), $this->objects[$pointer]->diff()); + $this->removePointers($cloned); + $this->addPointers($this->objects[$pointer], $pointer); + $this->logger->debug(sprintf('Updating %s. State changed.', $name), [ + 'changes' => $this->objects[$pointer]->diff(all: true), + ]); return $this; } @@ -122,34 +128,7 @@ final class MemoryMapper implements ImportInterface return $this->objects[$entity->id]; } - if ($entity->hasGuids()) { - foreach ($entity->getPointers() as $key) { - if (null !== ($this->guids[$key] ?? null)) { - return $this->objects[$this->guids[$key]]; - } - } - } - - if ($entity->isEpisode() && $entity->hasRelativeGuid()) { - foreach ($entity->getRelativePointers() as $key) { - if (null !== ($this->guids[$key] ?? null)) { - return $this->objects[$this->guids[$key]]; - } - } - } - - if (true === $this->fullyLoaded) { - return null; - } - - if (null !== ($lazyEntity = $this->storage->get($entity))) { - $this->objects[] = $lazyEntity; - $id = array_key_last($this->objects); - $this->addGuids($this->objects[$id], $id); - return $this->objects[$id]; - } - - return null; + return false === ($pointer = $this->getPointer($entity)) ? null : $this->objects[$pointer]; } public function remove(StateInterface $entity): bool @@ -160,21 +139,7 @@ final class MemoryMapper implements ImportInterface $this->storage->remove($this->objects[$pointer]); - if ($entity->hasGuids()) { - foreach ($entity->getPointers() as $key) { - if (null !== ($this->guids[$key] ?? null)) { - unset($this->guids[$key]); - } - } - } - - if ($entity->isEpisode() && $entity->hasRelativeGuid()) { - foreach ($entity->getRelativePointers() as $key) { - if (null !== ($this->guids[$key] ?? null)) { - unset($this->guids[$key]); - } - } - } + $this->removePointers($this->objects[$pointer]); unset($this->objects[$pointer]); @@ -246,41 +211,34 @@ final class MemoryMapper implements ImportInterface */ private function getPointer(StateInterface $entity): int|bool { - foreach ($entity->getPointers() as $key) { + foreach ([...$entity->getRelativePointers(), ...$entity->getPointers()] as $key) { if (null !== ($this->guids[$key] ?? null)) { return $this->guids[$key]; } } - if ($entity->isEpisode()) { - foreach ($entity->getRelativePointers() as $key) { - if (null !== ($this->guids[$key] ?? null)) { - return $this->guids[$key]; - } - } - } - if (false === $this->fullyLoaded && null !== ($lazyEntity = $this->storage->get($entity))) { $this->objects[] = $lazyEntity; $id = array_key_last($this->objects); - $this->addGuids($this->objects[$id], $id); + $this->addPointers($this->objects[$id], $id); return $id; } return false; } - private function addGuids(StateInterface $entity, int $pointer): void + private function addPointers(StateInterface $entity, int $pointer): void { - if ($entity->hasGuids()) { - foreach ($entity->getPointers() as $key) { - $this->guids[$key] = $pointer; - } + foreach ([...$entity->getPointers(), ...$entity->getRelativePointers()] as $key) { + $this->guids[$key] = $pointer; } + } - if ($entity->isEpisode()) { - foreach ($entity->getRelativePointers() as $key) { - $this->guids[$key] = $pointer; + private function removePointers(StateInterface $entity): void + { + foreach ([...$entity->getPointers(), ...$entity->getRelativePointers()] as $key) { + if (isset($this->guids[$key])) { + unset($this->guids[$key]); } } } diff --git a/src/Libs/Servers/EmbyServer.php b/src/Libs/Servers/EmbyServer.php index 3c8f54eb..6c44dd41 100644 --- a/src/Libs/Servers/EmbyServer.php +++ b/src/Libs/Servers/EmbyServer.php @@ -151,7 +151,7 @@ class EmbyServer extends JellyfinServer 'updated' => time(), 'watched' => $isWatched, 'meta' => $meta, - ...$this->getGuids($providersId, $type) + ...$this->getGuids($providersId) ]; $entity = Container::get(StateInterface::class)::fromArray($row)->setIsTainted($isTainted); diff --git a/src/Libs/Servers/JellyfinServer.php b/src/Libs/Servers/JellyfinServer.php index 1254d96c..37c06ebc 100644 --- a/src/Libs/Servers/JellyfinServer.php +++ b/src/Libs/Servers/JellyfinServer.php @@ -38,7 +38,6 @@ use Throwable; class JellyfinServer implements ServerInterface { protected const GUID_MAPPER = [ - 'plex' => Guid::GUID_PLEX, 'imdb' => Guid::GUID_IMDB, 'tmdb' => Guid::GUID_TMDB, 'tvdb' => Guid::GUID_TVDB, @@ -328,7 +327,7 @@ class JellyfinServer implements ServerInterface 'updated' => time(), 'watched' => (int)(bool)ag($json, 'Played', ag($json, 'PlayedToCompletion', 0)), 'meta' => $meta, - ...$this->getGuids($providersId, $type) + ...$this->getGuids($providersId) ]; $entity = Container::get(StateInterface::class)::fromArray($row)->setIsTainted($isTainted); @@ -348,16 +347,8 @@ class JellyfinServer implements ServerInterface ); } - if ($entity->hasGuids()) { - foreach ($entity->getPointers() as $guid) { - $this->cacheData[$guid] = ag($json, 'Item.ItemId'); - } - } - - if ($entity->isEpisode() && $entity->hasRelativeGuid()) { - foreach ($entity->getPointers() as $guid) { - $this->cacheData[$guid] = ag($json, 'Item.ItemId'); - } + foreach ([...$entity->getRelativePointers(), ...$entity->getPointers()] as $guid) { + $this->cacheData[$guid] = ag($json, 'Item.ItemId'); } if (false === $isTainted && (true === Config::get('webhook.debug') || null !== ag( @@ -949,24 +940,12 @@ class JellyfinServer implements ServerInterface $entity->jf_id = null; - if ($entity->hasGuids()) { - foreach ($entity->getPointers() as $guid) { - if (null === ($this->cacheData[$guid] ?? null)) { - continue; - } - $entity->jf_id = $this->cacheData[$guid]; - break; - } - } - - if ($entity->isEpisode() && $entity->hasRelativeGuid()) { - foreach ($entity->getRelativePointers() as $guid) { - if (null === ($this->cacheData[$guid] ?? null)) { - continue; - } - $entity->jf_id = $this->cacheData[$guid]; - break; + foreach ([...$entity->getRelativePointers(), ...$entity->getPointers()] as $guid) { + if (null === ($this->cacheData[$guid] ?? null)) { + continue; } + $entity->jf_id = $this->cacheData[$guid]; + break; } } @@ -1589,7 +1568,7 @@ class JellyfinServer implements ServerInterface } } - protected function getGuids(array $ids, string|null $type = null): array + protected function getGuids(array $ids): array { $guid = []; @@ -1600,13 +1579,17 @@ class JellyfinServer implements ServerInterface continue; } - if (null !== $type) { - $value = $type . '/' . $value; + if (null !== ($guid[self::GUID_MAPPER[$key]] ?? null) && ctype_digit($value)) { + if ((int)$guid[self::GUID_MAPPER[$key]] > (int)$value) { + continue; + } } $guid[self::GUID_MAPPER[$key]] = $value; } + ksort($guid); + return $guid; } @@ -1661,49 +1644,43 @@ class JellyfinServer implements ServerInterface { $date = strtotime($item->UserData?->LastPlayedDate ?? $item->DateCreated ?? $item->PremiereDate); + /** @noinspection PhpArrayIndexImmediatelyRewrittenInspection */ + $row = [ + 'type' => $type, + 'updated' => $date, + 'watched' => (int)(bool)($item->UserData?->Played ?? false), + 'via' => $this->name, + 'title' => '??', + 'year' => $item->ProductionYear ?? 0000, + 'season' => null, + 'episode' => null, + 'parent' => [], + 'guids' => $this->getGuids((array)($item->ProviderIds ?? [])), + 'extra' => [ + 'date' => makeDate($item->PremiereDate ?? $item->ProductionYear ?? 'now')->format('Y-m-d'), + ], + ]; + if (StateInterface::TYPE_MOVIE === $type) { - $meta = [ - 'via' => $this->name, - 'title' => $item->Name ?? $item->OriginalTitle ?? '??', - 'year' => $item->ProductionYear ?? 0000, - 'date' => makeDate($item->PremiereDate ?? $item->ProductionYear ?? 'now')->format('Y-m-d'), - ]; + $row['title'] = $item->Name ?? $item->OriginalTitle ?? '??'; } else { - $meta = [ - 'via' => $this->name, - 'series' => $item->SeriesName ?? '??', - 'year' => $item->ProductionYear ?? 0000, - 'season' => $item->ParentIndexNumber ?? 0, - 'episode' => $item->IndexNumber ?? 0, - 'title' => $item->Name ?? '', - 'date' => makeDate($item->PremiereDate ?? $item->ProductionYear ?? 'now')->format('Y-m-d'), - ]; + $row['title'] = $item->SeriesName ?? '??'; + $row['season'] = $item->ParentIndexNumber ?? 0; + $row['episode'] = $item->IndexNumber ?? 0; + + if (null !== ($item->Name ?? null)) { + $row['extra']['title'] = $item->Name; + } if (null !== ($item->SeriesId ?? null)) { - $meta['parent'] = $this->showInfo[$item->SeriesId] ?? []; + $row['parent'] = $this->showInfo[$item->SeriesId] ?? []; } } - $entity = Container::get(StateInterface::class)::fromArray( - [ - 'type' => $type, - 'updated' => $date, - 'watched' => (int)(bool)($item->UserData?->Played ?? false), - 'meta' => $meta, - ...$this->getGuids((array)($item->ProviderIds ?? []), $type), - ] - ); + $entity = Container::get(StateInterface::class)::fromArray($row); - if ($entity->hasGuids()) { - foreach ($entity->getPointers() as $guid) { - $this->cacheData[$guid] = $item->Id; - } - } - - if ($entity->isEpisode()) { - foreach ($entity->getRelativePointers() as $guid) { - $this->cacheData[$guid] = $item->Id; - } + foreach ([...$entity->getRelativePointers(), ...$entity->getPointers()] as $guid) { + $this->cacheData[$guid] = $item->Id; } return $entity; diff --git a/src/Libs/Servers/PlexServer.php b/src/Libs/Servers/PlexServer.php index a94686b7..a861f148 100644 --- a/src/Libs/Servers/PlexServer.php +++ b/src/Libs/Servers/PlexServer.php @@ -375,7 +375,7 @@ class PlexServer implements ServerInterface 'updated' => time(), 'watched' => (int)(bool)ag($item, 'viewCount', 0), 'meta' => $meta, - ...$this->getGuids(ag($item, 'Guid', []), $type, isParent: false) + ...$this->getGuids(ag($item, 'Guid', []), isParent: false) ]; $entity = Container::get(StateInterface::class)::fromArray($row)->setIsTainted($isTainted); @@ -395,16 +395,8 @@ class PlexServer implements ServerInterface ); } - if ($entity->hasGuids()) { - foreach ($entity->getPointers() as $guid) { - $this->cacheData[$guid] = ag($item, 'guid'); - } - } - - if ($entity->isEpisode() && $entity->hasRelativeGuid()) { - foreach ($entity->getRelativePointers() as $guid) { - $this->cacheData[$guid] = ag($item, 'guid'); - } + foreach ([...$entity->getRelativePointers(), ...$entity->getPointers()] as $guid) { + $this->cacheData[$guid] = ag($item, 'guid'); } if (false !== $isTainted && (true === Config::get('webhook.debug') || null !== ag( @@ -977,29 +969,17 @@ class PlexServer implements ServerInterface $entity->plex_id = null; - if (null !== $entity->guid_plex) { - $entity->plex_id = 'plex://' . $entity->guid_plex; + if (null !== ($entity->guids[Guid::GUID_PLEX] ?? null)) { + $entity->plex_id = 'plex://' . $entity->guids[Guid::GUID_PLEX]; continue; } - if ($entity->hasGuids()) { - foreach ($entity->getPointers() as $guid) { - if (null === ($this->cacheData[$guid] ?? null)) { - continue; - } - $entity->plex_id = $this->cacheData[$guid]; - break; - } - } - - if ($entity->isEpisode() && $entity->hasRelativeGuid()) { - foreach ($entity->getRelativePointers() as $guid) { - if (null === ($this->cacheData[$guid] ?? null)) { - continue; - } - $entity->plex_id = $this->cacheData[$guid]; - break; + foreach ([...$entity->getRelativePointers(), ...$entity->getPointers()] as $guid) { + if (null === ($this->cacheData[$guid] ?? null)) { + continue; } + $entity->plex_id = $this->cacheData[$guid]; + break; } } @@ -1577,13 +1557,12 @@ class PlexServer implements ServerInterface } else { $iName = trim( sprintf( - '%s - %s - [%s - (%dx%d) - %s]', + '%s - %s - [%s - (%dx%d)]', $this->name, $library, $item->grandparentTitle ?? $item->originalTitle ?? '??', $item->parentIndex ?? 0, $item->index ?? 0, - $item->title ?? $item->originalTitle ?? '', ) ); } @@ -1657,7 +1636,7 @@ class PlexServer implements ServerInterface } } - protected function getGuids(array $guids, string|null $type = null, bool $isParent = false): array + protected function getGuids(array $guids, bool $isParent = false): array { $guid = []; @@ -1679,13 +1658,17 @@ class PlexServer implements ServerInterface continue; } - if ('plex' !== $key && null !== $type) { - $value = $type . '/' . $value; + if (null !== ($guid[self::GUID_MAPPER[$key]] ?? null) && ctype_digit($val)) { + if ((int)$guid[self::GUID_MAPPER[$key]] > (int)$val) { + continue; + } } $guid[self::GUID_MAPPER[$key]] = $value; } + ksort($guid); + return $guid; } @@ -1877,51 +1860,42 @@ class PlexServer implements ServerInterface $date = (int)($item->lastViewedAt ?? $item->updatedAt ?? $item->addedAt ?? 0); + /** @noinspection PhpArrayIndexImmediatelyRewrittenInspection */ + $row = [ + 'type' => $type, + 'updated' => $date, + 'watched' => (int)(bool)($item->viewCount ?? false), + 'via' => $this->name, + 'title' => '??', + 'year' => (int)($item->grandParentYear ?? $item->parentYear ?? $item->year ?? 0000), + 'season' => null, + 'episode' => null, + 'parent' => [], + 'guids' => $this->getGuids($item->Guid ?? [], isParent: false), + 'extra' => [ + 'date' => makeDate($item->originallyAvailableAt ?? 'now')->format('Y-m-d'), + ], + ]; + if (StateInterface::TYPE_MOVIE === $type) { - $meta = [ - 'via' => $this->name, - 'title' => $item->title ?? $item->originalTitle ?? '??', - 'year' => $item->year ?? 0000, - 'date' => makeDate($item->originallyAvailableAt ?? 'now')->format('Y-m-d'), - ]; + $row['title'] = $item->title ?? $item->originalTitle ?? '??'; } else { - $meta = [ - 'via' => $this->name, - 'series' => $item->grandparentTitle ?? '??', - 'year' => $item->year ?? 0000, - 'season' => $item->parentIndex ?? 0, - 'episode' => $item->index ?? 0, - 'title' => $item->title ?? $item->originalTitle ?? '??', - 'date' => makeDate($item->originallyAvailableAt ?? 'now')->format('Y-m-d'), - ]; + $row['title'] = $item->grandparentTitle ?? '??'; + $row['season'] = $item->parentIndex ?? 0; + $row['episode'] = $item->index ?? 0; + $row['extra']['title'] = $item->title ?? $item->originalTitle ?? '??'; $parentId = $item->grandparentRatingKey ?? $item->parentRatingKey ?? null; if (null !== $parentId) { - $meta['parent'] = $this->showInfo[$parentId] ?? []; + $row['parent'] = $this->showInfo[$parentId] ?? []; } } - $entity = Container::get(StateInterface::class)::fromArray( - [ - 'type' => $type, - 'updated' => $date, - 'watched' => (int)(bool)($item->viewCount ?? false), - 'meta' => $meta, - ...$this->getGuids($item->Guid ?? [], $type, isParent: false) - ] - ); + $entity = Container::get(StateInterface::class)::fromArray($row); - if ($entity->hasGuids()) { - foreach ($entity->getPointers() as $guid) { - $this->cacheData[$guid] = $item->guid; - } - } - - if ($entity->isEpisode()) { - foreach ($entity->getRelativePointers() as $guid) { - $this->cacheData[$guid] = $item->guid; - } + foreach ([...$entity->getRelativePointers(), ...$entity->getPointers()] as $guid) { + $this->cacheData[$guid] = $item->guid; } return $entity; diff --git a/src/Libs/Storage/PDO/PDOAdapter.php b/src/Libs/Storage/PDO/PDOAdapter.php index 4f0c297c..b91b5292 100644 --- a/src/Libs/Storage/PDO/PDOAdapter.php +++ b/src/Libs/Storage/PDO/PDOAdapter.php @@ -41,8 +41,11 @@ final class PDOAdapter implements StorageInterface try { $data = $entity->getAll(); - if (is_array($data['meta'])) { - $data['meta'] = json_encode($data['meta']); + foreach (StateInterface::ENTITY_ARRAY_KEYS as $key) { + if (null !== ($data[$key] ?? null) && is_array($data[$key])) { + ksort($data[$key]); + $data[$key] = json_encode($data[$key], flags: JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } } if (null !== $data['id']) { @@ -65,7 +68,7 @@ final class PDOAdapter implements StorageInterface } catch (PDOException $e) { $this->stmt['insert'] = null; if (false === $this->viaCommit) { - $this->logger->error($e->getMessage(), $entity->meta ?? []); + $this->logger->error($e->getMessage(), $entity->getAll()); return $entity; } throw $e; @@ -76,11 +79,11 @@ final class PDOAdapter implements StorageInterface public function get(StateInterface $entity): StateInterface|null { - if ($entity->hasGuids() && null !== ($item = $this->findByGuid($entity))) { + if ($entity->isEpisode() && $entity->hasRelativeGuid() && null !== ($item = $this->findByRGuid($entity))) { return $item; } - if ($entity->isEpisode() && $entity->hasRelativeGuid() && null !== ($item = $this->findByRGuid($entity))) { + if ($entity->hasGuids() && null !== ($item = $this->findByGuid($entity))) { return $item; } @@ -113,8 +116,10 @@ final class PDOAdapter implements StorageInterface try { $data = $entity->getAll(); - if (is_array($data['meta'])) { - $data['meta'] = json_encode($data['meta']); + foreach (StateInterface::ENTITY_ARRAY_KEYS as $key) { + if (is_array($data[$key] ?? [])) { + $data[$key] = json_encode($data[$key], flags: JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } } if (null === $data['id']) { @@ -131,7 +136,7 @@ final class PDOAdapter implements StorageInterface } catch (PDOException $e) { $this->stmt['update'] = null; if (false === $this->viaCommit) { - $this->logger->error($e->getMessage(), $entity->meta ?? []); + $this->logger->error($e->getMessage(), $entity->getAll()); return $entity; } throw $e; @@ -184,19 +189,10 @@ final class PDOAdapter implements StorageInterface foreach ($entities as $entity) { try { if (null === $entity->id) { - $this->logger->info( - 'Adding ' . $entity->type . ' - [' . $entity->getName() . '].', - $entity->getAll() - ); - $this->insert($entity); $list[$entity->type]['added']++; } else { - $this->logger->info( - 'Updating ' . $entity->type . ':' . $entity->id . ' - [' . $entity->getName() . '].', - $entity->diff() - ); $this->update($entity); $list[$entity->type]['updated']++; } @@ -374,58 +370,49 @@ final class PDOAdapter implements StorageInterface { $cond = $where = []; - foreach ($entity->getParentGuids() as $key => $val) { + foreach ($entity->parent as $key => $val) { if (null === ($val ?? null)) { continue; } - $where[] = "json_extract(meta,'$.parent.{$key}') = :{$key}"; + $where[] = "JSON_EXTRACT(parent,'$.{$key}') = :{$key}"; $cond[$key] = $val; } - $sqlType = ''; - - if (null !== ($entity?->type ?? null)) { - $sqlType = 'type = :s_type AND '; - $cond['s_type'] = $entity->type; - } - $sql = "SELECT * FROM state WHERE - {$sqlType} - json_extract(meta, '$.season') = " . (int)ag($entity->meta, 'season', 0) . " + ( + type = :type AND - json_extract(meta, '$.episode') = " . (int)ag($entity->meta, 'episode', 0) . " + season = :season + AND + episode = :episode + ) AND ( " . implode(' OR ', $where) . " ) + LIMIT 1 "; - $cachedKey = md5($sql); + $cond['season'] = $entity->season; + $cond['episode'] = $entity->episode; + $cond['type'] = StateInterface::TYPE_EPISODE; - try { - if (null === ($this->stmt[$cachedKey] ?? null)) { - $this->stmt[$cachedKey] = $this->pdo->prepare($sql); - } + $stmt = $this->pdo->prepare($sql); - if (false === $this->stmt[$cachedKey]->execute($cond)) { - $this->stmt[$cachedKey] = null; - throw new StorageException('Failed to execute sql query.', 61); - } - - if (false === ($row = $this->stmt[$cachedKey]->fetch(PDO::FETCH_ASSOC))) { - return null; - } - - return $entity::fromArray($row); - } catch (PDOException|StorageException $e) { - $this->stmt[$cachedKey] = null; - throw $e; + if (false === $stmt->execute($cond)) { + throw new StorageException('Failed to execute sql query.', 61); } + + if (false === ($row = $stmt->fetch(PDO::FETCH_ASSOC))) { + return null; + } + + return $entity::fromArray($row); } /** @@ -447,50 +434,46 @@ final class PDOAdapter implements StorageInterface return $entity::fromArray($row); } - $cond = $where = []; + $guids = []; + $cond = [ + 'type' => $entity->type, + ]; foreach (array_keys(Guid::SUPPORTED) as $key) { - if (null === ($entity->{$key} ?? null)) { + if (null === ($entity->guids[$key] ?? null)) { continue; } - $where[] = "{$key} = :{$key}"; - $cond[$key] = $entity->{$key}; + $guids[] = "JSON_EXTRACT(guids,'$.{$key}') = :{$key}"; + $cond[$key] = $entity->guids[$key]; } if (empty($cond)) { return null; } - $sqlWhere = implode(' OR ', $where); + $sqlEpisode = ''; - $cachedKey = md5($sqlWhere . ($entity?->type ?? '')); - - try { - if (null === ($this->stmt[$cachedKey] ?? null)) { - $sqlType = ''; - - if (null !== ($entity?->type ?? null)) { - $sqlType = 'type = :s_type AND '; - $cond['s_type'] = $entity->type; - } - - $this->stmt[$cachedKey] = $this->pdo->prepare("SELECT * FROM state WHERE {$sqlType} {$sqlWhere}"); - } - - if (false === $this->stmt[$cachedKey]->execute($cond)) { - $this->stmt[$cachedKey] = null; - throw new StorageException('Failed to execute sql query.', 61); - } - - if (false === ($row = $this->stmt[$cachedKey]->fetch(PDO::FETCH_ASSOC))) { - return null; - } - - return $entity::fromArray($row); - } catch (PDOException|StorageException $e) { - $this->stmt[$cachedKey] = null; - throw $e; + if ($entity->isEpisode()) { + $sqlEpisode = ' AND season = :season AND episode = :episode '; + $cond['season'] = $entity->season; + $cond['episode'] = $entity->episode; } + + $sqlGuids = ' AND (' . implode(' OR ', $guids) . ' ) '; + + $sql = "SELECT * FROM state WHERE ( type = :type {$sqlEpisode} ) {$sqlGuids} LIMIT 1"; + + $stmt = $this->pdo->prepare($sql); + + if (false === $stmt->execute($cond)) { + throw new StorageException('Failed to execute sql query.', 61); + } + + if (false === ($row = $stmt->fetch(PDO::FETCH_ASSOC))) { + return null; + } + + return $entity::fromArray($row); } } diff --git a/src/Libs/helpers.php b/src/Libs/helpers.php index b89e3652..c26ffe36 100644 --- a/src/Libs/helpers.php +++ b/src/Libs/helpers.php @@ -419,7 +419,7 @@ if (!function_exists('arrayToString')) { } if (is_array($val)) { - $val = json_encode($val, flags: JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $val = '[ ' . arrayToString($val) . ' ]'; } else { $val = $val ?? 'None'; } diff --git a/tests/Fixtures/EpisodeEntity.php b/tests/Fixtures/EpisodeEntity.php index 51c9f3c7..45a0b643 100644 --- a/tests/Fixtures/EpisodeEntity.php +++ b/tests/Fixtures/EpisodeEntity.php @@ -7,29 +7,31 @@ use App\Libs\Entity\StateInterface; return [ 'id' => null, 'type' => StateInterface::TYPE_EPISODE, - 'updated' => 0, + 'updated' => 1, 'watched' => 1, - 'meta' => [ - 'via' => 'Plex@Home', - 'series' => 'Series Title', - 'year' => 2020, - 'season' => 1, - 'episode' => 2, + 'via' => 'Plex@Home', + 'title' => 'Series Title', + 'year' => 2020, + 'season' => 1, + 'episode' => 2, + 'parent' => [ + 'guid_imdb' => '510', + 'guid_tvdb' => '520', + ], + 'guids' => [ + 'guid_plex' => '6000', + 'guid_imdb' => '6100', + 'guid_tvdb' => '6200', + 'guid_tmdb' => '6300', + 'guid_tvmaze' => '6400', + 'guid_tvrage' => '6500', + 'guid_anidb' => '6600', + ], + 'extra' => [ 'title' => 'Episode Title', 'date' => '2020-01-03', 'webhook' => [ 'event' => 'media.scrobble' ], - 'parent' => [ - 'guid_imdb' => '510', - 'guid_tvdb' => '520', - ], ], - 'guid_plex' => StateInterface::TYPE_EPISODE . '/6000', - 'guid_imdb' => StateInterface::TYPE_EPISODE . '/6100', - 'guid_tvdb' => StateInterface::TYPE_EPISODE . '/6200', - 'guid_tmdb' => StateInterface::TYPE_EPISODE . '/6300', - 'guid_tvmaze' => StateInterface::TYPE_EPISODE . '/6400', - 'guid_tvrage' => StateInterface::TYPE_EPISODE . '/6500', - 'guid_anidb' => StateInterface::TYPE_EPISODE . '/6600', ]; diff --git a/tests/Fixtures/MovieEntity.php b/tests/Fixtures/MovieEntity.php index 95acbd78..3ec6a7ae 100644 --- a/tests/Fixtures/MovieEntity.php +++ b/tests/Fixtures/MovieEntity.php @@ -9,19 +9,25 @@ return [ 'type' => StateInterface::TYPE_MOVIE, 'updated' => 1, 'watched' => 1, - 'meta' => [ - 'via' => 'JF@Home', - 'title' => 'Movie Title', - 'year' => 2020, + 'via' => 'JF@Home', + 'title' => 'Movie Title', + 'year' => 2020, + 'season' => null, + 'episode' => null, + 'parent' => [], + 'guids' => [ + 'guid_plex' => '1000', + 'guid_imdb' => '1100', + 'guid_tvdb' => '1200', + 'guid_tmdb' => '1300', + 'guid_tvmaze' => '1400', + 'guid_tvrage' => '1500', + 'guid_anidb' => '1600', + ], + 'extra' => [ 'webhook' => [ 'event' => 'ItemAdded' ] ], - 'guid_plex' => StateInterface::TYPE_MOVIE . '/1000', - 'guid_imdb' => StateInterface::TYPE_MOVIE . '/1100', - 'guid_tvdb' => StateInterface::TYPE_MOVIE . '/1200', - 'guid_tmdb' => StateInterface::TYPE_MOVIE . '/1300', - 'guid_tvmaze' => StateInterface::TYPE_MOVIE . '/1400', - 'guid_tvrage' => StateInterface::TYPE_MOVIE . '/1500', - 'guid_anidb' => StateInterface::TYPE_MOVIE . '/1600', + ]; diff --git a/tests/Mappers/Import/DirectMapperTest.php b/tests/Mappers/Import/DirectMapperTest.php deleted file mode 100644 index 7969b872..00000000 --- a/tests/Mappers/Import/DirectMapperTest.php +++ /dev/null @@ -1,163 +0,0 @@ -output = new NullOutput(); - $this->input = new ArrayInput([]); - - $this->testMovie = require __DIR__ . '/../../Fixtures/MovieEntity.php'; - $this->testEpisode = require __DIR__ . '/../../Fixtures/EpisodeEntity.php'; - - $logger = new CliLogger($this->output); - - $this->storage = new PDOAdapter($logger, new PDO('sqlite::memory:')); - $this->storage->migrations('up'); - - $this->mapper = new DirectMapper($logger, $this->storage); - $this->mapper->setUp(['class' => new StateEntity([])]); - } - - public function test_add_conditions(): void - { - $testMovie = new StateEntity($this->testMovie); - $testEpisode = new StateEntity($this->testEpisode); - - // -- expect 0 as we have not modified or added new item yet. - $this->assertCount(0, $this->mapper); - - $this->mapper->add('test', 'test1', $testEpisode)->add('test', 'test2', $testMovie); - - $this->assertCount(2, $this->mapper); - - $this->assertSame( - [ - StateInterface::TYPE_MOVIE => ['added' => 1, 'updated' => 0, 'failed' => 0], - StateInterface::TYPE_EPISODE => ['added' => 1, 'updated' => 0, 'failed' => 0], - ], - $this->mapper->commit() - ); - - // -- assert 0 as we have committed the changes to the db, and the state should have been reset. - $this->assertCount(0, $this->mapper); - - $testEpisode->guid_tvrage = StateInterface::TYPE_EPISODE . '/2'; - - $this->mapper->add('test', 'test1', $testEpisode); - - $this->assertCount(1, $this->mapper); - } - - public function test_get_conditions(): void - { - $testMovie = new StateEntity($this->testMovie); - $testEpisode = new StateEntity($this->testEpisode); - - // -- expect null as we haven't added anything to db yet. - $this->assertNull($this->mapper->get($testEpisode)); - - $this->storage->commit([$testEpisode, $testMovie]); - - clone $testMovie2 = $testMovie; - clone $testEpisode2 = $testEpisode; - $testMovie2->id = 2; - $testEpisode2->id = 1; - - $this->assertSame($testEpisode2->getAll(), $this->mapper->get($testEpisode)->getAll()); - $this->assertSame($testMovie2->getAll(), $this->mapper->get($testMovie)->getAll()); - } - - public function test_remove_conditions(): void - { - $testMovie = new StateEntity($this->testMovie); - $testEpisode = new StateEntity($this->testEpisode); - - $this->assertFalse($this->mapper->remove($testEpisode)); - $this->mapper->add('test', 'episode', $testEpisode)->add('test', 'movie', $testMovie)->commit(); - $this->assertTrue($this->mapper->remove($testEpisode)); - } - - public function test_commit_conditions(): void - { - $testMovie = new StateEntity($this->testMovie); - $testEpisode = new StateEntity($this->testEpisode); - - // -- expect 0 as we have not modified or added new item yet. - $this->assertCount(0, $this->mapper); - - $this->mapper->add('test', 'test1', $testEpisode)->add('test', 'test2', $testMovie); - - $this->assertCount(2, $this->mapper); - - $this->assertSame( - [ - StateInterface::TYPE_MOVIE => ['added' => 1, 'updated' => 0, 'failed' => 0], - StateInterface::TYPE_EPISODE => ['added' => 1, 'updated' => 0, 'failed' => 0], - ], - $this->mapper->commit() - ); - - $this->assertSame( - [ - StateInterface::TYPE_MOVIE => ['added' => 0, 'updated' => 0, 'failed' => 0], - StateInterface::TYPE_EPISODE => ['added' => 0, 'updated' => 0, 'failed' => 0], - ], - $this->mapper->commit() - ); - - $testEpisode->guid_tvrage = StateInterface::TYPE_EPISODE . '/1'; - $testMovie->guid_tvrage = StateInterface::TYPE_MOVIE . '/1'; - - $this->mapper->add('test', 'test1', $testEpisode)->add('test', 'test2', $testMovie); - - $this->assertSame( - [ - StateInterface::TYPE_MOVIE => ['added' => 0, 'updated' => 1, 'failed' => 0], - StateInterface::TYPE_EPISODE => ['added' => 0, 'updated' => 1, 'failed' => 0], - ], - $this->mapper->commit() - ); - } - - public function test_has_conditions(): void - { - $testEpisode = new StateEntity($this->testEpisode); - $this->assertFalse($this->mapper->has($testEpisode)); - $this->storage->commit([$testEpisode]); - $this->assertTrue($this->mapper->has($testEpisode)); - } - - public function test_reset_conditions(): void - { - $testEpisode = new StateEntity($this->testEpisode); - $this->assertCount(0, $this->mapper); - - $this->mapper->add('test', 'episode', $testEpisode); - $this->assertCount(1, $this->mapper); - - $this->mapper->reset(); - $this->assertCount(0, $this->mapper); - } -} diff --git a/tests/Mappers/Import/MemoryMapperTest.php b/tests/Mappers/Import/MemoryMapperTest.php index cd3f8f44..fd604900 100644 --- a/tests/Mappers/Import/MemoryMapperTest.php +++ b/tests/Mappers/Import/MemoryMapperTest.php @@ -97,7 +97,7 @@ class MemoryMapperTest extends TestCase // -- assert 0 as we have committed the changes to the db, and the state should have been reset. $this->assertCount(0, $this->mapper); - $testEpisode->guid_tvrage = StateInterface::TYPE_EPISODE . '/2'; + $testEpisode->guids['guid_tvrage'] = '2'; $this->mapper->add('test', 'test1', $testEpisode); @@ -116,8 +116,19 @@ class MemoryMapperTest extends TestCase public function test_get_conditions(): void { - $testMovie = new StateEntity($this->testMovie); - $testEpisode = new StateEntity($this->testEpisode); + $movie = $this->testMovie; + $episode = $this->testEpisode; + + ksort($movie['parent']); + ksort($movie['guids']); + ksort($movie['extra']); + + ksort($episode['parent']); + ksort($episode['guids']); + ksort($episode['extra']); + + $testMovie = new StateEntity($movie); + $testEpisode = new StateEntity($episode); // -- expect null as we haven't added anything to db yet. $this->assertNull($this->mapper->get($testEpisode)); @@ -168,8 +179,8 @@ class MemoryMapperTest extends TestCase $this->mapper->commit() ); - $testMovie->guid_anidb = StateInterface::TYPE_MOVIE . '/1'; - $testEpisode->guid_anidb = StateInterface::TYPE_EPISODE . '/1'; + $testMovie->guids['guid_anidb'] = '1'; + $testEpisode->guids['guid_anidb'] = '1'; $this->assertSame( [ diff --git a/tests/Storage/PDOAdapterTest.php b/tests/Storage/PDOAdapterTest.php index 577fd22d..c2e14290 100644 --- a/tests/Storage/PDOAdapterTest.php +++ b/tests/Storage/PDOAdapterTest.php @@ -53,7 +53,13 @@ class PDOAdapterTest extends TestCase public function test_get_conditions(): void { - $item = new StateEntity($this->testEpisode); + $test = $this->testEpisode; + + ksort($test['parent']); + ksort($test['guids']); + ksort($test['extra']); + + $item = new StateEntity($test); // -- db should be empty at this stage. as such we expect null. $this->assertNull($this->storage->get($item)); @@ -100,7 +106,7 @@ class PDOAdapterTest extends TestCase public function test_update_conditions(): void { $item = $this->storage->insert(new StateEntity($this->testEpisode)); - $item->guid_plex = StateInterface::TYPE_EPISODE . '/1000'; + $item->guids['guid_plex'] = StateInterface::TYPE_EPISODE . '/1000'; $updatedItem = $this->storage->update($item); @@ -140,8 +146,8 @@ class PDOAdapterTest extends TestCase $this->storage->commit([$item1, $item2]) ); - $item1->guid_anidb = StateInterface::TYPE_EPISODE . '/1'; - $item2->guid_anidb = StateInterface::TYPE_MOVIE . '/1'; + $item1->guids['guid_anidb'] = StateInterface::TYPE_EPISODE . '/1'; + $item2->guids['guid_anidb'] = StateInterface::TYPE_MOVIE . '/1'; $this->assertSame( [