Do you wish to register an account?
API client for Kitsu.io, with optional Anime collection, and optional Anilist syncing.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

374 lines
8.6 KiB

  1. <?php declare(strict_types=1);
  2. /**
  3. * Hummingbird Anime List Client
  4. *
  5. * An API client for Kitsu to manage anime and manga watch lists
  6. *
  7. * PHP version 7.1
  8. *
  9. * @package HummingbirdAnimeClient
  10. * @author Timothy J. Warren <tim@timshomepage.net>
  11. * @copyright 2015 - 2018 Timothy J. Warren
  12. * @license http://www.opensource.org/licenses/mit-license.html MIT License
  13. * @version 4.1
  14. * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
  15. */
  16. namespace Aviat\AnimeClient\Controller;
  17. use function Amp\Promise\wait;
  18. use Aviat\AnimeClient\Controller as BaseController;
  19. use Aviat\AnimeClient\API\{HummingbirdClient, JsonAPI};
  20. use Aviat\Ion\Di\ContainerInterface;
  21. use Aviat\Ion\View\HtmlView;
  22. /**
  23. * Controller for handling routes that don't fit elsewhere
  24. */
  25. final class Index extends BaseController {
  26. /**
  27. * @var \Aviat\API\Anilist\Model
  28. */
  29. private $anilistModel;
  30. /**
  31. * @var \Aviat\AnimeClient\Model\Settings
  32. */
  33. private $settingsModel;
  34. public function __construct(ContainerInterface $container)
  35. {
  36. parent::__construct($container);
  37. $this->anilistModel = $container->get('anilist-model');
  38. $this->settingsModel = $container->get('settings-model');
  39. }
  40. /**
  41. * Purges the API cache
  42. *
  43. * @return void
  44. */
  45. public function clearCache()
  46. {
  47. $this->cache->clear();
  48. $this->outputHTML('blank', [
  49. 'title' => 'Cache cleared'
  50. ]);
  51. }
  52. /**
  53. * Show the login form
  54. *
  55. * @param string $status
  56. * @return void
  57. */
  58. public function login(string $status = '')
  59. {
  60. $message = '';
  61. $view = new HtmlView($this->container);
  62. if ($status !== '')
  63. {
  64. $message = $this->showMessage($view, 'error', $status);
  65. }
  66. // Set the redirect url
  67. $this->setSessionRedirect();
  68. $this->outputHTML('login', [
  69. 'title' => 'Api login',
  70. 'message' => $message
  71. ], $view);
  72. }
  73. /**
  74. * Redirect to Anilist to start Oauth flow
  75. */
  76. public function anilistRedirect()
  77. {
  78. $redirectUrl = 'https://anilist.co/api/v2/oauth/authorize?' .
  79. http_build_query([
  80. 'client_id' => $this->config->get(['anilist', 'client_id']),
  81. 'redirect_uri' => $this->urlGenerator->url('/anilist-oauth'),
  82. 'response_type' => 'code',
  83. ]);
  84. $this->redirect($redirectUrl, 303);
  85. }
  86. /**
  87. * Oauth callback for Anilist API
  88. */
  89. public function anilistCallback()
  90. {
  91. $query = $this->request->getQueryParams();
  92. $authCode = $query['code'];
  93. $uri = $this->urlGenerator->url('/anilist-oauth');
  94. $authData = $this->anilistModel->authenticate($authCode, $uri);
  95. $settings = $this->settingsModel->getSettings();
  96. if (array_key_exists('error', $authData))
  97. {
  98. $this->errorPage(400, 'Error Linking Account', $authData['hint']);
  99. return;
  100. }
  101. // Update the override config file
  102. $anilistSettings = [
  103. 'access_token' => $authData['access_token'],
  104. 'access_token_expires' => (time() - 10) + $authData['expires_in'],
  105. 'refresh_token' => $authData['refresh_token'],
  106. ];
  107. $newSettings = $settings;
  108. $newSettings['anilist'] = array_merge($settings['anilist'], $anilistSettings);
  109. foreach($newSettings['config'] as $key => $value)
  110. {
  111. $newSettings[$key] = $value;
  112. }
  113. unset($newSettings['config']);
  114. $saved = $this->settingsModel->saveSettingsFile($newSettings);
  115. if ($saved)
  116. {
  117. $this->setFlashMessage('Linked Anilist Account', 'success');
  118. }
  119. else
  120. {
  121. $this->setFlashMessage('Error Linking Anilist Account', 'error');
  122. }
  123. $this->redirect($this->url->generate('settings'), 303);
  124. }
  125. /**
  126. * Attempt login authentication
  127. *
  128. * @return void
  129. */
  130. public function loginAction()
  131. {
  132. $auth = $this->container->get('auth');
  133. $post = $this->request->getParsedBody();
  134. if ($auth->authenticate($post['password']))
  135. {
  136. $this->sessionRedirect();
  137. return;
  138. }
  139. $this->setFlashMessage('Invalid username or password.');
  140. $this->redirect($this->url->generate('login'), 303);
  141. }
  142. /**
  143. * Deauthorize the current user
  144. *
  145. * @return void
  146. */
  147. public function logout()
  148. {
  149. $auth = $this->container->get('auth');
  150. $auth->logout();
  151. $this->redirectToDefaultRoute();
  152. }
  153. /**
  154. * Show the user profile page
  155. *
  156. * @return void
  157. */
  158. public function me()
  159. {
  160. $username = $this->config->get(['kitsu_username']);
  161. $model = $this->container->get('kitsu-model');
  162. $data = $model->getUserData($username);
  163. $orgData = JsonAPI::organizeData($data)[0];
  164. $rels = $orgData['relationships'] ?? [];
  165. $favorites = array_key_exists('favorites', $rels) ? $rels['favorites'] : [];
  166. $this->outputHTML('me', [
  167. 'title' => 'About ' . $this->config->get('whose_list'),
  168. 'data' => $orgData,
  169. 'attributes' => $orgData['attributes'],
  170. 'relationships' => $rels,
  171. 'favorites' => $this->organizeFavorites($favorites),
  172. ]);
  173. }
  174. /**
  175. * Show the user settings, if logged in
  176. */
  177. public function settings()
  178. {
  179. $auth = $this->container->get('auth');
  180. $form = $this->settingsModel->getSettingsForm();
  181. $hasAnilistLogin = $this->config->has(['anilist','access_token']);
  182. $this->outputHTML('settings', [
  183. 'anilistModel' => $this->anilistModel,
  184. 'auth' => $auth,
  185. 'form' => $form,
  186. 'hasAnilistLogin' => $hasAnilistLogin,
  187. 'config' => $this->config,
  188. 'title' => $this->config->get('whose_list') . "'s Settings",
  189. ]);
  190. }
  191. /**
  192. * Attempt to save the user's settings
  193. *
  194. * @throws \Aura\Router\Exception\RouteNotFound
  195. */
  196. public function settings_post()
  197. {
  198. $post = $this->request->getParsedBody();
  199. unset($post['settings-tabs']);
  200. // dump($post);
  201. $saved = $this->settingsModel->saveSettingsFile($post);
  202. if ($saved)
  203. {
  204. $this->setFlashMessage('Saved config settings.', 'success');
  205. }
  206. else
  207. {
  208. $this->setFlashMessage('Failed to save config file.', 'error');
  209. }
  210. $this->redirect($this->url->generate('settings'), 303);
  211. }
  212. /**
  213. * Get image covers from kitsu
  214. *
  215. * @param string $type The category of image
  216. * @param string $file The filename to look for
  217. * @param bool $display Whether to output the image to the server
  218. * @throws \Aviat\Ion\Di\ContainerException
  219. * @throws \Aviat\Ion\Di\NotFoundException
  220. * @throws \InvalidArgumentException
  221. * @throws \TypeError
  222. * @throws \Error
  223. * @throws \Throwable
  224. * @return void
  225. */
  226. public function images(string $type, string $file, $display = TRUE): void
  227. {
  228. $kitsuUrl = 'https://media.kitsu.io/';
  229. $fileName = str_replace('-original', '', $file);
  230. [$id, $ext] = explode('.', basename($fileName));
  231. $typeMap = [
  232. 'anime' => [
  233. 'kitsuUrl' => "anime/poster_images/{$id}/medium.{$ext}",
  234. 'width' => 220,
  235. ],
  236. 'avatars' => [
  237. 'kitsuUrl' => "users/avatars/{$id}/original.{$ext}",
  238. 'width' => null,
  239. ],
  240. 'characters' => [
  241. 'kitsuUrl' => "characters/images/{$id}/original.{$ext}",
  242. 'width' => 225,
  243. ],
  244. 'manga' => [
  245. 'kitsuUrl' => "manga/poster_images/{$id}/medium.{$ext}",
  246. 'width' => 220,
  247. ],
  248. 'people' => [
  249. 'kitsuUrl' => "people/images/{$id}/original.{$ext}",
  250. 'width' => null,
  251. ],
  252. ];
  253. if ( ! array_key_exists($type, $typeMap))
  254. {
  255. $this->notFound();
  256. return;
  257. }
  258. $kitsuUrl .= $typeMap[$type]['kitsuUrl'];
  259. $width = $typeMap[$type]['width'];
  260. $promise = (new HummingbirdClient)->request($kitsuUrl);
  261. $response = wait($promise);
  262. $data = wait($response->getBody());
  263. // echo "Fetching {$kitsuUrl}\n";
  264. $baseSavePath = $this->config->get('img_cache_path');
  265. $filePrefix = "{$baseSavePath}/{$type}/{$id}";
  266. [$origWidth] = getimagesizefromstring($data);
  267. $gdImg = imagecreatefromstring($data);
  268. $resizedImg = imagescale($gdImg, $width ?? $origWidth);
  269. // save the webp versions
  270. imagewebp($gdImg, "{$filePrefix}-original.webp");
  271. imagewebp($resizedImg, "{$filePrefix}.webp");
  272. // save the scaled jpeg file
  273. imagejpeg($resizedImg, "{$filePrefix}.jpg");
  274. imagedestroy($gdImg);
  275. imagedestroy($resizedImg);
  276. // And the original
  277. file_put_contents("{$filePrefix}-original.jpg", $data);
  278. if ($display)
  279. {
  280. $contentType = ($ext === 'webp')
  281. ? "image/webp"
  282. : $response->getHeader('content-type')[0];
  283. $outputFile = (strpos($file, '-original') !== FALSE)
  284. ? "{$filePrefix}-original.{$ext}"
  285. : "{$filePrefix}.{$ext}";
  286. header("Content-Type: {$contentType}");
  287. echo file_get_contents($outputFile);
  288. }
  289. }
  290. /**
  291. * Reorganize favorites data to be more useful
  292. *
  293. * @param array $rawfavorites
  294. * @return array
  295. */
  296. private function organizeFavorites(array $rawfavorites): array
  297. {
  298. $output = [];
  299. unset($rawfavorites['data']);
  300. foreach($rawfavorites as $item)
  301. {
  302. $rank = $item['attributes']['favRank'];
  303. foreach($item['relationships']['item'] as $key => $fav)
  304. {
  305. $output[$key] = $output[$key] ?? [];
  306. foreach ($fav as $id => $data)
  307. {
  308. $output[$key][$rank] = array_merge(['id' => $id], $data['attributes']);
  309. }
  310. }
  311. ksort($output[$key]);
  312. }
  313. return $output;
  314. }
  315. }