Merge branch 'develop' into 'master'

Develop

See merge request !13
This commit is contained in:
Timothy Warren 2017-02-14 16:39:37 -05:00
commit a29feae442
48 changed files with 1071 additions and 602 deletions

View File

@ -32,7 +32,7 @@ test:hhvm:
- /usr/local/bin/composer self-update - /usr/local/bin/composer self-update
- curl -Lo /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar - curl -Lo /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar
- chmod +x /usr/local/bin/phpunit - chmod +x /usr/local/bin/phpunit
- composer install --no-dev - composer install --no-dev --ignore-platform-reqs
image: 51systems/docker-gitlab-ci-runner-hhvm image: 51systems/docker-gitlab-ci-runner-hhvm
script: script:
- hhvm -d hhvm.php7.all=true /usr/local/bin/phpunit -c build --coverage-text --colors=never - hhvm -d hhvm.php7.all=true /usr/local/bin/phpunit -c build --coverage-text --colors=never

View File

@ -1,10 +1,11 @@
language: php language: php
install: install:
- composer install - composer install --ignore-platform-reqs
php: php:
- 7 - 7
- 7.1
- hhvm - hhvm
- nightly - nightly

View File

@ -14,6 +14,11 @@
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient
*/ */
use const Aviat\AnimeClient\{
DEFAULT_CONTROLLER_METHOD,
DEFAULT_CONTROLLER_NAMESPACE
};
use Aviat\AnimeClient\AnimeClient; use Aviat\AnimeClient\AnimeClient;
return [ return [
@ -148,25 +153,25 @@ return [
'cache_purge' => [ 'cache_purge' => [
'path' => '/cache_purge', 'path' => '/cache_purge',
'action' => 'clearCache', 'action' => 'clearCache',
'controller' => AnimeClient::DEFAULT_CONTROLLER_NAMESPACE, 'controller' => DEFAULT_CONTROLLER_NAMESPACE,
'verb' => 'get', 'verb' => 'get',
], ],
'login' => [ 'login' => [
'path' => '/login', 'path' => '/login',
'action' => 'login', 'action' => 'login',
'controller' => AnimeClient::DEFAULT_CONTROLLER_NAMESPACE, 'controller' => DEFAULT_CONTROLLER_NAMESPACE,
'verb' => 'get', 'verb' => 'get',
], ],
'login.post' => [ 'login.post' => [
'path' => '/login', 'path' => '/login',
'action' => 'loginAction', 'action' => 'loginAction',
'controller' => AnimeClient::DEFAULT_CONTROLLER_NAMESPACE, 'controller' => DEFAULT_CONTROLLER_NAMESPACE,
'verb' => 'post', 'verb' => 'post',
], ],
'logout' => [ 'logout' => [
'path' => '/logout', 'path' => '/logout',
'action' => 'logout', 'action' => 'logout',
'controller' => AnimeClient::DEFAULT_CONTROLLER_NAMESPACE, 'controller' => DEFAULT_CONTROLLER_NAMESPACE,
], ],
'update' => [ 'update' => [
'path' => '/{controller}/update', 'path' => '/{controller}/update',
@ -194,7 +199,7 @@ return [
], ],
'list' => [ 'list' => [
'path' => '/{controller}/{type}{/view}', 'path' => '/{controller}/{type}{/view}',
'action' => AnimeClient::DEFAULT_CONTROLLER_METHOD, 'action' => DEFAULT_CONTROLLER_METHOD,
'tokens' => [ 'tokens' => [
'type' => '[a-z_]+', 'type' => '[a-z_]+',
'view' => '[a-z_]+', 'view' => '[a-z_]+',
@ -202,7 +207,7 @@ return [
], ],
'index_redirect' => [ 'index_redirect' => [
'path' => '/', 'path' => '/',
'controller' => AnimeClient::DEFAULT_CONTROLLER_NAMESPACE, 'controller' => DEFAULT_CONTROLLER_NAMESPACE,
'action' => 'redirectToDefaultRoute', 'action' => 'redirectToDefaultRoute',
], ],
], ],

View File

@ -19,17 +19,9 @@ namespace Aviat\AnimeClient;
use Aura\Html\HelperLocatorFactory; use Aura\Html\HelperLocatorFactory;
use Aura\Router\RouterContainer; use Aura\Router\RouterContainer;
use Aura\Session\SessionFactory; use Aura\Session\SessionFactory;
use Aviat\AnimeClient\API\Kitsu\{ use Aviat\AnimeClient\API\{Kitsu, MAL};
Auth as KitsuAuth, use Aviat\AnimeClient\API\Kitsu\KitsuRequestBuilder;
ListItem as KitsuListItem, use Aviat\AnimeClient\API\MAL\MALRequestBuilder;
KitsuRequestBuilder,
Model as KitsuModel
};
use Aviat\AnimeClient\API\MAL\{
ListItem as MALListItem,
MALRequestBuilder,
Model as MALModel
};
use Aviat\AnimeClient\Model; use Aviat\AnimeClient\Model;
use Aviat\Banker\Pool; use Aviat\Banker\Pool;
use Aviat\Ion\Config; use Aviat\Ion\Config;
@ -120,11 +112,11 @@ return function(array $config_array = []) {
$requestBuilder = new KitsuRequestBuilder(); $requestBuilder = new KitsuRequestBuilder();
$requestBuilder->setLogger($container->getLogger('kitsu-request')); $requestBuilder->setLogger($container->getLogger('kitsu-request'));
$listItem = new KitsuListItem(); $listItem = new Kitsu\ListItem();
$listItem->setContainer($container); $listItem->setContainer($container);
$listItem->setRequestBuilder($requestBuilder); $listItem->setRequestBuilder($requestBuilder);
$model = new KitsuModel($listItem); $model = new Kitsu\Model($listItem);
$model->setContainer($container); $model->setContainer($container);
$model->setRequestBuilder($requestBuilder); $model->setRequestBuilder($requestBuilder);
@ -136,11 +128,11 @@ return function(array $config_array = []) {
$requestBuilder = new MALRequestBuilder(); $requestBuilder = new MALRequestBuilder();
$requestBuilder->setLogger($container->getLogger('mal-request')); $requestBuilder->setLogger($container->getLogger('mal-request'));
$listItem = new MALListItem(); $listItem = new MAL\ListItem();
$listItem->setContainer($container); $listItem->setContainer($container);
$listItem->setRequestBuilder($requestBuilder); $listItem->setRequestBuilder($requestBuilder);
$model = new MALModel($listItem); $model = new MAL\Model($listItem);
$model->setContainer($container); $model->setContainer($container);
$model->setRequestBuilder($requestBuilder); $model->setRequestBuilder($requestBuilder);
return $model; return $model;
@ -161,7 +153,7 @@ return function(array $config_array = []) {
// Miscellaneous Classes // Miscellaneous Classes
$container->set('auth', function($container) { $container->set('auth', function($container) {
return new KitsuAuth($container); return new Kitsu\Auth($container);
}); });
$container->set('url-generator', function($container) { $container->set('url-generator', function($container) {
return new UrlGenerator($container); return new UrlGenerator($container);

View File

@ -6,6 +6,7 @@
<meta http-equiv="cache-control" content="no-store" /> <meta http-equiv="cache-control" content="no-store" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self'" /> <meta http-equiv="Content-Security-Policy" content="script-src 'self'" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0" />
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="<?= $urlGenerator->asset_url('css.php/g/base') ?>" /> <link rel="stylesheet" href="<?= $urlGenerator->asset_url('css.php/g/base') ?>" />
<script src="<?= $urlGenerator->asset_url('js.php/g/base') ?>"></script> <script src="<?= $urlGenerator->asset_url('js.php/g/base') ?>"></script>
</head> </head>

View File

@ -3,6 +3,9 @@
"description": "A self-hosted anime/manga client for Kitsu.", "description": "A self-hosted anime/manga client for Kitsu.",
"license":"MIT", "license":"MIT",
"autoload": { "autoload": {
"files": [
"src/AnimeClient.php"
],
"psr-4": { "psr-4": {
"Aviat\\AnimeClient\\": "src/" "Aviat\\AnimeClient\\": "src/"
} }
@ -20,7 +23,6 @@
"aviat/banker": "^1.0.0", "aviat/banker": "^1.0.0",
"aviat/ion": "1.0.*", "aviat/ion": "1.0.*",
"filp/whoops": "^2.1.5", "filp/whoops": "^2.1.5",
"guzzlehttp/guzzle": "^6.0",
"monolog/monolog": "^1.0", "monolog/monolog": "^1.0",
"psr/http-message": "~1.0", "psr/http-message": "~1.0",
"psr/log": "~1.0", "psr/log": "~1.0",

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -15,6 +15,8 @@
*/ */
namespace Aviat\AnimeClient; namespace Aviat\AnimeClient;
use function Aviat\AnimeClient\loadToml;
use Aviat\AnimeClient\AnimeClient; use Aviat\AnimeClient\AnimeClient;
use Whoops\Handler\PrettyPageHandler; use Whoops\Handler\PrettyPageHandler;
use Whoops\Run; use Whoops\Run;
@ -39,10 +41,11 @@ function _dir()
// Define base directories // Define base directories
$APP_DIR = _dir(__DIR__, 'app'); $APP_DIR = _dir(__DIR__, 'app');
$APPCONF_DIR = _dir($APP_DIR, 'appConf');
$CONF_DIR = _dir($APP_DIR, 'config'); $CONF_DIR = _dir($APP_DIR, 'config');
// Load composer autoloader // Load composer autoloader
require _dir(__DIR__, '/vendor/autoload.php'); require _dir(__DIR__, 'vendor/autoload.php');
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Setup error handling // Setup error handling
@ -54,18 +57,15 @@ $defaultHandler = new PrettyPageHandler();
$whoops->pushHandler($defaultHandler); $whoops->pushHandler($defaultHandler);
// Register as the error handler // Register as the error handler
if (array_key_exists('whoops', $_GET)) $whoops->register();
{
$whoops->register();
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Dependency Injection setup // 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'); $di = require _dir($APP_DIR, 'bootstrap.php');
$config = AnimeClient::loadToml($CONF_DIR); $config = loadToml($CONF_DIR);
$config_array = array_merge($base_config, $config); $config_array = array_merge($base_config, $config);
$container = $di($config_array); $container = $di($config_array);
@ -78,5 +78,3 @@ unset($CONF_DIR);
// Dispatch to the current route // Dispatch to the current route
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
$container->get('dispatcher')->__invoke(); $container->get('dispatcher')->__invoke();
// End of index.php

View File

@ -160,7 +160,7 @@ class CSSMin extends BaseMin {
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
//Get config files //Get config files
$config = require('../app/config/minify_config.php'); $config = require('../app/appConf/minify_config.php');
$groups = require($config['css_groups_file']); $groups = require($config['css_groups_file']);
if ( ! array_key_exists($_GET['g'], $groups)) if ( ! array_key_exists($_GET['g'], $groups))

View File

@ -16,8 +16,9 @@
namespace Aviat\EasyMin; namespace Aviat\EasyMin;
use GuzzleHttp\Client; use function Amp\wait;
use GuzzleHttp\Psr7\Request; use Amp\Artax\{Client, FormBody, Request};
use Aviat\Ion\Json;
// Include guzzle // Include guzzle
require_once('../vendor/autoload.php'); require_once('../vendor/autoload.php');
@ -97,14 +98,21 @@ class JSMin extends BaseMin {
*/ */
protected function closure_call(array $options) protected function closure_call(array $options)
{ {
$client = new Client(); $formFields = http_build_query($options);
$response = $client->post('http://closure-compiler.appspot.com/compile', [
'headers' => [ $request = (new Request)
->setMethod('POST')
->setUri('http://closure-compiler.appspot.com/compile')
->setAllHeaders([
'Accept' => 'application/json',
'Accept-Encoding' => 'gzip', 'Accept-Encoding' => 'gzip',
'Content-type' => 'application/x-www-form-urlencoded' '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; return $response;
} }
@ -118,13 +126,14 @@ class JSMin extends BaseMin {
protected function check_minify_errors($options) protected function check_minify_errors($options)
{ {
$error_res = $this->closure_call($options); $error_res = $this->closure_call($options);
$error_json = (string)$error_res->getBody(); $error_json = $error_res->getBody();
$error_obj = json_decode($error_json) ?: (object)[]; $error_obj = Json::decode($error_json) ?: (object)[];
// Show error if exists // Show error if exists
if ( ! empty($error_obj->errors) || ! empty($error_obj->serverErrors)) 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'); header('Content-type: application/javascript');
echo "console.error(${error_json});"; echo "console.error(${error_json});";
die(); die();
@ -201,10 +210,11 @@ class JSMin extends BaseMin {
// Now actually retrieve the compiled code // Now actually retrieve the compiled code
$options['output_info'] = 'compiled_code'; $options['output_info'] = 'compiled_code';
$res = $this->closure_call($options); $res = $this->closure_call($options);
$json = (string)$res->getBody(); $json = $res->getBody();
$obj = json_decode($json); $obj = Json::decode($json);
return $obj->compiledCode; //return $obj;
return $obj['compiledCode'];
} }
/** /**
@ -223,7 +233,7 @@ class JSMin extends BaseMin {
// ! Start Minifying // ! 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']); $groups = require_once($config['js_groups_file']);
$cache_dir = "{$config['js_root']}cache"; $cache_dir = "{$config['js_root']}cache";

View File

@ -16,12 +16,14 @@
namespace Aviat\AnimeClient\API; namespace Aviat\AnimeClient\API;
use Amp;
use Amp\Artax\{ use Amp\Artax\{
Client, Client,
FormBody, FormBody,
Request Request
}; };
use Aviat\Ion\Di\ContainerAware; use Aviat\Ion\Di\ContainerAware;
use Aviat\Ion\Json;
use InvalidArgumentException; use InvalidArgumentException;
use Psr\Log\LoggerAwareTrait; use Psr\Log\LoggerAwareTrait;
@ -68,15 +70,30 @@ class APIRequestBuilder {
protected $request; protected $request;
/** /**
* Set body as form fields * Set an authorization header
* *
* @param array $fields Mapping of field names to values * @param string $type The type of authorization, eg, basic, bearer, etc.
* @param string $value The authorization value
* @return self * @return self
*/ */
public function setFormFields(array $fields): self public function setAuth(string $type, string $value): self
{ {
$body = $this->fixBody((new FormBody)->addFields($createData)); $authString = ucfirst($type) . ' ' . $value;
$this->setBody($body); $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; return $this;
} }
@ -92,6 +109,20 @@ class APIRequestBuilder {
return $this; 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 * Set a request header
* *
@ -123,6 +154,21 @@ class APIRequestBuilder {
return $this; 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 * Append a query string in array format
* *
@ -143,6 +189,16 @@ class APIRequestBuilder {
public function getFullRequest() public function getFullRequest()
{ {
$this->buildUri(); $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; return $this->request;
} }
@ -166,6 +222,13 @@ class APIRequestBuilder {
->setMethod($type) ->setMethod($type)
->setProtocol('1.1'); ->setProtocol('1.1');
$this->path = $uri;
if ( ! empty($this->defaultHeaders))
{
$this->setHeaders($this->defaultHeaders);
}
return $this; return $this;
} }
@ -178,7 +241,7 @@ class APIRequestBuilder {
{ {
$url = (strpos($this->path, '//') !== FALSE) $url = (strpos($this->path, '//') !== FALSE)
? $this->path ? $this->path
: $this->baseUrl . $url; : $this->baseUrl . $this->path;
if ( ! empty($this->query)) if ( ! empty($this->query))
{ {
@ -188,21 +251,6 @@ class APIRequestBuilder {
$this->request->setUri($url); $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 * Reset the class state for a new request
* *

View File

@ -14,20 +14,10 @@
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient
*/ */
namespace Aviat\AnimeClient\API\MAL; namespace Aviat\AnimeClient\API;
use Aviat\Ion\Transformer\AbstractTransformer; use UnexpectedValueException;
class MALToKitsuTransformer extends AbstractTransformer { class FailedResponseException extends UnexpectedValueException {
public function transform($item)
{
}
public function untransform($item)
{
}
} }

View File

@ -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();
}

View File

@ -23,6 +23,10 @@ use Aviat\AnimeClient\API\Kitsu\Enum\{
}; };
use DateTimeImmutable; 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 * Data massaging helpers for the Kitsu API
*/ */

View File

@ -16,6 +16,8 @@
namespace Aviat\AnimeClient\API\Kitsu; namespace Aviat\AnimeClient\API\Kitsu;
use const Aviat\AnimeClient\SESSION_SEGMENT;
use Aviat\AnimeClient\AnimeClient; use Aviat\AnimeClient\AnimeClient;
use Aviat\AnimeClient\API\{ use Aviat\AnimeClient\API\{
CacheTrait, CacheTrait,
@ -55,7 +57,7 @@ class Auth {
$this->setContainer($container); $this->setContainer($container);
$this->setCache($container->get('cache')); $this->setCache($container->get('cache'));
$this->segment = $container->get('session') $this->segment = $container->get('session')
->getSegment(AnimeClient::SESSION_SEGMENT); ->getSegment(SESSION_SEGMENT);
$this->model = $container->get('kitsu-model'); $this->model = $container->get('kitsu-model');
} }

View File

@ -40,16 +40,4 @@ class KitsuRequestBuilder extends APIRequestBuilder {
'client_id' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd', 'client_id' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd',
'client_secret' => '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151', '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);
}
} }

View File

@ -16,13 +16,14 @@
namespace Aviat\AnimeClient\API\Kitsu; 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\AnimeClient;
use Aviat\AnimeClient\API\GuzzleTrait;
use Aviat\AnimeClient\API\Kitsu as K; use Aviat\AnimeClient\API\Kitsu as K;
use Aviat\Ion\Json; use Aviat\Ion\Json;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Psr7\Response;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
@ -34,37 +35,6 @@ trait KitsuTrait {
*/ */
protected $requestBuilder; 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 * 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 = [ $config = $this->container->get('config');
'cookies' => $this->cookieJar,
'headers' => $this->defaultHeaders,
'timeout' => 25,
'connect_timeout' => 25
];
$this->cookieJar = new CookieJar(); $request = $this->requestBuilder->newRequest($type, $url);
$this->client = new Client([
'base_uri' => $this->baseUrl, $sessionSegment = $this->getContainer()
'cookies' => TRUE, ->get('session')
'http_errors' => TRUE, ->getSegment(SESSION_SEGMENT);
'defaults' => $defaults
]); 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 $type
* @param string $url * @param string $url
@ -110,48 +99,24 @@ trait KitsuTrait {
*/ */
private function getResponse(string $type, string $url, array $options = []) private function getResponse(string $type, string $url, array $options = [])
{ {
$logger = null; $request = $this->setUpRequest($type, $url, $options);
$validTypes = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
if ( ! in_array($type, $validTypes))
{
throw new InvalidArgumentException('Invalid http request type');
}
$defaultOptions = [
'headers' => $this->defaultHeaders
];
$logger = $this->container->getLogger('kitsu-request'); $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) $response = wait((new Client)->request($request));
{
$token = $sessionSegment->get('auth_token');
$defaultOptions['headers']['Authorization'] = "bearer {$token}";
}
$options = array_merge($defaultOptions, $options); /* $logger->debug('Kitsu api response', [
'status' => $response->getStatus(),
$response = $this->client->request($type, $url, $options); 'reason' => $response->getReason(),
'body' => $response->getBody(),
$logger->debug('Kitsu API request', [ 'headers' => $response->getAllHeaders(),
'requestParams' => [ 'requestHeaders' => $request->getAllHeaders(),
'type' => $type, ]); */
'url' => $url,
],
'responseValues' => [
'status' => $response->getStatusCode()
]
]);
return $response; return $response;
} }
/** /**
* Make a request via Guzzle * Make a request
* *
* @param string $type * @param string $type
* @param string $url * @param string $url
@ -168,11 +133,11 @@ trait KitsuTrait {
$response = $this->getResponse($type, $url, $options); $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) 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); $response = $this->getResponse('POST', ...$args);
$validResponseCodes = [200, 201]; $validResponseCodes = [200, 201];
if ( ! in_array((int) $response->getStatusCode(), $validResponseCodes)) if ( ! in_array((int) $response->getStatus(), $validResponseCodes))
{ {
if ($logger) if ($logger)
{ {
@ -238,6 +203,6 @@ trait KitsuTrait {
protected function deleteRequest(...$args): bool protected function deleteRequest(...$args): bool
{ {
$response = $this->getResponse('DELETE', ...$args); $response = $this->getResponse('DELETE', ...$args);
return ((int) $response->getStatusCode() === 204); return ((int) $response->getStatus() === 204);
} }
} }

View File

@ -16,11 +16,12 @@
namespace Aviat\AnimeClient\API\Kitsu; namespace Aviat\AnimeClient\API\Kitsu;
use const Aviat\AnimeClient\SESSION_SEGMENT;
use Amp\Artax\Request;
use Aviat\AnimeClient\API\AbstractListItem; use Aviat\AnimeClient\API\AbstractListItem;
use Aviat\Ion\Di\ContainerAware; use Aviat\Ion\Di\ContainerAware;
use Aviat\Ion\Json; use Aviat\Ion\Json;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Psr7\Response;
use RuntimeException; use RuntimeException;
/** /**
@ -30,59 +31,100 @@ class ListItem extends AbstractListItem {
use ContainerAware; use ContainerAware;
use KitsuTrait; 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 = [
'body' => Json::encode([ 'data' => [
'data' => [ 'type' => 'libraryEntries',
'type' => 'libraryEntries', 'attributes' => [
'attributes' => [ 'status' => $data['status'],
'status' => $data['status'], 'progress' => $data['progress'] ?? 0
'progress' => $data['progress'] ?? 0 ],
'relationships' => [
'user' => [
'data' => [
'id' => $data['user_id'],
'type' => 'users'
]
], ],
'relationships' => [ 'media' => [
'user' => [ 'data' => [
'data' => [ 'id' => $data['id'],
'id' => $data['user_id'], 'type' => $data['type']
'type' => 'users'
]
],
'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}"); $authHeader = $this->getAuthHeader();
return ($response->getStatusCode() === 204); $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 public function get(string $id): array
{ {
return $this->getRequest("library-entries/{$id}", [ $authHeader = $this->getAuthHeader();
'query' => [
$request = $this->requestBuilder->newRequest('GET', "library-entries/{$id}")
->setQuery([
'include' => 'media,media.genres,media.mappings' '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 = [ $requestData = [
'data' => [ 'data' => [
'id' => $id, 'id' => $id,
@ -91,10 +133,14 @@ class ListItem extends AbstractListItem {
] ]
]; ];
$response = $this->getResponse('PATCH', "library-entries/{$id}", [ $request = $this->requestBuilder->newRequest('PATCH', "library-entries/{$id}")
'body' => JSON::encode($requestData) ->setJsonBody($requestData);
]);
return $response; if ($authHeader !== FALSE)
{
$request = $request->setHeader('Authorization', $authHeader);
}
return $request->getFullRequest();
} }
} }

View File

@ -16,6 +16,7 @@
namespace Aviat\AnimeClient\API\Kitsu; namespace Aviat\AnimeClient\API\Kitsu;
use Amp\Artax\Request;
use Aviat\AnimeClient\API\CacheTrait; use Aviat\AnimeClient\API\CacheTrait;
use Aviat\AnimeClient\API\JsonAPI; use Aviat\AnimeClient\API\JsonAPI;
use Aviat\AnimeClient\API\Kitsu as K; 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\Di\ContainerAware;
use Aviat\Ion\Json; use Aviat\Ion\Json;
use GuzzleHttp\Exception\ClientException;
/** /**
* Kitsu API Model * Kitsu API Model
@ -72,9 +72,6 @@ class Model {
*/ */
public function __construct(ListItem $listItem) public function __construct(ListItem $listItem)
{ {
// Set up Guzzle trait
$this->init();
$this->animeTransformer = new AnimeTransformer(); $this->animeTransformer = new AnimeTransformer();
$this->animeListTransformer = new AnimeListTransformer(); $this->animeListTransformer = new AnimeListTransformer();
$this->listItem = $listItem; $this->listItem = $listItem;
@ -207,14 +204,55 @@ class Model {
return $this->mangaTransformer->transform($baseData); 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 * 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 * Create a list item
* *
* @param array $data * @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()); $data['user_id'] = $this->getUserIdByUsername($this->getUsername());
return $this->listItem->create($data); return $this->listItem->create($data);
@ -397,34 +435,20 @@ class Model {
* Modify a list item * Modify a list item
* *
* @param array $data * @param array $data
* @return array * @return Request
*/ */
public function updateListItem(array $data) public function updateListItem(array $data): Request
{ {
try return $this->listItem->update($data['id'], $data['data']);
{
$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())
];
}
} }
/** /**
* Remove a list item * Remove a list item
* *
* @param string $id - The id of the list item to remove * @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); return $this->listItem->delete($id);
} }

View File

@ -116,7 +116,6 @@ class AnimeListTransformer extends AbstractTransformer {
'mal_id' => $item['mal_id'] ?? null, 'mal_id' => $item['mal_id'] ?? null,
'data' => [ 'data' => [
'status' => $item['watching_status'], 'status' => $item['watching_status'],
'rating' => $item['user_rating'] / 2,
'reconsuming' => $rewatching, 'reconsuming' => $rewatching,
'reconsumeCount' => $item['rewatched'], 'reconsumeCount' => $item['rewatched'],
'notes' => $item['notes'], 'notes' => $item['notes'],
@ -125,9 +124,9 @@ class AnimeListTransformer extends AbstractTransformer {
] ]
]; ];
if ((int) $untransformed['data']['rating'] === 0) if ( ! empty($item['user_rating']))
{ {
unset($untransformed['data']['rating']); $untransformed['data']['rating'] = $item['user_rating'] / 2;
} }
return $untransformed; return $untransformed;

View File

@ -16,7 +16,7 @@
namespace Aviat\AnimeClient\API; namespace Aviat\AnimeClient\API;
use GuzzleHttp\Psr7\Response; use Amp\Artax\Request;
/** /**
* Common interface for anime and manga list item CRUD * Common interface for anime and manga list item CRUD
@ -29,7 +29,7 @@ interface ListItemInterface {
* @param array $data - * @param array $data -
* @return bool * @return bool
*/ */
public function create(array $data): bool; public function create(array $data): Request;
/** /**
* Retrieve a list item * Retrieve a list item
@ -46,7 +46,7 @@ interface ListItemInterface {
* @param array $data - The data with which to update the list item * @param array $data - The data with which to update the list item
* @return Response * @return Response
*/ */
public function update(string $id, array $data): Response; public function update(string $id, array $data): Request;
/** /**
* Delete a list item * Delete a list item
@ -54,5 +54,5 @@ interface ListItemInterface {
* @param string $id - The id of the list item to delete * @param string $id - The id of the list item to delete
* @return bool * @return bool
*/ */
public function delete(string $id): bool; public function delete(string $id): Request;
} }

View File

@ -37,6 +37,14 @@ class MAL {
KAWS::PLAN_TO_WATCH => AnimeWatchingStatus::PLAN_TO_WATCH 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() public static function getIdToWatchingStatusMap()
{ {
return [ return [

View File

@ -16,7 +16,7 @@
namespace Aviat\AnimeClient\API\MAL; namespace Aviat\AnimeClient\API\MAL;
use Amp\Artax\FormBody; use Amp\Artax\{FormBody, Request};
use Aviat\AnimeClient\API\{ use Aviat\AnimeClient\API\{
AbstractListItem, AbstractListItem,
XML XML
@ -30,7 +30,7 @@ class ListItem {
use ContainerAware; use ContainerAware;
use MALTrait; use MALTrait;
public function create(array $data): bool public function create(array $data): Request
{ {
$id = $data['id']; $id = $data['id'];
$createData = [ $createData = [
@ -40,20 +40,26 @@ class ListItem {
]) ])
]; ];
$response = $this->getResponse('POST', "animelist/add/{$id}.xml", [ $config = $this->container->get('config');
'body' => $this->fixBody((new FormBody)->addFields($createData))
]);
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", [ $config = $this->container->get('config');
'body' => $this->fixBody((new FormBody)->addField('id', $id))
]);
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 public function get(string $id): array
@ -61,15 +67,21 @@ class ListItem {
return []; 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]); $xml = XML::toXML(['entry' => $data]);
$body = (new FormBody) $body = (new FormBody)
->addField('id', $id) ->addField('id', $id)
->addField('data', $xml); ->addField('data', $xml);
return $this->getResponse('POST', "animelist/update/{$id}.xml", [ return $this->requestBuilder->newRequest('POST', "animelist/update/{$id}.xml")
'body' => $this->fixBody($body) ->setFormFields([
]); 'id' => $id,
'data' => $xml
])
->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password']))
->getFullRequest();
} }
} }

View File

@ -85,51 +85,27 @@ trait MALTrait {
* @param string $type * @param string $type
* @param string $url * @param string $url
* @param array $options * @param array $options
* @return \Amp\Promise * @return \Amp\Artax\Response
*/ */
public function setUpRequest(string $type, string $url, array $options = []) 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'); $config = $this->container->get('config');
$logger = $this->container->getLogger('mal-request');
$headers = array_merge($this->defaultHeaders, $options['headers'] ?? [], [ $request = $this->requestBuilder
'Authorization' => 'Basic ' . ->newRequest($type, $url)
base64_encode($config->get(['mal','username']) . ':' .$config->get(['mal','password'])) ->setBasicAuth($config->get(['mal','username']), $config->get(['mal','password']));
]);
$query = $options['query'] ?? []; if (array_key_exists('query', $options))
$url = (strpos($url, '//') !== FALSE)
? $url
: $this->baseUrl . $url;
if ( ! empty($query))
{ {
$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)) if (array_key_exists('body', $options))
{ {
$request->setBody($options['body']); $request->setBody($options['body']);
} }
return $request; return $request->getFullRequest();
} }
/** /**
@ -151,15 +127,12 @@ trait MALTrait {
$request = $this->setUpRequest($type, $url, $options); $request = $this->setUpRequest($type, $url, $options);
$response = \Amp\wait((new Client)->request($request)); $response = \Amp\wait((new Client)->request($request));
$logger->debug('MAL api request', [ $logger->debug('MAL api response', [
'url' => $url,
'status' => $response->getStatus(), 'status' => $response->getStatus(),
'reason' => $response->getReason(), 'reason' => $response->getReason(),
'body' => $response->getBody(),
'headers' => $response->getAllHeaders(), 'headers' => $response->getAllHeaders(),
'requestHeaders' => $request->getAllHeaders(), 'requestHeaders' => $request->getAllHeaders(),
'requestBody' => $request->hasBody() ? $request->getBody() : 'No request body',
'requestBodyBeforeEncode' => $request->hasBody() ? urldecode($request->getBody()) : '',
'body' => $response->getBody()
]); ]);
return $response; return $response;

View File

@ -16,6 +16,7 @@
namespace Aviat\AnimeClient\API\MAL; namespace Aviat\AnimeClient\API\MAL;
use Amp\Artax\Request;
use Aviat\AnimeClient\API\MAL as M; use Aviat\AnimeClient\API\MAL as M;
use Aviat\AnimeClient\API\MAL\ListItem; use Aviat\AnimeClient\API\MAL\ListItem;
use Aviat\AnimeClient\API\MAL\Transformer\AnimeListTransformer; use Aviat\AnimeClient\API\MAL\Transformer\AnimeListTransformer;
@ -35,7 +36,7 @@ class Model {
protected $animeListTransformer; protected $animeListTransformer;
/** /**
* KitsuModel constructor. * MAL Model constructor.
*/ */
public function __construct(ListItem $listItem) public function __construct(ListItem $listItem)
{ {
@ -43,7 +44,12 @@ class Model {
$this->listItem = $listItem; $this->listItem = $listItem;
} }
public function createListItem(array $data): bool public function createFullListItem(array $data): Request
{
return $this->listItem->create($data);
}
public function createListItem(array $data): Request
{ {
$createData = [ $createData = [
'id' => $data['id'], 'id' => $data['id'],
@ -69,7 +75,7 @@ class Model {
] ]
]); ]);
return $list;//['anime']; return $list['myanimelist']['anime'];
} }
public function getListItem(string $listId): array public function getListItem(string $listId): array
@ -77,13 +83,13 @@ class Model {
return []; return [];
} }
public function updateListItem(array $data) public function updateListItem(array $data): Request
{ {
$updateData = $this->animeListTransformer->untransform($data); $updateData = $this->animeListTransformer->untransform($data);
return $this->listItem->update($updateData['id'], $updateData['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); return $this->listItem->delete($id);
} }

View File

@ -32,6 +32,12 @@ class AnimeListTransformer extends AbstractTransformer {
AnimeWatchingStatus::PLAN_TO_WATCH => '6' AnimeWatchingStatus::PLAN_TO_WATCH => '6'
]; ];
/**
* Transform MAL episode data to Kitsu episode data
*
* @param array $item
* @return array
*/
public function transform($item) public function transform($item)
{ {
$rewatching = (array_key_exists('rewatching', $item) && $item['rewatching']); $rewatching = (array_key_exists('rewatching', $item) && $item['rewatching']);
@ -57,26 +63,42 @@ class AnimeListTransformer extends AbstractTransformer {
*/ */
public function untransform(array $item): array public function untransform(array $item): array
{ {
$rewatching = (array_key_exists('reconsuming', $item['data']) && $item['data']['reconsuming']);
$map = [ $map = [
'id' => $item['mal_id'], 'id' => $item['mal_id'],
'data' => [ 'data' => [
'episode' => $item['data']['progress'], 'episode' => $item['data']['progress']
// 'enable_rewatching' => $rewatching,
// 'times_rewatched' => $item['data']['reconsumeCount'],
// 'comments' => $item['data']['notes'],
] ]
]; ];
if (array_key_exists('rating', $item['data'])) $data =& $item['data'];
{
$map['data']['score'] = $item['data']['rating'] * 2;
}
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; return $map;

View File

@ -20,50 +20,43 @@ use Yosymfony\Toml\Toml;
define('SRC_DIR', realpath(__DIR__)); 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'; foreach ($files as $file)
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
{ {
$output = []; $key = str_replace('.toml', '', basename($file));
$files = glob("{$path}/*.toml"); $toml = file_get_contents($file);
$config = Toml::Parse($toml);
foreach ($files as $file) if ($key === 'config')
{ {
$key = str_replace('.toml', '', basename($file)); foreach($config as $name => $value)
$toml = file_get_contents($file);
$config = Toml::Parse($toml);
if ($key === 'config')
{ {
foreach($config as $name => $value) $output[$name] = $value;
{
$output[$name] = $value;
}
continue;
} }
$output[$key] = $config; continue;
} }
return $output; $output[$key] = $config;
} }
return $output;
} }
// End of AnimeClient.php

View File

@ -16,6 +16,8 @@
namespace Aviat\AnimeClient\Command; namespace Aviat\AnimeClient\Command;
use function Aviat\AnimeClient\loadToml;
use Aura\Session\SessionFactory; use Aura\Session\SessionFactory;
use Aviat\AnimeClient\{ use Aviat\AnimeClient\{
AnimeClient, AnimeClient,
@ -23,15 +25,9 @@ use Aviat\AnimeClient\{
Util Util
}; };
use Aviat\AnimeClient\API\CacheTrait; use Aviat\AnimeClient\API\CacheTrait;
use Aviat\AnimeClient\API\Kitsu\{ use Aviat\AnimeClient\API\{Kitsu, MAL};
Auth as KitsuAuth, use Aviat\AnimeClient\API\Kitsu\KitsuRequestBuilder;
ListItem as KitsuListItem, use Aviat\AnimeClient\API\MAL\MALRequestBuilder;
Model as KitsuModel
};
use Aviat\AnimeClient\API\MAL\{
ListItem as MALListItem,
Model as MALModel
};
use Aviat\Banker\Pool; use Aviat\Banker\Pool;
use Aviat\Ion\Config; use Aviat\Ion\Config;
use Aviat\Ion\Di\{Container, ContainerAware}; use Aviat\Ion\Di\{Container, ContainerAware};
@ -69,10 +65,11 @@ class BaseCommand extends Command {
protected function setupContainer() protected function setupContainer()
{ {
$APP_DIR = realpath(__DIR__ . '/../../app'); $APP_DIR = realpath(__DIR__ . '/../../app');
$APPCONF_DIR = realpath("{$APP_DIR}/appConf/");
$CONF_DIR = realpath("{$APP_DIR}/config/"); $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); $config_array = array_merge($base_config, $config);
$di = function ($config_array) use ($APP_DIR) { $di = function ($config_array) use ($APP_DIR) {
@ -84,10 +81,13 @@ class BaseCommand extends Command {
$app_logger = new Logger('animeclient'); $app_logger = new Logger('animeclient');
$app_logger->pushHandler(new NullHandler); $app_logger->pushHandler(new NullHandler);
$request_logger = new Logger('request'); $kitsu_request_logger = new Logger('kitsu-request');
$request_logger->pushHandler(new NullHandler); $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($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 // Create Config Object
$container->set('config', function() use ($config_array) { $container->set('config', function() use ($config_array) {
@ -108,21 +108,35 @@ class BaseCommand extends Command {
// Models // Models
$container->set('kitsu-model', function($container) { $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); $listItem->setContainer($container);
$model = new KitsuModel($listItem); $listItem->setRequestBuilder($requestBuilder);
$model = new Kitsu\Model($listItem);
$model->setContainer($container); $model->setContainer($container);
$model->setRequestBuilder($requestBuilder);
$cache = $container->get('cache'); $cache = $container->get('cache');
$model->setCache($cache); $model->setCache($cache);
return $model; return $model;
}); });
$container->set('mal-model', function($container) { $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); $listItem->setContainer($container);
$model = new MALModel($listItem); $listItem->setRequestBuilder($requestBuilder);
$model = new MAL\Model($listItem);
$model->setContainer($container); $model->setContainer($container);
$model->setRequestBuilder($requestBuilder);
return $model; return $model;
}); });
$container->set('util', function($container) { $container->set('util', function($container) {
return new Util($container); return new Util($container);
}); });

View File

@ -16,8 +16,13 @@
namespace Aviat\AnimeClient\Command; namespace Aviat\AnimeClient\Command;
use function Amp\{all, wait};
use Amp\Artax; 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 * Clears the API Cache
@ -25,46 +30,7 @@ use Aviat\AnimeClient\API\Kitsu;
class SyncKitsuWithMal extends BaseCommand { class SyncKitsuWithMal extends BaseCommand {
protected $kitsuModel; protected $kitsuModel;
protected $malModel;
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'];
}
/** /**
* Run the image conversion script * Run the image conversion script
@ -79,8 +45,216 @@ class SyncKitsuWithMal extends BaseCommand {
$this->setContainer($this->setupContainer()); $this->setContainer($this->setupContainer());
$this->setCache($this->container->get('cache')); $this->setCache($this->container->get('cache'));
$this->kitsuModel = $this->container->get('kitsu-model'); $this->kitsuModel = $this->container->get('kitsu-model');
$this->malModel = $this->container->get('mal-model');
$malCount = count($this->getMALList());
$kitsuCount = $this->getKitsuAnimeListPageCount(); $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}");
}
}
}
} }

View File

@ -16,6 +16,8 @@
namespace Aviat\AnimeClient; namespace Aviat\AnimeClient;
use const Aviat\AnimeClient\SESSION_SEGMENT;
use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
use Aviat\Ion\View\{HtmlView, HttpView, JsonView}; use Aviat\Ion\View\{HtmlView, HttpView, JsonView};
use InvalidArgumentException; use InvalidArgumentException;
@ -102,7 +104,7 @@ class Controller {
$this->urlGenerator = $urlGenerator; $this->urlGenerator = $urlGenerator;
$session = $container->get('session'); $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 // Set a 'previous' flash value for better redirects
$server_params = $this->request->getServerParams(); $server_params = $this->request->getServerParams();

View File

@ -16,9 +16,16 @@
namespace Aviat\AnimeClient; 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\Di\ContainerInterface;
use Aviat\Ion\Friend; use Aviat\Ion\Friend;
use GuzzleHttp\Exception\ServerException;
/** /**
* Basic routing/ dispatch * Basic routing/ dispatch
@ -125,27 +132,12 @@ class Dispatcher extends RoutingBase {
// If not route was matched, return an appropriate http // If not route was matched, return an appropriate http
// error message // error message
$error_route = $this->getErrorParams(); $error_route = $this->getErrorParams();
$controllerName = AnimeClient::DEFAULT_CONTROLLER; $controllerName = DEFAULT_CONTROLLER;
$actionMethod = $error_route['action_method']; $actionMethod = $error_route['action_method'];
$params = $error_route['params']; $params = $error_route['params'];
} }
// Try to catch API errors in a presentable fashion $this->call($controllerName, $actionMethod, $params);
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()
]);
}
} }
/** /**
@ -176,7 +168,7 @@ class Dispatcher extends RoutingBase {
$action_method = (array_key_exists('action', $route->attributes)) $action_method = (array_key_exists('action', $route->attributes))
? $route->attributes['action'] ? $route->attributes['action']
: AnimeClient::NOT_FOUND_METHOD; : NOT_FOUND_METHOD;
$params = []; $params = [];
if ( ! empty($route->__get('tokens'))) if ( ! empty($route->__get('tokens')))
@ -229,11 +221,11 @@ class Dispatcher extends RoutingBase {
*/ */
public function getControllerList() public function getControllerList()
{ {
$default_namespace = AnimeClient::DEFAULT_CONTROLLER_NAMESPACE; $default_namespace = DEFAULT_CONTROLLER_NAMESPACE;
$path = str_replace('\\', '/', $default_namespace); $path = str_replace('\\', '/', $default_namespace);
$path = str_replace('Aviat/AnimeClient/', '', $path); $path = str_replace('Aviat/AnimeClient/', '', $path);
$path = trim($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"); $class_files = glob("{$actual_path}/*.php");
$controllers = []; $controllers = [];
@ -285,7 +277,7 @@ class Dispatcher extends RoutingBase {
$logger->info('Dispatcher - failed route'); $logger->info('Dispatcher - failed route');
$logger->info(print_r($failure, TRUE)); $logger->info(print_r($failure, TRUE));
$action_method = AnimeClient::ERROR_MESSAGE_METHOD; $action_method = ERROR_MESSAGE_METHOD;
$params = []; $params = [];
@ -308,7 +300,7 @@ class Dispatcher extends RoutingBase {
default: default:
// Fall back to a 404 message // Fall back to a 404 message
$action_method = AnimeClient::NOT_FOUND_METHOD; $action_method = NOT_FOUND_METHOD;
break; break;
} }
@ -337,7 +329,7 @@ class Dispatcher extends RoutingBase {
$controller_map = $this->getControllerList(); $controller_map = $this->getControllerList();
$controller_class = (array_key_exists($route_type, $controller_map)) $controller_class = (array_key_exists($route_type, $controller_map))
? $controller_map[$route_type] ? $controller_map[$route_type]
: AnimeClient::DEFAULT_CONTROLLER; : DEFAULT_CONTROLLER;
if (array_key_exists($route_type, $controller_map)) if (array_key_exists($route_type, $controller_map))
{ {

View File

@ -38,12 +38,6 @@ class API extends Model {
*/ */
protected $cache; protected $cache;
/**
* Default settings for Guzzle
* @var array
*/
protected $connectionDefaults = [];
/** /**
* Constructor * Constructor
* *

View File

@ -15,6 +15,10 @@
*/ */
namespace Aviat\AnimeClient\Model; 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\AnimeClient\API\Kitsu\Enum\AnimeWatchingStatus;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Json; use Aviat\Ion\Json;
@ -91,9 +95,15 @@ class Anime extends API {
return $this->kitsuModel->getAnime($slug); 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) public function search($name)
{ {
// $raw = $this->kitsuModel->search('anime', $name);
return $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 public function createLibraryItem(array $data): bool
{ {
$requests = [];
if ($this->useMALAPI) if ($this->useMALAPI)
{ {
$malData = $data; $malData = $data;
@ -136,11 +147,17 @@ class Anime extends API {
if ( ! is_null($malId)) if ( ! is_null($malId))
{ {
$malData['id'] = $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 public function updateLibraryItem(array $data): array
{ {
$requests = [];
if ($this->useMALAPI) 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 public function deleteLibraryItem(string $id, string $malId = null): bool
{ {
$requests = [];
if ($this->useMALAPI && ! is_null($malId)) 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 // End of AnimeModel.php

View File

@ -0,0 +1,135 @@
<?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\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']);
}
}

View File

@ -1,4 +1,18 @@
<?php declare(strict_types=1); <?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\Tests\API; namespace Aviat\AnimeClient\Tests\API;

View File

@ -1,4 +1,18 @@
<?php declare(strict_types=1); <?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\Tests\API\Kitsu\Transformer; namespace Aviat\AnimeClient\Tests\API\Kitsu\Transformer;

View File

@ -1,4 +1,18 @@
<?php declare(strict_types=1); <?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\Tests\API\Kitsu\Transformer; namespace Aviat\AnimeClient\Tests\API\Kitsu\Transformer;

View File

@ -1,4 +1,18 @@
<?php declare(strict_types=1); <?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\Tests\API\Kitsu\Transformer; namespace Aviat\AnimeClient\Tests\API\Kitsu\Transformer;

View File

@ -1,4 +1,18 @@
<?php declare(strict_types=1); <?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\Tests\API\Kitsu\Transformer; namespace Aviat\AnimeClient\Tests\API\Kitsu\Transformer;

View File

@ -1,4 +1,18 @@
<?php declare(strict_types=1); <?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\Tests\API; namespace Aviat\AnimeClient\Tests\API;

View File

@ -1,4 +1,18 @@
<?php declare(strict_types=1); <?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\Tests\API; namespace Aviat\AnimeClient\Tests\API;

View File

@ -1,12 +1,10 @@
<?php <?php
use const Aviat\AnimeClient\SRC_DIR;
use Aura\Web\WebFactory; use Aura\Web\WebFactory;
use Aviat\AnimeClient\AnimeClient; use Aviat\AnimeClient\AnimeClient;
use Aviat\Ion\Json; use Aviat\Ion\Json;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Zend\Diactoros\{ use Zend\Diactoros\{
Response as HttpResponse, Response as HttpResponse,
@ -23,7 +21,7 @@ define('TEST_VIEW_DIR', __DIR__ . '/test_views');
class AnimeClient_TestCase extends TestCase { class AnimeClient_TestCase extends TestCase {
// Test directory constants // Test directory constants
const ROOT_DIR = ROOT_DIR; 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_DATA_DIR = TEST_DATA_DIR;
const TEST_VIEW_DIR = TEST_VIEW_DIR; const TEST_VIEW_DIR = TEST_VIEW_DIR;
@ -159,25 +157,5 @@ class AnimeClient_TestCase extends TestCase {
return Json::decode($rawData); 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 // End of AnimeClient_TestCase.php