Version 5.1 - All the GraphQL #32

Closed
timw4mail wants to merge 1160 commits from develop into master
8 changed files with 685 additions and 536 deletions
Showing only changes of commit c2d51b2b7e - Show all commits

View File

@ -279,7 +279,7 @@ final class Anime extends BaseController {
$characters = []; $characters = [];
$staff = []; $staff = [];
if ($data->title === '') if (empty($data))
{ {
$this->notFound( $this->notFound(
$this->config->get('whose_list') . $this->config->get('whose_list') .
@ -326,6 +326,10 @@ final class Anime extends BaseController {
'name' => $personDetails['name'] ?? '??', 'name' => $personDetails['name'] ?? '??',
'image' => $personDetails['image'], 'image' => $personDetails['image'],
]; ];
usort($staff[$role], function ($a, $b) {
return $a['name'] <=> $b['name'];
});
} }
} }

View File

@ -101,7 +101,7 @@ class Character extends BaseController {
} }
} }
$this->outputHTML('character', $viewData); $this->outputHTML('character/details', $viewData);
} }
/** /**

198
src/Controller/Images.php Normal file
View File

@ -0,0 +1,198 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @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);
}
}

View File

@ -1,464 +0,0 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @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;
}
}

98
src/Controller/Misc.php Normal file
View File

@ -0,0 +1,98 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @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();
}
}

View File

@ -49,105 +49,109 @@ final class People extends BaseController {
} }
$data = JsonAPI::organizeData($rawData); $data = JsonAPI::organizeData($rawData);
$included = JsonAPI::organizeIncludes($rawData['included']);
$orgData = $this->organizeData($included);
$viewData = [ $viewData = [
'included' => $included,
'title' => $this->formatTitle( 'title' => $this->formatTitle(
'People', 'People',
$data['attributes']['name'] $data['attributes']['name']
), ),
'data' => $data, 'data' => $data,
'castCount' => 0, 'castCount' => 0,
'castings' => [] 'castings' => [],
'characters' => $orgData['characters'],
'staff' => $orgData['staff'],
]; ];
if (array_key_exists('included', $data) && array_key_exists('castings', $data['included'])) $this->outputHTML('person/details', $viewData);
{
$viewData['included'] = $data['included'];
$viewData['castings'] = $this->organizeCast($data['included']['castings']);
$viewData['castCount'] = count($viewData['castings']);
} }
$this->outputHTML('person/index', $viewData); protected function organizeData(array $data): array
}
protected function organizeCast(array $cast): 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)
{
$mcId = $cv['relationships']['mediaCharacter']['data']['id'];
if ( ! array_key_exists($mcId, $data['mediaCharacters']))
{ {
continue; continue;
} }
$roleName = $role['attributes']['role']; $mc = $data['mediaCharacters'][$mcId];
$media = $role['relationships']['media'];
$chars = $role['relationships']['character']['characters'] ?? [];
if ( ! array_key_exists($roleName, $output)) $role = $mc['role'];
{
$output[$roleName] = [
'characters' => [],
];
}
if ( ! empty($chars)) $charId = $mc['relationships']['character']['data']['id'];
{ $mediaId = $mc['relationships']['media']['data']['id'];
$relatedMedia = [];
if (array_key_exists('anime', $media)) $existingMedia = array_key_exists($charId, $output['characters'][$role])
{ ? $output['characters'][$role][$charId]['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']
: []; : [];
$relatedMedia = [
$mediaId => $data['anime'][$mediaId],
];
$includedMedia = array_replace_recursive($existingMedia, $relatedMedia); $includedMedia = array_replace_recursive($existingMedia, $relatedMedia);
uasort($includedMedia, function ($a, $b) { uasort($includedMedia, function ($a, $b) {
return $a['canonicalTitle'] <=> $b['canonicalTitle']; return $a['canonicalTitle'] <=> $b['canonicalTitle'];
}); });
$output[$roleName]['characters'][$cid] = [ $character = $data['characters'][$charId];
'character' => $character['attributes'],
$output['characters'][$role][$charId] = [
'character' => $character,
'media' => $includedMedia, 'media' => $includedMedia,
]; ];
} }
}
uasort($output[$roleName]['characters'], function ($a, $b) { if (array_key_exists('mediaStaff', $data))
{
foreach($data['mediaStaff'] as $rid => $role)
{
$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']; 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)) if (array_key_exists('anime', $media))
{ {
foreach($media['anime'] as $sid => $series) uasort($media['anime'], function ($a, $b) {
{ return $a['canonicalTitle'] <=> $b['canonicalTitle'];
$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))
if (array_key_exists('manga', $media))
{ {
foreach ($media['manga'] as $sid => $series) uasort($media['manga'], function ($a, $b) {
{ return $a['canonicalTitle'] <=> $b['canonicalTitle'];
$output[$roleName]['manga'][$sid] = $series;
}
uasort($output[$roleName]['manga'], function ($a, $b) {
return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle'];
}); });
} }
} }

View File

@ -0,0 +1,87 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @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);
}
}

222
src/Controller/User.php Normal file
View File

@ -0,0 +1,222 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.1
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @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;
}
}