Actually update MAL if enabled

This commit is contained in:
Timothy Warren 2017-02-04 15:18:34 -05:00
parent 1835e34690
commit 5eea985828
16 changed files with 328 additions and 118 deletions

View File

@ -48,10 +48,13 @@ return function(array $config_array = []) {
$app_logger = new Logger('animeclient');
$app_logger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/app.log', Logger::NOTICE));
$request_logger = new Logger('request');
$request_logger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/request.log', Logger::NOTICE));
$kitsu_request_logger = new Logger('kitsu_request');
$kitsu_request_logger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/kitsu_request.log', Logger::NOTICE));
$mal_request_logger = new Logger('mal_request');
$mal_request_logger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/mal_request.log', Logger::NOTICE));
$container->setLogger($app_logger, 'default');
$container->setLogger($request_logger, 'request');
$container->setLogger($kitsu_request_logger, 'kitsu_request');
$container->setLogger($mal_request_logger, 'mal_request');
// -------------------------------------------------------------------------
// Injected Objects

View File

@ -1,5 +1,4 @@
<?php if ($auth->is_authenticated()): ?>
<?php /* <pre><?= json_encode($item, \JSON_PRETTY_PRINT); ?></pre> */ ?>
<main>
<h2>Edit Anime List Item</h2>
<form action="<?= $action ?>" method="post">
@ -86,15 +85,20 @@
</tbody>
</table>
</form>
<br />
<br />
<fieldset>
<legend>Danger Zone</legend>
<form class="js-delete" action="<?= $url->generate('anime.delete') ?>" method="post">
<table class="form invisible">
<tbody>
<tr>
<td>&nbsp;</td>
<td>
<strong>Permanently</strong> remove this list item and <strong>all</strong> its data?
</td>
<td>
<input type="hidden" value="<?= $item['id'] ?>" name="id" />
<input type="hidden" value="<?= $item['mal_id'] ?>" name="mal_id" />
<button type="submit" class="danger">Delete Entry</button>
</td>
</tr>

View File

@ -28,4 +28,3 @@ class AnimeWatchingStatus extends BaseEnum {
const ON_HOLD = 'on_hold';
const DROPPED = 'dropped';
}
// End of AnimeWatchingStatus.php

View File

@ -93,7 +93,7 @@ trait KitsuTrait {
'headers' => $this->defaultHeaders
];
$logger = $this->container->getLogger('request');
$logger = $this->container->getLogger('kitsu_request');
$sessionSegment = $this->getContainer()
->get('session')
->getSegment(AnimeClient::SESSION_SEGMENT);
@ -106,10 +106,19 @@ trait KitsuTrait {
$options = array_merge($defaultOptions, $options);
$logger->debug(Json::encode([$type, $url]));
$logger->debug(Json::encode($options));
$response = $this->client->request($type, $url, $options);
return $this->client->request($type, $url, $options);
$logger->debug('Kitsu API request', [
'requestParams' => [
'type' => $type,
'url' => $url,
],
'responseValues' => [
'status' => $response->getStatusCode()
]
]);
return $response;
}
/**
@ -125,7 +134,7 @@ trait KitsuTrait {
$logger = null;
if ($this->getContainer())
{
$logger = $this->container->getLogger('request');
$logger = $this->container->getLogger('kitsu_request');
}
$response = $this->getResponse($type, $url, $options);
@ -134,11 +143,8 @@ trait KitsuTrait {
{
if ($logger)
{
$logger->warning('Non 200 response for api call');
$logger->warning($response->getBody());
$logger->warning('Non 200 response for api call', $response->getBody());
}
// throw new RuntimeException($response->getBody());
}
return JSON::decode($response->getBody(), TRUE);
@ -177,7 +183,7 @@ trait KitsuTrait {
$logger = null;
if ($this->getContainer())
{
$logger = $this->container->getLogger('request');
$logger = $this->container->getLogger('kitsu_request');
}
$response = $this->getResponse('POST', ...$args);
@ -187,11 +193,8 @@ trait KitsuTrait {
{
if ($logger)
{
$logger->warning('Non 201 response for POST api call');
$logger->warning($response->getBody());
$logger->warning('Non 201 response for POST api call', $response->getBody());
}
// throw new RuntimeException($response->getBody());
}
return JSON::decode($response->getBody(), TRUE);

View File

@ -167,6 +167,34 @@ class Model {
return $this->animeTransformer->transform($baseData);
}
/**
* Get the mal id for the anime represented by the kitsu id
* to enable updating MyAnimeList
*
* @param string $kitsuAnimeId The id of the anime on Kitsu
* @return string|null Returns the mal id if it exists, otherwise null
*/
public function getMalIdForAnime(string $kitsuAnimeId)
{
$options = [
'query' => [
'include' => 'mappings'
]
];
$data = $this->getRequest("anime/{$kitsuAnimeId}", $options);
$mappings = array_column($data['included'], 'attributes');
foreach($mappings as $map)
{
if ($map['externalSite'] === 'myanimelist/anime')
{
return $map['externalId'];
}
}
return null;
}
/**
* Get information about a particular manga
*
@ -179,6 +207,16 @@ class Model {
return $this->mangaTransformer->transform($baseData);
}
/**
* Get and transform the entirety of the user's anime list
*
* @return array
*/
public function getFullAnimeList(): array
{
}
/**
* Get the raw (unorganized) anime list for the configured user
*

View File

@ -113,6 +113,7 @@ class AnimeListTransformer extends AbstractTransformer {
$untransformed = [
'id' => $item['id'],
'mal_id' => $item['mal_id'] ?? null,
'data' => [
'status' => $item['watching_status'],
'rating' => $item['user_rating'] / 2,

View File

@ -16,6 +16,10 @@
namespace Aviat\AnimeClient\API;
use Aviat\AnimeClient\API\Kitsu\Enum\{
AnimeWatchingStatus as KAWS,
MangaReadingStatus as KMRS
};
use Aviat\AnimeClient\API\MAL\Enum\{AnimeWatchingStatus, MangaReadingStatus};
/**
@ -25,6 +29,14 @@ class MAL {
const AUTH_URL = 'https://myanimelist.net/api/account/verify_credentials.xml';
const BASE_URL = 'https://myanimelist.net/api/';
const KITSU_MAL_WATCHING_STATUS_MAP = [
KAWS::WATCHING => AnimeWatchingStatus::WATCHING,
KAWS::COMPLETED => AnimeWatchingStatus::COMPLETED,
KAWS::ON_HOLD => AnimeWatchingStatus::ON_HOLD,
KAWS::DROPPED => AnimeWatchingStatus::DROPPED,
KAWS::PLAN_TO_WATCH => AnimeWatchingStatus::PLAN_TO_WATCH
];
public static function getIdToWatchingStatusMap()
{
return [
@ -32,7 +44,12 @@ class MAL {
2 => AnimeWatchingStatus::COMPLETED,
3 => AnimeWatchingStatus::ON_HOLD,
4 => AnimeWatchingStatus::DROPPED,
5 => AnimeWatchingStatus::PLAN_TO_WATCH
6 => AnimeWatchingStatus::PLAN_TO_WATCH,
'watching' => AnimeWatchingStatus::WATCHING,
'completed' => AnimeWatchingStatus::COMPLETED,
'onhold' => AnimeWatchingStatus::ON_HOLD,
'dropped' => AnimeWatchingStatus::DROPPED,
'plantowatch' => AnimeWatchingStatus::PLAN_TO_WATCH
];
}
@ -43,7 +60,12 @@ class MAL {
2 => MangaReadingStatus::COMPLETED,
3 => MangaReadingStatus::ON_HOLD,
4 => MangaReadingStatus::DROPPED,
5 => MangaReadingStatus::PLAN_TO_READ
6 => MangaReadingStatus::PLAN_TO_READ,
'reading' => MangaReadingStatus::READING,
'completed' => MangaReadingStatus::COMPLETED,
'onhold' => MangaReadingStatus::ON_HOLD,
'dropped' => MangaReadingStatus::DROPPED,
'plantoread' => MangaReadingStatus::PLAN_TO_WATCH
];
}
}

View File

@ -22,9 +22,9 @@ use Aviat\Ion\Enum as BaseEnum;
* Possible values for watching status for the current anime
*/
class AnimeWatchingStatus extends BaseEnum {
const WATCHING = 'watching';
const COMPLETED = 'completed';
const ON_HOLD = 'onhold';
const DROPPED = 'dropped';
const PLAN_TO_WATCH = 'plantowatch';
const WATCHING = 1;
const COMPLETED = 2;
const ON_HOLD = 3;
const DROPPED = 4;
const PLAN_TO_WATCH = 6;
}

View File

@ -33,24 +33,24 @@ class ListItem {
public function create(array $data): bool
{
$id = $data['id'];
$body = (new FormBody)
->addField('id', $data['id'])
->addField('data', XML::toXML(['entry' => $data['data']]));
$createData = [
'id' => $id,
'data' => XML::toXML([
'entry' => $data['data']
])
];
$response = $this->getResponse('POST', "animelist/add/{$id}.xml", [
'headers' => [
'Content-type' => 'application/x-www-form-urlencoded',
'Accept' => 'text/plain'
],
'body' => $body
'body' => $this->fixBody((new FormBody)->addFields($createData))
]);
return $response->getStatus() === 201;
return $response->getBody() === 'Created';
}
public function delete(string $id): bool
{
$response = $this->getResponse('DELETE', "animeclient/delete/{$id}.xml", [
'body' => (new FormBody)->addField('id', $id)
$response = $this->getResponse('DELETE', "animelist/delete/{$id}.xml", [
'body' => $this->fixBody((new FormBody)->addField('id', $id))
]);
return $response->getBody() === 'Deleted';
@ -61,18 +61,15 @@ class ListItem {
return [];
}
public function update(string $id, array $data): Response
public function update(string $id, array $data)
{
$xml = XML::toXML(['entry' => $data]);
$body = (new FormBody)
->addField('id', $id)
->addField('data', XML::toXML(['entry' => $data]))
->addField('data', $xml);
return $this->postRequest("animelist/update/{$id}.xml", [
'headers' => [
'Content-type' => 'application/x-www-form-urlencoded',
'Accept' => 'text/plain'
],
'body' => $body
return $this->getResponse('POST', "animelist/update/{$id}.xml", [
'body' => $this->fixBody($body)
]);
}
}

View File

@ -16,7 +16,7 @@
namespace Aviat\AnimeClient\API\MAL;
use Amp\Artax\{Client, Request};
use Amp\Artax\{Client, FormBody, Request};
use Aviat\AnimeClient\API\{
MAL as M,
XML
@ -38,9 +38,27 @@ trait MALTrait {
* @var array
*/
protected $defaultHeaders = [
'Accept' => 'text/xml',
'Accept-Encoding' => 'gzip',
'Content-type' => 'application/x-www-form-urlencoded',
'User-Agent' => "Tim's Anime Client/4.0"
];
/**
* 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');
}
/**
* Make a request via Guzzle
*
@ -51,8 +69,10 @@ trait MALTrait {
*/
private function getResponse(string $type, string $url, array $options = [])
{
$this->defaultHeaders['User-Agent'] = $_SERVER['HTTP_USER_AGENT'] ?? $this->defaultHeaders;
$type = strtoupper($type);
$validTypes = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
$validTypes = ['GET', 'POST', 'DELETE'];
if ( ! in_array($type, $validTypes))
{
@ -60,7 +80,7 @@ trait MALTrait {
}
$config = $this->container->get('config');
$logger = $this->container->getLogger('request');
$logger = $this->container->getLogger('mal_request');
$headers = array_merge($this->defaultHeaders, $options['headers'] ?? [], [
'Authorization' => 'Basic ' .
@ -70,20 +90,39 @@ trait MALTrait {
$query = $options['query'] ?? [];
$url = (strpos($url, '//') !== FALSE)
? $url . '?' . http_build_query($query)
: $this->baseUrl . $url . '?' . http_build_query($query);
? $url
: $this->baseUrl . $url;
if ( ! empty($query))
{
$url .= '?' . http_build_query($query);
}
$request = (new Request)
->setMethod($type)
->setUri($url)
->setProtocol('1.1')
->setAllHeaders($headers)
->setBody($options['body']);
->setAllHeaders($headers);
$logger->debug(Json::encode([$type, $url]));
$logger->debug(Json::encode($options));
if (array_key_exists('body', $options))
{
$request->setBody($options['body']);
}
return \Amp\wait((new Client)->request($request));
$response = \Amp\wait((new Client)->request($request));
$logger->debug('MAL api request', [
'url' => $url,
'status' => $response->getStatus(),
'reason' => $response->getReason(),
'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;
}
/**
@ -99,7 +138,7 @@ trait MALTrait {
$logger = null;
if ($this->getContainer())
{
$logger = $this->container->getLogger('request');
$logger = $this->container->getLogger('mal_request');
}
$response = $this->getResponse($type, $url, $options);
@ -108,8 +147,7 @@ trait MALTrait {
{
if ($logger)
{
$logger->warning('Non 200 response for api call');
$logger->warning($response->getBody());
$logger->warning('Non 200 response for api call', $response->getBody());
}
}
@ -138,7 +176,7 @@ trait MALTrait {
$logger = null;
if ($this->getContainer())
{
$logger = $this->container->getLogger('request');
$logger = $this->container->getLogger('mal_request');
}
$response = $this->getResponse('POST', ...$args);
@ -148,8 +186,7 @@ trait MALTrait {
{
if ($logger)
{
$logger->warning('Non 201 response for POST api call');
$logger->warning($response->getBody());
$logger->warning('Non 201 response for POST api call', $response->getBody());
}
}

View File

@ -17,13 +17,10 @@
namespace Aviat\AnimeClient\API\MAL;
use Aviat\AnimeClient\API\MAL as M;
use Aviat\AnimeClient\API\MAL\{
AnimeListTransformer,
ListItem
};
use Aviat\AnimeClient\API\MAL\ListItem;
use Aviat\AnimeClient\API\MAL\Transformer\AnimeListTransformer;
use Aviat\AnimeClient\API\XML;
use Aviat\Ion\Di\ContainerAware;
use Aviat\Ion\Json;
/**
* MyAnimeList API Model
@ -42,13 +39,37 @@ class Model {
*/
public function __construct(ListItem $listItem)
{
//$this->animeListTransformer = new AnimeListTransformer();
$this->animeListTransformer = new AnimeListTransformer();
$this->listItem = $listItem;
}
public function createListItem(array $data): bool
{
return $this->listItem->create($data);
$createData = [
'id' => $data['id'],
'data' => [
'status' => M::KITSU_MAL_WATCHING_STATUS_MAP[$data['status']]
]
];
return $this->listItem->create($createData);
}
public function getFullList(): array
{
$config = $this->container->get('config');
$userName = $config->get(['mal', 'username']);
$list = $this->getRequest('https://myanimelist.net/malappinfo.php', [
'headers' => [
'Accept' => 'text/xml'
],
'query' => [
'u' => $userName,
'status' => 'all'
]
]);
return $list;//['anime'];
}
public function getListItem(string $listId): array
@ -58,8 +79,8 @@ class Model {
public function updateListItem(array $data)
{
//$updateData = $this->animeListTransformer->transform($data['data']);
return $this->listItem->update($data['mal_id'], $updateData);
$updateData = $this->animeListTransformer->untransform($data);
return $this->listItem->update($updateData['id'], $updateData['data']);
}
public function deleteListItem(string $id): bool

View File

@ -14,8 +14,9 @@
* @link https://github.com/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\MAL;
namespace Aviat\AnimeClient\API\MAL\Transformer;
use Aviat\AnimeClient\API\Kitsu\Enum\AnimeWatchingStatus;
use Aviat\Ion\Transformer\AbstractTransformer;
/**
@ -23,18 +24,22 @@ use Aviat\Ion\Transformer\AbstractTransformer;
*/
class AnimeListTransformer extends AbstractTransformer {
const statusMap = [
AnimeWatchingStatus::WATCHING => '1',
AnimeWatchingStatus::COMPLETED => '2',
AnimeWatchingStatus::ON_HOLD => '3',
AnimeWatchingStatus::DROPPED => '4',
AnimeWatchingStatus::PLAN_TO_WATCH => '6'
];
public function transform($item)
{
$rewatching = 'false';
if (array_key_exists('rewatching', $item) && $item['rewatching'])
{
$rewatching = 'true';
}
$rewatching = (array_key_exists('rewatching', $item) && $item['rewatching']);
return [
'id' => $item['id'],
'id' => $item['mal_id'],
'data' => [
'status' => $item['watching_status'],
'status' => self::statusMap[$item['watching_status']],
'rating' => $item['user_rating'],
'rewatch_value' => (int) $rewatching,
'times_rewatched' => $item['rewatched'],
@ -43,4 +48,31 @@ class AnimeListTransformer extends AbstractTransformer {
]
];
}
/**
* Transform Kitsu episode data to MAL episode data
*
* @param array $item
* @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'],
'status' => self::statusMap[$item['data']['status']],
'score' => (array_key_exists('rating', $item['data']))
? $item['data']['rating'] * 2
: "",
// 'enable_rewatching' => $rewatching,
// 'times_rewatched' => $item['data']['reconsumeCount'],
// 'comments' => $item['data']['notes'],
]
];
return $map;
}
}

View File

@ -107,12 +107,7 @@ class XML {
{
$data = [];
// Get rid of unimportant text nodes by removing
// whitespace characters from between xml tags,
// except for the xml declaration tag, Which looks
// something like:
/* <?xml version="1.0" encoding="UTF-8"?> */
$xml = preg_replace('/([^\?])>\s+</', '$1><', $xml);
$xml = static::stripXMLWhitespace($xml);
$dom = new DOMDocument();
$dom->loadXML($xml);
@ -166,6 +161,16 @@ class XML {
return static::toXML($this->getData());
}
private static function stripXMLWhitespace(string $xml): string
{
// Get rid of unimportant text nodes by removing
// whitespace characters from between xml tags,
// except for the xml declaration tag, Which looks
// something like:
/* <?xml version="1.0" encoding="UTF-8"?> */
return preg_replace('/([^\?])>\s+</', '$1><', $xml);
}
/**
* Recursively create array structure based on xml structure
*

View File

@ -259,7 +259,7 @@ class Anime extends BaseController {
$data = $this->request->getParsedBody();
}
$response = $this->model->updateLibraryItem($data);
$response = $this->model->updateLibraryItem($data, $data);
$this->cache->clear();
$this->outputJSON($response['body'], $response['statusCode']);
@ -273,7 +273,7 @@ class Anime extends BaseController {
public function delete()
{
$body = $this->request->getParsedBody();
$response = $this->model->deleteLibraryItem($body['id']);
$response = $this->model->deleteLibraryItem($body['id'], $body['mal_id']);
if ((bool)$response === TRUE)
{

View File

@ -43,6 +43,12 @@ class Anime extends API {
AnimeWatchingStatus::COMPLETED => self::COMPLETED,
];
protected $kitsuModel;
protected $malModel;
protected $useMALAPI;
/**
* Anime constructor.
* @param ContainerInterface $container
@ -50,7 +56,11 @@ class Anime extends API {
public function __construct(ContainerInterface $container) {
parent::__construct($container);
$config = $container->get('config');
$this->kitsuModel = $container->get('kitsu-model');
$this->malModel = $container->get('mal-model');
$this->useMALAPI = $config->get(['use_mal_api']) === TRUE;
}
/**
@ -110,8 +120,26 @@ class Anime extends API {
return $this->kitsuModel->getListItem($itemId);
}
/**
* Add an anime to your list
*
* @param array $data
* @return bool
*/
public function createLibraryItem(array $data): bool
{
if ($this->useMALAPI)
{
$malData = $data;
$malId = $this->kitsuModel->getMalIdForAnime($malData['id']);
if ( ! is_null($malId))
{
$malData['id'] = $malId;
$this->malModel->createListItem($malData);
}
}
return $this->kitsuModel->createListItem($data);
}
@ -123,11 +151,28 @@ class Anime extends API {
*/
public function updateLibraryItem(array $data): array
{
if ($this->useMALAPI)
{
$this->malModel->updateListItem($data);
}
return $this->kitsuModel->updateListItem($data);
}
public function deleteLibraryItem($id): bool
/**
* Delete a list entry
*
* @param string $id
* @param string|null $malId
* @return bool
*/
public function deleteLibraryItem(string $id, string $malId = null): bool
{
if ($this->useMALAPI && ! is_null($malId))
{
$this->malModel->deleteListItem($malId);
}
return $this->kitsuModel->deleteListItem($id);
}
}

View File

@ -44,6 +44,7 @@ class AnimeListTransformerTest extends AnimeClient_TestCase {
],
'expected' => [
'id' => 14047981,
'mal_id' => null,
'data' => [
'status' => 'current',
'rating' => 4,
@ -57,6 +58,7 @@ class AnimeListTransformerTest extends AnimeClient_TestCase {
], [
'input' => [
'id' => 14047981,
'mal_id' => '12345',
'watching_status' => 'current',
'user_rating' => 8,
'episodes_watched' => 38,
@ -68,6 +70,7 @@ class AnimeListTransformerTest extends AnimeClient_TestCase {
],
'expected' => [
'id' => 14047981,
'mal_id' => '12345',
'data' => [
'status' => 'current',
'rating' => 4,