diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c237b88d..c7ea169f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -32,7 +32,7 @@ test:hhvm: - /usr/local/bin/composer self-update - curl -Lo /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar - chmod +x /usr/local/bin/phpunit - - composer install --no-dev + - composer install --no-dev --ignore-platform-reqs image: 51systems/docker-gitlab-ci-runner-hhvm script: - hhvm -d hhvm.php7.all=true /usr/local/bin/phpunit -c build --coverage-text --colors=never \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index bd0fa78d..bcf84dd4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,11 @@ language: php install: - - composer install + - composer install --ignore-platform-reqs php: - 7 + - 7.1 - hhvm - nightly diff --git a/app/config/base_config.php b/app/appConf/base_config.php similarity index 100% rename from app/config/base_config.php rename to app/appConf/base_config.php diff --git a/app/config/menus.php b/app/appConf/menus.php similarity index 100% rename from app/config/menus.php rename to app/appConf/menus.php diff --git a/app/config/minify_config.php b/app/appConf/minify_config.php similarity index 100% rename from app/config/minify_config.php rename to app/appConf/minify_config.php diff --git a/app/config/minify_css_groups.php b/app/appConf/minify_css_groups.php similarity index 100% rename from app/config/minify_css_groups.php rename to app/appConf/minify_css_groups.php diff --git a/app/config/minify_js_groups.php b/app/appConf/minify_js_groups.php similarity index 100% rename from app/config/minify_js_groups.php rename to app/appConf/minify_js_groups.php diff --git a/app/config/routes.php b/app/appConf/routes.php similarity index 93% rename from app/config/routes.php rename to app/appConf/routes.php index df9329ad..4ead95bf 100644 --- a/app/config/routes.php +++ b/app/appConf/routes.php @@ -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', ], ], diff --git a/app/bootstrap.php b/app/bootstrap.php index 01e06953..599a275b 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -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); diff --git a/app/views/header.php b/app/views/header.php index 4326beb3..e4196144 100644 --- a/app/views/header.php +++ b/app/views/header.php @@ -6,6 +6,7 @@ + diff --git a/composer.json b/composer.json index c21f88ae..4b91d3b0 100644 --- a/composer.json +++ b/composer.json @@ -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" } -} +} \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 00000000..c34a19de Binary files /dev/null and b/favicon.ico differ diff --git a/index.php b/index.php index fff3616a..14eda8dc 100644 --- a/index.php +++ b/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; @@ -39,10 +41,11 @@ function _dir() // Define base directories $APP_DIR = _dir(__DIR__, 'app'); +$APPCONF_DIR = _dir($APP_DIR, 'appConf'); $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,18 +57,15 @@ $defaultHandler = new PrettyPageHandler(); $whoops->pushHandler($defaultHandler); // Register as the error handler -if (array_key_exists('whoops', $_GET)) -{ - $whoops->register(); -} +$whoops->register(); // ----------------------------------------------------------------------------- // Dependency Injection setup // ----------------------------------------------------------------------------- -require _dir($CONF_DIR, 'base_config.php'); // $base_config +require _dir($APPCONF_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 +77,4 @@ unset($CONF_DIR); // ----------------------------------------------------------------------------- // Dispatch to the current route // ----------------------------------------------------------------------------- -$container->get('dispatcher')->__invoke(); - -// End of index.php \ No newline at end of file +$container->get('dispatcher')->__invoke(); \ No newline at end of file diff --git a/public/css.php b/public/css.php index 0f2f5a42..3253765b 100644 --- a/public/css.php +++ b/public/css.php @@ -160,7 +160,7 @@ class CSSMin extends BaseMin { // -------------------------------------------------------------------------- //Get config files -$config = require('../app/config/minify_config.php'); +$config = require('../app/appConf/minify_config.php'); $groups = require($config['css_groups_file']); if ( ! array_key_exists($_GET['g'], $groups)) diff --git a/public/js.php b/public/js.php index 22bc1718..e8f50232 100644 --- a/public/js.php +++ b/public/js.php @@ -16,8 +16,9 @@ namespace Aviat\EasyMin; -use GuzzleHttp\Client; -use GuzzleHttp\Psr7\Request; +use function Amp\wait; +use Amp\Artax\{Client, FormBody, Request}; +use Aviat\Ion\Json; // Include guzzle require_once('../vendor/autoload.php'); @@ -97,14 +98,21 @@ class JSMin extends BaseMin { */ protected function closure_call(array $options) { - $client = new Client(); - $response = $client->post('http://closure-compiler.appspot.com/compile', [ - 'headers' => [ + $formFields = http_build_query($options); + + $request = (new Request) + ->setMethod('POST') + ->setUri('http://closure-compiler.appspot.com/compile') + ->setAllHeaders([ + 'Accept' => 'application/json', 'Accept-Encoding' => 'gzip', 'Content-type' => 'application/x-www-form-urlencoded' - ], - 'form_params' => $options - ]); + ]) + ->setBody($formFields); + + $response = wait((new Client)->request($request, [ + Client::OP_AUTO_ENCODING => false + ])); return $response; } @@ -118,13 +126,14 @@ class JSMin extends BaseMin { protected function check_minify_errors($options) { $error_res = $this->closure_call($options); - $error_json = (string)$error_res->getBody(); - $error_obj = json_decode($error_json) ?: (object)[]; + $error_json = $error_res->getBody(); + $error_obj = Json::decode($error_json) ?: (object)[]; + // Show error if exists if ( ! empty($error_obj->errors) || ! empty($error_obj->serverErrors)) { - $error_json = json_encode($error_obj, JSON_PRETTY_PRINT); + $error_json = Json::encode($error_obj, JSON_PRETTY_PRINT); header('Content-type: application/javascript'); echo "console.error(${error_json});"; die(); @@ -201,10 +210,11 @@ class JSMin extends BaseMin { // Now actually retrieve the compiled code $options['output_info'] = 'compiled_code'; $res = $this->closure_call($options); - $json = (string)$res->getBody(); - $obj = json_decode($json); + $json = $res->getBody(); + $obj = Json::decode($json); - return $obj->compiledCode; + //return $obj; + return $obj['compiledCode']; } /** @@ -223,7 +233,7 @@ class JSMin extends BaseMin { // ! Start Minifying // -------------------------------------------------------------------------- -$config = require_once('../app/config/minify_config.php'); +$config = require_once('../app/appConf/minify_config.php'); $groups = require_once($config['js_groups_file']); $cache_dir = "{$config['js_root']}cache"; diff --git a/src/API/APIRequestBuilder.php b/src/API/APIRequestBuilder.php index 52fc91c2..4a96f63b 100644 --- a/src/API/APIRequestBuilder.php +++ b/src/API/APIRequestBuilder.php @@ -16,12 +16,14 @@ namespace Aviat\AnimeClient\API; +use Amp; use Amp\Artax\{ - Client, - FormBody, + Client, + FormBody, Request }; use Aviat\Ion\Di\ContainerAware; +use Aviat\Ion\Json; use InvalidArgumentException; use Psr\Log\LoggerAwareTrait; @@ -30,56 +32,71 @@ use Psr\Log\LoggerAwareTrait; */ class APIRequestBuilder { use LoggerAwareTrait; - + /** * Url prefix for making url requests * @var string */ protected $baseUrl = ''; - + /** * Url path of the request * @var string */ protected $path = ''; - + /** * Query string for the request * @var string */ protected $query = ''; - + /** * Default request headers * @var array */ protected $defaultHeaders = []; - + /** * Valid HTTP request methos * @var array */ protected $validMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']; - + /** * The current request * @var \Amp\Promise */ protected $request; - + /** - * Set body as form fields - * - * @param array $fields Mapping of field names to values + * 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 setFormFields(array $fields): self + public function setAuth(string $type, string $value): self { - $body = $this->fixBody((new FormBody)->addFields($createData)); - $this->setBody($body); + $authString = ucfirst($type) . ' ' . $value; + $this->setHeader('Authorization', $authString); + return $this; } - + + /** + * Set a basic authentication header + * + * @param string $username + * @param string $password + * @return self + */ + public function setBasicAuth(string $username, string $password): self + { + $this->setAuth('basic', base64_encode($username . ':' . $password)); + return $this; + } + /** * Set the request body * @@ -91,7 +108,21 @@ class APIRequestBuilder { $this->request->setBody($body); return $this; } - + + /** + * Set body as form fields + * + * @param array $fields Mapping of field names to values + * @return self + */ + public function setFormFields(array $fields): self + { + $this->setHeader("Content-Type", "application/x-www-form-urlencoded"); + $body = (new FormBody)->addFields($fields); + $this->setBody($body); + return $this; + } + /** * Set a request header * @@ -104,10 +135,10 @@ class APIRequestBuilder { $this->request->setHeader($name, $value); return $this; } - + /** * Set multiple request headers - * + * * name => value * * @param array $headers @@ -119,10 +150,25 @@ class APIRequestBuilder { { $this->setHeader($name, $value); } - + return $this; } - + + /** + * Set the request body + * + * @param mixed $body + * @return self + */ + public function setJsonBody($body): self + { + $requestBody = ( ! is_scalar($body)) + ? Json::encode($body) + : $body; + + return $this->setBody($requestBody); + } + /** * Append a query string in array format * @@ -131,10 +177,10 @@ class APIRequestBuilder { */ public function setQuery(array $params): self { - $this->query = http_build_query($params); + $this->query = http_build_query($params); return $this; } - + /** * Return the promise for the current request * @@ -143,9 +189,19 @@ class APIRequestBuilder { public function getFullRequest() { $this->buildUri(); + + if ($this->logger) + { + $this->logger->debug('API Request', [ + 'request_url' => $this->request->getUri(), + 'request_headers' => $this->request->getAllHeaders(), + 'request_body' => $this->request->getBody() + ]); + } + return $this->request; } - + /** * Create a new http request * @@ -159,16 +215,23 @@ class APIRequestBuilder { { throw new InvalidArgumentException('Invalid HTTP methods'); } - + $this->resetState(); - + $this->request ->setMethod($type) ->setProtocol('1.1'); - + + $this->path = $uri; + + if ( ! empty($this->defaultHeaders)) + { + $this->setHeaders($this->defaultHeaders); + } + return $this; } - + /** * Create the full request url * @@ -178,31 +241,16 @@ class APIRequestBuilder { { $url = (strpos($this->path, '//') !== FALSE) ? $this->path - : $this->baseUrl . $url; + : $this->baseUrl . $this->path; if ( ! empty($this->query)) { $url .= '?' . $this->query; } - + $this->request->setUri($url); } - - /** - * Unencode the dual-encoded ampersands in the body - * - * This is a dirty hack until I can fully track down where - * the dual-encoding happens - * - * @param FormBody $formBody The form builder object to fix - * @return string - */ - private function fixBody(FormBody $formBody): string - { - $rawBody = \Amp\wait($formBody->getBody()); - return html_entity_decode($rawBody, \ENT_HTML5, 'UTF-8'); - } - + /** * Reset the class state for a new request * diff --git a/src/API/MAL/Transformer/MALToKitsuTransformer.php b/src/API/FailedResponseException.php similarity index 66% rename from src/API/MAL/Transformer/MALToKitsuTransformer.php rename to src/API/FailedResponseException.php index dc8fed17..648bb83d 100644 --- a/src/API/MAL/Transformer/MALToKitsuTransformer.php +++ b/src/API/FailedResponseException.php @@ -14,20 +14,10 @@ * @link https://github.com/timw4mail/HummingBirdAnimeClient */ -namespace Aviat\AnimeClient\API\MAL; +namespace Aviat\AnimeClient\API; -use Aviat\Ion\Transformer\AbstractTransformer; +use UnexpectedValueException; + +class FailedResponseException extends UnexpectedValueException { -class MALToKitsuTransformer extends AbstractTransformer { - - - public function transform($item) - { - - } - - public function untransform($item) - { - - } } \ No newline at end of file diff --git a/src/API/GuzzleTrait.php b/src/API/GuzzleTrait.php deleted file mode 100644 index 6af78cde..00000000 --- a/src/API/GuzzleTrait.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @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(); -} \ No newline at end of file diff --git a/src/API/Kitsu.php b/src/API/Kitsu.php index 34907c90..117f7bdd 100644 --- a/src/API/Kitsu.php +++ b/src/API/Kitsu.php @@ -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' => '' ]; - + case 'www.funimation.com': return [ 'name' => 'Funimation', 'link' => true, 'logo' => '' ]; - + case 'www.hulu.com': return [ 'name' => 'Hulu', 'link' => true, 'logo' => '' ]; - - // 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 []; } diff --git a/src/API/Kitsu/Auth.php b/src/API/Kitsu/Auth.php index 70691eb1..ecee237b 100644 --- a/src/API/Kitsu/Auth.php +++ b/src/API/Kitsu/Auth.php @@ -16,6 +16,8 @@ namespace Aviat\AnimeClient\API\Kitsu; +use const Aviat\AnimeClient\SESSION_SEGMENT; + use Aviat\AnimeClient\AnimeClient; use Aviat\AnimeClient\API\{ CacheTrait, @@ -55,7 +57,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 +72,7 @@ class Auth { { $config = $this->container->get('config'); $username = $config->get(['kitsu_username']); - + try { $auth = $this->model->authenticate($username, $password); @@ -79,7 +81,7 @@ class Auth { { return FALSE; } - + if (FALSE !== $auth) { @@ -87,7 +89,7 @@ class Auth { $cacheItem = $this->cache->getItem(K::AUTH_TOKEN_CACHE_KEY); $cacheItem->set($auth['access_token']); $cacheItem->save(); - + $this->segment->set('auth_token', $auth['access_token']); return TRUE; } diff --git a/src/API/Kitsu/KitsuRequestBuilder.php b/src/API/Kitsu/KitsuRequestBuilder.php index c155794a..7a3cab02 100644 --- a/src/API/Kitsu/KitsuRequestBuilder.php +++ b/src/API/Kitsu/KitsuRequestBuilder.php @@ -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); - } -} +} \ No newline at end of file diff --git a/src/API/Kitsu/KitsuTrait.php b/src/API/Kitsu/KitsuTrait.php index 148c35c5..84bcf42c 100644 --- a/src/API/Kitsu/KitsuTrait.php +++ b/src/API/Kitsu/KitsuTrait.php @@ -16,55 +16,25 @@ namespace Aviat\AnimeClient\API\Kitsu; +use const Aviat\AnimeClient\SESSION_SEGMENT; + +use function Amp\wait; + +use Amp\Artax\{Client, Request}; 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; trait KitsuTrait { - + /** * The request builder for the MAL API * @var MALRequestBuilder */ 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,49 @@ 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\Request */ - protected function init() + public function setUpRequest(string $type, string $url, array $options = []): Request { - $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); + } + + if (array_key_exists('form_params', $options)) + { + $request->setFormFields($options['form_params']); + } + + 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 +99,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,11 +133,11 @@ 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) { - $logger->warning('Non 200 response for api call', $response->getBody()); + $logger->warning('Non 200 response for api call', (array)$response->getBody()); } } @@ -218,7 +183,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 +203,6 @@ trait KitsuTrait { protected function deleteRequest(...$args): bool { $response = $this->getResponse('DELETE', ...$args); - return ((int) $response->getStatusCode() === 204); + return ((int) $response->getStatus() === 204); } } \ No newline at end of file diff --git a/src/API/Kitsu/ListItem.php b/src/API/Kitsu/ListItem.php index 57787f15..509fab80 100644 --- a/src/API/Kitsu/ListItem.php +++ b/src/API/Kitsu/ListItem.php @@ -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,59 +31,100 @@ 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 { - $response = $this->getResponse('POST', 'library-entries', [ - 'body' => Json::encode([ - 'data' => [ - 'type' => 'libraryEntries', - 'attributes' => [ - 'status' => $data['status'], - 'progress' => $data['progress'] ?? 0 + $body = [ + 'data' => [ + 'type' => 'libraryEntries', + 'attributes' => [ + 'status' => $data['status'], + 'progress' => $data['progress'] ?? 0 + ], + 'relationships' => [ + 'user' => [ + 'data' => [ + 'id' => $data['user_id'], + 'type' => 'users' + ] ], - 'relationships' => [ - 'user' => [ - 'data' => [ - 'id' => $data['user_id'], - 'type' => 'users' - ] - ], - 'media' => [ - 'data' => [ - 'id' => $data['id'], - 'type' => $data['type'] - ] + 'media' => [ + 'data' => [ + 'id' => $data['id'], + 'type' => $data['type'] ] ] ] - ]) - ]); + ] + ]; - 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 { - return $this->getRequest("library-entries/{$id}", [ - 'query' => [ + $authHeader = $this->getAuthHeader(); + + $request = $this->requestBuilder->newRequest('GET', "library-entries/{$id}") + ->setQuery([ 'include' => 'media,media.genres,media.mappings' - ] - ]); + ]); + + if ($authHeader !== FALSE) + { + $request = $request->setHeader('Authorization', $authHeader); + } + + $request = $request->getFullRequest(); + + $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 { + $authHeader = $this->getAuthHeader(); $requestData = [ 'data' => [ 'id' => $id, @@ -90,11 +132,15 @@ class ListItem extends AbstractListItem { 'attributes' => $data ] ]; - - $response = $this->getResponse('PATCH', "library-entries/{$id}", [ - 'body' => JSON::encode($requestData) - ]); - - return $response; + + $request = $this->requestBuilder->newRequest('PATCH', "library-entries/{$id}") + ->setJsonBody($requestData); + + if ($authHeader !== FALSE) + { + $request = $request->setHeader('Authorization', $authHeader); + } + + return $request->getFullRequest(); } } \ No newline at end of file diff --git a/src/API/Kitsu/Model.php b/src/API/Kitsu/Model.php index 7077f809..96fd54c5 100644 --- a/src/API/Kitsu/Model.php +++ b/src/API/Kitsu/Model.php @@ -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; @@ -206,15 +203,56 @@ class Model { $baseData = $this->getRawMediaData('manga', $mangaId); return $this->mangaTransformer->transform($baseData); } + + /** + * Get the number of anime list items + * + * @return int + */ + public function getAnimeListCount() : int + { + $options = [ + 'query' => [ + 'filter' => [ + 'user_id' => $this->getUserIdByUsername(), + 'media_type' => 'Anime' + ], + 'page' => [ + 'limit' => 1 + ], + 'sort' => '-updated_at' + ] + ]; + + $response = $this->getRequest('library-entries', $options); + + return $response['meta']['count']; + + } /** * Get and transform the entirety of the user's anime list * - * @return array + * @return Request */ - public function getFullAnimeList(): array + public function getFullAnimeList(int $limit = 100, int $offset = 0): Request { - + $options = [ + 'query' => [ + 'filter' => [ + 'user_id' => $this->getUserIdByUsername($this->getUsername()), + 'media_type' => 'Anime' + ], + 'include' => 'anime.mappings', + 'page' => [ + 'offset' => $offset, + 'limit' => $limit + ], + 'sort' => '-updated_at' + ] + ]; + + return $this->setUpRequest('GET', 'library-entries', $options); } /** @@ -355,9 +393,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,34 +435,20 @@ 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(), - 'body' => $response->getBody(), - ]; - } - catch(ClientException $e) - { - return [ - 'statusCode' => $e->getResponse()->getStatusCode(), - 'body' => Json::decode((string)$e->getResponse()->getBody()) - ]; - } + return $this->listItem->update($data['id'], $data['data']); } /** * 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); } diff --git a/src/API/Kitsu/Transformer/AnimeListTransformer.php b/src/API/Kitsu/Transformer/AnimeListTransformer.php index 2591aab0..cd87707b 100644 --- a/src/API/Kitsu/Transformer/AnimeListTransformer.php +++ b/src/API/Kitsu/Transformer/AnimeListTransformer.php @@ -116,7 +116,6 @@ class AnimeListTransformer extends AbstractTransformer { 'mal_id' => $item['mal_id'] ?? null, 'data' => [ 'status' => $item['watching_status'], - 'rating' => $item['user_rating'] / 2, 'reconsuming' => $rewatching, 'reconsumeCount' => $item['rewatched'], 'notes' => $item['notes'], @@ -124,10 +123,10 @@ class AnimeListTransformer extends AbstractTransformer { 'private' => $privacy ] ]; - - if ((int) $untransformed['data']['rating'] === 0) + + if ( ! empty($item['user_rating'])) { - unset($untransformed['data']['rating']); + $untransformed['data']['rating'] = $item['user_rating'] / 2; } return $untransformed; diff --git a/src/API/ListItemInterface.php b/src/API/ListItemInterface.php index b0001428..17f0cad8 100644 --- a/src/API/ListItemInterface.php +++ b/src/API/ListItemInterface.php @@ -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; } \ No newline at end of file diff --git a/src/API/MAL.php b/src/API/MAL.php index a346993a..972b4534 100644 --- a/src/API/MAL.php +++ b/src/API/MAL.php @@ -36,6 +36,14 @@ class MAL { KAWS::DROPPED => AnimeWatchingStatus::DROPPED, KAWS::PLAN_TO_WATCH => AnimeWatchingStatus::PLAN_TO_WATCH ]; + + const MAL_KITSU_WATCHING_STATUS_MAP = [ + 1 => KAWS::WATCHING, + 2 => KAWS::COMPLETED, + 3 => KAWS::ON_HOLD, + 4 => KAWS::DROPPED, + 6 => KAWS::PLAN_TO_WATCH + ]; public static function getIdToWatchingStatusMap() { diff --git a/src/API/MAL/ListItem.php b/src/API/MAL/ListItem.php index 715be392..686c3455 100644 --- a/src/API/MAL/ListItem.php +++ b/src/API/MAL/ListItem.php @@ -16,7 +16,7 @@ namespace Aviat\AnimeClient\API\MAL; -use Amp\Artax\FormBody; +use Amp\Artax\{FormBody, Request}; use Aviat\AnimeClient\API\{ AbstractListItem, XML @@ -30,7 +30,7 @@ class ListItem { use ContainerAware; use MALTrait; - public function create(array $data): bool + public function create(array $data): Request { $id = $data['id']; $createData = [ @@ -40,20 +40,26 @@ class ListItem { ]) ]; - $response = $this->getResponse('POST', "animelist/add/{$id}.xml", [ - 'body' => $this->fixBody((new FormBody)->addFields($createData)) - ]); + $config = $this->container->get('config'); - return $response->getBody() === 'Created'; + return $this->requestBuilder->newRequest('POST', "animelist/add/{$id}.xml") + ->setFormFields($createData) + ->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password'])) + ->getFullRequest(); } - public function delete(string $id): bool + public function delete(string $id): Request { - $response = $this->getResponse('DELETE', "animelist/delete/{$id}.xml", [ - 'body' => $this->fixBody((new FormBody)->addField('id', $id)) - ]); + $config = $this->container->get('config'); - return $response->getBody() === 'Deleted'; + return $this->requestBuilder->newRequest('DELETE', "animelist/delete/{$id}.xml") + ->setFormFields([ + 'id' => $id + ]) + ->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password'])) + ->getFullRequest(); + + // return $response->getBody() === 'Deleted' } public function get(string $id): array @@ -61,15 +67,21 @@ 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", [ - 'body' => $this->fixBody($body) - ]); + return $this->requestBuilder->newRequest('POST', "animelist/update/{$id}.xml") + ->setFormFields([ + 'id' => $id, + 'data' => $xml + ]) + ->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password'])) + ->getFullRequest(); } } \ No newline at end of file diff --git a/src/API/MAL/MALRequestBuilder.php b/src/API/MAL/MALRequestBuilder.php index ca0d88b1..326ad44a 100644 --- a/src/API/MAL/MALRequestBuilder.php +++ b/src/API/MAL/MALRequestBuilder.php @@ -23,7 +23,7 @@ use Aviat\AnimeClient\API\{ }; class MALRequestBuilder extends APIRequestBuilder { - + /** * The base url for api requests * @var string $base_url @@ -41,7 +41,7 @@ class MALRequestBuilder extends APIRequestBuilder { 'Content-type' => 'application/x-www-form-urlencoded', 'User-Agent' => "Tim's Anime Client/4.0" ]; - + /** * Valid HTTP request methos * @var array diff --git a/src/API/MAL/MALTrait.php b/src/API/MAL/MALTrait.php index 5d8e84dc..ba69da6e 100644 --- a/src/API/MAL/MALTrait.php +++ b/src/API/MAL/MALTrait.php @@ -27,7 +27,7 @@ use Aviat\Ion\Json; use InvalidArgumentException; trait MALTrait { - + /** * The request builder for the MAL API * @var MALRequestBuilder @@ -51,7 +51,7 @@ trait MALTrait { 'Content-type' => 'application/x-www-form-urlencoded', 'User-Agent' => "Tim's Anime Client/4.0" ]; - + /** * Set the request builder object * @@ -63,7 +63,7 @@ trait MALTrait { $this->requestBuilder = $requestBuilder; return $this; } - + /** * Unencode the dual-encoded ampersands in the body * @@ -78,58 +78,34 @@ trait MALTrait { $rawBody = \Amp\wait($formBody->getBody()); return html_entity_decode($rawBody, \ENT_HTML5, 'UTF-8'); } - + /** * Create a request object * * @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 = []) { - $this->defaultHeaders['User-Agent'] = $_SERVER['HTTP_USER_AGENT'] ?? $this->defaultHeaders; - - $type = strtoupper($type); - $validTypes = ['GET', 'POST', 'DELETE']; - - if ( ! in_array($type, $validTypes)) - { - throw new InvalidArgumentException('Invalid http request type'); - } - $config = $this->container->get('config'); - $logger = $this->container->getLogger('mal-request'); - $headers = array_merge($this->defaultHeaders, $options['headers'] ?? [], [ - 'Authorization' => 'Basic ' . - base64_encode($config->get(['mal','username']) . ':' .$config->get(['mal','password'])) - ]); + $request = $this->requestBuilder + ->newRequest($type, $url) + ->setBasicAuth($config->get(['mal','username']), $config->get(['mal','password'])); - $query = $options['query'] ?? []; - - $url = (strpos($url, '//') !== FALSE) - ? $url - : $this->baseUrl . $url; - - if ( ! empty($query)) + if (array_key_exists('query', $options)) { - $url .= '?' . http_build_query($query); + $request->setQuery($options['query']); } - $request = (new Request) - ->setMethod($type) - ->setUri($url) - ->setProtocol('1.1') - ->setAllHeaders($headers); - if (array_key_exists('body', $options)) { $request->setBody($options['body']); } - - return $request; + + return $request->getFullRequest(); } /** @@ -147,19 +123,16 @@ trait MALTrait { { $logger = $this->container->getLogger('mal-request'); } - + $request = $this->setUpRequest($type, $url, $options); $response = \Amp\wait((new Client)->request($request)); - $logger->debug('MAL api request', [ - 'url' => $url, + $logger->debug('MAL api response', [ 'status' => $response->getStatus(), 'reason' => $response->getReason(), + 'body' => $response->getBody(), 'headers' => $response->getAllHeaders(), 'requestHeaders' => $request->getAllHeaders(), - 'requestBody' => $request->hasBody() ? $request->getBody() : 'No request body', - 'requestBodyBeforeEncode' => $request->hasBody() ? urldecode($request->getBody()) : '', - 'body' => $response->getBody() ]); return $response; diff --git a/src/API/MAL/Model.php b/src/API/MAL/Model.php index a2d0633e..46355b27 100644 --- a/src/API/MAL/Model.php +++ b/src/API/MAL/Model.php @@ -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; @@ -35,15 +36,20 @@ class Model { protected $animeListTransformer; /** - * KitsuModel constructor. + * MAL Model constructor. */ public function __construct(ListItem $listItem) { $this->animeListTransformer = new AnimeListTransformer(); $this->listItem = $listItem; } + + public function createFullListItem(array $data): Request + { + return $this->listItem->create($data); + } - public function createListItem(array $data): bool + public function createListItem(array $data): Request { $createData = [ 'id' => $data['id'], @@ -69,7 +75,7 @@ class Model { ] ]); - return $list;//['anime']; + return $list['myanimelist']['anime']; } public function getListItem(string $listId): array @@ -77,13 +83,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); } diff --git a/src/API/MAL/Transformer/AnimeListTransformer.php b/src/API/MAL/Transformer/AnimeListTransformer.php index 38cf8182..16f59d8a 100644 --- a/src/API/MAL/Transformer/AnimeListTransformer.php +++ b/src/API/MAL/Transformer/AnimeListTransformer.php @@ -32,6 +32,12 @@ class AnimeListTransformer extends AbstractTransformer { AnimeWatchingStatus::PLAN_TO_WATCH => '6' ]; + /** + * Transform MAL episode data to Kitsu episode data + * + * @param array $item + * @return array + */ public function transform($item) { $rewatching = (array_key_exists('rewatching', $item) && $item['rewatching']); @@ -57,28 +63,44 @@ class AnimeListTransformer extends AbstractTransformer { */ public function untransform(array $item): array { - $rewatching = (array_key_exists('reconsuming', $item['data']) && $item['data']['reconsuming']); - $map = [ 'id' => $item['mal_id'], 'data' => [ - 'episode' => $item['data']['progress'], - // 'enable_rewatching' => $rewatching, - // 'times_rewatched' => $item['data']['reconsumeCount'], - // 'comments' => $item['data']['notes'], + 'episode' => $item['data']['progress'] ] ]; - if (array_key_exists('rating', $item['data'])) - { - $map['data']['score'] = $item['data']['rating'] * 2; - } + $data =& $item['data']; - if (array_key_exists('status', $item['data'])) + foreach($item['data'] as $key => $value) { - $map['data']['status'] = self::statusMap[$item['data']['status']]; + switch($key) + { + case 'notes': + $map['data']['comments'] = $value; + break; + + case 'rating': + $map['data']['score'] = $value * 2; + break; + + case 'reconsuming': + $map['data']['enable_rewatching'] = (bool) $value; + break; + + case 'reconsumeCount': + $map['data']['times_rewatched'] = $value; + break; + + case 'status': + $map['data']['status'] = self::statusMap[$value]; + break; + + default: + break; + } } - + return $map; } } \ No newline at end of file diff --git a/src/AnimeClient.php b/src/AnimeClient.php index 0ffaca69..b43531bd 100644 --- a/src/AnimeClient.php +++ b/src/AnimeClient.php @@ -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 \ No newline at end of file + + return $output; +} \ No newline at end of file diff --git a/src/Command/BaseCommand.php b/src/Command/BaseCommand.php index e929578f..cff2621c 100644 --- a/src/Command/BaseCommand.php +++ b/src/Command/BaseCommand.php @@ -16,6 +16,8 @@ namespace Aviat\AnimeClient\Command; +use function Aviat\AnimeClient\loadToml; + use Aura\Session\SessionFactory; use Aviat\AnimeClient\{ AnimeClient, @@ -23,15 +25,9 @@ use Aviat\AnimeClient\{ 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\{Kitsu, MAL}; +use Aviat\AnimeClient\API\Kitsu\KitsuRequestBuilder; +use Aviat\AnimeClient\API\MAL\MALRequestBuilder; use Aviat\Banker\Pool; use Aviat\Ion\Config; use Aviat\Ion\Di\{Container, ContainerAware}; @@ -69,26 +65,30 @@ class BaseCommand extends Command { protected function setupContainer() { $APP_DIR = realpath(__DIR__ . '/../../app'); + $APPCONF_DIR = realpath("{$APP_DIR}/appConf/"); $CONF_DIR = realpath("{$APP_DIR}/config/"); - require_once $CONF_DIR . '/base_config.php'; // $base_config + require_once $APPCONF_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,21 +108,35 @@ class BaseCommand extends Command { // Models $container->set('kitsu-model', function($container) { - $listItem = new KitsuListItem(); + $requestBuilder = new KitsuRequestBuilder(); + $requestBuilder->setLogger($container->getLogger('kitsu-request')); + + $listItem = new Kitsu\ListItem(); $listItem->setContainer($container); - $model = new KitsuModel($listItem); + $listItem->setRequestBuilder($requestBuilder); + + $model = new Kitsu\Model($listItem); $model->setContainer($container); + $model->setRequestBuilder($requestBuilder); + $cache = $container->get('cache'); $model->setCache($cache); return $model; }); $container->set('mal-model', function($container) { - $listItem = new MALListItem(); + $requestBuilder = new MALRequestBuilder(); + $requestBuilder->setLogger($container->getLogger('mal-request')); + + $listItem = new MAL\ListItem(); $listItem->setContainer($container); - $model = new MALModel($listItem); + $listItem->setRequestBuilder($requestBuilder); + + $model = new MAL\Model($listItem); $model->setContainer($container); + $model->setRequestBuilder($requestBuilder); return $model; }); + $container->set('util', function($container) { return new Util($container); }); diff --git a/src/Command/SyncKitsuWithMal.php b/src/Command/SyncKitsuWithMal.php index 93a714c2..c219bfff 100644 --- a/src/Command/SyncKitsuWithMal.php +++ b/src/Command/SyncKitsuWithMal.php @@ -16,56 +16,22 @@ namespace Aviat\AnimeClient\Command; +use function Amp\{all, wait}; + use Amp\Artax; -use Aviat\AnimeClient\API\Kitsu; +use Amp\Artax\Client; +use Aviat\AnimeClient\API\{JsonAPI, Kitsu, MAL}; +use Aviat\AnimeClient\API\MAL\Transformer\AnimeListTransformer as ALT; +use Aviat\Ion\Json; /** * 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 +45,216 @@ class SyncKitsuWithMal extends BaseCommand { $this->setContainer($this->setupContainer()); $this->setCache($this->container->get('cache')); $this->kitsuModel = $this->container->get('kitsu-model'); + $this->malModel = $this->container->get('mal-model'); + $malCount = count($this->getMALList()); $kitsuCount = $this->getKitsuAnimeListPageCount(); - $this->echoBox("List item count: {$kitsuCount}"); + + $this->echoBox("Number of MAL list items: {$malCount}"); + $this->echoBox("Number of Kitsu list items: {$kitsuCount}"); + + $data = $this->diffLists(); + $this->echoBox("Number of items that need to be added to MAL: " . count($data)); + + if (! empty($data['addToMAL'])) + { + $this->echoBox("Adding missing list items to MAL"); + $this->createMALListItems($data['addToMAL']); + } + } -} + + public function getKitsuList() + { + $count = $this->getKitsuAnimeListPageCount(); + $size = 100; + $pages = ceil($count / $size); + + $requests = []; + + // Set up requests + for ($i = 0; $i < $count; $i++) + { + $offset = $i * $size; + $requests[] = $this->kitsuModel->getFullAnimeList($size, $offset); + } + + $promiseArray = (new Client())->requestMulti($requests); + + $responses = wait(all($promiseArray)); + $output = []; + + foreach($responses as $response) + { + $data = Json::decode($response->getBody()); + $output = array_merge_recursive($output, $data); + } + + return $output; + } + + public function getMALList() + { + return $this->malModel->getFullList(); + } + + public function filterMappings(array $includes): array + { + $output = []; + + foreach($includes as $id => $mapping) + { + if ($mapping['externalSite'] === 'myanimelist/anime') + { + $output[$id] = $mapping; + } + } + + return $output; + } + + // 2015-05-20T23:48:47.731Z + + public function formatMALList() + { + $orig = $this->getMALList(); + $output = []; + + foreach($orig as $item) + { + $output[$item['series_animedb_id']] = [ + 'id' => $item['series_animedb_id'], + 'data' => [ + 'status' => MAL::MAL_KITSU_WATCHING_STATUS_MAP[$item['my_status']], + 'progress' => $item['my_watched_episodes'], + 'reconsuming' => (bool) $item['my_rewatching'], + 'reconsumeCount' => array_key_exists('times_rewatched', $item) + ? $item['times_rewatched'] + : 0, + // 'notes' => , + 'rating' => $item['my_score'], + 'updatedAt' => (new \DateTime()) + ->setTimestamp((int)$item['my_last_updated']) + ->format(\DateTime::W3C), + ] + ]; + } + + return $output; + } + + public function filterKitsuList() + { + $data = $this->getKitsuList(); + $includes = JsonAPI::organizeIncludes($data['included']); + $includes['mappings'] = $this->filterMappings($includes['mappings']); + + $output = []; + + foreach($data['data'] as $listItem) + { + $animeId = $listItem['relationships']['anime']['data']['id']; + $potentialMappings = $includes['anime'][$animeId]['relationships']['mappings']; + $malId = null; + + foreach ($potentialMappings as $mappingId) + { + if (array_key_exists($mappingId, $includes['mappings'])) + { + $malId = $includes['mappings'][$mappingId]['externalId']; + } + } + + // Skip to the next item if there isn't a MAL ID + if ($malId === null) + { + continue; + } + + $output[$listItem['id']] = [ + 'id' => $listItem['id'], + 'malId' => $malId, + 'data' => $listItem['attributes'], + ]; + } + + return $output; + } + + public function getKitsuAnimeListPageCount() + { + return $this->kitsuModel->getAnimeListCount(); + } + + public function diffLists() + { + // Get libraryEntries with media.mappings from Kitsu + // Organize mappings, and ignore entries without mappings + $kitsuList = $this->filterKitsuList(); + + // Get MAL list data + $malList = $this->formatMALList(); + + $itemsToAddToMAL = []; + + foreach($kitsuList as $kitsuItem) + { + if (array_key_exists($kitsuItem['malId'], $malList)) + { + // Eventually, compare the list entries, and determine which + // needs to be updated + continue; + } + + // Looks like this item only exists on Kitsu + $itemsToAddToMAL[] = [ + 'mal_id' => $kitsuItem['malId'], + 'data' => $kitsuItem['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 + + return [ + 'addToMAL' => $itemsToAddToMAL, + ]; + } + + public function createMALListItems($itemsToAdd) + { + $transformer = new ALT(); + $requests = []; + + foreach($itemsToAdd as $item) + { + $data = $transformer->untransform($item); + $requests[] = $this->malModel->createFullListItem($data); + } + + $promiseArray = (new Client())->requestMulti($requests); + + $responses = wait(all($promiseArray)); + + foreach($responses as $key => $response) + { + $id = $itemsToAdd[$key]['mal_id']; + if ($response->getBody() === 'Created') + { + $this->echoBox("Successfully create list item with id: {$id}"); + } + else + { + $this->echoBox("Failed to create list item with id: {$id}"); + } + } + } + +} \ No newline at end of file diff --git a/src/Controller.php b/src/Controller.php index 91148358..e83d71cd 100644 --- a/src/Controller.php +++ b/src/Controller.php @@ -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(); diff --git a/src/Dispatcher.php b/src/Dispatcher.php index 9b5892aa..f06a185b 100644 --- a/src/Dispatcher.php +++ b/src/Dispatcher.php @@ -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)) { diff --git a/src/Model/API.php b/src/Model/API.php index d2c423af..24879073 100644 --- a/src/Model/API.php +++ b/src/Model/API.php @@ -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); } -} +} \ No newline at end of file diff --git a/src/Model/Anime.php b/src/Model/Anime.php index 14bc58a1..4064bbb3 100644 --- a/src/Model/Anime.php +++ b/src/Model/Anime.php @@ -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; } /** @@ -151,12 +168,23 @@ class Anime extends API { */ public function updateLibraryItem(array $data): array { + $requests = []; + if ($this->useMALAPI) { - $this->malModel->updateListItem($data); + $requests['mal'] = $this->malModel->updateListItem($data); } - return $this->kitsuModel->updateListItem($data); + $requests['kitsu'] = $this->kitsuModel->updateListItem($data); + + $promises = (new Client)->requestMulti($requests); + + $results = wait(some($promises)); + + return [ + 'body' => Json::decode($results[1]['kitsu']->getBody()), + 'statusCode' => $results[1]['kitsu']->getStatus() + ]; } /** @@ -168,12 +196,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 \ No newline at end of file diff --git a/tests/API/APIRequestBuilderTest.php b/tests/API/APIRequestBuilderTest.php new file mode 100644 index 00000000..f2050e86 --- /dev/null +++ b/tests/API/APIRequestBuilderTest.php @@ -0,0 +1,135 @@ + + * @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\Tests\API; + +use Amp; +use Amp\Artax\Client; +use Aviat\AnimeClient\API\APIRequestBuilder; +use Aviat\Ion\Json; +use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; + +class APIRequestBuilderTest extends TestCase { + + public function setUp() + { + $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') + ->getFullRequest(); + $response = Amp\wait((new Client)->request($request)); + $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 = [ + 'foo' => 'bar', + 'bar' => [ + 'foo' => 'bar' + ], + 'baz' => [ + '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']); + } + + 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 = [ + 'foo' => [ + 'bar' => 1, + 'baz' => [2, 3, 4], + 'bar' => [ + 'a' => 1, + 'b' => 2 + ] + ] + ]; + + $request = $this->builder->newRequest('PUT', 'https://httpbin.org/put') + ->setHeader('Content-Type', 'application/json') + ->setJsonBody($data) + ->getFullRequest(); + + $response = Amp\wait((new Client)->request($request)); + $body = Json::decode($response->getBody()); + + $this->assertEquals($data, $body['json']); + } +} \ No newline at end of file diff --git a/tests/API/CacheTraitTest.php b/tests/API/CacheTraitTest.php index 7f34b8eb..a221e4e4 100644 --- a/tests/API/CacheTraitTest.php +++ b/tests/API/CacheTraitTest.php @@ -1,4 +1,18 @@ + * @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\Tests\API; diff --git a/tests/API/Kitsu/Transformer/AnimeListTransformerTest.php b/tests/API/Kitsu/Transformer/AnimeListTransformerTest.php index de87f29a..a18ace95 100644 --- a/tests/API/Kitsu/Transformer/AnimeListTransformerTest.php +++ b/tests/API/Kitsu/Transformer/AnimeListTransformerTest.php @@ -1,4 +1,18 @@ + * @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\Tests\API\Kitsu\Transformer; diff --git a/tests/API/Kitsu/Transformer/AnimeTransformerTest.php b/tests/API/Kitsu/Transformer/AnimeTransformerTest.php index 7d50b096..a0adae93 100644 --- a/tests/API/Kitsu/Transformer/AnimeTransformerTest.php +++ b/tests/API/Kitsu/Transformer/AnimeTransformerTest.php @@ -1,4 +1,18 @@ + * @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\Tests\API\Kitsu\Transformer; diff --git a/tests/API/Kitsu/Transformer/MangaListTransformerTest.php b/tests/API/Kitsu/Transformer/MangaListTransformerTest.php index 658e58cf..9c229a7a 100644 --- a/tests/API/Kitsu/Transformer/MangaListTransformerTest.php +++ b/tests/API/Kitsu/Transformer/MangaListTransformerTest.php @@ -1,4 +1,18 @@ + * @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\Tests\API\Kitsu\Transformer; diff --git a/tests/API/Kitsu/Transformer/MangaTransformerTest.php b/tests/API/Kitsu/Transformer/MangaTransformerTest.php index eb5c3f05..1f2df8cd 100644 --- a/tests/API/Kitsu/Transformer/MangaTransformerTest.php +++ b/tests/API/Kitsu/Transformer/MangaTransformerTest.php @@ -1,4 +1,18 @@ + * @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\Tests\API\Kitsu\Transformer; diff --git a/tests/API/KitsuTest.php b/tests/API/KitsuTest.php index 2d69e8dc..6364fcc6 100644 --- a/tests/API/KitsuTest.php +++ b/tests/API/KitsuTest.php @@ -1,4 +1,18 @@ + * @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\Tests\API; diff --git a/tests/API/XMLTest.php b/tests/API/XMLTest.php index 12a1e724..67cb861b 100644 --- a/tests/API/XMLTest.php +++ b/tests/API/XMLTest.php @@ -1,4 +1,18 @@ + * @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\Tests\API; diff --git a/tests/AnimeClient_TestCase.php b/tests/AnimeClient_TestCase.php index 6e8ab7fa..90733e39 100644 --- a/tests/AnimeClient_TestCase.php +++ b/tests/AnimeClient_TestCase.php @@ -1,12 +1,10 @@ $handler]); - - return $client; - } } // End of AnimeClient_TestCase.php \ No newline at end of file