Version 5.1 - All the GraphQL #32
@ -19,17 +19,9 @@ namespace Aviat\AnimeClient;
|
||||
use Aura\Html\HelperLocatorFactory;
|
||||
use Aura\Router\RouterContainer;
|
||||
use Aura\Session\SessionFactory;
|
||||
use Aviat\AnimeClient\API\Kitsu\{
|
||||
Auth as KitsuAuth,
|
||||
ListItem as KitsuListItem,
|
||||
KitsuRequestBuilder,
|
||||
Model as KitsuModel
|
||||
};
|
||||
use Aviat\AnimeClient\API\MAL\{
|
||||
ListItem as MALListItem,
|
||||
MALRequestBuilder,
|
||||
Model as MALModel
|
||||
};
|
||||
use Aviat\AnimeClient\API\{Kitsu, MAL};
|
||||
use Aviat\AnimeClient\API\Kitsu\KitsuRequestBuilder;
|
||||
use Aviat\AnimeClient\API\MAL\MALRequestBuilder;
|
||||
use Aviat\AnimeClient\Model;
|
||||
use Aviat\Banker\Pool;
|
||||
use Aviat\Ion\Config;
|
||||
@ -119,15 +111,15 @@ return function(array $config_array = []) {
|
||||
$container->set('kitsu-model', function($container) {
|
||||
$requestBuilder = new KitsuRequestBuilder();
|
||||
$requestBuilder->setLogger($container->getLogger('kitsu-request'));
|
||||
|
||||
$listItem = new KitsuListItem();
|
||||
|
||||
$listItem = new Kitsu\ListItem();
|
||||
$listItem->setContainer($container);
|
||||
$listItem->setRequestBuilder($requestBuilder);
|
||||
|
||||
$model = new KitsuModel($listItem);
|
||||
|
||||
$model = new Kitsu\Model($listItem);
|
||||
$model->setContainer($container);
|
||||
$model->setRequestBuilder($requestBuilder);
|
||||
|
||||
|
||||
$cache = $container->get('cache');
|
||||
$model->setCache($cache);
|
||||
return $model;
|
||||
@ -135,12 +127,12 @@ return function(array $config_array = []) {
|
||||
$container->set('mal-model', function($container) {
|
||||
$requestBuilder = new MALRequestBuilder();
|
||||
$requestBuilder->setLogger($container->getLogger('mal-request'));
|
||||
|
||||
$listItem = new MALListItem();
|
||||
|
||||
$listItem = new MAL\ListItem();
|
||||
$listItem->setContainer($container);
|
||||
$listItem->setRequestBuilder($requestBuilder);
|
||||
|
||||
$model = new MALModel($listItem);
|
||||
|
||||
$model = new MAL\Model($listItem);
|
||||
$model->setContainer($container);
|
||||
$model->setRequestBuilder($requestBuilder);
|
||||
return $model;
|
||||
@ -161,7 +153,7 @@ return function(array $config_array = []) {
|
||||
|
||||
// Miscellaneous Classes
|
||||
$container->set('auth', function($container) {
|
||||
return new KitsuAuth($container);
|
||||
return new Kitsu\Auth($container);
|
||||
});
|
||||
$container->set('url-generator', function($container) {
|
||||
return new UrlGenerator($container);
|
||||
|
@ -14,6 +14,11 @@
|
||||
* @link https://github.com/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
use const Aviat\AnimeClient\{
|
||||
DEFAULT_CONTROLLER_METHOD,
|
||||
DEFAULT_CONTROLLER_NAMESPACE
|
||||
};
|
||||
|
||||
use Aviat\AnimeClient\AnimeClient;
|
||||
|
||||
return [
|
||||
@ -148,25 +153,25 @@ return [
|
||||
'cache_purge' => [
|
||||
'path' => '/cache_purge',
|
||||
'action' => 'clearCache',
|
||||
'controller' => AnimeClient::DEFAULT_CONTROLLER_NAMESPACE,
|
||||
'controller' => DEFAULT_CONTROLLER_NAMESPACE,
|
||||
'verb' => 'get',
|
||||
],
|
||||
'login' => [
|
||||
'path' => '/login',
|
||||
'action' => 'login',
|
||||
'controller' => AnimeClient::DEFAULT_CONTROLLER_NAMESPACE,
|
||||
'controller' => DEFAULT_CONTROLLER_NAMESPACE,
|
||||
'verb' => 'get',
|
||||
],
|
||||
'login.post' => [
|
||||
'path' => '/login',
|
||||
'action' => 'loginAction',
|
||||
'controller' => AnimeClient::DEFAULT_CONTROLLER_NAMESPACE,
|
||||
'controller' => DEFAULT_CONTROLLER_NAMESPACE,
|
||||
'verb' => 'post',
|
||||
],
|
||||
'logout' => [
|
||||
'path' => '/logout',
|
||||
'action' => 'logout',
|
||||
'controller' => AnimeClient::DEFAULT_CONTROLLER_NAMESPACE,
|
||||
'controller' => DEFAULT_CONTROLLER_NAMESPACE,
|
||||
],
|
||||
'update' => [
|
||||
'path' => '/{controller}/update',
|
||||
@ -194,7 +199,7 @@ return [
|
||||
],
|
||||
'list' => [
|
||||
'path' => '/{controller}/{type}{/view}',
|
||||
'action' => AnimeClient::DEFAULT_CONTROLLER_METHOD,
|
||||
'action' => DEFAULT_CONTROLLER_METHOD,
|
||||
'tokens' => [
|
||||
'type' => '[a-z_]+',
|
||||
'view' => '[a-z_]+',
|
||||
@ -202,7 +207,7 @@ return [
|
||||
],
|
||||
'index_redirect' => [
|
||||
'path' => '/',
|
||||
'controller' => AnimeClient::DEFAULT_CONTROLLER_NAMESPACE,
|
||||
'controller' => DEFAULT_CONTROLLER_NAMESPACE,
|
||||
'action' => 'redirectToDefaultRoute',
|
||||
],
|
||||
],
|
||||
|
@ -3,6 +3,9 @@
|
||||
"description": "A self-hosted anime/manga client for Kitsu.",
|
||||
"license":"MIT",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/AnimeClient.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Aviat\\AnimeClient\\": "src/"
|
||||
}
|
||||
@ -20,7 +23,6 @@
|
||||
"aviat/banker": "^1.0.0",
|
||||
"aviat/ion": "1.0.*",
|
||||
"filp/whoops": "^2.1.5",
|
||||
"guzzlehttp/guzzle": "^6.0",
|
||||
"monolog/monolog": "^1.0",
|
||||
"psr/http-message": "~1.0",
|
||||
"psr/log": "~1.0",
|
||||
@ -46,4 +48,4 @@
|
||||
"build:css": "cd public && npm run build && cd ..",
|
||||
"watch:css": "cd public && npm run watch"
|
||||
}
|
||||
}
|
||||
}
|
15
index.php
15
index.php
@ -15,6 +15,8 @@
|
||||
*/
|
||||
namespace Aviat\AnimeClient;
|
||||
|
||||
use function Aviat\AnimeClient\loadToml;
|
||||
|
||||
use Aviat\AnimeClient\AnimeClient;
|
||||
use Whoops\Handler\PrettyPageHandler;
|
||||
use Whoops\Run;
|
||||
@ -42,7 +44,7 @@ $APP_DIR = _dir(__DIR__, 'app');
|
||||
$CONF_DIR = _dir($APP_DIR, 'config');
|
||||
|
||||
// Load composer autoloader
|
||||
require _dir(__DIR__, '/vendor/autoload.php');
|
||||
require _dir(__DIR__, 'vendor/autoload.php');
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Setup error handling
|
||||
@ -54,10 +56,7 @@ $defaultHandler = new PrettyPageHandler();
|
||||
$whoops->pushHandler($defaultHandler);
|
||||
|
||||
// Register as the error handler
|
||||
if (array_key_exists('whoops', $_GET))
|
||||
{
|
||||
$whoops->register();
|
||||
}
|
||||
$whoops->register();
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Dependency Injection setup
|
||||
@ -65,7 +64,7 @@ if (array_key_exists('whoops', $_GET))
|
||||
require _dir($CONF_DIR, 'base_config.php'); // $base_config
|
||||
$di = require _dir($APP_DIR, 'bootstrap.php');
|
||||
|
||||
$config = AnimeClient::loadToml($CONF_DIR);
|
||||
$config = loadToml($CONF_DIR);
|
||||
$config_array = array_merge($base_config, $config);
|
||||
|
||||
$container = $di($config_array);
|
||||
@ -77,6 +76,4 @@ unset($CONF_DIR);
|
||||
// -----------------------------------------------------------------------------
|
||||
// Dispatch to the current route
|
||||
// -----------------------------------------------------------------------------
|
||||
$container->get('dispatcher')->__invoke();
|
||||
|
||||
// End of index.php
|
||||
$container->get('dispatcher')->__invoke();
|
@ -16,12 +16,14 @@
|
||||
|
||||
namespace Aviat\AnimeClient\API;
|
||||
|
||||
use Amp;
|
||||
use Amp\Artax\{
|
||||
Client,
|
||||
FormBody,
|
||||
Request
|
||||
};
|
||||
use Aviat\Ion\Di\ContainerAware;
|
||||
use Aviat\Ion\Json;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
|
||||
@ -67,6 +69,21 @@ class APIRequestBuilder {
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* Set an authorization header
|
||||
*
|
||||
* @param string $type The type of authorization, eg, basic, bearer, etc.
|
||||
* @param string $value The authorization value
|
||||
* @return self
|
||||
*/
|
||||
public function setAuth(string $type, string $value): self
|
||||
{
|
||||
$authString = ucfirst($type) . ' ' . $value;
|
||||
$this->setHeader('Authorization', $authString);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a basic authentication header
|
||||
*
|
||||
@ -76,9 +93,7 @@ class APIRequestBuilder {
|
||||
*/
|
||||
public function setBasicAuth(string $username, string $password): self
|
||||
{
|
||||
$authString = 'Basic ' . base64_encode($username . ':' . $password);
|
||||
$this->setHeader('Authorization', $authString);
|
||||
|
||||
$this->setAuth('basic', base64_encode($username . ':' . $password));
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -139,6 +154,18 @@ class APIRequestBuilder {
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the request body
|
||||
*
|
||||
* @param array|FormBody|string $body
|
||||
* @return self
|
||||
*/
|
||||
public function setJsonBody(array $body): self
|
||||
{
|
||||
$requestBody = Json::encode($body);
|
||||
return $this->setBody($requestBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a query string in array format
|
||||
*
|
||||
@ -159,7 +186,7 @@ class APIRequestBuilder {
|
||||
public function getFullRequest()
|
||||
{
|
||||
$this->buildUri();
|
||||
|
||||
|
||||
if ($this->logger)
|
||||
{
|
||||
$this->logger->debug('API Request', [
|
||||
@ -168,7 +195,7 @@ class APIRequestBuilder {
|
||||
'request_body' => $this->request->getBody()
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
@ -191,13 +218,13 @@ class APIRequestBuilder {
|
||||
$this->request
|
||||
->setMethod($type)
|
||||
->setProtocol('1.1');
|
||||
|
||||
|
||||
$this->path = $uri;
|
||||
|
||||
|
||||
if ( ! empty($this->defaultHeaders))
|
||||
{
|
||||
$this->setHeaders($this->defaultHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -232,7 +259,7 @@ class APIRequestBuilder {
|
||||
*/
|
||||
private function fixBody(FormBody $formBody): string
|
||||
{
|
||||
$rawBody = \Amp\wait($formBody->getBody());
|
||||
$rawBody = Amp\wait($formBody->getBody());
|
||||
return html_entity_decode($rawBody, \ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
|
@ -16,24 +16,8 @@
|
||||
|
||||
namespace Aviat\AnimeClient\API;
|
||||
|
||||
use Amp;
|
||||
use Amp\Artax\{
|
||||
Client,
|
||||
Response,
|
||||
Request
|
||||
}
|
||||
use UnexpectedValueException;
|
||||
|
||||
class APIClient {
|
||||
class FailedResponseException extends UnexpectedValueException {
|
||||
|
||||
/**
|
||||
* Get a syncronous response for a request
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
static public function syncResponse(Request $request): Response
|
||||
{
|
||||
$client = new Client();
|
||||
return wait($client->request($request));
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Anime List Client
|
||||
*
|
||||
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7
|
||||
*
|
||||
* @package AnimeListClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2017 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;
|
||||
|
||||
/**
|
||||
* Base trait for api interaction
|
||||
*/
|
||||
trait GuzzleTrait {
|
||||
/**
|
||||
* The Guzzle http client object
|
||||
* @var object
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* Cookie jar object for api requests
|
||||
* @var object
|
||||
*/
|
||||
protected $cookieJar;
|
||||
|
||||
/**
|
||||
* Set up the class properties
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function init();
|
||||
}
|
@ -23,6 +23,10 @@ use Aviat\AnimeClient\API\Kitsu\Enum\{
|
||||
};
|
||||
use DateTimeImmutable;
|
||||
|
||||
const AUTH_URL = 'https://kitsu.io/api/oauth/token';
|
||||
const AUTH_USER_ID_KEY = 'kitsu-auth-userid';
|
||||
const AUTH_TOKEN_CACHE_KEY = 'kitsu-auth-token';
|
||||
|
||||
/**
|
||||
* Data massaging helpers for the Kitsu API
|
||||
*/
|
||||
@ -91,7 +95,7 @@ class Kitsu {
|
||||
return AnimeAiringStatus::NOT_YET_AIRED;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the name and logo for the streaming service of the current link
|
||||
*
|
||||
@ -108,22 +112,22 @@ class Kitsu {
|
||||
'link' => true,
|
||||
'logo' => '<svg class="streaming-logo" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><g fill="#F78B24" fill-rule="evenodd"><path d="M22.549 49.145c-.815-.077-2.958-.456-3.753-.663-6.873-1.79-12.693-6.59-15.773-13.009C1.335 31.954.631 28.807.633 24.788c.003-4.025.718-7.235 2.38-10.686 1.243-2.584 2.674-4.609 4.706-6.66 3.8-3.834 8.614-6.208 14.067-6.936 1.783-.239 5.556-.161 7.221.148 3.463.642 6.571 1.904 9.357 3.797 5.788 3.934 9.542 9.951 10.52 16.861.21 1.48.332 4.559.19 4.816-.077.14-.117-.007-.167-.615-.25-3.015-1.528-6.66-3.292-9.388C40.253 7.836 30.249 4.32 20.987 7.467c-7.15 2.43-12.522 8.596-13.997 16.06-.73 3.692-.51 7.31.658 10.882a21.426 21.426 0 0 0 13.247 13.518c1.475.515 3.369.944 4.618 1.047 1.496.122 1.119.239-.727.224-1.006-.008-2.013-.032-2.237-.053z"></path><path d="M27.685 46.1c-7.731-.575-14.137-6.455-15.474-14.204-.243-1.41-.29-4.047-.095-5.345 1.16-7.706 6.97-13.552 14.552-14.639 1.537-.22 4.275-.143 5.746.162 1.28.266 2.7.737 3.814 1.266l.865.411-.814.392c-2.936 1.414-4.748 4.723-4.323 7.892.426 3.173 2.578 5.664 5.667 6.56 1.112.322 2.812.322 3.925 0 1.438-.417 2.566-1.1 3.593-2.173.346-.362.652-.621.68-.576.027.046.106.545.176 1.11.171 1.395.07 4.047-.204 5.371-.876 4.218-3.08 7.758-6.463 10.374-3.2 2.476-7.434 3.711-11.645 3.399z"></path></g></svg>'
|
||||
];
|
||||
|
||||
|
||||
case 'www.funimation.com':
|
||||
return [
|
||||
'name' => 'Funimation',
|
||||
'link' => true,
|
||||
'logo' => '<svg class="streaming-logo" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="M24.066.017a24.922 24.922 0 0 1 13.302 3.286 25.098 25.098 0 0 1 7.833 7.058 24.862 24.862 0 0 1 4.207 9.575c.82 4.001.641 8.201-.518 12.117a24.946 24.946 0 0 1-4.868 9.009 24.98 24.98 0 0 1-7.704 6.118 24.727 24.727 0 0 1-10.552 2.718A24.82 24.82 0 0 1 13.833 47.3c-5.815-2.872-10.408-8.107-12.49-14.25-2.162-6.257-1.698-13.375 1.303-19.28C5.483 8.07 10.594 3.55 16.602 1.435A24.94 24.94 0 0 1 24.066.017zm-8.415 33.31c.464 2.284 1.939 4.358 3.99 5.48 2.174 1.217 4.765 1.444 7.202 1.181 2.002-.217 3.986-.992 5.455-2.397 1.173-1.151 2.017-2.648 2.33-4.267-1.189-.027-2.378 0-3.566-.03-.568.082-1.137-.048-1.705.014-1.232.012-2.465.003-3.697-.01-.655.066-1.309-.035-1.963.013-1.166-.053-2.334.043-3.5-.025-1.515.08-3.03-.035-4.546.042z" fill="#411299" fill-rule="evenodd"></path></svg>'
|
||||
];
|
||||
|
||||
|
||||
case 'www.hulu.com':
|
||||
return [
|
||||
'name' => 'Hulu',
|
||||
'link' => true,
|
||||
'logo' => '<svg class="streaming-logo" viewBox="0 0 34 50" xmlns="http://www.w3.org/2000/svg"><path d="M22.222 13.889h-11.11V0H0v50h11.111V27.778c0-1.39 1.111-2.778 2.778-2.778h5.555c1.39 0 2.778 1.111 2.778 2.778V50h11.111V25c0-6.111-5-11.111-11.11-11.111z" fill="#8BC34A" fill-rule="evenodd"></path></svg>'
|
||||
];
|
||||
|
||||
// Default to Netflix, because the API links are broken,
|
||||
|
||||
// Default to Netflix, because the API links are broken,
|
||||
// and there's no other real identifier for Netflix
|
||||
default:
|
||||
return [
|
||||
@ -133,7 +137,7 @@ class Kitsu {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reorganize streaming links
|
||||
*
|
||||
@ -146,13 +150,13 @@ class Kitsu {
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
$links = [];
|
||||
|
||||
|
||||
foreach ($included['streamingLinks'] as $streamingLink)
|
||||
{
|
||||
$host = parse_url($streamingLink['url'], \PHP_URL_HOST);
|
||||
|
||||
|
||||
$links[] = [
|
||||
'meta' => static::getServiceMetaData($host),
|
||||
'link' => $streamingLink['url'],
|
||||
@ -160,10 +164,10 @@ class Kitsu {
|
||||
'dubs' => $streamingLink['dubs']
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reorganize streaming links for the current list item
|
||||
*
|
||||
@ -192,10 +196,10 @@ class Kitsu {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,9 @@
|
||||
|
||||
namespace Aviat\AnimeClient\API\Kitsu;
|
||||
|
||||
use const Aviat\AnimeClient\SESSION_SEGMENT;
|
||||
use const Aviat\AnimeClient\API\Kitsu\AUTH_TOKEN_CACHE_KEY;
|
||||
|
||||
use Aviat\AnimeClient\AnimeClient;
|
||||
use Aviat\AnimeClient\API\{
|
||||
CacheTrait,
|
||||
@ -55,7 +58,7 @@ class Auth {
|
||||
$this->setContainer($container);
|
||||
$this->setCache($container->get('cache'));
|
||||
$this->segment = $container->get('session')
|
||||
->getSegment(AnimeClient::SESSION_SEGMENT);
|
||||
->getSegment(SESSION_SEGMENT);
|
||||
$this->model = $container->get('kitsu-model');
|
||||
}
|
||||
|
||||
@ -70,7 +73,7 @@ class Auth {
|
||||
{
|
||||
$config = $this->container->get('config');
|
||||
$username = $config->get(['kitsu_username']);
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
$auth = $this->model->authenticate($username, $password);
|
||||
@ -79,15 +82,15 @@ class Auth {
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (FALSE !== $auth)
|
||||
{
|
||||
// Set the token in the cache for command line operations
|
||||
$cacheItem = $this->cache->getItem(K::AUTH_TOKEN_CACHE_KEY);
|
||||
$cacheItem = $this->cache->getItem(AUTH_TOKEN_CACHE_KEY);
|
||||
$cacheItem->set($auth['access_token']);
|
||||
$cacheItem->save();
|
||||
|
||||
|
||||
$this->segment->set('auth_token', $auth['access_token']);
|
||||
return TRUE;
|
||||
}
|
||||
|
@ -20,8 +20,8 @@ use Aviat\AnimeClient\API\APIRequestBuilder;
|
||||
use Aviat\AnimeClient\API\Kitsu as K;
|
||||
use Aviat\Ion\Json;
|
||||
|
||||
class KitsuRequestBuilder extends APIRequestBuilder {
|
||||
|
||||
class KitsuRequestBuilder extends APIRequestBuilder {
|
||||
|
||||
/**
|
||||
* The base url for api requests
|
||||
* @var string $base_url
|
||||
@ -40,16 +40,4 @@ class KitsuRequestBuilder extends APIRequestBuilder {
|
||||
'client_id' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd',
|
||||
'client_secret' => '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151',
|
||||
];
|
||||
|
||||
/**
|
||||
* Set the request body
|
||||
*
|
||||
* @param array|FormBody|string $body
|
||||
* @return self
|
||||
*/
|
||||
public function setJsonBody(array $body): self
|
||||
{
|
||||
$requestBody = Json::encode($body);
|
||||
return $this->setBody($requestBody);
|
||||
}
|
||||
}
|
||||
}
|
@ -16,13 +16,14 @@
|
||||
|
||||
namespace Aviat\AnimeClient\API\Kitsu;
|
||||
|
||||
use const Aviat\AnimeClient\SESSION_SEGMENT;
|
||||
|
||||
use function Amp\wait;
|
||||
|
||||
use Amp\Artax\Client;
|
||||
use Aviat\AnimeClient\AnimeClient;
|
||||
use Aviat\AnimeClient\API\GuzzleTrait;
|
||||
use Aviat\AnimeClient\API\Kitsu as K;
|
||||
use Aviat\Ion\Json;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Cookie\CookieJar;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
@ -34,37 +35,6 @@ trait KitsuTrait {
|
||||
*/
|
||||
protected $requestBuilder;
|
||||
|
||||
/**
|
||||
* The Guzzle http client object
|
||||
* @var object
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* Cookie jar object for api requests
|
||||
* @var object
|
||||
*/
|
||||
protected $cookieJar;
|
||||
|
||||
/**
|
||||
* The base url for api requests
|
||||
* @var string $base_url
|
||||
*/
|
||||
protected $baseUrl = "https://kitsu.io/api/edge/";
|
||||
|
||||
/**
|
||||
* HTTP headers to send with every request
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultHeaders = [
|
||||
'User-Agent' => "Tim's Anime Client/4.0",
|
||||
'Accept-Encoding' => 'application/vnd.api+json',
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
'client_id' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd',
|
||||
'client_secret' => '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151',
|
||||
];
|
||||
|
||||
/**
|
||||
* Set the request builder object
|
||||
*
|
||||
@ -78,30 +48,45 @@ trait KitsuTrait {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the class properties
|
||||
* Create a request object
|
||||
*
|
||||
* @return void
|
||||
* @param string $type
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @return \Amp\Artax\Response
|
||||
*/
|
||||
protected function init()
|
||||
public function setUpRequest(string $type, string $url, array $options = [])
|
||||
{
|
||||
$defaults = [
|
||||
'cookies' => $this->cookieJar,
|
||||
'headers' => $this->defaultHeaders,
|
||||
'timeout' => 25,
|
||||
'connect_timeout' => 25
|
||||
];
|
||||
$config = $this->container->get('config');
|
||||
|
||||
$this->cookieJar = new CookieJar();
|
||||
$this->client = new Client([
|
||||
'base_uri' => $this->baseUrl,
|
||||
'cookies' => TRUE,
|
||||
'http_errors' => TRUE,
|
||||
'defaults' => $defaults
|
||||
]);
|
||||
$request = $this->requestBuilder->newRequest($type, $url);
|
||||
|
||||
$sessionSegment = $this->getContainer()
|
||||
->get('session')
|
||||
->getSegment(SESSION_SEGMENT);
|
||||
|
||||
if ($sessionSegment->get('auth_token') !== null && $url !== K::AUTH_URL)
|
||||
{
|
||||
$token = $sessionSegment->get('auth_token');
|
||||
$request = $request->setAuth('bearer', $token);
|
||||
// $defaultOptions['headers']['Authorization'] = "bearer {$token}";
|
||||
}
|
||||
|
||||
if (array_key_exists('query', $options))
|
||||
{
|
||||
$request->setQuery($options['query']);
|
||||
}
|
||||
|
||||
if (array_key_exists('body', $options))
|
||||
{
|
||||
$request->setJsonBody($options['body']);
|
||||
}
|
||||
|
||||
return $request->getFullRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request via Guzzle
|
||||
* Make a request
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $url
|
||||
@ -110,48 +95,24 @@ trait KitsuTrait {
|
||||
*/
|
||||
private function getResponse(string $type, string $url, array $options = [])
|
||||
{
|
||||
$logger = null;
|
||||
$validTypes = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
|
||||
|
||||
if ( ! in_array($type, $validTypes))
|
||||
{
|
||||
throw new InvalidArgumentException('Invalid http request type');
|
||||
}
|
||||
|
||||
$defaultOptions = [
|
||||
'headers' => $this->defaultHeaders
|
||||
];
|
||||
|
||||
$request = $this->setUpRequest($type, $url, $options);
|
||||
$logger = $this->container->getLogger('kitsu-request');
|
||||
$sessionSegment = $this->getContainer()
|
||||
->get('session')
|
||||
->getSegment(AnimeClient::SESSION_SEGMENT);
|
||||
|
||||
if ($sessionSegment->get('auth_token') !== null && $url !== K::AUTH_URL)
|
||||
{
|
||||
$token = $sessionSegment->get('auth_token');
|
||||
$defaultOptions['headers']['Authorization'] = "bearer {$token}";
|
||||
}
|
||||
$response = wait((new Client)->request($request));
|
||||
|
||||
$options = array_merge($defaultOptions, $options);
|
||||
|
||||
$response = $this->client->request($type, $url, $options);
|
||||
|
||||
$logger->debug('Kitsu API request', [
|
||||
'requestParams' => [
|
||||
'type' => $type,
|
||||
'url' => $url,
|
||||
],
|
||||
'responseValues' => [
|
||||
'status' => $response->getStatusCode()
|
||||
]
|
||||
]);
|
||||
/* $logger->debug('Kitsu api response', [
|
||||
'status' => $response->getStatus(),
|
||||
'reason' => $response->getReason(),
|
||||
'body' => $response->getBody(),
|
||||
'headers' => $response->getAllHeaders(),
|
||||
'requestHeaders' => $request->getAllHeaders(),
|
||||
]); */
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request via Guzzle
|
||||
* Make a request
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $url
|
||||
@ -168,7 +129,7 @@ trait KitsuTrait {
|
||||
|
||||
$response = $this->getResponse($type, $url, $options);
|
||||
|
||||
if ((int) $response->getStatusCode() > 299 || (int) $response->getStatusCode() < 200)
|
||||
if ((int) $response->getStatus() > 299 || (int) $response->getStatus() < 200)
|
||||
{
|
||||
if ($logger)
|
||||
{
|
||||
@ -218,7 +179,7 @@ trait KitsuTrait {
|
||||
$response = $this->getResponse('POST', ...$args);
|
||||
$validResponseCodes = [200, 201];
|
||||
|
||||
if ( ! in_array((int) $response->getStatusCode(), $validResponseCodes))
|
||||
if ( ! in_array((int) $response->getStatus(), $validResponseCodes))
|
||||
{
|
||||
if ($logger)
|
||||
{
|
||||
@ -238,6 +199,6 @@ trait KitsuTrait {
|
||||
protected function deleteRequest(...$args): bool
|
||||
{
|
||||
$response = $this->getResponse('DELETE', ...$args);
|
||||
return ((int) $response->getStatusCode() === 204);
|
||||
return ((int) $response->getStatus() === 204);
|
||||
}
|
||||
}
|
@ -16,11 +16,12 @@
|
||||
|
||||
namespace Aviat\AnimeClient\API\Kitsu;
|
||||
|
||||
use const Aviat\AnimeClient\SESSION_SEGMENT;
|
||||
|
||||
use Amp\Artax\Request;
|
||||
use Aviat\AnimeClient\API\AbstractListItem;
|
||||
use Aviat\Ion\Di\ContainerAware;
|
||||
use Aviat\Ion\Json;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
@ -30,12 +31,22 @@ class ListItem extends AbstractListItem {
|
||||
use ContainerAware;
|
||||
use KitsuTrait;
|
||||
|
||||
public function __construct()
|
||||
private function getAuthHeader()
|
||||
{
|
||||
$this->init();
|
||||
$sessionSegment = $this->getContainer()
|
||||
->get('session')
|
||||
->getSegment(SESSION_SEGMENT);
|
||||
|
||||
if ($sessionSegment->get('auth_token') !== null)
|
||||
{
|
||||
$token = $sessionSegment->get('auth_token');
|
||||
return "bearer {$token}";
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
public function create(array $data): bool
|
||||
public function create(array $data): Request
|
||||
{
|
||||
$body = [
|
||||
'data' => [
|
||||
@ -60,21 +71,35 @@ class ListItem extends AbstractListItem {
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$request = $this->requestBuilder->newRequest('POST', 'library-entries')
|
||||
->setJsonBody($body)
|
||||
->getFullRequest();
|
||||
$response = $this->getResponse('POST', 'library-entries', [
|
||||
'body' => Json::encode($body)
|
||||
]);
|
||||
|
||||
return ($response->getStatusCode() === 201);
|
||||
$authHeader = $this->getAuthHeader();
|
||||
|
||||
$request = $this->requestBuilder->newRequest('POST', 'library-entries');
|
||||
|
||||
if ($authHeader !== FALSE)
|
||||
{
|
||||
$request = $request->setHeader('Authorization', $authHeader);
|
||||
}
|
||||
|
||||
return $request->setJsonBody($body)
|
||||
->getFullRequest();
|
||||
|
||||
// return ($response->getStatus() === 201);
|
||||
}
|
||||
|
||||
public function delete(string $id): bool
|
||||
public function delete(string $id): Request
|
||||
{
|
||||
$response = $this->getResponse('DELETE', "library-entries/{$id}");
|
||||
return ($response->getStatusCode() === 204);
|
||||
$authHeader = $this->getAuthHeader();
|
||||
$request = $this->requestBuilder->newRequest('DELETE', "library-entries/{$id}");
|
||||
|
||||
if ($authHeader !== FALSE)
|
||||
{
|
||||
$request = $request->setHeader('Authorization', $authHeader);
|
||||
}
|
||||
|
||||
return $request->getFullRequest();
|
||||
|
||||
// return ($response->getStatus() === 204);
|
||||
}
|
||||
|
||||
public function get(string $id): array
|
||||
@ -84,17 +109,12 @@ class ListItem extends AbstractListItem {
|
||||
'include' => 'media,media.genres,media.mappings'
|
||||
])
|
||||
->getFullRequest();
|
||||
/*return $this->getRequest("library-entries/{$id}", [
|
||||
'query' => [
|
||||
'include' => 'media,media.genres,media.mappings'
|
||||
]
|
||||
]);*/
|
||||
|
||||
|
||||
$response = \Amp\wait((new \Amp\Artax\Client)->request($request));
|
||||
return Json::decode($response->getBody());
|
||||
}
|
||||
|
||||
public function update(string $id, array $data): Response
|
||||
public function update(string $id, array $data): Request
|
||||
{
|
||||
$requestData = [
|
||||
'data' => [
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
namespace Aviat\AnimeClient\API\Kitsu;
|
||||
|
||||
use Amp\Artax\Request;
|
||||
use Aviat\AnimeClient\API\CacheTrait;
|
||||
use Aviat\AnimeClient\API\JsonAPI;
|
||||
use Aviat\AnimeClient\API\Kitsu as K;
|
||||
@ -27,7 +28,6 @@ use Aviat\AnimeClient\API\Kitsu\Transformer\{
|
||||
};
|
||||
use Aviat\Ion\Di\ContainerAware;
|
||||
use Aviat\Ion\Json;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
|
||||
/**
|
||||
* Kitsu API Model
|
||||
@ -72,9 +72,6 @@ class Model {
|
||||
*/
|
||||
public function __construct(ListItem $listItem)
|
||||
{
|
||||
// Set up Guzzle trait
|
||||
$this->init();
|
||||
|
||||
$this->animeTransformer = new AnimeTransformer();
|
||||
$this->animeListTransformer = new AnimeListTransformer();
|
||||
$this->listItem = $listItem;
|
||||
@ -355,9 +352,9 @@ class Model {
|
||||
* Create a list item
|
||||
*
|
||||
* @param array $data
|
||||
* @return bool
|
||||
* @return Request
|
||||
*/
|
||||
public function createListItem(array $data): bool
|
||||
public function createListItem(array $data): Request
|
||||
{
|
||||
$data['user_id'] = $this->getUserIdByUsername($this->getUsername());
|
||||
return $this->listItem->create($data);
|
||||
@ -397,22 +394,22 @@ class Model {
|
||||
* Modify a list item
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
* @return Request
|
||||
*/
|
||||
public function updateListItem(array $data)
|
||||
public function updateListItem(array $data): Request
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = $this->listItem->update($data['id'], $data['data']);
|
||||
return [
|
||||
'statusCode' => $response->getStatusCode(),
|
||||
'statusCode' => $response->getStatus(),
|
||||
'body' => $response->getBody(),
|
||||
];
|
||||
}
|
||||
catch(ClientException $e)
|
||||
{
|
||||
return [
|
||||
'statusCode' => $e->getResponse()->getStatusCode(),
|
||||
'statusCode' => $e->getResponse()->getStatus(),
|
||||
'body' => Json::decode((string)$e->getResponse()->getBody())
|
||||
];
|
||||
}
|
||||
@ -422,9 +419,9 @@ class Model {
|
||||
* Remove a list item
|
||||
*
|
||||
* @param string $id - The id of the list item to remove
|
||||
* @return bool
|
||||
* @return Request
|
||||
*/
|
||||
public function deleteListItem(string $id): bool
|
||||
public function deleteListItem(string $id): Request
|
||||
{
|
||||
return $this->listItem->delete($id);
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
namespace Aviat\AnimeClient\API;
|
||||
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Amp\Artax\Request;
|
||||
|
||||
/**
|
||||
* Common interface for anime and manga list item CRUD
|
||||
@ -29,7 +29,7 @@ interface ListItemInterface {
|
||||
* @param array $data -
|
||||
* @return bool
|
||||
*/
|
||||
public function create(array $data): bool;
|
||||
public function create(array $data): Request;
|
||||
|
||||
/**
|
||||
* Retrieve a list item
|
||||
@ -46,7 +46,7 @@ interface ListItemInterface {
|
||||
* @param array $data - The data with which to update the list item
|
||||
* @return Response
|
||||
*/
|
||||
public function update(string $id, array $data): Response;
|
||||
public function update(string $id, array $data): Request;
|
||||
|
||||
/**
|
||||
* Delete a list item
|
||||
@ -54,5 +54,5 @@ interface ListItemInterface {
|
||||
* @param string $id - The id of the list item to delete
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(string $id): bool;
|
||||
public function delete(string $id): Request;
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
|
||||
namespace Aviat\AnimeClient\API\MAL;
|
||||
|
||||
use Amp\Artax\FormBody;
|
||||
use Amp\Artax\Request;
|
||||
use Aviat\AnimeClient\API\{
|
||||
AbstractListItem,
|
||||
XML
|
||||
@ -30,7 +30,7 @@ class ListItem {
|
||||
use ContainerAware;
|
||||
use MALTrait;
|
||||
|
||||
public function create(array $data)
|
||||
public function create(array $data): Request
|
||||
{
|
||||
$id = $data['id'];
|
||||
$createData = [
|
||||
@ -40,29 +40,36 @@ class ListItem {
|
||||
])
|
||||
];
|
||||
|
||||
// $config = $this->container->get('config');
|
||||
$config = $this->container->get('config');
|
||||
|
||||
/*$request = $this->requestBuilder->newRequest('POST', "animelist/add/{$id}.xml")
|
||||
return $this->requestBuilder->newRequest('POST', "animelist/add/{$id}.xml")
|
||||
->setFormFields($createData)
|
||||
->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password']))
|
||||
->getFullRequest();*/
|
||||
->getFullRequest();
|
||||
|
||||
$response = $this->getResponse('POST', "animelist/add/{$id}.xml", [
|
||||
/* $response = $this->getResponse('POST', "animelist/add/{$id}.xml", [
|
||||
'body' => $this->fixBody((new FormBody)->addFields($createData))
|
||||
]);
|
||||
|
||||
return $response->getBody() === 'Created';
|
||||
|
||||
// return $request;
|
||||
return $response->getBody() === 'Created'; */
|
||||
}
|
||||
|
||||
public function delete(string $id): bool
|
||||
public function delete(string $id): Request
|
||||
{
|
||||
$response = $this->getResponse('DELETE', "animelist/delete/{$id}.xml", [
|
||||
$config = $this->container->get('config');
|
||||
|
||||
return $this->requestBuilder->newRequest('DELETE', "animelist/delete/{$id}.xml")
|
||||
->setFormFields([
|
||||
'id' => $id
|
||||
])
|
||||
->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password']))
|
||||
->getFullRequest();
|
||||
|
||||
/*$response = $this->getResponse('DELETE', "animelist/delete/{$id}.xml", [
|
||||
'body' => $this->fixBody((new FormBody)->addField('id', $id))
|
||||
]);
|
||||
|
||||
return $response->getBody() === 'Deleted';
|
||||
return $response->getBody() === 'Deleted';*/
|
||||
}
|
||||
|
||||
public function get(string $id): array
|
||||
@ -70,15 +77,25 @@ class ListItem {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function update(string $id, array $data)
|
||||
public function update(string $id, array $data): Request
|
||||
{
|
||||
$config = $this->container->get('config');
|
||||
|
||||
$xml = XML::toXML(['entry' => $data]);
|
||||
$body = (new FormBody)
|
||||
->addField('id', $id)
|
||||
->addField('data', $xml);
|
||||
|
||||
return $this->getResponse('POST', "animelist/update/{$id}.xml", [
|
||||
return $this->requestBuilder->newRequest('POST', "animelist/update/{$id}.xml")
|
||||
->setFormFields([
|
||||
'id' => $id,
|
||||
'data' => $xml
|
||||
])
|
||||
->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password']))
|
||||
->getFullRequest();
|
||||
|
||||
/* return $this->getResponse('POST', "animelist/update/{$id}.xml", [
|
||||
'body' => $this->fixBody($body)
|
||||
]);
|
||||
]); */
|
||||
}
|
||||
}
|
@ -85,7 +85,7 @@ trait MALTrait {
|
||||
* @param string $type
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @return \Amp\Promise
|
||||
* @return \Amp\Artax\Response
|
||||
*/
|
||||
public function setUpRequest(string $type, string $url, array $options = [])
|
||||
{
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
namespace Aviat\AnimeClient\API\MAL;
|
||||
|
||||
use Amp\Artax\Request;
|
||||
use Aviat\AnimeClient\API\MAL as M;
|
||||
use Aviat\AnimeClient\API\MAL\ListItem;
|
||||
use Aviat\AnimeClient\API\MAL\Transformer\AnimeListTransformer;
|
||||
@ -43,7 +44,7 @@ class Model {
|
||||
$this->listItem = $listItem;
|
||||
}
|
||||
|
||||
public function createListItem(array $data): bool
|
||||
public function createListItem(array $data): Request
|
||||
{
|
||||
$createData = [
|
||||
'id' => $data['id'],
|
||||
@ -77,13 +78,13 @@ class Model {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function updateListItem(array $data)
|
||||
public function updateListItem(array $data): Request
|
||||
{
|
||||
$updateData = $this->animeListTransformer->untransform($data);
|
||||
return $this->listItem->update($updateData['id'], $updateData['data']);
|
||||
}
|
||||
|
||||
public function deleteListItem(string $id): bool
|
||||
public function deleteListItem(string $id): Request
|
||||
{
|
||||
return $this->listItem->delete($id);
|
||||
}
|
||||
|
@ -20,50 +20,43 @@ use Yosymfony\Toml\Toml;
|
||||
|
||||
define('SRC_DIR', realpath(__DIR__));
|
||||
|
||||
const SESSION_SEGMENT = 'Aviat\AnimeClient\Auth';
|
||||
const DEFAULT_CONTROLLER_NAMESPACE = 'Aviat\AnimeClient\Controller';
|
||||
const DEFAULT_CONTROLLER = 'Aviat\AnimeClient\Controller\Anime';
|
||||
const DEFAULT_CONTROLLER_METHOD = 'index';
|
||||
const NOT_FOUND_METHOD = 'notFound';
|
||||
const ERROR_MESSAGE_METHOD = 'errorPage';
|
||||
const SRC_DIR = SRC_DIR;
|
||||
|
||||
/**
|
||||
* Application constants
|
||||
* Load configuration options from .toml files
|
||||
*
|
||||
* @param string $path - Path to load config
|
||||
* @return array
|
||||
*/
|
||||
class AnimeClient {
|
||||
function loadToml(string $path): array
|
||||
{
|
||||
$output = [];
|
||||
$files = glob("{$path}/*.toml");
|
||||
|
||||
const SESSION_SEGMENT = 'Aviat\AnimeClient\Auth';
|
||||
const DEFAULT_CONTROLLER_NAMESPACE = 'Aviat\AnimeClient\Controller';
|
||||
const DEFAULT_CONTROLLER = 'Aviat\AnimeClient\Controller\Anime';
|
||||
const DEFAULT_CONTROLLER_METHOD = 'index';
|
||||
const NOT_FOUND_METHOD = 'notFound';
|
||||
const ERROR_MESSAGE_METHOD = 'errorPage';
|
||||
const SRC_DIR = SRC_DIR;
|
||||
|
||||
/**
|
||||
* Load configuration options from .toml files
|
||||
*
|
||||
* @param string $path - Path to load config
|
||||
* @return array
|
||||
*/
|
||||
public static function loadToml(string $path): array
|
||||
foreach ($files as $file)
|
||||
{
|
||||
$output = [];
|
||||
$files = glob("{$path}/*.toml");
|
||||
$key = str_replace('.toml', '', basename($file));
|
||||
$toml = file_get_contents($file);
|
||||
$config = Toml::Parse($toml);
|
||||
|
||||
foreach ($files as $file)
|
||||
if ($key === 'config')
|
||||
{
|
||||
$key = str_replace('.toml', '', basename($file));
|
||||
$toml = file_get_contents($file);
|
||||
$config = Toml::Parse($toml);
|
||||
|
||||
if ($key === 'config')
|
||||
foreach($config as $name => $value)
|
||||
{
|
||||
foreach($config as $name => $value)
|
||||
{
|
||||
$output[$name] = $value;
|
||||
}
|
||||
|
||||
continue;
|
||||
$output[$name] = $value;
|
||||
}
|
||||
|
||||
$output[$key] = $config;
|
||||
continue;
|
||||
}
|
||||
|
||||
return $output;
|
||||
$output[$key] = $config;
|
||||
}
|
||||
}
|
||||
// End of AnimeClient.php
|
||||
|
||||
return $output;
|
||||
}
|
@ -16,21 +16,18 @@
|
||||
|
||||
namespace Aviat\AnimeClient\Command;
|
||||
|
||||
use function Aviat\AnimeClient\loadToml;
|
||||
|
||||
use Aura\Session\SessionFactory;
|
||||
use Aviat\AnimeClient\{
|
||||
AnimeClient,
|
||||
Model,
|
||||
Util
|
||||
};
|
||||
use Aviat\AnimeClient\API\CacheTrait;
|
||||
use Aviat\AnimeClient\API\Kitsu\{
|
||||
Auth as KitsuAuth,
|
||||
ListItem as KitsuListItem,
|
||||
Model as KitsuModel
|
||||
};
|
||||
use Aviat\AnimeClient\API\MAL\{
|
||||
ListItem as MALListItem,
|
||||
Model as MALModel
|
||||
use Aviat\AnimeClient\API\{
|
||||
CacheTrait,
|
||||
Kitsu,
|
||||
MAL
|
||||
};
|
||||
use Aviat\Banker\Pool;
|
||||
use Aviat\Ion\Config;
|
||||
@ -72,23 +69,26 @@ class BaseCommand extends Command {
|
||||
$CONF_DIR = realpath("{$APP_DIR}/config/");
|
||||
require_once $CONF_DIR . '/base_config.php'; // $base_config
|
||||
|
||||
$config = AnimeClient::loadToml($CONF_DIR);
|
||||
$config = loadToml($CONF_DIR);
|
||||
$config_array = array_merge($base_config, $config);
|
||||
|
||||
$di = function ($config_array) use ($APP_DIR) {
|
||||
$container = new Container();
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Logging
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
$app_logger = new Logger('animeclient');
|
||||
$app_logger->pushHandler(new NullHandler);
|
||||
$request_logger = new Logger('request');
|
||||
$request_logger->pushHandler(new NullHandler);
|
||||
$kitsu_request_logger = new Logger('kitsu-request');
|
||||
$kitsu_request_logger->pushHandler(new NullHandler);
|
||||
$mal_request_logger = new Logger('mal-request');
|
||||
$mal_request_logger->pushHandler(new NullHandler);
|
||||
$container->setLogger($app_logger, 'default');
|
||||
$container->setLogger($request_logger, 'request');
|
||||
|
||||
$container->setLogger($kitsu_request_logger, 'kitsu-request');
|
||||
$container->setLogger($mal_request_logger, 'mal-request');
|
||||
|
||||
// Create Config Object
|
||||
$container->set('config', function() use ($config_array) {
|
||||
return new Config($config_array);
|
||||
@ -108,18 +108,18 @@ class BaseCommand extends Command {
|
||||
|
||||
// Models
|
||||
$container->set('kitsu-model', function($container) {
|
||||
$listItem = new KitsuListItem();
|
||||
$listItem = new Kitsu\istItem();
|
||||
$listItem->setContainer($container);
|
||||
$model = new KitsuModel($listItem);
|
||||
$model = new Kitsu\Model($listItem);
|
||||
$model->setContainer($container);
|
||||
$cache = $container->get('cache');
|
||||
$model->setCache($cache);
|
||||
return $model;
|
||||
});
|
||||
$container->set('mal-model', function($container) {
|
||||
$listItem = new MALListItem();
|
||||
$listItem = new MAL\ListItem();
|
||||
$listItem->setContainer($container);
|
||||
$model = new MALModel($listItem);
|
||||
$model = new MAL\Model($listItem);
|
||||
$model->setContainer($container);
|
||||
return $model;
|
||||
});
|
||||
|
@ -23,49 +23,10 @@ use Aviat\AnimeClient\API\Kitsu;
|
||||
* Clears the API Cache
|
||||
*/
|
||||
class SyncKitsuWithMal extends BaseCommand {
|
||||
|
||||
|
||||
protected $kitsuModel;
|
||||
|
||||
public function getKitsuAnimeListPageCount()
|
||||
{
|
||||
$cacheItem = $this->cache->getItem(Kitsu::AUTH_TOKEN_CACHE_KEY);
|
||||
|
||||
$query = http_build_query([
|
||||
'filter' => [
|
||||
'user_id' => $this->kitsuModel->getUserIdByUsername(),
|
||||
'media_type' => 'Anime'
|
||||
],
|
||||
'include' => 'anime,anime.genres,anime.mappings,anime.streamingLinks',
|
||||
'page' => [
|
||||
'limit' => 1
|
||||
],
|
||||
'sort' => '-updated_at'
|
||||
]);
|
||||
$request = (new Artax\Request)
|
||||
->setUri("https://kitsu.io/api/edge/library-entries?{$query}")
|
||||
->setProtocol('1.1')
|
||||
->setAllHeaders([
|
||||
'Accept' => 'application/vnd.api+json',
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
'User-Agent' => "Tim's Anime Client/4.0"
|
||||
]);
|
||||
|
||||
if ($cacheItem->isHit())
|
||||
{
|
||||
$token = $cacheItem->get();
|
||||
$request->setHeader('Authorization', "bearer {$token}");
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->echoBox("WARNING: NOT LOGGED IN\nSome data might be missing");
|
||||
}
|
||||
|
||||
$response = \Amp\wait((new Artax\Client)->request($request));
|
||||
|
||||
$body = json_decode($response->getBody(), TRUE);
|
||||
return $body['meta']['count'];
|
||||
}
|
||||
|
||||
protected $malModel;
|
||||
|
||||
/**
|
||||
* Run the image conversion script
|
||||
*
|
||||
@ -79,8 +40,102 @@ class SyncKitsuWithMal extends BaseCommand {
|
||||
$this->setContainer($this->setupContainer());
|
||||
$this->setCache($this->container->get('cache'));
|
||||
$this->kitsuModel = $this->container->get('kitsu-model');
|
||||
|
||||
$kitsuCount = $this->getKitsuAnimeListPageCount();
|
||||
$this->echoBox("List item count: {$kitsuCount}");
|
||||
$this->malModel = $this->container->get('mal-model');
|
||||
|
||||
//$kitsuCount = $this->getKitsuAnimeListPageCount();
|
||||
//$this->echoBox("List item count: {$kitsuCount}");
|
||||
$this->MALItemCreate();
|
||||
|
||||
//echo json_encode($this->getMALList(), \JSON_PRETTY_PRINT);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getMALList()
|
||||
{
|
||||
return $this->malModel->getFullList();
|
||||
}
|
||||
|
||||
public function getKitsuAnimeListPageCount()
|
||||
{
|
||||
$cacheItem = $this->cache->getItem(Kitsu::AUTH_TOKEN_CACHE_KEY);
|
||||
|
||||
$query = http_build_query([
|
||||
'filter' => [
|
||||
'user_id' => $this->kitsuModel->getUserIdByUsername(),
|
||||
'media_type' => 'Anime'
|
||||
],
|
||||
// 'include' => 'anime,anime.genres,anime.mappings,anime.streamingLinks',
|
||||
'page' => [
|
||||
'limit' => 1
|
||||
],
|
||||
'sort' => '-updated_at'
|
||||
]);
|
||||
$request = (new Artax\Request)
|
||||
->setUri("https://kitsu.io/api/edge/library-entries?{$query}")
|
||||
->setProtocol('1.1')
|
||||
->setAllHeaders([
|
||||
'Accept' => 'application/vnd.api+json',
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
'User-Agent' => "Tim's Anime Client/4.0"
|
||||
]);
|
||||
|
||||
if ($cacheItem->isHit())
|
||||
{
|
||||
$token = $cacheItem->get();
|
||||
$request->setHeader('Authorization', "bearer {$token}");
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->echoBox("WARNING: NOT LOGGED IN\nSome data might be missing");
|
||||
}
|
||||
|
||||
$response = \Amp\wait((new Artax\Client)->request($request));
|
||||
|
||||
$body = json_decode($response->getBody(), TRUE);
|
||||
return $body['meta']['count'];
|
||||
}
|
||||
|
||||
public function MALItemCreate()
|
||||
{
|
||||
$input = json_decode('{
|
||||
"watching_status": "current",
|
||||
"user_rating": "",
|
||||
"episodes_watched": "4",
|
||||
"rewatched": "0",
|
||||
"notes": "",
|
||||
"id": "15794526",
|
||||
"mal_id": "33731",
|
||||
"edit": "true"
|
||||
}', TRUE);
|
||||
|
||||
$response = $this->malModel->createListItem([
|
||||
'id' => 12255,
|
||||
'status' => 'planned',
|
||||
'type' => 'anime'
|
||||
]);
|
||||
|
||||
//$response = $this->malModel->updateListItem($input);
|
||||
//print_r($response);
|
||||
//echo $response->getBody();
|
||||
|
||||
}
|
||||
|
||||
public function diffLists()
|
||||
{
|
||||
// Get libraryEntries with media.mappings from Kitsu
|
||||
// Organize mappings, and ignore entries without mappings
|
||||
|
||||
// Get MAL list data
|
||||
|
||||
// Compare each list entry
|
||||
// If a list item exists only on MAL, create it on Kitsu with the existing data from MAL
|
||||
// If a list item exists only on Kitsu, create it on MAL with the existing data from Kitsu
|
||||
// If an item already exists on both APIS:
|
||||
// Compare last updated dates, and use the later one
|
||||
// Otherwise, use rewatch count, then episode progress as critera for selecting the more up
|
||||
// to date entry
|
||||
// Based on the 'newer' entry, update the other api list item
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -16,6 +16,8 @@
|
||||
|
||||
namespace Aviat\AnimeClient;
|
||||
|
||||
use const Aviat\AnimeClient\SESSION_SEGMENT;
|
||||
|
||||
use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
|
||||
use Aviat\Ion\View\{HtmlView, HttpView, JsonView};
|
||||
use InvalidArgumentException;
|
||||
@ -102,7 +104,7 @@ class Controller {
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
|
||||
$session = $container->get('session');
|
||||
$this->session = $session->getSegment(AnimeClient::SESSION_SEGMENT);
|
||||
$this->session = $session->getSegment(SESSION_SEGMENT);
|
||||
|
||||
// Set a 'previous' flash value for better redirects
|
||||
$server_params = $this->request->getServerParams();
|
||||
|
@ -16,9 +16,16 @@
|
||||
|
||||
namespace Aviat\AnimeClient;
|
||||
|
||||
use const Aviat\AnimeClient\{
|
||||
DEFAULT_CONTROLLER,
|
||||
DEFAULT_CONTROLLER_NAMESPACE,
|
||||
ERROR_MESSAGE_METHOD,
|
||||
NOT_FOUND_METHOD,
|
||||
SRC_DIR
|
||||
};
|
||||
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\Friend;
|
||||
use GuzzleHttp\Exception\ServerException;
|
||||
|
||||
/**
|
||||
* Basic routing/ dispatch
|
||||
@ -125,27 +132,12 @@ class Dispatcher extends RoutingBase {
|
||||
// If not route was matched, return an appropriate http
|
||||
// error message
|
||||
$error_route = $this->getErrorParams();
|
||||
$controllerName = AnimeClient::DEFAULT_CONTROLLER;
|
||||
$controllerName = DEFAULT_CONTROLLER;
|
||||
$actionMethod = $error_route['action_method'];
|
||||
$params = $error_route['params'];
|
||||
}
|
||||
|
||||
// Try to catch API errors in a presentable fashion
|
||||
try
|
||||
{
|
||||
// Actually instantiate the controller
|
||||
$this->call($controllerName, $actionMethod, $params);
|
||||
}
|
||||
catch (ServerException $e)
|
||||
{
|
||||
$response = $e->getResponse();
|
||||
$this->call(AnimeClient::DEFAULT_CONTROLLER, AnimeClient::ERROR_MESSAGE_METHOD, [
|
||||
$response->getStatusCode(),
|
||||
'API Error',
|
||||
'There was a problem getting data from an external source.',
|
||||
(string) $response->getBody()
|
||||
]);
|
||||
}
|
||||
|
||||
$this->call($controllerName, $actionMethod, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -176,7 +168,7 @@ class Dispatcher extends RoutingBase {
|
||||
|
||||
$action_method = (array_key_exists('action', $route->attributes))
|
||||
? $route->attributes['action']
|
||||
: AnimeClient::NOT_FOUND_METHOD;
|
||||
: NOT_FOUND_METHOD;
|
||||
|
||||
$params = [];
|
||||
if ( ! empty($route->__get('tokens')))
|
||||
@ -229,11 +221,11 @@ class Dispatcher extends RoutingBase {
|
||||
*/
|
||||
public function getControllerList()
|
||||
{
|
||||
$default_namespace = AnimeClient::DEFAULT_CONTROLLER_NAMESPACE;
|
||||
$default_namespace = DEFAULT_CONTROLLER_NAMESPACE;
|
||||
$path = str_replace('\\', '/', $default_namespace);
|
||||
$path = str_replace('Aviat/AnimeClient/', '', $path);
|
||||
$path = trim($path, '/');
|
||||
$actual_path = realpath(_dir(AnimeClient::SRC_DIR, $path));
|
||||
$actual_path = realpath(_dir(SRC_DIR, $path));
|
||||
$class_files = glob("{$actual_path}/*.php");
|
||||
|
||||
$controllers = [];
|
||||
@ -285,7 +277,7 @@ class Dispatcher extends RoutingBase {
|
||||
$logger->info('Dispatcher - failed route');
|
||||
$logger->info(print_r($failure, TRUE));
|
||||
|
||||
$action_method = AnimeClient::ERROR_MESSAGE_METHOD;
|
||||
$action_method = ERROR_MESSAGE_METHOD;
|
||||
|
||||
$params = [];
|
||||
|
||||
@ -308,7 +300,7 @@ class Dispatcher extends RoutingBase {
|
||||
|
||||
default:
|
||||
// Fall back to a 404 message
|
||||
$action_method = AnimeClient::NOT_FOUND_METHOD;
|
||||
$action_method = NOT_FOUND_METHOD;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -337,7 +329,7 @@ class Dispatcher extends RoutingBase {
|
||||
$controller_map = $this->getControllerList();
|
||||
$controller_class = (array_key_exists($route_type, $controller_map))
|
||||
? $controller_map[$route_type]
|
||||
: AnimeClient::DEFAULT_CONTROLLER;
|
||||
: DEFAULT_CONTROLLER;
|
||||
|
||||
if (array_key_exists($route_type, $controller_map))
|
||||
{
|
||||
|
@ -38,12 +38,6 @@ class API extends Model {
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* Default settings for Guzzle
|
||||
* @var array
|
||||
*/
|
||||
protected $connectionDefaults = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
@ -74,4 +68,4 @@ class API extends Model {
|
||||
|
||||
array_multisort($sort, SORT_ASC, $array);
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,10 @@
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\Model;
|
||||
|
||||
use function Amp\some;
|
||||
use function Amp\wait;
|
||||
use Amp\Artax\Client;
|
||||
use Aviat\AnimeClient\API\Kitsu\Enum\AnimeWatchingStatus;
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\Json;
|
||||
@ -91,9 +95,15 @@ class Anime extends API {
|
||||
return $this->kitsuModel->getAnime($slug);
|
||||
}
|
||||
|
||||
public function getAnimeById($anime_id)
|
||||
/**
|
||||
* Get anime by its kitsu id
|
||||
*
|
||||
* @param string $animeId
|
||||
* @return array
|
||||
*/
|
||||
public function getAnimeById($animeId)
|
||||
{
|
||||
return $this->kitsuModel->getAnimeById($anime_id);
|
||||
return $this->kitsuModel->getAnimeById($animeId);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -104,7 +114,6 @@ class Anime extends API {
|
||||
*/
|
||||
public function search($name)
|
||||
{
|
||||
// $raw = $this->kitsuModel->search('anime', $name);
|
||||
return $this->kitsuModel->search('anime', $name);
|
||||
}
|
||||
|
||||
@ -128,6 +137,8 @@ class Anime extends API {
|
||||
*/
|
||||
public function createLibraryItem(array $data): bool
|
||||
{
|
||||
$requests = [];
|
||||
|
||||
if ($this->useMALAPI)
|
||||
{
|
||||
$malData = $data;
|
||||
@ -136,11 +147,17 @@ class Anime extends API {
|
||||
if ( ! is_null($malId))
|
||||
{
|
||||
$malData['id'] = $malId;
|
||||
$this->malModel->createListItem($malData);
|
||||
$requests['mal'] = $this->malModel->createListItem($malData);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->kitsuModel->createListItem($data);
|
||||
$requests['kitsu'] = $this->kitsuModel->createListItem($data);
|
||||
|
||||
$promises = (new Client)->requestMulti($requests);
|
||||
|
||||
$results = wait(some($promises));
|
||||
|
||||
return count($results[1]) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -168,12 +185,18 @@ class Anime extends API {
|
||||
*/
|
||||
public function deleteLibraryItem(string $id, string $malId = null): bool
|
||||
{
|
||||
$requests = [];
|
||||
|
||||
if ($this->useMALAPI && ! is_null($malId))
|
||||
{
|
||||
$this->malModel->deleteListItem($malId);
|
||||
$requests['mal'] = $this->malModel->deleteListItem($malId);
|
||||
}
|
||||
|
||||
return $this->kitsuModel->deleteListItem($id);
|
||||
$requests['kitsu'] = $this->kitsuModel->deleteListItem($id);
|
||||
|
||||
$results = wait(some((new Client)->requestMulti($requests)));
|
||||
|
||||
return count($results[1]) > 0;
|
||||
}
|
||||
}
|
||||
// End of AnimeModel.php
|
@ -29,13 +29,13 @@ class APIRequestBuilderTest extends TestCase {
|
||||
{
|
||||
$this->builder = new class extends APIRequestBuilder {
|
||||
protected $baseUrl = 'https://httpbin.org/';
|
||||
|
||||
|
||||
protected $defaultHeaders = ['User-Agent' => "Tim's Anime Client Testsuite / 4.0"];
|
||||
};
|
||||
|
||||
|
||||
$this->builder->setLogger(new NullLogger);
|
||||
}
|
||||
|
||||
|
||||
public function testGzipRequest()
|
||||
{
|
||||
$request = $this->builder->newRequest('GET', 'gzip')
|
||||
@ -44,26 +44,26 @@ class APIRequestBuilderTest extends TestCase {
|
||||
$body = Json::decode($response->getBody());
|
||||
$this->assertEquals(1, $body['gzipped']);
|
||||
}
|
||||
|
||||
|
||||
public function testInvalidRequestMethod()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->builder->newRequest('FOO', 'gzip')
|
||||
->getFullRequest();
|
||||
}
|
||||
|
||||
|
||||
public function testRequestWithBasicAuth()
|
||||
{
|
||||
$request = $this->builder->newRequest('GET', 'headers')
|
||||
->setBasicAuth('username', 'password')
|
||||
->getFullRequest();
|
||||
|
||||
|
||||
$response = Amp\wait((new Client)->request($request));
|
||||
$body = Json::decode($response->getBody());
|
||||
|
||||
|
||||
$this->assertEquals('Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $body['headers']['Authorization']);
|
||||
}
|
||||
|
||||
|
||||
public function testRequestWithQueryString()
|
||||
{
|
||||
$query = [
|
||||
@ -75,40 +75,40 @@ class APIRequestBuilderTest extends TestCase {
|
||||
'bar' => 'foo'
|
||||
]
|
||||
];
|
||||
|
||||
|
||||
$expected = [
|
||||
'foo' => 'bar',
|
||||
'bar[foo]' => 'bar',
|
||||
'baz[bar]' => 'foo'
|
||||
];
|
||||
|
||||
|
||||
$request = $this->builder->newRequest('GET', 'get')
|
||||
->setQuery($query)
|
||||
->getFullRequest();
|
||||
|
||||
|
||||
$response = Amp\wait((new Client)->request($request));
|
||||
$body = Json::decode($response->getBody());
|
||||
|
||||
$this->assertEquals($expected, $body['args']);
|
||||
|
||||
$this->assertEquals($expected, $body['args']);
|
||||
}
|
||||
|
||||
|
||||
public function testFormValueRequest()
|
||||
{
|
||||
$formValues = [
|
||||
'foo' => 'bar',
|
||||
'bar' => 'foo'
|
||||
];
|
||||
|
||||
|
||||
$request = $this->builder->newRequest('POST', 'post')
|
||||
->setFormFields($formValues)
|
||||
->getFullRequest();
|
||||
|
||||
|
||||
$response = Amp\wait((new Client)->request($request));
|
||||
$body = Json::decode($response->getBody());
|
||||
|
||||
|
||||
$this->assertEquals($formValues, $body['form']);
|
||||
}
|
||||
|
||||
|
||||
public function testFullUrlRequest()
|
||||
{
|
||||
$data = [
|
||||
@ -121,16 +121,15 @@ class APIRequestBuilderTest extends TestCase {
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
|
||||
$request = $this->builder->newRequest('PUT', 'https://httpbin.org/put')
|
||||
->setHeader('Content-Type', 'application/json')
|
||||
->setBody(Json::encode($data))
|
||||
->setJsonBody($data)
|
||||
->getFullRequest();
|
||||
|
||||
|
||||
$response = Amp\wait((new Client)->request($request));
|
||||
$body = Json::decode($response->getBody());
|
||||
|
||||
|
||||
$this->assertEquals($data, $body['json']);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +1,10 @@
|
||||
<?php
|
||||
|
||||
use const Aviat\AnimeClient\SRC_DIR;
|
||||
|
||||
use Aura\Web\WebFactory;
|
||||
use Aviat\AnimeClient\AnimeClient;
|
||||
use Aviat\Ion\Json;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Handler\MockHandler;
|
||||
use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Zend\Diactoros\{
|
||||
Response as HttpResponse,
|
||||
@ -23,7 +21,7 @@ define('TEST_VIEW_DIR', __DIR__ . '/test_views');
|
||||
class AnimeClient_TestCase extends TestCase {
|
||||
// Test directory constants
|
||||
const ROOT_DIR = ROOT_DIR;
|
||||
const SRC_DIR = AnimeClient::SRC_DIR;
|
||||
const SRC_DIR = SRC_DIR;
|
||||
const TEST_DATA_DIR = TEST_DATA_DIR;
|
||||
const TEST_VIEW_DIR = TEST_VIEW_DIR;
|
||||
|
||||
@ -159,25 +157,5 @@ class AnimeClient_TestCase extends TestCase {
|
||||
|
||||
return Json::decode($rawData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock guzzle client for testing
|
||||
* api call methods
|
||||
*
|
||||
* @param int $code The status code
|
||||
* @param array $headers
|
||||
* @param string $body
|
||||
* @return Client
|
||||
*/
|
||||
public function getMockClient($code, $headers, $body)
|
||||
{
|
||||
$mock = new MockHandler([
|
||||
new Response($code, $headers, $body)
|
||||
]);
|
||||
$handler = HandlerStack::create($mock);
|
||||
$client = new Client(['handler' => $handler]);
|
||||
|
||||
return $client;
|
||||
}
|
||||
}
|
||||
// End of AnimeClient_TestCase.php
|
Loading…
Reference in New Issue
Block a user