From 1eecff017815ae1b1f6ae5c4acb9373772053920 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 13 Jan 2017 16:53:56 -0500 Subject: [PATCH] Streaming links, caching, and more MAL integration --- app/bootstrap.php | 21 ++- src/API/CacheTrait.php | 63 ++++++++ src/API/Kitsu.php | 139 ++++++++++-------- src/API/Kitsu/KitsuModel.php | 60 +++++--- src/API/Kitsu/ListItem.php | 3 +- .../Transformer/AnimeListTransformer.php | 23 +-- .../Kitsu/Transformer/AnimeTransformer.php | 9 +- src/API/MAL/Auth.php | 108 -------------- src/API/MAL/ListItem.php | 3 +- src/API/MAL/Model.php | 29 +++- 10 files changed, 248 insertions(+), 210 deletions(-) create mode 100644 src/API/CacheTrait.php delete mode 100644 src/API/MAL/Auth.php diff --git a/app/bootstrap.php b/app/bootstrap.php index 8f6dd9f4..7a0454ef 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -19,9 +19,15 @@ namespace Aviat\AnimeClient; use Aura\Html\HelperLocatorFactory; use Aura\Router\RouterContainer; use Aura\Session\SessionFactory; -use Aviat\AnimeClient\API\Kitsu\Auth as KitsuAuth; -use Aviat\AnimeClient\API\Kitsu\ListItem as KitsuListItem; -use Aviat\AnimeClient\API\Kitsu\KitsuModel; +use Aviat\AnimeClient\API\Kitsu\{ + Auth as KitsuAuth, + ListItem as KitsuListItem, + KitsuModel +}; +use Aviat\AnimeClient\API\MAL\{ + ListItem as MALListItem, + Model as MALModel +}; use Aviat\AnimeClient\Model; use Aviat\Banker\Pool; use Aviat\Ion\Config; @@ -111,6 +117,15 @@ return function(array $config_array = []) { $listItem->setContainer($container); $model = new KitsuModel($listItem); $model->setContainer($container); + $cache = $container->get('cache'); + $model->setCache($cache); + return $model; + }); + $container->set('mal-model', function($container) { + $listItem = new MALListItem(); + $listItem->setContainer($container); + $model = new MALModel($listItem); + $model->setContainer($container); return $model; }); $container->set('api-model', function($container) { diff --git a/src/API/CacheTrait.php b/src/API/CacheTrait.php new file mode 100644 index 00000000..6ce11410 --- /dev/null +++ b/src/API/CacheTrait.php @@ -0,0 +1,63 @@ + + * @copyright 2015 - 2017 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.0 + * @link https://github.com/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\API; + +use Aviat\Banker\Pool; +use Aviat\Ion\Di\ContainerAware; + +/** + * Helper methods for dealing with the Cache + */ +trait CacheTrait { + + /** + * @var Aviat\Banker\Pool + */ + protected $cache; + + /** + * Inject the cache object + * + * @param Pool $cache + * @return $this + */ + public function setCache(Pool $cache): self + { + $this->cache = $cache; + return $this; + } + + /** + * Generate a hash as a cache key from the current method call + * + * @param object $object + * @param string $method + * @param array $args + * @return string + */ + public function getHashForMethodCall($object, string $method, array $args = []): string + { + $classname = get_class($object); + $keyObj = [ + 'class' => $classname, + 'method' => $method, + 'args' => $args, + ]; + $hash = sha1(json_encode($keyObj)); + return $hash; + } +} \ No newline at end of file diff --git a/src/API/Kitsu.php b/src/API/Kitsu.php index 9e0fc161..6a17f941 100644 --- a/src/API/Kitsu.php +++ b/src/API/Kitsu.php @@ -24,7 +24,7 @@ use Aviat\AnimeClient\API\Kitsu\Enum\{ use DateTimeImmutable; /** - * Constants and mappings for the Kitsu API + * Data massaging helpers for the Kitsu API */ class Kitsu { const AUTH_URL = 'https://kitsu.io/api/oauth/token'; @@ -45,6 +45,11 @@ class Kitsu { ]; } + /** + * Map of Kitsu Manga status to label for select menus + * + * @return array + */ public static function getStatusToMangaSelectMap() { return [ @@ -84,6 +89,78 @@ class Kitsu { return AnimeAiringStatus::NOT_YET_AIRED; } } + + /** + * Get the name and logo for the streaming service of the current link + * + * @param string $hostname + * @return array + */ + protected static function getServiceMetaData(string $hostname = null): array + { + switch($hostname) + { + case 'www.crunchyroll.com': + return [ + 'name' => 'Crunchyroll', + 'link' => true, + 'logo' => '' + ]; + + case 'www.funimation.com': + return [ + 'name' => 'Funimation', + 'link' => true, + 'logo' => '' + ]; + + case 'www.hulu.com': + return [ + 'name' => 'Hulu', + 'link' => true, + 'logo' => '' + ]; + + // Default to Netflix, because the API links are broken, + // and there's no other real identifier for Netflix + default: + return [ + 'name' => 'Netflix', + 'link' => false, + 'logo' => '' + ]; + } + } + + /** + * Reorganize streaming links + * + * @param array $included + * @return array + */ + public static function parseStreamingLinks(array $included): array + { + if ( ! array_key_exists('streamingLinks', $included)) + { + return []; + } + + $links = []; + + foreach ($included['streamingLinks'] as $streamingLink) + { + $host = parse_url($streamingLink['url'], \PHP_URL_HOST); + + $links[] = [ + 'meta' => static::getServiceMetaData($host), + 'link' => $streamingLink['url'], + 'subs' => $streamingLink['subs'], + 'dubs' => $streamingLink['dubs'] + ]; + } + + return $links; + } /** * Filter out duplicate and very similar names from @@ -110,66 +187,6 @@ class Kitsu { return $valid; } - /** - * Reorganizes 'included' data to be keyed by - * type => [ - * id => data/attributes, - * ] - * - * @param array $includes - * @return array - */ - public static function organizeIncludes(array $includes): array - { - $organized = []; - - foreach ($includes as $item) - { - $type = $item['type']; - $id = $item['id']; - $organized[$type] = $organized[$type] ?? []; - $organized[$type][$id] = $item['attributes']; - - if (array_key_exists('relationships', $item)) - { - $organized[$type][$id]['relationships'] = self::organizeRelationships($item['relationships']); - } - } - - return $organized; - } - - /** - * Reorganize relationship mappings to make them simpler to use - * - * Remove verbose structure, and just map: - * type => [ idArray ] - * - * @param array $relationships - * @return array - */ - public static function organizeRelationships(array $relationships): array - { - $organized = []; - - foreach($relationships as $key => $data) - { - if ( ! array_key_exists('data', $data)) - { - continue; - } - - $organized[$key] = $organized[$key] ?? []; - - foreach ($data['data'] as $item) - { - $organized[$key][] = $item['id']; - } - } - - return $organized; - } - /** * Determine if an alternate title is unique enough to list * diff --git a/src/API/Kitsu/KitsuModel.php b/src/API/Kitsu/KitsuModel.php index e1adfa08..d50cacb6 100644 --- a/src/API/Kitsu/KitsuModel.php +++ b/src/API/Kitsu/KitsuModel.php @@ -16,6 +16,7 @@ namespace Aviat\AnimeClient\API\Kitsu; +use Aviat\AnimeClient\API\CacheTrait; use Aviat\AnimeClient\API\JsonAPI; use Aviat\AnimeClient\API\Kitsu as K; use Aviat\AnimeClient\API\Kitsu\Transformer\{ @@ -29,6 +30,7 @@ use GuzzleHttp\Exception\ClientException; * Kitsu API Model */ class KitsuModel { + use CacheTrait; use ContainerAware; use KitsuTrait; @@ -60,6 +62,7 @@ class KitsuModel { * @var MangaListTransformer */ protected $mangaListTransformer; + /** * KitsuModel constructor. @@ -131,6 +134,7 @@ class KitsuModel { */ public function getAnime(string $animeId): array { + // @TODO catch non-existent anime $baseData = $this->getRawMediaData('anime', $animeId); return $this->animeTransformer->transform($baseData); } @@ -147,7 +151,13 @@ class KitsuModel { return $this->mangaTransformer->transform($baseData); } - public function getAnimeList($status): array + /** + * Get the anime list for the configured user + * + * @param string $status - The watching status to filter the list with + * @return array + */ + public function getAnimeList(string $status): array { $options = [ 'query' => [ @@ -156,26 +166,33 @@ class KitsuModel { 'media_type' => 'Anime', 'status' => $status, ], - 'include' => 'media,media.genres,media.mappings', + 'include' => 'media,media.genres,media.mappings,anime.streamingLinks', 'page' => [ 'offset' => 0, 'limit' => 500 - ], - 'sort' => '-updated_at' + ] ] ]; - - $data = $this->getRequest('library-entries', $options); - $included = JsonAPI::organizeIncludes($data['included']); - $included = JsonAPI::inlineIncludedRelationships($included, 'anime'); - - foreach($data['data'] as $i => &$item) + + $cacheItem = $this->cache->getItem($this->getHashForMethodCall($this, __METHOD__, $options)); + + if ( ! $cacheItem->isHit()) { - $item['included'] =& $included; - } - $transformed = $this->animeListTransformer->transformCollection($data['data']); + $data = $this->getRequest('library-entries', $options); + $included = JsonAPI::organizeIncludes($data['included']); + $included = JsonAPI::inlineIncludedRelationships($included, 'anime'); - return $transformed; + foreach($data['data'] as $i => &$item) + { + $item['included'] = $included; + } + $transformed = $this->animeListTransformer->transformCollection($data['data']); + + $cacheItem->set($transformed); + $cacheItem->save(); + } + + return $cacheItem->get(); } public function getMangaList($status): array @@ -242,19 +259,24 @@ class KitsuModel { public function getListItem(string $listId): array { $baseData = $this->listItem->get($listId); + $included = JsonAPI::organizeIncludes($baseData['included']); - switch ($baseData['included'][0]['type']) + + switch (TRUE) { - case 'anime': - $baseData['data']['anime'] = $baseData['included'][0]; + case in_array('anime', array_keys($included)): + $included = JsonAPI::inlineIncludedRelationships($included, 'anime'); + $baseData['data']['included'] = $included; return $this->animeListTransformer->transform($baseData['data']); - case 'manga': + case in_array('manga', array_keys($included)): + $included = JsonAPI::inlineIncludedRelationships($included, 'manga'); + $baseData['data']['included'] = $included; $baseData['data']['manga'] = $baseData['included'][0]; return $this->mangaListTransformer->transform($baseData['data']); default: - return $baseData['data']['attributes']; + return $baseData['data']; } } diff --git a/src/API/Kitsu/ListItem.php b/src/API/Kitsu/ListItem.php index f21101f8..57787f15 100644 --- a/src/API/Kitsu/ListItem.php +++ b/src/API/Kitsu/ListItem.php @@ -37,7 +37,6 @@ class ListItem extends AbstractListItem { public function create(array $data): bool { -/*?>
getResponse('POST', 'library-entries', [ 'body' => Json::encode([ 'data' => [ @@ -77,7 +76,7 @@ class ListItem extends AbstractListItem { { return $this->getRequest("library-entries/{$id}", [ 'query' => [ - 'include' => 'media' + 'include' => 'media,media.genres,media.mappings' ] ]); } diff --git a/src/API/Kitsu/Transformer/AnimeListTransformer.php b/src/API/Kitsu/Transformer/AnimeListTransformer.php index 6f1817cf..e153a2a1 100644 --- a/src/API/Kitsu/Transformer/AnimeListTransformer.php +++ b/src/API/Kitsu/Transformer/AnimeListTransformer.php @@ -33,28 +33,30 @@ class AnimeListTransformer extends AbstractTransformer { */ public function transform($item) { - $included = $item['included'] ?? []; +/* ?>
$item['id'], 'mal_id' => $MALid, 'episodes' => [ - 'watched' => $item['attributes']['progress'], + 'watched' => (int) $item['attributes']['progress'] !== '0' + ? (int) $item['attributes']['progress'] + : '-', 'total' => $total_episodes, 'length' => $anime['episodeLength'], ], @@ -79,6 +83,7 @@ class AnimeListTransformer extends AbstractTransformer { 'type' => $this->string($anime['showType'])->upperCaseFirst()->__toString(), 'image' => $anime['posterImage']['small'], 'genres' => $genres, + 'streaming_links' => Kitsu::parseStreamingLinks($included), ], 'watching_status' => $item['attributes']['status'], 'notes' => $item['attributes']['notes'], diff --git a/src/API/Kitsu/Transformer/AnimeTransformer.php b/src/API/Kitsu/Transformer/AnimeTransformer.php index e4339886..66f344f1 100644 --- a/src/API/Kitsu/Transformer/AnimeTransformer.php +++ b/src/API/Kitsu/Transformer/AnimeTransformer.php @@ -36,12 +36,15 @@ class AnimeTransformer extends AbstractTransformer { $item['included'] = JsonAPI::organizeIncludes($item['included']); $item['genres'] = array_column($item['included']['genres'], 'name') ?? []; sort($item['genres']); + + $titles = Kitsu::filterTitles($item); return [ - 'titles' => Kitsu::filterTitles($item), + 'title' => $titles[0], + 'titles' => $titles, 'status' => Kitsu::getAiringStatus($item['startDate'], $item['endDate']), 'cover_image' => $item['posterImage']['small'], - 'show_type' => $item['showType'], + 'show_type' => $this->string($item['showType'])->upperCaseFirst()->__toString(), 'episode_count' => $item['episodeCount'], 'episode_length' => $item['episodeLength'], 'synopsis' => $item['synopsis'], @@ -49,7 +52,7 @@ class AnimeTransformer extends AbstractTransformer { 'age_rating_guide' => $item['ageRatingGuide'], 'url' => "https://kitsu.io/anime/{$item['slug']}", 'genres' => $item['genres'], - 'included' => $item['included'] + 'streaming_links' => Kitsu::parseStreamingLinks($item['included']) ]; } } \ No newline at end of file diff --git a/src/API/MAL/Auth.php b/src/API/MAL/Auth.php deleted file mode 100644 index 4f03508c..00000000 --- a/src/API/MAL/Auth.php +++ /dev/null @@ -1,108 +0,0 @@ - - * @copyright 2015 - 2017 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://github.com/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient\API\MAL; - -use Aviat\AnimeClient\AnimeClient; -use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; - -/** - * MAL API Authentication - */ -class Auth { - - use \Aviat\Ion\Di\ContainerAware; - - /** - * Anime API Model - * - * @var \Aviat\AnimeClient\Model\API - */ - protected $model; - - /** - * Session object - * - * @var Aura\Session\Segment - */ - protected $segment; - - /** - * Constructor - * - * @param ContainerInterface $container - */ - public function __construct(ContainerInterface $container) - { - $this->setContainer($container); - $this->segment = $container->get('session') - ->getSegment(AnimeClient::SESSION_SEGMENT); - $this->model = $container->get('api-model'); - } - - /** - * Make the appropriate authentication call, - * and save the resulting auth token if successful - * - * @param string $password - * @return boolean - */ - public function authenticate($password) - { - $username = $this->container->get('config') - ->get('hummingbird_username'); - $auth_token = $this->model->authenticate($username, $password); - - if (FALSE !== $auth_token) - { - $this->segment->set('auth_token', $auth_token); - return TRUE; - } - - return FALSE; - } - - /** - * Check whether the current user is authenticated - * - * @return boolean - */ - public function is_authenticated() - { - return ($this->get_auth_token() !== FALSE); - } - - /** - * Clear authentication values - * - * @return void - */ - public function logout() - { - $this->segment->clear(); - } - - /** - * Retrieve the authentication token from the session - * - * @return string|false - */ - public function get_auth_token() - { - return $this->segment->get('auth_token', FALSE); - } -} -// End of KitsuAuth.php \ No newline at end of file diff --git a/src/API/MAL/ListItem.php b/src/API/MAL/ListItem.php index d5506f9a..03a04473 100644 --- a/src/API/MAL/ListItem.php +++ b/src/API/MAL/ListItem.php @@ -24,6 +24,7 @@ use Aviat\Ion\Di\ContainerAware; */ class ListItem extends AbstractListItem { use ContainerAware; + use MALTrait; public function __construct() { @@ -47,6 +48,6 @@ class ListItem extends AbstractListItem { public function update(string $id, array $data): Response { - // @TODO implement + } } \ No newline at end of file diff --git a/src/API/MAL/Model.php b/src/API/MAL/Model.php index b7f0f0c9..a2861c88 100644 --- a/src/API/MAL/Model.php +++ b/src/API/MAL/Model.php @@ -17,29 +17,50 @@ namespace Aviat\AnimeClient\API\MAL; use Aviat\AnimeClient\API\MAL as M; +use Aviat\AnimeClient\API\MAL\{ + AnimeListTransformer, + ListItem +}; +use Aviat\AnimeClient\API\XML; use Aviat\Ion\Di\ContainerAware; /** * MyAnimeList API Model */ class Model { - use ContainerAware; use MALTrait; + /** + * @var AnimeListTransformer + */ + protected $animeListTransformer; + + /** + * KitsuModel constructor. + */ + public function __construct(ListItem $listItem) + { + // Set up Guzzle trait + $this->init(); + $this->animeListTransformer = new AnimeListTransformer(); + $this->listItem = $listItem; + } + public function createListItem(array $data): bool { - + return FALSE; } public function getListItem(string $listId): array { - + return []; } public function updateListItem(array $data) { - + $updateData = $this->animeListTransformer->transform($data['data']); + return $this->listItem->update($data['mal_id'], $updateData); } public function deleteListItem(string $id): bool