From 0e684736bda8ec64d4b3c6ce848638eae3b4b0f2 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Thu, 26 Oct 2023 16:00:13 -0400 Subject: [PATCH] Vastly simplify logic for getting a user's anime library. Most basic API functionality seems to be working --- app/appConf/routes.php | 27 +- composer.json | 2 +- src/AnimeClient/API/Anilist/Model.php | 3 +- .../API/Anilist/RequestBuilder.php | 6 +- .../API/Kitsu/Enum/MediaStatus.php | 30 ++ src/AnimeClient/API/Kitsu/Model.php | 317 ++++++------------ src/AnimeClient/API/Kitsu/RequestBuilder.php | 15 +- src/AnimeClient/API/ParallelAPIRequest.php | 69 +--- src/AnimeClient/Controller/Images.php | 4 +- src/AnimeClient/constants.php | 8 +- 10 files changed, 178 insertions(+), 303 deletions(-) create mode 100644 src/AnimeClient/API/Kitsu/Enum/MediaStatus.php diff --git a/app/appConf/routes.php b/app/appConf/routes.php index 2e3bc8e1..6bbf61ba 100644 --- a/app/appConf/routes.php +++ b/app/appConf/routes.php @@ -18,9 +18,9 @@ use const Aviat\AnimeClient\{ ALPHA_SLUG_PATTERN, DEFAULT_CONTROLLER, DEFAULT_CONTROLLER_METHOD, + KITSU_SLUG_PATTERN, NUM_PATTERN, SLUG_PATTERN, - SLUG_SPACE_PATTERN, }; // ------------------------------------------------------------------------- @@ -28,7 +28,7 @@ use const Aviat\AnimeClient\{ // // Maps paths to controllers and methods // ------------------------------------------------------------------------- -$routes = [ +$base_routes = [ // --------------------------------------------------------------------- // AJAX Routes // --------------------------------------------------------------------- @@ -60,7 +60,7 @@ $routes = [ 'path' => '/anime/details/{id}', 'action' => 'details', 'tokens' => [ - 'id' => SLUG_PATTERN, + 'id' => KITSU_SLUG_PATTERN, ], ], 'anime.delete' => [ @@ -97,7 +97,7 @@ $routes = [ 'path' => '/manga/details/{id}', 'action' => 'details', 'tokens' => [ - 'id' => SLUG_PATTERN, + 'id' => KITSU_SLUG_PATTERN, ], ], // --------------------------------------------------------------------- @@ -191,13 +191,13 @@ $routes = [ 'character' => [ 'path' => '/character/{slug}', 'tokens' => [ - 'slug' => SLUG_PATTERN, + 'slug' => KITSU_SLUG_PATTERN, ], ], 'person' => [ 'path' => '/people/{slug}', 'tokens' => [ - 'slug' => SLUG_PATTERN, + 'slug' => KITSU_SLUG_PATTERN, ], ], 'default_user_info' => [ @@ -291,8 +291,8 @@ $routes = [ 'path' => '/{controller}/edit/{id}/{status}', 'action' => 'edit', 'tokens' => [ - 'id' => SLUG_PATTERN, - 'status' => SLUG_SPACE_PATTERN, + 'id' => KITSU_SLUG_PATTERN, + 'status' => SLUG_PATTERN, ], ], 'list' => [ @@ -315,15 +315,10 @@ $defaultMap = [ 'verb' => 'get', ]; -foreach ($routes as &$route) +$routes = []; +foreach ($base_routes as $name => $route) { - foreach ($defaultMap as $key => $val) - { - if ( ! array_key_exists($key, $route)) - { - $route[$key] = $val; - } - } + $routes[$name] = array_merge($defaultMap, $route); } return $routes; diff --git a/composer.json b/composer.json index f893bdc2..525d7126 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "require": { "amphp/http-client": "^v5.0.0", "aura/html": "^2.5.0", - "aura/router": "3.2.0", + "aura/router": "^3.3.0", "aura/session": "^2.1.0", "aviat/banker": "^4.1.2", "aviat/query": "^4.1.0", diff --git a/src/AnimeClient/API/Anilist/Model.php b/src/AnimeClient/API/Anilist/Model.php index 292f4ad8..a0569e11 100644 --- a/src/AnimeClient/API/Anilist/Model.php +++ b/src/AnimeClient/API/Anilist/Model.php @@ -24,7 +24,6 @@ use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException}; use Aviat\Ion\Json; use InvalidArgumentException; use Throwable; -use function Amp\Promise\wait; /** * Anilist API Model @@ -67,7 +66,7 @@ final class Model $response = $this->requestBuilder->getResponseFromRequest($request); - return Json::decode(wait($response->getBody()->buffer())); + return Json::decode($response->getBody()->buffer()); } /** diff --git a/src/AnimeClient/API/Anilist/RequestBuilder.php b/src/AnimeClient/API/Anilist/RequestBuilder.php index b7ef70c9..8e5ce6c0 100644 --- a/src/AnimeClient/API/Anilist/RequestBuilder.php +++ b/src/AnimeClient/API/Anilist/RequestBuilder.php @@ -22,8 +22,6 @@ use Aviat\Ion\{Json, JsonException}; use LogicException; use Throwable; -use function Amp\Promise\wait; - use function Aviat\AnimeClient\getResponse; use function in_array; use const Aviat\AnimeClient\USER_AGENT; @@ -170,7 +168,7 @@ final class RequestBuilder extends APIRequestBuilder $request = $this->mutateRequest($name, $variables); $response = $this->getResponseFromRequest($request); - return Json::decode(wait($response->getBody()->buffer())); + return Json::decode($response->getBody()->buffer()); } /** @@ -246,7 +244,7 @@ final class RequestBuilder extends APIRequestBuilder $logger?->warning('Non 200 response for POST api call', (array) $response->getBody()); } - $rawBody = wait($response->getBody()->buffer()); + $rawBody = $response->getBody()->buffer(); try { diff --git a/src/AnimeClient/API/Kitsu/Enum/MediaStatus.php b/src/AnimeClient/API/Kitsu/Enum/MediaStatus.php new file mode 100644 index 00000000..a3b3c76a --- /dev/null +++ b/src/AnimeClient/API/Kitsu/Enum/MediaStatus.php @@ -0,0 +1,30 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 5.2 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\API\Kitsu\Enum; + +use Aviat\Ion\Enum as BaseEnum; + +/** + * Status of when anime is being/was/will be aired + */ +final class MediaStatus extends BaseEnum +{ + public const CURRENT = 'CURRENT'; + public const PLANNED = 'PLANNED'; + public const ON_HOLD = 'ON_HOLD'; + public const DROPPED = 'DROPPED'; + public const COMPLETED = 'COMPLETED'; +} +// End of MediaStatus diff --git a/src/AnimeClient/API/Kitsu/Model.php b/src/AnimeClient/API/Kitsu/Model.php index 719a0cb9..3d97010a 100644 --- a/src/AnimeClient/API/Kitsu/Model.php +++ b/src/AnimeClient/API/Kitsu/Model.php @@ -14,8 +14,6 @@ namespace Aviat\AnimeClient\API\Kitsu; -use Amp; -use Amp\Future; use Aviat\AnimeClient\API\Kitsu\Transformer\{ AnimeHistoryTransformer, AnimeListTransformer, @@ -29,6 +27,7 @@ use Aviat\AnimeClient\API\{ CacheTrait, Enum\AnimeWatchingStatus\Kitsu as KitsuWatchingStatus, Enum\MangaReadingStatus\Kitsu as KitsuReadingStatus, + Kitsu\Enum\MediaStatus, Mapping\AnimeWatchingStatus, Mapping\MangaReadingStatus }; @@ -553,27 +552,7 @@ final class Model */ public function getThumbList(string $type): array { - $statuses = [ - 'CURRENT', - 'PLANNED', - 'ON_HOLD', - 'DROPPED', - 'COMPLETED', - ]; - - $pages = []; - - // Although I can fetch the whole list without segregating by status, - // this way is much faster... - foreach ($statuses as $status) - { - foreach ($this->getPages($this->getThumbListPages(...), strtoupper($type), $status) as $page) - { - $pages[] = $page; - } - } - - return array_merge(...$pages); + return $this->getZippedListPerStatus('GetLibraryThumbs', $type); } /** @@ -583,27 +562,7 @@ final class Model */ public function getSyncList(string $type): array { - $statuses = [ - 'CURRENT', - 'PLANNED', - 'ON_HOLD', - 'DROPPED', - 'COMPLETED', - ]; - - $pages = []; - - // Although I can fetch the whole list without segregating by status, - // this way is much faster... - foreach ($statuses as $status) - { - foreach ($this->getPages($this->getSyncPages(...), strtoupper($type), $status) as $page) - { - $pages[] = $page; - } - } - - return array_merge(...$pages); + return $this->getZippedListPerStatus('GetSyncLibrary', $type); } /** @@ -619,15 +578,39 @@ final class Model } /** - * Get the raw anime/manga list from GraphQL + * Get all the raw data for the current list, chunking by status * - * @return mixed[] + * @param string $queryName - The GraphQL query + * @param string $type - Media type (anime, manga) + * @return array */ - protected function getList(string $type, string $status = ''): array + protected function getZippedListPerStatus(string $queryName, string $type): array + { + $statusPages = []; + + // Although I can fetch the whole list without segregating by status, + // this way is much faster... + foreach (MediaStatus::getConstList() as $status) + { + $statusPages[] = $this->getZippedList($queryName, $type, $status); + } + + return array_merge(...$statusPages); + } + + /** + * Get all the raw data for the current list + * + * @param string $queryName - The GraphQL query + * @param string $type - Media type (anime, manga) + * @param string $status - Media 'consumption' status + * @return array + */ + protected function getZippedList(string $queryName, string $type, string $status): array { $pages = []; - foreach ($this->getPages($this->getListPages(...), strtoupper($type), strtoupper($status)) as $page) + foreach ($this->getListPages($queryName, $type, $status) as $page) { $pages[] = $page; } @@ -635,158 +618,92 @@ final class Model return array_merge(...$pages); } - private function getListPages(string $type, string $status = ''): Amp\Iterator + /** + * Get the raw anime/manga list from GraphQL + * + * @return mixed[] + */ + protected function getList(string $type, string $status = ''): array + { + return $this->getZippedList('GetLibrary', $type, $status); + } + + /** + * A generator returning the relevant snippet for each 'page' of + * a media list request + * + * @param string $queryName - The GraphQL query + * @param string $type - Media type (anime, manga) + * @param string $status - Media 'consumption' status + * @return iterable + */ + private function getListPages(string $queryName, string $type, string $status): iterable { $cursor = ''; $username = $this->getUsername(); - return new Amp\Producer(function (callable $emit) use ($type, $status, $cursor, $username): Generator { - while (TRUE) - { - $vars = [ - 'type' => $type, - 'slug' => $username, - ]; - if ($status !== '') - { - $vars['status'] = $status; - } - - if ($cursor !== '') - { - $vars['after'] = $cursor; - } - - $request = $this->requestBuilder->queryRequest('GetLibrary', $vars); - $response = yield getApiClient()->request($request); - $json = yield $response->getBody()->buffer(); - - $rawData = Json::decode($json); - $data = $rawData['data']['findProfileBySlug']['library']['all'] ?? []; - $page = $data['pageInfo'] ?? []; - if (empty($data)) - { - // Clear session, in case the error is an invalid token. - $segment = $this->container->get('session') - ->getSegment(SESSION_SEGMENT); - $segment->clear(); - - // @TODO Proper Error logging - dump($rawData); - - exit(); - } - - $cursor = $page['endCursor']; - - yield $emit($data['nodes']); - - if ($page['hasNextPage'] !== TRUE) - { - break; - } - } - }); - } - - private function getSyncPages(string $type, string $status): Amp\Iterator - { - $cursor = ''; - $username = $this->getUsername(); - - return new Amp\Producer(function (callable $emit) use ($type, $status, $cursor, $username): Generator { - while (TRUE) - { - $vars = [ - 'type' => $type, - 'slug' => $username, - 'status' => $status, - ]; - if ($cursor !== '') - { - $vars['after'] = $cursor; - } - - $request = $this->requestBuilder->queryRequest('GetSyncLibrary', $vars); - $response = yield getApiClient()->request($request); - $json = yield $response->getBody()->buffer(); - - $rawData = Json::decode($json); - $data = $rawData['data']['findProfileBySlug']['library']['all'] ?? []; - $page = $data['pageInfo']; - if (empty($data)) - { - dump($rawData); - - exit(); - } - - $cursor = $page['endCursor']; - - yield $emit($data['nodes']); - - if ($page['hasNextPage'] === FALSE) - { - break; - } - } - }); - } - - private function getThumbListPages(string $type, string $status): Amp\Iterator - { - $cursor = ''; - $username = $this->getUsername(); - - return new Amp\Producer(function (callable $emit) use ($type, $status, $cursor, $username): Generator { - while (TRUE) - { - $vars = [ - 'type' => $type, - 'slug' => $username, - 'status' => $status, - ]; - if ($cursor !== '') - { - $vars['after'] = $cursor; - } - - $request = $this->requestBuilder->queryRequest('GetLibraryThumbs', $vars); - $response = yield getApiClient()->request($request); - $json = yield $response->getBody()->buffer(); - - $rawData = Json::decode($json); - $data = $rawData['data']['findProfileBySlug']['library']['all'] ?? []; - $page = $data['pageInfo']; - if (empty($data)) - { - dump($rawData); - - exit(); - } - - $cursor = $page['endCursor']; - - yield $emit($data['nodes']); - - if ($page['hasNextPage'] === FALSE) - { - break; - } - } - }); - } - - private function getPages(callable $method, mixed ...$args): Generator - { - $items = $method(...$args); - - while (wait($items->advance())) + while (TRUE) { - yield $items->getCurrent(); + $vars = [ + 'type' => strtoupper($type), + 'slug' => $username, + ]; + if ($status !== '') + { + $vars['status'] = strtoupper($status); + } + if ($cursor !== '') + { + $vars['after'] = $cursor; + } + + $request = $this->requestBuilder->queryRequest($queryName, $vars); + $response = getApiClient()->request($request); + $json = $response->getBody()->buffer(); + + $rawData = Json::decode($json); + $data = $rawData['data']['findProfileBySlug']['library']['all'] ?? []; + $page = $data['pageInfo'] ?? []; + if (empty($data)) + { + // Clear session, in case the error is an invalid token. + $segment = $this->container->get('session') + ->getSegment(SESSION_SEGMENT); + $segment->clear(); + + // @TODO Proper Error logging + dump($rawData); + + exit(); + } + + $cursor = $page['endCursor']; + + yield $data['nodes']; + + if ($page['hasNextPage'] === FALSE || $page === []) + { + break; + } } } + private function getListCount(string $type, string $status = ''): int + { + $args = [ + 'type' => strtoupper($type), + 'slug' => $this->getUsername(), + ]; + if ($status !== '') + { + $args['status'] = strtoupper($status); + } + + $res = $this->requestBuilder->runQuery('GetLibraryCount', $args); + + return $res['data']['findProfileBySlug']['library']['all']['totalCount']; + } + protected function getUserId(): string { static $userId = NULL; @@ -808,20 +725,4 @@ final class Model ->get('config') ->get(['kitsu_username']); } - - private function getListCount(string $type, string $status = ''): int - { - $args = [ - 'type' => strtoupper($type), - 'slug' => $this->getUsername(), - ]; - if ($status !== '') - { - $args['status'] = strtoupper($status); - } - - $res = $this->requestBuilder->runQuery('GetLibraryCount', $args); - - return $res['data']['findProfileBySlug']['library']['all']['totalCount']; - } } diff --git a/src/AnimeClient/API/Kitsu/RequestBuilder.php b/src/AnimeClient/API/Kitsu/RequestBuilder.php index 96374b3d..2d59aa40 100644 --- a/src/AnimeClient/API/Kitsu/RequestBuilder.php +++ b/src/AnimeClient/API/Kitsu/RequestBuilder.php @@ -21,7 +21,6 @@ use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; use Aviat\Ion\{Event, Json, JsonException}; use LogicException; -use function Amp\Promise\wait; use function Aviat\AnimeClient\getResponse; use function in_array; use const Aviat\AnimeClient\{SESSION_SEGMENT, USER_AGENT}; @@ -125,13 +124,10 @@ final class RequestBuilder extends APIRequestBuilder if ( ! in_array($response->getStatus(), $validResponseCodes, TRUE)) { $logger = $this->container->getLogger('kitsu-graphql'); - if ($logger !== NULL) - { - $logger->warning('Non 200 response for GraphQL call', (array) $response->getBody()); - } + $logger?->warning('Non 200 response for GraphQL call', (array)$response->getBody()); } - return Json::decode(wait($response->getBody()->buffer())); + return Json::decode($response->getBody()->buffer()); } /** @@ -148,13 +144,10 @@ final class RequestBuilder extends APIRequestBuilder if ( ! in_array($response->getStatus(), $validResponseCodes, TRUE)) { $logger = $this->container->getLogger('kitsu-graphql'); - if ($logger !== NULL) - { - $logger->warning('Non 200 response for GraphQL call', (array) $response->getBody()); - } + $logger?->warning('Non 200 response for GraphQL call', (array)$response->getBody()); } - return Json::decode(wait($response->getBody()->buffer())); + return Json::decode($response->getBody()->buffer()); } /** diff --git a/src/AnimeClient/API/ParallelAPIRequest.php b/src/AnimeClient/API/ParallelAPIRequest.php index af49c5a7..d6794593 100644 --- a/src/AnimeClient/API/ParallelAPIRequest.php +++ b/src/AnimeClient/API/ParallelAPIRequest.php @@ -14,13 +14,11 @@ namespace Aviat\AnimeClient\API; -use Amp\Http\Client\{HttpException, Request}; -use Generator; +use Amp\Future; +use Amp\Http\Client\{Request, Response}; use Throwable; -use function Amp\call; -// use function Amp\Future\{async, await}; -use function Amp\Promise\{all, wait}; +use function Amp\async; use function Aviat\AnimeClient\getApiClient; /** @@ -69,7 +67,14 @@ final class ParallelAPIRequest */ public function makeRequests(): array { - return $this->makeRequestOld(); + $futures = []; + + foreach ($this->requests as $key => $url) + { + $futures[$key] = async(static fn () => self::bodyHandler($url)); + } + + return Future\await($futures); } /** @@ -78,54 +83,6 @@ final class ParallelAPIRequest * @throws Throwable */ public function getResponses(): array - { - return $this->getResponsesOld(); - } - - private function makeRequestOld(): array - { - $client = getApiClient(); - - $promises = []; - - foreach ($this->requests as $key => $url) - { - $promises[$key] = call(static function () use ($client, $url): Generator { - $response = yield $client->request($url); - return yield $response->getBody()->buffer(); - }); - } - - return wait(all($promises)); - } - - private function makeRequestsNew(): array - { - $futures = []; - - foreach ($this->requests as $key => $url) - { - $futures[$key] = async(static fn () => self::bodyHandler($url)); - } - - return await($futures); - } - - private function getResponsesOld(): array - { - $client = getApiClient(); - - $promises = []; - - foreach ($this->requests as $key => $url) - { - $promises[$key] = call(static fn () => yield $client->request($url)); - } - - return wait(all($promises)); - } - - private function getResponsesNew(): array { $futures = []; @@ -134,7 +91,7 @@ final class ParallelAPIRequest $futures[$key] = async(static fn () => self::responseHandler($url)); } - return await($futures); + return Future\await($futures); } private static function bodyHandler(string|Request $uri): string @@ -150,7 +107,7 @@ final class ParallelAPIRequest return $response->getBody()->buffer(); } - private static function responseHandler(string|Request $uri) + private static function responseHandler(string|Request $uri): Response { $client = getApiClient(); diff --git a/src/AnimeClient/Controller/Images.php b/src/AnimeClient/Controller/Images.php index e1edf61d..3908824e 100644 --- a/src/AnimeClient/Controller/Images.php +++ b/src/AnimeClient/Controller/Images.php @@ -17,9 +17,7 @@ namespace Aviat\AnimeClient\Controller; use Aviat\AnimeClient\Controller as BaseController; use Aviat\Ion\Attribute\{Controller, Route}; use Throwable; -use function Amp\Promise\wait; use function Aviat\AnimeClient\{createPlaceholderImage, getResponse}; -use function imagepalletetotruecolor; use function in_array; @@ -130,7 +128,7 @@ final class Images extends BaseController return; } - $data = wait($response->getBody()->buffer()); + $data = $response->getBody()->buffer(); $size = getimagesizefromstring($data); if ($size === FALSE) diff --git a/src/AnimeClient/constants.php b/src/AnimeClient/constants.php index 7d8471eb..e4bf1e6c 100644 --- a/src/AnimeClient/constants.php +++ b/src/AnimeClient/constants.php @@ -27,8 +27,12 @@ const USER_AGENT = "Tim's Anime Client/5.2"; // Regex patterns const ALPHA_SLUG_PATTERN = '[a-zA-Z_]+'; const NUM_PATTERN = '[0-9]+'; -const SLUG_PATTERN = '[a-zA-Z0-9\-]+'; -const SLUG_SPACE_PATTERN = '[a-zA-Z_\- ]+'; +/** + * Eugh...url slugs can have weird characters + * So...if it's not a forward slash, sure it's valid 😅 + */ +const KITSU_SLUG_PATTERN = '[^\/]+'; +const SLUG_PATTERN = '[a-zA-Z0-9\- ]+'; // Why doesn't this already exist? const MILLI_FROM_NANO = 1000 * 1000;