diff --git a/src/API/AbstractListItem.php b/src/API/AbstractListItem.php new file mode 100644 index 00000000..e5c1e524 --- /dev/null +++ b/src/API/AbstractListItem.php @@ -0,0 +1,21 @@ + + * @copyright 2015 - 2016 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; + +abstract class AbstractListItem implements ListItemInterface { + +} \ No newline at end of file diff --git a/src/API/Kitsu/Auth.php b/src/API/Kitsu/Auth.php new file mode 100644 index 00000000..90358a10 --- /dev/null +++ b/src/API/Kitsu/Auth.php @@ -0,0 +1,108 @@ + + * @copyright 2015 - 2016 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\Kitsu; + +use Aviat\AnimeClient\AnimeClient; +use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; + +/** + * Kitsu API Authentication + */ +class Auth { + + use ContainerAware; + + /** + * Anime API Model + * + * @var \Aviat\AnimeClient\API\Kitsu\Model + */ + 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('kitsu-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('kitsu_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/Kitsu/Enum/AnimeAiringStatus.php b/src/API/Kitsu/Enum/AnimeAiringStatus.php new file mode 100644 index 00000000..a7bf27e9 --- /dev/null +++ b/src/API/Kitsu/Enum/AnimeAiringStatus.php @@ -0,0 +1,29 @@ + + * @copyright 2015 - 2016 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\Kitsu\Enum; + +use Aviat\Ion\Enum as BaseEnum; + +/** + * Status of when anime is being/was/will be aired + */ +class AnimeAiringStatus extends BaseEnum { + const NOT_YET_AIRED = 'Not Yet Aired'; + const AIRING = 'Currently Airing'; + const FINISHED_AIRING = 'Finished Airing'; +} +// End of AnimeAiringStatus.php diff --git a/src/API/Kitsu/Enum/AnimeShowType.php b/src/API/Kitsu/Enum/AnimeShowType.php new file mode 100644 index 00000000..9fb37260 --- /dev/null +++ b/src/API/Kitsu/Enum/AnimeShowType.php @@ -0,0 +1,32 @@ + + * @copyright 2015 - 2016 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\Kitsu\Enum; + +use Aviat\Ion\Enum as BaseEnum; + +/** + * Type of Anime + */ +class AnimeShowType extends BaseEnum { + const TV = 'TV'; + const MOVIE = 'Movie'; + const OVA = 'OVA'; + const ONA = 'ONA'; + const SPECIAL = 'Special'; + const MUSIC = 'Music'; +} +// End of AnimeShowType.php diff --git a/src/API/Kitsu/Enum/AnimeWatchingStatus.php b/src/API/Kitsu/Enum/AnimeWatchingStatus.php new file mode 100644 index 00000000..7cfe3883 --- /dev/null +++ b/src/API/Kitsu/Enum/AnimeWatchingStatus.php @@ -0,0 +1,31 @@ + + * @copyright 2015 - 2016 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\Kitsu\Enum; + +use Aviat\Ion\Enum as BaseEnum; + +/** + * Possible values for watching status for the current anime + */ +class AnimeWatchingStatus extends BaseEnum { + const WATCHING = 'current'; + const PLAN_TO_WATCH = 'planned'; + const COMPLETED = 'completed'; + const ON_HOLD = 'on_hold'; + const DROPPED = 'dropped'; +} +// End of AnimeWatchingStatus.php diff --git a/src/API/Kitsu/Enum/MangaReadingStatus.php b/src/API/Kitsu/Enum/MangaReadingStatus.php new file mode 100644 index 00000000..288ffe74 --- /dev/null +++ b/src/API/Kitsu/Enum/MangaReadingStatus.php @@ -0,0 +1,31 @@ + + * @copyright 2015 - 2016 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\Kitsu\Enum; + +use Aviat\Ion\Enum as BaseEnum; + +/** + * Possible values for current reading status of manga + */ +class MangaReadingStatus extends BaseEnum { + const READING = 'Currently Reading'; + const PLAN_TO_READ = 'Plan to Read'; + const DROPPED = 'Dropped'; + const ON_HOLD = 'On Hold'; + const COMPLETED = 'Completed'; +} +// End of MangaReadingStatus.php diff --git a/src/API/Kitsu/Model.php b/src/API/Kitsu/Model.php new file mode 100644 index 00000000..99223a74 --- /dev/null +++ b/src/API/Kitsu/Model.php @@ -0,0 +1,68 @@ + + * @copyright 2015 - 2016 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\Kitsu; + +use Aviat\AnimeClient\Model\API; + +/** + * Kitsu API Model + */ +class Model extends API { + + const CLIENT_ID = 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd'; + const CLIENT_SECRET = '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151'; + + /** + * Base url for Kitsu API + */ + protected $baseUrl = 'https://kitsu.io/api/edge/'; + + /** + * Default settings for Guzzle + * @var array + */ + protected $connectionDefaults = []; + + /** + * 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->post('https://kitsu.io/api/oauth/token', [ + 'body' => http_build_query([ + 'grant_type' => 'password', + 'username' => $username, + 'password' => $password, + 'client_id' => self::CLIENT_ID, + 'client_secret' => self::CLIENT_SECRET + ]) + ]); + + $info = $response->getBody(); + + if (array_key_exists('access_token', $info)) { + // @TODO save token + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/src/API/Kitsu/Transformer/AnimeListTransformer.php b/src/API/Kitsu/Transformer/AnimeListTransformer.php new file mode 100644 index 00000000..e90c42e7 --- /dev/null +++ b/src/API/Kitsu/Transformer/AnimeListTransformer.php @@ -0,0 +1,149 @@ + + * @copyright 2015 - 2016 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\Kitsu\Transformer; + +use Aviat\Ion\Transformer\AbstractTransformer; + +/** + * Transformer for anime list + */ +class AnimeListTransformer extends AbstractTransformer { + + /** + * Convert raw api response to a more + * logical and workable structure + * + * @param array $item API library item + * @return array + */ + public function transform($item) + { +print_r($item); +die(); + $anime =& $item['anime']; + $genres = $this->linearizeGenres($item['anime']['genres']); + + $rating = NULL; + if ($item['rating']['type'] === 'advanced') + { + $rating = is_numeric($item['rating']['value']) + ? (int) 2 * $item['rating']['value'] + : '-'; + } + + $total_episodes = is_numeric($anime['episode_count']) + ? $anime['episode_count'] + : '-'; + + $alternate_title = NULL; + if (array_key_exists('alternate_title', $anime)) + { + // If the alternate title is very similar, or + // a subset of the main title, don't list the + // alternate title + $not_subset = stripos($anime['title'], $anime['alternate_title']) === FALSE; + $diff = levenshtein($anime['title'], $anime['alternate_title'] ?? ''); + if ($not_subset && $diff >= 5) + { + $alternate_title = $anime['alternate_title']; + } + } + + return [ + 'id' => $item['id'], + 'episodes' => [ + 'watched' => $item['episodes_watched'], + 'total' => $total_episodes, + 'length' => $anime['episode_length'], + ], + 'airing' => [ + 'status' => $anime['status'], + 'started' => $anime['started_airing'], + 'ended' => $anime['finished_airing'] + ], + 'anime' => [ + 'age_rating' => $anime['age_rating'], + 'title' => $anime['title'], + 'alternate_title' => $alternate_title, + 'slug' => $anime['slug'], + 'url' => $anime['url'], + 'type' => $anime['show_type'], + 'image' => $anime['cover_image'], + 'genres' => $genres, + ], + 'watching_status' => $item['status'], + 'notes' => $item['notes'], + 'rewatching' => (bool) $item['rewatching'], + 'rewatched' => $item['rewatched_times'], + 'user_rating' => $rating, + 'private' => (bool) $item['private'], + ]; + } + + /** + * Convert transformed data to + * api response format + * + * @param array $item Transformed library item + * @return array API library item + */ + public function untransform($item) + { + // Messy mapping of boolean values to their API string equivalents + $privacy = 'public'; + if (array_key_exists('private', $item) && $item['private']) + { + $privacy = 'private'; + } + + $rewatching = 'false'; + if (array_key_exists('rewatching', $item) && $item['rewatching']) + { + $rewatching = 'true'; + } + + return [ + 'id' => $item['id'], + 'status' => $item['watching_status'], + 'sane_rating_update' => $item['user_rating'] / 2, + 'rewatching' => $rewatching, + 'rewatched_times' => $item['rewatched'], + 'notes' => $item['notes'], + 'episodes_watched' => $item['episodes_watched'], + 'privacy' => $privacy + ]; + } + + /** + * Simplify structure of genre list + * + * @param array $rawGenres + * @return array + */ + protected function linearizeGenres(array $rawGenres): array + { + $genres = []; + + foreach ($rawGenres as $genre) + { + $genres[] = $genre['name']; + } + + return $genres; + } +} +// End of AnimeListTransformer.php \ No newline at end of file diff --git a/src/API/Kitsu/Transformer/MangaListTransformer.php b/src/API/Kitsu/Transformer/MangaListTransformer.php new file mode 100644 index 00000000..79d70182 --- /dev/null +++ b/src/API/Kitsu/Transformer/MangaListTransformer.php @@ -0,0 +1,121 @@ + + * @copyright 2015 - 2016 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\Kitsu\Transformer; + +use Aviat\Ion\Transformer\AbstractTransformer; + +/** + * Data transformation class for zippered Hummingbird manga + */ +class MangaListTransformer extends AbstractTransformer { + + use \Aviat\Ion\StringWrapper; + + /** + * Remap zipped anime data to a more logical form + * + * @param array $item manga entry item + * @return array + */ + public function transform($item) + { + $manga =& $item['manga']; + + $rating = (is_numeric($item['rating'])) + ? intval(2 * $item['rating']) + : '-'; + + $total_chapters = ($manga['chapter_count'] > 0) + ? $manga['chapter_count'] + : '-'; + + $total_volumes = ($manga['volume_count'] > 0) + ? $manga['volume_count'] + : '-'; + + $map = [ + 'id' => $item['id'], + 'chapters' => [ + 'read' => $item['chapters_read'], + 'total' => $total_chapters + ], + 'volumes' => [ + 'read' => $item['volumes_read'], + 'total' => $total_volumes + ], + 'manga' => [ + 'title' => $manga['romaji_title'], + 'alternate_title' => NULL, + 'slug' => $manga['id'], + 'url' => 'https://hummingbird.me/manga/' . $manga['id'], + 'type' => $manga['manga_type'], + 'image' => $manga['poster_image_thumb'], + 'genres' => $manga['genres'], + ], + 'reading_status' => $item['status'], + 'notes' => $item['notes'], + 'rereading' => (bool)$item['rereading'], + 'reread' => $item['reread_count'], + 'user_rating' => $rating, + ]; + + if (array_key_exists('english_title', $manga)) + { + $diff = levenshtein($manga['romaji_title'], $manga['english_title']); + + // If the titles are REALLY similar, don't bother showing both + if ($diff >= 5) + { + $map['manga']['alternate_title'] = $manga['english_title']; + } + } + + return $map; + } + + /** + * Untransform data to update the api + * + * @param array $item + * @return array + */ + public function untransform($item) + { + $rereading = (array_key_exists('rereading', $item)) && (bool)$item['rereading']; + + $map = [ + 'id' => $item['id'], + 'manga_id' => $item['manga_id'], + 'status' => $item['status'], + 'chapters_read' => (int)$item['chapters_read'], + 'volumes_read' => (int)$item['volumes_read'], + 'rereading' => $rereading, + 'reread_count' => (int)$item['reread_count'], + 'notes' => $item['notes'], + ]; + + if ($item['new_rating'] !== $item['old_rating'] && $item['new_rating'] !== "") + { + $map['rating'] = ($item['new_rating'] > 0) + ? $item['new_rating'] / 2 + : $item['old_rating'] / 2; + } + + return $map; + } +} +// End of MangaListTransformer.php \ No newline at end of file diff --git a/src/API/ListItemInterface.php b/src/API/ListItemInterface.php new file mode 100644 index 00000000..776f5c6f --- /dev/null +++ b/src/API/ListItemInterface.php @@ -0,0 +1,56 @@ + + * @copyright 2015 - 2016 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; + +/** + * Common interface for anime and manga list item CRUD + */ +interface ListItemInterface { + + /** + * Create a list item + * + * @param array $data - + * @return bool + */ + public function create(array $data): bool; + + /** + * Retrieve a list item + * + * @param string $id - The id of the list item + * @return array + */ + public function get(string $id): array; + + /** + * Update a list item + * + * @param string $id - The id of the list tiem to update + * @param array $data - The data with which to update the list itme + * @return bool + */ + public function update(string $id, array $data): bool; + + /** + * Delete a list item + * + * @param string $id - The id of the list item to delete + * @return bool + */ + public function delete(string $id): bool; +} \ No newline at end of file diff --git a/src/API/MAL/Auth.php b/src/API/MAL/Auth.php new file mode 100644 index 00000000..33752ff5 --- /dev/null +++ b/src/API/MAL/Auth.php @@ -0,0 +1,108 @@ + + * @copyright 2015 - 2016 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/Model.php b/src/API/MAL/Model.php new file mode 100644 index 00000000..459b7414 --- /dev/null +++ b/src/API/MAL/Model.php @@ -0,0 +1,63 @@ + + * @copyright 2015 - 2016 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\Kitsu; + +use Aviat\AnimeClient\Model\API; + +/** + * MyAnimeList API Model + */ +class Model extends API { + + /** + * Base url for Kitsu API + */ + protected $baseUrl = 'https://myanimelist.net/api/'; + + /** + * Default settings for Guzzle + * @var array + */ + protected $connectionDefaults = []; + + /** + * 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->post('account/', [ + 'body' => http_build_query([ + 'grant_type' => 'password', + 'username' => $username, + 'password' => $password + ]) + ]); + + $info = $response->getBody(); + + if (array_key_exists('access_token', $info)) { + // @TODO save token + return true; + } + + return false; + } +} \ No newline at end of file