From 7d6af5ad00c5337e9fce28d5c8adceaa7ecbb953 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Mon, 24 Aug 2020 13:07:47 -0400 Subject: [PATCH] Get library entry via GraphQL, see #28 --- src/AnimeClient/API/Kitsu/ListItem.php | 18 +- src/AnimeClient/API/Kitsu/MangaTrait.php | 2 - src/AnimeClient/API/Kitsu/Model.php | 89 +-------- .../Kitsu/Queries/CharacterDetails.graphql | 11 - .../API/Kitsu/Queries/GetLibraryItem.graphql | 73 +++++++ src/AnimeClient/API/Kitsu/RequestBuilder.php | 25 +-- .../Kitsu/Transformer/AnimeTransformer.php | 29 +-- .../Transformer/LibraryEntryTransformer.php | 188 ++++++++++++++++++ .../Kitsu/Transformer/MangaTransformer.php | 26 ++- 9 files changed, 304 insertions(+), 157 deletions(-) create mode 100644 src/AnimeClient/API/Kitsu/Queries/GetLibraryItem.graphql create mode 100644 src/AnimeClient/API/Kitsu/Transformer/LibraryEntryTransformer.php diff --git a/src/AnimeClient/API/Kitsu/ListItem.php b/src/AnimeClient/API/Kitsu/ListItem.php index 476bce89..d3db19b2 100644 --- a/src/AnimeClient/API/Kitsu/ListItem.php +++ b/src/AnimeClient/API/Kitsu/ListItem.php @@ -115,21 +115,9 @@ final class ListItem extends AbstractListItem { */ public function get(string $id): array { - $authHeader = $this->getAuthHeader(); - - $request = $this->requestBuilder->newRequest('GET', "library-entries/{$id}") - ->setQuery([ - 'include' => 'media,media.categories,media.mappings' - ]); - - if ($authHeader !== NULL) - { - $request = $request->setHeader('Authorization', $authHeader); - } - - $request = $request->getFullRequest(); - $response = getResponse($request); - return Json::decode(wait($response->getBody()->buffer())); + return $this->requestBuilder->runQuery('GetLibraryItem', [ + 'id' => $id, + ]); } /** diff --git a/src/AnimeClient/API/Kitsu/MangaTrait.php b/src/AnimeClient/API/Kitsu/MangaTrait.php index 58a8a663..cd3a938b 100644 --- a/src/AnimeClient/API/Kitsu/MangaTrait.php +++ b/src/AnimeClient/API/Kitsu/MangaTrait.php @@ -59,7 +59,6 @@ trait MangaTrait { $baseData = $this->requestBuilder->runQuery('MangaDetails', [ 'slug' => $slug ]); - // $baseData = $this->getRawMediaData('manga', $slug); if (empty($baseData)) { @@ -80,7 +79,6 @@ trait MangaTrait { $baseData = $this->requestBuilder->runQuery('MangaDetailsById', [ 'id' => $mangaId, ]); - // $baseData = $this->getRawMediaDataById('manga', $mangaId); return $this->mangaTransformer->transform($baseData); } diff --git a/src/AnimeClient/API/Kitsu/Model.php b/src/AnimeClient/API/Kitsu/Model.php index af852d3a..cb339312 100644 --- a/src/AnimeClient/API/Kitsu/Model.php +++ b/src/AnimeClient/API/Kitsu/Model.php @@ -28,6 +28,7 @@ use Aviat\AnimeClient\API\{ use Aviat\AnimeClient\API\Kitsu\Transformer\{ AnimeTransformer, AnimeListTransformer, + LibraryEntryTransformer, MangaTransformer, MangaListTransformer }; @@ -331,24 +332,12 @@ final class Model { public function getListItem(string $listId) { $baseData = $this->listItem->get($listId); - $included = JsonAPI::organizeIncludes($baseData['included']); - - if (array_key_exists('anime', $included)) + if ( ! isset($baseData['data']['findLibraryEntryById'])) { - $included = JsonAPI::inlineIncludedRelationships($included, 'anime'); - $baseData['data']['included'] = $included; - return $this->animeListTransformer->transform($baseData['data']); + return []; } - if (array_key_exists('manga', $included)) - { - $included = JsonAPI::inlineIncludedRelationships($included, 'manga'); - $baseData['data']['included'] = $included; - $baseData['data']['manga'] = $baseData['included'][0]; - return $this->mangaListTransformer->transform($baseData['data']); - } - - return $baseData['data']; + return (new LibraryEntryTransformer())->transform($baseData['data']['findLibraryEntryById']); } /** @@ -464,76 +453,6 @@ final class Model { ->get(['kitsu_username']); } - /** - * Get the raw data for the anime/manga id - * - * @param string $type - * @param string $id - * @return array - */ - private function getRawMediaDataById(string $type, string $id): array - { - $options = [ - 'query' => [ - 'include' => ($type === 'anime') - ? 'categories,mappings,streamingLinks' - : 'categories,mappings', - ] - ]; - - $data = $this->requestBuilder->getRequest("{$type}/{$id}", $options); - - if (empty($data['data'])) - { - return []; - } - - $baseData = $data['data']['attributes']; - $baseData['id'] = $id; - $baseData['included'] = $data['included']; - return $baseData; - } - - /** - * Get media item by slug - * - * @param string $type - * @param string $slug - * @return array - */ - private function getRawMediaData(string $type, string $slug): array - { - $options = [ - 'query' => [ - 'filter' => [ - 'slug' => $slug - ], - 'fields' => [ - 'categories' => 'slug,title', - 'characters' => 'slug,name,image', - 'mappings' => 'externalSite,externalId', - 'animeCharacters' => 'character,role', - 'mediaCharacters' => 'character,role', - ], - 'include' => ($type === 'anime') - ? 'staff,staff.person,categories,mappings,streamingLinks,animeCharacters.character,characters.character' - : 'staff,staff.person,categories,mappings,characters.character', - ] - ]; - - $data = $this->requestBuilder->getRequest($type, $options); - - if (empty($data['data'])) - { - return []; - } - - $baseData = $data['data'][0]['attributes']; - $baseData['id'] = $data['data'][0]['id']; - $baseData['included'] = $data['included']; - return $baseData; - } - private function getListCount(string $type, string $status = ''): int { $options = [ diff --git a/src/AnimeClient/API/Kitsu/Queries/CharacterDetails.graphql b/src/AnimeClient/API/Kitsu/Queries/CharacterDetails.graphql index 6054f9e0..25e89db3 100644 --- a/src/AnimeClient/API/Kitsu/Queries/CharacterDetails.graphql +++ b/src/AnimeClient/API/Kitsu/Queries/CharacterDetails.graphql @@ -13,17 +13,6 @@ query ($slug: String!) { canonicalLocale localized }, - primaryMedia { - posterImage { - original { - url - } - } - titles { - canonical - } - type - }, media { nodes { media { diff --git a/src/AnimeClient/API/Kitsu/Queries/GetLibraryItem.graphql b/src/AnimeClient/API/Kitsu/Queries/GetLibraryItem.graphql new file mode 100644 index 00000000..ad9f61cc --- /dev/null +++ b/src/AnimeClient/API/Kitsu/Queries/GetLibraryItem.graphql @@ -0,0 +1,73 @@ +query($id: ID!) { + findLibraryEntryById(id: $id) { + id + updatedAt + notes + nsfw + private + progress + reconsumeCount + reconsuming + status + rating + media { + id + slug + ageRating + categories { + nodes { + title + } + } + mappings { + nodes { + externalId + externalSite + } + } + posterImage { + views { + width + height + url + } + original { + width + height + url + } + } + startDate + endDate + titles { + canonical + localized + canonicalLocale + } + type + ...on Anime { + episodeCount + episodeLength + streamingLinks { + nodes { + dubs + subs + regions + streamer { + id + siteName + } + url + } + } + subtype + } + ...on Manga { + chapterCount + volumeCount + subtype + + } + } + } +} \ No newline at end of file diff --git a/src/AnimeClient/API/Kitsu/RequestBuilder.php b/src/AnimeClient/API/Kitsu/RequestBuilder.php index d688a941..586efb27 100644 --- a/src/AnimeClient/API/Kitsu/RequestBuilder.php +++ b/src/AnimeClient/API/Kitsu/RequestBuilder.php @@ -255,7 +255,7 @@ final class RequestBuilder extends APIRequestBuilder { public function mutate(string $name, array $variables = []): array { $request = $this->mutateRequest($name, $variables); - $response = $this->getResponseFromRequest($request); + $response = getResponse($request); return Json::decode(wait($response->getBody()->buffer())); } @@ -267,7 +267,7 @@ final class RequestBuilder extends APIRequestBuilder { * @param string $url * @param array $options * @return Response - * @throws Throwable + * @throws \Throwable */ public function getResponse(string $type, string $url, array $options = []): Response { @@ -286,27 +286,6 @@ final class RequestBuilder extends APIRequestBuilder { return $response; } - /** - * @param Request $request - * @return Response - * @throws Throwable - */ - private function getResponseFromRequest(Request $request): Response - { - $logger = $this->container->getLogger('kitsu-request'); - $response = getResponse($request); - - $logger->debug('Kitsu GraphQL response', [ - 'status' => $response->getStatus(), - 'reason' => $response->getReason(), - 'body' => $response->getBody(), - 'headers' => $response->getHeaders(), - 'requestHeaders' => $request->getHeaders(), - ]); - - return $response; - } - /** * Remove some boilerplate for GraphQL requests * diff --git a/src/AnimeClient/API/Kitsu/Transformer/AnimeTransformer.php b/src/AnimeClient/API/Kitsu/Transformer/AnimeTransformer.php index 68352edb..40d9dec7 100644 --- a/src/AnimeClient/API/Kitsu/Transformer/AnimeTransformer.php +++ b/src/AnimeClient/API/Kitsu/Transformer/AnimeTransformer.php @@ -34,9 +34,6 @@ final class AnimeTransformer extends AbstractTransformer { */ public function transform($item): AnimePage { - // TODO: missing GraphQL data: - // * streaming links - $base = array_key_exists('findAnimeBySlug', $item['data']) ? $item['data']['findAnimeBySlug'] : $item['data']['findAnimeById']; @@ -52,12 +49,14 @@ final class AnimeTransformer extends AbstractTransformer { if (count($base['characters']['nodes']) > 0) { - $characters['main'] = []; - $characters['supporting'] = []; - foreach ($base['characters']['nodes'] as $rawCharacter) { - $type = $rawCharacter['role'] === 'MAIN' ? 'main' : 'supporting'; + $type = mb_strtolower($rawCharacter['role']); + if ( ! isset($characters[$type])) + { + $characters[$type] = []; + } + $details = $rawCharacter['character']; $characters[$type][$details['id']] = [ 'image' => $details['image'], @@ -66,13 +65,19 @@ final class AnimeTransformer extends AbstractTransformer { ]; } - uasort($characters['main'], fn($a, $b) => $a['name'] <=> $b['name']); - uasort($characters['supporting'], fn($a, $b) => $a['name'] <=> $b['name']); - - if (empty($characters['supporting'])) + foreach (array_keys($characters) as $type) { - unset($characters['supporting']); + if (empty($characters[$type])) + { + unset($characters[$type]); + } + else + { + uasort($characters[$type], fn($a, $b) => $a['name'] <=> $b['name']); + } } + + krsort($characters); } if (count($base['staff']['nodes']) > 0) diff --git a/src/AnimeClient/API/Kitsu/Transformer/LibraryEntryTransformer.php b/src/AnimeClient/API/Kitsu/Transformer/LibraryEntryTransformer.php new file mode 100644 index 00000000..47e10586 --- /dev/null +++ b/src/AnimeClient/API/Kitsu/Transformer/LibraryEntryTransformer.php @@ -0,0 +1,188 @@ + + * @copyright 2015 - 2020 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 5.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\API\Kitsu\Transformer; + +use Aviat\AnimeClient\API\Kitsu; +use Aviat\AnimeClient\Types\{FormItem, AnimeListItem, MangaListItem, MangaListItemDetail}; +use Aviat\Ion\Transformer\AbstractTransformer; +use Aviat\Ion\Type\StringType; + +/** + * Transformer for anime list + */ +final class LibraryEntryTransformer extends AbstractTransformer +{ + public function transform($item) + { + $type = $item['media']['type'] ?? ''; + + $genres = []; + if ($type !== '') + { + $genres = array_column($item['media']['categories']['nodes'], 'title'); + sort($genres); + } + + switch (strtolower($type)) + { + case 'anime': + return $this->animeTransform($item, $genres); + + case 'manga': + return $this->mangaTransform($item, $genres); + + default: + return []; + } + } + + private function animeTransform($item, array $genres): AnimeListItem + { + $animeId = $item['media']['id']; + $anime = $item['media']; + + $rating = (int) $item['rating'] !== 0 + ? $item['rating'] / 2 + : '-'; + + $total_episodes = array_key_exists('episodeCount', $anime) && (int) $anime['episodeCount'] !== 0 + ? (int) $anime['episodeCount'] + : '-'; + + $MALid = NULL; + + if (isset($anime['mappings']['nodes'])) + { + foreach ($anime['mappings']['nodes'] as $mapping) + { + if ($mapping['externalSite'] === 'MYANIMELIST_ANIME') + { + $MALid = $mapping['externalId']; + break; + } + } + } + + $streamingLinks = array_key_exists('nodes', $anime['streamingLinks']) + ? Kitsu::parseStreamingLinks($anime['streamingLinks']['nodes']) + : []; + + $titles = Kitsu::getFilteredTitles($anime['titles']); + $title = $anime['titles']['canonical']; + + return AnimeListItem::from([ + 'id' => $item['id'], + 'mal_id' => $MALid, + 'episodes' => [ + 'watched' => (int) $item['progress'] !== 0 + ? (int) $item['progress'] + : '-', + 'total' => $total_episodes, + 'length' => $anime['episodeLength'], + ], + 'airing' => [ + 'status' => Kitsu::getAiringStatus($anime['startDate'], $anime['endDate']), + 'started' => $anime['startDate'], + 'ended' => $anime['endDate'] + ], + 'anime' => [ + 'id' => $animeId, + 'age_rating' => $anime['ageRating'], + 'title' => $title, + 'titles' => $titles, + 'slug' => $anime['slug'], + 'show_type' => (string)StringType::from($anime['subtype'])->upperCaseFirst(), + 'cover_image' => $anime['posterImage']['views'][1]['url'], + 'genres' => $genres, + 'streaming_links' => $streamingLinks, + ], + 'watching_status' => $item['status'], + 'notes' => $item['notes'], + 'rewatching' => (bool) $item['reconsuming'], + 'rewatched' => (int) $item['reconsumeCount'], + 'user_rating' => $rating, + 'private' => $item['private'] ?? FALSE, + ]); + } + + private function mangaTransform($item, array $genres): MangaListItem + { + $mangaId = $item['media']['id']; + $manga = $item['media']; + + $rating = (int) $item['rating'] !== 0 + ? $item['rating'] / 2 + : '-'; + + $totalChapters = ((int) $manga['chapterCount'] !== 0) + ? $manga['chapterCount'] + : '-'; + + $totalVolumes = ((int) $manga['volumeCount'] !== 0) + ? $manga['volumeCount'] + : '-'; + + $readChapters = ((int) $item['progress'] !== 0) + ? $item['progress'] + : '-'; + + $MALid = NULL; + + if (isset($manga['mappings']['nodes'])) + { + foreach ($manga['mappings']['nodes'] as $mapping) + { + if ($mapping['externalSite'] === 'MYANIMELIST_MANGA') + { + $MALid = $mapping['externalId']; + break; + } + } + } + + $titles = Kitsu::getFilteredTitles($manga['titles']); + $title = $manga['titles']['canonical']; + + return MangaListItem::from([ + 'id' => $item['id'], + 'mal_id' => $MALid, + 'chapters' => [ + 'read' => $readChapters, + 'total' => $totalChapters + ], + 'volumes' => [ + 'read' => '-', //$item['attributes']['volumes_read'], + 'total' => $totalVolumes + ], + 'manga' => MangaListItemDetail::from([ + 'genres' => $genres, + 'id' => $mangaId, + 'image' => $manga['posterImage']['views'][1]['url'], + 'slug' => $manga['slug'], + 'title' => $title, + 'titles' => $titles, + 'type' => (string)StringType::from($manga['subtype'])->upperCaseFirst(), + 'url' => 'https://kitsu.io/manga/' . $manga['slug'], + ]), + 'reading_status' => strtolower($item['status']), + 'notes' => $item['notes'], + 'rereading' => (bool)$item['reconsuming'], + 'reread' => $item['reconsumeCount'], + 'user_rating' => $rating, + ]); + } +} \ No newline at end of file diff --git a/src/AnimeClient/API/Kitsu/Transformer/MangaTransformer.php b/src/AnimeClient/API/Kitsu/Transformer/MangaTransformer.php index 09e5b40a..77f2d3b1 100644 --- a/src/AnimeClient/API/Kitsu/Transformer/MangaTransformer.php +++ b/src/AnimeClient/API/Kitsu/Transformer/MangaTransformer.php @@ -49,12 +49,14 @@ final class MangaTransformer extends AbstractTransformer { if (count($base['characters']['nodes']) > 0) { - $characters['main'] = []; - $characters['supporting'] = []; - foreach ($base['characters']['nodes'] as $rawCharacter) { - $type = $rawCharacter['role'] === 'MAIN' ? 'main' : 'supporting'; + $type = mb_strtolower($rawCharacter['role']); + if ( ! isset($characters[$type])) + { + $characters[$type] = []; + } + $details = $rawCharacter['character']; $characters[$type][$details['id']] = [ 'image' => $details['image'], @@ -63,13 +65,19 @@ final class MangaTransformer extends AbstractTransformer { ]; } - uasort($characters['main'], fn($a, $b) => $a['name'] <=> $b['name']); - uasort($characters['supporting'], fn($a, $b) => $a['name'] <=> $b['name']); - - if (empty($characters['supporting'])) + foreach (array_keys($characters) as $type) { - unset($characters['supporting']); + if (empty($characters[$type])) + { + unset($characters[$type]); + } + else + { + uasort($characters[$type], fn($a, $b) => $a['name'] <=> $b['name']); + } } + + krsort($characters); } if (count($base['staff']['nodes']) > 0)