From ee18d407a2afbfd74f20c410cff92554d23cba0a Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Fri, 1 May 2020 17:08:20 -0400 Subject: [PATCH 01/21] Make authentication more reliable for list syncing --- src/AnimeClient/API/APIRequestBuilder.php | 1 - .../Transformer/AnimeListTransformer.php | 2 +- .../Transformer/MangaListTransformer.php | 9 +- src/AnimeClient/API/Kitsu/Auth.php | 143 +++++++++--------- src/AnimeClient/API/Kitsu/KitsuTrait.php | 16 +- src/AnimeClient/Command/BaseCommand.php | 109 ++++++++----- src/AnimeClient/Enum/ListType.php | 28 ++++ src/AnimeClient/Enum/SyncAction.php | 28 ++++ 8 files changed, 216 insertions(+), 120 deletions(-) create mode 100644 src/AnimeClient/Enum/ListType.php create mode 100644 src/AnimeClient/Enum/SyncAction.php diff --git a/src/AnimeClient/API/APIRequestBuilder.php b/src/AnimeClient/API/APIRequestBuilder.php index 2e018fb0..d638935c 100644 --- a/src/AnimeClient/API/APIRequestBuilder.php +++ b/src/AnimeClient/API/APIRequestBuilder.php @@ -21,7 +21,6 @@ use const Aviat\AnimeClient\USER_AGENT; use function Amp\Promise\wait; use function Aviat\AnimeClient\getResponse; -use Amp; use Amp\Http\Client\Request; use Amp\Http\Client\Body\FormBody; use Aviat\Ion\Json; diff --git a/src/AnimeClient/API/Anilist/Transformer/AnimeListTransformer.php b/src/AnimeClient/API/Anilist/Transformer/AnimeListTransformer.php index 464d423c..8187e60d 100644 --- a/src/AnimeClient/API/Anilist/Transformer/AnimeListTransformer.php +++ b/src/AnimeClient/API/Anilist/Transformer/AnimeListTransformer.php @@ -54,7 +54,7 @@ class AnimeListTransformer extends AbstractTransformer { 'reconsuming' => $reconsuming, 'status' => $reconsuming ? KitsuStatus::WATCHING - :AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']], + : AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']], 'updatedAt' => (new DateTime()) ->setTimestamp($item['updatedAt']) ->format(DateTime::W3C) diff --git a/src/AnimeClient/API/Anilist/Transformer/MangaListTransformer.php b/src/AnimeClient/API/Anilist/Transformer/MangaListTransformer.php index 184dd73a..f60c7025 100644 --- a/src/AnimeClient/API/Anilist/Transformer/MangaListTransformer.php +++ b/src/AnimeClient/API/Anilist/Transformer/MangaListTransformer.php @@ -17,6 +17,7 @@ namespace Aviat\AnimeClient\API\Anilist\Transformer; use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Anilist as AnilistStatus; +use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Kitsu as KitsuStatus; use Aviat\AnimeClient\API\Mapping\MangaReadingStatus; use Aviat\AnimeClient\Types\MangaListItem; use Aviat\AnimeClient\Types\FormItem; @@ -40,6 +41,8 @@ class MangaListTransformer extends AbstractTransformer { */ public function untransform(array $item): FormItem { + $reconsuming = $item['status'] === AnilistStatus::REPEATING; + return FormItem::from([ 'id' => $item['id'], 'mal_id' => $item['media']['idMal'], @@ -49,8 +52,10 @@ class MangaListTransformer extends AbstractTransformer { 'progress' => $item['progress'], 'rating' => $item['score'], 'reconsumeCount' => $item['repeat'], - 'reconsuming' => $item['status'] === AnilistStatus::REPEATING, - 'status' => MangaReadingStatus::ANILIST_TO_KITSU[$item['status']], + 'reconsuming' => $reconsuming, + 'status' => $reconsuming + ? KitsuStatus::READING + : MangaReadingStatus::ANILIST_TO_KITSU[$item['status']], 'updatedAt' => (new DateTime()) ->setTimestamp($item['updatedAt']) ->format(DateTime::W3C), diff --git a/src/AnimeClient/API/Kitsu/Auth.php b/src/AnimeClient/API/Kitsu/Auth.php index c0997f6c..1a36b2a2 100644 --- a/src/AnimeClient/API/Kitsu/Auth.php +++ b/src/AnimeClient/API/Kitsu/Auth.php @@ -29,6 +29,7 @@ use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException}; use Throwable; +use const PHP_SAPI; /** * Kitsu API Authentication @@ -83,35 +84,67 @@ final class Auth { $auth = $this->model->authenticate($username, $password); - if (FALSE !== $auth) - { - // Set the token in the cache for command line operations - $cacheItem = $this->cache->getItem(K::AUTH_TOKEN_CACHE_KEY); - $cacheItem->set($auth['access_token']); - $cacheItem->save(); - - // Set the token expiration in the cache - $expireTime = $auth['created_at'] + $auth['expires_in']; - $cacheItem = $this->cache->getItem(K::AUTH_TOKEN_EXP_CACHE_KEY); - $cacheItem->set($expireTime); - $cacheItem->save(); - - // Set the refresh token in the cache - $cacheItem = $this->cache->getItem(K::AUTH_TOKEN_REFRESH_CACHE_KEY); - $cacheItem->set($auth['refresh_token']); - $cacheItem->save(); - - // Set the session values - $this->segment->set('auth_token', $auth['access_token']); - $this->segment->set('auth_token_expires', $expireTime); - $this->segment->set('refresh_token', $auth['refresh_token']); - - return TRUE; - } - - return FALSE; + return $this->storeAuth($auth); } + /** + * Check whether the current user is authenticated + * + * @return boolean + */ + public function isAuthenticated(): bool + { + return ($this->getAuthToken() !== NULL); + } + + /** + * Clear authentication values + * + * @return void + */ + public function logout(): void + { + $this->segment->clear(); + } + + /** + * Retrieve the authentication token from the session + * + * @return string|false + */ + private function getAuthToken(): ?string + { + $now = time(); + + if (PHP_SAPI === 'cli') + { + $token = $this->cacheGet(K::AUTH_TOKEN_CACHE_KEY, NULL); + $refreshToken = $this->cacheGet(K::AUTH_TOKEN_REFRESH_CACHE_KEY, NULL); + $expireTime = $this->cacheGet(K::AUTH_TOKEN_EXP_CACHE_KEY); + $isExpired = $now > $expireTime; + } + else + { + $token = $this->segment->get('auth_token', NULL); + $refreshToken = $this->segment->get('refresh_token', NULL); + $isExpired = $now > $this->segment->get('auth_token_expires', $now + 5000); + } + + // Attempt to re-authenticate with refresh token + /* if ($isExpired === TRUE && $refreshToken !== NULL) + { + if ($this->reAuthenticate($refreshToken) !== NULL) + { + return (PHP_SAPI === 'cli') + ? $this->cacheGet(K::AUTH_TOKEN_CACHE_KEY, NULL) + : $this->segment->get('auth_token', NULL); + } + + return NULL; + }*/ + + return $token; + } /** * Make the call to re-authenticate with the existing refresh token @@ -121,10 +154,15 @@ final class Auth { * @throws InvalidArgumentException * @throws Throwable */ - public function reAuthenticate(string $token): bool + private function reAuthenticate(string $token): bool { $auth = $this->model->reAuthenticate($token); + return $this->storeAuth($auth); + } + + private function storeAuth($auth): bool + { if (FALSE !== $auth) { // Set the token in the cache for command line operations @@ -153,52 +191,15 @@ final class Auth { return FALSE; } - - /** - * Check whether the current user is authenticated - * - * @return boolean - */ - public function isAuthenticated(): bool + private function cacheGet(string $key, $default = NULL) { - return ($this->get_auth_token() !== FALSE); - } - - /** - * Clear authentication values - * - * @return void - */ - public function logout(): void - { - $this->segment->clear(); - } - - /** - * Retrieve the authentication token from the session - * - * @return string|false - */ - public function get_auth_token() - { - $now = time(); - - $token = $this->segment->get('auth_token', FALSE); - $refreshToken = $this->segment->get('refresh_token', FALSE); - $isExpired = time() > $this->segment->get('auth_token_expires', $now + 5000); - - // Attempt to re-authenticate with refresh token - /* if ($isExpired && $refreshToken) + $cacheItem = $this->cache->getItem($key); + if ( ! $cacheItem->isHit()) { - if ($this->reAuthenticate($refreshToken)) - { - return $this->segment->get('auth_token', FALSE); - } + return $default; + } - return FALSE; - } */ - - return $token; + return $cacheItem->get(); } } // End of KitsuAuth.php \ No newline at end of file diff --git a/src/AnimeClient/API/Kitsu/KitsuTrait.php b/src/AnimeClient/API/Kitsu/KitsuTrait.php index 2503a25b..7242246a 100644 --- a/src/AnimeClient/API/Kitsu/KitsuTrait.php +++ b/src/AnimeClient/API/Kitsu/KitsuTrait.php @@ -16,6 +16,7 @@ namespace Aviat\AnimeClient\API\Kitsu; +use const PHP_SAPI; use const Aviat\AnimeClient\SESSION_SEGMENT; use function Amp\Promise\wait; @@ -69,11 +70,14 @@ trait KitsuTrait { ->getSegment(SESSION_SEGMENT); $cache = $this->getContainer()->get('cache'); - $cacheItem = $cache->getItem('kitsu-auth-token'); + $cacheItem = $cache->getItem(K::AUTH_TOKEN_CACHE_KEY); $token = null; - - if ($sessionSegment->get('auth_token') !== NULL && $url !== K::AUTH_URL) + if (PHP_SAPI === 'cli' && $cacheItem->isHit()) + { + $token = $cacheItem->get(); + } + else if ($url !== K::AUTH_URL && $sessionSegment->get('auth_token') !== NULL) { $token = $sessionSegment->get('auth_token'); if ( ! $cacheItem->isHit()) @@ -82,12 +86,8 @@ trait KitsuTrait { $cacheItem->save(); } } - else if ($sessionSegment->get('auth_token') === NULL && $cacheItem->isHit()) - { - $token = $cacheItem->get(); - } - if (NULL !== $token) + if ($token !== NULL) { $request = $request->setAuth('bearer', $token); } diff --git a/src/AnimeClient/Command/BaseCommand.php b/src/AnimeClient/Command/BaseCommand.php index 17e65b24..4308c00b 100644 --- a/src/AnimeClient/Command/BaseCommand.php +++ b/src/AnimeClient/Command/BaseCommand.php @@ -26,8 +26,8 @@ use Aviat\AnimeClient\API\{Anilist, CacheTrait, Kitsu}; use Aviat\AnimeClient\API\Kitsu\KitsuRequestBuilder; use Aviat\Banker\Pool; use Aviat\Ion\Config; -use Aviat\Ion\Di\{Container, ContainerAware}; -use ConsoleKit\{Command, ConsoleException}; +use Aviat\Ion\Di\{Container, ContainerInterface, ContainerAware}; +use ConsoleKit\{Colors, Command, ConsoleException}; use ConsoleKit\Widgets\Box; use Laminas\Diactoros\{Response, ServerRequestFactory}; use Monolog\Handler\RotatingFileHandler; @@ -44,15 +44,30 @@ abstract class BaseCommand extends Command { * Echo text in a box * * @param string $message + * @param string|int|null $fgColor + * @param string|int|null $bgColor * @return void */ - protected function echoBox($message): void + public function echoBox(string $message, $fgColor = NULL, $bgColor = NULL): void { try { - echo "\n"; + $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"; } catch (ConsoleException $e) @@ -61,12 +76,42 @@ abstract class BaseCommand extends Command { } } + public function echo(string $message): void + { + $this->_line($message); + } + + public function echoSuccess(string $message): void + { + $this->_line($message, Colors::GREEN | Colors::BOLD, Colors::BLACK); + } + + public function echoWarning(string $message): void + { + $this->_line($message, Colors::YELLOW | Colors::BOLD, Colors::BLACK); + } + + public function echoWarningBox(string $message): void + { + $this->echoBox($message, Colors::YELLOW | Colors::BOLD, Colors::BLACK); + } + + public function echoError(string $message): void + { + $this->_line($message, Colors::RED | Colors::BOLD, Colors::BLACK); + } + + public function echoErrorBox(string $message): void + { + $this->echoBox($message, Colors::RED | Colors::BOLD, Colors::BLACK); + } + /** * Setup the Di container * - * @return Container + * @return Containerinterface */ - protected function setupContainer(): Container + public function setupContainer(): ContainerInterface { $APP_DIR = realpath(__DIR__ . '/../../../app'); $APPCONF_DIR = realpath("{$APP_DIR}/appConf/"); @@ -82,7 +127,7 @@ abstract class BaseCommand extends Command { $configArray = array_replace_recursive($baseConfig, $config, $overrideConfig); - $di = static function ($configArray) use ($APP_DIR): Container { + $di = static function (array $configArray) use ($APP_DIR): Container { $container = new Container(); // ------------------------------------------------------------------------- @@ -103,9 +148,7 @@ abstract class BaseCommand extends Command { $container->setLogger($kitsu_request_logger, 'kitsu-request'); // Create Config Object - $container->set('config', static function() use ($configArray): Config { - return new Config($configArray); - }); + $container->set('config', fn () => new Config($configArray)); // Create Cache Object $container->set('cache', static function($container) { @@ -115,28 +158,20 @@ abstract class BaseCommand extends Command { }); // Create Aura Router Object - $container->set('aura-router', static function() { - return new RouterContainer; - }); + $container->set('aura-router', fn () => new RouterContainer); // Create Request/Response Objects - $container->set('request', static function() { - return ServerRequestFactory::fromGlobals( - $_SERVER, - $_GET, - $_POST, - $_COOKIE, - $_FILES - ); - }); - $container->set('response', static function(): Response { - return new Response; - }); + $container->set('request', fn () => ServerRequestFactory::fromGlobals( + $_SERVER, + $_GET, + $_POST, + $_COOKIE, + $_FILES + )); + $container->set('response', fn () => new Response); // Create session Object - $container->set('session', static function() { - return (new SessionFactory())->newInstance($_COOKIE); - }); + $container->set('session', fn () => (new SessionFactory())->newInstance($_COOKIE)); // Models $container->set('kitsu-model', static function($container): Kitsu\Model { @@ -175,21 +210,21 @@ abstract class BaseCommand extends Command { return $model; }); - $container->set('auth', static function($container): Kitsu\Auth { - return new Kitsu\Auth($container); - }); + $container->set('auth', fn ($container) => new Kitsu\Auth($container)); - $container->set('url-generator', static function($container): UrlGenerator { - return new UrlGenerator($container); - }); + $container->set('url-generator', fn ($container) => new UrlGenerator($container)); - $container->set('util', static function($container): Util { - return new Util($container); - }); + $container->set('util', fn ($container) => new Util($container)); return $container; }; return $di($configArray); } + + private function _line(string $message, $fgColor = NULL, $bgColor = NULL): void + { + $message = Colors::colorize($message, $fgColor, $bgColor); + $this->getConsole()->writeln($message); + } } \ No newline at end of file diff --git a/src/AnimeClient/Enum/ListType.php b/src/AnimeClient/Enum/ListType.php new file mode 100644 index 00000000..fc13ac37 --- /dev/null +++ b/src/AnimeClient/Enum/ListType.php @@ -0,0 +1,28 @@ + + * @copyright 2015 - 2020 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 5 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Enum; + +use Aviat\Ion\Enum as BaseEnum; + +/** + * Types of lists + */ +final class ListType extends BaseEnum { + public const ANIME = 'anime'; + public const DRAMA = 'drama'; + public const MANGA = 'manga'; +} \ No newline at end of file diff --git a/src/AnimeClient/Enum/SyncAction.php b/src/AnimeClient/Enum/SyncAction.php new file mode 100644 index 00000000..1fe2156b --- /dev/null +++ b/src/AnimeClient/Enum/SyncAction.php @@ -0,0 +1,28 @@ + + * @copyright 2015 - 2020 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 5 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Enum; + +use Aviat\Ion\Enum as BaseEnum; + +/** + * Types of actions when syncing lists from different APIs + */ +final class SyncAction extends BaseEnum { + public const CREATE = 'create'; + public const UPDATE = 'update'; + public const DELETE = 'delete'; +} \ No newline at end of file From 9344d980562646e82b4a01ecef0fb8baa97968c0 Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Fri, 1 May 2020 19:33:51 -0400 Subject: [PATCH 02/21] Fix collection episode length/count, display newlines in notes --- app/views/collection/list-all.php | 2 +- app/views/collection/list-item.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/collection/list-all.php b/app/views/collection/list-all.php index c7ff2d3d..b14db2b1 100644 --- a/app/views/collection/list-all.php +++ b/app/views/collection/list-all.php @@ -35,7 +35,7 @@ - + diff --git a/app/views/collection/list-item.php b/app/views/collection/list-item.php index d163ae42..9c8ac967 100644 --- a/app/views/collection/list-item.php +++ b/app/views/collection/list-item.php @@ -11,10 +11,10 @@ ' . $item['alternate_title'] . '' : '' ?> - 1) ? $item['episode_count'] : '-' ?> + - + \ No newline at end of file From 2d44435c598f8c78d665704c7dd57b2d89d0c0f4 Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Fri, 1 May 2020 19:38:45 -0400 Subject: [PATCH 03/21] Various code tweaks --- .../Transformer/AnimeListTransformer.php | 2 +- src/AnimeClient/API/Kitsu/KitsuTrait.php | 2 +- src/AnimeClient/Enum/APISource.php | 27 +++++++++++++++++++ src/Ion/Di/ContainerAware.php | 2 +- src/Ion/Enum.php | 7 ++--- 5 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 src/AnimeClient/Enum/APISource.php diff --git a/src/AnimeClient/API/Anilist/Transformer/AnimeListTransformer.php b/src/AnimeClient/API/Anilist/Transformer/AnimeListTransformer.php index 8187e60d..c09f63c1 100644 --- a/src/AnimeClient/API/Anilist/Transformer/AnimeListTransformer.php +++ b/src/AnimeClient/API/Anilist/Transformer/AnimeListTransformer.php @@ -29,7 +29,7 @@ class AnimeListTransformer extends AbstractTransformer { public function transform($item): AnimeListItem { - return new AnimeListItem([]); + return AnimeListItem::from([]); } /** diff --git a/src/AnimeClient/API/Kitsu/KitsuTrait.php b/src/AnimeClient/API/Kitsu/KitsuTrait.php index 7242246a..1041c3c2 100644 --- a/src/AnimeClient/API/Kitsu/KitsuTrait.php +++ b/src/AnimeClient/API/Kitsu/KitsuTrait.php @@ -176,7 +176,7 @@ trait KitsuTrait { $logger->warning('Non 200 response for api call', (array)$response); } - // throw new FailedResponseException('Failed to get the proper response from the API'); + throw new FailedResponseException('Failed to get the proper response from the API'); } try diff --git a/src/AnimeClient/Enum/APISource.php b/src/AnimeClient/Enum/APISource.php new file mode 100644 index 00000000..65549401 --- /dev/null +++ b/src/AnimeClient/Enum/APISource.php @@ -0,0 +1,27 @@ + + * @copyright 2015 - 2020 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 5 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Enum; + +use Aviat\Ion\Enum as BaseEnum; + +/** + * Types of lists + */ +final class APISource extends BaseEnum { + public const KITSU = 'kitsu'; + public const ANILIST = 'anilist'; +} \ No newline at end of file diff --git a/src/Ion/Di/ContainerAware.php b/src/Ion/Di/ContainerAware.php index 3b43a0cf..4da0f84d 100644 --- a/src/Ion/Di/ContainerAware.php +++ b/src/Ion/Di/ContainerAware.php @@ -26,7 +26,7 @@ trait ContainerAware { * * @var ContainerInterface */ - protected $container; + protected ContainerInterface $container; /** * Set the container for the current object diff --git a/src/Ion/Enum.php b/src/Ion/Enum.php index e51bfc6a..d0b3a610 100644 --- a/src/Ion/Enum.php +++ b/src/Ion/Enum.php @@ -17,6 +17,7 @@ namespace Aviat\Ion; use ReflectionClass; +use ReflectionException; /** * Class emulating an enumeration type @@ -27,7 +28,7 @@ abstract class Enum { * Return the list of constant values for the Enum * * @return array - * @throws \ReflectionException + * @throws ReflectionException */ public static function getConstList(): array { @@ -48,12 +49,12 @@ abstract class Enum { * * @param mixed $key * @return boolean - * @throws \ReflectionException + * @throws ReflectionException */ public static function isValid($key): bool { $values = array_values(static::getConstList()); - return \in_array($key, $values, TRUE); + return in_array($key, $values, TRUE); } } // End of Enum.php \ No newline at end of file From fa89df567dfe98e229ffaa3a5e6c8a3aaa601f23 Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Mon, 4 May 2020 16:46:27 -0400 Subject: [PATCH 04/21] Improve rewatched messages --- app/views/anime/cover-item.php | 10 +++++++++- app/views/anime/list.php | 12 +++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/views/anime/cover-item.php b/app/views/anime/cover-item.php index b1ad8cb0..af680967 100644 --- a/app/views/anime/cover-item.php +++ b/app/views/anime/cover-item.php @@ -30,7 +30,15 @@ 0): ?>
-
Rewatched time(s)
+ +
Rewatched once
+ +
Rewatched twice
+ +
Rewatched thrice
+ +
Rewatched times
+
diff --git a/app/views/anime/list.php b/app/views/anime/list.php index 5b3fd6be..1c2d482b 100644 --- a/app/views/anime/list.php +++ b/app/views/anime/list.php @@ -86,7 +86,17 @@
    - 0): ?>li>Rewatched time(s) + 0): ?> + +
  • Rewatched once
  • + +
  • Rewatched twice
  • + +
  • Rewatched thrice
  • + +
  • Rewatched times
  • + +
  • From 77ee6ae50ed670be65b54bb1b2600474d924239d Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Mon, 4 May 2020 17:13:03 -0400 Subject: [PATCH 05/21] 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 From 9045eb6b8f1a762cd0920b772efb6b2661a7abd6 Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Mon, 4 May 2020 17:15:50 -0400 Subject: [PATCH 06/21] Comment cleanup of sync command --- src/AnimeClient/Command/SyncLists.php | 43 +++++++++++++++++---------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/AnimeClient/Command/SyncLists.php b/src/AnimeClient/Command/SyncLists.php index cf1732fd..5daaa9a3 100644 --- a/src/AnimeClient/Command/SyncLists.php +++ b/src/AnimeClient/Command/SyncLists.php @@ -74,27 +74,13 @@ final class SyncLists extends BaseCommand { foreach ([ListType::ANIME, ListType::MANGA] as $type) { + // Main Sync flow $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'); */ } // ------------------------------------------------------------------------ @@ -158,6 +144,12 @@ final class SyncLists extends BaseCommand { $this->echoBox($displayLines); } + /** + * Get the list data + * + * @param string $type + * @return array + */ protected function fetch(string $type): array { $this->echo('Fetching List Data'); @@ -177,6 +169,13 @@ final class SyncLists extends BaseCommand { ]; } + /** + * Normalize the list data for comparison + * + * @param string $type + * @param array $data + * @return array + */ protected function transform(string $type, array $data): array { $this->echo('Normalizing List Data'); @@ -196,6 +195,13 @@ final class SyncLists extends BaseCommand { ]; } + /** + * Compare the lists data + * + * @param string $type + * @param array $data + * @return array|array[] + */ protected function compare(string $type, array $data): array { $this->echo('Comparing List Items'); @@ -203,6 +209,13 @@ final class SyncLists extends BaseCommand { return $this->compareLists($type, $data['anilist'], $data['kitsu']); } + /** + * Updated outdated list items + * + * @param string $type + * @param array $data + * @throws Throwable + */ protected function update(string $type, array $data) { if ( ! empty($data['addToAnilist'])) From 48e51c816fb62973d367c13c0d8e50b7ea6b8912 Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Tue, 5 May 2020 19:12:17 -0400 Subject: [PATCH 07/21] Add better re-read messages to manga --- app/views/manga/cover.php | 10 +++++++++- app/views/manga/list.php | 10 ++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/views/manga/cover.php b/app/views/manga/cover.php index 0f183334..7d3edbae 100644 --- a/app/views/manga/cover.php +++ b/app/views/manga/cover.php @@ -68,7 +68,15 @@ 0): ?>
    -
    Reread time(s)
    + +
    Reread once
    + +
    Reread twice
    + +
    Reread thrice
    + +
    Reread times
    +
    diff --git a/app/views/manga/list.php b/app/views/manga/list.php index 4cba2b88..02aa879d 100644 --- a/app/views/manga/list.php +++ b/app/views/manga/list.php @@ -53,8 +53,14 @@
      - 0): ?> -
    • Reread time(s)
    • + +
    • Reread once
    • + +
    • Reread twice
    • + +
    • Reread thrice
    • + 3): ?> +
    • Reread times
    • From b806519b2207c894f06f44e35f523fe635fa1ee3 Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Wed, 6 May 2020 09:08:27 -0400 Subject: [PATCH 08/21] Show more alternate titles on anime detail pages --- app/views/anime/details.php | 2 +- src/AnimeClient/API/Kitsu.php | 28 +++++++++++++++++++ src/AnimeClient/API/Kitsu/Model.php | 4 +-- .../Kitsu/Transformer/AnimeTransformer.php | 2 ++ src/AnimeClient/Types/Anime.php | 5 ++++ src/AnimeClient/Types/AnimePage.php | 4 +-- 6 files changed, 40 insertions(+), 5 deletions(-) diff --git a/app/views/anime/details.php b/app/views/anime/details.php index c3089004..9e72b539 100644 --- a/app/views/anime/details.php +++ b/app/views/anime/details.php @@ -45,7 +45,7 @@

      - +


      diff --git a/src/AnimeClient/API/Kitsu.php b/src/AnimeClient/API/Kitsu.php index d7c6c69e..94bc3edb 100644 --- a/src/AnimeClient/API/Kitsu.php +++ b/src/AnimeClient/API/Kitsu.php @@ -195,6 +195,23 @@ final class Kitsu { return []; } + /** + * Get the list of titles + * + * @param array $data + * @return array + */ + public static function getTitles(array $data): array + { + $raw = array_unique([ + $data['canonicalTitle'], + ...array_values($data['titles']), + ...array_values($data['abbreviatedTitles']), + ]); + + return array_diff($raw,[$data['canonicalTitle']]); + } + /** * Filter out duplicate and very similar names from * @@ -217,6 +234,17 @@ final class Kitsu { } } + if (array_key_exists('abbreviatedTitles', $data)) + { + foreach ($data['abbreviatedTitles'] as $alternateTitle) + { + if (self::titleIsUnique($alternateTitle, $valid)) + { + $valid[] = $alternateTitle; + } + } + } + return $valid; } diff --git a/src/AnimeClient/API/Kitsu/Model.php b/src/AnimeClient/API/Kitsu/Model.php index 8571e30d..15c5687d 100644 --- a/src/AnimeClient/API/Kitsu/Model.php +++ b/src/AnimeClient/API/Kitsu/Model.php @@ -252,8 +252,8 @@ final class Model { 'fields' => [ 'characters' => 'canonicalName,slug,image', 'characterVoices' => 'mediaCharacter', - 'anime' => 'canonicalTitle,titles,slug,posterImage', - 'manga' => 'canonicalTitle,titles,slug,posterImage', + 'anime' => 'canonicalTitle,abbreviatedTitles,titles,slug,posterImage', + 'manga' => 'canonicalTitle,abbreviatedTitles,titles,slug,posterImage', 'mediaCharacters' => 'role,media,character', 'mediaStaff' => 'role,media,person', ], diff --git a/src/AnimeClient/API/Kitsu/Transformer/AnimeTransformer.php b/src/AnimeClient/API/Kitsu/Transformer/AnimeTransformer.php index e0b049b0..838f6549 100644 --- a/src/AnimeClient/API/Kitsu/Transformer/AnimeTransformer.php +++ b/src/AnimeClient/API/Kitsu/Transformer/AnimeTransformer.php @@ -42,6 +42,7 @@ final class AnimeTransformer extends AbstractTransformer { $title = $item['canonicalTitle']; $titles = Kitsu::filterTitles($item); + $titles_more = Kitsu::getTitles($item); $characters = []; $staff = []; @@ -123,6 +124,7 @@ final class AnimeTransformer extends AbstractTransformer { 'synopsis' => $item['synopsis'], 'title' => $title, 'titles' => $titles, + 'titles_more' => $titles_more, 'trailer_id' => $item['youtubeVideoId'], 'url' => "https://kitsu.io/anime/{$item['slug']}", ]); diff --git a/src/AnimeClient/Types/Anime.php b/src/AnimeClient/Types/Anime.php index b9435937..af6b649a 100644 --- a/src/AnimeClient/Types/Anime.php +++ b/src/AnimeClient/Types/Anime.php @@ -97,6 +97,11 @@ class Anime extends AbstractType { */ public array $titles = []; + /** + * @var array + */ + public array $titles_more = []; + /** * @var string */ diff --git a/src/AnimeClient/Types/AnimePage.php b/src/AnimeClient/Types/AnimePage.php index b1a3b5d9..ef530373 100644 --- a/src/AnimeClient/Types/AnimePage.php +++ b/src/AnimeClient/Types/AnimePage.php @@ -23,10 +23,10 @@ final class AnimePage extends Anime { /** * @var array */ - public $characters; + public array $characters = []; /** * @var array */ - public $staff; + public array $staff = []; } \ No newline at end of file From dc20d8de7c8a0faacd6d6a6c7bd701b2df539e11 Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Wed, 6 May 2020 10:12:49 -0400 Subject: [PATCH 09/21] Fix failing test --- src/AnimeClient/API/Kitsu.php | 13 +------------ .../AnimeTransformerTest__testTransform__1.yml | 3 +++ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/AnimeClient/API/Kitsu.php b/src/AnimeClient/API/Kitsu.php index 94bc3edb..40268466 100644 --- a/src/AnimeClient/API/Kitsu.php +++ b/src/AnimeClient/API/Kitsu.php @@ -206,7 +206,7 @@ final class Kitsu { $raw = array_unique([ $data['canonicalTitle'], ...array_values($data['titles']), - ...array_values($data['abbreviatedTitles']), + ...array_values($data['abbreviatedTitles'] ?? []), ]); return array_diff($raw,[$data['canonicalTitle']]); @@ -234,17 +234,6 @@ final class Kitsu { } } - if (array_key_exists('abbreviatedTitles', $data)) - { - foreach ($data['abbreviatedTitles'] as $alternateTitle) - { - if (self::titleIsUnique($alternateTitle, $valid)) - { - $valid[] = $alternateTitle; - } - } - } - return $valid; } diff --git a/tests/AnimeClient/API/Kitsu/Transformer/__snapshots__/AnimeTransformerTest__testTransform__1.yml b/tests/AnimeClient/API/Kitsu/Transformer/__snapshots__/AnimeTransformerTest__testTransform__1.yml index f736c331..3e28a782 100644 --- a/tests/AnimeClient/API/Kitsu/Transformer/__snapshots__/AnimeTransformerTest__testTransform__1.yml +++ b/tests/AnimeClient/API/Kitsu/Transformer/__snapshots__/AnimeTransformerTest__testTransform__1.yml @@ -29,5 +29,8 @@ titles: - 'Attack on Titan' - 'Shingeki no Kyojin' - 進撃の巨人 +titles_more: + 2: 'Shingeki no Kyojin' + 3: 進撃の巨人 trailer_id: n4Nj6Y_SNYI url: 'https://kitsu.io/anime/attack-on-titan' From 05c50387f6200eab188f5dcc42262f3e2e4a68a9 Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Wed, 6 May 2020 13:16:40 -0400 Subject: [PATCH 10/21] Set up Event-based handling for a few things --- src/AnimeClient/API/Kitsu/Auth.php | 45 +++++++++++++------ src/AnimeClient/API/Kitsu/KitsuTrait.php | 32 ++++++++------ src/AnimeClient/API/Kitsu/Model.php | 30 +++++++++---- src/AnimeClient/Controller.php | 15 +++++++ src/AnimeClient/Controller/Misc.php | 9 ++-- src/AnimeClient/Dispatcher.php | 18 +++++--- src/AnimeClient/Enum/EventType.php | 25 +++++++++++ src/Ion/Event.php | 55 ++++++++++++++++++++++++ 8 files changed, 183 insertions(+), 46 deletions(-) create mode 100644 src/AnimeClient/Enum/EventType.php create mode 100644 src/Ion/Event.php diff --git a/src/AnimeClient/API/Kitsu/Auth.php b/src/AnimeClient/API/Kitsu/Auth.php index 1a36b2a2..729aa5a7 100644 --- a/src/AnimeClient/API/Kitsu/Auth.php +++ b/src/AnimeClient/API/Kitsu/Auth.php @@ -27,6 +27,7 @@ use Aviat\AnimeClient\API\{ }; use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException}; +use Aviat\Ion\Event; use Throwable; use const PHP_SAPI; @@ -66,6 +67,8 @@ final class Auth { $this->segment = $container->get('session') ->getSegment(SESSION_SEGMENT); $this->model = $container->get('kitsu-model'); + + Event::on('::unauthorized::', [$this, 'reAuthenticate']); } /** @@ -87,6 +90,28 @@ final class Auth { return $this->storeAuth($auth); } + /** + * Make the call to re-authenticate with the existing refresh token + * + * @param string $refreshToken + * @return boolean + * @throws InvalidArgumentException + * @throws Throwable + */ + public function reAuthenticate(?string $refreshToken): bool + { + $refreshToken ??= $this->getAuthToken(); + + if (empty($refreshToken)) + { + return FALSE; + } + + $auth = $this->model->reAuthenticate($refreshToken); + + return $this->storeAuth($auth); + } + /** * Check whether the current user is authenticated * @@ -110,7 +135,7 @@ final class Auth { /** * Retrieve the authentication token from the session * - * @return string|false + * @return string */ private function getAuthToken(): ?string { @@ -146,22 +171,14 @@ final class Auth { return $token; } - /** - * Make the call to re-authenticate with the existing refresh token - * - * @param string $token - * @return boolean - * @throws InvalidArgumentException - * @throws Throwable - */ - private function reAuthenticate(string $token): bool + private function getRefreshToken(): ?string { - $auth = $this->model->reAuthenticate($token); - - return $this->storeAuth($auth); + return (PHP_SAPI === 'cli') + ? $this->cacheGet(K::AUTH_TOKEN_REFRESH_CACHE_KEY, NULL) + : $this->segment->get('refresh_token'); } - private function storeAuth($auth): bool + private function storeAuth(bool $auth): bool { if (FALSE !== $auth) { diff --git a/src/AnimeClient/API/Kitsu/KitsuTrait.php b/src/AnimeClient/API/Kitsu/KitsuTrait.php index 1041c3c2..67ce2815 100644 --- a/src/AnimeClient/API/Kitsu/KitsuTrait.php +++ b/src/AnimeClient/API/Kitsu/KitsuTrait.php @@ -16,6 +16,8 @@ namespace Aviat\AnimeClient\API\Kitsu; +use Aviat\AnimeClient\Enum\EventType; +use function in_array; use const PHP_SAPI; use const Aviat\AnimeClient\SESSION_SEGMENT; @@ -24,10 +26,8 @@ use function Aviat\AnimeClient\getResponse; use Amp\Http\Client\Request; use Amp\Http\Client\Response; -use Aviat\AnimeClient\API\{ - FailedResponseException, - Kitsu as K -}; +use Aviat\AnimeClient\API\{FailedResponseException, Kitsu as K}; +use Aviat\Ion\Event; use Aviat\Ion\Json; use Aviat\Ion\JsonException; @@ -80,7 +80,7 @@ trait KitsuTrait { else if ($url !== K::AUTH_URL && $sessionSegment->get('auth_token') !== NULL) { $token = $sessionSegment->get('auth_token'); - if ( ! $cacheItem->isHit()) + if ( ! (empty($token) || $cacheItem->isHit())) { $cacheItem->set($token); $cacheItem->save(); @@ -168,12 +168,20 @@ trait KitsuTrait { } $response = $this->getResponse($type, $url, $options); + $statusCode = $response->getStatus(); - if ((int) $response->getStatus() > 299 || (int) $response->getStatus() < 200) + // Check for requests that are unauthorized + if ($statusCode === 401 || $statusCode === 403) + { + Event::emit(EventType::UNAUTHORIZED); + } + + // Any other type of failed request + if ($statusCode > 299 || $statusCode < 200) { if ($logger) { - $logger->warning('Non 200 response for api call', (array)$response); + $logger->warning('Non 2xx response for api call', (array)$response); } throw new FailedResponseException('Failed to get the proper response from the API'); @@ -188,7 +196,6 @@ trait KitsuTrait { print_r($e); die(); } - } /** @@ -233,12 +240,9 @@ trait KitsuTrait { $response = $this->getResponse('POST', ...$args); $validResponseCodes = [200, 201]; - if ( ! \in_array((int) $response->getStatus(), $validResponseCodes, TRUE)) + if ( ! in_array($response->getStatus(), $validResponseCodes, TRUE) && $logger) { - if ($logger) - { - $logger->warning('Non 201 response for POST api call', $response->getBody()); - } + $logger->warning('Non 201 response for POST api call', $response->getBody()); } return JSON::decode(wait($response->getBody()->buffer()), TRUE); @@ -254,6 +258,6 @@ trait KitsuTrait { protected function deleteRequest(...$args): bool { $response = $this->getResponse('DELETE', ...$args); - return ((int) $response->getStatus() === 204); + return ($response->getStatus() === 204); } } \ No newline at end of file diff --git a/src/AnimeClient/API/Kitsu/Model.php b/src/AnimeClient/API/Kitsu/Model.php index 15c5687d..ad02b958 100644 --- a/src/AnimeClient/API/Kitsu/Model.php +++ b/src/AnimeClient/API/Kitsu/Model.php @@ -582,7 +582,7 @@ final class Model { { $defaultOptions = [ 'filter' => [ - 'user_id' => $this->getUserIdByUsername($this->getUsername()), + 'user_id' => $this->getUserId(), 'kind' => 'anime' ], 'page' => [ @@ -608,7 +608,7 @@ final class Model { { $options = [ 'filter' => [ - 'user_id' => $this->getUserIdByUsername($this->getUsername()), + 'user_id' => $this->getUserId(), 'kind' => 'anime', 'status' => $status, ], @@ -683,7 +683,7 @@ final class Model { $options = [ 'query' => [ 'filter' => [ - 'user_id' => $this->getUserIdByUsername($this->getUsername()), + 'user_id' => $this->getUserId(), 'kind' => 'manga', 'status' => $status, ], @@ -811,7 +811,7 @@ final class Model { { $defaultOptions = [ 'filter' => [ - 'user_id' => $this->getUserIdByUsername($this->getUsername()), + 'user_id' => $this->getUserId(), 'kind' => 'manga' ], 'page' => [ @@ -866,7 +866,7 @@ final class Model { */ public function createListItem(array $data): ?Request { - $data['user_id'] = $this->getUserIdByUsername($this->getUsername()); + $data['user_id'] = $this->getUserId(); if ($data['id'] === NULL) { return NULL; @@ -941,7 +941,7 @@ final class Model { { $options = [ 'filter' => [ - 'user_id' => $this->getUserIdByUsername($this->getUsername()), + 'user_id' => $this->getUserId(), 'kind' => $type, ], 'include' => "{$type},{$type}.mappings", @@ -1001,7 +1001,7 @@ final class Model { 'query' => [ 'filter' => [ 'kind' => 'progressed,updated', - 'userId' => $this->getUserIdByUsername($this->getUsername()), + 'userId' => $this->getUserId(), ], 'page' => [ 'offset' => $offset, @@ -1018,6 +1018,18 @@ final class Model { ]); } + private function getUserId(): string + { + static $userId = NULL; + + if ($userId === NULL) + { + $userId = $this->getUserIdByUsername($this->getUsername()); + } + + return $userId; + } + /** * Get the kitsu username from config * @@ -1105,7 +1117,7 @@ final class Model { $options = [ 'query' => [ 'filter' => [ - 'user_id' => $this->getUserIdByUsername(), + 'user_id' => $this->getUserId(), 'kind' => $type, ], 'page' => [ @@ -1175,7 +1187,7 @@ final class Model { { $defaultOptions = [ 'filter' => [ - 'user_id' => $this->getUserIdByUsername($this->getUsername()), + 'user_id' => $this->getUserId(), 'kind' => $type, ], 'page' => [ diff --git a/src/AnimeClient/Controller.php b/src/AnimeClient/Controller.php index 46ae7e4f..5fc6c034 100644 --- a/src/AnimeClient/Controller.php +++ b/src/AnimeClient/Controller.php @@ -16,6 +16,7 @@ namespace Aviat\AnimeClient; +use Aviat\AnimeClient\Enum\EventType; use function Aviat\Ion\_dir; use Aura\Router\Generator; @@ -32,6 +33,7 @@ use Aviat\Ion\Di\{ Exception\ContainerException, Exception\NotFoundException }; +use Aviat\Ion\Event; use Aviat\Ion\Exception\DoubleRenderException; use Aviat\Ion\View\{HtmlView, HttpView, JsonView}; use InvalidArgumentException; @@ -131,6 +133,9 @@ class Controller { 'url_type' => 'anime', 'urlGenerator' => $urlGenerator, ]; + + Event::on(EventType::CLEAR_CACHE, fn () => $this->emptyCache()); + Event::on(EventType::RESET_CACHE_KEY, fn (string $key) => $this->removeCacheItem($key)); } /** @@ -430,5 +435,15 @@ class Controller { (new HttpView($this->container))->redirect($url, $code); exit(); } + + private function emptyCache(): void + { + $this->cache->emptyCache(); + } + + private function removeCacheItem(string $key): void + { + $this->cache->deleteItem($key); + } } // End of BaseController.php \ No newline at end of file diff --git a/src/AnimeClient/Controller/Misc.php b/src/AnimeClient/Controller/Misc.php index 035248f4..0e5a5d42 100644 --- a/src/AnimeClient/Controller/Misc.php +++ b/src/AnimeClient/Controller/Misc.php @@ -17,6 +17,8 @@ namespace Aviat\AnimeClient\Controller; use Aviat\AnimeClient\Controller as BaseController; +use Aviat\AnimeClient\Enum\EventType; +use Aviat\Ion\Event; use Aviat\Ion\View\HtmlView; /** @@ -30,7 +32,10 @@ final class Misc extends BaseController { */ public function clearCache(): void { - $this->cache->clear(); + $this->checkAuth(); + + Event::emit(EventType::CLEAR_CACHE); + $this->outputHTML('blank', [ 'title' => 'Cache cleared' ]); @@ -89,8 +94,6 @@ final class Misc extends BaseController { */ public function logout(): void { - $this->checkAuth(); - $auth = $this->container->get('auth'); $auth->logout(); diff --git a/src/AnimeClient/Dispatcher.php b/src/AnimeClient/Dispatcher.php index ecb650a3..5d58b413 100644 --- a/src/AnimeClient/Dispatcher.php +++ b/src/AnimeClient/Dispatcher.php @@ -16,12 +16,14 @@ namespace Aviat\AnimeClient; +use Aviat\AnimeClient\Enum\EventType; use function Aviat\Ion\_dir; use Aura\Router\{Map, Matcher, Route, Rule}; use Aviat\AnimeClient\API\FailedResponseException; use Aviat\Ion\Di\ContainerInterface; +use Aviat\Ion\Event; use Aviat\Ion\Friend; use Aviat\Ion\Type\StringType; use LogicException; @@ -161,10 +163,7 @@ final class Dispatcher extends RoutingBase { throw new LogicException('Missing controller'); } - if (array_key_exists('controller', $route->attributes)) - { - $controllerName = $route->attributes['controller']; - } + $controllerName = $route->attributes['controller']; // Get the full namespace for a controller if a short name is given if (strpos($controllerName, '\\') === FALSE) @@ -283,7 +282,7 @@ final class Dispatcher extends RoutingBase { $logger->debug('Dispatcher - controller arguments', $params); } - \call_user_func_array([$controller, $method], $params); + call_user_func_array([$controller, $method], $params); } catch (FailedResponseException $e) { @@ -293,7 +292,14 @@ final class Dispatcher extends RoutingBase { 'API request timed out', 'Failed to retrieve data from API (╯°□°)╯︵ ┻━┻'); } - + finally + { + // Log out on session/api token expiration + Event::on(EventType::UNAUTHORIZED, static function () { + $controllerName = DEFAULT_CONTROLLER; + (new $controllerName($this->container))->logout(); + }); + } } /** diff --git a/src/AnimeClient/Enum/EventType.php b/src/AnimeClient/Enum/EventType.php new file mode 100644 index 00000000..5edf0c9f --- /dev/null +++ b/src/AnimeClient/Enum/EventType.php @@ -0,0 +1,25 @@ + + * @copyright 2015 - 2020 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 5 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Enum; + +use Aviat\Ion\Enum as BaseEnum; + +final class EventType extends BaseEnum { + public const CLEAR_CACHE = '::clear-cache::'; + public const RESET_CACHE_KEY = '::reset-cache-key::'; + public const UNAUTHORIZED = '::unauthorized::'; +} \ No newline at end of file diff --git a/src/Ion/Event.php b/src/Ion/Event.php new file mode 100644 index 00000000..9cf226d7 --- /dev/null +++ b/src/Ion/Event.php @@ -0,0 +1,55 @@ + + * @copyright 2015 - 2020 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 5 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\Ion; + +/** + * A basic event handler + */ +class Event { + private static array $eventMap = []; + + /** + * Subscribe to an event + * + * @param string $eventName + * @param callable $handler + */ + public static function on(string $eventName, callable $handler): void + { + if ( ! array_key_exists($eventName, static::$eventMap)) + { + static::$eventMap[$eventName] = []; + } + + static::$eventMap[$eventName][] = $handler; + } + + /** + * Fire off an event + * + * @param string $eventName + * @param array $args + */ + public static function emit(string $eventName, array $args = []): void + { + // Call each subscriber with the provided arguments + if (array_key_exists($eventName, static::$eventMap)) + { + array_walk(static::$eventMap[$eventName], fn ($fn) => $fn(...$args)); + } + } +} \ No newline at end of file From e4018c269f3cd2c7ca04fd285e76b2c37acec2e3 Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Fri, 8 May 2020 19:15:21 -0400 Subject: [PATCH 11/21] Simplify caching --- .gitignore | 4 +- app/bootstrap.php | 9 +- app/config/cache.toml.example | 2 +- composer.json | 2 +- index.php | 2 +- src/AnimeClient/API/CacheTrait.php | 45 ++++- src/AnimeClient/API/Kitsu.php | 2 + src/AnimeClient/API/Kitsu/Auth.php | 107 ++++------ src/AnimeClient/API/Kitsu/Model.php | 79 +++++--- .../Transformer/CharacterTransformer.php | 2 +- .../Kitsu/Transformer/MangaTransformer.php | 8 +- src/AnimeClient/AnimeClient.php | 36 +++- src/AnimeClient/Command/BaseCommand.php | 189 +++++++++--------- src/AnimeClient/Command/CacheClear.php | 14 +- src/AnimeClient/Command/CachePrime.php | 25 +-- src/AnimeClient/Controller.php | 23 +-- 16 files changed, 300 insertions(+), 249 deletions(-) diff --git a/.gitignore b/.gitignore index cab47bdd..9056ec31 100644 --- a/.gitignore +++ b/.gitignore @@ -146,4 +146,6 @@ public/images/manga/** public/images/characters/** public/images/people/** public/mal_mappings.json -.phpunit.result.cache \ No newline at end of file +.phpunit.result.cache + +.is-dev \ No newline at end of file diff --git a/app/bootstrap.php b/app/bootstrap.php index 02dd99c7..34068f1b 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -25,10 +25,11 @@ use Aviat\AnimeClient\API\{ Kitsu\KitsuRequestBuilder }; use Aviat\AnimeClient\Model; -use Aviat\Banker\Pool; +use Aviat\Banker\Teller; use Aviat\Ion\Config; use Aviat\Ion\Di\Container; use Aviat\Ion\Di\ContainerInterface; +use Psr\SimpleCache\CacheInterface; use Laminas\Diactoros\{Response, ServerRequestFactory}; use Monolog\Handler\RotatingFileHandler; use Monolog\Logger; @@ -64,10 +65,10 @@ return static function (array $configArray = []): Container { $container->set('config', fn () => new Config($configArray)); // Create Cache Object - $container->set('cache', static function(ContainerInterface $container): Pool { + $container->set('cache', static function(ContainerInterface $container): CacheInterface { $logger = $container->getLogger(); $config = $container->get('config')->get('cache'); - return new Pool($config, $logger); + return new Teller($config, $logger); }); // Create Aura Router Object @@ -113,7 +114,7 @@ return static function (array $configArray = []): Container { // Models $container->set('kitsu-model', static function(ContainerInterface $container): Kitsu\Model { - $requestBuilder = new KitsuRequestBuilder(); + $requestBuilder = new KitsuRequestBuilder($container); $requestBuilder->setLogger($container->getLogger('kitsu-request')); $listItem = new Kitsu\ListItem(); diff --git a/app/config/cache.toml.example b/app/config/cache.toml.example index 6de18499..74935015 100644 --- a/app/config/cache.toml.example +++ b/app/config/cache.toml.example @@ -4,7 +4,7 @@ # See https://git.timshomepage.net/aviat/banker for more information -# Available drivers are apcu, memcache, memcached, redis or null +# Available drivers are memcached, redis or null # Null cache driver means no caching driver = "redis" diff --git a/composer.json b/composer.json index b4b2c23c..cea18548 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "aura/html": "^2.5.0", "aura/router": "^3.1.0", "aura/session": "^2.1.0", - "aviat/banker": "^2.0.0", + "aviat/banker": "^3.1.1", "aviat/query": "^3.0.0", "danielstjules/stringy": "^3.1.0", "ext-dom": "*", diff --git a/index.php b/index.php index 93f533d9..be33f4aa 100644 --- a/index.php +++ b/index.php @@ -27,7 +27,7 @@ setlocale(LC_CTYPE, 'en_US'); // Load composer autoloader require_once __DIR__ . '/vendor/autoload.php'; -if (array_key_exists('ENV', $_SERVER) && $_SERVER['ENV'] === 'development') +if (file_exists('.is-dev')) { $whoops = new Run; $whoops->pushHandler(new PrettyPageHandler); diff --git a/src/AnimeClient/API/CacheTrait.php b/src/AnimeClient/API/CacheTrait.php index bc1e945d..dbb01271 100644 --- a/src/AnimeClient/API/CacheTrait.php +++ b/src/AnimeClient/API/CacheTrait.php @@ -16,7 +16,8 @@ namespace Aviat\AnimeClient\API; -use Aviat\Banker\Pool; +use Psr\SimpleCache\CacheInterface; +use Psr\SimpleCache\InvalidArgumentException; /** * Helper methods for dealing with the Cache @@ -24,17 +25,17 @@ use Aviat\Banker\Pool; trait CacheTrait { /** - * @var Pool + * @var CacheInterface */ - protected Pool $cache; + protected CacheInterface $cache; /** * Inject the cache object * - * @param Pool $cache + * @param CacheInterface $cache * @return $this */ - public function setCache(Pool $cache): self + public function setCache(CacheInterface $cache): self { $this->cache = $cache; return $this; @@ -43,13 +44,41 @@ trait CacheTrait { /** * Get the cache object if it exists * - * @return Pool + * @return CacheInterface */ - public function getCache(): Pool + public function getCache(): CacheInterface { return $this->cache; } + /** + * Get the cached value if it exists, otherwise set the cache value + * and return it. + * + * @param string $key + * @param callable $primer + * @param array $primeArgs + * @return mixed|null + * @throws InvalidArgumentException + */ + public function getCached(string $key, callable $primer, ?array $primeArgs = []) + { + $value = $this->cache->get($key, NULL); + + if ($value === NULL) + { + $value = $primer(...$primeArgs); + if ($value === NULL) + { + return NULL; + } + + $this->cache->set($key, $value); + } + + return $value; + } + /** * Generate a hash as a cache key from the current method call * @@ -61,7 +90,7 @@ trait CacheTrait { public function getHashForMethodCall($object, string $method, array $args = []): string { $keyObj = [ - 'class' => \get_class($object), + 'class' => get_class($object), 'method' => $method, 'args' => $args, ]; diff --git a/src/AnimeClient/API/Kitsu.php b/src/AnimeClient/API/Kitsu.php index 40268466..409c905a 100644 --- a/src/AnimeClient/API/Kitsu.php +++ b/src/AnimeClient/API/Kitsu.php @@ -28,6 +28,8 @@ final class Kitsu { public const AUTH_TOKEN_CACHE_KEY = 'kitsu-auth-token'; public const AUTH_TOKEN_EXP_CACHE_KEY = 'kitsu-auth-token-expires'; public const AUTH_TOKEN_REFRESH_CACHE_KEY = 'kitsu-auth-token-refresh'; + public const ANIME_HISTORY_LIST_CACHE_KEY = 'kitsu-anime-history-list'; + public const MANGA_HISTORY_LIST_CACHE_KEY = 'kitsu-manga-history-list'; /** * Determine whether an anime is airing, finished airing, or has not yet aired diff --git a/src/AnimeClient/API/Kitsu/Auth.php b/src/AnimeClient/API/Kitsu/Auth.php index 729aa5a7..cfb5c38b 100644 --- a/src/AnimeClient/API/Kitsu/Auth.php +++ b/src/AnimeClient/API/Kitsu/Auth.php @@ -18,7 +18,6 @@ namespace Aviat\AnimeClient\API\Kitsu; use Aura\Session\Segment; -use Aviat\Banker\Exception\InvalidArgumentException; use const Aviat\AnimeClient\SESSION_SEGMENT; use Aviat\AnimeClient\API\{ @@ -29,8 +28,9 @@ use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException}; use Aviat\Ion\Event; +use Psr\SimpleCache\InvalidArgumentException; + use Throwable; -use const PHP_SAPI; /** * Kitsu API Authentication @@ -77,7 +77,6 @@ final class Auth { * * @param string $password * @return boolean - * @throws InvalidArgumentException * @throws Throwable */ public function authenticate(string $password): bool @@ -95,12 +94,11 @@ final class Auth { * * @param string $refreshToken * @return boolean - * @throws InvalidArgumentException - * @throws Throwable + * @throws Throwable|InvalidArgumentException */ public function reAuthenticate(?string $refreshToken): bool { - $refreshToken ??= $this->getAuthToken(); + $refreshToken ??= $this->getRefreshToken(); if (empty($refreshToken)) { @@ -116,6 +114,7 @@ final class Auth { * Check whether the current user is authenticated * * @return boolean + * @throws InvalidArgumentException */ public function isAuthenticated(): bool { @@ -136,87 +135,59 @@ final class Auth { * Retrieve the authentication token from the session * * @return string + * @throws InvalidArgumentException */ - private function getAuthToken(): ?string + public function getAuthToken(): ?string { - $now = time(); - - if (PHP_SAPI === 'cli') - { - $token = $this->cacheGet(K::AUTH_TOKEN_CACHE_KEY, NULL); - $refreshToken = $this->cacheGet(K::AUTH_TOKEN_REFRESH_CACHE_KEY, NULL); - $expireTime = $this->cacheGet(K::AUTH_TOKEN_EXP_CACHE_KEY); - $isExpired = $now > $expireTime; - } - else - { - $token = $this->segment->get('auth_token', NULL); - $refreshToken = $this->segment->get('refresh_token', NULL); - $isExpired = $now > $this->segment->get('auth_token_expires', $now + 5000); - } - - // Attempt to re-authenticate with refresh token - /* if ($isExpired === TRUE && $refreshToken !== NULL) - { - if ($this->reAuthenticate($refreshToken) !== NULL) - { - return (PHP_SAPI === 'cli') - ? $this->cacheGet(K::AUTH_TOKEN_CACHE_KEY, NULL) - : $this->segment->get('auth_token', NULL); - } - - return NULL; - }*/ - - return $token; + return $this->segment->get('auth_token', NULL) + ?? $this->cache->get(K::AUTH_TOKEN_CACHE_KEY, NULL); } + /** + * Retrieve the refresh token + * + * @return string|null + * @throws InvalidArgumentException + */ private function getRefreshToken(): ?string { - return (PHP_SAPI === 'cli') - ? $this->cacheGet(K::AUTH_TOKEN_REFRESH_CACHE_KEY, NULL) - : $this->segment->get('refresh_token'); + return $this->segment->get('refresh_token') + ?? $this->cache->get(K::AUTH_TOKEN_REFRESH_CACHE_KEY, NULL); } - private function storeAuth(bool $auth): bool + /** + * Save the new authentication information + * + * @param $auth + * @return bool + * @throws InvalidArgumentException + */ + private function storeAuth($auth): bool { if (FALSE !== $auth) { - // Set the token in the cache for command line operations - $cacheItem = $this->cache->getItem(K::AUTH_TOKEN_CACHE_KEY); - $cacheItem->set($auth['access_token']); - $cacheItem->save(); - - // Set the token expiration in the cache $expire_time = $auth['created_at'] + $auth['expires_in']; - $cacheItem = $this->cache->getItem(K::AUTH_TOKEN_EXP_CACHE_KEY); - $cacheItem->set($expire_time); - $cacheItem->save(); + // Set the token in the cache for command line operations + // Set the token expiration in the cache // Set the refresh token in the cache - $cacheItem = $this->cache->getItem(K::AUTH_TOKEN_REFRESH_CACHE_KEY); - $cacheItem->set($auth['refresh_token']); - $cacheItem->save(); + $saved = $this->cache->setMultiple([ + K::AUTH_TOKEN_CACHE_KEY => $auth['access_token'], + K::AUTH_TOKEN_EXP_CACHE_KEY => $expire_time, + K::AUTH_TOKEN_REFRESH_CACHE_KEY => $auth['refresh_token'], + ]); // Set the session values - $this->segment->set('auth_token', $auth['access_token']); - $this->segment->set('auth_token_expires', $expire_time); - $this->segment->set('refresh_token', $auth['refresh_token']); - return TRUE; + if ($saved) + { + $this->segment->set('auth_token', $auth['access_token']); + $this->segment->set('auth_token_expires', $expire_time); + $this->segment->set('refresh_token', $auth['refresh_token']); + return TRUE; + } } return FALSE; } - - private function cacheGet(string $key, $default = NULL) - { - $cacheItem = $this->cache->getItem($key); - if ( ! $cacheItem->isHit()) - { - return $default; - } - - return $cacheItem->get(); - } } // End of KitsuAuth.php \ No newline at end of file diff --git a/src/AnimeClient/API/Kitsu/Model.php b/src/AnimeClient/API/Kitsu/Model.php index ad02b958..5c5a90e4 100644 --- a/src/AnimeClient/API/Kitsu/Model.php +++ b/src/AnimeClient/API/Kitsu/Model.php @@ -190,11 +190,8 @@ final class Model { $username = $this->getUsername(); } - $cacheItem = $this->cache->getItem(K::AUTH_USER_ID_KEY); - - if ( ! $cacheItem->isHit()) - { - $data = $this->getRequest('users', [ + return $this->getCached(K::AUTH_USER_ID_KEY, function(string $username) { + $data = $this->requestBuilder->getRequest('users', [ 'query' => [ 'filter' => [ 'name' => $username @@ -202,11 +199,8 @@ final class Model { ] ]); - $cacheItem->set($data['data'][0]['id']); - $cacheItem->save(); - } - - return $cacheItem->get(); + return $data['data'][0]['id'] ?? NULL; + }, [$username]); } /** @@ -398,11 +392,23 @@ final class Model { */ public function getAnimeHistory(): array { - $raw = $this->getRawHistoryList('anime'); - $organized = JsonAPI::organizeData($raw); - $organized = array_filter($organized, fn ($item) => array_key_exists('relationships', $item)); + $key = K::ANIME_HISTORY_LIST_CACHE_KEY; + $list = $this->cache->get($key, NULL); - return (new AnimeHistoryTransformer())->transform($organized); + if ($list === NULL) + { + $raw = $this->getRawHistoryList('anime'); + + $organized = JsonAPI::organizeData($raw); + $organized = array_filter($organized, fn ($item) => array_key_exists('relationships', $item)); + + $list = (new AnimeHistoryTransformer())->transform($organized); + + $this->cache->set($key, $list); + + } + + return $list; } /** @@ -426,9 +432,11 @@ final class Model { */ public function getAnimeList(string $status): array { - $cacheItem = $this->cache->getItem("kitsu-anime-list-{$status}"); + $key = "kitsu-anime-list-{$status}"; - if ( ! $cacheItem->isHit()) + $list = $this->cache->get($key, NULL); + + if ($list === NULL) { $data = $this->getRawAnimeList($status) ?? []; @@ -454,11 +462,11 @@ final class Model { $keyed[$item['id']] = $item; } - $cacheItem->set($keyed); - $cacheItem->save(); + $list = $keyed; + $this->cache->set($key, $list); } - return $cacheItem->get(); + return $list; } /** @@ -650,11 +658,21 @@ final class Model { */ public function getMangaHistory(): array { - $raw = $this->getRawHistoryList('manga'); - $organized = JsonAPI::organizeData($raw); - $organized = array_filter($organized, fn ($item) => array_key_exists('relationships', $item)); + $key = K::MANGA_HISTORY_LIST_CACHE_KEY; + $list = $this->cache->get($key, NULL); - return (new MangaHistoryTransformer())->transform($organized); + if ($list === NULL) + { + $raw = $this->getRawHistoryList('manga'); + $organized = JsonAPI::organizeData($raw); + $organized = array_filter($organized, fn ($item) => array_key_exists('relationships', $item)); + + $list = (new MangaHistoryTransformer())->transform($organized); + + $this->cache->set($key, $list); + } + + return $list; } /** @@ -696,11 +714,13 @@ final class Model { ] ]; - $cacheItem = $this->cache->getItem("kitsu-manga-list-{$status}"); + $key = "kitsu-manga-list-{$status}"; - if ( ! $cacheItem->isHit()) + $list = $this->cache->get($key, NULL); + + if ($list === NULL) { - $data = $this->getRequest('library-entries', $options) ?? []; + $data = $this->requestBuilder->getRequest('library-entries', $options) ?? []; // Bail out on no data if (empty($data) || ( ! array_key_exists('included', $data))) @@ -717,13 +737,12 @@ final class Model { } unset($item); - $transformed = $this->mangaListTransformer->transformCollection($data['data']); + $list = $this->mangaListTransformer->transformCollection($data['data']); - $cacheItem->set($transformed); - $cacheItem->save(); + $this->cache->set($key, $list); } - return $cacheItem->get(); + return $list; } /** diff --git a/src/AnimeClient/API/Kitsu/Transformer/CharacterTransformer.php b/src/AnimeClient/API/Kitsu/Transformer/CharacterTransformer.php index 17a259fc..49fb45f7 100644 --- a/src/AnimeClient/API/Kitsu/Transformer/CharacterTransformer.php +++ b/src/AnimeClient/API/Kitsu/Transformer/CharacterTransformer.php @@ -149,7 +149,7 @@ final class CharacterTransformer extends AbstractTransformer { $person = $p['attributes']; $person['id'] = $pid; - $person['image'] = $person['image']['original']; + $person['image'] = $person['image']['original'] ?? ''; uasort($role['relationships']['media']['anime'], static function ($a, $b) { return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle']; diff --git a/src/AnimeClient/API/Kitsu/Transformer/MangaTransformer.php b/src/AnimeClient/API/Kitsu/Transformer/MangaTransformer.php index c5018318..b697d4d0 100644 --- a/src/AnimeClient/API/Kitsu/Transformer/MangaTransformer.php +++ b/src/AnimeClient/API/Kitsu/Transformer/MangaTransformer.php @@ -98,16 +98,12 @@ final class MangaTransformer extends AbstractTransformer { if ( ! empty($characters['main'])) { - uasort($characters['main'], static function ($a, $b) { - return $a['name'] <=> $b['name']; - }); + uasort($characters['main'], fn ($a, $b) => $a['name'] <=> $b['anime']); } if ( ! empty($characters['supporting'])) { - uasort($characters['supporting'], static function ($a, $b) { - return $a['name'] <=> $b['name']; - }); + uasort($characters['supporting'], fn ($a, $b) => $a['name'] <=> $b['anime']); } ksort($characters); diff --git a/src/AnimeClient/AnimeClient.php b/src/AnimeClient/AnimeClient.php index 4fca5075..42eadf73 100644 --- a/src/AnimeClient/AnimeClient.php +++ b/src/AnimeClient/AnimeClient.php @@ -16,6 +16,9 @@ namespace Aviat\AnimeClient; +use Aviat\AnimeClient\API\Kitsu; +use Psr\SimpleCache\CacheInterface; +use Psr\SimpleCache\InvalidArgumentException; use function Amp\Promise\wait; use Amp\Http\Client\Request; @@ -26,6 +29,8 @@ use Amp\Http\Client\HttpClientBuilder; use Aviat\Ion\ConfigInterface; use Yosymfony\Toml\{Toml, TomlBuilder}; +use Throwable; + // ---------------------------------------------------------------------------- //! TOML Functions // ---------------------------------------------------------------------------- @@ -232,7 +237,7 @@ function getApiClient (): HttpClient * * @param string|Request $request * @return Response - * @throws \Throwable + * @throws Throwable */ function getResponse ($request): Response { @@ -256,7 +261,7 @@ function getResponse ($request): Response */ function getLocalImg ($kitsuUrl, $webp = TRUE): string { - if ( ! is_string($kitsuUrl)) + if (empty($kitsuUrl) || ( ! is_string($kitsuUrl))) { return 'images/placeholder.webp'; } @@ -345,4 +350,31 @@ function col_not_empty(array $search, string $key): bool { $items = array_filter(array_column($search, $key), fn ($x) => ( ! empty($x))); return count($items) > 0; +} + +/** + * Clear the cache, but save user auth data + * + * @param CacheInterface $cache + * @return bool + * @throws InvalidArgumentException + */ +function clearCache(CacheInterface $cache): bool +{ + // Save the user data, if it exists, for priming the cache + $userData = $cache->getMultiple([ + Kitsu::AUTH_USER_ID_KEY, + Kitsu::AUTH_TOKEN_CACHE_KEY, + Kitsu::AUTH_TOKEN_EXP_CACHE_KEY, + Kitsu::AUTH_TOKEN_REFRESH_CACHE_KEY, + ], NULL); + + $userData = array_filter((array)$userData, fn ($value) => $value !== NULL); + $cleared = $cache->clear(); + + $saved = ( ! empty($userData)) + ? $cache->setMultiple($userData) + : TRUE; + + return $cleared && $saved; } \ No newline at end of file diff --git a/src/AnimeClient/Command/BaseCommand.php b/src/AnimeClient/Command/BaseCommand.php index 5f6002cf..7f03656d 100644 --- a/src/AnimeClient/Command/BaseCommand.php +++ b/src/AnimeClient/Command/BaseCommand.php @@ -24,7 +24,7 @@ use Aura\Session\SessionFactory; use Aviat\AnimeClient\{Model, UrlGenerator, Util}; use Aviat\AnimeClient\API\{Anilist, CacheTrait, Kitsu}; use Aviat\AnimeClient\API\Kitsu\KitsuRequestBuilder; -use Aviat\Banker\Pool; +use Aviat\Banker\Teller; use Aviat\Ion\Config; use Aviat\Ion\Di\{Container, ContainerInterface, ContainerAware}; use ConsoleKit\{Colors, Command, ConsoleException}; @@ -129,99 +129,7 @@ abstract class BaseCommand extends Command { $configArray = array_replace_recursive($baseConfig, $config, $overrideConfig); - $di = static function (array $configArray) use ($APP_DIR): Container { - $container = new Container(); - - // ------------------------------------------------------------------------- - // Logging - // ------------------------------------------------------------------------- - - $app_logger = new Logger('animeclient'); - $app_logger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/app-cli.log', Logger::NOTICE)); - - $kitsu_request_logger = new Logger('kitsu-request'); - $kitsu_request_logger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/kitsu_request-cli.log', Logger::NOTICE)); - - $anilistRequestLogger = new Logger('anilist-request'); - $anilistRequestLogger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/anilist_request-cli.log', Logger::NOTICE)); - - $container->setLogger($app_logger); - $container->setLogger($anilistRequestLogger, 'anilist-request'); - $container->setLogger($kitsu_request_logger, 'kitsu-request'); - - // Create Config Object - $container->set('config', fn () => new Config($configArray)); - - // Create Cache Object - $container->set('cache', static function($container) { - $logger = $container->getLogger(); - $config = $container->get('config')->get('cache'); - return new Pool($config, $logger); - }); - - // Create Aura Router Object - $container->set('aura-router', fn () => new RouterContainer); - - // Create Request/Response Objects - $container->set('request', fn () => ServerRequestFactory::fromGlobals( - $_SERVER, - $_GET, - $_POST, - $_COOKIE, - $_FILES - )); - $container->set('response', fn () => new Response); - - // Create session Object - $container->set('session', fn () => (new SessionFactory())->newInstance($_COOKIE)); - - // Models - $container->set('kitsu-model', static function($container): Kitsu\Model { - $requestBuilder = new KitsuRequestBuilder(); - $requestBuilder->setLogger($container->getLogger('kitsu-request')); - - $listItem = new Kitsu\ListItem(); - $listItem->setContainer($container); - $listItem->setRequestBuilder($requestBuilder); - - $model = new Kitsu\Model($listItem); - $model->setContainer($container); - $model->setRequestBuilder($requestBuilder); - - $cache = $container->get('cache'); - $model->setCache($cache); - return $model; - }); - $container->set('anilist-model', static function ($container): Anilist\Model { - $requestBuilder = new Anilist\AnilistRequestBuilder(); - $requestBuilder->setLogger($container->getLogger('anilist-request')); - - $listItem = new Anilist\ListItem(); - $listItem->setContainer($container); - $listItem->setRequestBuilder($requestBuilder); - - $model = new Anilist\Model($listItem); - $model->setContainer($container); - $model->setRequestBuilder($requestBuilder); - - return $model; - }); - $container->set('settings-model', static function($container): Model\Settings { - $model = new Model\Settings($container->get('config')); - $model->setContainer($container); - return $model; - }); - - $container->set('auth', fn ($container) => new Kitsu\Auth($container)); - - $container->set('url-generator', fn ($container) => new UrlGenerator($container)); - - $container->set('util', fn ($container) => new Util($container)); - - return $container; - }; - - return $di($configArray); + return $this->_di($configArray, $APP_DIR); } private function _line(string $message, $fgColor = NULL, $bgColor = NULL): void @@ -229,4 +137,97 @@ abstract class BaseCommand extends Command { $message = Colors::colorize($message, $fgColor, $bgColor); $this->getConsole()->writeln($message); } + + private function _di(array $configArray, string $APP_DIR): ContainerInterface + { + $container = new Container(); + + // ------------------------------------------------------------------------- + // Logging + // ------------------------------------------------------------------------- + + $app_logger = new Logger('animeclient'); + $app_logger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/app-cli.log', Logger::NOTICE)); + + $kitsu_request_logger = new Logger('kitsu-request'); + $kitsu_request_logger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/kitsu_request-cli.log', Logger::NOTICE)); + + $anilistRequestLogger = new Logger('anilist-request'); + $anilistRequestLogger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/anilist_request-cli.log', Logger::NOTICE)); + + $container->setLogger($app_logger); + $container->setLogger($anilistRequestLogger, 'anilist-request'); + $container->setLogger($kitsu_request_logger, 'kitsu-request'); + + // Create Config Object + $container->set('config', fn () => new Config($configArray)); + + // Create Cache Object + $container->set('cache', static function($container) { + $logger = $container->getLogger(); + $config = $container->get('config')->get('cache'); + return new Teller($config, $logger); + }); + + // Create Aura Router Object + $container->set('aura-router', fn () => new RouterContainer); + + // Create Request/Response Objects + $container->set('request', fn () => ServerRequestFactory::fromGlobals( + $_SERVER, + $_GET, + $_POST, + $_COOKIE, + $_FILES + )); + $container->set('response', fn () => new Response); + + // Create session Object + $container->set('session', fn () => (new SessionFactory())->newInstance($_COOKIE)); + + // Models + $container->set('kitsu-model', static function($container): Kitsu\Model { + $requestBuilder = new KitsuRequestBuilder($container); + $requestBuilder->setLogger($container->getLogger('kitsu-request')); + + $listItem = new Kitsu\ListItem(); + $listItem->setContainer($container); + $listItem->setRequestBuilder($requestBuilder); + + $model = new Kitsu\Model($listItem); + $model->setContainer($container); + $model->setRequestBuilder($requestBuilder); + + $cache = $container->get('cache'); + $model->setCache($cache); + return $model; + }); + $container->set('anilist-model', static function ($container): Anilist\Model { + $requestBuilder = new Anilist\AnilistRequestBuilder(); + $requestBuilder->setLogger($container->getLogger('anilist-request')); + + $listItem = new Anilist\ListItem(); + $listItem->setContainer($container); + $listItem->setRequestBuilder($requestBuilder); + + $model = new Anilist\Model($listItem); + $model->setContainer($container); + $model->setRequestBuilder($requestBuilder); + + return $model; + }); + $container->set('settings-model', static function($container): Model\Settings { + $model = new Model\Settings($container->get('config')); + $model->setContainer($container); + return $model; + }); + + $container->set('auth', fn ($container) => new Kitsu\Auth($container)); + + $container->set('url-generator', fn ($container) => new UrlGenerator($container)); + + $container->set('util', fn ($container) => new Util($container)); + + return $container; + } } \ No newline at end of file diff --git a/src/AnimeClient/Command/CacheClear.php b/src/AnimeClient/Command/CacheClear.php index 2d3495e5..f56a93ac 100644 --- a/src/AnimeClient/Command/CacheClear.php +++ b/src/AnimeClient/Command/CacheClear.php @@ -18,6 +18,7 @@ namespace Aviat\AnimeClient\Command; use Aviat\Ion\Di\Exception\ContainerException; use Aviat\Ion\Di\Exception\NotFoundException; +use function Aviat\AnimeClient\clearCache; /** * Clears the API Cache @@ -36,8 +37,17 @@ final class CacheClear extends BaseCommand { { $this->setContainer($this->setupContainer()); - $this->container->get('cache')->clear(); + $cache = $this->container->get('cache'); - $this->echoBox('API Cache has been cleared.'); + $cleared = clearCache($cache); + + if ($cleared) + { + $this->echoBox('API Cache has been cleared.'); + } + else + { + $this->echoErrorBox('Failed to clear cache.'); + } } } diff --git a/src/AnimeClient/Command/CachePrime.php b/src/AnimeClient/Command/CachePrime.php index 2b25cca7..7072fda9 100644 --- a/src/AnimeClient/Command/CachePrime.php +++ b/src/AnimeClient/Command/CachePrime.php @@ -16,8 +16,10 @@ namespace Aviat\AnimeClient\Command; +use Aviat\AnimeClient\API\Kitsu; use Aviat\Ion\Di\Exception\ContainerException; use Aviat\Ion\Di\Exception\NotFoundException; +use function Aviat\AnimeClient\clearCache; /** * Clears the API Cache @@ -35,30 +37,25 @@ final class CachePrime extends BaseCommand { public function execute(array $args, array $options = []): void { $this->setContainer($this->setupContainer()); - $cache = $this->container->get('cache'); - // Save the user id, if it exists, for priming the cache - $userIdItem = $cache->getItem('kitsu-auth-token'); - $userId = $userIdItem->isHit() ? $userIdItem->get() : null; - - $cache->clear(); + $cleared = clearCache($cache); + if ( ! $cleared) + { + $this->echoErrorBox('Failed to clear cache.'); + return; + } $this->echoBox('Cache cleared, re-priming...'); - if ($userId !== NULL) - { - $userIdItem = $cache->getItem('kitsu-auth-token'); - $userIdItem->set($userId); - $userIdItem->save(); - } - $kitsuModel = $this->container->get('kitsu-model'); - // Prime anime list cache + // Prime anime list and history cache + $kitsuModel->getAnimeHistory(); $kitsuModel->getFullOrganizedAnimeList(); // Prime manga list cache + $kitsuModel->getMangaHistory(); $kitsuModel->getFullOrganizedMangaList(); $this->echoBox('API Cache has been primed.'); diff --git a/src/AnimeClient/Controller.php b/src/AnimeClient/Controller.php index 5fc6c034..b15ed34f 100644 --- a/src/AnimeClient/Controller.php +++ b/src/AnimeClient/Controller.php @@ -16,16 +16,16 @@ namespace Aviat\AnimeClient; -use Aviat\AnimeClient\Enum\EventType; use function Aviat\Ion\_dir; +use Aviat\AnimeClient\Enum\EventType; use Aura\Router\Generator; use Aura\Session\Segment; use Aviat\AnimeClient\API\Kitsu\Auth; use Aviat\Ion\ConfigInterface; -use Psr\Cache\CacheItemPoolInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Psr\SimpleCache\CacheInterface; use Aviat\Ion\Di\{ ContainerAware, @@ -53,9 +53,9 @@ class Controller { /** * Cache manager - * @var CacheItemPoolInterface + * @var CacheInterface */ - protected CacheItemPoolInterface $cache; + protected CacheInterface $cache; /** * The global configuration object @@ -134,8 +134,9 @@ class Controller { 'urlGenerator' => $urlGenerator, ]; - Event::on(EventType::CLEAR_CACHE, fn () => $this->emptyCache()); - Event::on(EventType::RESET_CACHE_KEY, fn (string $key) => $this->removeCacheItem($key)); + // Set up 'global' events + Event::on(EventType::CLEAR_CACHE, fn () => clearCache($this->cache)); + Event::on(EventType::RESET_CACHE_KEY, fn (string $key) => $this->cache->delete($key)); } /** @@ -435,15 +436,5 @@ class Controller { (new HttpView($this->container))->redirect($url, $code); exit(); } - - private function emptyCache(): void - { - $this->cache->emptyCache(); - } - - private function removeCacheItem(string $key): void - { - $this->cache->deleteItem($key); - } } // End of BaseController.php \ No newline at end of file From c4fb095eba5e3bc715fc4f4ae4a0cd4491a0285c Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Fri, 8 May 2020 19:16:04 -0400 Subject: [PATCH 12/21] Fix settings page subforms (so all the fields show for the cache) --- app/views/settings/_field.php | 5 +++++ app/views/settings/_form.php | 16 ++++++++-------- app/views/settings/_subfield.php | 20 ++++++++++++++++++++ 3 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 app/views/settings/_field.php create mode 100644 app/views/settings/_subfield.php diff --git a/app/views/settings/_field.php b/app/views/settings/_field.php new file mode 100644 index 00000000..94d5e031 --- /dev/null +++ b/app/views/settings/_field.php @@ -0,0 +1,5 @@ +
      +
      +
      + field($fieldName, $field); ?> +
      \ No newline at end of file diff --git a/app/views/settings/_form.php b/app/views/settings/_form.php index eccaf1ec..9112d175 100644 --- a/app/views/settings/_form.php +++ b/app/views/settings/_form.php @@ -6,19 +6,19 @@ ?> $field): ?> - +

      - +
      -
      -
      -
      - field($fieldname, $field); ?> -
      + - field($fieldname, $field); ?> + field($fieldName, $field); ?> diff --git a/app/views/settings/_subfield.php b/app/views/settings/_subfield.php new file mode 100644 index 00000000..0de2297c --- /dev/null +++ b/app/views/settings/_subfield.php @@ -0,0 +1,20 @@ + + + $field): ?> + + + + + field($fieldName, $field); ?> + + \ No newline at end of file From 4131a019ca58df0f244b1828315c378250a961e2 Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Fri, 8 May 2020 19:17:11 -0400 Subject: [PATCH 13/21] Remove APCu as a cache option...it doesn't work with CLI authentication --- src/AnimeClient/Dispatcher.php | 4 ++-- src/AnimeClient/constants.php | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/AnimeClient/Dispatcher.php b/src/AnimeClient/Dispatcher.php index 5d58b413..e8b58546 100644 --- a/src/AnimeClient/Dispatcher.php +++ b/src/AnimeClient/Dispatcher.php @@ -292,14 +292,14 @@ final class Dispatcher extends RoutingBase { 'API request timed out', 'Failed to retrieve data from API (╯°□°)╯︵ ┻━┻'); } - finally + /* finally { // Log out on session/api token expiration Event::on(EventType::UNAUTHORIZED, static function () { $controllerName = DEFAULT_CONTROLLER; (new $controllerName($this->container))->logout(); }); - } + } */ } /** diff --git a/src/AnimeClient/constants.php b/src/AnimeClient/constants.php index ffde78d4..51f994fa 100644 --- a/src/AnimeClient/constants.php +++ b/src/AnimeClient/constants.php @@ -92,7 +92,6 @@ const SETTINGS_MAP = [ 'title' => 'Cache Type', 'description' => 'The Cache backend', 'options' => [ - 'APCu' => 'apcu', 'Memcached' => 'memcached', 'Redis' => 'redis', 'No Cache' => 'null' From 545c495869338b4c364f3761091c9572a16ee787 Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Fri, 8 May 2020 19:18:10 -0400 Subject: [PATCH 14/21] Refactor KitsuTrait --- .../API/Kitsu/KitsuRequestBuilder.php | 229 ++++++++++++++++++ src/AnimeClient/API/Kitsu/KitsuTrait.php | 226 ----------------- src/AnimeClient/API/Kitsu/ListItem.php | 28 +-- src/AnimeClient/API/Kitsu/Model.php | 72 +++--- 4 files changed, 270 insertions(+), 285 deletions(-) diff --git a/src/AnimeClient/API/Kitsu/KitsuRequestBuilder.php b/src/AnimeClient/API/Kitsu/KitsuRequestBuilder.php index aa105062..4e6b7c21 100644 --- a/src/AnimeClient/API/Kitsu/KitsuRequestBuilder.php +++ b/src/AnimeClient/API/Kitsu/KitsuRequestBuilder.php @@ -16,10 +16,26 @@ namespace Aviat\AnimeClient\API\Kitsu; +use const Aviat\AnimeClient\SESSION_SEGMENT; use const Aviat\AnimeClient\USER_AGENT; + +use function Amp\Promise\wait; +use function Aviat\AnimeClient\getResponse; + +use Amp\Http\Client\Request; +use Amp\Http\Client\Response; use Aviat\AnimeClient\API\APIRequestBuilder; +use Aviat\AnimeClient\API\FailedResponseException; +use Aviat\AnimeClient\API\Kitsu as K; +use Aviat\AnimeClient\Enum\EventType; +use Aviat\Ion\Di\ContainerAware; +use Aviat\Ion\Di\ContainerInterface; +use Aviat\Ion\Event; +use Aviat\Ion\Json; +use Aviat\Ion\JsonException; final class KitsuRequestBuilder extends APIRequestBuilder { + use ContainerAware; /** * The base url for api requests @@ -39,4 +55,217 @@ final class KitsuRequestBuilder extends APIRequestBuilder { 'CLIENT_ID' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd', 'CLIENT_SECRET' => '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151', ]; + + public function __construct(ContainerInterface $container) + { + $this->setContainer($container); + } + + /** + * Create a request object + * + * @param string $type + * @param string $url + * @param array $options + * @return Request + */ + public function setUpRequest(string $type, string $url, array $options = []): Request + { + $request = $this->newRequest($type, $url); + + $sessionSegment = $this->getContainer() + ->get('session') + ->getSegment(SESSION_SEGMENT); + + $cache = $this->getContainer()->get('cache'); + $token = null; + + if ($cache->has(K::AUTH_TOKEN_CACHE_KEY)) + { + $token = $cache->get(K::AUTH_TOKEN_CACHE_KEY); + } + else if ($url !== K::AUTH_URL && $sessionSegment->get('auth_token') !== NULL) + { + $token = $sessionSegment->get('auth_token'); + if ( ! (empty($token) || $cache->has(K::AUTH_TOKEN_CACHE_KEY))) + { + $cache->set(K::AUTH_TOKEN_CACHE_KEY, $token); + } + } + + if ($token !== NULL) + { + $request = $request->setAuth('bearer', $token); + } + + if (array_key_exists('form_params', $options)) + { + $request = $request->setFormFields($options['form_params']); + } + + if (array_key_exists('query', $options)) + { + $request = $request->setQuery($options['query']); + } + + if (array_key_exists('body', $options)) + { + $request = $request->setJsonBody($options['body']); + } + + if (array_key_exists('headers', $options)) + { + $request = $request->setHeaders($options['headers']); + } + + return $request->getFullRequest(); + } + + /** + * Remove some boilerplate for get requests + * + * @param mixed ...$args + * @throws Throwable + * @return array + */ + public function getRequest(...$args): array + { + return $this->request('GET', ...$args); + } + + /** + * Remove some boilerplate for patch requests + * + * @param mixed ...$args + * @throws Throwable + * @return array + */ + public function patchRequest(...$args): array + { + return $this->request('PATCH', ...$args); + } + + /** + * Remove some boilerplate for post requests + * + * @param mixed ...$args + * @throws Throwable + * @return array + */ + public function postRequest(...$args): array + { + $logger = NULL; + if ($this->getContainer()) + { + $logger = $this->container->getLogger('kitsu-request'); + } + + $response = $this->getResponse('POST', ...$args); + $validResponseCodes = [200, 201]; + + if ( ! in_array($response->getStatus(), $validResponseCodes, TRUE) && $logger) + { + $logger->warning('Non 2xx response for POST api call', $response->getBody()); + } + + return JSON::decode(wait($response->getBody()->buffer()), TRUE); + } + + /** + * Remove some boilerplate for delete requests + * + * @param mixed ...$args + * @throws Throwable + * @return bool + */ + public function deleteRequest(...$args): bool + { + $response = $this->getResponse('DELETE', ...$args); + return ($response->getStatus() === 204); + } + + /** + * Make a request + * + * @param string $type + * @param string $url + * @param array $options + * @return Response + * @throws Throwable + */ + public function getResponse(string $type, string $url, array $options = []): Response + { + $logger = NULL; + if ($this->getContainer()) + { + $logger = $this->container->getLogger('kitsu-request'); + } + + $request = $this->setUpRequest($type, $url, $options); + + $response = getResponse($request); + + if ($logger) + { + $logger->debug('Kitsu API Response', [ + 'response_status' => $response->getStatus(), + 'request_headers' => $response->getOriginalRequest()->getHeaders(), + 'response_headers' => $response->getHeaders() + ]); + } + + return $response; + } + + /** + * Make a request + * + * @param string $type + * @param string $url + * @param array $options + * @throws JsonException + * @throws FailedResponseException + * @throws Throwable + * @return array + */ + private function request(string $type, string $url, array $options = []): array + { + $logger = NULL; + if ($this->getContainer()) + { + $logger = $this->container->getLogger('kitsu-request'); + } + + $response = $this->getResponse($type, $url, $options); + $statusCode = $response->getStatus(); + + // Check for requests that are unauthorized + if ($statusCode === 401 || $statusCode === 403) + { + Event::emit(EventType::UNAUTHORIZED); + } + + // Any other type of failed request + if ($statusCode > 299 || $statusCode < 200) + { + if ($logger) + { + $logger->warning('Non 2xx response for api call', (array)$response); + } + + throw new FailedResponseException('Failed to get the proper response from the API'); + } + + try + { + return Json::decode(wait($response->getBody()->buffer())); + } + catch (JsonException $e) + { + print_r($e); + die(); + } + } + + } \ No newline at end of file diff --git a/src/AnimeClient/API/Kitsu/KitsuTrait.php b/src/AnimeClient/API/Kitsu/KitsuTrait.php index 67ce2815..c4d477af 100644 --- a/src/AnimeClient/API/Kitsu/KitsuTrait.php +++ b/src/AnimeClient/API/Kitsu/KitsuTrait.php @@ -16,25 +16,7 @@ namespace Aviat\AnimeClient\API\Kitsu; -use Aviat\AnimeClient\Enum\EventType; -use function in_array; -use const PHP_SAPI; -use const Aviat\AnimeClient\SESSION_SEGMENT; - -use function Amp\Promise\wait; -use function Aviat\AnimeClient\getResponse; - -use Amp\Http\Client\Request; -use Amp\Http\Client\Response; -use Aviat\AnimeClient\API\{FailedResponseException, Kitsu as K}; -use Aviat\Ion\Event; -use Aviat\Ion\Json; -use Aviat\Ion\JsonException; - -use Throwable; - trait KitsuTrait { - /** * The request builder for the Kitsu API * @var KitsuRequestBuilder @@ -52,212 +34,4 @@ trait KitsuTrait { $this->requestBuilder = $requestBuilder; return $this; } - - /** - * Create a request object - * - * @param string $type - * @param string $url - * @param array $options - * @return Request - */ - public function setUpRequest(string $type, string $url, array $options = []): Request - { - $request = $this->requestBuilder->newRequest($type, $url); - - $sessionSegment = $this->getContainer() - ->get('session') - ->getSegment(SESSION_SEGMENT); - - $cache = $this->getContainer()->get('cache'); - $cacheItem = $cache->getItem(K::AUTH_TOKEN_CACHE_KEY); - $token = null; - - if (PHP_SAPI === 'cli' && $cacheItem->isHit()) - { - $token = $cacheItem->get(); - } - else if ($url !== K::AUTH_URL && $sessionSegment->get('auth_token') !== NULL) - { - $token = $sessionSegment->get('auth_token'); - if ( ! (empty($token) || $cacheItem->isHit())) - { - $cacheItem->set($token); - $cacheItem->save(); - } - } - - if ($token !== NULL) - { - $request = $request->setAuth('bearer', $token); - } - - if (array_key_exists('form_params', $options)) - { - $request = $request->setFormFields($options['form_params']); - } - - if (array_key_exists('query', $options)) - { - $request = $request->setQuery($options['query']); - } - - if (array_key_exists('body', $options)) - { - $request = $request->setJsonBody($options['body']); - } - - if (array_key_exists('headers', $options)) - { - $request = $request->setHeaders($options['headers']); - } - - return $request->getFullRequest(); - } - - /** - * Make a request - * - * @param string $type - * @param string $url - * @param array $options - * @return Response - * @throws Throwable - */ - private function getResponse(string $type, string $url, array $options = []): Response - { - $logger = NULL; - if ($this->getContainer()) - { - $logger = $this->container->getLogger('kitsu-request'); - } - - $request = $this->setUpRequest($type, $url, $options); - - $response = getResponse($request); - - if ($logger) - { - $logger->debug('Kitsu API Response', [ - 'response_status' => $response->getStatus(), - 'request_headers' => $response->getOriginalRequest()->getHeaders(), - 'response_headers' => $response->getHeaders() - ]); - } - - return $response; - } - - /** - * Make a request - * - * @param string $type - * @param string $url - * @param array $options - * @throws JsonException - * @throws FailedResponseException - * @throws Throwable - * @return array - */ - private function request(string $type, string $url, array $options = []): array - { - $logger = NULL; - if ($this->getContainer()) - { - $logger = $this->container->getLogger('kitsu-request'); - } - - $response = $this->getResponse($type, $url, $options); - $statusCode = $response->getStatus(); - - // Check for requests that are unauthorized - if ($statusCode === 401 || $statusCode === 403) - { - Event::emit(EventType::UNAUTHORIZED); - } - - // Any other type of failed request - if ($statusCode > 299 || $statusCode < 200) - { - if ($logger) - { - $logger->warning('Non 2xx response for api call', (array)$response); - } - - throw new FailedResponseException('Failed to get the proper response from the API'); - } - - try - { - return Json::decode(wait($response->getBody()->buffer())); - } - catch (JsonException $e) - { - print_r($e); - die(); - } - } - - /** - * Remove some boilerplate for get requests - * - * @param mixed ...$args - * @throws Throwable - * @return array - */ - protected function getRequest(...$args): array - { - return $this->request('GET', ...$args); - } - - /** - * Remove some boilerplate for patch requests - * - * @param mixed ...$args - * @throws Throwable - * @return array - */ - protected function patchRequest(...$args): array - { - return $this->request('PATCH', ...$args); - } - - /** - * Remove some boilerplate for post requests - * - * @param mixed ...$args - * @throws Throwable - * @return array - */ - protected function postRequest(...$args): array - { - $logger = NULL; - if ($this->getContainer()) - { - $logger = $this->container->getLogger('kitsu-request'); - } - - $response = $this->getResponse('POST', ...$args); - $validResponseCodes = [200, 201]; - - if ( ! in_array($response->getStatus(), $validResponseCodes, TRUE) && $logger) - { - $logger->warning('Non 201 response for POST api call', $response->getBody()); - } - - return JSON::decode(wait($response->getBody()->buffer()), TRUE); - } - - /** - * Remove some boilerplate for delete requests - * - * @param mixed ...$args - * @throws Throwable - * @return bool - */ - protected function deleteRequest(...$args): bool - { - $response = $this->getResponse('DELETE', ...$args); - return ($response->getStatus() === 204); - } } \ No newline at end of file diff --git a/src/AnimeClient/API/Kitsu/ListItem.php b/src/AnimeClient/API/Kitsu/ListItem.php index 4b11080e..76fc7fef 100644 --- a/src/AnimeClient/API/Kitsu/ListItem.php +++ b/src/AnimeClient/API/Kitsu/ListItem.php @@ -18,7 +18,6 @@ namespace Aviat\AnimeClient\API\Kitsu; use Aviat\Ion\Di\Exception\ContainerException; use Aviat\Ion\Di\Exception\NotFoundException; -use const Aviat\AnimeClient\SESSION_SEGMENT; use function Amp\Promise\wait; use function Aviat\AnimeClient\getResponse; @@ -78,7 +77,7 @@ final class ListItem extends AbstractListItem { $request = $this->requestBuilder->newRequest('POST', 'library-entries'); - if ($authHeader !== FALSE) + if ($authHeader !== NULL) { $request = $request->setHeader('Authorization', $authHeader); } @@ -97,7 +96,7 @@ final class ListItem extends AbstractListItem { $authHeader = $this->getAuthHeader(); $request = $this->requestBuilder->newRequest('DELETE', "library-entries/{$id}"); - if ($authHeader !== FALSE) + if ($authHeader !== NULL) { $request = $request->setHeader('Authorization', $authHeader); } @@ -119,7 +118,7 @@ final class ListItem extends AbstractListItem { 'include' => 'media,media.categories,media.mappings' ]); - if ($authHeader !== FALSE) + if ($authHeader !== NULL) { $request = $request->setHeader('Authorization', $authHeader); } @@ -159,7 +158,7 @@ final class ListItem extends AbstractListItem { $request = $this->requestBuilder->newRequest('PATCH', "library-entries/{$id}") ->setJsonBody($requestData); - if ($authHeader !== FALSE) + if ($authHeader !== NULL) { $request = $request->setHeader('Authorization', $authHeader); } @@ -172,24 +171,15 @@ final class ListItem extends AbstractListItem { * @throws ContainerException * @throws NotFoundException */ - private function getAuthHeader() + private function getAuthHeader(): ?string { - $cache = $this->getContainer()->get('cache'); - $cacheItem = $cache->getItem('kitsu-auth-token'); - $sessionSegment = $this->getContainer() - ->get('session') - ->getSegment(SESSION_SEGMENT); + $auth = $this->getContainer()->get('auth'); + $token = $auth->getAuthToken(); - if ($sessionSegment->get('auth_token') !== NULL) { - $token = $sessionSegment->get('auth_token'); + if ( ! empty($token)) { return "bearer {$token}"; } - if ($cacheItem->isHit()) { - $token = $cacheItem->get(); - return "bearer {$token}"; - } - - return FALSE; + return NULL; } } \ No newline at end of file diff --git a/src/AnimeClient/API/Kitsu/Model.php b/src/AnimeClient/API/Kitsu/Model.php index 5c5a90e4..929e3ffe 100644 --- a/src/AnimeClient/API/Kitsu/Model.php +++ b/src/AnimeClient/API/Kitsu/Model.php @@ -116,7 +116,7 @@ final class Model { public function authenticate(string $username, string $password) { // K::AUTH_URL - $response = $this->getResponse('POST', K::AUTH_URL, [ + $response = $this->requestBuilder->getResponse('POST', K::AUTH_URL, [ 'headers' => [ 'accept' => NULL, 'Content-type' => 'application/x-www-form-urlencoded', @@ -182,6 +182,7 @@ final class Model { * @param string $username * @return string * @throws InvalidArgumentException + * @throws Throwable */ public function getUserIdByUsername(string $username = NULL): string { @@ -211,14 +212,14 @@ final class Model { */ public function getCharacter(string $slug): array { - return $this->getRequest('characters', [ + return $this->requestBuilder->getRequest('characters', [ 'query' => [ 'filter' => [ 'slug' => $slug, ], 'fields' => [ - 'anime' => 'canonicalTitle,titles,slug,posterImage', - 'manga' => 'canonicalTitle,titles,slug,posterImage' + 'anime' => 'canonicalTitle,abbreviatedTitles,titles,slug,posterImage', + 'manga' => 'canonicalTitle,abbreviatedTitles,titles,slug,posterImage' ], 'include' => 'castings.person,castings.media' ] @@ -234,31 +235,22 @@ final class Model { */ public function getPerson(string $id): array { - $cacheItem = $this->cache->getItem("kitsu-person-{$id}"); - - if ( ! $cacheItem->isHit()) - { - $data = $this->getRequest("people/{$id}", [ - 'query' => [ - 'filter' => [ - 'id' => $id, - ], - 'fields' => [ - 'characters' => 'canonicalName,slug,image', - 'characterVoices' => 'mediaCharacter', - 'anime' => 'canonicalTitle,abbreviatedTitles,titles,slug,posterImage', - 'manga' => 'canonicalTitle,abbreviatedTitles,titles,slug,posterImage', - 'mediaCharacters' => 'role,media,character', - 'mediaStaff' => 'role,media,person', - ], - 'include' => 'voices.mediaCharacter.media,voices.mediaCharacter.character,staff.media', + return $this->getCached("kitsu-person-{$id}", fn () => $this->requestBuilder->getRequest("people/{$id}", [ + 'query' => [ + 'filter' => [ + 'id' => $id, ], - ]); - $cacheItem->set($data); - $cacheItem->save(); - } - - return $cacheItem->get(); + 'fields' => [ + 'characters' => 'canonicalName,slug,image', + 'characterVoices' => 'mediaCharacter', + 'anime' => 'canonicalTitle,abbreviatedTitles,titles,slug,posterImage', + 'manga' => 'canonicalTitle,abbreviatedTitles,titles,slug,posterImage', + 'mediaCharacters' => 'role,media,character', + 'mediaStaff' => 'role,media,person', + ], + 'include' => 'voices.mediaCharacter.media,voices.mediaCharacter.character,staff.media', + ], + ])); } /** @@ -269,7 +261,7 @@ final class Model { */ public function getUserData(string $username): array { - return $this->getRequest('users', [ + return $this->requestBuilder->getRequest('users', [ 'query' => [ 'filter' => [ 'name' => $username, @@ -306,7 +298,7 @@ final class Model { ] ]; - $raw = $this->getRequest($type, $options); + $raw = $this->requestBuilder->getRequest($type, $options); $raw['included'] = JsonAPI::organizeIncluded($raw['included']); foreach ($raw['data'] as &$item) @@ -351,7 +343,7 @@ final class Model { ] ]; - $raw = $this->getRequest('mappings', $options); + $raw = $this->requestBuilder->getRequest('mappings', $options); if ( ! array_key_exists('included', $raw)) { @@ -555,7 +547,7 @@ final class Model { 'include' => 'mappings' ] ]; - $data = $this->getRequest("anime/{$kitsuAnimeId}", $options); + $data = $this->requestBuilder->getRequest("anime/{$kitsuAnimeId}", $options); if ( ! array_key_exists('included', $data)) { @@ -601,7 +593,7 @@ final class Model { ]; $options = array_merge($defaultOptions, $options); - return $this->setUpRequest('GET', 'library-entries', ['query' => $options]); + return $this->requestBuilder->setUpRequest('GET', 'library-entries', ['query' => $options]); } /** @@ -841,7 +833,7 @@ final class Model { ]; $options = array_merge($defaultOptions, $options); - return $this->setUpRequest('GET', 'library-entries', ['query' => $options]); + return $this->requestBuilder->setUpRequest('GET', 'library-entries', ['query' => $options]); } /** @@ -858,7 +850,7 @@ final class Model { 'include' => 'mappings' ] ]; - $data = $this->getRequest("manga/{$kitsuMangaId}", $options); + $data = $this->requestBuilder->getRequest("manga/{$kitsuMangaId}", $options); $mappings = array_column($data['included'], 'attributes'); foreach($mappings as $map) @@ -1016,7 +1008,7 @@ final class Model { */ protected function getRawHistoryPage(string $type, int $offset, int $limit = 20): Request { - return $this->setUpRequest('GET', 'library-events', [ + return $this->requestBuilder->setUpRequest('GET', 'library-events', [ 'query' => [ 'filter' => [ 'kind' => 'progressed,updated', @@ -1078,7 +1070,7 @@ final class Model { ] ]; - $data = $this->getRequest("{$type}/{$id}", $options); + $data = $this->requestBuilder->getRequest("{$type}/{$id}", $options); if (empty($data['data'])) { @@ -1118,7 +1110,7 @@ final class Model { ] ]; - $data = $this->getRequest($type, $options); + $data = $this->requestBuilder->getRequest($type, $options); if (empty($data['data'])) { @@ -1151,7 +1143,7 @@ final class Model { $options['query']['filter']['status'] = $status; } - $response = $this->getRequest('library-entries', $options); + $response = $this->requestBuilder->getRequest('library-entries', $options); return $response['meta']['count']; } @@ -1217,6 +1209,6 @@ final class Model { ]; $options = array_merge($defaultOptions, $options); - return $this->setUpRequest('GET', 'library-entries', ['query' => $options]); + return $this->requestBuilder->setUpRequest('GET', 'library-entries', ['query' => $options]); } } \ No newline at end of file From cacf19781d1d9e50a04a41050de298015a2d28b0 Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Fri, 8 May 2020 21:34:36 -0400 Subject: [PATCH 15/21] Only the command line should be able to get credentials from the cache --- src/AnimeClient/API/Kitsu/Auth.php | 18 ++++++++++++++---- src/AnimeClient/API/Kitsu/Model.php | 13 ++++++++++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/AnimeClient/API/Kitsu/Auth.php b/src/AnimeClient/API/Kitsu/Auth.php index cfb5c38b..1ec5c26f 100644 --- a/src/AnimeClient/API/Kitsu/Auth.php +++ b/src/AnimeClient/API/Kitsu/Auth.php @@ -139,8 +139,13 @@ final class Auth { */ public function getAuthToken(): ?string { - return $this->segment->get('auth_token', NULL) - ?? $this->cache->get(K::AUTH_TOKEN_CACHE_KEY, NULL); + if (PHP_SAPI === 'cli') + { + return $this->segment->get('auth_token', NULL) + ?? $this->cache->get(K::AUTH_TOKEN_CACHE_KEY, NULL); + } + + return $this->segment->get('auth_token', NULL); } /** @@ -151,8 +156,13 @@ final class Auth { */ private function getRefreshToken(): ?string { - return $this->segment->get('refresh_token') - ?? $this->cache->get(K::AUTH_TOKEN_REFRESH_CACHE_KEY, NULL); + if (PHP_SAPI === 'cli') + { + return $this->segment->get('refresh_token') + ?? $this->cache->get(K::AUTH_TOKEN_REFRESH_CACHE_KEY, NULL); + } + + return $this->segment->get('refresh_token'); } /** diff --git a/src/AnimeClient/API/Kitsu/Model.php b/src/AnimeClient/API/Kitsu/Model.php index 929e3ffe..671a04d6 100644 --- a/src/AnimeClient/API/Kitsu/Model.php +++ b/src/AnimeClient/API/Kitsu/Model.php @@ -155,19 +155,26 @@ final class Model { */ public function reAuthenticate(string $token) { - $response = $this->getResponse('POST', K::AUTH_URL, [ + $response = $this->requestBuilder->getResponse('POST', K::AUTH_URL, [ 'headers' => [ + 'accept' => NULL, + 'Content-type' => 'application/x-www-form-urlencoded', 'Accept-encoding' => '*' - ], 'form_params' => [ 'grant_type' => 'refresh_token', 'refresh_token' => $token ] ]); - $data = Json::decode(wait($response->getBody()->buffer())); + if (array_key_exists('error', $data)) + { + dump($data['error']); + dump($response); + die(); + } + if (array_key_exists('access_token', $data)) { return $data; From 332ff723600ee33efa62b8e491109d0144c2747e Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Mon, 11 May 2020 09:17:11 -0400 Subject: [PATCH 16/21] Bug fixes --- src/AnimeClient/API/Kitsu.php | 11 +++++++---- .../API/Kitsu/Transformer/MangaTransformer.php | 4 ++-- src/AnimeClient/API/ParallelAPIRequest.php | 4 +--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/AnimeClient/API/Kitsu.php b/src/AnimeClient/API/Kitsu.php index 409c905a..441525cd 100644 --- a/src/AnimeClient/API/Kitsu.php +++ b/src/AnimeClient/API/Kitsu.php @@ -225,13 +225,16 @@ final class Kitsu { // The 'canonical' title is always returned $valid = [$data['canonicalTitle']]; - if (array_key_exists('titles', $data)) + foreach (['titles', 'abbreviatedTitles'] as $key) { - foreach($data['titles'] as $alternateTitle) + if (array_key_exists($key, $data) && is_array($data[$key])) { - if (self::titleIsUnique($alternateTitle, $valid)) + foreach($data[$key] as $alternateTitle) { - $valid[] = $alternateTitle; + if (self::titleIsUnique($alternateTitle, $valid)) + { + $valid[] = $alternateTitle; + } } } } diff --git a/src/AnimeClient/API/Kitsu/Transformer/MangaTransformer.php b/src/AnimeClient/API/Kitsu/Transformer/MangaTransformer.php index b697d4d0..2d428537 100644 --- a/src/AnimeClient/API/Kitsu/Transformer/MangaTransformer.php +++ b/src/AnimeClient/API/Kitsu/Transformer/MangaTransformer.php @@ -98,12 +98,12 @@ final class MangaTransformer extends AbstractTransformer { if ( ! empty($characters['main'])) { - uasort($characters['main'], fn ($a, $b) => $a['name'] <=> $b['anime']); + uasort($characters['main'], fn ($a, $b) => $a['name'] <=> $b['name']); } if ( ! empty($characters['supporting'])) { - uasort($characters['supporting'], fn ($a, $b) => $a['name'] <=> $b['anime']); + uasort($characters['supporting'], fn ($a, $b) => $a['name'] <=> $b['name']); } ksort($characters); diff --git a/src/AnimeClient/API/ParallelAPIRequest.php b/src/AnimeClient/API/ParallelAPIRequest.php index 83389c92..d781471f 100644 --- a/src/AnimeClient/API/ParallelAPIRequest.php +++ b/src/AnimeClient/API/ParallelAPIRequest.php @@ -103,9 +103,7 @@ final class ParallelAPIRequest { foreach ($this->requests as $key => $url) { - $promises[$key] = call(static function () use ($client, $url) { - return yield $client->request($url); - }); + $promises[$key] = call(fn () => yield $client->request($url)); } return wait(all($promises)); From 493f849aeb5f0c00293f5cd6fba28ec5fc786c8e Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Mon, 18 May 2020 12:52:32 -0400 Subject: [PATCH 17/21] Remove extra titles from list/cover display --- src/AnimeClient/API/Kitsu.php | 135 +++++++++++++++++----------------- 1 file changed, 68 insertions(+), 67 deletions(-) diff --git a/src/AnimeClient/API/Kitsu.php b/src/AnimeClient/API/Kitsu.php index 441525cd..51b85e3a 100644 --- a/src/AnimeClient/API/Kitsu.php +++ b/src/AnimeClient/API/Kitsu.php @@ -60,73 +60,6 @@ final class Kitsu { return AnimeAiringStatus::NOT_YET_AIRED; } - /** - * Get the name and logo for the streaming service of the current link - * - * @param string $hostname - * @return array - */ - protected static function getServiceMetaData(string $hostname = NULL): array - { - $hostname = str_replace('www.', '', $hostname); - - $serviceMap = [ - 'amazon.com' => [ - 'name' => 'Amazon Prime', - 'link' => TRUE, - 'image' => 'streaming-logos/amazon.svg', - ], - 'crunchyroll.com' => [ - 'name' => 'Crunchyroll', - 'link' => TRUE, - 'image' => 'streaming-logos/crunchyroll.svg', - ], - 'daisuki.net' => [ - 'name' => 'Daisuki', - 'link' => TRUE, - 'image' => 'streaming-logos/daisuki.svg' - ], - 'funimation.com' => [ - 'name' => 'Funimation', - 'link' => TRUE, - 'image' => 'streaming-logos/funimation.svg', - ], - 'hidive.com' => [ - 'name' => 'Hidive', - 'link' => TRUE, - 'image' => 'streaming-logos/hidive.svg', - ], - 'hulu.com' => [ - 'name' => 'Hulu', - 'link' => TRUE, - 'image' => 'streaming-logos/hulu.svg', - ], - 'tubitv.com' => [ - 'name' => 'TubiTV', - 'link' => TRUE, - 'image' => 'streaming-logos/tubitv.svg', - ], - 'viewster.com' => [ - 'name' => 'Viewster', - 'link' => TRUE, - 'image' => 'streaming-logos/viewster.svg' - ], - ]; - - if (array_key_exists($hostname, $serviceMap)) - { - return $serviceMap[$hostname]; - } - - // Default to Netflix, because the API links are broken, - // and there's no other real identifier for Netflix - return [ - 'name' => 'Netflix', - 'link' => FALSE, - 'image' => 'streaming-logos/netflix.svg', - ]; - } - /** * Reorganize streaming links * @@ -242,6 +175,74 @@ final class Kitsu { return $valid; } + + /** + * Get the name and logo for the streaming service of the current link + * + * @param string $hostname + * @return array + */ + protected static function getServiceMetaData(string $hostname = NULL): array + { + $hostname = str_replace('www.', '', $hostname); + + $serviceMap = [ + 'amazon.com' => [ + 'name' => 'Amazon Prime', + 'link' => TRUE, + 'image' => 'streaming-logos/amazon.svg', + ], + 'crunchyroll.com' => [ + 'name' => 'Crunchyroll', + 'link' => TRUE, + 'image' => 'streaming-logos/crunchyroll.svg', + ], + 'daisuki.net' => [ + 'name' => 'Daisuki', + 'link' => TRUE, + 'image' => 'streaming-logos/daisuki.svg' + ], + 'funimation.com' => [ + 'name' => 'Funimation', + 'link' => TRUE, + 'image' => 'streaming-logos/funimation.svg', + ], + 'hidive.com' => [ + 'name' => 'Hidive', + 'link' => TRUE, + 'image' => 'streaming-logos/hidive.svg', + ], + 'hulu.com' => [ + 'name' => 'Hulu', + 'link' => TRUE, + 'image' => 'streaming-logos/hulu.svg', + ], + 'tubitv.com' => [ + 'name' => 'TubiTV', + 'link' => TRUE, + 'image' => 'streaming-logos/tubitv.svg', + ], + 'viewster.com' => [ + 'name' => 'Viewster', + 'link' => TRUE, + 'image' => 'streaming-logos/viewster.svg' + ], + ]; + + if (array_key_exists($hostname, $serviceMap)) + { + return $serviceMap[$hostname]; + } + + // Default to Netflix, because the API links are broken, + // and there's no other real identifier for Netflix + return [ + 'name' => 'Netflix', + 'link' => FALSE, + 'image' => 'streaming-logos/netflix.svg', + ]; + } + /** * Determine if an alternate title is unique enough to list * From ebe8626ba78af579f28fc722c77dda42c3d13a54 Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Mon, 18 May 2020 12:53:00 -0400 Subject: [PATCH 18/21] Add aria attributes to selected menu items --- app/views/main-menu.php | 21 +++++++++++++++------ src/AnimeClient/MenuGenerator.php | 5 ++++- src/AnimeClient/Util.php | 25 ++++++++++++++++++++++++- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/app/views/main-menu.php b/app/views/main-menu.php index 1f38d6ab..39d68032 100644 --- a/app/views/main-menu.php +++ b/app/views/main-menu.php @@ -14,7 +14,8 @@ $hasManga = stripos($_SERVER['REQUEST_URI'], 'manga') !== FALSE; a( $urlGenerator->defaultUrl($url_type), - $whose . ucfirst($url_type) . ' List' + $whose . ucfirst($url_type) . ' List', + ['aria-current'=> 'page'] ) ?> get("show_{$url_type}_collection")): ?> [a( @@ -35,7 +36,8 @@ $hasManga = stripos($_SERVER['REQUEST_URI'], 'manga') !== FALSE; a( $url->generate("{$url_type}.collection.view") . $extraSegment, - $whose . ucfirst($url_type) . ' Collection' + $whose . ucfirst($url_type) . ' Collection', + ['aria-current'=> 'page'] ) ?> get("show_{$other_type}_collection")): ?> [a( @@ -79,15 +81,22 @@ $hasManga = stripos($_SERVER['REQUEST_URI'], 'manga') !== FALSE; +get('util')->isViewPage() && ($hasAnime || $hasManga)): ?> + diff --git a/src/AnimeClient/MenuGenerator.php b/src/AnimeClient/MenuGenerator.php index a42a9787..d08aa64e 100644 --- a/src/AnimeClient/MenuGenerator.php +++ b/src/AnimeClient/MenuGenerator.php @@ -105,7 +105,10 @@ final class MenuGenerator extends UrlGenerator { $has = StringType::from($this->path())->contains($path); $selected = ($has && mb_strlen($this->path()) >= mb_strlen($path)); - $link = $this->helper->a($this->url($path), $title); + $linkAttrs = ($selected) + ? ['aria-current' => 'location'] + : []; + $link = $this->helper->a($this->url($path), $title, $linkAttrs); $attrs = $selected ? ['class' => 'selected'] diff --git a/src/AnimeClient/Util.php b/src/AnimeClient/Util.php index 7e4d6f37..d427c3d3 100644 --- a/src/AnimeClient/Util.php +++ b/src/AnimeClient/Util.php @@ -54,6 +54,29 @@ class Util { $this->setContainer($container); } + /** + * Absolutely equal? + * + * @param $left + * @param $right + * @return bool + */ + public static function eq($left, $right): bool + { + return $left === $right; + } + + /** + * Set aria-current attribute based on a condition check + * + * @param bool $condition + * @return string + */ + public static function ariaCurrent(bool $condition): string + { + return $condition ? 'true' : 'false'; + } + /** * HTML selection helper function * @@ -63,7 +86,7 @@ class Util { */ public static function isSelected(string $left, string $right): string { - return ($left === $right) ? 'selected' : ''; + return static::eq($left, $right) ? 'selected' : ''; } /** From 035e142c98a6ad8f97543da3050960a40f6e1d72 Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Mon, 18 May 2020 13:32:02 -0400 Subject: [PATCH 19/21] Make sure re-authenticate gets arguments --- src/AnimeClient/API/Kitsu/Auth.php | 2 +- tests/AnimeClient/Helper/MenuHelperTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AnimeClient/API/Kitsu/Auth.php b/src/AnimeClient/API/Kitsu/Auth.php index 1ec5c26f..ac17d05a 100644 --- a/src/AnimeClient/API/Kitsu/Auth.php +++ b/src/AnimeClient/API/Kitsu/Auth.php @@ -68,7 +68,7 @@ final class Auth { ->getSegment(SESSION_SEGMENT); $this->model = $container->get('kitsu-model'); - Event::on('::unauthorized::', [$this, 'reAuthenticate']); + Event::on('::unauthorized::', [$this, 'reAuthenticate'], []); } /** diff --git a/tests/AnimeClient/Helper/MenuHelperTest.php b/tests/AnimeClient/Helper/MenuHelperTest.php index 1e863b1d..537074ff 100644 --- a/tests/AnimeClient/Helper/MenuHelperTest.php +++ b/tests/AnimeClient/Helper/MenuHelperTest.php @@ -55,7 +55,7 @@ class MenuHelperTest extends AnimeClientTestCase { $expected['no selection'] = $this->helper->ul()->__toString(); // selected - $link = $this->helper->a($this->urlGenerator->url('/foobar'), 'Index'); + $link = $this->helper->a($this->urlGenerator->url('/foobar'), 'Index', ['aria-current' => 'location']); $this->helper->ul()->rawItem($link, ['class' => 'selected']); $expected['selected'] = $this->helper->ul()->__toString(); From 5e157e4a4e642841aa4945ed0bbce9253128313f Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Mon, 18 May 2020 13:47:41 -0400 Subject: [PATCH 20/21] Make sure reAuthenticate method has optional parameter --- src/AnimeClient/API/Kitsu/Auth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AnimeClient/API/Kitsu/Auth.php b/src/AnimeClient/API/Kitsu/Auth.php index ac17d05a..676fc141 100644 --- a/src/AnimeClient/API/Kitsu/Auth.php +++ b/src/AnimeClient/API/Kitsu/Auth.php @@ -96,7 +96,7 @@ final class Auth { * @return boolean * @throws Throwable|InvalidArgumentException */ - public function reAuthenticate(?string $refreshToken): bool + public function reAuthenticate(?string $refreshToken = NULL): bool { $refreshToken ??= $this->getRefreshToken(); From 9e8b3f466366e4ccec11eb6bc8c821ce892e7ab9 Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Mon, 18 May 2020 13:52:27 -0400 Subject: [PATCH 21/21] Remove extra titles from cover/list views --- src/AnimeClient/API/Kitsu.php | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/AnimeClient/API/Kitsu.php b/src/AnimeClient/API/Kitsu.php index 51b85e3a..3716d62e 100644 --- a/src/AnimeClient/API/Kitsu.php +++ b/src/AnimeClient/API/Kitsu.php @@ -158,16 +158,13 @@ final class Kitsu { // The 'canonical' title is always returned $valid = [$data['canonicalTitle']]; - foreach (['titles', 'abbreviatedTitles'] as $key) + if (array_key_exists('titles', $data) && is_array($data['titles'])) { - if (array_key_exists($key, $data) && is_array($data[$key])) + foreach($data['titles'] as $alternateTitle) { - foreach($data[$key] as $alternateTitle) + if (self::titleIsUnique($alternateTitle, $valid)) { - if (self::titleIsUnique($alternateTitle, $valid)) - { - $valid[] = $alternateTitle; - } + $valid[] = $alternateTitle; } } }