HummingBirdAnimeClient/src/AnimeClient/API/Kitsu/Model.php

517 lines
11 KiB
PHP
Raw Normal View History

2016-12-21 12:46:20 -05:00
<?php declare(strict_types=1);
/**
2017-02-15 16:13:32 -05:00
* Hummingbird Anime List Client
2016-12-21 12:46:20 -05:00
*
2018-08-22 13:48:27 -04:00
* An API client for Kitsu to manage anime and manga watch lists
2016-12-21 12:46:20 -05:00
*
* PHP version 7.4
2016-12-21 12:46:20 -05:00
*
2017-02-15 16:13:32 -05:00
* @package HummingbirdAnimeClient
2017-01-06 23:34:56 -05:00
* @author Timothy J. Warren <tim@timshomepage.net>
2020-01-08 15:39:49 -05:00
* @copyright 2015 - 2020 Timothy J. Warren
2017-01-06 23:34:56 -05:00
* @license http://www.opensource.org/licenses/mit-license.html MIT License
2020-08-04 09:30:21 -04:00
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu;
2016-12-21 12:46:20 -05:00
use function Amp\Promise\wait;
2020-10-09 16:18:45 -04:00
use function Aviat\AnimeClient\getApiClient;
2017-03-07 17:51:08 -05:00
2020-10-09 16:18:45 -04:00
use Amp;
2020-03-11 16:26:17 -04:00
use Amp\Http\Client\Request;
2020-08-26 15:22:14 -04:00
use Aviat\AnimeClient\Kitsu as K;
use Aviat\AnimeClient\API\{
CacheTrait,
ParallelAPIRequest
};
2017-01-03 21:06:49 -05:00
use Aviat\AnimeClient\API\Kitsu\Transformer\{
2017-02-04 15:18:34 -05:00
AnimeTransformer,
2020-08-24 13:07:47 -04:00
LibraryEntryTransformer,
2017-02-04 15:18:34 -05:00
MangaTransformer,
2017-01-03 21:06:49 -05:00
};
2019-12-09 14:34:23 -05:00
use Aviat\Banker\Exception\InvalidArgumentException;
use Aviat\Ion\{Di\ContainerAware, Json};
2016-12-21 12:46:20 -05:00
2019-12-09 14:34:23 -05:00
use Throwable;
2016-12-21 12:46:20 -05:00
/**
* Kitsu API Model
*/
final class Model {
use CacheTrait;
use ContainerAware;
2020-08-06 09:39:12 -04:00
use RequestBuilderTrait;
use AnimeTrait;
use MangaTrait;
use MutationTrait;
2016-12-21 12:46:20 -05:00
2020-10-09 16:18:45 -04:00
protected const LIST_PAGE_SIZE = 100;
/**
* @var ListItem
*/
2020-07-28 16:11:13 -04:00
protected ListItem $listItem;
2017-02-04 15:18:34 -05:00
/**
2017-02-17 11:37:22 -05:00
* Constructor
*
* @param ListItem $listItem
*/
public function __construct(ListItem $listItem)
2016-12-21 12:46:20 -05:00
{
$this->animeTransformer = new AnimeTransformer();
2017-01-04 13:16:58 -05:00
$this->mangaTransformer = new MangaTransformer();
2018-11-09 10:38:35 -05:00
$this->listItem = $listItem;
2016-12-21 12:46:20 -05:00
}
2017-03-28 14:34:33 -04:00
/**
* Get the access token from the Kitsu API
*
* @param string $username
* @param string $password
* @return bool|array
2019-12-09 14:34:23 -05:00
* @throws Throwable
2017-03-28 14:34:33 -04:00
*/
public function authenticate(string $username, string $password)
{
// K::AUTH_URL
$response = $this->requestBuilder->getResponse('POST', K::AUTH_URL, [
'headers' => [
'accept' => NULL,
'Content-type' => 'application/x-www-form-urlencoded',
'client_id' => NULL,
'client_secret' => NULL
],
2017-03-28 14:34:33 -04:00
'form_params' => [
'grant_type' => 'password',
'username' => $username,
'password' => $password
]
]);
2020-03-11 16:26:17 -04:00
$data = Json::decode(wait($response->getBody()->buffer()));
2018-01-16 14:58:07 -05:00
if (array_key_exists('error', $data))
{
2020-08-17 10:23:32 -04:00
dump([
'method' => __CLASS__ . '\\' . __METHOD__,
'error' => $data['error'],
'response' => $response,
]);
die();
}
2018-10-05 14:32:05 -04:00
if (array_key_exists('access_token', $data))
{
return $data;
}
2017-03-28 14:34:33 -04:00
return FALSE;
}
/**
* Extend the current session with a refresh token
*
* @param string $token
* @return bool|array
2019-12-09 14:34:23 -05:00
* @throws Throwable
*/
public function reAuthenticate(string $token)
{
$response = $this->requestBuilder->getResponse('POST', K::AUTH_URL, [
'headers' => [
'accept' => NULL,
'Content-type' => 'application/x-www-form-urlencoded',
'Accept-encoding' => '*'
],
'form_params' => [
'grant_type' => 'refresh_token',
'refresh_token' => $token
]
]);
2020-03-11 16:26:17 -04:00
$data = Json::decode(wait($response->getBody()->buffer()));
if (array_key_exists('error', $data))
{
2020-08-17 10:23:32 -04:00
dump([
'method' => __CLASS__ . '\\' . __METHOD__,
'error' => $data['error'],
'response' => $response,
]);
die();
}
if (array_key_exists('access_token', $data))
{
return $data;
}
return FALSE;
}
/**
* Get the userid for a username from Kitsu
*
* @param string $username
* @return string
2019-12-09 14:34:23 -05:00
* @throws InvalidArgumentException
2020-05-08 19:18:10 -04:00
* @throws Throwable
*/
2017-03-08 12:55:49 -05:00
public function getUserIdByUsername(string $username = NULL): string
{
2018-02-02 09:50:58 -05:00
if ($username === NULL)
2017-01-27 12:35:28 -05:00
{
$username = $this->getUsername();
}
2017-02-04 15:18:34 -05:00
2020-05-08 19:15:21 -04:00
return $this->getCached(K::AUTH_USER_ID_KEY, function(string $username) {
$data = $this->requestBuilder->getRequest('users', [
'query' => [
'filter' => [
'name' => $username
]
]
]);
2020-05-08 19:15:21 -04:00
return $data['data'][0]['id'] ?? NULL;
}, [$username]);
}
2017-03-08 12:55:49 -05:00
/**
* Get information about a character
*
* @param string $slug
* @return array
*/
public function getCharacter(string $slug): array
{
return $this->requestBuilder->runQuery('CharacterDetails', [
'slug' => $slug
2017-03-08 12:55:49 -05:00
]);
}
/**
* Get information about a person
*
* @param string $slug
* @return array
2019-12-09 14:34:23 -05:00
* @throws InvalidArgumentException
*/
public function getPerson(string $slug): array
{
return $this->getCached("kitsu-person-{$slug}", fn () => $this->requestBuilder->runQuery('PersonDetails', [
'slug' => $slug
2020-05-08 19:18:10 -04:00
]));
}
2017-03-08 13:46:50 -05:00
/**
* Get profile information for the configured user
*
* @param string $username
* @return array
*/
2017-03-08 12:55:49 -05:00
public function getUserData(string $username): array
{
return $this->requestBuilder->runQuery('UserDetails', [
'slug' => $username,
2017-03-08 12:55:49 -05:00
]);
}
2016-12-21 12:46:20 -05:00
/**
2017-03-28 14:34:33 -04:00
* Search for an anime or manga
2016-12-21 12:46:20 -05:00
*
2017-03-28 14:34:33 -04:00
* @param string $type - 'anime' or 'manga'
* @param string $query - name of the item to search for
* @return array
2016-12-21 12:46:20 -05:00
*/
2017-03-28 14:34:33 -04:00
public function search(string $type, string $query): array
2016-12-21 12:46:20 -05:00
{
2020-10-21 15:45:30 -04:00
$uType = ucfirst(strtolower($type));
$raw = $this->requestBuilder->runQuery("Search{$uType}", [
'query' => $query,
]);
2016-12-21 12:46:20 -05:00
2020-10-21 15:45:30 -04:00
$nodes = $raw['data']["search{$uType}ByTitle"]['nodes'];
$data = [];
2020-10-21 15:45:30 -04:00
foreach ($nodes as $item)
{
2020-10-21 15:45:30 -04:00
$searchItem = [
'id' => $item['id'],
'slug' => $item['slug'],
'canonicalTitle' => $item['titles']['canonical'],
'titles' => array_values(K::getTitles($item['titles'])),
];
2020-10-21 15:45:30 -04:00
// Search for MAL mapping
if (is_array($item['mappings']['nodes']))
{
2020-10-21 15:45:30 -04:00
foreach($item['mappings']['nodes'] as $mapping)
{
2020-10-21 15:45:30 -04:00
if ($mapping['externalSite'] === "MYANIMELIST_" . strtoupper($type))
{
$searchItem['mal_id'] = $mapping['externalId'];
break;
}
}
}
2020-10-21 15:45:30 -04:00
$data[] = $searchItem;
2016-12-21 12:46:20 -05:00
}
2020-10-21 15:45:30 -04:00
return $data;
2016-12-21 12:46:20 -05:00
}
/**
* Find a media item on Kitsu by its associated MAL id
*
* @param string $malId
* @param string $type "anime" or "manga"
2017-04-10 15:31:35 -04:00
* @return string|NULL
*/
2018-11-09 10:38:35 -05:00
public function getKitsuIdFromMALId(string $malId, string $type='anime'): ?string
{
$raw = $this->requestBuilder->runQuery('GetIdByMapping', [
'id' => $malId,
'site' => strtoupper("MYANIMELIST_{$type}"),
]);
2017-04-10 15:31:35 -04:00
return $raw['data']['lookupMapping']['id'] ?? NULL;
}
/**
* Get the data for a specific list item, generally for editing
*
* @param string $listId - The unique identifier of that list item
* @return mixed
*/
public function getListItem(string $listId)
{
$baseData = $this->listItem->get($listId);
2020-08-24 13:07:47 -04:00
if ( ! isset($baseData['data']['findLibraryEntryById']))
{
2020-08-24 13:07:47 -04:00
return [];
}
2018-10-19 10:40:11 -04:00
2020-08-24 13:07:47 -04:00
return (new LibraryEntryTransformer())->transform($baseData['data']['findLibraryEntryById']);
}
/**
2020-07-28 16:11:13 -04:00
* Get the data to sync Kitsu anime/manga list with another API
*
2020-07-28 16:11:13 -04:00
* @param string $type
* @return array
* @throws InvalidArgumentException
* @throws Throwable
*/
public function getSyncList(string $type): array
{
$statuses = [
'CURRENT',
'PLANNED',
'ON_HOLD',
'DROPPED',
'COMPLETED',
];
$pages = [];
// Although I can fetch the whole list without segregating by status,
// this way is much faster...
foreach ($statuses as $status)
{
foreach ($this->getRawSyncListPages(strtoupper($type), $status) as $page)
{
$pages[] = $page;
}
}
return array_merge(...$pages);
}
2020-04-21 19:22:56 -04:00
/**
* Get the aggregated pages of anime or manga history
*
* @return array
*/
protected function getRawHistoryList(): array
2020-04-21 19:22:56 -04:00
{
return $this->requestBuilder->runQuery('GetUserHistory', [
'slug' => $this->getUsername(),
2020-04-21 19:22:56 -04:00
]);
}
2020-10-09 16:18:45 -04:00
/**
* Get the raw anime/manga list from GraphQL
*
* @param string $type
* @param string $status
* @return array
*/
public function getRawList(string $type, string $status = ''): array
{
$pages = [];
foreach ($this->getRawListPages(strtoupper($type), strtoupper($status)) as $page)
{
$pages[] = $page;
}
return array_merge(...$pages);
}
protected function getRawListPages(string $type, string $status = ''): ?\Generator
{
$items = $this->getRawPages($type, $status);
while (wait($items->advance()))
{
yield $items->getCurrent();
}
}
protected function getRawPages(string $type, string $status = ''): Amp\Iterator
{
$cursor = '';
$username = $this->getUsername();
return new Amp\Producer(function (callable $emit) use ($type, $status, $cursor, $username) {
while (TRUE)
{
$vars = [
'type' => $type,
'slug' => $username,
];
if ($status !== '')
{
$vars['status'] = $status;
}
if ($cursor !== '')
{
$vars['after'] = $cursor;
}
$request = $this->requestBuilder->queryRequest('GetLibrary', $vars);
$response = yield getApiClient()->request($request);
$json = yield $response->getBody()->buffer();
$rawData = Json::decode($json);
$data = $rawData['data']['findProfileBySlug']['library']['all'] ?? [];
$page = $data['pageInfo'];
if (empty($data))
{
dump($rawData);
die();
2020-10-09 16:18:45 -04:00
// @TODO Error logging
break;
}
$cursor = $page['endCursor'];
yield $emit($data['nodes']);
if ($page['hasNextPage'] === FALSE)
{
break;
}
}
});
}
protected function getRawSyncListPages(string $type, string $status): ?\Generator
{
$items = $this->getRawSyncPages($type, $status);
while (wait($items->advance()))
{
yield $items->getCurrent();
}
}
protected function getRawSyncPages(string $type, $status): Amp\Iterator {
$cursor = '';
$username = $this->getUsername();
return new Amp\Producer(function (callable $emit) use ($type, $status, $cursor, $username) {
while (TRUE)
{
$vars = [
'type' => $type,
'slug' => $username,
'status' => $status,
];
if ($cursor !== '')
{
$vars['after'] = $cursor;
}
$request = $this->requestBuilder->queryRequest('GetSyncLibrary', $vars);
$response = yield getApiClient()->request($request);
$json = yield $response->getBody()->buffer();
$rawData = Json::decode($json);
$data = $rawData['data']['findProfileBySlug']['library']['all'] ?? [];
$page = $data['pageInfo'];
if (empty($data))
{
dump($rawData);
die();
}
$cursor = $page['endCursor'];
yield $emit($data['nodes']);
if ($page['hasNextPage'] === FALSE)
{
break;
}
}
});
}
private function getUserId(): string
{
static $userId = NULL;
if ($userId === NULL)
{
$userId = $this->getUserIdByUsername($this->getUsername());
}
return $userId;
}
/**
* Get the kitsu username from config
*
* @return string
*/
private function getUsername(): string
{
return $this->getContainer()
->get('config')
->get(['kitsu_username']);
}
2017-02-04 15:18:34 -05:00
private function getListCount(string $type, string $status = ''): int
{
2020-08-25 13:22:38 -04:00
$args = [
'type' => strtoupper($type),
'slug' => $this->getUsername()
];
2020-08-25 13:22:38 -04:00
if ($status !== '')
{
2020-08-25 13:22:38 -04:00
$args['status'] = strtoupper($status);
}
2020-08-25 13:22:38 -04:00
$res = $this->requestBuilder->runQuery('GetLibraryCount', $args);
2020-08-25 13:22:38 -04:00
return $res['data']['findProfileBySlug']['library']['all']['totalCount'];
}
2016-12-21 12:46:20 -05:00
}