From 77ee6ae50ed670be65b54bb1b2600474d924239d Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Mon, 4 May 2020 17:13:03 -0400 Subject: [PATCH] Refactor list sync to be easier to follow --- src/AnimeClient/API/APIRequestBuilder.php | 6 +- src/AnimeClient/API/Kitsu/Model.php | 212 +++++---- src/AnimeClient/Command/BaseCommand.php | 22 +- src/AnimeClient/Command/SyncLists.php | 509 +++++++++++++--------- src/AnimeClient/Types/FormItemData.php | 2 +- 5 files changed, 467 insertions(+), 284 deletions(-) diff --git a/src/AnimeClient/API/APIRequestBuilder.php b/src/AnimeClient/API/APIRequestBuilder.php index d638935c..4d4113f9 100644 --- a/src/AnimeClient/API/APIRequestBuilder.php +++ b/src/AnimeClient/API/APIRequestBuilder.php @@ -79,6 +79,8 @@ abstract class APIRequestBuilder { { $request = (new Request($uri)); $request->setHeader('User-Agent', USER_AGENT); + $request->setTcpConnectTimeout(300000); + $request->setTransferTimeout(300000); return $request; } @@ -269,7 +271,7 @@ abstract class APIRequestBuilder { */ public function newRequest(string $type, string $uri): self { - if ( ! \in_array($type, $this->validMethods, TRUE)) + if ( ! in_array($type, $this->validMethods, TRUE)) { throw new InvalidArgumentException('Invalid HTTP method'); } @@ -327,6 +329,8 @@ abstract class APIRequestBuilder { $this->path = ''; $this->query = ''; $this->request = new Request($requestUrl, $type); + $this->request->setInactivityTimeout(300000); + $this->request->setTlsHandshakeTimeout(300000); $this->request->setTcpConnectTimeout(300000); $this->request->setTransferTimeout(300000); } diff --git a/src/AnimeClient/API/Kitsu/Model.php b/src/AnimeClient/API/Kitsu/Model.php index 9b43dc1d..8571e30d 100644 --- a/src/AnimeClient/API/Kitsu/Model.php +++ b/src/AnimeClient/API/Kitsu/Model.php @@ -38,6 +38,7 @@ use Aviat\AnimeClient\API\Kitsu\Transformer\{ MangaTransformer, MangaListTransformer }; +use Aviat\AnimeClient\Enum\ListType; use Aviat\AnimeClient\Types\{ Anime, FormItem, @@ -175,38 +176,6 @@ final class Model { return FALSE; } - /** - * Retrieve the data for the anime watch history page - * - * @return array - * @throws InvalidArgumentException - * @throws Throwable - */ - public function getAnimeHistory(): array - { - $raw = $this->getRawHistoryList('anime'); - $organized = JsonAPI::organizeData($raw); - $organized = array_filter($organized, fn ($item) => array_key_exists('relationships', $item)); - - return (new AnimeHistoryTransformer())->transform($organized); - } - - /** - * Retrieve the data for the manga read history page - * - * @return array - * @throws InvalidArgumentException - * @throws Throwable - */ - public function getMangaHistory(): array - { - $raw = $this->getRawHistoryList('manga'); - $organized = JsonAPI::organizeData($raw); - $organized = array_filter($organized, fn ($item) => array_key_exists('relationships', $item)); - - return (new MangaHistoryTransformer())->transform($organized); - } - /** * Get the userid for a username from Kitsu * @@ -420,6 +389,22 @@ final class Model { return $this->animeTransformer->transform($baseData); } + /** + * Retrieve the data for the anime watch history page + * + * @return array + * @throws InvalidArgumentException + * @throws Throwable + */ + public function getAnimeHistory(): array + { + $raw = $this->getRawHistoryList('anime'); + $organized = JsonAPI::organizeData($raw); + $organized = array_filter($organized, fn ($item) => array_key_exists('relationships', $item)); + + return (new AnimeHistoryTransformer())->transform($organized); + } + /** * Get information about a particular anime * @@ -485,27 +470,7 @@ final class Model { */ public function getAnimeListCount(string $status = '') : int { - $options = [ - 'query' => [ - 'filter' => [ - 'user_id' => $this->getUserIdByUsername(), - 'kind' => 'anime' - ], - 'page' => [ - 'limit' => 1 - ], - 'sort' => '-updated_at' - ] - ]; - - if ( ! empty($status)) - { - $options['query']['filter']['status'] = $status; - } - - $response = $this->getRequest('library-entries', $options); - - return $response['meta']['count']; + return $this->getListCount(ListType::ANIME, $status); } /** @@ -676,6 +641,22 @@ final class Model { return $this->mangaTransformer->transform($baseData); } + /** + * Retrieve the data for the manga read history page + * + * @return array + * @throws InvalidArgumentException + * @throws Throwable + */ + public function getMangaHistory(): array + { + $raw = $this->getRawHistoryList('manga'); + $organized = JsonAPI::organizeData($raw); + $organized = array_filter($organized, fn ($item) => array_key_exists('relationships', $item)); + + return (new MangaHistoryTransformer())->transform($organized); + } + /** * Get information about a particular manga * @@ -754,27 +735,7 @@ final class Model { */ public function getMangaListCount(string $status = '') : int { - $options = [ - 'query' => [ - 'filter' => [ - 'user_id' => $this->getUserIdByUsername(), - 'kind' => 'manga' - ], - 'page' => [ - 'limit' => 1 - ], - 'sort' => '-updated_at' - ] - ]; - - if ( ! empty($status)) - { - $options['query']['filter']['status'] = $status; - } - - $response = $this->getRequest('library-entries', $options); - - return $response['meta']['count']; + return $this->getListCount(ListType::MANGA, $status); } /** @@ -976,6 +937,20 @@ final class Model { return $this->listItem->delete($id); } + public function getSyncList(string $type): array + { + $options = [ + 'filter' => [ + 'user_id' => $this->getUserIdByUsername($this->getUsername()), + 'kind' => $type, + ], + 'include' => "{$type},{$type}.mappings", + 'sort' => '-updated_at' + ]; + + return $this->getRawSyncList($type, $options); + } + /** * Get the aggregated pages of anime or manga history * @@ -1124,4 +1099,93 @@ final class Model { $baseData['included'] = $data['included']; return $baseData; } + + private function getListCount(string $type, string $status = ''): int + { + $options = [ + 'query' => [ + 'filter' => [ + 'user_id' => $this->getUserIdByUsername(), + 'kind' => $type, + ], + 'page' => [ + 'limit' => 1 + ], + 'sort' => '-updated_at' + ] + ]; + + if ( ! empty($status)) + { + $options['query']['filter']['status'] = $status; + } + + $response = $this->getRequest('library-entries', $options); + + return $response['meta']['count']; + } + + /** + * Get the full anime list + * + * @param string $type + * @param array $options + * @return array + * @throws InvalidArgumentException + * @throws Throwable + */ + private function getRawSyncList(string $type, array $options): array + { + $count = $this->getListCount($type); + $size = static::LIST_PAGE_SIZE; + $pages = ceil($count / $size); + + $requester = new ParallelAPIRequest(); + + // Set up requests + for ($i = 0; $i < $pages; $i++) + { + $offset = $i * $size; + $requester->addRequest($this->getRawSyncListPage($type, $size, $offset, $options)); + } + + $responses = $requester->makeRequests(); + $output = []; + + foreach($responses as $response) + { + $data = Json::decode($response); + $output[] = $data; + } + + return array_merge_recursive(...$output); + } + + /** + * Get the full anime list in paginated form + * + * @param string $type + * @param int $limit + * @param int $offset + * @param array $options + * @return Request + * @throws InvalidArgumentException + */ + private function getRawSyncListPage(string $type, int $limit, int $offset = 0, array $options = []): Request + { + $defaultOptions = [ + 'filter' => [ + 'user_id' => $this->getUserIdByUsername($this->getUsername()), + 'kind' => $type, + ], + 'page' => [ + 'offset' => $offset, + 'limit' => $limit + ], + 'sort' => '-updated_at' + ]; + $options = array_merge($defaultOptions, $options); + + return $this->setUpRequest('GET', 'library-entries', ['query' => $options]); + } } \ No newline at end of file diff --git a/src/AnimeClient/Command/BaseCommand.php b/src/AnimeClient/Command/BaseCommand.php index 4308c00b..5f6002cf 100644 --- a/src/AnimeClient/Command/BaseCommand.php +++ b/src/AnimeClient/Command/BaseCommand.php @@ -43,29 +43,26 @@ abstract class BaseCommand extends Command { /** * Echo text in a box * - * @param string $message + * @param string|array $message * @param string|int|null $fgColor * @param string|int|null $bgColor * @return void */ - public function echoBox(string $message, $fgColor = NULL, $bgColor = NULL): void + public function echoBox($message, $fgColor = NULL, $bgColor = NULL): void { + if (is_array($message)) + { + $message = implode("\n", $message); + } + try { - $len = strlen($message); - // color message $message = Colors::colorize($message, $fgColor, $bgColor); - $colorLen = strlen($message); // create the box $box = new Box($this->getConsole(), $message); - if ($len !== $colorLen) - { - $box->setPadding((($colorLen - $len) / 2) + 2); - } - $box->write(); echo "\n"; @@ -106,6 +103,11 @@ abstract class BaseCommand extends Command { $this->echoBox($message, Colors::RED | Colors::BOLD, Colors::BLACK); } + public function clearLine(): void + { + $this->getConsole()->write("\r\e[2K"); + } + /** * Setup the Di container * diff --git a/src/AnimeClient/Command/SyncLists.php b/src/AnimeClient/Command/SyncLists.php index b29e1226..cf1732fd 100644 --- a/src/AnimeClient/Command/SyncLists.php +++ b/src/AnimeClient/Command/SyncLists.php @@ -16,19 +16,18 @@ namespace Aviat\AnimeClient\Command; +use ConsoleKit\Widgets; + use Aviat\AnimeClient\API\{ Anilist\MissingIdException, FailedResponseException, JsonAPI, ParallelAPIRequest }; -use Aviat\AnimeClient\API\Anilist\Transformer\{ - AnimeListTransformer as AALT, - MangaListTransformer as AMLT -}; -use Aviat\AnimeClient\API\Anilist\Model as AnilistModel; -use Aviat\AnimeClient\API\Kitsu\Model as KitsuModel; +use Aviat\AnimeClient\API\Anilist; +use Aviat\AnimeClient\API\Kitsu; use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus}; +use Aviat\AnimeClient\Enum\{APISource, ListType, SyncAction}; use Aviat\AnimeClient\Types\FormItem; use Aviat\Ion\Di\Exception\ContainerException; use Aviat\Ion\Di\Exception\NotFoundException; @@ -44,18 +43,24 @@ final class SyncLists extends BaseCommand { /** * Model for making requests to Anilist API - * @var AnilistModel + * @var Anilist\Model */ - protected AnilistModel $anilistModel; + private Anilist\Model $anilistModel; /** * Model for making requests to Kitsu API - * @var KitsuModel + * @var Kitsu\Model */ - protected KitsuModel $kitsuModel; + private Kitsu\Model $kitsuModel; /** - * Run the Kitsu <=> Anilist sync script + * Does the Kitsu API have valid authentication? + * @var bool + */ + private bool $isKitsuAuthenticated = FALSE; + + /** + * Sync Kitsu <=> Anilist * * @param array $args * @param array $options @@ -64,6 +69,45 @@ final class SyncLists extends BaseCommand { * @throws Throwable */ public function execute(array $args, array $options = []): void + { + $this->init(); + + foreach ([ListType::ANIME, ListType::MANGA] as $type) + { + $this->fetchCount($type); + $rawData = $this->fetch($type); + $normalized = $this->transform($type, $rawData); + $compared = $this->compare($type, $normalized); + + /* $toUpdateCounts = [ + 'addToAnilist' => count($compared['addToAnilist']), + 'updateAnilist' => count($compared['updateAnilist']), + 'addToKitsu' => count($compared['addToKitsu']), + 'updateKitsu' => count($compared['updateKitsu']), + ]; + + dump($toUpdateCounts); */ + + $this->update($type, $compared); + } + + /* $this->sync(ListType::ANIME); + $this->sync(ListType::MANGA); + + $this->echoBox('Finished syncing lists'); */ + } + + // ------------------------------------------------------------------------ + // Main sync flow methods + // ------------------------------------------------------------------------ + + /** + * Set up dependencies + * + * @throws ContainerException + * @throws NotFoundException + */ + protected function init(): void { $this->setContainer($this->setupContainer()); $this->setCache($this->container->get('cache')); @@ -71,28 +115,171 @@ final class SyncLists extends BaseCommand { $config = $this->container->get('config'); $anilistEnabled = $config->get(['anilist', 'enabled']); + // We can't sync kitsu against itself! if ( ! $anilistEnabled) { - $this->echoBox('Anlist API is not enabled. Can not sync.'); - return; + $this->echoErrorBox('Anlist API is not enabled. Can not sync.'); + die(); + } + + // Authentication is required to update Kitsu + $this->isKitsuAuthenticated = $this->container->get('auth')->isAuthenticated(); + if ( ! $this->isKitsuAuthenticated) + { + $this->echoWarningBox('Kitsu is not authenticated. Kitsu list can not be updated.'); } $this->anilistModel = $this->container->get('anilist-model'); $this->kitsuModel = $this->container->get('kitsu-model'); - - $this->sync('anime'); - $this->sync('manga'); - - $this->echoBox('Finished syncing lists'); } /** - * Attempt to synchronize external APIs + * Get and display the count of items for each API * * @param string $type - * @throws Throwable */ - protected function sync(string $type): void + protected function fetchCount(string $type): void + { + $this->echo('Fetching List Counts'); + $progress = new Widgets\ProgressBar($this->getConsole(), 2, 50, FALSE); + + $displayLines = []; + + $kitsuCount = $this->fetchKitsuCount($type); + $displayLines[] = "Number of Kitsu {$type} list items: {$kitsuCount}"; + $progress->incr(); + + $anilistCount = $this->fetchAnilistCount($type); + $displayLines[] = "Number of Anilist {$type} list items: {$anilistCount}"; + $progress->incr(); + + $this->clearLine(); + + $this->echoBox($displayLines); + } + + protected function fetch(string $type): array + { + $this->echo('Fetching List Data'); + $progress = new Widgets\ProgressBar($this->getConsole(), 2, 50, FALSE); + + $anilist = $this->fetchAnilist($type); + $progress->incr(); + + $kitsu = $this->fetchKitsu($type); + $progress->incr(); + + $this->clearLine(); + + return [ + 'anilist' => $anilist, + 'kitsu' => $kitsu, + ]; + } + + protected function transform(string $type, array $data): array + { + $this->echo('Normalizing List Data'); + $progress = new Widgets\ProgressBar($this->getConsole(), 2, 50, FALSE); + + $kitsu = $this->transformKitsu($type, $data['kitsu']); + $progress->incr(); + + $anilist = $this->transformAnilist($type, $data['anilist']); + $progress->incr(); + + $this->clearLine(); + + return [ + 'anilist' => $anilist, + 'kitsu' => $kitsu, + ]; + } + + protected function compare(string $type, array $data): array + { + $this->echo('Comparing List Items'); + + return $this->compareLists($type, $data['anilist'], $data['kitsu']); + } + + protected function update(string $type, array $data) + { + if ( ! empty($data['addToAnilist'])) + { + $count = count($data['addToAnilist']); + $this->echoBox("Adding {$count} missing {$type} list items to Anilist"); + $this->updateAnilistListItems($data['addToAnilist'], SyncAction::CREATE, $type); + } + + if ( ! empty($data['updateAnilist'])) + { + $count = count($data['updateAnilist']); + $this->echoBox("Updating {$count} outdated Anilist {$type} list items"); + $this->updateAnilistListItems($data['updateAnilist'], SyncAction::UPDATE, $type); + } + + if ($this->isKitsuAuthenticated) + { + if ( ! empty($data['addToKitsu'])) + { + $count = count($data['addToKitsu']); + $this->echoBox("Adding {$count} missing {$type} list items to Kitsu"); + $this->updateKitsuListItems($data['addToKitsu'], SyncAction::CREATE, $type); + } + + if ( ! empty($data['updateKitsu'])) + { + $count = count($data['updateKitsu']); + $this->echoBox("Updating {$count} outdated Kitsu {$type} list items"); + $this->updateKitsuListItems($data['updateKitsu'], SyncAction::UPDATE, $type); + } + } + else + { + $this->echoErrorBox('Kitsu is not authenticated, so lists can not be updated'); + } + } + + // ------------------------------------------------------------------------ + // Fetch helpers + // ------------------------------------------------------------------------ + private function fetchAnilistCount(string $type) + { + $list = $this->fetchAnilist($type); + + if ( ! isset($list['data']['MediaListCollection']['lists'])) + { + return 0; + } + + $count = 0; + + foreach ($list['data']['MediaListCollection']['lists'] as $subList) + { + $count += array_reduce($subList, fn ($carry, $item) => $carry + count(array_values($item)), 0); + } + + return $count; + } + + private function fetchAnilist(string $type): array + { + static $list = [ + ListType::ANIME => NULL, + ListType::MANGA => NULL, + ]; + + // This uses a static so I don't have to fetch this list twice for a count + if ($list[$type] === NULL) + { + $list[$type] = $this->anilistModel->getSyncList(strtoupper($type)); + } + + return $list[$type]; + } + + private function fetchKitsuCount(string $type): int { $uType = ucfirst($type); @@ -106,157 +293,31 @@ final class SyncLists extends BaseCommand { dump($e); } - - $this->echoBox("Number of Kitsu {$type} list items: {$kitsuCount}"); - - $data = $this->diffLists($type); - - if ( ! empty($data['addToAnilist'])) - { - $count = count($data['addToAnilist']); - $this->echoBox("Adding {$count} missing {$type} list items to Anilist"); - $this->updateAnilistListItems($data['addToAnilist'], 'create', $type); - } - - if ( ! empty($data['updateAnilist'])) - { - $count = count($data['updateAnilist']); - $this->echoBox("Updating {$count} outdated Anilist {$type} list items"); - $this->updateAnilistListItems($data['updateAnilist'], 'update', $type); - } - - if ( ! empty($data['addToKitsu'])) - { - $count = count($data['addToKitsu']); - $this->echoBox("Adding {$count} missing {$type} list items to Kitsu"); - $this->updateKitsuListItems($data['addToKitsu'], 'create', $type); - } - - if ( ! empty($data['updateKitsu'])) - { - $count = count($data['updateKitsu']); - $this->echoBox("Updating {$count} outdated Kitsu {$type} list items"); - $this->updateKitsuListItems($data['updateKitsu'], 'update', $type); - } + return $kitsuCount; } - /** - * Filter Kitsu mappings for the specified type - * - * @param array $includes - * @param string $type - * @return array - */ - protected function filterMappings(array $includes, string $type = 'anime'): array + private function fetchKitsu(string $type): array { - $output = []; - - foreach($includes as $id => $mapping) - { - if ($mapping['externalSite'] === "myanimelist/{$type}") - { - $output[$id] = $mapping; - } - } - - return $output; + return $this->kitsuModel->getSyncList($type); } - /** - * Format an Anilist list for comparison - * - * @param string $type - * @return array - */ - protected function formatAnilistList(string $type): array + // ------------------------------------------------------------------------ + // Transform Helpers + // ------------------------------------------------------------------------ + + private function transformKitsu(string $type, array $data): array { - $type = ucfirst($type); - $method = "formatAnilist{$type}List"; - return $this->$method(); - } - - /** - * Format an Anilist anime list for comparison - * - * @return array - * @throws ContainerException - * @throws NotFoundException - */ - protected function formatAnilistAnimeList(): array - { - $anilistList = $this->anilistModel->getSyncList('ANIME'); - $anilistTransformer = new AALT(); - - $transformedAnilist = []; - - foreach ($anilistList['data']['MediaListCollection']['lists'] as $list) - { - $newTransformed = $anilistTransformer->untransformCollection($list['entries']); - $transformedAnilist = array_merge($transformedAnilist, $newTransformed); - } - - // Key the array by the mal_id for easier reference in the next comparision step - $output = []; - foreach ($transformedAnilist as $item) - { - $output[$item['mal_id']] = $item->toArray(); - } - - $count = count($output); - $this->echoBox("Number of Anilist anime list items: {$count}"); - - return $output; - } - - /** - * Format an Anilist manga list for comparison - * - * @return array - * @throws ContainerException - * @throws NotFoundException - */ - protected function formatAnilistMangaList(): array - { - $anilistList = $this->anilistModel->getSyncList('MANGA'); - $anilistTransformer = new AMLT(); - - $transformedAnilist = []; - - foreach ($anilistList['data']['MediaListCollection']['lists'] as $list) - { - $newTransformed = $anilistTransformer->untransformCollection($list['entries']); - $transformedAnilist = array_merge($transformedAnilist, $newTransformed); - } - - // Key the array by the mal_id for easier reference in the next comparision step - $output = []; - foreach ($transformedAnilist as $item) - { - $output[$item['mal_id']] = $item->toArray(); - } - - $count = count($output); - $this->echoBox("Number of Anilist manga list items: {$count}"); - - return $output; - } - - /** - * Format a kitsu list for the sake of comparision - * - * @param string $type - * @return array - */ - protected function formatKitsuList(string $type = 'anime'): array - { - $method = 'getFullRaw' . ucfirst($type) . 'List'; - $data = $this->kitsuModel->$method(); - if (empty($data)) { return []; } + if ( ! array_key_exists('included', $data)) + { + dump($data); + return []; + } + $includes = JsonAPI::organizeIncludes($data['included']); $includes['mappings'] = $this->filterMappings($includes['mappings'], $type); @@ -271,7 +332,7 @@ final class SyncLists extends BaseCommand { foreach ($potentialMappings as $mappingId) { - if (\is_array($mappingId)) + if (is_array($mappingId)) { continue; } @@ -298,21 +359,37 @@ final class SyncLists extends BaseCommand { return $output; } - /** - * Go through lists of the specified type, and determine what kind of action each item needs - * - * @param string $type - * @return array - */ - protected function diffLists(string $type = 'anime'): array + private function transformAnilist(string $type, array $data): array { - // Get libraryEntries with media.mappings from Kitsu - // Organize mappings, and ignore entries without mappings - $kitsuList = $this->formatKitsuList($type); + $uType = ucfirst($type); + $className = "\\Aviat\\AnimeClient\\API\\Anilist\\Transformer\\{$uType}ListTransformer"; + $transformer = new $className; - // Get Anilist list data - $anilistList = $this->formatAnilistList($type); + $firstTransformed = []; + foreach ($data['data']['MediaListCollection']['lists'] as $list) + { + $firstTransformed[] = $transformer->untransformCollection($list['entries']); + } + + $transformed = array_merge_recursive(...$firstTransformed); + + // Key the array by mal_id + $output = []; + foreach ($transformed as $item) + { + $output[$item['mal_id']] = $item->toArray(); + } + + return $output; + } + + // ------------------------------------------------------------------------ + // Compare Helpers + // ------------------------------------------------------------------------ + + private function compareLists(string $type, array $anilistList, array $kitsuList): array + { $itemsToAddToAnilist = []; $itemsToAddToKitsu = []; $anilistUpdateItems = []; @@ -320,15 +397,21 @@ final class SyncLists extends BaseCommand { $malIds = array_keys($anilistList); $kitsuMalIds = array_map('intval', array_column($kitsuList, 'malId')); - $missingMalIds = array_diff($malIds, $kitsuMalIds); + $missingMalIds = array_filter(array_diff($kitsuMalIds, $malIds), fn ($id) => ! in_array($id, $kitsuMalIds)); // Add items on Anilist, but not Kitsu to Kitsu foreach($missingMalIds as $mid) { - $itemsToAddToKitsu[] = array_merge($anilistList[$mid]['data'], [ - 'id' => $this->kitsuModel->getKitsuIdFromMALId((string)$mid, $type), - 'type' => $type - ]); + if ( ! array_key_exists($mid, $anilistList)) + { + continue; + } + + $data = $anilistList[$mid]['data']; + $data['id'] = $this->kitsuModel->getKitsuIdFromMALId((string)$mid, $type); + $data['type'] = $type; + + $itemsToAddToKitsu[] = $data; } foreach($kitsuList as $kitsuItem) @@ -359,7 +442,7 @@ final class SyncLists extends BaseCommand { continue; } - $statusMap = ($type === 'anime') ? AnimeWatchingStatus::class : MangaReadingStatus::class; + $statusMap = ($type === ListType::ANIME) ? AnimeWatchingStatus::class : MangaReadingStatus::class; // Looks like this item only exists on Kitsu $kItem = $kitsuItem['data']; @@ -392,7 +475,7 @@ final class SyncLists extends BaseCommand { * @param array $anilistItem * @return array|null */ - protected function compareListItems(array $kitsuItem, array $anilistItem): ?array + private function compareListItems(array $kitsuItem, array $anilistItem): ?array { $compareKeys = [ 'notes', @@ -585,6 +668,10 @@ final class SyncLists extends BaseCommand { return $return; } + // ------------------------------------------------------------------------ + // Update Helpers + // ------------------------------------------------------------------------ + /** * Create/Update list items on Kitsu * @@ -593,23 +680,23 @@ final class SyncLists extends BaseCommand { * @param string $type * @throws Throwable */ - protected function updateKitsuListItems(array $itemsToUpdate, string $action = 'update', string $type = 'anime'): void + private function updateKitsuListItems(array $itemsToUpdate, string $action = SyncAction::UPDATE, string $type = ListType::ANIME): void { $requester = new ParallelAPIRequest(); foreach($itemsToUpdate as $item) { - if ($action === 'update') + if ($action === SyncAction::UPDATE) { $requester->addRequest( $this->kitsuModel->updateListItem(FormItem::from($item)) ); } - else if ($action === 'create') + else if ($action === SyncAction::CREATE) { $maybeRequest = $this->kitsuModel->createListItem($item); if ($maybeRequest === NULL) { - $this->echoBox("Skipped creating Kitsu {$type} due to missing id ¯\_(ツ)_/¯"); + $this->echoWarning("Skipped creating Kitsu {$type} due to missing id ¯\_(ツ)_/¯"); continue; } $requester->addRequest($this->kitsuModel->createListItem($item)); @@ -625,8 +712,8 @@ final class SyncLists extends BaseCommand { $id = $itemsToUpdate[$key]['id']; if ( ! array_key_exists('errors', $responseData)) { - $verb = ($action === 'update') ? 'updated' : 'created'; - $this->echoBox("Successfully {$verb} Kitsu {$type} list item with id: {$id}"); + $verb = ($action === SyncAction::UPDATE) ? 'updated' : 'created'; + $this->echoSuccess("Successfully {$verb} Kitsu {$type} list item with id: {$id}"); continue; } @@ -637,14 +724,14 @@ final class SyncLists extends BaseCommand { if ($errorTitle === 'cannot exceed length of media') { - $this->echoBox("Skipped Kitsu {$type} {$id} due to episode count mismatch with other API"); + $this->echoWarning("Skipped Kitsu {$type} {$id} due to episode count mismatch with other API"); continue; } } dump($responseData); - $verb = ($action === 'update') ? 'update' : 'create'; - $this->echoBox("Failed to {$verb} Kitsu {$type} list item with id: {$id}"); + $verb = ($action === SyncAction::UPDATE) ? SyncAction::UPDATE : SyncAction::CREATE; + $this->echoError("Failed to {$verb} Kitsu {$type} list item with id: {$id}"); } } @@ -657,19 +744,19 @@ final class SyncLists extends BaseCommand { * @param string $type * @throws Throwable */ - protected function updateAnilistListItems(array $itemsToUpdate, string $action = 'update', string $type = 'anime'): void + private function updateAnilistListItems(array $itemsToUpdate, string $action = SyncAction::UPDATE, string $type = ListType::ANIME): void { $requester = new ParallelAPIRequest(); foreach($itemsToUpdate as $item) { - if ($action === 'update') + if ($action === SyncAction::UPDATE) { $requester->addRequest( $this->anilistModel->updateListItem(FormItem::from($item), $type) ); } - else if ($action === 'create') + else if ($action === SyncAction::CREATE) { try { @@ -679,7 +766,7 @@ final class SyncLists extends BaseCommand { { // Case where there's a MAL mapping from Kitsu, but no equivalent Anlist item $id = $item['mal_id']; - $this->echoBox("Skipping Anilist ${type} with mal_id: {$id} due to missing mapping"); + $this->echoWarning("Skipping Anilist ${type} with MAL id: {$id} due to missing mapping"); } } } @@ -694,15 +781,41 @@ final class SyncLists extends BaseCommand { if ( ! array_key_exists('errors', $responseData)) { - $verb = ($action === 'update') ? 'updated' : 'created'; - $this->echoBox("Successfully {$verb} Anilist {$type} list item with id: {$id}"); + $verb = ($action === SyncAction::UPDATE) ? 'updated' : 'created'; + $this->echoSuccess("Successfully {$verb} Anilist {$type} list item with id: {$id}"); } else { dump($responseData); - $verb = ($action === 'update') ? 'update' : 'create'; - $this->echoBox("Failed to {$verb} Anilist {$type} list item with id: {$id}"); + $verb = ($action === SyncAction::UPDATE) ? SyncAction::UPDATE : SyncAction::CREATE; + $this->echoError("Failed to {$verb} Anilist {$type} list item with id: {$id}"); } } } + + // ------------------------------------------------------------------------ + // Other Helpers + // ------------------------------------------------------------------------ + + /** + * Filter Kitsu mappings for the specified type + * + * @param array $includes + * @param string $type + * @return array + */ + private function filterMappings(array $includes, string $type = ListType::ANIME): array + { + $output = []; + + foreach($includes as $id => $mapping) + { + if ($mapping['externalSite'] === "myanimelist/{$type}") + { + $output[$id] = $mapping; + } + } + + return $output; + } } diff --git a/src/AnimeClient/Types/FormItemData.php b/src/AnimeClient/Types/FormItemData.php index 1058d0e8..e9956a92 100644 --- a/src/AnimeClient/Types/FormItemData.php +++ b/src/AnimeClient/Types/FormItemData.php @@ -28,7 +28,7 @@ class FormItemData extends AbstractType { /** * @var bool */ - public bool $private = FALSE; + public ?bool $private = FALSE; /** * @var int