From 16f62ceb8d8fe97513fbc0efdee353e0b196127f Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Fri, 19 Oct 2018 09:30:27 -0400 Subject: [PATCH] Miscellaneous page improvements, including additional data and sorting --- .gitlab-ci.yml | 21 ------ CHANGELOG.md | 2 + app/appConf/routes.php | 8 +++ app/views/character.php | 16 +++-- app/views/person.php | 74 +++++++++++++++++++++ src/API/JsonAPI.php | 5 ++ src/API/Kitsu/ListItem.php | 2 +- src/API/Kitsu/Model.php | 21 +++++- src/AnimeClient.php | 90 ++++++++++++++++++------- src/Command/UpdateThumbnails.php | 18 ++--- src/Controller/Anime.php | 47 +++++++++++-- src/Controller/Character.php | 60 +++++++++++++---- src/Controller/Index.php | 55 +++++++++------- src/Controller/Manga.php | 38 ++++++++++- src/Controller/People.php | 110 +++++++++++++++++++++++++++++++ 15 files changed, 457 insertions(+), 110 deletions(-) delete mode 100644 .gitlab-ci.yml create mode 100644 app/views/person.php create mode 100644 src/Controller/People.php diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 6a3d138b..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,21 +0,0 @@ -test:7.1: - stage: test - before_script: - - sh build/docker_install.sh > /dev/null - - apk add --no-cache php7-phpdbg - - curl -sS https://getcomposer.org/installer | php - - php composer.phar install --ignore-platform-reqs - image: php:7.1-alpine - script: - - phpdbg -qrr -- ./vendor/bin/phpunit --coverage-text --colors=never - -test:7.2: - stage: test - before_script: - - sh build/docker_install.sh > /dev/null - - apk add --no-cache php7-phpdbg - - curl -sS https://getcomposer.org/installer | php - - php composer.phar install --ignore-platform-reqs - image: php:7.2-alpine - script: - - phpdbg -qrr -- ./vendor/bin/phpunit --coverage-text --colors=never diff --git a/CHANGELOG.md b/CHANGELOG.md index 39f11102..3583d3e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ * Updated console command to sync Kitsu and Anilist data (Kitsu can sync MAL, and MAL's API broke, so MAL sync was removed) * Added page to update settings without having to edit config files * Defaulted to secure (HTTPS) urls +* Updated Character pages to show voice actors +* Added People pages, showing which works they contributed to, and in what role ## Version 4 * Updated to use Kitsu API after discontinuation of Hummingbird diff --git a/app/appConf/routes.php b/app/appConf/routes.php index 2246f47f..37e4ca70 100644 --- a/app/appConf/routes.php +++ b/app/appConf/routes.php @@ -174,6 +174,14 @@ return [ 'slug' => '[a-z0-9\-]+' ] ], + 'person' => [ + 'path' => '/people/{id}', + 'action' => 'index', + 'params' => [], + 'tokens' => [ + 'id' => '[a-z0-9\-]+' + ] + ], 'user_info' => [ 'path' => '/me', 'action' => 'me', diff --git a/app/views/character.php b/app/views/character.php index 1f0cdb23..d4b90e69 100644 --- a/app/views/character.php +++ b/app/views/character.php @@ -1,4 +1,7 @@ - +
@@ -89,14 +92,19 @@ Cast Member Series - + $c):?>
- + generate('person', ['id' => $c['person']['id']]); + ?> + +
+
@@ -108,7 +116,7 @@ $titles = Kitsu::filterTitles($series['attributes']); ?> - +
diff --git a/app/views/person.php b/app/views/person.php new file mode 100644 index 00000000..166cf687 --- /dev/null +++ b/app/views/person.php @@ -0,0 +1,74 @@ + +
+
+
+ + " + type="image/webp" + > + " + type="image/jpeg" + > + " alt="" /> + +
+
+

+
+
+ +
+ 0): ?> +

Castings

+ $entries): ?> +

+ $casting): ?> + +
+ +
+ $series): ?> + + +
+
+ + + +
+
diff --git a/src/API/JsonAPI.php b/src/API/JsonAPI.php index 8a1ebc70..234df2ec 100644 --- a/src/API/JsonAPI.php +++ b/src/API/JsonAPI.php @@ -61,6 +61,11 @@ final class JsonAPI { // Inline organized data foreach($data['data'] as $i => &$item) { + if ( ! is_array($item)) + { + continue; + } + if (array_key_exists('relationships', $item)) { foreach($item['relationships'] as $relType => $props) diff --git a/src/API/Kitsu/ListItem.php b/src/API/Kitsu/ListItem.php index c251ccc9..4b09124f 100644 --- a/src/API/Kitsu/ListItem.php +++ b/src/API/Kitsu/ListItem.php @@ -103,7 +103,7 @@ final class ListItem implements ListItemInterface { $request = $this->requestBuilder->newRequest('GET', "library-entries/{$id}") ->setQuery([ - 'include' => 'media,media.genres,media.mappings' + 'include' => 'media,media.categories,media.mappings' ]); if ($authHeader !== FALSE) diff --git a/src/API/Kitsu/Model.php b/src/API/Kitsu/Model.php index ec5e592c..e06c8ef8 100644 --- a/src/API/Kitsu/Model.php +++ b/src/API/Kitsu/Model.php @@ -221,6 +221,21 @@ final class Model { return $data; } + /** + * Get information about a person + * + * @param string $id + * @return array + */ + public function getPerson(string $id): array + { + return $this->getRequest("people/{$id}", [ + 'query' => [ + 'include' => 'castings,castings.media,staff,staff.media,voices' + ], + ]); + } + /** * Get profile information for the configured user * @@ -585,7 +600,7 @@ final class Model { } $transformed = $this->mangaTransformer->transform($baseData); - $transformed['included'] = $baseData['included']; + $transformed['included'] = JsonAPI::organizeIncluded($baseData['included']); return $transformed; } @@ -936,8 +951,8 @@ final class Model { 'characters' => 'slug,name,image' ], 'include' => ($type === 'anime') - ? 'categories,mappings,streamingLinks,animeCharacters.character' - : 'categories,mappings,mangaCharacters.character,castings.character', + ? 'staff,staff.person,categories,mappings,streamingLinks,animeCharacters.character' + : 'staff,staff.person,categories,mappings,mangaCharacters.character,castings.character', ] ]; diff --git a/src/AnimeClient.php b/src/AnimeClient.php index 84a722ce..a305c919 100644 --- a/src/AnimeClient.php +++ b/src/AnimeClient.php @@ -19,6 +19,10 @@ namespace Aviat\AnimeClient; use Aviat\Ion\ConfigInterface; use Yosymfony\Toml\{Toml, TomlBuilder}; +// ---------------------------------------------------------------------------- +//! TOML Functions +// ---------------------------------------------------------------------------- + /** * Load configuration options from .toml files * @@ -67,30 +71,6 @@ function loadTomlFile(string $filename): array return Toml::parseFile($filename); } -/** - * Is the array sequential, not associative? - * - * @param mixed $array - * @return bool - */ -function isSequentialArray($array): bool -{ - if ( ! is_array($array)) - { - return FALSE; - } - - $i = 0; - foreach ($array as $k => $v) - { - if ($k !== $i++) - { - return FALSE; - } - } - return TRUE; -} - function _iterateToml(TomlBuilder $builder, $data, $parentKey = NULL): void { foreach ($data as $key => $value) @@ -147,6 +127,34 @@ function tomlToArray(string $toml): array return Toml::parse($toml); } +// ---------------------------------------------------------------------------- +//! Misc Functions +// ---------------------------------------------------------------------------- + +/** + * Is the array sequential, not associative? + * + * @param mixed $array + * @return bool + */ +function isSequentialArray($array): bool +{ + if ( ! is_array($array)) + { + return FALSE; + } + + $i = 0; + foreach ($array as $k => $v) + { + if ($k !== $i++) + { + return FALSE; + } + } + return TRUE; +} + /** * Check that folder permissions are correct for proper operation * @@ -186,4 +194,38 @@ function checkFolderPermissions(ConfigInterface $config): array } return $errors; +} + +/** + * Generate the path for the cached image from the original iamge + * + * @param string $kitsuUrl + * @return string + */ +function getLocalImg ($kitsuUrl): string +{ + if ( ! is_string($kitsuUrl)) + { + return '/404'; + } + + $parts = parse_url($kitsuUrl); + + if ($parts === FALSE) + { + return '/404'; + } + + $file = basename($parts['path']); + $fileParts = explode('.', $file); + $ext = array_pop($fileParts); + $segments = explode('/', trim($parts['path'], '/')); + + // dump($segments); + + $type = $segments[0] === 'users' ? $segments[1] : $segments[0]; + + $id = $segments[count($segments) - 2]; + + return implode('/', ['images', $type, "{$id}.{$ext}"]); } \ No newline at end of file diff --git a/src/Command/UpdateThumbnails.php b/src/Command/UpdateThumbnails.php index 33d1c61b..5819b7a7 100644 --- a/src/Command/UpdateThumbnails.php +++ b/src/Command/UpdateThumbnails.php @@ -16,20 +16,8 @@ namespace Aviat\AnimeClient\Command; -use Aviat\AnimeClient\API\{ - FailedResponseException, - JsonAPI, - ParallelAPIRequest -}; -use Aviat\AnimeClient\API\Anilist\Transformer\{ - AnimeListTransformer as AALT, - MangaListTransformer as AMLT -}; -use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus}; +use Aviat\AnimeClient\API\JsonAPI; use Aviat\AnimeClient\Controller\Index; -use Aviat\AnimeClient\Types\FormItem; -use Aviat\Ion\Json; -use DateTime; /** * Clears out image cache directories, then re-creates the image cache @@ -69,9 +57,11 @@ final class UpdateThumbnails extends BaseCommand { { $this->controller->images($type, "{$id}.jpg", FALSE); } + + $this->echoBox("Finished regenerating {$type} thumbnails"); } - $this->echoBox('Finished regenerating thumbnails'); + $this->echoBox('Finished regenerating all thumbnails'); } public function clearThumbs() diff --git a/src/Controller/Anime.php b/src/Controller/Anime.php index e4770008..93c5e37a 100644 --- a/src/Controller/Anime.php +++ b/src/Controller/Anime.php @@ -275,10 +275,11 @@ final class Anime extends BaseController { */ public function details(string $animeId): void { - $show_data = $this->model->getAnime($animeId); + $data = $this->model->getAnime($animeId); $characters = []; + $staff = []; - if ($show_data->title === '') + if ($data->title === '') { $this->notFound( $this->config->get('whose_list') . @@ -290,22 +291,56 @@ final class Anime extends BaseController { return; } - if (array_key_exists('characters', $show_data['included'])) + if (array_key_exists('characters', $data['included'])) { - foreach($show_data['included']['characters'] as $id => $character) + + + foreach($data['included']['characters'] as $id => $character) { $characters[$id] = $character['attributes']; } } + if (array_key_exists('mediaStaff', $data['included'])) + { + foreach ($data['included']['mediaStaff'] as $id => $person) + { + $personDetails = []; + foreach ($person['relationships']['person']['people'] as $p) + { + $personDetails = $p['attributes']; + } + + $role = $person['attributes']['role']; + + if ( ! array_key_exists($role, $staff)) + { + $staff[$role] = []; + } + + $staff[$role][$id] = [ + 'name' => $personDetails['name'] ?? '??', + 'image' => $personDetails['image'], + ]; + } + } + + uasort($characters, function ($a, $b) { + return $a['name'] <=> $b['name']; + }); + + // dump($characters); + // dump($staff); + $this->outputHTML('anime/details', [ 'title' => $this->formatTitle( $this->config->get('whose_list') . "'s Anime List", 'Anime', - $show_data->title + $data->title ), 'characters' => $characters, - 'show_data' => $show_data, + 'show_data' => $data, + 'staff' => $staff, ]); } diff --git a/src/Controller/Character.php b/src/Controller/Character.php index 8258e806..6eba7d76 100644 --- a/src/Controller/Character.php +++ b/src/Controller/Character.php @@ -16,6 +16,8 @@ namespace Aviat\AnimeClient\Controller; +use function Aviat\AnimeClient\getLocalImg; + use Aviat\AnimeClient\Controller as BaseController; use Aviat\AnimeClient\API\JsonAPI; use Aviat\Ion\ArrayWrapper; @@ -23,7 +25,7 @@ use Aviat\Ion\ArrayWrapper; /** * Controller for character description pages */ -final class Character extends BaseController { +class Character extends BaseController { use ArrayWrapper; @@ -57,6 +59,23 @@ final class Character extends BaseController { $data = JsonAPI::organizeData($rawData); + if (array_key_exists('included', $data)) + { + if (array_key_exists('anime', $data['included'])) + { + uasort($data['included']['anime'], function ($a, $b) { + return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle']; + }); + } + + if (array_key_exists('manga', $data['included'])) + { + uasort($data['included']['manga'], function ($a, $b) { + return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle']; + }); + } + } + $viewData = [ 'title' => $this->formatTitle( 'Characters', @@ -67,10 +86,13 @@ final class Character extends BaseController { 'castings' => [] ]; - if (array_key_exists('included', $data) && array_key_exists('castings', $data['included'])) + if (array_key_exists('included', $data)) { - $viewData['castings'] = $this->organizeCast($data['included']['castings']); - $viewData['castCount'] = $this->getCastCount($viewData['castings']); + if (array_key_exists('castings', $data['included'])) + { + $viewData['castings'] = $this->organizeCast($data['included']['castings']); + $viewData['castCount'] = $this->getCastCount($viewData['castings']); + } } $this->outputHTML('character', $viewData); @@ -121,25 +143,26 @@ final class Character extends BaseController { return $output; } - private function getCastCount(array $cast): int + protected function getCastCount(array $cast): int { $count = 0; foreach($cast as $role) { - if ( + $count++; + /* if ( array_key_exists('attributes', $role) && array_key_exists('role', $role['attributes']) && $role['attributes']['role'] !== NULL ) { $count++; - } + } */ } return $count; } - private function organizeCast(array $cast): array + protected function organizeCast(array $cast): array { $cast = $this->dedupeCast($cast); $output = []; @@ -157,8 +180,19 @@ final class Character extends BaseController { if ($isVA) { - $person = current($role['relationships']['person']['people'])['attributes']; - $name = $person['name']; + foreach($role['relationships']['person']['people'] as $pid => $peoples) + { + $p = $peoples; + } + + $person = $p['attributes']; + $person['id'] = $pid; + $person['image'] = $person['image']['original']; + + uasort($role['relationships']['media']['anime'], function ($a, $b) { + return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle']; + }); + $item = [ 'person' => $person, 'series' => $role['relationships']['media']['anime'] @@ -168,7 +202,11 @@ final class Character extends BaseController { } else { - $output[$roleName][] = $role['relationships']['person']['people']; + foreach($role['relationships']['person']['people'] as $pid => $person) + { + $person['id'] = $pid; + $output[$roleName][$pid] = $person; + } } } diff --git a/src/Controller/Index.php b/src/Controller/Index.php index 64f9649e..5fad99ef 100644 --- a/src/Controller/Index.php +++ b/src/Controller/Index.php @@ -268,36 +268,45 @@ final class Index extends BaseController { $kitsuUrl = 'https://media.kitsu.io/'; $fileName = str_replace('-original', '', $file); [$id, $ext] = explode('.', basename($fileName)); - switch ($type) + + $typeMap = [ + 'anime' => [ + 'kitsuUrl' => "anime/poster_images/{$id}/medium.{$ext}", + 'width' => 220, + ], + 'avatars' => [ + 'kitsuUrl' => "users/avatars/{$id}/original.{$ext}", + 'width' => null, + ], + 'characters' => [ + 'kitsuUrl' => "characters/images/{$id}/original.{$ext}", + 'width' => 225, + ], + 'manga' => [ + 'kitsuUrl' => "manga/poster_images/{$id}/medium.{$ext}", + 'width' => 220, + ], + 'people' => [ + 'kitsuUrl' => "people/images/{$id}/original.{$ext}", + 'width' => null, + ], + ]; + + if ( ! array_key_exists($type, $typeMap)) { - case 'anime': - $kitsuUrl .= "anime/poster_images/{$id}/small.jpg"; - $width = 220; - break; - - case 'avatars': - $kitsuUrl .= "users/avatars/{$id}/original.jpg"; - break; - - case 'manga': - $kitsuUrl .= "manga/poster_images/{$id}/small.jpg"; - $width = 220; - break; - - case 'characters': - $kitsuUrl .= "characters/images/{$id}/original.jpg"; - $width = 225; - break; - - default: - $this->notFound(); - return; + $this->notFound(); + return; } + $kitsuUrl .= $typeMap[$type]['kitsuUrl']; + $width = $typeMap[$type]['width']; + $promise = (new HummingbirdClient)->request($kitsuUrl); $response = wait($promise); $data = wait($response->getBody()); + // echo "Fetching {$kitsuUrl}\n"; + $baseSavePath = $this->config->get('img_cache_path'); $filePrefix = "{$baseSavePath}/{$type}/{$id}"; diff --git a/src/Controller/Manga.php b/src/Controller/Manga.php index 12e41bf7..c0256553 100644 --- a/src/Controller/Manga.php +++ b/src/Controller/Manga.php @@ -280,6 +280,7 @@ final class Manga extends Controller { public function details($manga_id): void { $data = $this->model->getManga($manga_id); + $staff = []; $characters = []; if (empty($data)) @@ -293,14 +294,44 @@ final class Manga extends Controller { return; } - foreach($data['included'] as $included) + if (array_key_exists('characters', $data['included'])) { - if ($included['type'] === 'characters') + foreach ($data['included']['characters'] as $id => $character) { - $characters[$included['id']] = $included['attributes']; + $characters[$id] = $character['attributes']; } } + if (array_key_exists('mediaStaff', $data['included'])) + { + foreach ($data['included']['mediaStaff'] as $id => $person) + { + $personDetails = []; + foreach ($person['relationships']['person']['people'] as $p) + { + $personDetails = $p['attributes']; + } + + $role = $person['attributes']['role']; + + if ( ! array_key_exists($role, $staff)) + { + $staff[$role] = []; + } + + $staff[$role][$id] = [ + 'name' => $personDetails['name'] ?? '??', + 'image' => $personDetails['image'], + ]; + } + } + + uasort($characters, function ($a, $b) { + return $a['name'] <=> $b['name']; + }); + + // dump($staff); + $this->outputHTML('manga/details', [ 'title' => $this->formatTitle( $this->config->get('whose_list') . "'s Manga List", @@ -309,6 +340,7 @@ final class Manga extends Controller { ), 'characters' => $characters, 'data' => $data, + 'staff' => $staff, ]); } diff --git a/src/Controller/People.php b/src/Controller/People.php new file mode 100644 index 00000000..c20b5169 --- /dev/null +++ b/src/Controller/People.php @@ -0,0 +1,110 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Controller; + +use Aviat\AnimeClient\Controller as BaseController; +use Aviat\AnimeClient\API\JsonAPI; + +/** + * Controller for People pages + */ +final class People extends BaseController { + /** + * Show information about a person + * + * @param string $id + * @return void + */ + public function index(string $id): void + { + $model = $this->container->get('kitsu-model'); + + $rawData = $model->getPerson($id); + + if (( ! array_key_exists('data', $rawData)) || empty($rawData['data'])) + { + $this->notFound( + $this->formatTitle( + 'People', + 'Person not found' + ), + 'Person Not Found' + ); + + return; + } + + $data = JsonAPI::organizeData($rawData); + + $viewData = [ + 'title' => $this->formatTitle( + 'People', + $data['attributes']['name'] + ), + 'data' => $data, + 'castCount' => 0, + 'castings' => [] + ]; + + if (array_key_exists('included', $data) && array_key_exists('castings', $data['included'])) + { + $viewData['castings'] = $this->organizeCast($data['included']['castings']); + $viewData['castCount'] = count($viewData['castings']); + } + + $this->outputHTML('person', $viewData); + } + + protected function organizeCast(array $cast): array + { + $output = []; + + foreach ($cast as $id => $role) + { + if (empty($role['attributes']['role'])) + { + continue; + } + + $roleName = $role['attributes']['role']; + $media = $role['relationships']['media']; + + if (array_key_exists('anime', $media)) + { + foreach($media['anime'] as $sid => $series) + { + $output[$roleName]['anime'][$sid] = $series; + } + uasort($output[$roleName]['anime'], function ($a, $b) { + return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle']; + }); + } + else if (array_key_exists('manga', $media)) + { + foreach ($media['manga'] as $sid => $series) + { + $output[$roleName]['manga'][$sid] = $series; + } + uasort($output[$roleName]['anime'], function ($a, $b) { + return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle']; + }); + } + } + + return $output; + } +} \ No newline at end of file