Lots of Anilist integration, see #5

This commit is contained in:
Timothy Warren 2018-09-26 22:31:04 -04:00
parent ede69b6099
commit 2d3295757d
21 changed files with 659 additions and 259 deletions

View File

@ -24,6 +24,8 @@
"aura/session": "^2.0",
"aviat/banker": "^1.0.0",
"aviat/ion": "^2.3.0",
"ext-gd":"*",
"ext-pdo": "*",
"maximebf/consolekit": "^1.0",
"monolog/monolog": "^1.0",
"psr/http-message": "~1.0",

View File

@ -84,26 +84,30 @@ trait AnilistTrait {
->get('session')
->getSegment(SESSION_SEGMENT);
$authenticated = $sessionSegment->get('auth_token') !== NULL;
//$authenticated = $sessionSegment->get('auth_token') !== NULL;
if ($authenticated)
//if ($authenticated)
{
$request = $request->setAuth('bearer', $anilistConfig['access_token']);
}
if (array_key_exists('form_params', $options)) {
if (array_key_exists('form_params', $options))
{
$request = $request->setFormFields($options['form_params']);
}
if (array_key_exists('query', $options)) {
if (array_key_exists('query', $options))
{
$request = $request->setQuery($options['query']);
}
if (array_key_exists('body', $options)) {
if (array_key_exists('body', $options))
{
$request = $request->setJsonBody($options['body']);
}
if (array_key_exists('headers', $options)) {
if (array_key_exists('headers', $options))
{
$request = $request->setHeaders($options['headers']);
}
@ -148,7 +152,8 @@ trait AnilistTrait {
public function mutateRequest (string $name, array $variables = []): Request
{
$file = realpath(__DIR__ . "/GraphQL/Mutations/{$name}.graphql");
if (!file_exists($file)) {
if (!file_exists($file))
{
throw new \LogicException('GraphQL mutation file does not exist.');
}
@ -161,7 +166,8 @@ trait AnilistTrait {
if (!empty($variables)) {
$body['variables'] = [];
foreach ($variables as $key => $val) {
foreach ($variables as $key => $val)
{
$body['variables'][$key] = $val;
}
}
@ -211,7 +217,8 @@ trait AnilistTrait {
private function getResponseFromRequest(Request $request): Response
{
$logger = NULL;
if ($this->getContainer()) {
if ($this->getContainer())
{
$logger = $this->container->getLogger('anilist-request');
}
@ -236,15 +243,22 @@ trait AnilistTrait {
*/
protected function postRequest(array $options = []): array
{
$response = $this->getResponse(Anilist::BASE_URL, $options);
$validResponseCodes = [200, 201];
$logger = NULL;
if ($this->getContainer())
{
$logger = $this->container->getLogger('anilist-request');
$logger->debug('Anilist response', [
'status' => $response->getStatus(),
'reason' => $response->getReason(),
'body' => $response->getBody(),
'headers' => $response->getHeaders(),
//'requestHeaders' => $request->getHeaders(),
]);
}
$response = $this->getResponse(Anilist::BASE_URL, $options);
$validResponseCodes = [200, 201];
if ( ! \in_array($response->getStatus(), $validResponseCodes, TRUE))
{
if ($logger)

View File

@ -0,0 +1,27 @@
mutation (
$id: Int,
$notes: String,
$private: Boolean,
$progress: Int,
$repeat: Int,
$status: MediaListStatus,
$score: Int,
) {
SaveMediaListEntry (
mediaId: $id,
notes: $notes,
private: $private,
progress: $progress,
repeat: $repeat,
scoreRaw: $score,
status: $status
) {
mediaId
notes
private
progress
repeat
score(format: POINT_10)
status
}
}

View File

@ -1,5 +1,5 @@
query ($id: Int) {
Media (idMal: $id) {
query ($id: Int, $type: MediaType) {
Media (idMal: $id, type: $type) {
mediaListEntry {
id
userId

View File

@ -0,0 +1,7 @@
query ($id: Int, $userName: String) {
MediaList (mediaId: $id, userName: $userName) {
id
userId
mediaId
}
}

View File

@ -0,0 +1,27 @@
query ($name: String, $type: MediaType) {
MediaListCollection(userName: $name, type: $type) {
lists {
entries {
id
mediaId
score
progress
progressVolumes
repeat
private
notes
status
media {
id
idMal
title {
romaji
english
native
userPreferred
}
}
}
}
}
}

View File

@ -24,7 +24,6 @@ query ($name: String) {
status
episodes
season
seasonYear
genres
synonyms
countryOfOrigin

View File

@ -19,6 +19,7 @@ namespace Aviat\AnimeClient\API\Anilist;
use Amp\Artax\Request;
use Aviat\AnimeClient\API\ListItemInterface;
use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\Anilist as AnilistStatus;
use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
use Aviat\AnimeClient\Types\FormItemData;
@ -29,7 +30,7 @@ final class ListItem implements ListItemInterface{
use AnilistTrait;
/**
* Create a list item
* Create a minimal list item
*
* @param array $data
* @return Request
@ -39,6 +40,17 @@ final class ListItem implements ListItemInterface{
return $this->mutateRequest('CreateMediaListEntry', $data);
}
/**
* Create a fleshed-out list item
*
* @param array $data
* @return Request
*/
public function createFull(array $data): Request
{
return $this->mutateRequest('CreateFullMediaListEntry', $data);
}
/**
* Delete a list item
*
@ -85,23 +97,25 @@ final class ListItem implements ListItemInterface{
* @return Request
*/
public function update(string $id, FormItemData $data): Request
{
{
$array = $data->toArray();
$notes = $data['notes'] ?? '';
$progress = array_key_exists('progress', $array) ? $data['progress'] : 0;
$private = array_key_exists('private', $array) ? (bool)$data['private'] : false;
$rating = array_key_exists('rating', $array) ? $data['rating'] : NULL;
$status = $data['status'];
// @TODO Handle weirdness with reWatching
return $this->mutateRequest('UpdateMediaListEntry', [
'id' => $id,
'status' => AnimeWatchingStatus::KITSU_TO_ANILIST[$status],
'score' => $rating * 20,
$status = ($data['reconsuming'] === true) ? AnilistStatus::REPEATING : AnimeWatchingStatus::KITSU_TO_ANILIST[$data['status']];
$updateData = [
'id' => (int)$id,
'status' => $status,
'score' => $rating * 10,
'progress' => $progress,
'repeat' => (int)$data['reconsumeCount'],
'private' => (bool)$data['private'],
'private' => $private,
'notes' => $notes,
]);
];
return $this->mutateRequest('UpdateMediaListEntry', $updateData);
}
}

View File

@ -16,6 +16,8 @@
namespace Aviat\AnimeClient\API\Anilist;
use InvalidArgumentException;
use Amp\Artax\Request;
use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus};
use Aviat\AnimeClient\Types\FormItem;
@ -45,6 +47,30 @@ final class Model
// ! Generic API calls
// -------------------------------------------------------------------------
/**
* Get user list data for syncing with Kitsu
*
* @param string $type
* @return array
* @throws \Aviat\Ion\Di\Exception\ContainerException
* @throws \Aviat\Ion\Di\Exception\NotFoundException
*/
public function getSyncList(string $type = 'anime'): array
{
$config = $this->container->get('config');
$anilistUser = $config->get(['anilist', 'username']);
if ( ! is_string($anilistUser))
{
throw new InvalidArgumentException('Anilist username is not defined in config');
}
return $this->runQuery('SyncUserList', [
'name' => $anilistUser,
'type' => $type,
]);
}
/**
* Create a list item
*
@ -56,14 +82,22 @@ final class Model
{
$createData = [];
$mediaId = $this->getMediaIdFromMalId($data['mal_id'], strtoupper($type));
$mediaId = $this->getMediaIdFromMalId($data['mal_id'], mb_strtoupper($type));
if ($type === 'anime') {
if (empty($mediaId))
{
throw new InvalidArgumentException('Media id missing');
}
if ($type === 'anime')
{
$createData = [
'id' => $mediaId,
'status' => AnimeWatchingStatus::KITSU_TO_ANILIST[$data['status']],
];
} elseif ($type === 'manga') {
}
elseif ($type === 'manga')
{
$createData = [
'id' => $mediaId,
'status' => MangaReadingStatus::KITSU_TO_ANILIST[$data['status']],
@ -73,15 +107,32 @@ final class Model
return $this->listItem->create($createData, $type);
}
/**
* Create a list item with all the relevant data
*
* @param array $data
* @param string $type
* @return Request
*/
public function createFullListItem(array $data, string $type = 'anime'): Request
{
$createData = $data['data'];
$mediaId = $this->getMediaIdFromMalId($data['mal_id']);
$createData['id'] = $mediaId;
return $this->listItem->createFull($createData);
}
/**
* Get the data for a specific list item, generally for editing
*
* @param string $malId - The unique identifier of that list item
* @return mixed
*/
public function getListItem(string $malId): array
public function getListItem(string $malId, string $type): array
{
$id = $this->getListIdFromMalId($malId);
$id = $this->getListIdFromMalId($malId, $type);
$data = $this->listItem->get($id)['data'];
@ -96,9 +147,9 @@ final class Model
* @param FormItem $data
* @return Request
*/
public function incrementListItem(FormItem $data): Request
public function incrementListItem(FormItem $data, string $type): Request
{
$id = $this->getListIdFromMalId($data['mal_id']);
$id = $this->getListIdFromMalId($data['mal_id'], $type);
return $this->listItem->increment($id, $data['data']);
}
@ -107,11 +158,12 @@ final class Model
* Modify a list item
*
* @param FormItem $data
* @param int [$id]
* @return Request
*/
public function updateListItem(FormItem $data): Request
public function updateListItem(FormItem $data, string $type): Request
{
$id = $this->getListIdFromMalId($data['mal_id']);
$id = $this->getListIdFromMalId($data['mal_id'], mb_strtoupper($type));
return $this->listItem->update($id, $data['data']);
}
@ -122,9 +174,9 @@ final class Model
* @param string $malId - The id of the list item to remove
* @return Request
*/
public function deleteListItem(string $malId): Request
public function deleteListItem(string $malId, string $type): Request
{
$item_id = $this->getListIdFromMalId($malId);
$item_id = $this->getListIdFromMalId($malId, $type);
return $this->listItem->delete($item_id);
}
@ -135,10 +187,35 @@ final class Model
* @param string $malId
* @return string
*/
public function getListIdFromMalId(string $malId): ?string
public function getListIdFromMalId(string $malId, string $type): ?string
{
$info = $this->runQuery('ListItemIdByMalId', ['id' => $malId]);
return (string)$info['data']['Media']['mediaListEntry']['id'] ?? NULL;
$mediaId = $this->getMediaIdFromMalId($malId, $type);
return $this->getListIdFromMediaId($mediaId);
}
/**
* Get the Anilist media id from its MAL id
* this way is more accurate than getting the list item id
* directly from the MAL id
*/
private function getListIdFromMediaId(string $mediaId)
{
$config = $this->container->get('config');
$anilistUser = $config->get(['anilist', 'username']);
$info = $this->runQuery('ListItemIdByMediaId', [
'id' => $mediaId,
'userName' => $anilistUser,
]);
/* dump([
'media_id' => $mediaId,
'userName' => $anilistUser,
'response' => $info,
]);
die(); */
return (string)$info['data']['MediaList']['id'];
}
/**
@ -152,8 +229,14 @@ final class Model
{
$info = $this->runQuery('MediaIdByMalId', [
'id' => $malId,
'type' => $type
'type' => mb_strtoupper($type),
]);
/* dump([
'mal_id' => $malId,
'response' => $info,
]);
die(); */
return (string)$info['data']['Media']['id'];
}

View File

@ -16,22 +16,51 @@
namespace Aviat\AnimeClient\API\Anilist\Transformer;
use Aviat\AnimeClient\Types\{AnimeListItem, AnimeFormItem};
use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\Anilist as AnilistStatus;
use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
use Aviat\AnimeClient\Types\{Anime, AnimeListItem, AnimeFormItem};
use Aviat\Ion\Transformer\AbstractTransformer;
class AnimeListTransformer extends AbstractTransformer {
public function transform($item): AnimeListItem
{
dump($item); die();
return new AnimeListItem([
return new AnimeListItem([]);
}
/**
* Transform Anilist list item to Kitsu form update format
*
* @param array $item
* @return AnimeFormItem
*/
public function untransform(array $item): AnimeFormItem
{
return new AnimeFormItem([
'id' => $item['id'],
'mal_id' => $item['media']['idMal'],
'data' => [
'notes' => $item['notes'] ?? '',
'private' => $item['private'],
'progress' => $item['progress'],
'rating' => $item['score'],
'reconsumeCount' => $item['repeat'],
'reconsuming' => $item['status'] === AnilistStatus::REPEATING,
'status' => AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']],
]
]);
}
public function untransform(array $item): AnimeFormItem
/**
* Transform a set of structures
*
* @param array|object $collection
* @return array
*/
public function untransformCollection($collection): array
{
return new AnimeFormItem($item);
$list = (array)$collection;
return array_map([$this, 'untransform'], $list);
}
}

View File

@ -16,7 +16,10 @@
namespace Aviat\AnimeClient\API\Anilist\Transformer;
use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Anilist as AnilistStatus;
use Aviat\AnimeClient\API\Mapping\MangaReadingStatus;
use Aviat\AnimeClient\Types\MangaFormItem;
use Aviat\Ion\Transformer\AbstractTransformer;
class MangaListTransformer extends AbstractTransformer {
@ -26,8 +29,38 @@ class MangaListTransformer extends AbstractTransformer {
}
/**
* Transform Anilist list item to Kitsu form update format
*
* @param array $item
* @return MangaFormItem
*/
public function untransform(array $item): MangaFormItem
{
return new MangaFormItem($item);
return new MangaFormItem([
'id' => $item['id'],
'mal_id' => $item['media']['idMal'],
'data' => [
'notes' => $item['notes'] ?? '',
'private' => $item['private'],
'progress' => $item['progress'],
'rating' => $item['score'],
'reconsumeCount' => $item['repeat'],
'reconsuming' => $item['status'] === AnilistStatus::REPEATING,
'status' => MangaReadingStatus::ANILIST_TO_KITSU[$item['status']],
]
]);
}
/**
* Transform a set of structures
*
* @param array|object $collection
* @return array
*/
public function untransformCollection($collection): array
{
$list = (array)$collection;
return array_map([$this, 'untransform'], $list);
}
}

View File

@ -37,7 +37,7 @@ final class ListItem implements ListItemInterface {
use KitsuTrait;
public function create(array $data): Request
{
{
$body = [
'data' => [
'type' => 'libraryEntries',
@ -61,6 +61,11 @@ final class ListItem implements ListItemInterface {
]
]
];
if (array_key_exists('notes', $data))
{
$body['data']['attributes']['notes'] = $data['notes'];
}
$authHeader = $this->getAuthHeader();

View File

@ -37,11 +37,8 @@ use Aviat\AnimeClient\API\Kitsu\Transformer\{
MangaListTransformer
};
use Aviat\AnimeClient\Types\{
AbstractType,
Anime,
FormItem,
FormItemData,
AnimeListItem,
MangaPage
};
use Aviat\Ion\{Di\ContainerAware, Json};

View File

@ -45,8 +45,8 @@ final class AnimeListTransformer extends AbstractTransformer {
$genres = array_column($anime['relationships']['genres'], 'name') ?? [];
sort($genres);
$rating = (int) $item['attributes']['rating'] !== 0
? 2 * $item['attributes']['rating']
$rating = (int) $item['attributes']['ratingTwenty'] !== 0
? $item['attributes']['ratingTwenty'] / 2
: '-';
$total_episodes = array_key_exists('episodeCount', $anime) && (int) $anime['episodeCount'] !== 0
@ -141,7 +141,7 @@ final class AnimeListTransformer extends AbstractTransformer {
if (is_numeric($item['user_rating']) && $item['user_rating'] > 0)
{
$untransformed['data']['rating'] = $item['user_rating'] / 2;
$untransformed['data']['ratingTwenty'] = $item['user_rating'] * 2;
}
return $untransformed;

View File

@ -46,8 +46,8 @@ final class MangaListTransformer extends AbstractTransformer {
$genres = array_column($manga['relationships']['genres'], 'name') ?? [];
sort($genres);
$rating = (int) $item['attributes']['rating'] !== 0
? 2 * $item['attributes']['rating']
$rating = (int) $item['attributes']['ratingTwenty'] !== 0
? $item['attributes']['ratingTwenty'] / 2
: '-';
$totalChapters = ((int) $manga['chapterCount'] !== 0)
@ -138,7 +138,7 @@ final class MangaListTransformer extends AbstractTransformer {
if (is_numeric($item['new_rating']) && $item['new_rating'] > 0)
{
$map['data']['rating'] = $item['new_rating'] / 2;
$map['data']['ratingTwenty'] = $item['new_rating'] * 2;
}
return $map;

View File

@ -66,6 +66,7 @@ final class ParallelAPIRequest {
* Actually make the requests
*
* @return array
* @throws \Throwable
*/
public function makeRequests(): array
{

View File

@ -21,6 +21,7 @@ use function Aviat\AnimeClient\loadToml;
use Aura\Session\SessionFactory;
use Aviat\AnimeClient\Util;
use Aviat\AnimeClient\API\CacheTrait;
use Aviat\AnimeClient\API\Anilist;
use Aviat\AnimeClient\API\Kitsu;
use Aviat\AnimeClient\API\Kitsu\KitsuRequestBuilder;
use Aviat\Banker\Pool;
@ -85,7 +86,10 @@ class BaseCommand extends Command {
$app_logger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/app-cli.log', Logger::NOTICE));
$kitsu_request_logger = new Logger('kitsu-request');
$kitsu_request_logger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/kitsu_request-cli.log', Logger::NOTICE));
$anilistRequestLogger = new Logger('anilist-request');
$anilistRequestLogger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/anilist_request-cli.log', Logger::NOTICE));
$container->setLogger($app_logger);
$container->setLogger($anilistRequestLogger, 'anilist-request');
$container->setLogger($kitsu_request_logger, 'kitsu-request');
// Create Config Object
@ -122,6 +126,20 @@ class BaseCommand extends Command {
$model->setCache($cache);
return $model;
});
$container->set('anilist-model', function ($container) {
$requestBuilder = new Anilist\AnilistRequestBuilder();
$requestBuilder->setLogger($container->getLogger('anilist-request'));
$listItem = new Anilist\ListItem();
$listItem->setContainer($container);
$listItem->setRequestBuilder($requestBuilder);
$model = new Anilist\Model($listItem);
$model->setContainer($container);
$model->setRequestBuilder($requestBuilder);
return $model;
});
$container->set('util', function($container) {
return new Util($container);

View File

@ -16,19 +16,28 @@
namespace Aviat\AnimeClient\Command;
use Aviat\AnimeClient\API\{
FailedResponseException,
JsonAPI,
ParallelAPIRequest
use Aviat\AnimeClient\API\
{FailedResponseException, JsonAPI, Kitsu\Transformer\MangaListTransformer, ParallelAPIRequest};
use Aviat\AnimeClient\API\Anilist\Transformer\{
AnimeListTransformer as AALT,
MangaListTransformer as AMLT,
};
use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus};
use Aviat\AnimeClient\Types\{AnimeFormItem, MangaFormItem};
use Aviat\Ion\Json;
use DateTime;
/**
* Clears the API Cache
* Syncs list data between Anilist and Kitsu
*/
final class SyncLists extends BaseCommand {
/**
* Model for making requests to Anilist API
* @var \Aviat\AnimeClient\API\Anilist\Model
*/
protected $anilistModel;
/**
* Model for making requests to Kitsu API
* @var \Aviat\AnimeClient\API\Kitsu\Model
@ -36,7 +45,7 @@ final class SyncLists extends BaseCommand {
protected $kitsuModel;
/**
* Run the Kitsu <=> MAL sync script
* Run the Kitsu <=> Anilist sync script
*
* @param array $args
* @param array $options
@ -48,6 +57,7 @@ final class SyncLists extends BaseCommand {
{
$this->setContainer($this->setupContainer());
$this->setCache($this->container->get('cache'));
$this->anilistModel = $this->container->get('anilist-model');
$this->kitsuModel = $this->container->get('kitsu-model');
$this->sync('anime');
@ -64,18 +74,6 @@ final class SyncLists extends BaseCommand {
{
$uType = ucfirst($type);
// Do a little check to make sure you don't have immediate issues
// if you have 0 or 1 items in a list on MAL.
/* $malList = $this->malModel->getList($type);
$malCount = 0;
if ( ! empty($malList))
{
$malCount = count(array_key_exists(0, $malList)
? $malList
: [$malList]
);
} */
$kitsuCount = 0;
try
{
@ -87,24 +85,23 @@ final class SyncLists extends BaseCommand {
}
// $this->echoBox("Number of MAL {$type} list items: {$malCount}");
$this->echoBox("Number of Kitsu {$type} list items: {$kitsuCount}");
$data = $this->diffLists($type);
/* if ( ! empty($data['addToMAL']))
if ( ! empty($data['addToAnilist']))
{
$count = count($data['addToMAL']);
$this->echoBox("Adding {$count} missing {$type} list items to MAL");
$this->updateMALListItems($data['addToMAL'], 'create', $type);
$count = count($data['addToAnilist']);
$this->echoBox("Adding {$count} missing {$type} list items to Anilist");
$this->updateAnilistListItems($data['addToAnilist'], 'create', $type);
}
if ( ! empty($data['updateMAL']))
if ( ! empty($data['updateAnilist']))
{
$count = count($data['updateMAL']);
$this->echoBox("Updating {$count} outdated MAL {$type} list items");
$this->updateMALListItems($data['updateMAL'], 'update', $type);
} */
$count = count($data['updateAnilist']);
$this->echoBox("Updating {$count} outdated Anilist {$type} list items");
$this->updateAnilistListItems($data['updateAnilist'], 'update', $type);
}
if ( ! empty($data['addToKitsu']))
{
@ -144,107 +141,79 @@ final class SyncLists extends BaseCommand {
}
/**
* Format a MAL list for comparison
* Format an Anilist list for comparison
*
* @param string $type
* @return array
*/
/* protected function formatMALList(string $type): array
protected function formatAnilistList(string $type): array
{
$type = ucfirst($type);
$method = "formatMAL{$type}List";
$method = "formatAnilist{$type}List";
return $this->$method();
} */
}
/**
* Format a MAL anime list for comparison
* Format an Anilist anime list for comparison
*
* @return array
*/
/* protected function formatMALAnimeList(): array
protected function formatAnilistAnimeList(): array
{
$orig = $this->malModel->getList('anime');
$anilistList = $this->anilistModel->getSyncList('ANIME');
$anilistTransformer = new AALT();
$transformedAnilist = [];
foreach ($anilistList['data']['MediaListCollection']['lists'] as $list)
{
$newTransformed = $anilistTransformer->untransformCollection($list['entries']);
$transformedAnilist = array_merge($transformedAnilist, $newTransformed);
}
// Key the array by the mal_id for easier reference in the next comparision step
$output = [];
// Bail early on empty list
if (empty($orig))
foreach ($transformedAnilist as $item)
{
return [];
$output[$item['mal_id']] = $item->toArray();
}
// Due to xml parsing differences,
// 1 item has no wrapping array.
// In this case, just re-create the
// wrapper array
if ( ! array_key_exists(0, $orig))
{
$orig = [$orig];
}
foreach($orig as $item)
{
$output[$item['series_animedb_id']] = [
'id' => $item['series_animedb_id'],
'data' => [
'status' => AnimeWatchingStatus::MAL_TO_KITSU[$item['my_status']],
'progress' => $item['my_watched_episodes'],
'reconsuming' => (bool) $item['my_rewatching'],
'rating' => $item['my_score'] / 2,
'updatedAt' => (new \DateTime())
->setTimestamp((int)$item['my_last_updated'])
->format(\DateTime::W3C),
]
];
}
$count = count($output);
$this->echoBox("Number of Anilist anime list items: {$count}");
return $output;
} */
}
/**
* Format a MAL manga list for comparison
* Format an Anilist manga list for comparison
*
* @return array
*/
/* protected function formatMALMangaList(): array
protected function formatAnilistMangaList(): array
{
$orig = $this->malModel->getList('manga');
$anilistList = $this->anilistModel->getSyncList('MANGA');
$anilistTransformer = new AMLT();
$transformedAnilist = [];
foreach ($anilistList['data']['MediaListCollection']['lists'] as $list)
{
$newTransformed = $anilistTransformer->untransformCollection($list['entries']);
$transformedAnilist = array_merge($transformedAnilist, $newTransformed);
}
// Key the array by the mal_id for easier reference in the next comparision step
$output = [];
// Bail early on empty list
if (empty($orig))
foreach ($transformedAnilist as $item)
{
return [];
$output[$item['mal_id']] = $item->toArray();
}
// Due to xml parsing differences,
// 1 item has no wrapping array.
// In this case, just re-create the
// wrapper array
if ( ! array_key_exists(0, $orig))
{
$orig = [$orig];
}
foreach($orig as $item)
{
$output[$item['series_mangadb_id']] = [
'id' => $item['series_mangadb_id'],
'data' => [
'my_status' => $item['my_status'],
'status' => MangaReadingStatus::MAL_TO_KITSU[$item['my_status']],
'progress' => $item['my_read_chapters'],
'volumes' => $item['my_read_volumes'],
'reconsuming' => (bool) $item['my_rereadingg'],
'rating' => $item['my_score'] / 2,
'updatedAt' => (new \DateTime())
->setTimestamp((int)$item['my_last_updated'])
->format(\DateTime::W3C),
]
];
}
$count = count($output);
$this->echoBox("Number of Anilist manga list items: {$count}");
return $output;
} */
}
/**
* Format a kitsu list for the sake of comparision
@ -254,7 +223,7 @@ final class SyncLists extends BaseCommand {
*/
protected function formatKitsuList(string $type = 'anime'): array
{
$data = $this->kitsuModel->{'getFull' . ucfirst($type) . 'List'}();
$data = $this->kitsuModel->{'getFullRaw' . ucfirst($type) . 'List'}();
if (empty($data))
{
@ -281,7 +250,7 @@ final class SyncLists extends BaseCommand {
}
}
// Skip to the next item if there isn't a MAL ID
// Skip to the next item if there isn't a Anilist ID
if ($malId === NULL)
{
continue;
@ -309,31 +278,51 @@ final class SyncLists extends BaseCommand {
// Organize mappings, and ignore entries without mappings
$kitsuList = $this->formatKitsuList($type);
// Get MAL list data
// $malList = $this->formatMALList($type);
// Get Anilist list data
$anilistList = $this->formatAnilistList($type);
$itemsToAddToMAL = [];
$itemsToAddToAnilist = [];
$itemsToAddToKitsu = [];
$malUpdateItems = [];
$anilistUpdateItems = [];
$kitsuUpdateItems = [];
// $malIds = array_column($malList, 'id');
// $kitsuMalIds = array_column($kitsuList, 'malId');
// $missingMalIds = array_diff($malIds, $kitsuMalIds);
$malBlackList = ($type === 'anime')
? [
27821, // Fate/stay night: Unlimited Blade Works - Prologue
29317, // Saekano: How to Raise a Boring Girlfriend Prologue
30514, // Nisekoinogatari
] : [
114638, // Cells at Work: Black
];
$malIds = array_keys($anilistList);
$kitsuMalIds = array_map('intval', array_column($kitsuList, 'malId'));
$missingMalIds = array_diff($malIds, $kitsuMalIds);
$missingMalIds = array_diff($missingMalIds, $malBlackList);
/* foreach($missingMalIds as $mid)
{
$itemsToAddToKitsu[] = array_merge($malList[$mid]['data'], [
'id' => $this->kitsuModel->getKitsuIdFromMALId($mid, $type),
foreach($missingMalIds as $mid)
{
$itemsToAddToKitsu[] = array_merge($anilistList[$mid]['data'], [
'id' => $this->kitsuModel->getKitsuIdFromMALId((string)$mid, $type),
'type' => $type
]);
} */
}
/* foreach($kitsuList as $kitsuItem)
foreach($kitsuList as $kitsuItem)
{
if (\in_array($kitsuItem['malId'], $malIds, TRUE))
$malId = $kitsuItem['malId'];
if (in_array($malId, $malBlackList))
{
$item = $this->compareListItems($kitsuItem, $malList[$kitsuItem['malId']]);
continue;
}
if (array_key_exists($malId, $anilistList))
{
$anilistItem = $anilistList[$malId];
// dump($anilistItem);
$item = $this->compareListItems($kitsuItem, $anilistItem);
if ($item === NULL)
{
@ -345,25 +334,39 @@ final class SyncLists extends BaseCommand {
$kitsuUpdateItems[] = $item['data'];
}
if (\in_array('mal', $item['updateType'], TRUE))
if (\in_array('anilist', $item['updateType'], TRUE))
{
$malUpdateItems[] = $item['data'];
$anilistUpdateItems[] = $item['data'];
}
continue;
}
$statusMap = ($type === 'anime') ? AnimeWatchingStatus::class : MangaReadingStatus::class;
// Looks like this item only exists on Kitsu
$itemsToAddToMAL[] = [
'mal_id' => $kitsuItem['malId'],
'data' => $kitsuItem['data']
$kItem = $kitsuItem['data'];
$newItemStatus = ($kItem['reconsuming'] === true) ? 'REPEATING' : $statusMap::KITSU_TO_ANILIST[$kItem['status']];
$itemsToAddToAnilist[] = [
'mal_id' => $malId,
'data' => [
'notes' => $kItem['notes'],
'private' => $kItem['private'],
'progress' => $kItem['progress'],
'repeat' => $kItem['reconsumeCount'],
'score' => $kItem['ratingTwenty'] / 2,
'status' => $newItemStatus,
], // $kitsuItem['data']
];
} */
}
//dump($itemsToAddToAnilist);
//die();
return [
// 'addToMAL' => $itemsToAddToMAL,
// 'updateMAL' => $malUpdateItems,
'addToAnilist' => $itemsToAddToAnilist,
'updateAnilist' => $anilistUpdateItems,
'addToKitsu' => $itemsToAddToKitsu,
'updateKitsu' => $kitsuUpdateItems
];
@ -373,18 +376,27 @@ final class SyncLists extends BaseCommand {
* Compare two list items, and return the out of date one, if one exists
*
* @param array $kitsuItem
* @param array $malItem
* @param array $anilistItem
* @return array|null
*/
protected function compareListItems(array $kitsuItem, array $malItem): ?array
protected function compareListItems(array $kitsuItem, array $anilistItem): ?array
{
$compareKeys = ['status', 'progress', 'rating', 'reconsuming'];
$compareKeys = [
'notes',
'progress',
'rating',
'reconsumeCount',
'reconsuming',
'status',
];
$diff = [];
$dateDiff = new DateTime($kitsuItem['data']['updatedAt']) <=> new DateTime($malItem['data']['updatedAt']);
// Correct differences in notation
$kitsuItem['data']['rating'] = $kitsuItem['data']['ratingTwenty'] / 2;
foreach($compareKeys as $key)
{
$diff[$key] = $kitsuItem['data'][$key] <=> $malItem['data'][$key];
$diff[$key] = $kitsuItem['data'][$key] <=> $anilistItem['data'][$key];
}
// No difference? Bail out early
@ -404,9 +416,18 @@ final class SyncLists extends BaseCommand {
'updateType' => []
];
$sameNotes = $diff['notes'] === 0;
$sameStatus = $diff['status'] === 0;
$sameProgress = $diff['progress'] === 0;
$sameRating = $diff['rating'] === 0;
$sameRewatchCount = $diff['reconsumeCount'] === 0;
// If an item is completed, make sure the 'reconsuming' flag is false
if ($kitsuItem['data']['status'] === 'completed' && $kitsuItem['data']['reconsuming'] === TRUE)
{
$update['data']['reconsuming'] = FALSE;
$return['updateType'][] = 'kitsu';
}
// If status is the same, and progress count is different, use greater progress
@ -415,18 +436,25 @@ final class SyncLists extends BaseCommand {
if ($diff['progress'] === 1)
{
$update['data']['progress'] = $kitsuItem['data']['progress'];
$return['updateType'][] = 'mal';
$return['updateType'][] = 'anilist';
}
else if($diff['progress'] === -1)
{
$update['data']['progress'] = $malItem['data']['progress'];
$update['data']['progress'] = $anilistItem['data']['progress'];
$return['updateType'][] = 'kitsu';
}
}
// If status is different, go with Kitsu
if ( ! $sameStatus)
{
$update['data']['status'] = $kitsuItem['data']['status'];
$return['updateType'][] = 'anilist';
}
// If status and progress are different, it's a bit more complicated...
// But, at least for now, assume newer record is correct
if ( ! ($sameStatus || $sameProgress))
/* if ( ! ($sameStatus || $sameProgress))
{
if ($dateDiff === 1)
{
@ -437,59 +465,111 @@ final class SyncLists extends BaseCommand {
$update['data']['progress'] = $kitsuItem['data']['progress'];
}
$return['updateType'][] = 'mal';
$return['updateType'][] = 'anilist';
}
else if($dateDiff === -1)
{
$update['data']['status'] = $malItem['data']['status'];
$update['data']['status'] = $anilistItem['data']['status'];
if ((int)$malItem['data']['progress'] !== 0)
if ((int)$anilistItem['data']['progress'] !== 0)
{
$update['data']['progress'] = $kitsuItem['data']['progress'];
}
$return['updateType'][] = 'kitsu';
}
}
}*/
// If rating is different, use the rating from the item most recently updated
// If rating is different, use the kitsu rating, unless the other rating
// is set, and the kitsu rating is not set
if ( ! $sameRating)
{
if ($dateDiff === 1)
if ($kitsuItem['data']['rating'] !== 0)
{
$update['data']['rating'] = $kitsuItem['data']['rating'];
$return['updateType'][] = 'mal';
$return['updateType'][] = 'anilist';
}
else if ($dateDiff === -1)
else
{
$update['data']['rating'] = $malItem['data']['rating'];
$update['data']['rating'] = $anilistItem['data']['rating'];
$return['updateType'][] = 'kitsu';
}
}
// If notes are set, use kitsu, otherwise, set kitsu from anilist
if ( ! $sameNotes)
{
if ($kitsuItem['data']['notes'] !== '')
{
$update['data']['notes'] = $kitsuItem['data']['notes'];
$return['updateType'][] = 'anilist';
}
else
{
$update['data']['notes'] = $anilistItem['data']['notes'];
$return['updateType'][] = 'kitsu';
}
}
// Assume the larger reconsumeCount is correct
if ( ! $sameRewatchCount)
{
if ($diff['reconsumeCount'] === 1)
{
$update['data']['reconsumeCount'] = $kitsuItem['data']['reconsumeCount'];
$return['updateType'][] = 'anilist';
}
else if ($diff['reconsumeCount'] === -1)
{
$update['data']['reconsumeCount'] = $anilistItem['data']['reconsumeCount'];
$return['updateType'][] = 'kitsu';
}
}
// If status is different, use the status of the more recently updated item
if ( ! $sameStatus)
/* if ( ! $sameStatus)
{
if ($dateDiff === 1)
{
$update['data']['status'] = $kitsuItem['data']['status'];
$return['updateType'][] = 'mal';
$return['updateType'][] = 'anilist';
}
else if ($dateDiff === -1)
{
$update['data']['status'] = $malItem['data']['status'];
$update['data']['status'] = $anilistItem['data']['status'];
$return['updateType'][] = 'kitsu';
}
}
} */
$return['meta'] = [
'kitsu' => $kitsuItem['data'],
'mal' => $malItem['data'],
'dateDiff' => $dateDiff,
'anilist' => $anilistItem['data'],
// 'dateDiff' => $dateDiff,
'diff' => $diff,
];
$return['data'] = $update;
$return['updateType'] = array_unique($return['updateType']);
// Fill in missing data values for update on Anlist
// so I don't have to create a really complex graphql query
// to handle each combination of fields
if ($return['updateType'][0] === 'anilist')
{
$prevData = [
'notes' => $kitsuItem['data']['notes'],
'private' => $kitsuItem['data']['private'],
'progress' => $kitsuItem['data']['progress'],
'rating' => $kitsuItem['data']['rating'],
'reconsumeCount' => $kitsuItem['data']['reconsumeCount'],
'reconsuming' => $kitsuItem['data']['reconsuming'],
'status' => $kitsuItem['data']['status'],
];
$return['data']['data'] = array_merge($prevData, $return['data']['data']);
}
dump($return);
return $return;
}
@ -505,9 +585,13 @@ final class SyncLists extends BaseCommand {
$requester = new ParallelAPIRequest();
foreach($itemsToUpdate as $item)
{
$typeClass = '\\Aviat\\AnimeClient\\Types\\' . ucFirst($type) . 'FormItem';
if ($action === 'update')
{
$requester->addRequest($this->kitsuModel->updateListItem($item));
$requester->addRequest(
$this->kitsuModel->updateListItem(new $typeClass($item))
);
}
else if ($action === 'create')
{
@ -537,27 +621,29 @@ final class SyncLists extends BaseCommand {
}
/**
* Create/Update list items on MAL
* Create/Update list items on Anilist
*
* @param array $itemsToUpdate
* @param string $action
* @param string $type
*/
/* protected function updateMALListItems(array$itemsToUpdate, string $action = 'update', string $type = 'anime'): void
protected function updateAnilistListItems(array$itemsToUpdate, string $action = 'update', string $type = 'anime'): void
{
$transformer = new ALT();
$requester = new ParallelAPIRequest();
$typeClass = '\\Aviat\\AnimeClient\\Types\\' . ucFirst($type) . 'FormItem';
foreach($itemsToUpdate as $item)
{
if ($action === 'update')
{
$requester->addRequest($this->malModel->updateListItem($item, $type));
{
$requester->addRequest(
$this->anilistModel->updateListItem(new $typeClass($item), $type)
);
}
else if ($action === 'create')
{
$data = $transformer->untransform($item);
$requester->addRequest($this->malModel->createFullListItem($data, $type));
$requester->addRequest($this->anilistModel->createFullListItem($item, $type));
}
}
@ -566,20 +652,21 @@ final class SyncLists extends BaseCommand {
foreach($responses as $key => $response)
{
$id = $itemsToUpdate[$key]['mal_id'];
$goodResponse = (
($action === 'update' && $response === 'Updated') ||
($action === 'create' && $response === 'Created')
);
if ($goodResponse)
$responseData = Json::decode($response);
// $id = $itemsToUpdate[$key]['id'];
if ( ! array_key_exists('errors', $responseData))
{
$verb = ($action === 'update') ? 'updated' : 'created';
$this->echoBox("Successfully {$verb} MAL {$type} list item with id: {$id}");
$this->echoBox("Successfully {$verb} Anilist {$type} list item with id: {$id}");
}
else
{
dump($responseData);
$verb = ($action === 'update') ? 'update' : 'create';
$this->echoBox("Failed to {$verb} MAL {$type} list item with id: {$id}");
$this->echoBox("Failed to {$verb} Anilist {$type} list item with id: {$id}");
}
}
} */
}
}

View File

@ -130,6 +130,11 @@ final class Manga extends Controller {
$this->redirect('manga/add', 303);
}
if (empty($data['mal_id']))
{
unset($data['mal_id']);
}
$result = $this->model->createLibraryItem($data);
if ($result)
@ -182,8 +187,9 @@ final class Manga extends Controller {
*/
public function search(): void
{
$query_data = $this->request->getQueryParams();
$this->outputJSON($this->model->search($query_data['query']));
$queryParams = $this->request->getQueryParams();
$query = $queryParams['query'];
$this->outputJSON($this->model->search($query));
}
/**
@ -218,13 +224,9 @@ final class Manga extends Controller {
}
/**
* Update a manga item
*
* @throws \Aviat\Ion\Di\ContainerException
* @throws \Aviat\Ion\Di\NotFoundException
* @return void
* Increment the progress of a manga item
*/
public function update(): void
public function increment(): void
{
if (stripos($this->request->getHeader('content-type')[0], 'application/json') !== FALSE)
{
@ -235,7 +237,7 @@ final class Manga extends Controller {
$data = $this->request->getParsedBody();
}
$response = $this->model->updateLibraryItem(new MangaFormItem($data));
$response = $this->model->incrementLibraryItem(new MangaFormItem($data));
$this->cache->clear();
$this->outputJSON($response['body'], $response['statusCode']);
@ -251,13 +253,11 @@ final class Manga extends Controller {
public function delete(): void
{
$body = $this->request->getParsedBody();
$id = $body['id'];
$malId = $body['mal_id'];
$response = $this->model->deleteLibraryItem($id, $malId);
$response = $this->model->deleteLibraryItem($body['id'], $body['mal_id']);
if ($response)
{
$this->setFlashMessage("Successfully deleted manga.", 'success');
$this->setFlashMessage('Successfully deleted manga.', 'success');
$this->cache->clear();
}
else

View File

@ -31,6 +31,13 @@ use Aviat\Ion\Json;
*/
class Anime extends API {
/**
* Is the Anilist API enabled?
*
* @var boolean
*/
protected $anilistEnabled;
/**
* Model for making requests to Anilist API
*
@ -54,6 +61,9 @@ class Anime extends API {
{
$this->anilistModel = $container->get('anilist-model');
$this->kitsuModel = $container->get('kitsu-model');
$config = $container->get('config');
$this->anilistEnabled = (bool) $config->get(['anilist', 'enabled']);
}
/**
@ -137,18 +147,6 @@ class Anime extends API {
$item = $this->kitsuModel->getListItem($itemId);
$array = $item->toArray();
if ( ! empty($item->mal_id))
{
$anilistInfo = $this->anilistModel->getListItem($item['mal_id']);
if (empty($anilistInfo))
{
return $item;
}
$array['anilist_item_id'] = $anilistInfo['id'];
}
if (is_array($array['notes']))
{
$array['notes'] = '';
@ -168,9 +166,9 @@ class Anime extends API {
$requester = new ParallelAPIRequest();
$requester->addRequest($this->kitsuModel->createListItem($data), 'kitsu');
// @TODO Make sure Anilist integration is optional
if (array_key_exists('mal_id', $data)) {
$requester->addRequest($this->anilistModel->createListItem($data), 'anilist');
if (array_key_exists('mal_id', $data) && $this->anilistEnabled)
{
$requester->addRequest($this->anilistModel->createListItem($data, 'ANIME'), 'anilist');
}
$results = $requester->makeRequests();
@ -199,9 +197,9 @@ class Anime extends API {
$array = $data->toArray();
// @TODO Make sure Anilist integration is optional
if (array_key_exists('mal_id', $array)) {
$requester->addRequest($this->anilistModel->incrementListItem($data), 'anilist');
if (array_key_exists('mal_id', $array) && $this->anilistEnabled)
{
$requester->addRequest($this->anilistModel->incrementListItem($data, 'ANIME'), 'anilist');
}
$results = $requester->makeRequests();
@ -228,10 +226,9 @@ class Anime extends API {
$array = $data->toArray();
// @TODO Make sure Anilist integration is optional
if (array_key_exists('mal_id', $array))
if (array_key_exists('mal_id', $array) && $this->anilistEnabled)
{
$requester->addRequest($this->anilistModel->updateListItem($data), 'anilist');
$requester->addRequest($this->anilistModel->updateListItem($data, 'ANIME'), 'anilist');
}
$results = $requester->makeRequests();
@ -257,9 +254,9 @@ class Anime extends API {
$requester = new ParallelAPIRequest();
$requester->addRequest($this->kitsuModel->deleteListItem($id), 'kitsu');
// @TODO Make sure Anilist integration is optional
if ($malId !== null) {
$requester->addRequest($this->anilistModel->deleteListItem($malId), 'anilist');
if ($malId !== null && $this->anilistEnabled)
{
$requester->addRequest($this->anilistModel->deleteListItem($malId, 'ANIME'), 'anilist');
}
$results = $requester->makeRequests();

View File

@ -33,6 +33,19 @@ use Aviat\Ion\Json;
* Model for handling requests dealing with the manga list
*/
class Manga extends API {
/**
* Is the Anilist API enabled?
*
* @var boolean
*/
protected $anilistEnabled;
/**
* Model for making requests to the Anilist API
* @var \Aviat\AnimeClient\API\Anilist\Model
*/
protected $anilistModel;
/**
* Model for making requests to Kitsu API
* @var \Aviat\AnimeClient\API\Kitsu\Model
@ -43,12 +56,14 @@ class Manga extends API {
* Constructor
*
* @param ContainerInterface $container
* @throws \Aviat\Ion\Di\ContainerException
* @throws \Aviat\Ion\Di\NotFoundException
*/
public function __construct(ContainerInterface $container)
{
$this->anilistModel = $container->get('anilist-model');
$this->kitsuModel = $container->get('kitsu-model');
$config = $container->get('config');
$this->anilistEnabled = (bool)$config->get(['anilist', 'enabled']);
}
/**
@ -121,6 +136,11 @@ class Manga extends API {
$requester = new ParallelAPIRequest();
$requester->addRequest($this->kitsuModel->createListItem($data), 'kitsu');
if (array_key_exists('mal_id', $data) && $this->anilistEnabled)
{
$requester->addRequest($this->anilistModel->createListItem($data, 'MANGA'), 'anilist');
}
$results = $requester->makeRequests();
return count($results) > 0;
@ -137,6 +157,13 @@ class Manga extends API {
$requester = new ParallelAPIRequest();
$requester->addRequest($this->kitsuModel->updateListItem($data), 'kitsu');
$array = $data->toArray();
if (array_key_exists('mal_id', $array) && $this->anilistEnabled)
{
$requester->addRequest($this->anilistModel->updateListItem($data, 'MANGA'), 'anilist');
}
$results = $requester->makeRequests();
$body = Json::decode($results['kitsu']);
$statusCode = array_key_exists('error', $body) ? 400: 200;
@ -147,6 +174,34 @@ class Manga extends API {
];
}
/**
* Increase the progress of a list entry
*
* @param MangaFormItem $data
* @return array
*/
public function incrementLibraryItem(MangaFormItem $data): array
{
$requester = new ParallelAPIRequest();
$requester->addRequest($this->kitsuModel->incrementListItem($data), 'kitsu');
$array = $data->toArray();
if (array_key_exists('mal_id', $array) && $this->anilistEnabled)
{
$requester->addRequest($this->anilistModel->incrementListItem($data, 'MANGA'), 'anilist');
}
$results = $requester->makeRequests();
$body = Json::decode($results['kitsu']);
$statusCode = array_key_exists('error', $body) ? 400 : 200;
return [
'body' => Json::decode($results['kitsu']),
'statusCode' => $statusCode
];
}
/**
* Delete a list entry
*
@ -159,6 +214,11 @@ class Manga extends API {
$requester = new ParallelAPIRequest();
$requester->addRequest($this->kitsuModel->deleteListItem($id), 'kitsu');
if ($malId !== null && $this->anilistEnabled)
{
$requester->addRequest($this->anilistModel->deleteListItem($malId, 'MANGA'), 'anilist');
}
$results = $requester->makeRequests();
return count($results) > 0;