From 02838c502449c1ed2ec0063c9f135d5e3cb3a2df Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Tue, 7 Feb 2017 13:27:41 -0500 Subject: [PATCH 01/13] Update headers and some whitespace --- src/API/Kitsu/KitsuTrait.php | 26 +++++++++---------- tests/API/CacheTraitTest.php | 14 ++++++++++ .../Transformer/AnimeListTransformerTest.php | 14 ++++++++++ .../Transformer/AnimeTransformerTest.php | 14 ++++++++++ .../Transformer/MangaListTransformerTest.php | 14 ++++++++++ .../Transformer/MangaTransformerTest.php | 14 ++++++++++ tests/API/KitsuTest.php | 14 ++++++++++ tests/API/XMLTest.php | 14 ++++++++++ 8 files changed, 111 insertions(+), 13 deletions(-) diff --git a/src/API/Kitsu/KitsuTrait.php b/src/API/Kitsu/KitsuTrait.php index 148c35c5..612ee747 100644 --- a/src/API/Kitsu/KitsuTrait.php +++ b/src/API/Kitsu/KitsuTrait.php @@ -27,24 +27,24 @@ 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 Guzzle http client object + * @var object + */ + protected $client; + + /** + * Cookie jar object for api requests + * @var object + */ + protected $cookieJar; /** * The base url for api requests @@ -64,7 +64,7 @@ trait KitsuTrait { 'client_id' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd', 'client_secret' => '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151', ]; - + /** * Set the request builder object * 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; From deecb5a912aa7394b32ad256647caf778d37fccb Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Wed, 8 Feb 2017 00:44:57 -0500 Subject: [PATCH 02/13] Start of work to replace Guzzle with Artax --- src/API/APIClient.php | 39 +++++ src/API/APIRequestBuilder.php | 95 ++++++++---- src/API/Kitsu/ListItem.php | 57 +++++--- .../Transformer/AnimeListTransformer.php | 7 +- src/API/MAL/ListItem.php | 11 +- src/API/MAL/MALRequestBuilder.php | 4 +- src/API/MAL/MALTrait.php | 55 ++----- .../MAL/Transformer/AnimeListTransformer.php | 34 +++-- tests/API/APIRequestBuilderTest.php | 136 ++++++++++++++++++ 9 files changed, 323 insertions(+), 115 deletions(-) create mode 100644 src/API/APIClient.php create mode 100644 tests/API/APIRequestBuilderTest.php diff --git a/src/API/APIClient.php b/src/API/APIClient.php new file mode 100644 index 00000000..05d3b8a9 --- /dev/null +++ b/src/API/APIClient.php @@ -0,0 +1,39 @@ + + * @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; + +use Amp; +use Amp\Artax\{ + Client, + Response, + Request +} + +class APIClient { + + /** + * Get a syncronous response for a request + * + * @param Request $request + * @return Response + */ + static public function syncResponse(Request $request): Response + { + $client = new Client(); + return wait($client->request($request)); + } +} \ No newline at end of file diff --git a/src/API/APIRequestBuilder.php b/src/API/APIRequestBuilder.php index 52fc91c2..0ea1f77a 100644 --- a/src/API/APIRequestBuilder.php +++ b/src/API/APIRequestBuilder.php @@ -17,8 +17,8 @@ namespace Aviat\AnimeClient\API; use Amp\Artax\{ - Client, - FormBody, + Client, + FormBody, Request }; use Aviat\Ion\Di\ContainerAware; @@ -30,56 +30,58 @@ 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 a basic authentication header + * + * @param string $username + * @param string $password * @return self */ - public function setFormFields(array $fields): self + public function setBasicAuth(string $username, string $password): self { - $body = $this->fixBody((new FormBody)->addFields($createData)); - $this->setBody($body); + $authString = 'Basic ' . base64_encode($username . ':' . $password); + $this->setHeader('Authorization', $authString); + return $this; } - + /** * Set the request body * @@ -91,7 +93,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 = $this->fixBody((new FormBody)->addFields($fields)); + $this->setBody($body); + return $this; + } + /** * Set a request header * @@ -104,10 +120,10 @@ class APIRequestBuilder { $this->request->setHeader($name, $value); return $this; } - + /** * Set multiple request headers - * + * * name => value * * @param array $headers @@ -119,10 +135,10 @@ class APIRequestBuilder { { $this->setHeader($name, $value); } - + return $this; } - + /** * Append a query string in array format * @@ -131,10 +147,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 +159,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 +185,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,16 +211,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 * @@ -202,7 +235,7 @@ class APIRequestBuilder { $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/Kitsu/ListItem.php b/src/API/Kitsu/ListItem.php index 57787f15..51121471 100644 --- a/src/API/Kitsu/ListItem.php +++ b/src/API/Kitsu/ListItem.php @@ -37,30 +37,35 @@ class ListItem extends AbstractListItem { public function create(array $data): bool { - $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'] ] ] ] - ]) + ] + ]; + + $request = $this->requestBuilder->newRequest('POST', 'library-entries') + ->setJsonBody($body) + ->getFullRequest(); + $response = $this->getResponse('POST', 'library-entries', [ + 'body' => Json::encode($body) ]); return ($response->getStatusCode() === 201); @@ -74,11 +79,19 @@ class ListItem extends AbstractListItem { public function get(string $id): array { - return $this->getRequest("library-entries/{$id}", [ + $request = $this->requestBuilder->newRequest('GET', "library-entries/{$id}") + ->setQuery([ + 'include' => 'media,media.genres,media.mappings' + ]) + ->getFullRequest(); + /*return $this->getRequest("library-entries/{$id}", [ 'query' => [ 'include' => 'media,media.genres,media.mappings' ] - ]); + ]);*/ + + $response = \Amp\wait((new \Amp\Artax\Client)->request($request)); + return Json::decode($response->getBody()); } public function update(string $id, array $data): Response 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/MAL/ListItem.php b/src/API/MAL/ListItem.php index 715be392..8c87b166 100644 --- a/src/API/MAL/ListItem.php +++ b/src/API/MAL/ListItem.php @@ -30,7 +30,7 @@ class ListItem { use ContainerAware; use MALTrait; - public function create(array $data): bool + public function create(array $data) { $id = $data['id']; $createData = [ @@ -40,11 +40,20 @@ class ListItem { ]) ]; + // $config = $this->container->get('config'); + + /*$request = $this->requestBuilder->newRequest('POST', "animelist/add/{$id}.xml") + ->setFormFields($createData) + ->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password'])) + ->getFullRequest();*/ + $response = $this->getResponse('POST', "animelist/add/{$id}.xml", [ 'body' => $this->fixBody((new FormBody)->addFields($createData)) ]); return $response->getBody() === 'Created'; + + // return $request; } public function delete(string $id): bool 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..8769f632 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,7 +78,7 @@ trait MALTrait { $rawBody = \Amp\wait($formBody->getBody()); return html_entity_decode($rawBody, \ENT_HTML5, 'UTF-8'); } - + /** * Create a request object * @@ -89,47 +89,23 @@ trait MALTrait { */ 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/Transformer/AnimeListTransformer.php b/src/API/MAL/Transformer/AnimeListTransformer.php index 38cf8182..9054f2c1 100644 --- a/src/API/MAL/Transformer/AnimeListTransformer.php +++ b/src/API/MAL/Transformer/AnimeListTransformer.php @@ -56,27 +56,33 @@ class AnimeListTransformer extends AbstractTransformer { * @return array */ 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'])) + switch(TRUE) { - $map['data']['score'] = $item['data']['rating'] * 2; - } - - if (array_key_exists('status', $item['data'])) - { - $map['data']['status'] = self::statusMap[$item['data']['status']]; + case array_key_exists('notes', $item['data']): + $map['data']['comments'] = $item['data']['notes']; + + case array_key_exists('rating', $item['data']): + $map['data']['score'] = $item['data']['rating'] * 2; + + case array_key_exists('reconsuming', $item['data']): + $map['data']['enable_rewatching'] = (bool) $item['data']['reconsuming']; + + case array_key_exists('reconsumeCount', $item['data']): + $map['data']['times_rewatched'] = $item['data']['reconsumeCount']; + + case array_key_exists('status', $item['data']): + $map['data']['status'] = self::statusMap[$item['data']['status']]; + + default: + break; } return $map; diff --git a/tests/API/APIRequestBuilderTest.php b/tests/API/APIRequestBuilderTest.php new file mode 100644 index 00000000..4b3b10aa --- /dev/null +++ b/tests/API/APIRequestBuilderTest.php @@ -0,0 +1,136 @@ + + * @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') + ->setBody(Json::encode($data)) + ->getFullRequest(); + + $response = Amp\wait((new Client)->request($request)); + $body = Json::decode($response->getBody()); + + $this->assertEquals($data, $body['json']); + } +} + From 906a1f1efae10db869fddb341fb983910a26a7fc Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Wed, 8 Feb 2017 15:48:20 -0500 Subject: [PATCH 03/13] Another ugly progress commit - Eradicated Guzzle from main codebase - All API requests now use Artax - Refactor code to use function and constant imports - And more! --- app/bootstrap.php | 34 ++-- app/config/routes.php | 17 +- composer.json | 6 +- index.php | 15 +- src/API/APIRequestBuilder.php | 45 ++++-- ...Client.php => FailedResponseException.php} | 20 +-- src/API/GuzzleTrait.php | 41 ----- src/API/Kitsu.php | 30 ++-- src/API/Kitsu/Auth.php | 13 +- src/API/Kitsu/KitsuRequestBuilder.php | 18 +-- src/API/Kitsu/KitsuTrait.php | 139 ++++++----------- src/API/Kitsu/ListItem.php | 66 +++++--- src/API/Kitsu/Model.php | 21 ++- src/API/ListItemInterface.php | 8 +- src/API/MAL/ListItem.php | 47 ++++-- src/API/MAL/MALTrait.php | 2 +- src/API/MAL/Model.php | 7 +- src/AnimeClient.php | 63 ++++---- src/Command/BaseCommand.php | 38 ++--- src/Command/SyncKitsuWithMal.php | 147 ++++++++++++------ src/Controller.php | 4 +- src/Dispatcher.php | 42 ++--- src/Model/API.php | 8 +- src/Model/Anime.php | 37 ++++- tests/API/APIRequestBuilderTest.php | 47 +++--- tests/AnimeClient_TestCase.php | 28 +--- 26 files changed, 468 insertions(+), 475 deletions(-) rename src/API/{APIClient.php => FailedResponseException.php} (61%) delete mode 100644 src/API/GuzzleTrait.php 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/config/routes.php b/app/config/routes.php index df9329ad..4ead95bf 100644 --- a/app/config/routes.php +++ b/app/config/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/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/index.php b/index.php index fff3616a..0d8a5615 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; @@ -42,7 +44,7 @@ $APP_DIR = _dir(__DIR__, 'app'); $CONF_DIR = _dir($APP_DIR, 'config'); // Load composer autoloader -require _dir(__DIR__, '/vendor/autoload.php'); +require _dir(__DIR__, 'vendor/autoload.php'); // ------------------------------------------------------------------------- // Setup error handling @@ -54,10 +56,7 @@ $defaultHandler = new PrettyPageHandler(); $whoops->pushHandler($defaultHandler); // Register as the error handler -if (array_key_exists('whoops', $_GET)) -{ - $whoops->register(); -} +$whoops->register(); // ----------------------------------------------------------------------------- // Dependency Injection setup @@ -65,7 +64,7 @@ if (array_key_exists('whoops', $_GET)) require _dir($CONF_DIR, 'base_config.php'); // $base_config $di = require _dir($APP_DIR, 'bootstrap.php'); -$config = AnimeClient::loadToml($CONF_DIR); +$config = loadToml($CONF_DIR); $config_array = array_merge($base_config, $config); $container = $di($config_array); @@ -77,6 +76,4 @@ unset($CONF_DIR); // ----------------------------------------------------------------------------- // Dispatch to the current route // ----------------------------------------------------------------------------- -$container->get('dispatcher')->__invoke(); - -// End of index.php \ No newline at end of file +$container->get('dispatcher')->__invoke(); \ No newline at end of file diff --git a/src/API/APIRequestBuilder.php b/src/API/APIRequestBuilder.php index 0ea1f77a..d3d2a037 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, Request }; use Aviat\Ion\Di\ContainerAware; +use Aviat\Ion\Json; use InvalidArgumentException; use Psr\Log\LoggerAwareTrait; @@ -67,6 +69,21 @@ class APIRequestBuilder { */ protected $request; + /** + * Set an authorization header + * + * @param string $type The type of authorization, eg, basic, bearer, etc. + * @param string $value The authorization value + * @return self + */ + public function setAuth(string $type, string $value): self + { + $authString = ucfirst($type) . ' ' . $value; + $this->setHeader('Authorization', $authString); + + return $this; + } + /** * Set a basic authentication header * @@ -76,9 +93,7 @@ class APIRequestBuilder { */ public function setBasicAuth(string $username, string $password): self { - $authString = 'Basic ' . base64_encode($username . ':' . $password); - $this->setHeader('Authorization', $authString); - + $this->setAuth('basic', base64_encode($username . ':' . $password)); return $this; } @@ -139,6 +154,18 @@ class APIRequestBuilder { return $this; } + /** + * Set the request body + * + * @param array|FormBody|string $body + * @return self + */ + public function setJsonBody(array $body): self + { + $requestBody = Json::encode($body); + return $this->setBody($requestBody); + } + /** * Append a query string in array format * @@ -159,7 +186,7 @@ class APIRequestBuilder { public function getFullRequest() { $this->buildUri(); - + if ($this->logger) { $this->logger->debug('API Request', [ @@ -168,7 +195,7 @@ class APIRequestBuilder { 'request_body' => $this->request->getBody() ]); } - + return $this->request; } @@ -191,13 +218,13 @@ class APIRequestBuilder { $this->request ->setMethod($type) ->setProtocol('1.1'); - + $this->path = $uri; - + if ( ! empty($this->defaultHeaders)) { $this->setHeaders($this->defaultHeaders); - } + } return $this; } @@ -232,7 +259,7 @@ class APIRequestBuilder { */ private function fixBody(FormBody $formBody): string { - $rawBody = \Amp\wait($formBody->getBody()); + $rawBody = Amp\wait($formBody->getBody()); return html_entity_decode($rawBody, \ENT_HTML5, 'UTF-8'); } diff --git a/src/API/APIClient.php b/src/API/FailedResponseException.php similarity index 61% rename from src/API/APIClient.php rename to src/API/FailedResponseException.php index 05d3b8a9..648bb83d 100644 --- a/src/API/APIClient.php +++ b/src/API/FailedResponseException.php @@ -16,24 +16,8 @@ namespace Aviat\AnimeClient\API; -use Amp; -use Amp\Artax\{ - Client, - Response, - Request -} +use UnexpectedValueException; -class APIClient { +class FailedResponseException extends UnexpectedValueException { - /** - * Get a syncronous response for a request - * - * @param Request $request - * @return Response - */ - static public function syncResponse(Request $request): Response - { - $client = new Client(); - return wait($client->request($request)); - } } \ 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..7e01e592 100644 --- a/src/API/Kitsu/Auth.php +++ b/src/API/Kitsu/Auth.php @@ -16,6 +16,9 @@ namespace Aviat\AnimeClient\API\Kitsu; +use const Aviat\AnimeClient\SESSION_SEGMENT; +use const Aviat\AnimeClient\API\Kitsu\AUTH_TOKEN_CACHE_KEY; + use Aviat\AnimeClient\AnimeClient; use Aviat\AnimeClient\API\{ CacheTrait, @@ -55,7 +58,7 @@ class Auth { $this->setContainer($container); $this->setCache($container->get('cache')); $this->segment = $container->get('session') - ->getSegment(AnimeClient::SESSION_SEGMENT); + ->getSegment(SESSION_SEGMENT); $this->model = $container->get('kitsu-model'); } @@ -70,7 +73,7 @@ class Auth { { $config = $this->container->get('config'); $username = $config->get(['kitsu_username']); - + try { $auth = $this->model->authenticate($username, $password); @@ -79,15 +82,15 @@ class Auth { { return FALSE; } - + if (FALSE !== $auth) { // Set the token in the cache for command line operations - $cacheItem = $this->cache->getItem(K::AUTH_TOKEN_CACHE_KEY); + $cacheItem = $this->cache->getItem(AUTH_TOKEN_CACHE_KEY); $cacheItem->set($auth['access_token']); $cacheItem->save(); - + $this->segment->set('auth_token', $auth['access_token']); return TRUE; } 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 612ee747..75b9e554 100644 --- a/src/API/Kitsu/KitsuTrait.php +++ b/src/API/Kitsu/KitsuTrait.php @@ -16,13 +16,14 @@ namespace Aviat\AnimeClient\API\Kitsu; +use const Aviat\AnimeClient\SESSION_SEGMENT; + +use function Amp\wait; + +use Amp\Artax\Client; use Aviat\AnimeClient\AnimeClient; -use Aviat\AnimeClient\API\GuzzleTrait; use Aviat\AnimeClient\API\Kitsu as K; use Aviat\Ion\Json; -use GuzzleHttp\Client; -use GuzzleHttp\Cookie\CookieJar; -use GuzzleHttp\Psr7\Response; use InvalidArgumentException; use RuntimeException; @@ -34,37 +35,6 @@ trait KitsuTrait { */ protected $requestBuilder; - /** - * The Guzzle http client object - * @var object - */ - protected $client; - - /** - * Cookie jar object for api requests - * @var object - */ - protected $cookieJar; - - /** - * The base url for api requests - * @var string $base_url - */ - protected $baseUrl = "https://kitsu.io/api/edge/"; - - /** - * HTTP headers to send with every request - * - * @var array - */ - protected $defaultHeaders = [ - 'User-Agent' => "Tim's Anime Client/4.0", - 'Accept-Encoding' => 'application/vnd.api+json', - 'Content-Type' => 'application/vnd.api+json', - 'client_id' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd', - 'client_secret' => '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151', - ]; - /** * Set the request builder object * @@ -78,30 +48,45 @@ trait KitsuTrait { } /** - * Set up the class properties + * Create a request object * - * @return void + * @param string $type + * @param string $url + * @param array $options + * @return \Amp\Artax\Response */ - protected function init() + public function setUpRequest(string $type, string $url, array $options = []) { - $defaults = [ - 'cookies' => $this->cookieJar, - 'headers' => $this->defaultHeaders, - 'timeout' => 25, - 'connect_timeout' => 25 - ]; + $config = $this->container->get('config'); - $this->cookieJar = new CookieJar(); - $this->client = new Client([ - 'base_uri' => $this->baseUrl, - 'cookies' => TRUE, - 'http_errors' => TRUE, - 'defaults' => $defaults - ]); + $request = $this->requestBuilder->newRequest($type, $url); + + $sessionSegment = $this->getContainer() + ->get('session') + ->getSegment(SESSION_SEGMENT); + + if ($sessionSegment->get('auth_token') !== null && $url !== K::AUTH_URL) + { + $token = $sessionSegment->get('auth_token'); + $request = $request->setAuth('bearer', $token); + // $defaultOptions['headers']['Authorization'] = "bearer {$token}"; + } + + if (array_key_exists('query', $options)) + { + $request->setQuery($options['query']); + } + + if (array_key_exists('body', $options)) + { + $request->setJsonBody($options['body']); + } + + return $request->getFullRequest(); } /** - * Make a request via Guzzle + * Make a request * * @param string $type * @param string $url @@ -110,48 +95,24 @@ trait KitsuTrait { */ private function getResponse(string $type, string $url, array $options = []) { - $logger = null; - $validTypes = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']; - - if ( ! in_array($type, $validTypes)) - { - throw new InvalidArgumentException('Invalid http request type'); - } - - $defaultOptions = [ - 'headers' => $this->defaultHeaders - ]; - + $request = $this->setUpRequest($type, $url, $options); $logger = $this->container->getLogger('kitsu-request'); - $sessionSegment = $this->getContainer() - ->get('session') - ->getSegment(AnimeClient::SESSION_SEGMENT); - if ($sessionSegment->get('auth_token') !== null && $url !== K::AUTH_URL) - { - $token = $sessionSegment->get('auth_token'); - $defaultOptions['headers']['Authorization'] = "bearer {$token}"; - } + $response = wait((new Client)->request($request)); - $options = array_merge($defaultOptions, $options); - - $response = $this->client->request($type, $url, $options); - - $logger->debug('Kitsu API request', [ - 'requestParams' => [ - 'type' => $type, - 'url' => $url, - ], - 'responseValues' => [ - 'status' => $response->getStatusCode() - ] - ]); + /* $logger->debug('Kitsu api response', [ + 'status' => $response->getStatus(), + 'reason' => $response->getReason(), + 'body' => $response->getBody(), + 'headers' => $response->getAllHeaders(), + 'requestHeaders' => $request->getAllHeaders(), + ]); */ return $response; } /** - * Make a request via Guzzle + * Make a request * * @param string $type * @param string $url @@ -168,7 +129,7 @@ trait KitsuTrait { $response = $this->getResponse($type, $url, $options); - if ((int) $response->getStatusCode() > 299 || (int) $response->getStatusCode() < 200) + if ((int) $response->getStatus() > 299 || (int) $response->getStatus() < 200) { if ($logger) { @@ -218,7 +179,7 @@ trait KitsuTrait { $response = $this->getResponse('POST', ...$args); $validResponseCodes = [200, 201]; - if ( ! in_array((int) $response->getStatusCode(), $validResponseCodes)) + if ( ! in_array((int) $response->getStatus(), $validResponseCodes)) { if ($logger) { @@ -238,6 +199,6 @@ trait KitsuTrait { protected function deleteRequest(...$args): bool { $response = $this->getResponse('DELETE', ...$args); - return ((int) $response->getStatusCode() === 204); + return ((int) $response->getStatus() === 204); } } \ No newline at end of file diff --git a/src/API/Kitsu/ListItem.php b/src/API/Kitsu/ListItem.php index 51121471..c57d6d3e 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,12 +31,22 @@ class ListItem extends AbstractListItem { use ContainerAware; use KitsuTrait; - public function __construct() + private function getAuthHeader() { - $this->init(); + $sessionSegment = $this->getContainer() + ->get('session') + ->getSegment(SESSION_SEGMENT); + + if ($sessionSegment->get('auth_token') !== null) + { + $token = $sessionSegment->get('auth_token'); + return "bearer {$token}"; + } + + return FALSE; } - public function create(array $data): bool + public function create(array $data): Request { $body = [ 'data' => [ @@ -60,21 +71,35 @@ class ListItem extends AbstractListItem { ] ] ]; - - $request = $this->requestBuilder->newRequest('POST', 'library-entries') - ->setJsonBody($body) - ->getFullRequest(); - $response = $this->getResponse('POST', 'library-entries', [ - 'body' => Json::encode($body) - ]); - return ($response->getStatusCode() === 201); + $authHeader = $this->getAuthHeader(); + + $request = $this->requestBuilder->newRequest('POST', 'library-entries'); + + if ($authHeader !== FALSE) + { + $request = $request->setHeader('Authorization', $authHeader); + } + + return $request->setJsonBody($body) + ->getFullRequest(); + + // return ($response->getStatus() === 201); } - public function delete(string $id): bool + public function delete(string $id): Request { - $response = $this->getResponse('DELETE', "library-entries/{$id}"); - return ($response->getStatusCode() === 204); + $authHeader = $this->getAuthHeader(); + $request = $this->requestBuilder->newRequest('DELETE', "library-entries/{$id}"); + + if ($authHeader !== FALSE) + { + $request = $request->setHeader('Authorization', $authHeader); + } + + return $request->getFullRequest(); + + // return ($response->getStatus() === 204); } public function get(string $id): array @@ -84,17 +109,12 @@ class ListItem extends AbstractListItem { 'include' => 'media,media.genres,media.mappings' ]) ->getFullRequest(); - /*return $this->getRequest("library-entries/{$id}", [ - 'query' => [ - 'include' => 'media,media.genres,media.mappings' - ] - ]);*/ - + $response = \Amp\wait((new \Amp\Artax\Client)->request($request)); return Json::decode($response->getBody()); } - public function update(string $id, array $data): Response + public function update(string $id, array $data): Request { $requestData = [ 'data' => [ diff --git a/src/API/Kitsu/Model.php b/src/API/Kitsu/Model.php index 7077f809..1a401b1c 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; @@ -355,9 +352,9 @@ class Model { * Create a list item * * @param array $data - * @return bool + * @return Request */ - public function createListItem(array $data): bool + public function createListItem(array $data): Request { $data['user_id'] = $this->getUserIdByUsername($this->getUsername()); return $this->listItem->create($data); @@ -397,22 +394,22 @@ class Model { * Modify a list item * * @param array $data - * @return array + * @return Request */ - public function updateListItem(array $data) + public function updateListItem(array $data): Request { try { $response = $this->listItem->update($data['id'], $data['data']); return [ - 'statusCode' => $response->getStatusCode(), + 'statusCode' => $response->getStatus(), 'body' => $response->getBody(), ]; } catch(ClientException $e) { return [ - 'statusCode' => $e->getResponse()->getStatusCode(), + 'statusCode' => $e->getResponse()->getStatus(), 'body' => Json::decode((string)$e->getResponse()->getBody()) ]; } @@ -422,9 +419,9 @@ class Model { * Remove a list item * * @param string $id - The id of the list item to remove - * @return bool + * @return Request */ - public function deleteListItem(string $id): bool + public function deleteListItem(string $id): Request { return $this->listItem->delete($id); } 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/ListItem.php b/src/API/MAL/ListItem.php index 8c87b166..d1d16dfd 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\Request; use Aviat\AnimeClient\API\{ AbstractListItem, XML @@ -30,7 +30,7 @@ class ListItem { use ContainerAware; use MALTrait; - public function create(array $data) + public function create(array $data): Request { $id = $data['id']; $createData = [ @@ -40,29 +40,36 @@ class ListItem { ]) ]; - // $config = $this->container->get('config'); + $config = $this->container->get('config'); - /*$request = $this->requestBuilder->newRequest('POST', "animelist/add/{$id}.xml") + return $this->requestBuilder->newRequest('POST', "animelist/add/{$id}.xml") ->setFormFields($createData) ->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password'])) - ->getFullRequest();*/ + ->getFullRequest(); - $response = $this->getResponse('POST', "animelist/add/{$id}.xml", [ + /* $response = $this->getResponse('POST', "animelist/add/{$id}.xml", [ 'body' => $this->fixBody((new FormBody)->addFields($createData)) ]); - return $response->getBody() === 'Created'; - - // return $request; + return $response->getBody() === 'Created'; */ } - public function delete(string $id): bool + public function delete(string $id): Request { - $response = $this->getResponse('DELETE', "animelist/delete/{$id}.xml", [ + $config = $this->container->get('config'); + + return $this->requestBuilder->newRequest('DELETE', "animelist/delete/{$id}.xml") + ->setFormFields([ + 'id' => $id + ]) + ->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password'])) + ->getFullRequest(); + + /*$response = $this->getResponse('DELETE', "animelist/delete/{$id}.xml", [ 'body' => $this->fixBody((new FormBody)->addField('id', $id)) ]); - return $response->getBody() === 'Deleted'; + return $response->getBody() === 'Deleted';*/ } public function get(string $id): array @@ -70,15 +77,25 @@ class ListItem { return []; } - public function update(string $id, array $data) + public function update(string $id, array $data): Request { + $config = $this->container->get('config'); + $xml = XML::toXML(['entry' => $data]); $body = (new FormBody) ->addField('id', $id) ->addField('data', $xml); - return $this->getResponse('POST', "animelist/update/{$id}.xml", [ + return $this->requestBuilder->newRequest('POST', "animelist/update/{$id}.xml") + ->setFormFields([ + 'id' => $id, + 'data' => $xml + ]) + ->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password'])) + ->getFullRequest(); + + /* return $this->getResponse('POST', "animelist/update/{$id}.xml", [ 'body' => $this->fixBody($body) - ]); + ]); */ } } \ No newline at end of file diff --git a/src/API/MAL/MALTrait.php b/src/API/MAL/MALTrait.php index 8769f632..ba69da6e 100644 --- a/src/API/MAL/MALTrait.php +++ b/src/API/MAL/MALTrait.php @@ -85,7 +85,7 @@ trait MALTrait { * @param string $type * @param string $url * @param array $options - * @return \Amp\Promise + * @return \Amp\Artax\Response */ public function setUpRequest(string $type, string $url, array $options = []) { diff --git a/src/API/MAL/Model.php b/src/API/MAL/Model.php index a2d0633e..e8458121 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; @@ -43,7 +44,7 @@ class Model { $this->listItem = $listItem; } - public function createListItem(array $data): bool + public function createListItem(array $data): Request { $createData = [ 'id' => $data['id'], @@ -77,13 +78,13 @@ class Model { return []; } - public function updateListItem(array $data) + public function updateListItem(array $data): Request { $updateData = $this->animeListTransformer->untransform($data); return $this->listItem->update($updateData['id'], $updateData['data']); } - public function deleteListItem(string $id): bool + public function deleteListItem(string $id): Request { return $this->listItem->delete($id); } 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..63fc16c6 100644 --- a/src/Command/BaseCommand.php +++ b/src/Command/BaseCommand.php @@ -16,21 +16,18 @@ namespace Aviat\AnimeClient\Command; +use function Aviat\AnimeClient\loadToml; + use Aura\Session\SessionFactory; use Aviat\AnimeClient\{ AnimeClient, Model, Util }; -use Aviat\AnimeClient\API\CacheTrait; -use Aviat\AnimeClient\API\Kitsu\{ - Auth as KitsuAuth, - ListItem as KitsuListItem, - Model as KitsuModel -}; -use Aviat\AnimeClient\API\MAL\{ - ListItem as MALListItem, - Model as MALModel +use Aviat\AnimeClient\API\{ + CacheTrait, + Kitsu, + MAL }; use Aviat\Banker\Pool; use Aviat\Ion\Config; @@ -72,23 +69,26 @@ class BaseCommand extends Command { $CONF_DIR = realpath("{$APP_DIR}/config/"); require_once $CONF_DIR . '/base_config.php'; // $base_config - $config = AnimeClient::loadToml($CONF_DIR); + $config = loadToml($CONF_DIR); $config_array = array_merge($base_config, $config); $di = function ($config_array) use ($APP_DIR) { $container = new Container(); - + // ------------------------------------------------------------------------- // Logging // ------------------------------------------------------------------------- $app_logger = new Logger('animeclient'); $app_logger->pushHandler(new NullHandler); - $request_logger = new Logger('request'); - $request_logger->pushHandler(new NullHandler); + $kitsu_request_logger = new Logger('kitsu-request'); + $kitsu_request_logger->pushHandler(new NullHandler); + $mal_request_logger = new Logger('mal-request'); + $mal_request_logger->pushHandler(new NullHandler); $container->setLogger($app_logger, 'default'); - $container->setLogger($request_logger, 'request'); - + $container->setLogger($kitsu_request_logger, 'kitsu-request'); + $container->setLogger($mal_request_logger, 'mal-request'); + // Create Config Object $container->set('config', function() use ($config_array) { return new Config($config_array); @@ -108,18 +108,18 @@ class BaseCommand extends Command { // Models $container->set('kitsu-model', function($container) { - $listItem = new KitsuListItem(); + $listItem = new Kitsu\istItem(); $listItem->setContainer($container); - $model = new KitsuModel($listItem); + $model = new Kitsu\Model($listItem); $model->setContainer($container); $cache = $container->get('cache'); $model->setCache($cache); return $model; }); $container->set('mal-model', function($container) { - $listItem = new MALListItem(); + $listItem = new MAL\ListItem(); $listItem->setContainer($container); - $model = new MALModel($listItem); + $model = new MAL\Model($listItem); $model->setContainer($container); return $model; }); diff --git a/src/Command/SyncKitsuWithMal.php b/src/Command/SyncKitsuWithMal.php index 93a714c2..d7f13daf 100644 --- a/src/Command/SyncKitsuWithMal.php +++ b/src/Command/SyncKitsuWithMal.php @@ -23,49 +23,10 @@ use Aviat\AnimeClient\API\Kitsu; * Clears the API Cache */ class SyncKitsuWithMal extends BaseCommand { - + protected $kitsuModel; - - public function getKitsuAnimeListPageCount() - { - $cacheItem = $this->cache->getItem(Kitsu::AUTH_TOKEN_CACHE_KEY); - - $query = http_build_query([ - 'filter' => [ - 'user_id' => $this->kitsuModel->getUserIdByUsername(), - 'media_type' => 'Anime' - ], - 'include' => 'anime,anime.genres,anime.mappings,anime.streamingLinks', - 'page' => [ - 'limit' => 1 - ], - 'sort' => '-updated_at' - ]); - $request = (new Artax\Request) - ->setUri("https://kitsu.io/api/edge/library-entries?{$query}") - ->setProtocol('1.1') - ->setAllHeaders([ - 'Accept' => 'application/vnd.api+json', - 'Content-Type' => 'application/vnd.api+json', - 'User-Agent' => "Tim's Anime Client/4.0" - ]); - - if ($cacheItem->isHit()) - { - $token = $cacheItem->get(); - $request->setHeader('Authorization', "bearer {$token}"); - } - else - { - $this->echoBox("WARNING: NOT LOGGED IN\nSome data might be missing"); - } - - $response = \Amp\wait((new Artax\Client)->request($request)); - - $body = json_decode($response->getBody(), TRUE); - return $body['meta']['count']; - } - + protected $malModel; + /** * Run the image conversion script * @@ -79,8 +40,102 @@ class SyncKitsuWithMal extends BaseCommand { $this->setContainer($this->setupContainer()); $this->setCache($this->container->get('cache')); $this->kitsuModel = $this->container->get('kitsu-model'); - - $kitsuCount = $this->getKitsuAnimeListPageCount(); - $this->echoBox("List item count: {$kitsuCount}"); + $this->malModel = $this->container->get('mal-model'); + + //$kitsuCount = $this->getKitsuAnimeListPageCount(); + //$this->echoBox("List item count: {$kitsuCount}"); + $this->MALItemCreate(); + + //echo json_encode($this->getMALList(), \JSON_PRETTY_PRINT); } -} + + + public function getMALList() + { + return $this->malModel->getFullList(); + } + + public function getKitsuAnimeListPageCount() + { + $cacheItem = $this->cache->getItem(Kitsu::AUTH_TOKEN_CACHE_KEY); + + $query = http_build_query([ + 'filter' => [ + 'user_id' => $this->kitsuModel->getUserIdByUsername(), + 'media_type' => 'Anime' + ], + // 'include' => 'anime,anime.genres,anime.mappings,anime.streamingLinks', + 'page' => [ + 'limit' => 1 + ], + 'sort' => '-updated_at' + ]); + $request = (new Artax\Request) + ->setUri("https://kitsu.io/api/edge/library-entries?{$query}") + ->setProtocol('1.1') + ->setAllHeaders([ + 'Accept' => 'application/vnd.api+json', + 'Content-Type' => 'application/vnd.api+json', + 'User-Agent' => "Tim's Anime Client/4.0" + ]); + + if ($cacheItem->isHit()) + { + $token = $cacheItem->get(); + $request->setHeader('Authorization', "bearer {$token}"); + } + else + { + $this->echoBox("WARNING: NOT LOGGED IN\nSome data might be missing"); + } + + $response = \Amp\wait((new Artax\Client)->request($request)); + + $body = json_decode($response->getBody(), TRUE); + return $body['meta']['count']; + } + + public function MALItemCreate() + { + $input = json_decode('{ + "watching_status": "current", + "user_rating": "", + "episodes_watched": "4", + "rewatched": "0", + "notes": "", + "id": "15794526", + "mal_id": "33731", + "edit": "true" + }', TRUE); + + $response = $this->malModel->createListItem([ + 'id' => 12255, + 'status' => 'planned', + 'type' => 'anime' + ]); + + //$response = $this->malModel->updateListItem($input); + //print_r($response); + //echo $response->getBody(); + + } + + public function diffLists() + { + // Get libraryEntries with media.mappings from Kitsu + // Organize mappings, and ignore entries without mappings + + // Get MAL list data + + // Compare each list entry + // If a list item exists only on MAL, create it on Kitsu with the existing data from MAL + // If a list item exists only on Kitsu, create it on MAL with the existing data from Kitsu + // If an item already exists on both APIS: + // Compare last updated dates, and use the later one + // Otherwise, use rewatch count, then episode progress as critera for selecting the more up + // to date entry + // Based on the 'newer' entry, update the other api list item + } + + +} \ 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..328c8819 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; } /** @@ -168,12 +185,18 @@ class Anime extends API { */ public function deleteLibraryItem(string $id, string $malId = null): bool { + $requests = []; + if ($this->useMALAPI && ! is_null($malId)) { - $this->malModel->deleteListItem($malId); + $requests['mal'] = $this->malModel->deleteListItem($malId); } - return $this->kitsuModel->deleteListItem($id); + $requests['kitsu'] = $this->kitsuModel->deleteListItem($id); + + $results = wait(some((new Client)->requestMulti($requests))); + + return count($results[1]) > 0; } } // End of AnimeModel.php \ No newline at end of file diff --git a/tests/API/APIRequestBuilderTest.php b/tests/API/APIRequestBuilderTest.php index 4b3b10aa..f2050e86 100644 --- a/tests/API/APIRequestBuilderTest.php +++ b/tests/API/APIRequestBuilderTest.php @@ -29,13 +29,13 @@ class APIRequestBuilderTest extends TestCase { { $this->builder = new class extends APIRequestBuilder { protected $baseUrl = 'https://httpbin.org/'; - + protected $defaultHeaders = ['User-Agent' => "Tim's Anime Client Testsuite / 4.0"]; }; - + $this->builder->setLogger(new NullLogger); } - + public function testGzipRequest() { $request = $this->builder->newRequest('GET', 'gzip') @@ -44,26 +44,26 @@ class APIRequestBuilderTest extends TestCase { $body = Json::decode($response->getBody()); $this->assertEquals(1, $body['gzipped']); } - + public function testInvalidRequestMethod() { $this->expectException(\InvalidArgumentException::class); $this->builder->newRequest('FOO', 'gzip') ->getFullRequest(); } - + public function testRequestWithBasicAuth() { $request = $this->builder->newRequest('GET', 'headers') ->setBasicAuth('username', 'password') ->getFullRequest(); - + $response = Amp\wait((new Client)->request($request)); $body = Json::decode($response->getBody()); - + $this->assertEquals('Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $body['headers']['Authorization']); } - + public function testRequestWithQueryString() { $query = [ @@ -75,40 +75,40 @@ class APIRequestBuilderTest extends TestCase { 'bar' => 'foo' ] ]; - + $expected = [ 'foo' => 'bar', 'bar[foo]' => 'bar', 'baz[bar]' => 'foo' ]; - + $request = $this->builder->newRequest('GET', 'get') ->setQuery($query) ->getFullRequest(); - + $response = Amp\wait((new Client)->request($request)); $body = Json::decode($response->getBody()); - - $this->assertEquals($expected, $body['args']); + + $this->assertEquals($expected, $body['args']); } - + public function testFormValueRequest() { $formValues = [ 'foo' => 'bar', 'bar' => 'foo' ]; - + $request = $this->builder->newRequest('POST', 'post') ->setFormFields($formValues) ->getFullRequest(); - + $response = Amp\wait((new Client)->request($request)); $body = Json::decode($response->getBody()); - + $this->assertEquals($formValues, $body['form']); } - + public function testFullUrlRequest() { $data = [ @@ -121,16 +121,15 @@ class APIRequestBuilderTest extends TestCase { ] ] ]; - + $request = $this->builder->newRequest('PUT', 'https://httpbin.org/put') ->setHeader('Content-Type', 'application/json') - ->setBody(Json::encode($data)) + ->setJsonBody($data) ->getFullRequest(); - + $response = Amp\wait((new Client)->request($request)); $body = Json::decode($response->getBody()); - + $this->assertEquals($data, $body['json']); } -} - +} \ No newline at end of file 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 From 18af49f1f43893f3a846c98474c0a77f6eef6cf9 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Thu, 9 Feb 2017 13:44:56 -0500 Subject: [PATCH 04/13] Replace Guzzle with Artax --- public/js.php | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/public/js.php b/public/js.php index 22bc1718..83233975 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']; } /** From 50c543218b620aa6de6a07b733adc45173e9a95e Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Thu, 9 Feb 2017 13:45:40 -0500 Subject: [PATCH 05/13] Remove 'fix' for issue caused by php.ini setting --- src/API/APIRequestBuilder.php | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/API/APIRequestBuilder.php b/src/API/APIRequestBuilder.php index d3d2a037..0623fef7 100644 --- a/src/API/APIRequestBuilder.php +++ b/src/API/APIRequestBuilder.php @@ -118,7 +118,7 @@ class APIRequestBuilder { public function setFormFields(array $fields): self { $this->setHeader("Content-Type", "application/x-www-form-urlencoded"); - $body = $this->fixBody((new FormBody)->addFields($fields)); + $body = (new FormBody)->addFields($fields); $this->setBody($body); return $this; } @@ -248,21 +248,6 @@ class APIRequestBuilder { $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 * From 8f8c41392711ac0f859cbbdd90608d9bdea957cc Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Thu, 9 Feb 2017 20:10:13 -0500 Subject: [PATCH 06/13] Fix update requests broken by Artax conversion --- src/API/APIRequestBuilder.php | 9 ++++++--- src/API/Kitsu/Auth.php | 3 +-- src/API/Kitsu/KitsuTrait.php | 5 +++++ src/API/Kitsu/ListItem.php | 29 +++++++++++++++++++++-------- src/API/Kitsu/Model.php | 16 +--------------- src/API/MAL/ListItem.php | 2 +- src/Model/Anime.php | 15 +++++++++++++-- 7 files changed, 48 insertions(+), 31 deletions(-) diff --git a/src/API/APIRequestBuilder.php b/src/API/APIRequestBuilder.php index 0623fef7..4a96f63b 100644 --- a/src/API/APIRequestBuilder.php +++ b/src/API/APIRequestBuilder.php @@ -157,12 +157,15 @@ class APIRequestBuilder { /** * Set the request body * - * @param array|FormBody|string $body + * @param mixed $body * @return self */ - public function setJsonBody(array $body): self + public function setJsonBody($body): self { - $requestBody = Json::encode($body); + $requestBody = ( ! is_scalar($body)) + ? Json::encode($body) + : $body; + return $this->setBody($requestBody); } diff --git a/src/API/Kitsu/Auth.php b/src/API/Kitsu/Auth.php index 7e01e592..ecee237b 100644 --- a/src/API/Kitsu/Auth.php +++ b/src/API/Kitsu/Auth.php @@ -17,7 +17,6 @@ namespace Aviat\AnimeClient\API\Kitsu; use const Aviat\AnimeClient\SESSION_SEGMENT; -use const Aviat\AnimeClient\API\Kitsu\AUTH_TOKEN_CACHE_KEY; use Aviat\AnimeClient\AnimeClient; use Aviat\AnimeClient\API\{ @@ -87,7 +86,7 @@ class Auth { if (FALSE !== $auth) { // Set the token in the cache for command line operations - $cacheItem = $this->cache->getItem(AUTH_TOKEN_CACHE_KEY); + $cacheItem = $this->cache->getItem(K::AUTH_TOKEN_CACHE_KEY); $cacheItem->set($auth['access_token']); $cacheItem->save(); diff --git a/src/API/Kitsu/KitsuTrait.php b/src/API/Kitsu/KitsuTrait.php index 75b9e554..171d0817 100644 --- a/src/API/Kitsu/KitsuTrait.php +++ b/src/API/Kitsu/KitsuTrait.php @@ -71,6 +71,11 @@ trait KitsuTrait { $request = $request->setAuth('bearer', $token); // $defaultOptions['headers']['Authorization'] = "bearer {$token}"; } + + if (array_key_exists('form_params', $options)) + { + $request->setFormFields($options['form_params']); + } if (array_key_exists('query', $options)) { diff --git a/src/API/Kitsu/ListItem.php b/src/API/Kitsu/ListItem.php index c57d6d3e..509fab80 100644 --- a/src/API/Kitsu/ListItem.php +++ b/src/API/Kitsu/ListItem.php @@ -104,11 +104,19 @@ class ListItem extends AbstractListItem { public function get(string $id): array { + $authHeader = $this->getAuthHeader(); + $request = $this->requestBuilder->newRequest('GET', "library-entries/{$id}") ->setQuery([ 'include' => 'media,media.genres,media.mappings' - ]) - ->getFullRequest(); + ]); + + 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()); @@ -116,6 +124,7 @@ class ListItem extends AbstractListItem { public function update(string $id, array $data): Request { + $authHeader = $this->getAuthHeader(); $requestData = [ 'data' => [ 'id' => $id, @@ -123,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 1a401b1c..cd87a4cf 100644 --- a/src/API/Kitsu/Model.php +++ b/src/API/Kitsu/Model.php @@ -398,21 +398,7 @@ class Model { */ public function updateListItem(array $data): Request { - try - { - $response = $this->listItem->update($data['id'], $data['data']); - return [ - 'statusCode' => $response->getStatus(), - 'body' => $response->getBody(), - ]; - } - catch(ClientException $e) - { - return [ - 'statusCode' => $e->getResponse()->getStatus(), - 'body' => Json::decode((string)$e->getResponse()->getBody()) - ]; - } + return $this->listItem->update($data['id'], $data['data']); } /** diff --git a/src/API/MAL/ListItem.php b/src/API/MAL/ListItem.php index d1d16dfd..ca2d9086 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\Request; +use Amp\Artax\{FormBody, Request}; use Aviat\AnimeClient\API\{ AbstractListItem, XML diff --git a/src/Model/Anime.php b/src/Model/Anime.php index 328c8819..4064bbb3 100644 --- a/src/Model/Anime.php +++ b/src/Model/Anime.php @@ -168,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() + ]; } /** From 1ec7322b18ca46072de06030191d33b53c5acd70 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 10 Feb 2017 15:50:07 -0500 Subject: [PATCH 07/13] Split user config from application config --- app/{config => appConf}/base_config.php | 0 app/{config => appConf}/menus.php | 0 app/{config => appConf}/minify_config.php | 0 app/{config => appConf}/minify_css_groups.php | 0 app/{config => appConf}/minify_js_groups.php | 0 app/{config => appConf}/routes.php | 0 index.php | 3 ++- public/css.php | 2 +- public/js.php | 2 +- 9 files changed, 4 insertions(+), 3 deletions(-) rename app/{config => appConf}/base_config.php (100%) rename app/{config => appConf}/menus.php (100%) rename app/{config => appConf}/minify_config.php (100%) rename app/{config => appConf}/minify_css_groups.php (100%) rename app/{config => appConf}/minify_js_groups.php (100%) rename app/{config => appConf}/routes.php (100%) 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 100% rename from app/config/routes.php rename to app/appConf/routes.php diff --git a/index.php b/index.php index 0d8a5615..14eda8dc 100644 --- a/index.php +++ b/index.php @@ -41,6 +41,7 @@ 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 @@ -61,7 +62,7 @@ $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 = loadToml($CONF_DIR); 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 83233975..e8f50232 100644 --- a/public/js.php +++ b/public/js.php @@ -233,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"; From 8f8f528823d05d4f9177145cf816623a6ce37e7f Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 10 Feb 2017 16:12:02 -0500 Subject: [PATCH 08/13] Fix config mapping for BaseCommand --- src/Command/BaseCommand.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Command/BaseCommand.php b/src/Command/BaseCommand.php index 63fc16c6..f3dccc1d 100644 --- a/src/Command/BaseCommand.php +++ b/src/Command/BaseCommand.php @@ -66,8 +66,9 @@ 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 = loadToml($CONF_DIR); $config_array = array_merge($base_config, $config); @@ -108,7 +109,7 @@ class BaseCommand extends Command { // Models $container->set('kitsu-model', function($container) { - $listItem = new Kitsu\istItem(); + $listItem = new Kitsu\ListItem(); $listItem->setContainer($container); $model = new Kitsu\Model($listItem); $model->setContainer($container); From 7f5966a14777ef80f238c3d4db060a4afe1d6e54 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 10 Feb 2017 16:33:42 -0500 Subject: [PATCH 09/13] Attempt to fix hhvm tests --- .gitlab-ci.yml | 2 +- .travis.yml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) 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 From 4a3be8b4bfb3cee2f6ace1b18a52ceefc68e6446 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Mon, 13 Feb 2017 12:42:05 -0500 Subject: [PATCH 10/13] Fix mapping from Kitsu to MAL for updating a list item --- .../MAL/Transformer/AnimeListTransformer.php | 52 +++++++++++-------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/src/API/MAL/Transformer/AnimeListTransformer.php b/src/API/MAL/Transformer/AnimeListTransformer.php index 9054f2c1..56e9c508 100644 --- a/src/API/MAL/Transformer/AnimeListTransformer.php +++ b/src/API/MAL/Transformer/AnimeListTransformer.php @@ -56,7 +56,7 @@ class AnimeListTransformer extends AbstractTransformer { * @return array */ public function untransform(array $item): array - { + { $map = [ 'id' => $item['mal_id'], 'data' => [ @@ -64,27 +64,37 @@ class AnimeListTransformer extends AbstractTransformer { ] ]; - switch(TRUE) - { - case array_key_exists('notes', $item['data']): - $map['data']['comments'] = $item['data']['notes']; - - case array_key_exists('rating', $item['data']): - $map['data']['score'] = $item['data']['rating'] * 2; - - case array_key_exists('reconsuming', $item['data']): - $map['data']['enable_rewatching'] = (bool) $item['data']['reconsuming']; - - case array_key_exists('reconsumeCount', $item['data']): - $map['data']['times_rewatched'] = $item['data']['reconsumeCount']; - - case array_key_exists('status', $item['data']): - $map['data']['status'] = self::statusMap[$item['data']['status']]; - - default: - break; - } + $data =& $item['data']; + foreach($item['data'] as $key => $value) + { + 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 From dba0d47789cf3fbd9136f6afd6486560df94c0e1 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Mon, 13 Feb 2017 13:33:01 -0500 Subject: [PATCH 11/13] Uncomment rewatching stuff --- src/API/MAL/Transformer/AnimeListTransformer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/API/MAL/Transformer/AnimeListTransformer.php b/src/API/MAL/Transformer/AnimeListTransformer.php index 56e9c508..dc48adda 100644 --- a/src/API/MAL/Transformer/AnimeListTransformer.php +++ b/src/API/MAL/Transformer/AnimeListTransformer.php @@ -78,13 +78,13 @@ class AnimeListTransformer extends AbstractTransformer { $map['data']['score'] = $value * 2; break; - /* case 'reconsuming': + case 'reconsuming': $map['data']['enable_rewatching'] = (bool) $value; break; case 'reconsumeCount': $map['data']['times_rewatched'] = $value; - break; */ + break; case 'status': $map['data']['status'] = self::statusMap[$value]; From 652eac5be00866a390553a68318b4ad896957321 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Tue, 14 Feb 2017 15:29:13 -0500 Subject: [PATCH 12/13] Get sync-lists command to create missing entries on MAL --- src/API/Kitsu/KitsuTrait.php | 9 +- src/API/Kitsu/Model.php | 47 +++- src/API/MAL.php | 8 + src/API/MAL/ListItem.php | 16 +- src/API/MAL/Model.php | 9 +- .../MAL/Transformer/AnimeListTransformer.php | 6 + .../MAL/Transformer/MALToKitsuTransformer.php | 33 --- src/Command/BaseCommand.php | 23 +- src/Command/SyncKitsuWithMal.php | 255 +++++++++++++----- 9 files changed, 275 insertions(+), 131 deletions(-) delete mode 100644 src/API/MAL/Transformer/MALToKitsuTransformer.php diff --git a/src/API/Kitsu/KitsuTrait.php b/src/API/Kitsu/KitsuTrait.php index 171d0817..84bcf42c 100644 --- a/src/API/Kitsu/KitsuTrait.php +++ b/src/API/Kitsu/KitsuTrait.php @@ -20,7 +20,7 @@ use const Aviat\AnimeClient\SESSION_SEGMENT; use function Amp\wait; -use Amp\Artax\Client; +use Amp\Artax\{Client, Request}; use Aviat\AnimeClient\AnimeClient; use Aviat\AnimeClient\API\Kitsu as K; use Aviat\Ion\Json; @@ -53,9 +53,9 @@ trait KitsuTrait { * @param string $type * @param string $url * @param array $options - * @return \Amp\Artax\Response + * @return \Amp\Artax\Request */ - public function setUpRequest(string $type, string $url, array $options = []) + public function setUpRequest(string $type, string $url, array $options = []): Request { $config = $this->container->get('config'); @@ -69,7 +69,6 @@ trait KitsuTrait { { $token = $sessionSegment->get('auth_token'); $request = $request->setAuth('bearer', $token); - // $defaultOptions['headers']['Authorization'] = "bearer {$token}"; } if (array_key_exists('form_params', $options)) @@ -138,7 +137,7 @@ trait KitsuTrait { { if ($logger) { - $logger->warning('Non 200 response for api call', $response->getBody()); + $logger->warning('Non 200 response for api call', (array)$response->getBody()); } } diff --git a/src/API/Kitsu/Model.php b/src/API/Kitsu/Model.php index cd87a4cf..96fd54c5 100644 --- a/src/API/Kitsu/Model.php +++ b/src/API/Kitsu/Model.php @@ -203,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); } /** 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 ca2d9086..686c3455 100644 --- a/src/API/MAL/ListItem.php +++ b/src/API/MAL/ListItem.php @@ -46,12 +46,6 @@ class ListItem { ->setFormFields($createData) ->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password'])) ->getFullRequest(); - - /* $response = $this->getResponse('POST', "animelist/add/{$id}.xml", [ - 'body' => $this->fixBody((new FormBody)->addFields($createData)) - ]); - - return $response->getBody() === 'Created'; */ } public function delete(string $id): Request @@ -65,11 +59,7 @@ class ListItem { ->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password'])) ->getFullRequest(); - /*$response = $this->getResponse('DELETE', "animelist/delete/{$id}.xml", [ - 'body' => $this->fixBody((new FormBody)->addField('id', $id)) - ]); - - return $response->getBody() === 'Deleted';*/ + // return $response->getBody() === 'Deleted' } public function get(string $id): array @@ -93,9 +83,5 @@ class ListItem { ]) ->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password'])) ->getFullRequest(); - - /* return $this->getResponse('POST', "animelist/update/{$id}.xml", [ - 'body' => $this->fixBody($body) - ]); */ } } \ No newline at end of file diff --git a/src/API/MAL/Model.php b/src/API/MAL/Model.php index e8458121..46355b27 100644 --- a/src/API/MAL/Model.php +++ b/src/API/MAL/Model.php @@ -36,13 +36,18 @@ 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): Request { @@ -70,7 +75,7 @@ class Model { ] ]); - return $list;//['anime']; + return $list['myanimelist']['anime']; } public function getListItem(string $listId): array diff --git a/src/API/MAL/Transformer/AnimeListTransformer.php b/src/API/MAL/Transformer/AnimeListTransformer.php index dc48adda..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']); diff --git a/src/API/MAL/Transformer/MALToKitsuTransformer.php b/src/API/MAL/Transformer/MALToKitsuTransformer.php deleted file mode 100644 index dc8fed17..00000000 --- a/src/API/MAL/Transformer/MALToKitsuTransformer.php +++ /dev/null @@ -1,33 +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\MAL; - -use Aviat\Ion\Transformer\AbstractTransformer; - -class MALToKitsuTransformer extends AbstractTransformer { - - - public function transform($item) - { - - } - - public function untransform($item) - { - - } -} \ No newline at end of file diff --git a/src/Command/BaseCommand.php b/src/Command/BaseCommand.php index f3dccc1d..cff2621c 100644 --- a/src/Command/BaseCommand.php +++ b/src/Command/BaseCommand.php @@ -24,11 +24,10 @@ use Aviat\AnimeClient\{ Model, Util }; -use Aviat\AnimeClient\API\{ - CacheTrait, - Kitsu, - MAL -}; +use Aviat\AnimeClient\API\CacheTrait; +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}; @@ -109,21 +108,35 @@ class BaseCommand extends Command { // Models $container->set('kitsu-model', function($container) { + $requestBuilder = new KitsuRequestBuilder(); + $requestBuilder->setLogger($container->getLogger('kitsu-request')); + $listItem = new Kitsu\ListItem(); $listItem->setContainer($container); + $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) { + $requestBuilder = new MALRequestBuilder(); + $requestBuilder->setLogger($container->getLogger('mal-request')); + $listItem = new MAL\ListItem(); $listItem->setContainer($container); + $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 d7f13daf..c219bfff 100644 --- a/src/Command/SyncKitsuWithMal.php +++ b/src/Command/SyncKitsuWithMal.php @@ -16,8 +16,13 @@ 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 @@ -41,91 +46,173 @@ class SyncKitsuWithMal extends BaseCommand { $this->setCache($this->container->get('cache')); $this->kitsuModel = $this->container->get('kitsu-model'); $this->malModel = $this->container->get('mal-model'); - - //$kitsuCount = $this->getKitsuAnimeListPageCount(); - //$this->echoBox("List item count: {$kitsuCount}"); - $this->MALItemCreate(); - - //echo json_encode($this->getMALList(), \JSON_PRETTY_PRINT); + + $malCount = count($this->getMALList()); + $kitsuCount = $this->getKitsuAnimeListPageCount(); + + $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() { - $cacheItem = $this->cache->getItem(Kitsu::AUTH_TOKEN_CACHE_KEY); - - $query = http_build_query([ - 'filter' => [ - 'user_id' => $this->kitsuModel->getUserIdByUsername(), - 'media_type' => 'Anime' - ], - // 'include' => 'anime,anime.genres,anime.mappings,anime.streamingLinks', - 'page' => [ - 'limit' => 1 - ], - 'sort' => '-updated_at' - ]); - $request = (new Artax\Request) - ->setUri("https://kitsu.io/api/edge/library-entries?{$query}") - ->setProtocol('1.1') - ->setAllHeaders([ - 'Accept' => 'application/vnd.api+json', - 'Content-Type' => 'application/vnd.api+json', - 'User-Agent' => "Tim's Anime Client/4.0" - ]); - - if ($cacheItem->isHit()) - { - $token = $cacheItem->get(); - $request->setHeader('Authorization', "bearer {$token}"); - } - else - { - $this->echoBox("WARNING: NOT LOGGED IN\nSome data might be missing"); - } - - $response = \Amp\wait((new Artax\Client)->request($request)); - - $body = json_decode($response->getBody(), TRUE); - return $body['meta']['count']; - } - - public function MALItemCreate() - { - $input = json_decode('{ - "watching_status": "current", - "user_rating": "", - "episodes_watched": "4", - "rewatched": "0", - "notes": "", - "id": "15794526", - "mal_id": "33731", - "edit": "true" - }', TRUE); - - $response = $this->malModel->createListItem([ - 'id' => 12255, - 'status' => 'planned', - 'type' => 'anime' - ]); - - //$response = $this->malModel->updateListItem($input); - //print_r($response); - //echo $response->getBody(); - + 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 @@ -135,7 +222,39 @@ class SyncKitsuWithMal extends BaseCommand { // 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 From 312125f182db8bac0825cf676c4524aecab9a244 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Tue, 14 Feb 2017 16:23:18 -0500 Subject: [PATCH 13/13] Add hummingbird favicon --- app/views/header.php | 1 + favicon.ico | Bin 0 -> 1150 bytes 2 files changed, 1 insertion(+) create mode 100644 favicon.ico 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/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c34a19de65a53bcab706d9cb8468299088a2464a GIT binary patch literal 1150 zcmb8t%_~G<6vy#1Uh>YMMk!%87LyGNMKf6_Wg}KLEG+yLm?l}sUXijGW#JECWv3`R zSt!ba^12g>>-#g$Q!|a}&YbCU&-0w;oZH;nHH+|zMvZHqE!CLC%q$K{RC0Ba8E2hM zIe&Y1S=%#11mkGazLHQs+Odm%?aK}IIw|rM624Z>J$x@YjYll_S_ShB%pBnxhv-6C zp+5BB8UgelmeGjNP!IIXn$!F<^d7b_iYk;0onZhE(0smo_A^k=zv0eW$;Zf__3I1j z-L9|b9==WJeqMPK^(YF}+}Q^C3x4jE^{v}0XR(M8 z=o{@oEi|wCJ2d-=JM3T