From 161c2b3bc1fae37f0fe76f988197c2d49c51bf9e Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Tue, 28 Mar 2017 11:01:38 -0400 Subject: [PATCH 01/21] Update dependencies, use ParallelAPIRequest --- CHANGELOG.md | 1 + composer.json | 2 +- src/API/Kitsu/Model.php | 15 +++++++++------ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cab3e75f..872ade6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Updated to use Kitsu API after discontinuation of Hummingbird * Added streaming links to list entries from the Kitsu API * Added simple integration with MyAnimeList, so an update can cross-post to both Kitsu and MyAnimeList +* Added character pages ## Version 3 * Converted user configuration to toml files diff --git a/composer.json b/composer.json index e71d64ee..26e7ae82 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "aura/router": "^3.0", "aura/session": "^2.0", "aviat/banker": "^1.0.0", - "aviat/ion": "dev-master", + "aviat/ion": "^2.0.0", "monolog/monolog": "^1.0", "psr/http-message": "~1.0", "psr/log": "~1.0", diff --git a/src/API/Kitsu/Model.php b/src/API/Kitsu/Model.php index 7f4f5457..b7b840f1 100644 --- a/src/API/Kitsu/Model.php +++ b/src/API/Kitsu/Model.php @@ -19,7 +19,12 @@ namespace Aviat\AnimeClient\API\Kitsu; use function Amp\{all, wait}; use Amp\Artax\{Client, Request}; -use Aviat\AnimeClient\API\{CacheTrait, JsonAPI, Kitsu as K}; +use Aviat\AnimeClient\API\{ + CacheTrait, + JsonAPI, + Kitsu as K, + ParallelAPIRequest +}; use Aviat\AnimeClient\API\Enum\{ AnimeWatchingStatus\Title, AnimeWatchingStatus\Kitsu as KitsuWatchingStatus, @@ -343,18 +348,16 @@ class Model { $size = 100; $pages = ceil($count / $size); - $requests = []; + $requester = new ParallelAPIRequest(); // Set up requests for ($i = 0; $i < $pages; $i++) { $offset = $i * $size; - $requests[] = $this->getPagedAnimeList($size, $offset, $options); + $requester->addRequest($this->getPagedAnimeList($size, $offset, $options)); } - $promiseArray = (new Client())->requestMulti($requests); - - $responses = wait(all($promiseArray)); + $responses = $requester->makeRequests(); $output = []; foreach($responses as $response) From 754a5e7b98c8fedd3eb9b360a3e489bdc7666017 Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Tue, 28 Mar 2017 14:34:33 -0400 Subject: [PATCH 02/21] Reorgnize order of Kitsu model methods --- src/API/Kitsu/Model.php | 501 +++++++++++++++++++++------------------- 1 file changed, 265 insertions(+), 236 deletions(-) diff --git a/src/API/Kitsu/Model.php b/src/API/Kitsu/Model.php index b7b840f1..08dbcd84 100644 --- a/src/API/Kitsu/Model.php +++ b/src/API/Kitsu/Model.php @@ -78,7 +78,6 @@ class Model { */ protected $mangaListTransformer; - /** * Constructor * @@ -93,6 +92,34 @@ class Model { $this->mangaListTransformer = new MangaListTransformer(); } + /** + * Get the access token from the Kitsu API + * + * @param string $username + * @param string $password + * @return bool|string + */ + public function authenticate(string $username, string $password) + { + $response = $this->getResponse('POST', K::AUTH_URL, [ + 'headers' => [], + 'form_params' => [ + 'grant_type' => 'password', + 'username' => $username, + 'password' => $password + ] + ]); + + $data = Json::decode((string)$response->getBody()); + + if (array_key_exists('access_token', $data)) + { + return $data; + } + + return FALSE; + } + /** * Get the userid for a username from Kitsu * @@ -160,39 +187,46 @@ class Model { 'include' => 'waifu,pinnedPost,blocks,linkedAccounts,profileLinks,profileLinks.profileLinkSite,mediaFollows,userRoles' ] ]); - // $data['included'] = JsonAPI::organizeIncludes($data['included']); return $data; } /** - * Get the access token from the Kitsu API + * Search for an anime or manga * - * @param string $username - * @param string $password - * @return bool|string + * @param string $type - 'anime' or 'manga' + * @param string $query - name of the item to search for + * @return array */ - public function authenticate(string $username, string $password) + public function search(string $type, string $query): array { - $response = $this->getResponse('POST', K::AUTH_URL, [ - 'headers' => [], - 'form_params' => [ - 'grant_type' => 'password', - 'username' => $username, - 'password' => $password + $options = [ + 'query' => [ + 'filter' => [ + 'text' => $query + ], + 'page' => [ + 'offset' => 0, + 'limit' => 20 + ], ] - ]); + ]; - $data = Json::decode((string)$response->getBody()); + $raw = $this->getRequest($type, $options); - if (array_key_exists('access_token', $data)) + foreach ($raw['data'] as &$item) { - return $data; + $item['attributes']['titles'] = K::filterTitles($item['attributes']); + array_shift($item['attributes']['titles']); } - return FALSE; + return $raw; } + // ------------------------------------------------------------------------- + // ! Anime-specific methods + // ------------------------------------------------------------------------- + /** * Get information about a particular anime * @@ -225,200 +259,6 @@ class Model { return $this->animeTransformer->transform($baseData); } - /** - * Get the mal id for the anime represented by the kitsu id - * to enable updating MyAnimeList - * - * @param string $kitsuAnimeId The id of the anime on Kitsu - * @return string|null Returns the mal id if it exists, otherwise null - */ - public function getMalIdForAnime(string $kitsuAnimeId) - { - $options = [ - 'query' => [ - 'include' => 'mappings' - ] - ]; - $data = $this->getRequest("anime/{$kitsuAnimeId}", $options); - $mappings = array_column($data['included'], 'attributes'); - - foreach($mappings as $map) - { - if ($map['externalSite'] === 'myanimelist/anime') - { - return $map['externalId']; - } - } - - return NULL; - } - - /** - * Get information about a particular manga - * - * @param string $mangaId - * @return array - */ - public function getManga(string $mangaId): array - { - $baseData = $this->getRawMediaData('manga', $mangaId); - - if (empty($baseData)) - { - return []; - } - - $transformed = $this->mangaTransformer->transform($baseData); - $transformed['included'] = $baseData['included']; - return $transformed; - } - - /** - * Get the number of anime list items - * - * @param string $status - Optional status to filter by - * @return int - */ - public function getAnimeListCount(string $status = '') : int - { - $options = [ - 'query' => [ - 'filter' => [ - 'user_id' => $this->getUserIdByUsername(), - 'media_type' => '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']; - - } - - /** - * Get the full anime list in paginated form - * - * @param int $limit - * @param int $offset - * @param array $options - * @return Request - */ - public function getPagedAnimeList(int $limit = 100, int $offset = 0, array $options = [ - 'include' => 'anime.mappings' - ]): Request - { - $defaultOptions = [ - 'filter' => [ - 'user_id' => $this->getUserIdByUsername($this->getUsername()), - 'media_type' => 'Anime' - ], - 'page' => [ - 'offset' => $offset, - 'limit' => $limit - ], - 'sort' => '-updated_at' - ]; - $options = array_merge($defaultOptions, $options); - - return $this->setUpRequest('GET', 'library-entries', ['query' => $options]); - } - - /** - * Get the full anime list - * - * @param array $options - * @return array - */ - public function getFullAnimeList(array $options = [ - 'include' => 'anime.mappings' - ]): array - { - $status = $options['filter']['status'] ?? ''; - $count = $this->getAnimeListCount($status); - $size = 100; - $pages = ceil($count / $size); - - $requester = new ParallelAPIRequest(); - - // Set up requests - for ($i = 0; $i < $pages; $i++) - { - $offset = $i * $size; - $requester->addRequest($this->getPagedAnimeList($size, $offset, $options)); - } - - $responses = $requester->makeRequests(); - $output = []; - - foreach($responses as $response) - { - $data = Json::decode($response->getBody()); - $output = array_merge_recursive($output, $data); - } - - return $output; - } - - /** - * Get the raw (unorganized) anime list for the configured user - * - * @param string $status - The watching status to filter the list with - * @return array - */ - public function getRawAnimeList(string $status): array - { - - $options = [ - 'filter' => [ - 'user_id' => $this->getUserIdByUsername($this->getUsername()), - 'media_type' => 'Anime', - 'status' => $status, - ], - 'include' => 'media,media.genres,media.mappings,anime.streamingLinks', - 'sort' => '-updated_at' - ]; - - return $this->getFullAnimeList($options); - } - - /** - * Get all the anine entries, that are organized for output to html - * - * @return array - */ - public function getFullOrganizedAnimeList(): array - { - $cacheItem = $this->cache->getItem(self::FULL_TRANSFORMED_LIST_CACHE_KEY); - - if ( ! $cacheItem->isHit()) - { - $output = []; - - $statuses = KitsuWatchingStatus::getConstList(); - - foreach ($statuses as $key => $status) - { - $mappedStatus = AnimeWatchingStatus::KITSU_TO_TITLE[$status]; - $output[$mappedStatus] = $this->getAnimeList($status) ?? []; - } - - $cacheItem->set($output); - $cacheItem->save(); - } - - return $cacheItem->get(); - } - /** * Get the anime list for the configured user * @@ -458,23 +298,194 @@ class Model { } /** - * Get all Manga lists + * Get the number of anime list items * + * @param string $status - Optional status to filter by + * @return int + */ + public function getAnimeListCount(string $status = '') : int + { + $options = [ + 'query' => [ + 'filter' => [ + 'user_id' => $this->getUserIdByUsername(), + 'media_type' => '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']; + } + + /** + * Get the full anime list + * + * @param array $options * @return array */ - public function getFullOrganizedMangaList(): array + public function getFullAnimeList(array $options = [ + 'include' => 'anime.mappings' + ]): array { - $statuses = KitsuReadingStatus::getConstList(); - $output = []; - foreach ($statuses as $status) + $status = $options['filter']['status'] ?? ''; + $count = $this->getAnimeListCount($status); + $size = 100; + $pages = ceil($count / $size); + + $requester = new ParallelAPIRequest(); + + // Set up requests + for ($i = 0; $i < $pages; $i++) { - $mappedStatus = MangaReadingStatus::KITSU_TO_TITLE[$status]; - $output[$mappedStatus] = $this->getMangaList($status); + $offset = $i * $size; + $requester->addRequest($this->getPagedAnimeList($size, $offset, $options)); + } + + $responses = $requester->makeRequests(); + $output = []; + + foreach($responses as $response) + { + $data = Json::decode($response->getBody()); + $output = array_merge_recursive($output, $data); } return $output; } + /** + * Get all the anine entries, that are organized for output to html + * + * @return array + */ + public function getFullOrganizedAnimeList(): array + { + $output = []; + + $statuses = KitsuWatchingStatus::getConstList(); + + foreach ($statuses as $key => $status) + { + $mappedStatus = AnimeWatchingStatus::KITSU_TO_TITLE[$status]; + $output[$mappedStatus] = $this->getAnimeList($status) ?? []; + } + + return $output; + } + + /** + * Get the mal id for the anime represented by the kitsu id + * to enable updating MyAnimeList + * + * @param string $kitsuAnimeId The id of the anime on Kitsu + * @return string|null Returns the mal id if it exists, otherwise null + */ + public function getMalIdForAnime(string $kitsuAnimeId) + { + $options = [ + 'query' => [ + 'include' => 'mappings' + ] + ]; + $data = $this->getRequest("anime/{$kitsuAnimeId}", $options); + $mappings = array_column($data['included'], 'attributes'); + + foreach($mappings as $map) + { + if ($map['externalSite'] === 'myanimelist/anime') + { + return $map['externalId']; + } + } + + return NULL; + } + + /** + * Get the full anime list in paginated form + * + * @param int $limit + * @param int $offset + * @param array $options + * @return Request + */ + public function getPagedAnimeList(int $limit = 100, int $offset = 0, array $options = [ + 'include' => 'anime.mappings' + ]): Request + { + $defaultOptions = [ + 'filter' => [ + 'user_id' => $this->getUserIdByUsername($this->getUsername()), + 'media_type' => 'Anime' + ], + 'page' => [ + 'offset' => $offset, + 'limit' => $limit + ], + 'sort' => '-updated_at' + ]; + $options = array_merge($defaultOptions, $options); + + return $this->setUpRequest('GET', 'library-entries', ['query' => $options]); + } + + /** + * Get the raw (unorganized) anime list for the configured user + * + * @param string $status - The watching status to filter the list with + * @return array + */ + public function getRawAnimeList(string $status): array + { + + $options = [ + 'filter' => [ + 'user_id' => $this->getUserIdByUsername($this->getUsername()), + 'media_type' => 'Anime', + 'status' => $status, + ], + 'include' => 'media,media.genres,media.mappings,anime.streamingLinks', + 'sort' => '-updated_at' + ]; + + return $this->getFullAnimeList($options); + } + + // ------------------------------------------------------------------------- + // ! Manga-specific methods + // ------------------------------------------------------------------------- + + /** + * Get information about a particular manga + * + * @param string $slug + * @return array + */ + public function getManga(string $slug): array + { + $baseData = $this->getRawMediaData('manga', $slug); + + if (empty($baseData)) + { + return []; + } + + $transformed = $this->mangaTransformer->transform($baseData); + $transformed['included'] = $baseData['included']; + return $transformed; + } + /** * Get the manga list for the configured user * @@ -518,37 +529,55 @@ class Model { } /** - * Search for an anime or manga + * Get all Manga lists * - * @param string $type - 'anime' or 'manga' - * @param string $query - name of the item to search for * @return array */ - public function search(string $type, string $query): array + public function getFullOrganizedMangaList(): array + { + $statuses = KitsuReadingStatus::getConstList(); + $output = []; + foreach ($statuses as $status) + { + $mappedStatus = MangaReadingStatus::KITSU_TO_TITLE[$status]; + $output[$mappedStatus] = $this->getMangaList($status); + } + + return $output; + } + + /** + * Get the mal id for the manga represented by the kitsu id + * to enable updating MyAnimeList + * + * @param string $kitsuAnimeId The id of the anime on Kitsu + * @return string|null Returns the mal id if it exists, otherwise null + */ + public function getMalIdForManga(string $kitsuMangaId) { $options = [ 'query' => [ - 'filter' => [ - 'text' => $query - ], - 'page' => [ - 'offset' => 0, - 'limit' => 20 - ], + 'include' => 'mappings' ] ]; + $data = $this->getRequest("manga/{$kitsuMangaId}", $options); + $mappings = array_column($data['included'], 'attributes'); - $raw = $this->getRequest($type, $options); - - foreach ($raw['data'] as &$item) + foreach($mappings as $map) { - $item['attributes']['titles'] = K::filterTitles($item['attributes']); - array_shift($item['attributes']['titles']); + if ($map['externalSite'] === 'myanimelist/anime') + { + return $map['externalId']; + } } - return $raw; + return NULL; } + // ------------------------------------------------------------------------- + // ! Generic API calls + // ------------------------------------------------------------------------- + /** * Create a list item * From 4f528ca2c83344cc8352d71c3d7c06302d4d4a9a Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Tue, 28 Mar 2017 14:36:23 -0400 Subject: [PATCH 03/21] Minor api model refactoring --- src/Model/API.php | 7 +++++++ src/Model/Anime.php | 9 +-------- src/Model/Manga.php | 12 +++++++++--- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Model/API.php b/src/Model/API.php index a7508255..e13e0ee4 100644 --- a/src/Model/API.php +++ b/src/Model/API.php @@ -21,6 +21,13 @@ namespace Aviat\AnimeClient\Model; */ class API extends AbstractModel { + /** + * Whether to use the MAL api + * + * @var boolean + */ + protected $useMALAPI; + /** * Sort the list entries by their title * diff --git a/src/Model/Anime.php b/src/Model/Anime.php index 060f92c7..5fc2935c 100644 --- a/src/Model/Anime.php +++ b/src/Model/Anime.php @@ -39,13 +39,6 @@ class Anime extends API { */ protected $malModel; - /** - * Whether to use the MAL api - * - * @var boolean - */ - protected $useMALAPI; - /** * Anime constructor. * @@ -53,10 +46,10 @@ class Anime extends API { */ public function __construct(ContainerInterface $container) { - $config = $container->get('config'); $this->kitsuModel = $container->get('kitsu-model'); $this->malModel = $container->get('mal-model'); + $config = $container->get('config'); $this->useMALAPI = $config->get(['use_mal_api']) === TRUE; } diff --git a/src/Model/Manga.php b/src/Model/Manga.php index 627e6542..a772faca 100644 --- a/src/Model/Manga.php +++ b/src/Model/Manga.php @@ -16,8 +16,11 @@ namespace Aviat\AnimeClient\Model; -use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Title; -use Aviat\AnimeClient\API\Mapping\MangaReadingStatus; +use Aviat\AnimeClient\API\{ + Enum\MangaReadingStatus\Title, + Mapping\MangaReadingStatus, + ParallelAPIRequest +}; use Aviat\Ion\Di\ContainerInterface; /** @@ -46,6 +49,9 @@ class Manga extends API { $this->kitsuModel = $container->get('kitsu-model'); $this->malModel = $container->get('mal-model'); + + $config = $container->get('config'); + $this->useMALAPI = $config->get(['use_mal_api']) === TRUE; } /** @@ -60,7 +66,7 @@ class Manga extends API { return $this->kitsuModel->getFullOrganizedMangaList(); } - + $APIstatus = MangaReadingStatus::TITLE_TO_KITSU[$status]; $data = $this->kitsuModel->getMangaList($APIstatus); return $this->mapByStatus($data)[$status]; From a892d875fd2062eb7c11165bcf883b5187d0caaf Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Tue, 28 Mar 2017 16:52:27 -0400 Subject: [PATCH 04/21] Update sync lists command to create Kitsu items that are missing compared to MAL --- src/API/Kitsu/ListItem.php | 8 +++++ src/API/Kitsu/Model.php | 27 +++++++++++++++ src/Command/SyncKitsuWithMal.php | 56 ++++++++++++++++++++++++++++++-- 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/API/Kitsu/ListItem.php b/src/API/Kitsu/ListItem.php index 003308a9..bd852daa 100644 --- a/src/API/Kitsu/ListItem.php +++ b/src/API/Kitsu/ListItem.php @@ -33,6 +33,8 @@ class ListItem extends AbstractListItem { private function getAuthHeader() { + $cache = $this->getContainer()->get('cache'); + $cacheItem = $cache->getItem('kitsu-auth-token'); $sessionSegment = $this->getContainer() ->get('session') ->getSegment(SESSION_SEGMENT); @@ -43,6 +45,12 @@ class ListItem extends AbstractListItem { return "bearer {$token}"; } + if ($cacheItem->isHit()) + { + $token = $cacheItem->get(); + return "bearer {$token}"; + } + return FALSE; } diff --git a/src/API/Kitsu/Model.php b/src/API/Kitsu/Model.php index 08dbcd84..f44c53c3 100644 --- a/src/API/Kitsu/Model.php +++ b/src/API/Kitsu/Model.php @@ -223,6 +223,33 @@ class Model { return $raw; } + /** + * Find a media item on Kitsu by its associated MAL id + * + * @param string $malId + * @param string $type "anime" or "manga" + * @return string + */ + public function getKitsuIdFromMALId(string $malId, string $type="anime"): string + { + $options = [ + 'query' => [ + 'filter' => [ + 'external_site' => "myanimelist/{$type}", + 'external_id' => $malId + ], + 'fields' => [ + 'media' => 'id,slug' + ], + 'include' => 'media' + ] + ]; + + $raw = $this->getRequest('mappings', $options); + + return $raw['included'][0]['id']; + } + // ------------------------------------------------------------------------- // ! Anime-specific methods // ------------------------------------------------------------------------- diff --git a/src/Command/SyncKitsuWithMal.php b/src/Command/SyncKitsuWithMal.php index 441396f7..b33a8a04 100644 --- a/src/Command/SyncKitsuWithMal.php +++ b/src/Command/SyncKitsuWithMal.php @@ -62,13 +62,21 @@ class SyncKitsuWithMal extends BaseCommand { $this->echoBox("Number of Kitsu list items: {$kitsuCount}"); $data = $this->diffAnimeLists(); - $this->echoBox("Number of items that need to be added to MAL: " . count($data)); + $this->echoBox("Number of items that need to be added to MAL: " . count($data['addToMAL'])); if ( ! empty($data['addToMAL'])) { $this->echoBox("Adding missing list items to MAL"); $this->createMALAnimeListItems($data['addToMAL']); } + + $this->echoBox('Number of items that need to be added to Kitsu: ' . count($data['addToKitsu'])); + + if ( ! empty($data['addToKitsu'])) + { + $this->echoBox("Adding missing list items to Kitsu"); + $this->createKitusAnimeListItems($data['addToKitsu']); + } } public function getKitsuAnimeList() @@ -137,7 +145,7 @@ class SyncKitsuWithMal extends BaseCommand { ? $item['times_rewatched'] : 0, // 'notes' => , - 'rating' => $item['my_score'], + 'rating' => $item['my_score'] / 2, 'updatedAt' => (new \DateTime()) ->setTimestamp((int)$item['my_last_updated']) ->format(\DateTime::W3C), @@ -201,10 +209,24 @@ class SyncKitsuWithMal extends BaseCommand { $malList = $this->formatMALAnimeList(); $itemsToAddToMAL = []; + $itemsToAddToKitsu = []; + + $malIds = array_column($malList, 'id'); + $kitsuMalIds = array_column($kitsuList, 'malId'); + $missingMalIds = array_diff($malIds, $kitsuMalIds); + + foreach($missingMalIds as $mid) + { + // print_r($malList[$mid]); + $itemsToAddToKitsu[] = array_merge($malList[$mid]['data'], [ + 'id' => $this->kitsuModel->getKitsuIdFromMALId($mid), + 'type' => 'anime' + ]); + } foreach($kitsuList as $kitsuItem) { - if (array_key_exists($kitsuItem['malId'], $malList)) + if (in_array($kitsuItem['malId'], $malIds)) { // Eventually, compare the list entries, and determine which // needs to be updated @@ -230,9 +252,37 @@ class SyncKitsuWithMal extends BaseCommand { return [ 'addToMAL' => $itemsToAddToMAL, + 'addToKitsu' => $itemsToAddToKitsu ]; } + public function createKitusAnimeListItems($itemsToAdd) + { + $requests = []; + foreach($itemsToAdd as $item) + { + $requests[] = $this->kitsuModel->createListItem($item); + } + + $promiseArray = (new Client())->requestMulti($requests); + + $responses = wait(all($promiseArray)); + + foreach($responses as $key => $response) + { + $id = $itemsToAdd[$key]['id']; + if ($response->getStatus() === 201) + { + $this->echoBox("Successfully create list item with id: {$id}"); + } + else + { + echo $response->getBody(); + $this->echoBox("Failed to create list item with id: {$id}"); + } + } + } + public function createMALAnimeListItems($itemsToAdd) { $transformer = new ALT(); From 08a882bbb628fe5d172e432fffa487834f06b002 Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Wed, 29 Mar 2017 12:32:36 -0400 Subject: [PATCH 05/21] Create missing manga items on kitsu and mal with sync command --- src/API/Kitsu/Model.php | 95 +++++++ src/API/MAL/ListItem.php | 34 ++- src/API/MAL/Model.php | 83 ++++-- .../MAL/Transformer/AnimeListTransformer.php | 16 +- .../MAL/Transformer/MangaListTransformer.php | 85 ++++++ src/API/Mapping/MangaReadingStatus.php | 21 +- src/Command/SyncKitsuWithMal.php | 266 ++++++++++++++---- src/Model/Anime.php | 15 +- 8 files changed, 503 insertions(+), 112 deletions(-) create mode 100644 src/API/MAL/Transformer/MangaListTransformer.php diff --git a/src/API/Kitsu/Model.php b/src/API/Kitsu/Model.php index f44c53c3..5247286c 100644 --- a/src/API/Kitsu/Model.php +++ b/src/API/Kitsu/Model.php @@ -555,6 +555,73 @@ class Model { return $cacheItem->get(); } + /** + * Get the number of manga list items + * + * @param string $status - Optional status to filter by + * @return int + */ + public function getMangaListCount(string $status = '') : int + { + $options = [ + 'query' => [ + 'filter' => [ + 'user_id' => $this->getUserIdByUsername(), + 'media_type' => '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']; + } + + /** + * Get the full manga list + * + * @param array $options + * @return array + */ + public function getFullMangaList(array $options = [ + 'include' => 'manga.mappings' + ]): array + { + $status = $options['filter']['status'] ?? ''; + $count = $this->getMangaListCount($status); + $size = 100; + $pages = ceil($count / $size); + + $requester = new ParallelAPIRequest(); + + // Set up requests + for ($i = 0; $i < $pages; $i++) + { + $offset = $i * $size; + $requester->addRequest($this->getPagedMangaList($size, $offset, $options)); + } + + $responses = $requester->makeRequests(); + $output = []; + + foreach($responses as $response) + { + $data = Json::decode($response->getBody()); + $output = array_merge_recursive($output, $data); + } + + return $output; + } + /** * Get all Manga lists * @@ -573,6 +640,34 @@ class Model { return $output; } + /** + * Get the full manga list in paginated form + * + * @param int $limit + * @param int $offset + * @param array $options + * @return Request + */ + public function getPagedMangaList(int $limit = 100, int $offset = 0, array $options = [ + 'include' => 'manga.mappings' + ]): Request + { + $defaultOptions = [ + 'filter' => [ + 'user_id' => $this->getUserIdByUsername($this->getUsername()), + 'media_type' => 'Manga' + ], + 'page' => [ + 'offset' => $offset, + 'limit' => $limit + ], + 'sort' => '-updated_at' + ]; + $options = array_merge($defaultOptions, $options); + + return $this->setUpRequest('GET', 'library-entries', ['query' => $options]); + } + /** * Get the mal id for the manga represented by the kitsu id * to enable updating MyAnimeList diff --git a/src/API/MAL/ListItem.php b/src/API/MAL/ListItem.php index 20c285f9..e46199cf 100644 --- a/src/API/MAL/ListItem.php +++ b/src/API/MAL/ListItem.php @@ -30,7 +30,14 @@ class ListItem { use ContainerAware; use MALTrait; - public function create(array $data): Request + /** + * Create a list item + * + * @param array $data + * @param string $type + * @return Request + */ + public function create(array $data, string $type = 'anime'): Request { $id = $data['id']; $createData = [ @@ -42,17 +49,24 @@ class ListItem { $config = $this->container->get('config'); - return $this->requestBuilder->newRequest('POST', "animelist/add/{$id}.xml") + return $this->requestBuilder->newRequest('POST', "{$type}list/add/{$id}.xml") ->setFormFields($createData) ->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password'])) ->getFullRequest(); } - public function delete(string $id): Request + /** + * Delete a list item + * + * @param string $id + * @param string $type + * @return Request + */ + public function delete(string $id, string $type = 'anime'): Request { $config = $this->container->get('config'); - return $this->requestBuilder->newRequest('DELETE', "animelist/delete/{$id}.xml") + return $this->requestBuilder->newRequest('DELETE', "{$type}list/delete/{$id}.xml") ->setFormFields([ 'id' => $id ]) @@ -67,7 +81,15 @@ class ListItem { return []; } - public function update(string $id, array $data): Request + /** + * Update a list item + * + * @param string $id + * @param array $data + * @param string $type + * @return Request + */ + public function update(string $id, array $data, string $type = 'anime'): Request { $config = $this->container->get('config'); @@ -76,7 +98,7 @@ class ListItem { ->addField('id', $id) ->addField('data', $xml); - return $this->requestBuilder->newRequest('POST', "animelist/update/{$id}.xml") + return $this->requestBuilder->newRequest('POST', "{$type}list/update/{$id}.xml") ->setFormFields([ 'id' => $id, 'data' => $xml diff --git a/src/API/MAL/Model.php b/src/API/MAL/Model.php index e4a98591..7ef64243 100644 --- a/src/API/MAL/Model.php +++ b/src/API/MAL/Model.php @@ -17,10 +17,9 @@ namespace Aviat\AnimeClient\API\MAL; use Amp\Artax\Request; -use Aviat\AnimeClient\API\MAL\ListItem; -use Aviat\AnimeClient\API\MAL\Transformer\AnimeListTransformer; +use Aviat\AnimeClient\API\MAL\{ListItem, Transformer\AnimeListTransformer}; use Aviat\AnimeClient\API\XML; -use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus; +use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus}; use Aviat\Ion\Di\ContainerAware; /** @@ -50,39 +49,53 @@ class Model { $this->animeListTransformer = new AnimeListTransformer(); $this->listItem = $listItem; } - - public function createFullListItem(array $data): Request + + /** + * Create a list item on MAL + * + * @param array $data + * @param string $type "anime" or "manga" + * @return Request + */ + public function createFullListItem(array $data, string $type = 'anime'): Request { - return $this->listItem->create($data); + return $this->listItem->create($data, $type); } - public function createListItem(array $data): Request + public function createListItem(array $data, string $type = 'anime'): Request { - $createData = [ - 'id' => $data['id'], - 'data' => [ - 'status' => AnimeWatchingStatus::KITSU_TO_MAL[$data['status']] - ] - ]; + if ($type === 'anime') + { + $createData = [ + 'id' => $data['id'], + 'data' => [ + 'status' => AnimeWatchingStatus::KITSU_TO_MAL[$data['status']] + ] + ]; + } + elseif ($type === 'manga') + { + $createData = [ + 'id' => $data['id'], + 'data' => [ + 'status' => MangaReadingStatus::KITSU_TO_MAL[$data['status']] + ] + ]; + } + + return $this->listItem->create($createData); } - public function getFullList(): array + public function getMangaList(): array { - $config = $this->container->get('config'); - $userName = $config->get(['mal', 'username']); - $list = $this->getRequest('https://myanimelist.net/malappinfo.php', [ - 'headers' => [ - 'Accept' => 'text/xml' - ], - 'query' => [ - 'u' => $userName, - 'status' => 'all' - ] - ]); + return $this->getList('manga'); + } - return $list['myanimelist']['anime']; + public function getAnimeList(): array + { + return $this->getList('anime'); } public function getListItem(string $listId): array @@ -100,4 +113,22 @@ class Model { { return $this->listItem->delete($id); } + + private function getList(string $type): array + { + $config = $this->container->get('config'); + $userName = $config->get(['mal', 'username']); + $list = $this->getRequest('https://myanimelist.net/malappinfo.php', [ + 'headers' => [ + 'Accept' => 'text/xml' + ], + 'query' => [ + 'u' => $userName, + 'status' => 'all', + 'type' => $type + ] + ]); + + return $list['myanimelist'][$type]; + } } \ No newline at end of file diff --git a/src/API/MAL/Transformer/AnimeListTransformer.php b/src/API/MAL/Transformer/AnimeListTransformer.php index ac06da02..222a3600 100644 --- a/src/API/MAL/Transformer/AnimeListTransformer.php +++ b/src/API/MAL/Transformer/AnimeListTransformer.php @@ -24,26 +24,14 @@ use Aviat\Ion\Transformer\AbstractTransformer; */ class AnimeListTransformer extends AbstractTransformer { /** - * Transform MAL episode data to Kitsu episode data + * Identity transformation * * @param array $item * @return array */ public function transform($item) { - $rewatching = (array_key_exists('rewatching', $item) && $item['rewatching']); - - return [ - 'id' => $item['mal_id'], - 'data' => [ - 'status' => AnimeWatchingStatus::KITSU_TO_MAL[$item['watching_status']], - 'rating' => $item['user_rating'], - 'rewatch_value' => (int) $rewatching, - 'times_rewatched' => $item['rewatched'], - 'comments' => $item['notes'], - 'episode' => $item['episodes_watched'] - ] - ]; + return $item; } /** diff --git a/src/API/MAL/Transformer/MangaListTransformer.php b/src/API/MAL/Transformer/MangaListTransformer.php new file mode 100644 index 00000000..2a44e1f3 --- /dev/null +++ b/src/API/MAL/Transformer/MangaListTransformer.php @@ -0,0 +1,85 @@ + + * @copyright 2015 - 2017 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.0 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\API\MAL\Transformer; + +use Aviat\AnimeClient\API\Mapping\MangaReadingStatus; +use Aviat\Ion\Transformer\AbstractTransformer; + +/** + * Transformer for updating MAL List + */ +class MangaListTransformer extends AbstractTransformer { + /** + * Identity transformation + * + * @param array $item + * @return array + */ + public function transform($item) + { + return $item; + } + + /** + * Transform Kitsu data to MAL data + * + * @param array $item + * @return array + */ + public function untransform(array $item): array + { + $map = [ + 'id' => $item['mal_id'], + 'data' => [ + 'chapter' => $item['data']['progress'] + ] + ]; + + $data =& $item['data']; + + foreach($item['data'] as $key => $value) + { + switch($key) + { + case 'notes': + $map['data']['comments'] = $value; + break; + + case 'rating': + $map['data']['score'] = $value * 2; + break; + + case 'reconsuming': + $map['data']['enable_rereading'] = (bool) $value; + break; + + case 'reconsumeCount': + $map['data']['times_reread'] = $value; + break; + + case 'status': + $map['data']['status'] = MangaReadingStatus::KITSU_TO_MAL[$value]; + break; + + default: + break; + } + } + + return $map; + } +} \ No newline at end of file diff --git a/src/API/Mapping/MangaReadingStatus.php b/src/API/Mapping/MangaReadingStatus.php index 7b01c2ce..363483da 100644 --- a/src/API/Mapping/MangaReadingStatus.php +++ b/src/API/Mapping/MangaReadingStatus.php @@ -29,7 +29,7 @@ use Aviat\Ion\Enum; * and url route segments */ class MangaReadingStatus extends Enum { - const MAL_TO_KITSU = [ + const KITSU_TO_MAL = [ Kitsu::READING => MAL::READING, Kitsu::PLAN_TO_READ => MAL::PLAN_TO_READ, Kitsu::COMPLETED => MAL::COMPLETED, @@ -37,12 +37,17 @@ class MangaReadingStatus extends Enum { Kitsu::DROPPED => MAL::DROPPED ]; - const KITSU_TO_MAL = [ + const MAL_TO_KITSU = [ + '1' => Kitsu::READING, + '2' => Kitsu::COMPLETED, + '3' => Kitsu::ON_HOLD, + '4' => Kitsu::DROPPED, + '6' => Kitsu::PLAN_TO_READ, MAL::READING => Kitsu::READING, - MAL::PLAN_TO_READ => Kitsu::PLAN_TO_READ, MAL::COMPLETED => Kitsu::COMPLETED, MAL::ON_HOLD => Kitsu::ON_HOLD, - MAL::DROPPED => Kitsu::DROPPED + MAL::DROPPED => Kitsu::DROPPED, + MAL::PLAN_TO_READ => Kitsu::PLAN_TO_READ, ]; const KITSU_TO_TITLE = [ @@ -50,7 +55,7 @@ class MangaReadingStatus extends Enum { Kitsu::PLAN_TO_READ => Title::PLAN_TO_READ, Kitsu::COMPLETED => Title::COMPLETED, Kitsu::ON_HOLD => Title::ON_HOLD, - Kitsu::DROPPED => Title::DROPPED + Kitsu::DROPPED => Title::DROPPED, ]; const ROUTE_TO_KITSU = [ @@ -58,7 +63,7 @@ class MangaReadingStatus extends Enum { Route::READING => Kitsu::READING, Route::COMPLETED => Kitsu::COMPLETED, Route::DROPPED => Kitsu::DROPPED, - Route::ON_HOLD => Kitsu::ON_HOLD + Route::ON_HOLD => Kitsu::ON_HOLD, ]; const ROUTE_TO_TITLE = [ @@ -67,7 +72,7 @@ class MangaReadingStatus extends Enum { Route::READING => Title::READING, Route::COMPLETED => Title::COMPLETED, Route::DROPPED => Title::DROPPED, - Route::ON_HOLD => Title::ON_HOLD + Route::ON_HOLD => Title::ON_HOLD, ]; const TITLE_TO_KITSU = [ @@ -75,6 +80,6 @@ class MangaReadingStatus extends Enum { Title::READING => Kitsu::READING, Title::COMPLETED => Kitsu::COMPLETED, Title::DROPPED => Kitsu::DROPPED, - Title::ON_HOLD => Kitsu::ON_HOLD + Title::ON_HOLD => Kitsu::ON_HOLD, ]; } \ No newline at end of file diff --git a/src/Command/SyncKitsuWithMal.php b/src/Command/SyncKitsuWithMal.php index b33a8a04..d2ba6081 100644 --- a/src/Command/SyncKitsuWithMal.php +++ b/src/Command/SyncKitsuWithMal.php @@ -19,8 +19,16 @@ namespace Aviat\AnimeClient\Command; use function Amp\{all, wait}; use Amp\Artax\Client; -use Aviat\AnimeClient\API\{JsonAPI, Mapping\AnimeWatchingStatus}; -use Aviat\AnimeClient\API\MAL\Transformer\AnimeListTransformer as ALT; +use Aviat\AnimeClient\API\{ + JsonAPI, + ParallelAPIRequest, + Mapping\AnimeWatchingStatus, + Mapping\MangaReadingStatus +}; +use Aviat\AnimeClient\API\MAL\Transformer\{ + AnimeListTransformer as ALT, + MangaListTransformer as MLT +}; use Aviat\Ion\Json; /** @@ -55,71 +63,71 @@ class SyncKitsuWithMal extends BaseCommand { $this->kitsuModel = $this->container->get('kitsu-model'); $this->malModel = $this->container->get('mal-model'); - $malCount = count($this->getMALAnimeList()); - $kitsuCount = $this->getKitsuAnimeListPageCount(); + $this->syncAnime(); + $this->syncManga(); + } - $this->echoBox("Number of MAL list items: {$malCount}"); - $this->echoBox("Number of Kitsu list items: {$kitsuCount}"); + public function syncAnime() + { + $malCount = count($this->malModel->getAnimeList()); + $kitsuCount = $this->kitsuModel->getAnimeListCount(); + + $this->echoBox("Number of MAL anime list items: {$malCount}"); + $this->echoBox("Number of Kitsu anime list items: {$kitsuCount}"); $data = $this->diffAnimeLists(); - $this->echoBox("Number of items that need to be added to MAL: " . count($data['addToMAL'])); + + $this->echoBox("Number of anime items that need to be added to MAL: " . count($data['addToMAL'])); if ( ! empty($data['addToMAL'])) { - $this->echoBox("Adding missing list items to MAL"); + $this->echoBox("Adding missing anime list items to MAL"); $this->createMALAnimeListItems($data['addToMAL']); } - $this->echoBox('Number of items that need to be added to Kitsu: ' . count($data['addToKitsu'])); + $this->echoBox('Number of anime items that need to be added to Kitsu: ' . count($data['addToKitsu'])); if ( ! empty($data['addToKitsu'])) { - $this->echoBox("Adding missing list items to Kitsu"); + $this->echoBox("Adding missing anime list items to Kitsu"); $this->createKitusAnimeListItems($data['addToKitsu']); } } - public function getKitsuAnimeList() + public function syncManga() { - $count = $this->getKitsuAnimeListPageCount(); - $size = 100; - $pages = ceil($count / $size); + $malCount = count($this->malModel->getMangaList()); + $kitsuCount = $this->kitsuModel->getMangaListCount(); - $requests = []; + $this->echoBox("Number of MAL manga list items: {$malCount}"); + $this->echoBox("Number of Kitsu manga list items: {$kitsuCount}"); - // Set up requests - for ($i = 0; $i < $pages; $i++) + $data = $this->diffMangaLists(); + + $this->echoBox("Number of manga items that need to be added to MAL: " . count($data['addToMAL'])); + + if ( ! empty($data['addToMAL'])) { - $offset = $i * $size; - $requests[] = $this->kitsuModel->getPagedAnimeList($size, $offset); + $this->echoBox("Adding missing manga list items to MAL"); + $this->createMALMangaListItems($data['addToMAL']); } - $promiseArray = (new Client())->requestMulti($requests); + $this->echoBox('Number of manga items that need to be added to Kitsu: ' . count($data['addToKitsu'])); - $responses = wait(all($promiseArray)); - $output = []; - - foreach($responses as $response) + if ( ! empty($data['addToKitsu'])) { - $data = Json::decode($response->getBody()); - $output = array_merge_recursive($output, $data); + $this->echoBox("Adding missing manga list items to Kitsu"); + $this->createKitsuMangaListItems($data['addToKitsu']); } - - return $output; } - public function getMALAnimeList() - { - return $this->malModel->getFullList(); - } - - public function filterMappings(array $includes): array + public function filterMappings(array $includes, string $type = 'anime'): array { $output = []; foreach($includes as $id => $mapping) { - if ($mapping['externalSite'] === 'myanimelist/anime') + if ($mapping['externalSite'] === "myanimelist/{$type}") { $output[$id] = $mapping; } @@ -130,7 +138,7 @@ class SyncKitsuWithMal extends BaseCommand { public function formatMALAnimeList() { - $orig = $this->getMALAnimeList(); + $orig = $this->malModel->getAnimeList(); $output = []; foreach($orig as $item) @@ -156,6 +164,35 @@ class SyncKitsuWithMal extends BaseCommand { return $output; } + public function formatMALMangaList() + { + $orig = $this->malModel->getMangaList(); + $output = []; + + foreach($orig as $item) + { + $output[$item['series_mangadb_id']] = [ + 'id' => $item['series_mangadb_id'], + 'data' => [ + 'my_status' => $item['my_status'], + 'status' => MangaReadingStatus::MAL_TO_KITSU[$item['my_status']], + 'progress' => $item['my_read_chapters'], + 'reconsuming' => (bool) $item['my_rereadingg'], + /* 'reconsumeCount' => array_key_exists('times_rewatched', $item) + ? $item['times_rewatched'] + : 0, */ + // 'notes' => , + 'rating' => $item['my_score'] / 2, + 'updatedAt' => (new \DateTime()) + ->setTimestamp((int)$item['my_last_updated']) + ->format(\DateTime::W3C), + ] + ]; + } + + return $output; + } + public function filterKitsuAnimeList() { $data = $this->kitsuModel->getFullAnimeList(); @@ -194,9 +231,84 @@ class SyncKitsuWithMal extends BaseCommand { return $output; } - public function getKitsuAnimeListPageCount() + public function filterKitsuMangaList() { - return $this->kitsuModel->getAnimeListCount(); + $data = $this->kitsuModel->getFullMangaList(); + $includes = JsonAPI::organizeIncludes($data['included']); + $includes['mappings'] = $this->filterMappings($includes['mappings'], 'manga'); + + $output = []; + + foreach($data['data'] as $listItem) + { + $mangaId = $listItem['relationships']['manga']['data']['id']; + $potentialMappings = $includes['manga'][$mangaId]['relationships']['mappings']; + $malId = NULL; + + foreach ($potentialMappings as $mappingId) + { + if (array_key_exists($mappingId, $includes['mappings'])) + { + $malId = $includes['mappings'][$mappingId]['externalId']; + } + } + + // Skip to the next item if there isn't a MAL ID + if (is_null($malId)) + { + continue; + } + + $output[$listItem['id']] = [ + 'id' => $listItem['id'], + 'malId' => $malId, + 'data' => $listItem['attributes'], + ]; + } + + return $output; + } + + public function diffMangaLists() + { + $kitsuList = $this->filterKitsuMangaList(); + $malList = $this->formatMALMangaList(); + + $itemsToAddToMAL = []; + $itemsToAddToKitsu = []; + + $malIds = array_column($malList, 'id'); + $kitsuMalIds = array_column($kitsuList, 'malId'); + $missingMalIds = array_diff($malIds, $kitsuMalIds); + + foreach($missingMalIds as $mid) + { + $itemsToAddToKitsu[] = array_merge($malList[$mid]['data'], [ + 'id' => $this->kitsuModel->getKitsuIdFromMALId($mid, 'manga'), + 'type' => 'manga' + ]); + } + + foreach($kitsuList as $kitsuItem) + { + if (in_array($kitsuItem['malId'], $malIds)) + { + // Eventually, compare the list entries, and determine which + // needs to be updated + continue; + } + + // Looks like this item only exists on Kitsu + $itemsToAddToMAL[] = [ + 'mal_id' => $kitsuItem['malId'], + 'data' => $kitsuItem['data'] + ]; + } + + return [ + 'addToMAL' => $itemsToAddToMAL, + 'addToKitsu' => $itemsToAddToKitsu + ]; } public function diffAnimeLists() @@ -256,29 +368,79 @@ class SyncKitsuWithMal extends BaseCommand { ]; } - public function createKitusAnimeListItems($itemsToAdd) + public function createKitsuMangaListItems($itemsToAdd) { - $requests = []; + $requester = new ParallelAPIRequest(); foreach($itemsToAdd as $item) { - $requests[] = $this->kitsuModel->createListItem($item); + $requester->addRequest($this->kitsuModel->createListItem($item)); } - $promiseArray = (new Client())->requestMulti($requests); - - $responses = wait(all($promiseArray)); + $responses = $requester->makeRequests(); foreach($responses as $key => $response) { $id = $itemsToAdd[$key]['id']; if ($response->getStatus() === 201) { - $this->echoBox("Successfully create list item with id: {$id}"); + $this->echoBox("Successfully created Kitsu manga list item with id: {$id}"); } else { echo $response->getBody(); - $this->echoBox("Failed to create list item with id: {$id}"); + $this->echoBox("Failed to create Kitsu manga list item with id: {$id}"); + } + } + } + + public function createMALMangaListItems($itemsToAdd) + { + $transformer = new MLT(); + $requester = new ParallelAPIRequest(); + + foreach($itemsToAdd as $item) + { + $data = $transformer->untransform($item); + $requester->addRequest($this->malModel->createFullListItem($data, 'manga')); + } + + $responses = $requester->makeRequests(); + + foreach($responses as $key => $response) + { + $id = $itemsToAdd[$key]['mal_id']; + if ($response->getBody() === 'Created') + { + $this->echoBox("Successfully created MAL manga list item with id: {$id}"); + } + else + { + $this->echoBox("Failed to create MAL manga list item with id: {$id}"); + } + } + } + + public function createKitusAnimeListItems($itemsToAdd) + { + $requester = new ParallelAPIRequest(); + foreach($itemsToAdd as $item) + { + $requester->addRequest($this->kitsuModel->createListItem($item)); + } + + $responses = $requester->makeRequests(); + + foreach($responses as $key => $response) + { + $id = $itemsToAdd[$key]['id']; + if ($response->getStatus() === 201) + { + $this->echoBox("Successfully created Kitsu anime list item with id: {$id}"); + } + else + { + echo $response->getBody(); + $this->echoBox("Failed to create Kitsu anime list item with id: {$id}"); } } } @@ -286,28 +448,26 @@ class SyncKitsuWithMal extends BaseCommand { public function createMALAnimeListItems($itemsToAdd) { $transformer = new ALT(); - $requests = []; + $requester = new ParallelAPIRequest(); foreach($itemsToAdd as $item) { $data = $transformer->untransform($item); - $requests[] = $this->malModel->createFullListItem($data); + $requester->addRequest($this->malModel->createFullListItem($data)); } - $promiseArray = (new Client())->requestMulti($requests); - - $responses = wait(all($promiseArray)); + $responses = $requester->makeRequests(); foreach($responses as $key => $response) { $id = $itemsToAdd[$key]['mal_id']; if ($response->getBody() === 'Created') { - $this->echoBox("Successfully create list item with id: {$id}"); + $this->echoBox("Successfully created MAL anime list item with id: {$id}"); } else { - $this->echoBox("Failed to create list item with id: {$id}"); + $this->echoBox("Failed to create MAL anime list item with id: {$id}"); } } } diff --git a/src/Model/Anime.php b/src/Model/Anime.php index 5fc2935c..eccc2b3f 100644 --- a/src/Model/Anime.php +++ b/src/Model/Anime.php @@ -59,7 +59,7 @@ class Anime extends API { * @param string $status * @return array */ - public function getList($status) + public function getList($status): array { $data = $this->kitsuModel->getAnimeList($status); $this->sortByName($data, 'anime'); @@ -72,7 +72,12 @@ class Anime extends API { return $output; } - public function getAllLists() + /** + * Get data for the 'all' anime page + * + * @return array + */ + public function getAllLists(): array { $data = $this->kitsuModel->getFullOrganizedAnimeList(); @@ -90,7 +95,7 @@ class Anime extends API { * @param string $slug * @return array */ - public function getAnime($slug) + public function getAnime(string $slug): array { return $this->kitsuModel->getAnime($slug); } @@ -101,7 +106,7 @@ class Anime extends API { * @param string $animeId * @return array */ - public function getAnimeById($animeId) + public function getAnimeById(string $animeId): array { return $this->kitsuModel->getAnimeById($animeId); } @@ -112,7 +117,7 @@ class Anime extends API { * @param string $name * @return array */ - public function search($name) + public function search(string $name): array { return $this->kitsuModel->search('anime', $name); } From 3bb4f32bdb4950f935cc30af91e161758349c60c Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Wed, 29 Mar 2017 13:29:03 -0400 Subject: [PATCH 06/21] Simultaneously update kitsu and MAL manga list item --- app/views/manga/cover.php | 2 +- app/views/manga/edit.php | 2 + app/views/manga/list.php | 4 + src/API/Kitsu/Model.php | 13 +++- .../Transformer/MangaListTransformer.php | 43 ++++++++--- src/API/MAL/Model.php | 31 +++++--- src/Model/Manga.php | 77 +++++++++++++++---- 7 files changed, 131 insertions(+), 41 deletions(-) diff --git a/app/views/manga/cover.php b/app/views/manga/cover.php index 7f9ebdbd..9ed41fdd 100644 --- a/app/views/manga/cover.php +++ b/app/views/manga/cover.php @@ -10,7 +10,7 @@

html($name) ?>

-
+
isAuthenticated()): ?>
- -
+ \ No newline at end of file diff --git a/app/views/character.php b/app/views/character.php index 668a99ff..41a63087 100644 --- a/app/views/character.php +++ b/app/views/character.php @@ -1,7 +1,7 @@
- +

diff --git a/app/views/manga/details.php b/app/views/manga/details.php index c883706e..a4dbf48d 100644 --- a/app/views/manga/details.php +++ b/app/views/manga/details.php @@ -35,26 +35,25 @@

-
+ 0): ?>

Characters

-
+
-
+
+ -
-
- +
\ No newline at end of file diff --git a/app/views/me.php b/app/views/me.php index fc08198b..24c6b8f7 100644 --- a/app/views/me.php +++ b/app/views/me.php @@ -1,4 +1,5 @@ -
+ +

@@ -54,29 +55,73 @@
html($attributes['bio']) ?>
-

Favorites:

-
-

Characters

-
+

Favorite Characters

+
-
+
+ -
+
+ + +

Favorite Anime

+
+ +
+ generate('anime.details', ['id' => $anime['slug']]); + $titles = Kitsu::filterTitles($anime); + ?> + + + + +
+ +
+ + +

Favorite Manga

+
+ +
+ generate('manga.details', ['id' => $manga['slug']]); + $titles = Kitsu::filterTitles($manga); + ?> + + + + +
+
+ +
\ No newline at end of file diff --git a/public/css/base.css b/public/css/base.css index de39c8b9..a003688e 100644 --- a/public/css/base.css +++ b/public/css/base.css @@ -1025,7 +1025,7 @@ a:hover, a:active { Base list styles ------------------------------------------------------------------------------*/ -.media { +.media, .character, .small_character { position:relative; vertical-align:top; display:inline-block; @@ -1035,7 +1035,9 @@ a:hover, a:active { margin:0.25em 0.125em; } -.media > img { +.media > img, +.character > img, +.small_character > img { width: 100%; } @@ -1076,7 +1078,9 @@ a:hover, a:active { top: 0; } -.media:hover > .name, +.small_character:hover > .name, + .character:hover > .name, + .media:hover > .name, .media:hover > .media_metadata > div, .media:hover > .medium_metadata > div, .media:hover > .table .row @@ -1094,7 +1098,11 @@ a:hover, a:active { display:block; } -.media > .name a, +.small_character > .name a, + .small_character > .name a small, + .character > .name a, + .character > .name a small, + .media > .name a, .media > .name a small { background:none; @@ -1263,7 +1271,7 @@ a:hover, a:active { .details .cover { display: block; width: 284px; - height: 402px; + /* height: 402px; */ } .details h2 { @@ -1298,12 +1306,56 @@ a:hover, a:active { text-align:left; } +.character, +.small_character { + background: rgba(0, 0, 0, .5); + width: 225px; + height: 350px; + vertical-align: middle; + white-space: nowrap; +} + +.small_character a { + display:inline-block; + width: 100%; + height: 100%; + } + +.small_character .name, + .character .name { + position: absolute; + bottom: 0; + left: 0; + z-index: 10; + } + +.small_character img, + .character img { + position: relative; + top: 50%; + -webkit-transform: translateY(-50%); + transform: translateY(-50%); + z-index: 5; + width: 100%; + } + /* ---------------------------------------------------------------------------- User page styles -----------------------------------------------------------------------------*/ -.small_character img { - max-width: 300px; +.small_character { + width: 160px; + height: 250px; +} + +.user-page .media-wrap { + text-align: left; +} + +.media a { + display: inline-block; + width: 100%; + height: 100%; } /* ---------------------------------------------------------------------------- diff --git a/public/css/base.myth.css b/public/css/base.myth.css index 679eb3df..60fae728 100644 --- a/public/css/base.myth.css +++ b/public/css/base.myth.css @@ -297,7 +297,7 @@ a:hover, a:active { Base list styles ------------------------------------------------------------------------------*/ -.media { +.media, .character, .small_character { position:relative; vertical-align:top; display:inline-block; @@ -307,7 +307,9 @@ a:hover, a:active { margin: var(--normal-padding); } -.media > img { +.media > img, +.character > img, +.small_character > img { width: 100%; } @@ -347,7 +349,8 @@ a:hover, a:active { position:absolute; top: 0; } - + .small_character:hover > .name, + .character:hover > .name, .media:hover > .name, .media:hover > .media_metadata > div, .media:hover > .medium_metadata > div, @@ -364,6 +367,10 @@ a:hover, a:active { display:block; } + .small_character > .name a, + .small_character > .name a small, + .character > .name a, + .character > .name a small, .media > .name a, .media > .name a small { @@ -521,7 +528,7 @@ a:hover, a:active { .details .cover { display: block; width: 284px; - height: 402px; + /* height: 402px; */ } .details h2 { @@ -552,13 +559,54 @@ a:hover, a:active { text-align:left; } +.character, +.small_character { + background: rgba(0,0,0,0.5); + width: 225px; + height: 350px; + vertical-align: middle; + white-space: nowrap; +} + .small_character a { + display:inline-block; + width: 100%; + height: 100%; + } + + .small_character .name, + .character .name { + position: absolute; + bottom: 0; + left: 0; + z-index: 10; + } + + .small_character img, + .character img { + position: relative; + top: 50%; + transform: translateY(-50%); + z-index: 5; + width: 100%; + } + /* ---------------------------------------------------------------------------- User page styles -----------------------------------------------------------------------------*/ -.small_character img { - max-width: 300px; +.small_character { + width: 160px; + height: 250px; } +.user-page .media-wrap { + text-align: left; +} + +.media a { + display: inline-block; + width: 100%; + height: 100%; +} /* ---------------------------------------------------------------------------- Viewport-based styles diff --git a/src/API/Kitsu/Model.php b/src/API/Kitsu/Model.php index cc7f80ea..db34552a 100644 --- a/src/API/Kitsu/Model.php +++ b/src/API/Kitsu/Model.php @@ -433,6 +433,12 @@ class Model { ] ]; $data = $this->getRequest("anime/{$kitsuAnimeId}", $options); + + if ( ! array_key_exists('included', $data)) + { + return NULL; + } + $mappings = array_column($data['included'], 'attributes'); foreach($mappings as $map) diff --git a/src/Controller.php b/src/Controller.php index 21aea39b..5d655914 100644 --- a/src/Controller.php +++ b/src/Controller.php @@ -284,6 +284,17 @@ class Controller { ], NULL, $httpCode); } + /** + * Redirect to the default controller/url from an empty path + * + * @return void + */ + public function redirectToDefaultRoute() + { + $defaultType = $this->config->get(['routes', 'route_config', 'default_list']) ?? 'anime'; + $this->redirect($this->urlGenerator->defaultUrl($defaultType), 303); + } + /** * Set a session flash variable to display a message on * next page load diff --git a/src/Controller/Index.php b/src/Controller/Index.php index 9400e954..b9a9f08f 100644 --- a/src/Controller/Index.php +++ b/src/Controller/Index.php @@ -113,17 +113,6 @@ class Index extends BaseController { ]); } - /** - * Redirect to the default controller/url from an empty path - * - * @return void - */ - public function redirectToDefaultRoute() - { - $defaultType = $this->config->get(['routes', 'route_config', 'default_list']); - $this->redirect($this->urlGenerator->defaultUrl($defaultType), 303); - } - private function organizeFavorites(array $rawfavorites): array { // return $rawfavorites; From 9f4160315291bb766f734f7cad959e533eacee9e Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Fri, 31 Mar 2017 17:01:53 -0400 Subject: [PATCH 21/21] small tweak to user page --- app/views/me.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/views/me.php b/app/views/me.php index 24c6b8f7..f0f26405 100644 --- a/app/views/me.php +++ b/app/views/me.php @@ -2,8 +2,15 @@
-

- +
+

+ + + +

+ +


@@ -120,8 +127,6 @@ - - \ No newline at end of file