From 155650961bb182c08ae28b4c125c5f3a9e8b541d Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Thu, 1 Nov 2018 22:12:41 -0400 Subject: [PATCH] Make Controllers more specialized --- src/Controller/Anime.php | 6 +- src/Controller/Character.php | 2 +- src/Controller/Images.php | 198 +++++++++++++++ src/Controller/Index.php | 464 ----------------------------------- src/Controller/Misc.php | 98 ++++++++ src/Controller/People.php | 144 +++++------ src/Controller/Settings.php | 87 +++++++ src/Controller/User.php | 222 +++++++++++++++++ 8 files changed, 685 insertions(+), 536 deletions(-) create mode 100644 src/Controller/Images.php delete mode 100644 src/Controller/Index.php create mode 100644 src/Controller/Misc.php create mode 100644 src/Controller/Settings.php create mode 100644 src/Controller/User.php diff --git a/src/Controller/Anime.php b/src/Controller/Anime.php index 46690240..86e6a9d5 100644 --- a/src/Controller/Anime.php +++ b/src/Controller/Anime.php @@ -279,7 +279,7 @@ final class Anime extends BaseController { $characters = []; $staff = []; - if ($data->title === '') + if (empty($data)) { $this->notFound( $this->config->get('whose_list') . @@ -326,6 +326,10 @@ final class Anime extends BaseController { 'name' => $personDetails['name'] ?? '??', 'image' => $personDetails['image'], ]; + + usort($staff[$role], function ($a, $b) { + return $a['name'] <=> $b['name']; + }); } } diff --git a/src/Controller/Character.php b/src/Controller/Character.php index 0ced1dd2..cff88071 100644 --- a/src/Controller/Character.php +++ b/src/Controller/Character.php @@ -101,7 +101,7 @@ class Character extends BaseController { } } - $this->outputHTML('character', $viewData); + $this->outputHTML('character/details', $viewData); } /** diff --git a/src/Controller/Images.php b/src/Controller/Images.php new file mode 100644 index 00000000..cfc32bec --- /dev/null +++ b/src/Controller/Images.php @@ -0,0 +1,198 @@ + + * @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 function Aviat\AnimeClient\createPlaceholderImage; +use function Amp\Promise\wait; + +use Aviat\AnimeClient\Controller as BaseController; +use Aviat\AnimeClient\API\{HummingbirdClient, JsonAPI}; +use Aviat\Ion\Di\ContainerInterface; +use Aviat\Ion\View\HtmlView; + +/** + * Controller for handling routes that don't fit elsewhere + */ +final class Images extends BaseController { + /** + * Get image covers from kitsu + * + * @param string $type The category of image + * @param string $file The filename to look for + * @param bool $display Whether to output the image to the server + * @throws \Aviat\Ion\Di\ContainerException + * @throws \Aviat\Ion\Di\NotFoundException + * @throws \InvalidArgumentException + * @throws \TypeError + * @throws \Error + * @throws \Throwable + * @return void + */ + public function cache(string $type, string $file, $display = TRUE): void + { + $currentUrl = $this->request->getUri()->__toString(); + + $kitsuUrl = 'https://media.kitsu.io/'; + $fileName = str_replace('-original', '', $file); + [$id, $ext] = explode('.', basename($fileName)); + + $baseSavePath = $this->config->get('img_cache_path'); + + // Kitsu doesn't serve webp, but for most use cases, + // jpg is a safe assumption + $tryJpg = ['anime','characters','manga','people']; + if ($ext === 'webp' && in_array($type, $tryJpg, TRUE)) + { + $ext = 'jpg'; + $currentUrl = str_replace('webp', 'jpg', $currentUrl); + } + + $typeMap = [ + 'anime' => [ + 'kitsuUrl' => "anime/poster_images/{$id}/medium.{$ext}", + 'width' => 220, + 'height' => 312, + ], + 'avatars' => [ + 'kitsuUrl' => "users/avatars/{$id}/original.{$ext}", + 'width' => null, + 'height' => null, + ], + 'characters' => [ + 'kitsuUrl' => "characters/images/{$id}/original.{$ext}", + 'width' => 225, + 'height' => 350, + ], + 'manga' => [ + 'kitsuUrl' => "manga/poster_images/{$id}/medium.{$ext}", + 'width' => 220, + 'height' => 312, + ], + 'people' => [ + 'kitsuUrl' => "people/images/{$id}/original.{$ext}", + 'width' => null, + 'height' => null, + ], + ]; + + $imageType = $typeMap[$type] ?? NULL; + + if (NULL === $imageType) + { + $this->getPlaceholder($baseSavePath, 200, 200); + return; + } + + $kitsuUrl .= $imageType['kitsuUrl']; + $width = $imageType['width']; + $height = $imageType['height']; + $filePrefix = "{$baseSavePath}/{$type}/{$id}"; + + $promise = (new HummingbirdClient)->request($kitsuUrl); + $response = wait($promise); + + if ($response->getStatus() !== 200) + { + // Try a few different file types before giving up + // webm => jpg => png => gif + $nextType = [ + 'jpg' => 'png', + 'png' => 'gif', + ]; + + if (array_key_exists($ext, $nextType)) + { + $newUrl = str_replace($ext, $nextType[$ext], $currentUrl); + $this->redirect($newUrl, 303); + return; + } + + if ($display) + { + $this->getPlaceholder("{$baseSavePath}/{$type}", $width, $height); + } + else + { + createPlaceholderImage("{$baseSavePath}/{$type}", $width, $height); + } + return; + } + + $data = wait($response->getBody()); + + + + [$origWidth] = getimagesizefromstring($data); + $gdImg = imagecreatefromstring($data); + $resizedImg = imagescale($gdImg, $width ?? $origWidth); + + if ($ext === 'gif') + { + file_put_contents("{$filePrefix}.gif", $data); + imagepalletetotruecolor($gdImg); + } + + // save the webp versions + imagewebp($gdImg, "{$filePrefix}-original.webp"); + imagewebp($resizedImg, "{$filePrefix}.webp"); + + // save the scaled jpeg file + imagejpeg($resizedImg, "{$filePrefix}.jpg"); + + // And the original + file_put_contents("{$filePrefix}-original.jpg", $data); + + imagedestroy($gdImg); + imagedestroy($resizedImg); + + if ($display) + { + $contentType = ($ext === 'webp') + ? "image/webp" + : $response->getHeader('content-type')[0]; + + $outputFile = (strpos($file, '-original') !== FALSE) + ? "{$filePrefix}-original.{$ext}" + : "{$filePrefix}.{$ext}"; + + header("Content-Type: {$contentType}"); + echo file_get_contents($outputFile); + } + } + + /** + * Get a placeholder for a missing image + * + * @param string $path + * @param int|null $width + * @param int|null $height + */ + private function getPlaceholder (string $path, ?int $width = 200, ?int $height = NULL): void + { + $height = $height ?? $width; + + $filename = $path . '/placeholder.png'; + + if ( ! file_exists($path . '/placeholder.png')) + { + createPlaceholderImage($path, $width, $height); + } + + header('Content-Type: image/png'); + echo file_get_contents($filename); + } +} \ No newline at end of file diff --git a/src/Controller/Index.php b/src/Controller/Index.php deleted file mode 100644 index 056ed41c..00000000 --- a/src/Controller/Index.php +++ /dev/null @@ -1,464 +0,0 @@ - - * @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 function Aviat\AnimeClient\createPlaceholderImage; -use function Amp\Promise\wait; - -use Aviat\AnimeClient\Controller as BaseController; -use Aviat\AnimeClient\API\{HummingbirdClient, JsonAPI}; -use Aviat\Ion\Di\ContainerInterface; -use Aviat\Ion\View\HtmlView; - -/** - * Controller for handling routes that don't fit elsewhere - */ -final class Index extends BaseController { - /** - * @var \Aviat\API\Anilist\Model - */ - private $anilistModel; - - /** - * @var \Aviat\AnimeClient\Model\Settings - */ - private $settingsModel; - - public function __construct(ContainerInterface $container) - { - parent::__construct($container); - - $this->anilistModel = $container->get('anilist-model'); - $this->settingsModel = $container->get('settings-model'); - } - - /** - * Purges the API cache - * - * @return void - */ - public function clearCache() - { - $this->cache->clear(); - $this->outputHTML('blank', [ - 'title' => 'Cache cleared' - ]); - } - - /** - * Show the login form - * - * @param string $status - * @return void - */ - public function login(string $status = '') - { - $message = ''; - - $view = new HtmlView($this->container); - - if ($status !== '') - { - $message = $this->showMessage($view, 'error', $status); - } - - // Set the redirect url - $this->setSessionRedirect(); - - $this->outputHTML('login', [ - 'title' => 'Api login', - 'message' => $message - ], $view); - } - - /** - * Redirect to Anilist to start Oauth flow - */ - public function anilistRedirect() - { - $redirectUrl = 'https://anilist.co/api/v2/oauth/authorize?' . - http_build_query([ - 'client_id' => $this->config->get(['anilist', 'client_id']), - 'redirect_uri' => $this->urlGenerator->url('/anilist-oauth'), - 'response_type' => 'code', - ]); - - $this->redirect($redirectUrl, 303); - } - - /** - * Oauth callback for Anilist API - */ - public function anilistCallback() - { - $query = $this->request->getQueryParams(); - $authCode = $query['code']; - $uri = $this->urlGenerator->url('/anilist-oauth'); - - $authData = $this->anilistModel->authenticate($authCode, $uri); - $settings = $this->settingsModel->getSettings(); - - if (array_key_exists('error', $authData)) - { - $this->errorPage(400, 'Error Linking Account', $authData['hint']); - return; - } - - // Update the override config file - $anilistSettings = [ - 'access_token' => $authData['access_token'], - 'access_token_expires' => (time() - 10) + $authData['expires_in'], - 'refresh_token' => $authData['refresh_token'], - ]; - - $newSettings = $settings; - $newSettings['anilist'] = array_merge($settings['anilist'], $anilistSettings); - - foreach($newSettings['config'] as $key => $value) - { - $newSettings[$key] = $value; - } - unset($newSettings['config']); - - $saved = $this->settingsModel->saveSettingsFile($newSettings); - - if ($saved) - { - $this->setFlashMessage('Linked Anilist Account', 'success'); - } - else - { - $this->setFlashMessage('Error Linking Anilist Account', 'error'); - } - - $this->redirect($this->url->generate('settings'), 303); - } - - /** - * Attempt login authentication - * - * @return void - */ - public function loginAction() - { - $auth = $this->container->get('auth'); - $post = $this->request->getParsedBody(); - - if ($auth->authenticate($post['password'])) - { - $this->sessionRedirect(); - return; - } - - $this->setFlashMessage('Invalid username or password.'); - $this->redirect($this->url->generate('login'), 303); - } - - /** - * Deauthorize the current user - * - * @return void - */ - public function logout() - { - $auth = $this->container->get('auth'); - $auth->logout(); - - $this->redirectToDefaultRoute(); - } - - /** - * Show the user profile page - * - * @return void - */ - public function about($username = 'me') - { - $isMainUser = $username === 'me'; - - $username = $isMainUser - ? $this->config->get(['kitsu_username']) - : $username; - $model = $this->container->get('kitsu-model'); - $data = $model->getUserData($username); - $orgData = JsonAPI::organizeData($data)[0]; - $rels = $orgData['relationships'] ?? []; - $favorites = array_key_exists('favorites', $rels) ? $rels['favorites'] : []; - - $timeOnAnime = $this->formatAnimeTime($orgData['attributes']['lifeSpentOnAnime']); - - $whom = $isMainUser - ? $this->config->get('whose_list') - : $username; - - $this->outputHTML('me', [ - 'title' => 'About ' . $whom, - 'data' => $orgData, - 'attributes' => $orgData['attributes'], - 'relationships' => $rels, - 'favorites' => $this->organizeFavorites($favorites), - 'timeOnAnime' => $timeOnAnime, - ]); - } - - /** - * Show the user settings, if logged in - */ - public function settings() - { - $auth = $this->container->get('auth'); - $form = $this->settingsModel->getSettingsForm(); - - $hasAnilistLogin = $this->config->has(['anilist','access_token']); - - $this->outputHTML('settings', [ - 'anilistModel' => $this->anilistModel, - 'auth' => $auth, - 'form' => $form, - 'hasAnilistLogin' => $hasAnilistLogin, - 'config' => $this->config, - 'title' => $this->config->get('whose_list') . "'s Settings", - ]); - } - - /** - * Attempt to save the user's settings - * - * @throws \Aura\Router\Exception\RouteNotFound - */ - public function settings_post() - { - $post = $this->request->getParsedBody(); - unset($post['settings-tabs']); - - // dump($post); - $saved = $this->settingsModel->saveSettingsFile($post); - - if ($saved) - { - $this->setFlashMessage('Saved config settings.', 'success'); - } - else - { - $this->setFlashMessage('Failed to save config file.', 'error'); - } - - $this->redirect($this->url->generate('settings'), 303); - } - - /** - * Get image covers from kitsu - * - * @param string $type The category of image - * @param string $file The filename to look for - * @param bool $display Whether to output the image to the server - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException - * @throws \InvalidArgumentException - * @throws \TypeError - * @throws \Error - * @throws \Throwable - * @return void - */ - public function images(string $type, string $file, $display = TRUE): void - { - $kitsuUrl = 'https://media.kitsu.io/'; - $fileName = str_replace('-original', '', $file); - [$id, $ext] = explode('.', basename($fileName)); - - $baseSavePath = $this->config->get('img_cache_path'); - - $typeMap = [ - 'anime' => [ - 'kitsuUrl' => "anime/poster_images/{$id}/medium.{$ext}", - 'width' => 220, - 'height' => 312, - ], - 'avatars' => [ - 'kitsuUrl' => "users/avatars/{$id}/original.{$ext}", - 'width' => null, - 'height' => null, - ], - 'characters' => [ - 'kitsuUrl' => "characters/images/{$id}/original.{$ext}", - 'width' => 225, - 'height' => 350, - ], - 'manga' => [ - 'kitsuUrl' => "manga/poster_images/{$id}/medium.{$ext}", - 'width' => 220, - 'height' => 312, - ], - 'people' => [ - 'kitsuUrl' => "people/images/{$id}/original.{$ext}", - 'width' => null, - 'height' => null, - ], - ]; - - if ( ! array_key_exists($type, $typeMap)) - { - $this->getPlaceholder($baseSavePath, 100, 100); - return; - } - - $kitsuUrl .= $typeMap[$type]['kitsuUrl']; - $width = $typeMap[$type]['width']; - $height = $typeMap[$type]['height']; - - $promise = (new HummingbirdClient)->request($kitsuUrl); - $response = wait($promise); - - if ($response->getStatus() !== 200) - { - if ($display) - { - $this->getPlaceholder("{$baseSavePath}/{$type}", $width, $height); - } - return; - } - - $data = wait($response->getBody()); - - $filePrefix = "{$baseSavePath}/{$type}/{$id}"; - - [$origWidth] = getimagesizefromstring($data); - $gdImg = imagecreatefromstring($data); - $resizedImg = imagescale($gdImg, $width ?? $origWidth); - - if ($ext === 'gif') - { - file_put_contents("{$filePrefix}.gif", $data); - } - else - { - // save the webp versions - imagewebp($gdImg, "{$filePrefix}-original.webp"); - imagewebp($resizedImg, "{$filePrefix}.webp"); - - // save the scaled jpeg file - imagejpeg($resizedImg, "{$filePrefix}.jpg"); - - // And the original - file_put_contents("{$filePrefix}-original.jpg", $data); - } - - imagedestroy($gdImg); - imagedestroy($resizedImg); - - if ($display) - { - $contentType = ($ext === 'webp') - ? "image/webp" - : $response->getHeader('content-type')[0]; - - $outputFile = (strpos($file, '-original') !== FALSE) - ? "{$filePrefix}-original.{$ext}" - : "{$filePrefix}.{$ext}"; - - header("Content-Type: {$contentType}"); - echo file_get_contents($outputFile); - } - } - - /** - * Reorganize favorites data to be more useful - * - * @param array $rawfavorites - * @return array - */ - private function organizeFavorites(array $rawfavorites): array - { - $output = []; - - unset($rawfavorites['data']); - - foreach($rawfavorites as $item) - { - $rank = $item['attributes']['favRank']; - foreach($item['relationships']['item'] as $key => $fav) - { - $output[$key] = $output[$key] ?? []; - foreach ($fav as $id => $data) - { - $output[$key][$rank] = array_merge(['id' => $id], $data['attributes']); - } - } - - ksort($output[$key]); - } - - return $output; - } - - /** - * Get a placeholder for a missing image - * - * @param string $path - * @param int|null $width - * @param int|null $height - */ - private function getPlaceholder (string $path, ?int $width = 200, ?int $height = NULL): void - { - $height = $height ?? $width; - - $filename = $path . '/placeholder.png'; - - if ( ! file_exists($path . '/placeholder.png')) - { - createPlaceholderImage($path, $width, $height); - } - - header('Content-Type: image/png'); - echo file_get_contents($filename); - } - - /** - * Format the time spent on anime in a more readable format - * - * @param int $minutes - * @return string - */ - private function formatAnimeTime (int $minutes): string - { - $minutesPerDay = 1440; - $minutesPerYear = $minutesPerDay * 365; - - // Minutes short of a year - $years = (int)floor($minutes / $minutesPerYear); - $minutes %= $minutesPerYear; - - // Minutes short of a day - $extraMinutes = $minutes % $minutesPerDay; - - $days = ($minutes - $extraMinutes) / $minutesPerDay; - - // Minutes short of an hour - $remMinutes = $extraMinutes % 60; - - $hours = ($extraMinutes - $remMinutes) / 60; - - $output = "{$days} days, {$hours} hours, and {$remMinutes} minutes."; - - if ($years > 0) - { - $output = "{$years} year(s),{$output}"; - } - - return $output; - } -} \ No newline at end of file diff --git a/src/Controller/Misc.php b/src/Controller/Misc.php new file mode 100644 index 00000000..6d2b43fb --- /dev/null +++ b/src/Controller/Misc.php @@ -0,0 +1,98 @@ + + * @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\Ion\Di\ContainerInterface; +use Aviat\Ion\View\HtmlView; + +/** + * Controller for handling routes that don't fit elsewhere + */ +final class Misc extends BaseController { + /** + * Purges the API cache + * + * @return void + */ + public function clearCache() + { + $this->cache->clear(); + $this->outputHTML('blank', [ + 'title' => 'Cache cleared' + ]); + } + + /** + * Show the login form + * + * @param string $status + * @return void + */ + public function login(string $status = '') + { + $message = ''; + + $view = new HtmlView($this->container); + + if ($status !== '') + { + $message = $this->showMessage($view, 'error', $status); + } + + // Set the redirect url + $this->setSessionRedirect(); + + $this->outputHTML('login', [ + 'title' => 'Api login', + 'message' => $message + ], $view); + } + + /** + * Attempt login authentication + * + * @return void + */ + public function loginAction() + { + $auth = $this->container->get('auth'); + $post = $this->request->getParsedBody(); + + if ($auth->authenticate($post['password'])) + { + $this->sessionRedirect(); + return; + } + + $this->setFlashMessage('Invalid username or password.'); + $this->redirect($this->url->generate('login'), 303); + } + + /** + * Deauthorize the current user + * + * @return void + */ + public function logout() + { + $auth = $this->container->get('auth'); + $auth->logout(); + + $this->redirectToDefaultRoute(); + } +} \ No newline at end of file diff --git a/src/Controller/People.php b/src/Controller/People.php index 0b36b957..4c38d017 100644 --- a/src/Controller/People.php +++ b/src/Controller/People.php @@ -49,105 +49,109 @@ final class People extends BaseController { } $data = JsonAPI::organizeData($rawData); + $included = JsonAPI::organizeIncludes($rawData['included']); + + $orgData = $this->organizeData($included); $viewData = [ + 'included' => $included, 'title' => $this->formatTitle( 'People', $data['attributes']['name'] ), 'data' => $data, 'castCount' => 0, - 'castings' => [] + 'castings' => [], + 'characters' => $orgData['characters'], + 'staff' => $orgData['staff'], ]; - if (array_key_exists('included', $data) && array_key_exists('castings', $data['included'])) - { - $viewData['included'] = $data['included']; - $viewData['castings'] = $this->organizeCast($data['included']['castings']); - $viewData['castCount'] = count($viewData['castings']); - } - - $this->outputHTML('person/index', $viewData); + $this->outputHTML('person/details', $viewData); } - protected function organizeCast(array $cast): array + protected function organizeData(array $data): array { - $output = []; + $output = [ + 'characters' => [ + 'main' => [], + 'supporting' => [], + ], + 'staff' => [], + ]; - foreach ($cast as $id => $role) + if (array_key_exists('characterVoices', $data)) { - if (empty($role['attributes']['role'])) + foreach ($data['characterVoices'] as $cv) { - continue; - } + $mcId = $cv['relationships']['mediaCharacter']['data']['id']; - $roleName = $role['attributes']['role']; - $media = $role['relationships']['media']; - $chars = $role['relationships']['character']['characters'] ?? []; + if ( ! array_key_exists($mcId, $data['mediaCharacters'])) + { + continue; + } - if ( ! array_key_exists($roleName, $output)) - { - $output[$roleName] = [ - 'characters' => [], + $mc = $data['mediaCharacters'][$mcId]; + + $role = $mc['role']; + + $charId = $mc['relationships']['character']['data']['id']; + $mediaId = $mc['relationships']['media']['data']['id']; + + $existingMedia = array_key_exists($charId, $output['characters'][$role]) + ? $output['characters'][$role][$charId]['media'] + : []; + + $relatedMedia = [ + $mediaId => $data['anime'][$mediaId], + ]; + + $includedMedia = array_replace_recursive($existingMedia, $relatedMedia); + + uasort($includedMedia, function ($a, $b) { + return $a['canonicalTitle'] <=> $b['canonicalTitle']; + }); + + $character = $data['characters'][$charId]; + + $output['characters'][$role][$charId] = [ + 'character' => $character, + 'media' => $includedMedia, ]; } + } - if ( ! empty($chars)) + if (array_key_exists('mediaStaff', $data)) + { + foreach($data['mediaStaff'] as $rid => $role) { - $relatedMedia = []; - - if (array_key_exists('anime', $media)) - { - foreach($media['anime'] as $sid => $series) - { - $relatedMedia[$sid] = $series['attributes']; - } - } - - foreach($chars as $cid => $character) - { - // To make sure all the media are properly associated, - // merge the found media for this iteration with - // existing media, making sure to preserve array keys - $existingMedia = array_key_exists($cid, $output[$roleName]['characters']) - ? $output[$roleName]['characters'][$cid]['media'] - : []; - - $includedMedia = array_replace_recursive($existingMedia, $relatedMedia); - - uasort($includedMedia, function ($a, $b) { - return $a['canonicalTitle'] <=> $b['canonicalTitle']; - }); - - $output[$roleName]['characters'][$cid] = [ - 'character' => $character['attributes'], - 'media' => $includedMedia, - ]; - } - - uasort($output[$roleName]['characters'], function ($a, $b) { - return $a['character']['canonicalName'] <=> $b['character']['canonicalName']; - }); + $roleName = $role['role']; + $mediaType = $role['relationships']['media']['data']['type']; + $mediaId = $role['relationships']['media']['data']['id']; + $media = $data[$mediaType][$mediaId]; + $output['staff'][$roleName][$mediaType][$mediaId] = $media; } + } + uasort($output['characters']['main'], function ($a, $b) { + return $a['character']['canonicalName'] <=> $b['character']['canonicalName']; + }); + uasort($output['characters']['supporting'], function ($a, $b) { + return $a['character']['canonicalName'] <=> $b['character']['canonicalName']; + }); + ksort($output['staff']); + foreach($output['staff'] as $role => &$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']; + uasort($media['anime'], function ($a, $b) { + return $a['canonicalTitle'] <=> $b['canonicalTitle']; }); } - else if (array_key_exists('manga', $media)) + + if (array_key_exists('manga', $media)) { - foreach ($media['manga'] as $sid => $series) - { - $output[$roleName]['manga'][$sid] = $series; - } - uasort($output[$roleName]['manga'], function ($a, $b) { - return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle']; + uasort($media['manga'], function ($a, $b) { + return $a['canonicalTitle'] <=> $b['canonicalTitle']; }); } } diff --git a/src/Controller/Settings.php b/src/Controller/Settings.php new file mode 100644 index 00000000..62214748 --- /dev/null +++ b/src/Controller/Settings.php @@ -0,0 +1,87 @@ + + * @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\Ion\Di\ContainerInterface; + +/** + * Controller for user settings + */ +final class Settings extends BaseController { + /** + * @var \Aviat\API\Anilist\Model + */ + private $anilistModel; + + /** + * @var \Aviat\AnimeClient\Model\Settings + */ + private $settingsModel; + + public function __construct(ContainerInterface $container) + { + parent::__construct($container); + + $this->anilistModel = $container->get('anilist-model'); + $this->settingsModel = $container->get('settings-model'); + } + + /** + * Show the user settings, if logged in + */ + public function index() + { + $auth = $this->container->get('auth'); + $form = $this->settingsModel->getSettingsForm(); + + $hasAnilistLogin = $this->config->has(['anilist', 'access_token']); + + $this->outputHTML('settings/settings', [ + 'anilistModel' => $this->anilistModel, + 'auth' => $auth, + 'form' => $form, + 'hasAnilistLogin' => $hasAnilistLogin, + 'config' => $this->config, + 'title' => $this->config->get('whose_list') . "'s Settings", + ]); + } + + /** + * Attempt to save the user's settings + * + * @throws \Aura\Router\Exception\RouteNotFound + */ + public function update() + { + $post = $this->request->getParsedBody(); + unset($post['settings-tabs']); + + // dump($post); + $saved = $this->settingsModel->saveSettingsFile($post); + + if ($saved) + { + $this->setFlashMessage('Saved config settings.', 'success'); + } else + { + $this->setFlashMessage('Failed to save config file.', 'error'); + } + + $this->redirect($this->url->generate('settings'), 303); + } +} \ No newline at end of file diff --git a/src/Controller/User.php b/src/Controller/User.php new file mode 100644 index 00000000..59b09a41 --- /dev/null +++ b/src/Controller/User.php @@ -0,0 +1,222 @@ + + * @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; +use Aviat\Ion\Di\ContainerInterface; + +/** + * Controller for handling routes that don't fit elsewhere + */ +final class User extends BaseController { + + private $kitsuModel; + + public function __construct(ContainerInterface $container) + { + parent::__construct($container); + + $this->kitsuModel = $container->get('kitsu-model'); + } + + /** + * Show the user profile page for the configured user + */ + public function me(): void + { + $this->about('me'); + } + + /** + * Show the user profile page + * + * @param string $username + * @return void + */ + public function about(string $username): void + { + $isMainUser = $username === 'me'; + + $username = $isMainUser + ? $this->config->get(['kitsu_username']) + : $username; + + $data = $this->kitsuModel->getUserData($username); + $orgData = JsonAPI::organizeData($data)[0]; + $rels = $orgData['relationships'] ?? []; + $favorites = array_key_exists('favorites', $rels) ? $rels['favorites'] : []; + + $stats = []; + foreach ($rels['stats'] as $sid => &$item) + { + $key = $item['attributes']['kind']; + $stats[$key] = $item['attributes']['statsData']; + unset($item); + } + + //dump($orgData); + // dump($stats); + + // $timeOnAnime = $this->formatAnimeTime($orgData['attributes']['lifeSpentOnAnime']); + $timeOnAnime = $this->formatAnimeTime($stats['anime-amount-consumed']['time']); + + + $whom = $isMainUser + ? $this->config->get('whose_list') + : $username; + + $this->outputHTML('user/details', [ + 'title' => 'About ' . $whom, + 'data' => $orgData, + 'attributes' => $orgData['attributes'], + 'relationships' => $rels, + 'favorites' => $this->organizeFavorites($favorites), + 'stats' => $stats, + 'timeOnAnime' => $timeOnAnime, + ]); + } + + + /** + * Redirect to Anilist to start Oauth flow + */ + public function anilistRedirect() + { + $redirectUrl = 'https://anilist.co/api/v2/oauth/authorize?' . + http_build_query([ + 'client_id' => $this->config->get(['anilist', 'client_id']), + 'redirect_uri' => $this->urlGenerator->url('/anilist-oauth'), + 'response_type' => 'code', + ]); + + $this->redirect($redirectUrl, 303); + } + + /** + * Oauth callback for Anilist API + */ + public function anilistCallback() + { + $query = $this->request->getQueryParams(); + $authCode = $query['code']; + $uri = $this->urlGenerator->url('/anilist-oauth'); + + $authData = $this->anilistModel->authenticate($authCode, $uri); + $settings = $this->settingsModel->getSettings(); + + if (array_key_exists('error', $authData)) + { + $this->errorPage(400, 'Error Linking Account', $authData['hint']); + return; + } + + // Update the override config file + $anilistSettings = [ + 'access_token' => $authData['access_token'], + 'access_token_expires' => (time() - 10) + $authData['expires_in'], + 'refresh_token' => $authData['refresh_token'], + ]; + + $newSettings = $settings; + $newSettings['anilist'] = array_merge($settings['anilist'], $anilistSettings); + + foreach($newSettings['config'] as $key => $value) + { + $newSettings[$key] = $value; + } + unset($newSettings['config']); + + $saved = $this->settingsModel->saveSettingsFile($newSettings); + + if ($saved) + { + $this->setFlashMessage('Linked Anilist Account', 'success'); + } + else + { + $this->setFlashMessage('Error Linking Anilist Account', 'error'); + } + + $this->redirect($this->url->generate('settings'), 303); + } + + /** + * Reorganize favorites data to be more useful + * + * @param array $rawfavorites + * @return array + */ + private function organizeFavorites(array $rawfavorites): array + { + $output = []; + + unset($rawfavorites['data']); + + foreach ($rawfavorites as $item) + { + $rank = $item['attributes']['favRank']; + foreach ($item['relationships']['item'] as $key => $fav) + { + $output[$key] = $output[$key] ?? []; + foreach ($fav as $id => $data) + { + $output[$key][$rank] = array_merge(['id' => $id], $data['attributes']); + } + } + + ksort($output[$key]); + } + + return $output; + } + + /** + * Format the time spent on anime in a more readable format + * + * @param int $minutes + * @return string + */ + private function formatAnimeTime(int $minutes): string + { + $minutesPerDay = 1440; + $minutesPerYear = $minutesPerDay * 365; + + // Minutes short of a year + $years = (int)floor($minutes / $minutesPerYear); + $minutes %= $minutesPerYear; + + // Minutes short of a day + $extraMinutes = $minutes % $minutesPerDay; + + $days = ($minutes - $extraMinutes) / $minutesPerDay; + + // Minutes short of an hour + $remMinutes = $extraMinutes % 60; + + $hours = ($extraMinutes - $remMinutes) / 60; + + $output = "{$days} days, {$hours} hours, and {$remMinutes} minutes."; + + if ($years > 0) + { + $output = "{$years} year(s),{$output}"; + } + + return $output; + } +} \ No newline at end of file