From 9d84398ee728645dac8ad53a7ffcd626c80accf5 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Thu, 5 Jan 2017 22:24:45 -0500 Subject: [PATCH] Better handling of alternate titles, Airing Status and genres for anime list views --- app/views/anime/cover.php | 6 +- app/views/anime/details.php | 16 +- app/views/anime/edit.php | 8 +- app/views/anime/list.php | 22 +- app/views/manga/cover.php | 22 +- app/views/manga/edit.php | 19 +- app/views/manga/list.php | 14 +- index.php | 9 +- public/css/base.css | 4 +- public/css/base.myth.css | 4 +- src/API/Kitsu.php | 152 ++++++++++++- src/API/Kitsu/Auth.php | 214 +++++++++--------- src/API/Kitsu/KitsuModel.php | 22 +- src/API/Kitsu/KitsuTrait.php | 1 - .../Transformer/AnimeListTransformer.php | 44 ++-- .../Kitsu/Transformer/AnimeTransformer.php | 8 +- .../Transformer/MangaListTransformer.php | 8 +- src/AnimeClient.php | 1 - src/Controller/Anime.php | 2 +- src/Controller/Manga.php | 2 +- src/Model/API.php | 2 +- src/Model/Manga.php | 12 + 22 files changed, 379 insertions(+), 213 deletions(-) diff --git a/app/views/anime/cover.php b/app/views/anime/cover.php index c0fa5869..70443f36 100644 --- a/app/views/anime/cover.php +++ b/app/views/anime/cover.php @@ -18,8 +18,10 @@ img($item['anime']['image']); ?>
- html($item['anime']['title']) ?> - ({$item['anime']['alternate_title']})" : ""; ?> + + +
+
diff --git a/app/views/anime/details.php b/app/views/anime/details.php index b06eae01..0995fb65 100644 --- a/app/views/anime/details.php +++ b/app/views/anime/details.php @@ -5,10 +5,10 @@

- + - */ ?> + @@ -34,14 +34,10 @@
Airing Status
Show Type
-

- -

- - -

- - +

+ +

+

diff --git a/app/views/anime/edit.php b/app/views/anime/edit.php index d8d959bd..2e758bd4 100644 --- a/app/views/anime/edit.php +++ b/app/views/anime/edit.php @@ -6,10 +6,10 @@ -

html($item['anime']['title']) ?>

- -

html($item['anime']['alternate_title']) ?>

- +

html(array_shift($item['anime']['titles'])) ?>

+ +

html($title) ?>

+
diff --git a/app/views/anime/list.php b/app/views/anime/list.php index cafeb586..3f2471af 100644 --- a/app/views/anime/list.php +++ b/app/views/anime/list.php @@ -26,7 +26,7 @@ - is_authenticated()) continue; ?> + is_authenticated()) continue; ?> is_authenticated()): ?> @@ -35,36 +35,38 @@ - + - " . $item['anime']['alternate_title'] : "" ?> + +
+ - + / 10 - + Episodes:
 /  +

html($item['notes']) ?>

- + diff --git a/app/views/manga/cover.php b/app/views/manga/cover.php index 5bcacab6..496aa564 100644 --- a/app/views/manga/cover.php +++ b/app/views/manga/cover.php @@ -1,7 +1,7 @@
-is_authenticated()): ?> +is_authenticated()): ?> Add Item - +

There's nothing here!

@@ -11,26 +11,28 @@
-is_authenticated()): ?> +is_authenticated()): ?> - \ No newline at end of file + \ No newline at end of file diff --git a/app/views/manga/edit.php b/app/views/manga/edit.php index 7aabf1a9..a6eefb27 100644 --- a/app/views/manga/edit.php +++ b/app/views/manga/edit.php @@ -1,19 +1,18 @@ is_authenticated()): ?>

- Edit - + Edit

- + - + - + */ ?>
-

html($item['manga']['title']) ?>

- -

html($item['manga']['alternate_title']) ?>

- -
+

html(array_shift($item['manga']['titles'])) ?>

+ +

html($title) ?>

+ +
img($item['manga']['image']); ?> @@ -45,12 +44,12 @@ /
/
diff --git a/app/views/manga/list.php b/app/views/manga/list.php index dbf00076..7a44d851 100644 --- a/app/views/manga/list.php +++ b/app/views/manga/list.php @@ -10,9 +10,9 @@ - is_authenticated()): ?> + is_authenticated()): ?> - + @@ -23,16 +23,18 @@ - is_authenticated()): ?> + is_authenticated()): ?> - + diff --git a/index.php b/index.php index ca589e6f..fff3616a 100644 --- a/index.php +++ b/index.php @@ -53,12 +53,11 @@ $whoops = new Run(); $defaultHandler = new PrettyPageHandler(); $whoops->pushHandler($defaultHandler); -// Set up json handler for ajax errors -//$jsonHandler = new JsonResponseHandler(); -//$whoops->pushHandler($jsonHandler); - // Register as the error handler -$whoops->register(); +if (array_key_exists('whoops', $_GET)) +{ + $whoops->register(); +} // ----------------------------------------------------------------------------- // Dependency Injection setup diff --git a/public/css/base.css b/public/css/base.css index cd393a9b..b80a42b1 100644 --- a/public/css/base.css +++ b/public/css/base.css @@ -1085,7 +1085,9 @@ a:hover, a:active { display:block; } -.media > .name > a { +.media > .name a, + .media > .name a small + { background:none; color:#fff; text-shadow:1px 2px 1px rgba(0, 0, 0, .85); diff --git a/public/css/base.myth.css b/public/css/base.myth.css index 5c89e603..b91f8be5 100644 --- a/public/css/base.myth.css +++ b/public/css/base.myth.css @@ -354,7 +354,9 @@ a:hover, a:active { display:block; } - .media > .name > a { + .media > .name a, + .media > .name a small + { background:none; color:#fff; text-shadow: var(--shadow); diff --git a/src/API/Kitsu.php b/src/API/Kitsu.php index 9753e993..e89357c6 100644 --- a/src/API/Kitsu.php +++ b/src/API/Kitsu.php @@ -16,13 +16,20 @@ namespace Aviat\AnimeClient\API; -use Aviat\AnimeClient\API\Kitsu\Enum\AnimeWatchingStatus; +use Aviat\AnimeClient\API\Kitsu\Enum\{AnimeAiringStatus, AnimeWatchingStatus}; +use DateTimeImmutable; /** * Constants and mappings for the Kitsu API */ class Kitsu { + const AUTH_URL = 'https://kitsu.io/api/oauth/token'; + /** + * Map of Kitsu status to label for select menus + * + * @return array + */ public static function getStatusToSelectMap() { return [ @@ -33,4 +40,147 @@ class Kitsu { AnimeWatchingStatus::DROPPED => 'Dropped' ]; } + + /** + * Determine whether an anime is airing, finished airing, or has not yet aired + * + * @param string $startDate + * @param string $endDate + * @return string + */ + public static function getAiringStatus(string $startDate = null, string $endDate = null): string + { + $startAirDate = new DateTimeImmutable($startDate ?? 'tomorrow'); + $endAirDate = new DateTimeImmutable($endDate ?? 'tomorrow'); + $now = new DateTimeImmutable(); + + $isDoneAiring = $now > $endAirDate; + $isCurrentlyAiring = ($now > $startAirDate) && ! $isDoneAiring; + + switch (true) + { + case $isCurrentlyAiring: + return AnimeAiringStatus::AIRING; + + case $isDoneAiring: + return AnimeAiringStatus::FINISHED_AIRING; + + default: + return AnimeAiringStatus::NOT_YET_AIRED; + } + } + + /** + * Filter out duplicate and very similar names from + * + * @param array $data The 'attributes' section of the api data response + * @return array List of alternate titles + */ + public static function filterTitles(array $data): array + { + // The 'canonical' title is always returned + $valid = [$data['canonicalTitle']]; + + if (array_key_exists('titles', $data)) + { + foreach($data['titles'] as $alternateTitle) + { + if (self::titleIsUnique($alternateTitle, $valid)) + { + $valid[] = $alternateTitle; + } + } + } + + 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 + * + * @param string $title + * @param array $existingTitles + * @return bool + */ + private static function titleIsUnique(string $title = null, array $existingTitles): bool + { + if (empty($title)) + { + return false; + } + + foreach($existingTitles as $existing) + { + $isSubset = stripos($existing, $title) !== FALSE; + $diff = levenshtein($existing, $title); + $onlydifferentCase = (mb_strtolower($existing) === mb_strtolower($title)); + + if ($diff < 3 || $isSubset || $onlydifferentCase) + { + return false; + } + } + + return true; + } } \ No newline at end of file diff --git a/src/API/Kitsu/Auth.php b/src/API/Kitsu/Auth.php index e940d878..113d5fd8 100644 --- a/src/API/Kitsu/Auth.php +++ b/src/API/Kitsu/Auth.php @@ -1,108 +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) - { - $config = $this->container->get('config'); - $username = $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); - } -} + + * @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) + { + $config = $this->container->get('config'); + $username = $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/KitsuModel.php b/src/API/Kitsu/KitsuModel.php index f3a9f85f..2d8124f4 100644 --- a/src/API/Kitsu/KitsuModel.php +++ b/src/API/Kitsu/KitsuModel.php @@ -17,6 +17,7 @@ namespace Aviat\AnimeClient\API\Kitsu; use Aviat\AnimeClient\AnimeClient; +use Aviat\AnimeClient\API\Kitsu as K; use Aviat\AnimeClient\API\Kitsu\Transformer\{ AnimeTransformer, AnimeListTransformer, MangaTransformer, MangaListTransformer }; @@ -92,7 +93,7 @@ class KitsuModel { */ public function authenticate(string $username, string $password) { - $data = $this->postRequest(AnimeClient::KITSU_AUTH_URL, [ + $data = $this->postRequest(K::AUTH_URL, [ 'form_params' => [ 'grant_type' => 'password', 'username' => $username, @@ -164,7 +165,7 @@ class KitsuModel { 'media_type' => 'Anime', 'status' => $status, ], - 'include' => 'media', + 'include' => 'media,media.genres', 'page' => [ 'offset' => 0, 'limit' => 200 @@ -174,10 +175,21 @@ class KitsuModel { ]; $data = $this->getRequest('library-entries', $options); + $included = K::organizeIncludes($data['included']); +/*?>
&$item) { - $item['anime'] = $data['included'][$i]; + $item['anime'] = $included['anime'][$item['relationships']['media']['data']['id']]; + + $animeGenres = $item['anime']['relationships']['genres']; + + foreach($animeGenres as $id) + { + $item['genres'][] = $included['genres'][$id]['name']; + } + + // $item['genres'] = array_pluck($genres, 'name'); } $transformed = $this->animeListTransformer->transformCollection($data['data']); @@ -246,7 +258,9 @@ class KitsuModel { 'filter' => [ 'slug' => $slug ], - 'include' => 'genres,mappings,streamingLinks', + 'include' => ($type === 'anime') + ? 'genres,mappings,streamingLinks' + : 'genres,mappings', ] ]; diff --git a/src/API/Kitsu/KitsuTrait.php b/src/API/Kitsu/KitsuTrait.php index 204e7516..85397f4a 100644 --- a/src/API/Kitsu/KitsuTrait.php +++ b/src/API/Kitsu/KitsuTrait.php @@ -18,7 +18,6 @@ namespace Aviat\AnimeClient\API\Kitsu; use Aviat\AnimeClient\AnimeClient; use Aviat\AnimeClient\API\GuzzleTrait; -use Aviat\Ion\Di\ContainerAware; use Aviat\Ion\Json; use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; diff --git a/src/API/Kitsu/Transformer/AnimeListTransformer.php b/src/API/Kitsu/Transformer/AnimeListTransformer.php index 29f358b3..deb01987 100644 --- a/src/API/Kitsu/Transformer/AnimeListTransformer.php +++ b/src/API/Kitsu/Transformer/AnimeListTransformer.php @@ -16,6 +16,7 @@ namespace Aviat\AnimeClient\API\Kitsu\Transformer; +use Aviat\AnimeClient\API\Kitsu; use Aviat\Ion\Transformer\AbstractTransformer; /** @@ -34,49 +35,34 @@ class AnimeListTransformer extends AbstractTransformer { { /* ?>
= 5) - { - $alternate_title = $anime['titles']['en_jp']; - } - } - return [ 'id' => $item['id'], 'episodes' => [ 'watched' => $item['attributes']['progress'], 'total' => $total_episodes, - 'length' => $anime['attributes']['episodeLength'], + 'length' => $anime['episodeLength'], ], 'airing' => [ - 'status' => $anime['status'] ?? '', - 'started' => $anime['attributes']['startDate'], - 'ended' => $anime['attributes']['endDate'] + 'status' => Kitsu::getAiringStatus($anime['startDate'], $anime['endDate']), + 'started' => $anime['startDate'], + 'ended' => $anime['endDate'] ], 'anime' => [ - 'age_rating' => $anime['attributes']['ageRating'], - 'title' => $anime['attributes']['canonicalTitle'], - 'alternate_title' => $alternate_title, - 'slug' => $anime['attributes']['slug'], - 'url' => $anime['attributes']['url'] ?? '', - 'type' => $anime['attributes']['showType'], - 'image' => $anime['attributes']['posterImage']['small'], + 'age_rating' => $anime['ageRating'], + 'titles' => Kitsu::filterTitles($anime), + 'slug' => $anime['slug'], + 'url' => $anime['url'] ?? '', + 'type' => $this->string($anime['showType'])->upperCaseFirst()->__toString(), + 'image' => $anime['posterImage']['small'], 'genres' => $genres, ], 'watching_status' => $item['attributes']['status'], diff --git a/src/API/Kitsu/Transformer/AnimeTransformer.php b/src/API/Kitsu/Transformer/AnimeTransformer.php index 4fa22b6c..a4351857 100644 --- a/src/API/Kitsu/Transformer/AnimeTransformer.php +++ b/src/API/Kitsu/Transformer/AnimeTransformer.php @@ -16,6 +16,7 @@ namespace Aviat\AnimeClient\API\Kitsu\Transformer; +use Aviat\AnimeClient\API\Kitsu; use Aviat\Ion\Transformer\AbstractTransformer; /** @@ -32,15 +33,12 @@ class AnimeTransformer extends AbstractTransformer { */ public function transform($item) { - ?>
$item['canonicalTitle'], - 'en_title' => $item['titles']['en_jp'], - 'jp_title' => $item['titles']['ja_jp'], + 'titles' => Kitsu::filterTitles($item), + 'status' => Kitsu::getAiringStatus($item['startDate'], $item['endDate']), 'cover_image' => $item['posterImage']['small'], 'show_type' => $item['showType'], 'episode_count' => $item['episodeCount'], diff --git a/src/API/Kitsu/Transformer/MangaListTransformer.php b/src/API/Kitsu/Transformer/MangaListTransformer.php index 251f745c..0e648fe1 100644 --- a/src/API/Kitsu/Transformer/MangaListTransformer.php +++ b/src/API/Kitsu/Transformer/MangaListTransformer.php @@ -16,6 +16,7 @@ namespace Aviat\AnimeClient\API\Kitsu\Transformer; +use Aviat\AnimeClient\API\Kitsu; use Aviat\Ion\StringWrapper; use Aviat\Ion\Transformer\AbstractTransformer; @@ -34,6 +35,7 @@ class MangaListTransformer extends AbstractTransformer { */ public function transform($item) { +/*?>
$total_volumes ], 'manga' => [ - 'title' => $manga['attributes']['canonicalTitle'], + 'titles' => Kitsu::filterTitles($manga['attributes']), 'alternate_title' => NULL, - 'slug' => $manga['id'], - 'url' => 'https://kitsu.io/manga/' . $manga['id'], + 'slug' => $manga['attributes']['slug'], + 'url' => 'https://kitsu.io/manga/' . $manga['attributes']['slug'], 'type' => $manga['attributes']['mangaType'], 'image' => $manga['attributes']['posterImage']['small'], 'genres' => [], //$manga['genres'], diff --git a/src/AnimeClient.php b/src/AnimeClient.php index 4eac8c43..0ffaca69 100644 --- a/src/AnimeClient.php +++ b/src/AnimeClient.php @@ -25,7 +25,6 @@ define('SRC_DIR', realpath(__DIR__)); */ class AnimeClient { - const KITSU_AUTH_URL = 'https://kitsu.io/api/oauth/token'; const SESSION_SEGMENT = 'Aviat\AnimeClient\Auth'; const DEFAULT_CONTROLLER_NAMESPACE = 'Aviat\AnimeClient\Controller'; const DEFAULT_CONTROLLER = 'Aviat\AnimeClient\Controller\Anime'; diff --git a/src/Controller/Anime.php b/src/Controller/Anime.php index 3113402e..238a9c1e 100644 --- a/src/Controller/Anime.php +++ b/src/Controller/Anime.php @@ -293,7 +293,7 @@ class Anime extends BaseController { $data = $this->model->getAnime($anime_id); $this->outputHTML('anime/details', [ - 'title' => 'Anime · ' . $data['title'], + 'title' => 'Anime · ' . $data['titles'][0], 'data' => $data, ]); } diff --git a/src/Controller/Manga.php b/src/Controller/Manga.php index 72e99c9b..d8149885 100644 --- a/src/Controller/Manga.php +++ b/src/Controller/Manga.php @@ -161,7 +161,7 @@ class Manga extends Controller { public function edit($id, $status = "All") { $this->set_session_redirect(); - $item = $this->model->get_library_item($id, $status); + $item = $this->model->getLibraryItem($id); $title = $this->config->get('whose_list') . "'s Manga List · Edit"; $this->outputHTML('manga/edit', [ diff --git a/src/Model/API.php b/src/Model/API.php index 6d523d8d..a0ff29db 100644 --- a/src/Model/API.php +++ b/src/Model/API.php @@ -69,7 +69,7 @@ class API extends Model { foreach ($array as $key => $item) { - $sort[$key] = $item[$sort_key]['title']; + $sort[$key] = $item[$sort_key]['titles'][0]; } array_multisort($sort, SORT_ASC, $array); diff --git a/src/Model/Manga.php b/src/Model/Manga.php index 736d6ea2..6d62ac3e 100644 --- a/src/Model/Manga.php +++ b/src/Model/Manga.php @@ -83,6 +83,18 @@ class Manga extends API return $this->kitsuModel->getManga($manga_id); } + /** + * Get information about a specific list item + * for editing/updating that item + * + * @param string $itemId + * @return array + */ + public function getLibraryItem(string $itemId): array + { + return $this->kitsuModel->getListItem($itemId); + } + /** * Map transformed anime data to be organized by reading status *
  Title Rating Completed Chapters
">Edit - + - + +
+
/ 10 /