Version 5.1 - All the GraphQL #32
2
.gitignore
vendored
2
.gitignore
vendored
@ -147,3 +147,5 @@ public/images/characters/**
|
||||
public/images/people/**
|
||||
public/mal_mappings.json
|
||||
.phpunit.result.cache
|
||||
|
||||
.is-dev
|
@ -25,10 +25,11 @@ use Aviat\AnimeClient\API\{
|
||||
Kitsu\KitsuRequestBuilder
|
||||
};
|
||||
use Aviat\AnimeClient\Model;
|
||||
use Aviat\Banker\Pool;
|
||||
use Aviat\Banker\Teller;
|
||||
use Aviat\Ion\Config;
|
||||
use Aviat\Ion\Di\Container;
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use Laminas\Diactoros\{Response, ServerRequestFactory};
|
||||
use Monolog\Handler\RotatingFileHandler;
|
||||
use Monolog\Logger;
|
||||
@ -64,10 +65,10 @@ return static function (array $configArray = []): Container {
|
||||
$container->set('config', fn () => new Config($configArray));
|
||||
|
||||
// Create Cache Object
|
||||
$container->set('cache', static function(ContainerInterface $container): Pool {
|
||||
$container->set('cache', static function(ContainerInterface $container): CacheInterface {
|
||||
$logger = $container->getLogger();
|
||||
$config = $container->get('config')->get('cache');
|
||||
return new Pool($config, $logger);
|
||||
return new Teller($config, $logger);
|
||||
});
|
||||
|
||||
// Create Aura Router Object
|
||||
@ -113,7 +114,7 @@ return static function (array $configArray = []): Container {
|
||||
|
||||
// Models
|
||||
$container->set('kitsu-model', static function(ContainerInterface $container): Kitsu\Model {
|
||||
$requestBuilder = new KitsuRequestBuilder();
|
||||
$requestBuilder = new KitsuRequestBuilder($container);
|
||||
$requestBuilder->setLogger($container->getLogger('kitsu-request'));
|
||||
|
||||
$listItem = new Kitsu\ListItem();
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
# See https://git.timshomepage.net/aviat/banker for more information
|
||||
|
||||
# Available drivers are apcu, memcache, memcached, redis or null
|
||||
# Available drivers are memcached, redis or null
|
||||
# Null cache driver means no caching
|
||||
driver = "redis"
|
||||
|
||||
|
@ -30,7 +30,15 @@
|
||||
|
||||
<?php if ($item['rewatched'] > 0): ?>
|
||||
<div class="row">
|
||||
<div>Rewatched <?= $item['rewatched'] ?> time(s)</div>
|
||||
<?php if ($item['rewatched'] == 1): ?>
|
||||
<div>Rewatched once</div>
|
||||
<?php elseif ($item['rewatched'] == 2): ?>
|
||||
<div>Rewatched twice</div>
|
||||
<?php elseif ($item['rewatched'] == 3): ?>
|
||||
<div>Rewatched thrice</div>
|
||||
<?php else: ?>
|
||||
<div>Rewatched <?= $item['rewatched'] ?> times</div>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
|
@ -45,7 +45,7 @@
|
||||
</aside>
|
||||
<article class="text">
|
||||
<h2 class="toph"><a rel="external" href="<?= $data['url'] ?>"><?= $data['title'] ?></a></h2>
|
||||
<?php foreach ($data['titles'] as $title): ?>
|
||||
<?php foreach ($data['titles_more'] as $title): ?>
|
||||
<h3><?= $title ?></h3>
|
||||
<?php endforeach ?>
|
||||
<br />
|
||||
|
@ -86,7 +86,17 @@
|
||||
<br />
|
||||
|
||||
<ul>
|
||||
<?php if ($item['rewatched'] > 0): ?>li>Rewatched <?= $item['rewatched'] ?> time(s)</li><?php endif ?>
|
||||
<?php if ($item['rewatched'] > 0): ?>
|
||||
<?php if ($item['rewatched'] == 1): ?>
|
||||
<li>Rewatched once</li>
|
||||
<?php elseif ($item['rewatched'] == 2): ?>
|
||||
<li>Rewatched twice</li>
|
||||
<?php elseif ($item['rewatched'] == 3): ?>
|
||||
<li>Rewatched thrice</li>
|
||||
<?php else: ?>
|
||||
<li>Rewatched <?= $item['rewatched'] ?> times</li>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
<?php foreach(['private','rewatching'] as $attr): ?>
|
||||
<?php if($item[$attr]): ?><li><?= ucfirst($attr); ?></li><?php endif ?>
|
||||
<?php endforeach ?>
|
||||
|
@ -35,7 +35,7 @@
|
||||
<td><?= $item['episode_length'] ?></td>
|
||||
<td><?= $item['show_type'] ?></td>
|
||||
<td><?= $item['age_rating'] ?></td>
|
||||
<td class="align-left"><?= $item['notes'] ?></td>
|
||||
<td class="align-left"><?= nl2br($item['notes'], TRUE) ?></td>
|
||||
<td class="align-left"><?= implode(', ', $item['genres']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
|
@ -11,10 +11,10 @@
|
||||
</a>
|
||||
<?= ! empty($item['alternate_title']) ? ' <br /><small> ' . $item['alternate_title'] . '</small>' : '' ?>
|
||||
</td>
|
||||
<td><?= $item['episode_count'] ?></td>
|
||||
<td><?= ($item['episode_count'] > 1) ? $item['episode_count'] : '-' ?></td>
|
||||
<td><?= $item['episode_length'] ?></td>
|
||||
<td><?= $item['show_type'] ?></td>
|
||||
<td><?= $item['age_rating'] ?></td>
|
||||
<?php if ($hasNotes): ?><td class="align-left"><?= $item['notes'] ?></td><?php endif ?>
|
||||
<?php if ($hasNotes): ?><td class="align-left"><?= nl2br($item['notes'], TRUE) ?></td><?php endif ?>
|
||||
<td class="align-left"><?= implode(', ', $item['genres']) ?></td>
|
||||
</tr>
|
@ -14,7 +14,8 @@ $hasManga = stripos($_SERVER['REQUEST_URI'], 'manga') !== FALSE;
|
||||
<?php if(strpos($route_path, 'collection') === FALSE): ?>
|
||||
<?= $helper->a(
|
||||
$urlGenerator->defaultUrl($url_type),
|
||||
$whose . ucfirst($url_type) . ' List'
|
||||
$whose . ucfirst($url_type) . ' List',
|
||||
['aria-current'=> 'page']
|
||||
) ?>
|
||||
<?php if($config->get("show_{$url_type}_collection")): ?>
|
||||
[<?= $helper->a(
|
||||
@ -35,7 +36,8 @@ $hasManga = stripos($_SERVER['REQUEST_URI'], 'manga') !== FALSE;
|
||||
<?php else: ?>
|
||||
<?= $helper->a(
|
||||
$url->generate("{$url_type}.collection.view") . $extraSegment,
|
||||
$whose . ucfirst($url_type) . ' Collection'
|
||||
$whose . ucfirst($url_type) . ' Collection',
|
||||
['aria-current'=> 'page']
|
||||
) ?>
|
||||
<?php if($config->get("show_{$other_type}_collection")): ?>
|
||||
[<?= $helper->a(
|
||||
@ -79,15 +81,22 @@ $hasManga = stripos($_SERVER['REQUEST_URI'], 'manga') !== FALSE;
|
||||
</span>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?php if ($container->get('util')->isViewPage() && ($hasAnime || $hasManga)): ?>
|
||||
<nav>
|
||||
<?php if ($container->get('util')->isViewPage() && ($hasAnime || $hasManga)): ?>
|
||||
<?= $helper->menu($menu_name) ?>
|
||||
<?php if (stripos($_SERVER['REQUEST_URI'], 'history') === FALSE): ?>
|
||||
<br />
|
||||
<ul>
|
||||
<li class="<?= Util::isNotSelected('list', $lastSegment) ?>"><a href="<?= $urlGenerator->url($route_path) ?>">Cover View</a></li>
|
||||
<li class="<?= Util::isSelected('list', $lastSegment) ?>"><a href="<?= $urlGenerator->url("{$route_path}/list") ?>">List View</a></li>
|
||||
<?php $currentView = Util::eq('list', $lastSegment) ? 'list' : 'cover' ?>
|
||||
<li class="<?= Util::isNotSelected('list', $lastSegment) ?>">
|
||||
<a aria-current="<?= Util::ariaCurrent($currentView === 'cover') ?>"
|
||||
href="<?= $urlGenerator->url($route_path) ?>">Cover View</a>
|
||||
</li>
|
||||
<li class="<?= Util::isSelected('list', $lastSegment) ?>">
|
||||
<a aria-current="<?= Util::ariaCurrent($currentView === 'list') ?>"
|
||||
href="<?= $urlGenerator->url("{$route_path}/list") ?>">List View</a>
|
||||
</li>
|
||||
</ul>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
</nav>
|
||||
<?php endif ?>
|
||||
|
@ -68,7 +68,15 @@
|
||||
|
||||
<?php if ($item['reread'] > 0): ?>
|
||||
<div class="row">
|
||||
<div>Reread <?= $item['reread'] ?> time(s)</div>
|
||||
<?php if ($item['reread'] == 1): ?>
|
||||
<div>Reread once</div>
|
||||
<?php elseif ($item['reread'] == 2): ?>
|
||||
<div>Reread twice</div>
|
||||
<?php elseif ($item['reread'] == 3): ?>
|
||||
<div>Reread thrice</div>
|
||||
<?php else: ?>
|
||||
<div>Reread <?= $item['reread'] ?> times</div>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
|
@ -53,8 +53,14 @@
|
||||
<td><?= $item['volumes']['total'] ?></td>
|
||||
<td>
|
||||
<ul>
|
||||
<?php if ($item['reread'] > 0): ?>
|
||||
<li>Reread <?= $item['reread'] ?> time(s)</li>
|
||||
<?php if ($item['reread'] == 1): ?>
|
||||
<li>Reread once</li>
|
||||
<?php elseif ($item['reread'] == 2): ?>
|
||||
<li>Reread twice</li>
|
||||
<?php elseif ($item['reread'] == 3): ?>
|
||||
<li>Reread thrice</li>
|
||||
<?php elseif ($item['reread'] > 3): ?>
|
||||
<li>Reread <?= $item['reread'] ?> times</li>
|
||||
<?php endif ?>
|
||||
<?php foreach(['rereading'] as $attr): ?>
|
||||
<?php if($item[$attr]): ?>
|
||||
|
5
app/views/settings/_field.php
Normal file
5
app/views/settings/_field.php
Normal file
@ -0,0 +1,5 @@
|
||||
<article>
|
||||
<label for="<?= $fieldName ?>"><?= $field['title'] ?></label><br />
|
||||
<small><?= $field['description'] ?></small><br />
|
||||
<?= $helper->field($fieldName, $field); ?>
|
||||
</article>
|
@ -6,19 +6,19 @@
|
||||
?>
|
||||
|
||||
<?php foreach ($fields as $name => $field): ?>
|
||||
<?php $fieldname = ($section === 'config' || $nestedPrefix !== 'config') ? "{$nestedPrefix}[{$name}]" : "{$nestedPrefix}[{$section}][{$name}]"; ?>
|
||||
<?php
|
||||
$fieldName = ($section === 'config' || $nestedPrefix !== 'config')
|
||||
? "{$nestedPrefix}[{$name}]"
|
||||
: "{$nestedPrefix}[{$section}][{$name}]";
|
||||
?>
|
||||
<?php if ($field['type'] === 'subfield'): ?>
|
||||
<section>
|
||||
<h4><?= $field['title'] ?></h4>
|
||||
<?php include_once '_form.php'; ?>
|
||||
<?php include '_subfield.php'; ?>
|
||||
</section>
|
||||
<?php elseif ( ! empty($field['display'])): ?>
|
||||
<article>
|
||||
<label for="<?= $fieldname ?>"><?= $field['title'] ?></label><br />
|
||||
<small><?= $field['description'] ?></small><br />
|
||||
<?= $helper->field($fieldname, $field); ?>
|
||||
</article>
|
||||
<?php include '_field.php' ?>
|
||||
<?php else: ?>
|
||||
<?php $hiddenFields[] = $helper->field($fieldname, $field); ?>
|
||||
<?php $hiddenFields[] = $helper->field($fieldName, $field); ?>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
|
20
app/views/settings/_subfield.php
Normal file
20
app/views/settings/_subfield.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
// Higher scoped variables:
|
||||
// $field
|
||||
// $fields
|
||||
// $hiddenFields
|
||||
// $nestedPrefix
|
||||
?>
|
||||
|
||||
<?php foreach ($field['fields'] as $name => $field): ?>
|
||||
<?php
|
||||
$fieldName = ($section === 'config' || $nestedPrefix !== 'config')
|
||||
? "{$nestedPrefix}[{$name}]"
|
||||
: "{$nestedPrefix}[{$section}][{$name}]";
|
||||
?>
|
||||
<?php if ( ! empty($field['display'])): ?>
|
||||
<?php include '_field.php' ?>
|
||||
<?php else: ?>
|
||||
<?php $hiddenFields[] = $helper->field($fieldName, $field); ?>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
@ -38,7 +38,7 @@
|
||||
"aura/html": "^2.5.0",
|
||||
"aura/router": "^3.1.0",
|
||||
"aura/session": "^2.1.0",
|
||||
"aviat/banker": "^2.0.0",
|
||||
"aviat/banker": "^3.1.1",
|
||||
"aviat/query": "^3.0.0",
|
||||
"danielstjules/stringy": "^3.1.0",
|
||||
"ext-dom": "*",
|
||||
|
@ -27,7 +27,7 @@ setlocale(LC_CTYPE, 'en_US');
|
||||
// Load composer autoloader
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
if (array_key_exists('ENV', $_SERVER) && $_SERVER['ENV'] === 'development')
|
||||
if (file_exists('.is-dev'))
|
||||
{
|
||||
$whoops = new Run;
|
||||
$whoops->pushHandler(new PrettyPageHandler);
|
||||
|
@ -21,7 +21,6 @@ use const Aviat\AnimeClient\USER_AGENT;
|
||||
use function Amp\Promise\wait;
|
||||
use function Aviat\AnimeClient\getResponse;
|
||||
|
||||
use Amp;
|
||||
use Amp\Http\Client\Request;
|
||||
use Amp\Http\Client\Body\FormBody;
|
||||
use Aviat\Ion\Json;
|
||||
@ -80,6 +79,8 @@ abstract class APIRequestBuilder {
|
||||
{
|
||||
$request = (new Request($uri));
|
||||
$request->setHeader('User-Agent', USER_AGENT);
|
||||
$request->setTcpConnectTimeout(300000);
|
||||
$request->setTransferTimeout(300000);
|
||||
|
||||
return $request;
|
||||
}
|
||||
@ -270,7 +271,7 @@ abstract class APIRequestBuilder {
|
||||
*/
|
||||
public function newRequest(string $type, string $uri): self
|
||||
{
|
||||
if ( ! \in_array($type, $this->validMethods, TRUE))
|
||||
if ( ! in_array($type, $this->validMethods, TRUE))
|
||||
{
|
||||
throw new InvalidArgumentException('Invalid HTTP method');
|
||||
}
|
||||
@ -328,6 +329,8 @@ abstract class APIRequestBuilder {
|
||||
$this->path = '';
|
||||
$this->query = '';
|
||||
$this->request = new Request($requestUrl, $type);
|
||||
$this->request->setInactivityTimeout(300000);
|
||||
$this->request->setTlsHandshakeTimeout(300000);
|
||||
$this->request->setTcpConnectTimeout(300000);
|
||||
$this->request->setTransferTimeout(300000);
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ class AnimeListTransformer extends AbstractTransformer {
|
||||
|
||||
public function transform($item): AnimeListItem
|
||||
{
|
||||
return new AnimeListItem([]);
|
||||
return AnimeListItem::from([]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -54,7 +54,7 @@ class AnimeListTransformer extends AbstractTransformer {
|
||||
'reconsuming' => $reconsuming,
|
||||
'status' => $reconsuming
|
||||
? KitsuStatus::WATCHING
|
||||
:AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']],
|
||||
: AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']],
|
||||
'updatedAt' => (new DateTime())
|
||||
->setTimestamp($item['updatedAt'])
|
||||
->format(DateTime::W3C)
|
||||
|
@ -17,6 +17,7 @@
|
||||
namespace Aviat\AnimeClient\API\Anilist\Transformer;
|
||||
|
||||
use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Anilist as AnilistStatus;
|
||||
use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Kitsu as KitsuStatus;
|
||||
use Aviat\AnimeClient\API\Mapping\MangaReadingStatus;
|
||||
use Aviat\AnimeClient\Types\MangaListItem;
|
||||
use Aviat\AnimeClient\Types\FormItem;
|
||||
@ -40,6 +41,8 @@ class MangaListTransformer extends AbstractTransformer {
|
||||
*/
|
||||
public function untransform(array $item): FormItem
|
||||
{
|
||||
$reconsuming = $item['status'] === AnilistStatus::REPEATING;
|
||||
|
||||
return FormItem::from([
|
||||
'id' => $item['id'],
|
||||
'mal_id' => $item['media']['idMal'],
|
||||
@ -49,8 +52,10 @@ class MangaListTransformer extends AbstractTransformer {
|
||||
'progress' => $item['progress'],
|
||||
'rating' => $item['score'],
|
||||
'reconsumeCount' => $item['repeat'],
|
||||
'reconsuming' => $item['status'] === AnilistStatus::REPEATING,
|
||||
'status' => MangaReadingStatus::ANILIST_TO_KITSU[$item['status']],
|
||||
'reconsuming' => $reconsuming,
|
||||
'status' => $reconsuming
|
||||
? KitsuStatus::READING
|
||||
: MangaReadingStatus::ANILIST_TO_KITSU[$item['status']],
|
||||
'updatedAt' => (new DateTime())
|
||||
->setTimestamp($item['updatedAt'])
|
||||
->format(DateTime::W3C),
|
||||
|
@ -16,7 +16,8 @@
|
||||
|
||||
namespace Aviat\AnimeClient\API;
|
||||
|
||||
use Aviat\Banker\Pool;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use Psr\SimpleCache\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Helper methods for dealing with the Cache
|
||||
@ -24,17 +25,17 @@ use Aviat\Banker\Pool;
|
||||
trait CacheTrait {
|
||||
|
||||
/**
|
||||
* @var Pool
|
||||
* @var CacheInterface
|
||||
*/
|
||||
protected Pool $cache;
|
||||
protected CacheInterface $cache;
|
||||
|
||||
/**
|
||||
* Inject the cache object
|
||||
*
|
||||
* @param Pool $cache
|
||||
* @param CacheInterface $cache
|
||||
* @return $this
|
||||
*/
|
||||
public function setCache(Pool $cache): self
|
||||
public function setCache(CacheInterface $cache): self
|
||||
{
|
||||
$this->cache = $cache;
|
||||
return $this;
|
||||
@ -43,13 +44,41 @@ trait CacheTrait {
|
||||
/**
|
||||
* Get the cache object if it exists
|
||||
*
|
||||
* @return Pool
|
||||
* @return CacheInterface
|
||||
*/
|
||||
public function getCache(): Pool
|
||||
public function getCache(): CacheInterface
|
||||
{
|
||||
return $this->cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cached value if it exists, otherwise set the cache value
|
||||
* and return it.
|
||||
*
|
||||
* @param string $key
|
||||
* @param callable $primer
|
||||
* @param array $primeArgs
|
||||
* @return mixed|null
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function getCached(string $key, callable $primer, ?array $primeArgs = [])
|
||||
{
|
||||
$value = $this->cache->get($key, NULL);
|
||||
|
||||
if ($value === NULL)
|
||||
{
|
||||
$value = $primer(...$primeArgs);
|
||||
if ($value === NULL)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$this->cache->set($key, $value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a hash as a cache key from the current method call
|
||||
*
|
||||
@ -61,7 +90,7 @@ trait CacheTrait {
|
||||
public function getHashForMethodCall($object, string $method, array $args = []): string
|
||||
{
|
||||
$keyObj = [
|
||||
'class' => \get_class($object),
|
||||
'class' => get_class($object),
|
||||
'method' => $method,
|
||||
'args' => $args,
|
||||
];
|
||||
|
@ -28,6 +28,8 @@ final class Kitsu {
|
||||
public const AUTH_TOKEN_CACHE_KEY = 'kitsu-auth-token';
|
||||
public const AUTH_TOKEN_EXP_CACHE_KEY = 'kitsu-auth-token-expires';
|
||||
public const AUTH_TOKEN_REFRESH_CACHE_KEY = 'kitsu-auth-token-refresh';
|
||||
public const ANIME_HISTORY_LIST_CACHE_KEY = 'kitsu-anime-history-list';
|
||||
public const MANGA_HISTORY_LIST_CACHE_KEY = 'kitsu-manga-history-list';
|
||||
|
||||
/**
|
||||
* Determine whether an anime is airing, finished airing, or has not yet aired
|
||||
@ -58,73 +60,6 @@ final class Kitsu {
|
||||
return AnimeAiringStatus::NOT_YET_AIRED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name and logo for the streaming service of the current link
|
||||
*
|
||||
* @param string $hostname
|
||||
* @return array
|
||||
*/
|
||||
protected static function getServiceMetaData(string $hostname = NULL): array
|
||||
{
|
||||
$hostname = str_replace('www.', '', $hostname);
|
||||
|
||||
$serviceMap = [
|
||||
'amazon.com' => [
|
||||
'name' => 'Amazon Prime',
|
||||
'link' => TRUE,
|
||||
'image' => 'streaming-logos/amazon.svg',
|
||||
],
|
||||
'crunchyroll.com' => [
|
||||
'name' => 'Crunchyroll',
|
||||
'link' => TRUE,
|
||||
'image' => 'streaming-logos/crunchyroll.svg',
|
||||
],
|
||||
'daisuki.net' => [
|
||||
'name' => 'Daisuki',
|
||||
'link' => TRUE,
|
||||
'image' => 'streaming-logos/daisuki.svg'
|
||||
],
|
||||
'funimation.com' => [
|
||||
'name' => 'Funimation',
|
||||
'link' => TRUE,
|
||||
'image' => 'streaming-logos/funimation.svg',
|
||||
],
|
||||
'hidive.com' => [
|
||||
'name' => 'Hidive',
|
||||
'link' => TRUE,
|
||||
'image' => 'streaming-logos/hidive.svg',
|
||||
],
|
||||
'hulu.com' => [
|
||||
'name' => 'Hulu',
|
||||
'link' => TRUE,
|
||||
'image' => 'streaming-logos/hulu.svg',
|
||||
],
|
||||
'tubitv.com' => [
|
||||
'name' => 'TubiTV',
|
||||
'link' => TRUE,
|
||||
'image' => 'streaming-logos/tubitv.svg',
|
||||
],
|
||||
'viewster.com' => [
|
||||
'name' => 'Viewster',
|
||||
'link' => TRUE,
|
||||
'image' => 'streaming-logos/viewster.svg'
|
||||
],
|
||||
];
|
||||
|
||||
if (array_key_exists($hostname, $serviceMap))
|
||||
{
|
||||
return $serviceMap[$hostname];
|
||||
}
|
||||
|
||||
// Default to Netflix, because the API links are broken,
|
||||
// and there's no other real identifier for Netflix
|
||||
return [
|
||||
'name' => 'Netflix',
|
||||
'link' => FALSE,
|
||||
'image' => 'streaming-logos/netflix.svg',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorganize streaming links
|
||||
*
|
||||
@ -195,6 +130,23 @@ final class Kitsu {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of titles
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public static function getTitles(array $data): array
|
||||
{
|
||||
$raw = array_unique([
|
||||
$data['canonicalTitle'],
|
||||
...array_values($data['titles']),
|
||||
...array_values($data['abbreviatedTitles'] ?? []),
|
||||
]);
|
||||
|
||||
return array_diff($raw,[$data['canonicalTitle']]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out duplicate and very similar names from
|
||||
*
|
||||
@ -206,7 +158,7 @@ final class Kitsu {
|
||||
// The 'canonical' title is always returned
|
||||
$valid = [$data['canonicalTitle']];
|
||||
|
||||
if (array_key_exists('titles', $data))
|
||||
if (array_key_exists('titles', $data) && is_array($data['titles']))
|
||||
{
|
||||
foreach($data['titles'] as $alternateTitle)
|
||||
{
|
||||
@ -220,6 +172,74 @@ final class Kitsu {
|
||||
return $valid;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the name and logo for the streaming service of the current link
|
||||
*
|
||||
* @param string $hostname
|
||||
* @return array
|
||||
*/
|
||||
protected static function getServiceMetaData(string $hostname = NULL): array
|
||||
{
|
||||
$hostname = str_replace('www.', '', $hostname);
|
||||
|
||||
$serviceMap = [
|
||||
'amazon.com' => [
|
||||
'name' => 'Amazon Prime',
|
||||
'link' => TRUE,
|
||||
'image' => 'streaming-logos/amazon.svg',
|
||||
],
|
||||
'crunchyroll.com' => [
|
||||
'name' => 'Crunchyroll',
|
||||
'link' => TRUE,
|
||||
'image' => 'streaming-logos/crunchyroll.svg',
|
||||
],
|
||||
'daisuki.net' => [
|
||||
'name' => 'Daisuki',
|
||||
'link' => TRUE,
|
||||
'image' => 'streaming-logos/daisuki.svg'
|
||||
],
|
||||
'funimation.com' => [
|
||||
'name' => 'Funimation',
|
||||
'link' => TRUE,
|
||||
'image' => 'streaming-logos/funimation.svg',
|
||||
],
|
||||
'hidive.com' => [
|
||||
'name' => 'Hidive',
|
||||
'link' => TRUE,
|
||||
'image' => 'streaming-logos/hidive.svg',
|
||||
],
|
||||
'hulu.com' => [
|
||||
'name' => 'Hulu',
|
||||
'link' => TRUE,
|
||||
'image' => 'streaming-logos/hulu.svg',
|
||||
],
|
||||
'tubitv.com' => [
|
||||
'name' => 'TubiTV',
|
||||
'link' => TRUE,
|
||||
'image' => 'streaming-logos/tubitv.svg',
|
||||
],
|
||||
'viewster.com' => [
|
||||
'name' => 'Viewster',
|
||||
'link' => TRUE,
|
||||
'image' => 'streaming-logos/viewster.svg'
|
||||
],
|
||||
];
|
||||
|
||||
if (array_key_exists($hostname, $serviceMap))
|
||||
{
|
||||
return $serviceMap[$hostname];
|
||||
}
|
||||
|
||||
// Default to Netflix, because the API links are broken,
|
||||
// and there's no other real identifier for Netflix
|
||||
return [
|
||||
'name' => 'Netflix',
|
||||
'link' => FALSE,
|
||||
'image' => 'streaming-logos/netflix.svg',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an alternate title is unique enough to list
|
||||
*
|
||||
|
@ -18,7 +18,6 @@ namespace Aviat\AnimeClient\API\Kitsu;
|
||||
|
||||
use Aura\Session\Segment;
|
||||
|
||||
use Aviat\Banker\Exception\InvalidArgumentException;
|
||||
use const Aviat\AnimeClient\SESSION_SEGMENT;
|
||||
|
||||
use Aviat\AnimeClient\API\{
|
||||
@ -27,6 +26,9 @@ use Aviat\AnimeClient\API\{
|
||||
};
|
||||
use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
|
||||
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
|
||||
use Aviat\Ion\Event;
|
||||
|
||||
use Psr\SimpleCache\InvalidArgumentException;
|
||||
|
||||
use Throwable;
|
||||
|
||||
@ -65,6 +67,8 @@ final class Auth {
|
||||
$this->segment = $container->get('session')
|
||||
->getSegment(SESSION_SEGMENT);
|
||||
$this->model = $container->get('kitsu-model');
|
||||
|
||||
Event::on('::unauthorized::', [$this, 'reAuthenticate'], []);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -73,7 +77,6 @@ final class Auth {
|
||||
*
|
||||
* @param string $password
|
||||
* @return boolean
|
||||
* @throws InvalidArgumentException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function authenticate(string $password): bool
|
||||
@ -83,85 +86,39 @@ final class Auth {
|
||||
|
||||
$auth = $this->model->authenticate($username, $password);
|
||||
|
||||
if (FALSE !== $auth)
|
||||
{
|
||||
// Set the token in the cache for command line operations
|
||||
$cacheItem = $this->cache->getItem(K::AUTH_TOKEN_CACHE_KEY);
|
||||
$cacheItem->set($auth['access_token']);
|
||||
$cacheItem->save();
|
||||
|
||||
// Set the token expiration in the cache
|
||||
$expireTime = $auth['created_at'] + $auth['expires_in'];
|
||||
$cacheItem = $this->cache->getItem(K::AUTH_TOKEN_EXP_CACHE_KEY);
|
||||
$cacheItem->set($expireTime);
|
||||
$cacheItem->save();
|
||||
|
||||
// Set the refresh token in the cache
|
||||
$cacheItem = $this->cache->getItem(K::AUTH_TOKEN_REFRESH_CACHE_KEY);
|
||||
$cacheItem->set($auth['refresh_token']);
|
||||
$cacheItem->save();
|
||||
|
||||
// Set the session values
|
||||
$this->segment->set('auth_token', $auth['access_token']);
|
||||
$this->segment->set('auth_token_expires', $expireTime);
|
||||
$this->segment->set('refresh_token', $auth['refresh_token']);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
return $this->storeAuth($auth);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Make the call to re-authenticate with the existing refresh token
|
||||
*
|
||||
* @param string $token
|
||||
* @param string $refreshToken
|
||||
* @return boolean
|
||||
* @throws InvalidArgumentException
|
||||
* @throws Throwable
|
||||
* @throws Throwable|InvalidArgumentException
|
||||
*/
|
||||
public function reAuthenticate(string $token): bool
|
||||
public function reAuthenticate(?string $refreshToken = NULL): bool
|
||||
{
|
||||
$auth = $this->model->reAuthenticate($token);
|
||||
$refreshToken ??= $this->getRefreshToken();
|
||||
|
||||
if (FALSE !== $auth)
|
||||
if (empty($refreshToken))
|
||||
{
|
||||
// Set the token in the cache for command line operations
|
||||
$cacheItem = $this->cache->getItem(K::AUTH_TOKEN_CACHE_KEY);
|
||||
$cacheItem->set($auth['access_token']);
|
||||
$cacheItem->save();
|
||||
|
||||
// Set the token expiration in the cache
|
||||
$expire_time = $auth['created_at'] + $auth['expires_in'];
|
||||
$cacheItem = $this->cache->getItem(K::AUTH_TOKEN_EXP_CACHE_KEY);
|
||||
$cacheItem->set($expire_time);
|
||||
$cacheItem->save();
|
||||
|
||||
// Set the refresh token in the cache
|
||||
$cacheItem = $this->cache->getItem(K::AUTH_TOKEN_REFRESH_CACHE_KEY);
|
||||
$cacheItem->set($auth['refresh_token']);
|
||||
$cacheItem->save();
|
||||
|
||||
// Set the session values
|
||||
$this->segment->set('auth_token', $auth['access_token']);
|
||||
$this->segment->set('auth_token_expires', $expire_time);
|
||||
$this->segment->set('refresh_token', $auth['refresh_token']);
|
||||
return TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
$auth = $this->model->reAuthenticate($refreshToken);
|
||||
|
||||
return $this->storeAuth($auth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the current user is authenticated
|
||||
*
|
||||
* @return boolean
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function isAuthenticated(): bool
|
||||
{
|
||||
return ($this->get_auth_token() !== FALSE);
|
||||
return ($this->getAuthToken() !== NULL);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -177,28 +134,70 @@ final class Auth {
|
||||
/**
|
||||
* Retrieve the authentication token from the session
|
||||
*
|
||||
* @return string|false
|
||||
* @return string
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function get_auth_token()
|
||||
public function getAuthToken(): ?string
|
||||
{
|
||||
$now = time();
|
||||
|
||||
$token = $this->segment->get('auth_token', FALSE);
|
||||
$refreshToken = $this->segment->get('refresh_token', FALSE);
|
||||
$isExpired = time() > $this->segment->get('auth_token_expires', $now + 5000);
|
||||
|
||||
// Attempt to re-authenticate with refresh token
|
||||
/* if ($isExpired && $refreshToken)
|
||||
if (PHP_SAPI === 'cli')
|
||||
{
|
||||
if ($this->reAuthenticate($refreshToken))
|
||||
return $this->segment->get('auth_token', NULL)
|
||||
?? $this->cache->get(K::AUTH_TOKEN_CACHE_KEY, NULL);
|
||||
}
|
||||
|
||||
return $this->segment->get('auth_token', NULL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the refresh token
|
||||
*
|
||||
* @return string|null
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function getRefreshToken(): ?string
|
||||
{
|
||||
if (PHP_SAPI === 'cli')
|
||||
{
|
||||
return $this->segment->get('refresh_token')
|
||||
?? $this->cache->get(K::AUTH_TOKEN_REFRESH_CACHE_KEY, NULL);
|
||||
}
|
||||
|
||||
return $this->segment->get('refresh_token');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the new authentication information
|
||||
*
|
||||
* @param $auth
|
||||
* @return bool
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function storeAuth($auth): bool
|
||||
{
|
||||
if (FALSE !== $auth)
|
||||
{
|
||||
$expire_time = $auth['created_at'] + $auth['expires_in'];
|
||||
|
||||
// Set the token in the cache for command line operations
|
||||
// Set the token expiration in the cache
|
||||
// Set the refresh token in the cache
|
||||
$saved = $this->cache->setMultiple([
|
||||
K::AUTH_TOKEN_CACHE_KEY => $auth['access_token'],
|
||||
K::AUTH_TOKEN_EXP_CACHE_KEY => $expire_time,
|
||||
K::AUTH_TOKEN_REFRESH_CACHE_KEY => $auth['refresh_token'],
|
||||
]);
|
||||
|
||||
// Set the session values
|
||||
if ($saved)
|
||||
{
|
||||
return $this->segment->get('auth_token', FALSE);
|
||||
$this->segment->set('auth_token', $auth['access_token']);
|
||||
$this->segment->set('auth_token_expires', $expire_time);
|
||||
$this->segment->set('refresh_token', $auth['refresh_token']);
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
} */
|
||||
|
||||
return $token;
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
// End of KitsuAuth.php
|
@ -16,10 +16,26 @@
|
||||
|
||||
namespace Aviat\AnimeClient\API\Kitsu;
|
||||
|
||||
use const Aviat\AnimeClient\SESSION_SEGMENT;
|
||||
use const Aviat\AnimeClient\USER_AGENT;
|
||||
|
||||
use function Amp\Promise\wait;
|
||||
use function Aviat\AnimeClient\getResponse;
|
||||
|
||||
use Amp\Http\Client\Request;
|
||||
use Amp\Http\Client\Response;
|
||||
use Aviat\AnimeClient\API\APIRequestBuilder;
|
||||
use Aviat\AnimeClient\API\FailedResponseException;
|
||||
use Aviat\AnimeClient\API\Kitsu as K;
|
||||
use Aviat\AnimeClient\Enum\EventType;
|
||||
use Aviat\Ion\Di\ContainerAware;
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\Event;
|
||||
use Aviat\Ion\Json;
|
||||
use Aviat\Ion\JsonException;
|
||||
|
||||
final class KitsuRequestBuilder extends APIRequestBuilder {
|
||||
use ContainerAware;
|
||||
|
||||
/**
|
||||
* The base url for api requests
|
||||
@ -39,4 +55,217 @@ final class KitsuRequestBuilder extends APIRequestBuilder {
|
||||
'CLIENT_ID' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd',
|
||||
'CLIENT_SECRET' => '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151',
|
||||
];
|
||||
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
$this->setContainer($container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a request object
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @return Request
|
||||
*/
|
||||
public function setUpRequest(string $type, string $url, array $options = []): Request
|
||||
{
|
||||
$request = $this->newRequest($type, $url);
|
||||
|
||||
$sessionSegment = $this->getContainer()
|
||||
->get('session')
|
||||
->getSegment(SESSION_SEGMENT);
|
||||
|
||||
$cache = $this->getContainer()->get('cache');
|
||||
$token = null;
|
||||
|
||||
if ($cache->has(K::AUTH_TOKEN_CACHE_KEY))
|
||||
{
|
||||
$token = $cache->get(K::AUTH_TOKEN_CACHE_KEY);
|
||||
}
|
||||
else if ($url !== K::AUTH_URL && $sessionSegment->get('auth_token') !== NULL)
|
||||
{
|
||||
$token = $sessionSegment->get('auth_token');
|
||||
if ( ! (empty($token) || $cache->has(K::AUTH_TOKEN_CACHE_KEY)))
|
||||
{
|
||||
$cache->set(K::AUTH_TOKEN_CACHE_KEY, $token);
|
||||
}
|
||||
}
|
||||
|
||||
if ($token !== NULL)
|
||||
{
|
||||
$request = $request->setAuth('bearer', $token);
|
||||
}
|
||||
|
||||
if (array_key_exists('form_params', $options))
|
||||
{
|
||||
$request = $request->setFormFields($options['form_params']);
|
||||
}
|
||||
|
||||
if (array_key_exists('query', $options))
|
||||
{
|
||||
$request = $request->setQuery($options['query']);
|
||||
}
|
||||
|
||||
if (array_key_exists('body', $options))
|
||||
{
|
||||
$request = $request->setJsonBody($options['body']);
|
||||
}
|
||||
|
||||
if (array_key_exists('headers', $options))
|
||||
{
|
||||
$request = $request->setHeaders($options['headers']);
|
||||
}
|
||||
|
||||
return $request->getFullRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove some boilerplate for get requests
|
||||
*
|
||||
* @param mixed ...$args
|
||||
* @throws Throwable
|
||||
* @return array
|
||||
*/
|
||||
public function getRequest(...$args): array
|
||||
{
|
||||
return $this->request('GET', ...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove some boilerplate for patch requests
|
||||
*
|
||||
* @param mixed ...$args
|
||||
* @throws Throwable
|
||||
* @return array
|
||||
*/
|
||||
public function patchRequest(...$args): array
|
||||
{
|
||||
return $this->request('PATCH', ...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove some boilerplate for post requests
|
||||
*
|
||||
* @param mixed ...$args
|
||||
* @throws Throwable
|
||||
* @return array
|
||||
*/
|
||||
public function postRequest(...$args): array
|
||||
{
|
||||
$logger = NULL;
|
||||
if ($this->getContainer())
|
||||
{
|
||||
$logger = $this->container->getLogger('kitsu-request');
|
||||
}
|
||||
|
||||
$response = $this->getResponse('POST', ...$args);
|
||||
$validResponseCodes = [200, 201];
|
||||
|
||||
if ( ! in_array($response->getStatus(), $validResponseCodes, TRUE) && $logger)
|
||||
{
|
||||
$logger->warning('Non 2xx response for POST api call', $response->getBody());
|
||||
}
|
||||
|
||||
return JSON::decode(wait($response->getBody()->buffer()), TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove some boilerplate for delete requests
|
||||
*
|
||||
* @param mixed ...$args
|
||||
* @throws Throwable
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteRequest(...$args): bool
|
||||
{
|
||||
$response = $this->getResponse('DELETE', ...$args);
|
||||
return ($response->getStatus() === 204);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @return Response
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function getResponse(string $type, string $url, array $options = []): Response
|
||||
{
|
||||
$logger = NULL;
|
||||
if ($this->getContainer())
|
||||
{
|
||||
$logger = $this->container->getLogger('kitsu-request');
|
||||
}
|
||||
|
||||
$request = $this->setUpRequest($type, $url, $options);
|
||||
|
||||
$response = getResponse($request);
|
||||
|
||||
if ($logger)
|
||||
{
|
||||
$logger->debug('Kitsu API Response', [
|
||||
'response_status' => $response->getStatus(),
|
||||
'request_headers' => $response->getOriginalRequest()->getHeaders(),
|
||||
'response_headers' => $response->getHeaders()
|
||||
]);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @throws JsonException
|
||||
* @throws FailedResponseException
|
||||
* @throws Throwable
|
||||
* @return array
|
||||
*/
|
||||
private function request(string $type, string $url, array $options = []): array
|
||||
{
|
||||
$logger = NULL;
|
||||
if ($this->getContainer())
|
||||
{
|
||||
$logger = $this->container->getLogger('kitsu-request');
|
||||
}
|
||||
|
||||
$response = $this->getResponse($type, $url, $options);
|
||||
$statusCode = $response->getStatus();
|
||||
|
||||
// Check for requests that are unauthorized
|
||||
if ($statusCode === 401 || $statusCode === 403)
|
||||
{
|
||||
Event::emit(EventType::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// Any other type of failed request
|
||||
if ($statusCode > 299 || $statusCode < 200)
|
||||
{
|
||||
if ($logger)
|
||||
{
|
||||
$logger->warning('Non 2xx response for api call', (array)$response);
|
||||
}
|
||||
|
||||
throw new FailedResponseException('Failed to get the proper response from the API');
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Json::decode(wait($response->getBody()->buffer()));
|
||||
}
|
||||
catch (JsonException $e)
|
||||
{
|
||||
print_r($e);
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -16,24 +16,7 @@
|
||||
|
||||
namespace Aviat\AnimeClient\API\Kitsu;
|
||||
|
||||
use const Aviat\AnimeClient\SESSION_SEGMENT;
|
||||
|
||||
use function Amp\Promise\wait;
|
||||
use function Aviat\AnimeClient\getResponse;
|
||||
|
||||
use Amp\Http\Client\Request;
|
||||
use Amp\Http\Client\Response;
|
||||
use Aviat\AnimeClient\API\{
|
||||
FailedResponseException,
|
||||
Kitsu as K
|
||||
};
|
||||
use Aviat\Ion\Json;
|
||||
use Aviat\Ion\JsonException;
|
||||
|
||||
use Throwable;
|
||||
|
||||
trait KitsuTrait {
|
||||
|
||||
/**
|
||||
* The request builder for the Kitsu API
|
||||
* @var KitsuRequestBuilder
|
||||
@ -51,209 +34,4 @@ trait KitsuTrait {
|
||||
$this->requestBuilder = $requestBuilder;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a request object
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @return Request
|
||||
*/
|
||||
public function setUpRequest(string $type, string $url, array $options = []): Request
|
||||
{
|
||||
$request = $this->requestBuilder->newRequest($type, $url);
|
||||
|
||||
$sessionSegment = $this->getContainer()
|
||||
->get('session')
|
||||
->getSegment(SESSION_SEGMENT);
|
||||
|
||||
$cache = $this->getContainer()->get('cache');
|
||||
$cacheItem = $cache->getItem('kitsu-auth-token');
|
||||
$token = null;
|
||||
|
||||
|
||||
if ($sessionSegment->get('auth_token') !== NULL && $url !== K::AUTH_URL)
|
||||
{
|
||||
$token = $sessionSegment->get('auth_token');
|
||||
if ( ! $cacheItem->isHit())
|
||||
{
|
||||
$cacheItem->set($token);
|
||||
$cacheItem->save();
|
||||
}
|
||||
}
|
||||
else if ($sessionSegment->get('auth_token') === NULL && $cacheItem->isHit())
|
||||
{
|
||||
$token = $cacheItem->get();
|
||||
}
|
||||
|
||||
if (NULL !== $token)
|
||||
{
|
||||
$request = $request->setAuth('bearer', $token);
|
||||
}
|
||||
|
||||
if (array_key_exists('form_params', $options))
|
||||
{
|
||||
$request = $request->setFormFields($options['form_params']);
|
||||
}
|
||||
|
||||
if (array_key_exists('query', $options))
|
||||
{
|
||||
$request = $request->setQuery($options['query']);
|
||||
}
|
||||
|
||||
if (array_key_exists('body', $options))
|
||||
{
|
||||
$request = $request->setJsonBody($options['body']);
|
||||
}
|
||||
|
||||
if (array_key_exists('headers', $options))
|
||||
{
|
||||
$request = $request->setHeaders($options['headers']);
|
||||
}
|
||||
|
||||
return $request->getFullRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @return Response
|
||||
* @throws Throwable
|
||||
*/
|
||||
private function getResponse(string $type, string $url, array $options = []): Response
|
||||
{
|
||||
$logger = NULL;
|
||||
if ($this->getContainer())
|
||||
{
|
||||
$logger = $this->container->getLogger('kitsu-request');
|
||||
}
|
||||
|
||||
$request = $this->setUpRequest($type, $url, $options);
|
||||
|
||||
$response = getResponse($request);
|
||||
|
||||
if ($logger)
|
||||
{
|
||||
$logger->debug('Kitsu API Response', [
|
||||
'response_status' => $response->getStatus(),
|
||||
'request_headers' => $response->getOriginalRequest()->getHeaders(),
|
||||
'response_headers' => $response->getHeaders()
|
||||
]);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @throws JsonException
|
||||
* @throws FailedResponseException
|
||||
* @throws Throwable
|
||||
* @return array
|
||||
*/
|
||||
private function request(string $type, string $url, array $options = []): array
|
||||
{
|
||||
$logger = NULL;
|
||||
if ($this->getContainer())
|
||||
{
|
||||
$logger = $this->container->getLogger('kitsu-request');
|
||||
}
|
||||
|
||||
$response = $this->getResponse($type, $url, $options);
|
||||
|
||||
if ((int) $response->getStatus() > 299 || (int) $response->getStatus() < 200)
|
||||
{
|
||||
if ($logger)
|
||||
{
|
||||
$logger->warning('Non 200 response for api call', (array)$response);
|
||||
}
|
||||
|
||||
// throw new FailedResponseException('Failed to get the proper response from the API');
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Json::decode(wait($response->getBody()->buffer()));
|
||||
}
|
||||
catch (JsonException $e)
|
||||
{
|
||||
print_r($e);
|
||||
die();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove some boilerplate for get requests
|
||||
*
|
||||
* @param mixed ...$args
|
||||
* @throws Throwable
|
||||
* @return array
|
||||
*/
|
||||
protected function getRequest(...$args): array
|
||||
{
|
||||
return $this->request('GET', ...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove some boilerplate for patch requests
|
||||
*
|
||||
* @param mixed ...$args
|
||||
* @throws Throwable
|
||||
* @return array
|
||||
*/
|
||||
protected function patchRequest(...$args): array
|
||||
{
|
||||
return $this->request('PATCH', ...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove some boilerplate for post requests
|
||||
*
|
||||
* @param mixed ...$args
|
||||
* @throws Throwable
|
||||
* @return array
|
||||
*/
|
||||
protected function postRequest(...$args): array
|
||||
{
|
||||
$logger = NULL;
|
||||
if ($this->getContainer())
|
||||
{
|
||||
$logger = $this->container->getLogger('kitsu-request');
|
||||
}
|
||||
|
||||
$response = $this->getResponse('POST', ...$args);
|
||||
$validResponseCodes = [200, 201];
|
||||
|
||||
if ( ! \in_array((int) $response->getStatus(), $validResponseCodes, TRUE))
|
||||
{
|
||||
if ($logger)
|
||||
{
|
||||
$logger->warning('Non 201 response for POST api call', $response->getBody());
|
||||
}
|
||||
}
|
||||
|
||||
return JSON::decode(wait($response->getBody()->buffer()), TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove some boilerplate for delete requests
|
||||
*
|
||||
* @param mixed ...$args
|
||||
* @throws Throwable
|
||||
* @return bool
|
||||
*/
|
||||
protected function deleteRequest(...$args): bool
|
||||
{
|
||||
$response = $this->getResponse('DELETE', ...$args);
|
||||
return ((int) $response->getStatus() === 204);
|
||||
}
|
||||
}
|
@ -18,7 +18,6 @@ namespace Aviat\AnimeClient\API\Kitsu;
|
||||
|
||||
use Aviat\Ion\Di\Exception\ContainerException;
|
||||
use Aviat\Ion\Di\Exception\NotFoundException;
|
||||
use const Aviat\AnimeClient\SESSION_SEGMENT;
|
||||
|
||||
use function Amp\Promise\wait;
|
||||
use function Aviat\AnimeClient\getResponse;
|
||||
@ -78,7 +77,7 @@ final class ListItem extends AbstractListItem {
|
||||
|
||||
$request = $this->requestBuilder->newRequest('POST', 'library-entries');
|
||||
|
||||
if ($authHeader !== FALSE)
|
||||
if ($authHeader !== NULL)
|
||||
{
|
||||
$request = $request->setHeader('Authorization', $authHeader);
|
||||
}
|
||||
@ -97,7 +96,7 @@ final class ListItem extends AbstractListItem {
|
||||
$authHeader = $this->getAuthHeader();
|
||||
$request = $this->requestBuilder->newRequest('DELETE', "library-entries/{$id}");
|
||||
|
||||
if ($authHeader !== FALSE)
|
||||
if ($authHeader !== NULL)
|
||||
{
|
||||
$request = $request->setHeader('Authorization', $authHeader);
|
||||
}
|
||||
@ -119,7 +118,7 @@ final class ListItem extends AbstractListItem {
|
||||
'include' => 'media,media.categories,media.mappings'
|
||||
]);
|
||||
|
||||
if ($authHeader !== FALSE)
|
||||
if ($authHeader !== NULL)
|
||||
{
|
||||
$request = $request->setHeader('Authorization', $authHeader);
|
||||
}
|
||||
@ -159,7 +158,7 @@ final class ListItem extends AbstractListItem {
|
||||
$request = $this->requestBuilder->newRequest('PATCH', "library-entries/{$id}")
|
||||
->setJsonBody($requestData);
|
||||
|
||||
if ($authHeader !== FALSE)
|
||||
if ($authHeader !== NULL)
|
||||
{
|
||||
$request = $request->setHeader('Authorization', $authHeader);
|
||||
}
|
||||
@ -172,24 +171,15 @@ final class ListItem extends AbstractListItem {
|
||||
* @throws ContainerException
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
private function getAuthHeader()
|
||||
private function getAuthHeader(): ?string
|
||||
{
|
||||
$cache = $this->getContainer()->get('cache');
|
||||
$cacheItem = $cache->getItem('kitsu-auth-token');
|
||||
$sessionSegment = $this->getContainer()
|
||||
->get('session')
|
||||
->getSegment(SESSION_SEGMENT);
|
||||
$auth = $this->getContainer()->get('auth');
|
||||
$token = $auth->getAuthToken();
|
||||
|
||||
if ($sessionSegment->get('auth_token') !== NULL) {
|
||||
$token = $sessionSegment->get('auth_token');
|
||||
if ( ! empty($token)) {
|
||||
return "bearer {$token}";
|
||||
}
|
||||
|
||||
if ($cacheItem->isHit()) {
|
||||
$token = $cacheItem->get();
|
||||
return "bearer {$token}";
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
return NULL;
|
||||
}
|
||||
}
|
@ -38,6 +38,7 @@ use Aviat\AnimeClient\API\Kitsu\Transformer\{
|
||||
MangaTransformer,
|
||||
MangaListTransformer
|
||||
};
|
||||
use Aviat\AnimeClient\Enum\ListType;
|
||||
use Aviat\AnimeClient\Types\{
|
||||
Anime,
|
||||
FormItem,
|
||||
@ -115,7 +116,7 @@ final class Model {
|
||||
public function authenticate(string $username, string $password)
|
||||
{
|
||||
// K::AUTH_URL
|
||||
$response = $this->getResponse('POST', K::AUTH_URL, [
|
||||
$response = $this->requestBuilder->getResponse('POST', K::AUTH_URL, [
|
||||
'headers' => [
|
||||
'accept' => NULL,
|
||||
'Content-type' => 'application/x-www-form-urlencoded',
|
||||
@ -154,19 +155,26 @@ final class Model {
|
||||
*/
|
||||
public function reAuthenticate(string $token)
|
||||
{
|
||||
$response = $this->getResponse('POST', K::AUTH_URL, [
|
||||
$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
|
||||
]
|
||||
]);
|
||||
|
||||
$data = Json::decode(wait($response->getBody()->buffer()));
|
||||
|
||||
if (array_key_exists('error', $data))
|
||||
{
|
||||
dump($data['error']);
|
||||
dump($response);
|
||||
die();
|
||||
}
|
||||
|
||||
if (array_key_exists('access_token', $data))
|
||||
{
|
||||
return $data;
|
||||
@ -175,44 +183,13 @@ final class Model {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the data for the anime watch history page
|
||||
*
|
||||
* @return array
|
||||
* @throws InvalidArgumentException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function getAnimeHistory(): array
|
||||
{
|
||||
$raw = $this->getRawHistoryList('anime');
|
||||
$organized = JsonAPI::organizeData($raw);
|
||||
$organized = array_filter($organized, fn ($item) => array_key_exists('relationships', $item));
|
||||
|
||||
return (new AnimeHistoryTransformer())->transform($organized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the data for the manga read history page
|
||||
*
|
||||
* @return array
|
||||
* @throws InvalidArgumentException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function getMangaHistory(): array
|
||||
{
|
||||
$raw = $this->getRawHistoryList('manga');
|
||||
$organized = JsonAPI::organizeData($raw);
|
||||
$organized = array_filter($organized, fn ($item) => array_key_exists('relationships', $item));
|
||||
|
||||
return (new MangaHistoryTransformer())->transform($organized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the userid for a username from Kitsu
|
||||
*
|
||||
* @param string $username
|
||||
* @return string
|
||||
* @throws InvalidArgumentException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function getUserIdByUsername(string $username = NULL): string
|
||||
{
|
||||
@ -221,11 +198,8 @@ final class Model {
|
||||
$username = $this->getUsername();
|
||||
}
|
||||
|
||||
$cacheItem = $this->cache->getItem(K::AUTH_USER_ID_KEY);
|
||||
|
||||
if ( ! $cacheItem->isHit())
|
||||
{
|
||||
$data = $this->getRequest('users', [
|
||||
return $this->getCached(K::AUTH_USER_ID_KEY, function(string $username) {
|
||||
$data = $this->requestBuilder->getRequest('users', [
|
||||
'query' => [
|
||||
'filter' => [
|
||||
'name' => $username
|
||||
@ -233,11 +207,8 @@ final class Model {
|
||||
]
|
||||
]);
|
||||
|
||||
$cacheItem->set($data['data'][0]['id']);
|
||||
$cacheItem->save();
|
||||
}
|
||||
|
||||
return $cacheItem->get();
|
||||
return $data['data'][0]['id'] ?? NULL;
|
||||
}, [$username]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -248,14 +219,14 @@ final class Model {
|
||||
*/
|
||||
public function getCharacter(string $slug): array
|
||||
{
|
||||
return $this->getRequest('characters', [
|
||||
return $this->requestBuilder->getRequest('characters', [
|
||||
'query' => [
|
||||
'filter' => [
|
||||
'slug' => $slug,
|
||||
],
|
||||
'fields' => [
|
||||
'anime' => 'canonicalTitle,titles,slug,posterImage',
|
||||
'manga' => 'canonicalTitle,titles,slug,posterImage'
|
||||
'anime' => 'canonicalTitle,abbreviatedTitles,titles,slug,posterImage',
|
||||
'manga' => 'canonicalTitle,abbreviatedTitles,titles,slug,posterImage'
|
||||
],
|
||||
'include' => 'castings.person,castings.media'
|
||||
]
|
||||
@ -271,31 +242,22 @@ final class Model {
|
||||
*/
|
||||
public function getPerson(string $id): array
|
||||
{
|
||||
$cacheItem = $this->cache->getItem("kitsu-person-{$id}");
|
||||
|
||||
if ( ! $cacheItem->isHit())
|
||||
{
|
||||
$data = $this->getRequest("people/{$id}", [
|
||||
'query' => [
|
||||
'filter' => [
|
||||
'id' => $id,
|
||||
],
|
||||
'fields' => [
|
||||
'characters' => 'canonicalName,slug,image',
|
||||
'characterVoices' => 'mediaCharacter',
|
||||
'anime' => 'canonicalTitle,titles,slug,posterImage',
|
||||
'manga' => 'canonicalTitle,titles,slug,posterImage',
|
||||
'mediaCharacters' => 'role,media,character',
|
||||
'mediaStaff' => 'role,media,person',
|
||||
],
|
||||
'include' => 'voices.mediaCharacter.media,voices.mediaCharacter.character,staff.media',
|
||||
return $this->getCached("kitsu-person-{$id}", fn () => $this->requestBuilder->getRequest("people/{$id}", [
|
||||
'query' => [
|
||||
'filter' => [
|
||||
'id' => $id,
|
||||
],
|
||||
]);
|
||||
$cacheItem->set($data);
|
||||
$cacheItem->save();
|
||||
}
|
||||
|
||||
return $cacheItem->get();
|
||||
'fields' => [
|
||||
'characters' => 'canonicalName,slug,image',
|
||||
'characterVoices' => 'mediaCharacter',
|
||||
'anime' => 'canonicalTitle,abbreviatedTitles,titles,slug,posterImage',
|
||||
'manga' => 'canonicalTitle,abbreviatedTitles,titles,slug,posterImage',
|
||||
'mediaCharacters' => 'role,media,character',
|
||||
'mediaStaff' => 'role,media,person',
|
||||
],
|
||||
'include' => 'voices.mediaCharacter.media,voices.mediaCharacter.character,staff.media',
|
||||
],
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -306,7 +268,7 @@ final class Model {
|
||||
*/
|
||||
public function getUserData(string $username): array
|
||||
{
|
||||
return $this->getRequest('users', [
|
||||
return $this->requestBuilder->getRequest('users', [
|
||||
'query' => [
|
||||
'filter' => [
|
||||
'name' => $username,
|
||||
@ -343,7 +305,7 @@ final class Model {
|
||||
]
|
||||
];
|
||||
|
||||
$raw = $this->getRequest($type, $options);
|
||||
$raw = $this->requestBuilder->getRequest($type, $options);
|
||||
$raw['included'] = JsonAPI::organizeIncluded($raw['included']);
|
||||
|
||||
foreach ($raw['data'] as &$item)
|
||||
@ -388,7 +350,7 @@ final class Model {
|
||||
]
|
||||
];
|
||||
|
||||
$raw = $this->getRequest('mappings', $options);
|
||||
$raw = $this->requestBuilder->getRequest('mappings', $options);
|
||||
|
||||
if ( ! array_key_exists('included', $raw))
|
||||
{
|
||||
@ -420,6 +382,34 @@ final class Model {
|
||||
return $this->animeTransformer->transform($baseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the data for the anime watch history page
|
||||
*
|
||||
* @return array
|
||||
* @throws InvalidArgumentException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function getAnimeHistory(): array
|
||||
{
|
||||
$key = K::ANIME_HISTORY_LIST_CACHE_KEY;
|
||||
$list = $this->cache->get($key, NULL);
|
||||
|
||||
if ($list === NULL)
|
||||
{
|
||||
$raw = $this->getRawHistoryList('anime');
|
||||
|
||||
$organized = JsonAPI::organizeData($raw);
|
||||
$organized = array_filter($organized, fn ($item) => array_key_exists('relationships', $item));
|
||||
|
||||
$list = (new AnimeHistoryTransformer())->transform($organized);
|
||||
|
||||
$this->cache->set($key, $list);
|
||||
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about a particular anime
|
||||
*
|
||||
@ -441,9 +431,11 @@ final class Model {
|
||||
*/
|
||||
public function getAnimeList(string $status): array
|
||||
{
|
||||
$cacheItem = $this->cache->getItem("kitsu-anime-list-{$status}");
|
||||
$key = "kitsu-anime-list-{$status}";
|
||||
|
||||
if ( ! $cacheItem->isHit())
|
||||
$list = $this->cache->get($key, NULL);
|
||||
|
||||
if ($list === NULL)
|
||||
{
|
||||
$data = $this->getRawAnimeList($status) ?? [];
|
||||
|
||||
@ -469,11 +461,11 @@ final class Model {
|
||||
$keyed[$item['id']] = $item;
|
||||
}
|
||||
|
||||
$cacheItem->set($keyed);
|
||||
$cacheItem->save();
|
||||
$list = $keyed;
|
||||
$this->cache->set($key, $list);
|
||||
}
|
||||
|
||||
return $cacheItem->get();
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -485,27 +477,7 @@ final class Model {
|
||||
*/
|
||||
public function getAnimeListCount(string $status = '') : int
|
||||
{
|
||||
$options = [
|
||||
'query' => [
|
||||
'filter' => [
|
||||
'user_id' => $this->getUserIdByUsername(),
|
||||
'kind' => 'anime'
|
||||
],
|
||||
'page' => [
|
||||
'limit' => 1
|
||||
],
|
||||
'sort' => '-updated_at'
|
||||
]
|
||||
];
|
||||
|
||||
if ( ! empty($status))
|
||||
{
|
||||
$options['query']['filter']['status'] = $status;
|
||||
}
|
||||
|
||||
$response = $this->getRequest('library-entries', $options);
|
||||
|
||||
return $response['meta']['count'];
|
||||
return $this->getListCount(ListType::ANIME, $status);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -582,7 +554,7 @@ final class Model {
|
||||
'include' => 'mappings'
|
||||
]
|
||||
];
|
||||
$data = $this->getRequest("anime/{$kitsuAnimeId}", $options);
|
||||
$data = $this->requestBuilder->getRequest("anime/{$kitsuAnimeId}", $options);
|
||||
|
||||
if ( ! array_key_exists('included', $data))
|
||||
{
|
||||
@ -617,7 +589,7 @@ final class Model {
|
||||
{
|
||||
$defaultOptions = [
|
||||
'filter' => [
|
||||
'user_id' => $this->getUserIdByUsername($this->getUsername()),
|
||||
'user_id' => $this->getUserId(),
|
||||
'kind' => 'anime'
|
||||
],
|
||||
'page' => [
|
||||
@ -628,7 +600,7 @@ final class Model {
|
||||
];
|
||||
$options = array_merge($defaultOptions, $options);
|
||||
|
||||
return $this->setUpRequest('GET', 'library-entries', ['query' => $options]);
|
||||
return $this->requestBuilder->setUpRequest('GET', 'library-entries', ['query' => $options]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -643,7 +615,7 @@ final class Model {
|
||||
{
|
||||
$options = [
|
||||
'filter' => [
|
||||
'user_id' => $this->getUserIdByUsername($this->getUsername()),
|
||||
'user_id' => $this->getUserId(),
|
||||
'kind' => 'anime',
|
||||
'status' => $status,
|
||||
],
|
||||
@ -676,6 +648,32 @@ final class Model {
|
||||
return $this->mangaTransformer->transform($baseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the data for the manga read history page
|
||||
*
|
||||
* @return array
|
||||
* @throws InvalidArgumentException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function getMangaHistory(): array
|
||||
{
|
||||
$key = K::MANGA_HISTORY_LIST_CACHE_KEY;
|
||||
$list = $this->cache->get($key, NULL);
|
||||
|
||||
if ($list === NULL)
|
||||
{
|
||||
$raw = $this->getRawHistoryList('manga');
|
||||
$organized = JsonAPI::organizeData($raw);
|
||||
$organized = array_filter($organized, fn ($item) => array_key_exists('relationships', $item));
|
||||
|
||||
$list = (new MangaHistoryTransformer())->transform($organized);
|
||||
|
||||
$this->cache->set($key, $list);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about a particular manga
|
||||
*
|
||||
@ -702,7 +700,7 @@ final class Model {
|
||||
$options = [
|
||||
'query' => [
|
||||
'filter' => [
|
||||
'user_id' => $this->getUserIdByUsername($this->getUsername()),
|
||||
'user_id' => $this->getUserId(),
|
||||
'kind' => 'manga',
|
||||
'status' => $status,
|
||||
],
|
||||
@ -715,11 +713,13 @@ final class Model {
|
||||
]
|
||||
];
|
||||
|
||||
$cacheItem = $this->cache->getItem("kitsu-manga-list-{$status}");
|
||||
$key = "kitsu-manga-list-{$status}";
|
||||
|
||||
if ( ! $cacheItem->isHit())
|
||||
$list = $this->cache->get($key, NULL);
|
||||
|
||||
if ($list === NULL)
|
||||
{
|
||||
$data = $this->getRequest('library-entries', $options) ?? [];
|
||||
$data = $this->requestBuilder->getRequest('library-entries', $options) ?? [];
|
||||
|
||||
// Bail out on no data
|
||||
if (empty($data) || ( ! array_key_exists('included', $data)))
|
||||
@ -736,13 +736,12 @@ final class Model {
|
||||
}
|
||||
unset($item);
|
||||
|
||||
$transformed = $this->mangaListTransformer->transformCollection($data['data']);
|
||||
$list = $this->mangaListTransformer->transformCollection($data['data']);
|
||||
|
||||
$cacheItem->set($transformed);
|
||||
$cacheItem->save();
|
||||
$this->cache->set($key, $list);
|
||||
}
|
||||
|
||||
return $cacheItem->get();
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -754,27 +753,7 @@ final class Model {
|
||||
*/
|
||||
public function getMangaListCount(string $status = '') : int
|
||||
{
|
||||
$options = [
|
||||
'query' => [
|
||||
'filter' => [
|
||||
'user_id' => $this->getUserIdByUsername(),
|
||||
'kind' => 'manga'
|
||||
],
|
||||
'page' => [
|
||||
'limit' => 1
|
||||
],
|
||||
'sort' => '-updated_at'
|
||||
]
|
||||
];
|
||||
|
||||
if ( ! empty($status))
|
||||
{
|
||||
$options['query']['filter']['status'] = $status;
|
||||
}
|
||||
|
||||
$response = $this->getRequest('library-entries', $options);
|
||||
|
||||
return $response['meta']['count'];
|
||||
return $this->getListCount(ListType::MANGA, $status);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -850,7 +829,7 @@ final class Model {
|
||||
{
|
||||
$defaultOptions = [
|
||||
'filter' => [
|
||||
'user_id' => $this->getUserIdByUsername($this->getUsername()),
|
||||
'user_id' => $this->getUserId(),
|
||||
'kind' => 'manga'
|
||||
],
|
||||
'page' => [
|
||||
@ -861,7 +840,7 @@ final class Model {
|
||||
];
|
||||
$options = array_merge($defaultOptions, $options);
|
||||
|
||||
return $this->setUpRequest('GET', 'library-entries', ['query' => $options]);
|
||||
return $this->requestBuilder->setUpRequest('GET', 'library-entries', ['query' => $options]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -878,7 +857,7 @@ final class Model {
|
||||
'include' => 'mappings'
|
||||
]
|
||||
];
|
||||
$data = $this->getRequest("manga/{$kitsuMangaId}", $options);
|
||||
$data = $this->requestBuilder->getRequest("manga/{$kitsuMangaId}", $options);
|
||||
$mappings = array_column($data['included'], 'attributes');
|
||||
|
||||
foreach($mappings as $map)
|
||||
@ -905,7 +884,7 @@ final class Model {
|
||||
*/
|
||||
public function createListItem(array $data): ?Request
|
||||
{
|
||||
$data['user_id'] = $this->getUserIdByUsername($this->getUsername());
|
||||
$data['user_id'] = $this->getUserId();
|
||||
if ($data['id'] === NULL)
|
||||
{
|
||||
return NULL;
|
||||
@ -976,6 +955,20 @@ final class Model {
|
||||
return $this->listItem->delete($id);
|
||||
}
|
||||
|
||||
public function getSyncList(string $type): array
|
||||
{
|
||||
$options = [
|
||||
'filter' => [
|
||||
'user_id' => $this->getUserId(),
|
||||
'kind' => $type,
|
||||
],
|
||||
'include' => "{$type},{$type}.mappings",
|
||||
'sort' => '-updated_at'
|
||||
];
|
||||
|
||||
return $this->getRawSyncList($type, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the aggregated pages of anime or manga history
|
||||
*
|
||||
@ -1022,11 +1015,11 @@ final class Model {
|
||||
*/
|
||||
protected function getRawHistoryPage(string $type, int $offset, int $limit = 20): Request
|
||||
{
|
||||
return $this->setUpRequest('GET', 'library-events', [
|
||||
return $this->requestBuilder->setUpRequest('GET', 'library-events', [
|
||||
'query' => [
|
||||
'filter' => [
|
||||
'kind' => 'progressed,updated',
|
||||
'userId' => $this->getUserIdByUsername($this->getUsername()),
|
||||
'userId' => $this->getUserId(),
|
||||
],
|
||||
'page' => [
|
||||
'offset' => $offset,
|
||||
@ -1043,6 +1036,18 @@ final class Model {
|
||||
]);
|
||||
}
|
||||
|
||||
private function getUserId(): string
|
||||
{
|
||||
static $userId = NULL;
|
||||
|
||||
if ($userId === NULL)
|
||||
{
|
||||
$userId = $this->getUserIdByUsername($this->getUsername());
|
||||
}
|
||||
|
||||
return $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the kitsu username from config
|
||||
*
|
||||
@ -1072,7 +1077,7 @@ final class Model {
|
||||
]
|
||||
];
|
||||
|
||||
$data = $this->getRequest("{$type}/{$id}", $options);
|
||||
$data = $this->requestBuilder->getRequest("{$type}/{$id}", $options);
|
||||
|
||||
if (empty($data['data']))
|
||||
{
|
||||
@ -1112,7 +1117,7 @@ final class Model {
|
||||
]
|
||||
];
|
||||
|
||||
$data = $this->getRequest($type, $options);
|
||||
$data = $this->requestBuilder->getRequest($type, $options);
|
||||
|
||||
if (empty($data['data']))
|
||||
{
|
||||
@ -1124,4 +1129,93 @@ final class Model {
|
||||
$baseData['included'] = $data['included'];
|
||||
return $baseData;
|
||||
}
|
||||
|
||||
private function getListCount(string $type, string $status = ''): int
|
||||
{
|
||||
$options = [
|
||||
'query' => [
|
||||
'filter' => [
|
||||
'user_id' => $this->getUserId(),
|
||||
'kind' => $type,
|
||||
],
|
||||
'page' => [
|
||||
'limit' => 1
|
||||
],
|
||||
'sort' => '-updated_at'
|
||||
]
|
||||
];
|
||||
|
||||
if ( ! empty($status))
|
||||
{
|
||||
$options['query']['filter']['status'] = $status;
|
||||
}
|
||||
|
||||
$response = $this->requestBuilder->getRequest('library-entries', $options);
|
||||
|
||||
return $response['meta']['count'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full anime list
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $options
|
||||
* @return array
|
||||
* @throws InvalidArgumentException
|
||||
* @throws Throwable
|
||||
*/
|
||||
private function getRawSyncList(string $type, array $options): array
|
||||
{
|
||||
$count = $this->getListCount($type);
|
||||
$size = static::LIST_PAGE_SIZE;
|
||||
$pages = ceil($count / $size);
|
||||
|
||||
$requester = new ParallelAPIRequest();
|
||||
|
||||
// Set up requests
|
||||
for ($i = 0; $i < $pages; $i++)
|
||||
{
|
||||
$offset = $i * $size;
|
||||
$requester->addRequest($this->getRawSyncListPage($type, $size, $offset, $options));
|
||||
}
|
||||
|
||||
$responses = $requester->makeRequests();
|
||||
$output = [];
|
||||
|
||||
foreach($responses as $response)
|
||||
{
|
||||
$data = Json::decode($response);
|
||||
$output[] = $data;
|
||||
}
|
||||
|
||||
return array_merge_recursive(...$output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full anime list in paginated form
|
||||
*
|
||||
* @param string $type
|
||||
* @param int $limit
|
||||
* @param int $offset
|
||||
* @param array $options
|
||||
* @return Request
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function getRawSyncListPage(string $type, int $limit, int $offset = 0, array $options = []): Request
|
||||
{
|
||||
$defaultOptions = [
|
||||
'filter' => [
|
||||
'user_id' => $this->getUserId(),
|
||||
'kind' => $type,
|
||||
],
|
||||
'page' => [
|
||||
'offset' => $offset,
|
||||
'limit' => $limit
|
||||
],
|
||||
'sort' => '-updated_at'
|
||||
];
|
||||
$options = array_merge($defaultOptions, $options);
|
||||
|
||||
return $this->requestBuilder->setUpRequest('GET', 'library-entries', ['query' => $options]);
|
||||
}
|
||||
}
|
@ -42,6 +42,7 @@ final class AnimeTransformer extends AbstractTransformer {
|
||||
|
||||
$title = $item['canonicalTitle'];
|
||||
$titles = Kitsu::filterTitles($item);
|
||||
$titles_more = Kitsu::getTitles($item);
|
||||
|
||||
$characters = [];
|
||||
$staff = [];
|
||||
@ -123,6 +124,7 @@ final class AnimeTransformer extends AbstractTransformer {
|
||||
'synopsis' => $item['synopsis'],
|
||||
'title' => $title,
|
||||
'titles' => $titles,
|
||||
'titles_more' => $titles_more,
|
||||
'trailer_id' => $item['youtubeVideoId'],
|
||||
'url' => "https://kitsu.io/anime/{$item['slug']}",
|
||||
]);
|
||||
|
@ -149,7 +149,7 @@ final class CharacterTransformer extends AbstractTransformer {
|
||||
|
||||
$person = $p['attributes'];
|
||||
$person['id'] = $pid;
|
||||
$person['image'] = $person['image']['original'];
|
||||
$person['image'] = $person['image']['original'] ?? '';
|
||||
|
||||
uasort($role['relationships']['media']['anime'], static function ($a, $b) {
|
||||
return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle'];
|
||||
|
@ -98,16 +98,12 @@ final class MangaTransformer extends AbstractTransformer {
|
||||
|
||||
if ( ! empty($characters['main']))
|
||||
{
|
||||
uasort($characters['main'], static function ($a, $b) {
|
||||
return $a['name'] <=> $b['name'];
|
||||
});
|
||||
uasort($characters['main'], fn ($a, $b) => $a['name'] <=> $b['name']);
|
||||
}
|
||||
|
||||
if ( ! empty($characters['supporting']))
|
||||
{
|
||||
uasort($characters['supporting'], static function ($a, $b) {
|
||||
return $a['name'] <=> $b['name'];
|
||||
});
|
||||
uasort($characters['supporting'], fn ($a, $b) => $a['name'] <=> $b['name']);
|
||||
}
|
||||
|
||||
ksort($characters);
|
||||
|
@ -103,9 +103,7 @@ final class ParallelAPIRequest {
|
||||
|
||||
foreach ($this->requests as $key => $url)
|
||||
{
|
||||
$promises[$key] = call(static function () use ($client, $url) {
|
||||
return yield $client->request($url);
|
||||
});
|
||||
$promises[$key] = call(fn () => yield $client->request($url));
|
||||
}
|
||||
|
||||
return wait(all($promises));
|
||||
|
@ -16,6 +16,9 @@
|
||||
|
||||
namespace Aviat\AnimeClient;
|
||||
|
||||
use Aviat\AnimeClient\API\Kitsu;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use Psr\SimpleCache\InvalidArgumentException;
|
||||
use function Amp\Promise\wait;
|
||||
|
||||
use Amp\Http\Client\Request;
|
||||
@ -26,6 +29,8 @@ use Amp\Http\Client\HttpClientBuilder;
|
||||
use Aviat\Ion\ConfigInterface;
|
||||
use Yosymfony\Toml\{Toml, TomlBuilder};
|
||||
|
||||
use Throwable;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
//! TOML Functions
|
||||
// ----------------------------------------------------------------------------
|
||||
@ -232,7 +237,7 @@ function getApiClient (): HttpClient
|
||||
*
|
||||
* @param string|Request $request
|
||||
* @return Response
|
||||
* @throws \Throwable
|
||||
* @throws Throwable
|
||||
*/
|
||||
function getResponse ($request): Response
|
||||
{
|
||||
@ -256,7 +261,7 @@ function getResponse ($request): Response
|
||||
*/
|
||||
function getLocalImg ($kitsuUrl, $webp = TRUE): string
|
||||
{
|
||||
if ( ! is_string($kitsuUrl))
|
||||
if (empty($kitsuUrl) || ( ! is_string($kitsuUrl)))
|
||||
{
|
||||
return 'images/placeholder.webp';
|
||||
}
|
||||
@ -346,3 +351,30 @@ function col_not_empty(array $search, string $key): bool
|
||||
$items = array_filter(array_column($search, $key), fn ($x) => ( ! empty($x)));
|
||||
return count($items) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache, but save user auth data
|
||||
*
|
||||
* @param CacheInterface $cache
|
||||
* @return bool
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
function clearCache(CacheInterface $cache): bool
|
||||
{
|
||||
// Save the user data, if it exists, for priming the cache
|
||||
$userData = $cache->getMultiple([
|
||||
Kitsu::AUTH_USER_ID_KEY,
|
||||
Kitsu::AUTH_TOKEN_CACHE_KEY,
|
||||
Kitsu::AUTH_TOKEN_EXP_CACHE_KEY,
|
||||
Kitsu::AUTH_TOKEN_REFRESH_CACHE_KEY,
|
||||
], NULL);
|
||||
|
||||
$userData = array_filter((array)$userData, fn ($value) => $value !== NULL);
|
||||
$cleared = $cache->clear();
|
||||
|
||||
$saved = ( ! empty($userData))
|
||||
? $cache->setMultiple($userData)
|
||||
: TRUE;
|
||||
|
||||
return $cleared && $saved;
|
||||
}
|
@ -24,10 +24,10 @@ use Aura\Session\SessionFactory;
|
||||
use Aviat\AnimeClient\{Model, UrlGenerator, Util};
|
||||
use Aviat\AnimeClient\API\{Anilist, CacheTrait, Kitsu};
|
||||
use Aviat\AnimeClient\API\Kitsu\KitsuRequestBuilder;
|
||||
use Aviat\Banker\Pool;
|
||||
use Aviat\Banker\Teller;
|
||||
use Aviat\Ion\Config;
|
||||
use Aviat\Ion\Di\{Container, ContainerAware};
|
||||
use ConsoleKit\{Command, ConsoleException};
|
||||
use Aviat\Ion\Di\{Container, ContainerInterface, ContainerAware};
|
||||
use ConsoleKit\{Colors, Command, ConsoleException};
|
||||
use ConsoleKit\Widgets\Box;
|
||||
use Laminas\Diactoros\{Response, ServerRequestFactory};
|
||||
use Monolog\Handler\RotatingFileHandler;
|
||||
@ -43,16 +43,28 @@ abstract class BaseCommand extends Command {
|
||||
/**
|
||||
* Echo text in a box
|
||||
*
|
||||
* @param string $message
|
||||
* @param string|array $message
|
||||
* @param string|int|null $fgColor
|
||||
* @param string|int|null $bgColor
|
||||
* @return void
|
||||
*/
|
||||
protected function echoBox($message): void
|
||||
public function echoBox($message, $fgColor = NULL, $bgColor = NULL): void
|
||||
{
|
||||
if (is_array($message))
|
||||
{
|
||||
$message = implode("\n", $message);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
echo "\n";
|
||||
// color message
|
||||
$message = Colors::colorize($message, $fgColor, $bgColor);
|
||||
|
||||
// create the box
|
||||
$box = new Box($this->getConsole(), $message);
|
||||
|
||||
$box->write();
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
catch (ConsoleException $e)
|
||||
@ -61,12 +73,47 @@ abstract class BaseCommand extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
public function echo(string $message): void
|
||||
{
|
||||
$this->_line($message);
|
||||
}
|
||||
|
||||
public function echoSuccess(string $message): void
|
||||
{
|
||||
$this->_line($message, Colors::GREEN | Colors::BOLD, Colors::BLACK);
|
||||
}
|
||||
|
||||
public function echoWarning(string $message): void
|
||||
{
|
||||
$this->_line($message, Colors::YELLOW | Colors::BOLD, Colors::BLACK);
|
||||
}
|
||||
|
||||
public function echoWarningBox(string $message): void
|
||||
{
|
||||
$this->echoBox($message, Colors::YELLOW | Colors::BOLD, Colors::BLACK);
|
||||
}
|
||||
|
||||
public function echoError(string $message): void
|
||||
{
|
||||
$this->_line($message, Colors::RED | Colors::BOLD, Colors::BLACK);
|
||||
}
|
||||
|
||||
public function echoErrorBox(string $message): void
|
||||
{
|
||||
$this->echoBox($message, Colors::RED | Colors::BOLD, Colors::BLACK);
|
||||
}
|
||||
|
||||
public function clearLine(): void
|
||||
{
|
||||
$this->getConsole()->write("\r\e[2K");
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the Di container
|
||||
*
|
||||
* @return Container
|
||||
* @return Containerinterface
|
||||
*/
|
||||
protected function setupContainer(): Container
|
||||
public function setupContainer(): ContainerInterface
|
||||
{
|
||||
$APP_DIR = realpath(__DIR__ . '/../../../app');
|
||||
$APPCONF_DIR = realpath("{$APP_DIR}/appConf/");
|
||||
@ -82,114 +129,105 @@ abstract class BaseCommand extends Command {
|
||||
|
||||
$configArray = array_replace_recursive($baseConfig, $config, $overrideConfig);
|
||||
|
||||
$di = static function ($configArray) use ($APP_DIR): Container {
|
||||
$container = new Container();
|
||||
return $this->_di($configArray, $APP_DIR);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Logging
|
||||
// -------------------------------------------------------------------------
|
||||
private function _line(string $message, $fgColor = NULL, $bgColor = NULL): void
|
||||
{
|
||||
$message = Colors::colorize($message, $fgColor, $bgColor);
|
||||
$this->getConsole()->writeln($message);
|
||||
}
|
||||
|
||||
$app_logger = new Logger('animeclient');
|
||||
$app_logger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/app-cli.log', Logger::NOTICE));
|
||||
private function _di(array $configArray, string $APP_DIR): ContainerInterface
|
||||
{
|
||||
$container = new Container();
|
||||
|
||||
$kitsu_request_logger = new Logger('kitsu-request');
|
||||
$kitsu_request_logger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/kitsu_request-cli.log', Logger::NOTICE));
|
||||
// -------------------------------------------------------------------------
|
||||
// Logging
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
$anilistRequestLogger = new Logger('anilist-request');
|
||||
$anilistRequestLogger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/anilist_request-cli.log', Logger::NOTICE));
|
||||
$app_logger = new Logger('animeclient');
|
||||
$app_logger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/app-cli.log', Logger::NOTICE));
|
||||
|
||||
$container->setLogger($app_logger);
|
||||
$container->setLogger($anilistRequestLogger, 'anilist-request');
|
||||
$container->setLogger($kitsu_request_logger, 'kitsu-request');
|
||||
$kitsu_request_logger = new Logger('kitsu-request');
|
||||
$kitsu_request_logger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/kitsu_request-cli.log', Logger::NOTICE));
|
||||
|
||||
// Create Config Object
|
||||
$container->set('config', static function() use ($configArray): Config {
|
||||
return new Config($configArray);
|
||||
});
|
||||
$anilistRequestLogger = new Logger('anilist-request');
|
||||
$anilistRequestLogger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/anilist_request-cli.log', Logger::NOTICE));
|
||||
|
||||
// Create Cache Object
|
||||
$container->set('cache', static function($container) {
|
||||
$logger = $container->getLogger();
|
||||
$config = $container->get('config')->get('cache');
|
||||
return new Pool($config, $logger);
|
||||
});
|
||||
$container->setLogger($app_logger);
|
||||
$container->setLogger($anilistRequestLogger, 'anilist-request');
|
||||
$container->setLogger($kitsu_request_logger, 'kitsu-request');
|
||||
|
||||
// Create Aura Router Object
|
||||
$container->set('aura-router', static function() {
|
||||
return new RouterContainer;
|
||||
});
|
||||
// Create Config Object
|
||||
$container->set('config', fn () => new Config($configArray));
|
||||
|
||||
// Create Request/Response Objects
|
||||
$container->set('request', static function() {
|
||||
return ServerRequestFactory::fromGlobals(
|
||||
$_SERVER,
|
||||
$_GET,
|
||||
$_POST,
|
||||
$_COOKIE,
|
||||
$_FILES
|
||||
);
|
||||
});
|
||||
$container->set('response', static function(): Response {
|
||||
return new Response;
|
||||
});
|
||||
// Create Cache Object
|
||||
$container->set('cache', static function($container) {
|
||||
$logger = $container->getLogger();
|
||||
$config = $container->get('config')->get('cache');
|
||||
return new Teller($config, $logger);
|
||||
});
|
||||
|
||||
// Create session Object
|
||||
$container->set('session', static function() {
|
||||
return (new SessionFactory())->newInstance($_COOKIE);
|
||||
});
|
||||
// Create Aura Router Object
|
||||
$container->set('aura-router', fn () => new RouterContainer);
|
||||
|
||||
// Models
|
||||
$container->set('kitsu-model', static function($container): Kitsu\Model {
|
||||
$requestBuilder = new KitsuRequestBuilder();
|
||||
$requestBuilder->setLogger($container->getLogger('kitsu-request'));
|
||||
// Create Request/Response Objects
|
||||
$container->set('request', fn () => ServerRequestFactory::fromGlobals(
|
||||
$_SERVER,
|
||||
$_GET,
|
||||
$_POST,
|
||||
$_COOKIE,
|
||||
$_FILES
|
||||
));
|
||||
$container->set('response', fn () => new Response);
|
||||
|
||||
$listItem = new Kitsu\ListItem();
|
||||
$listItem->setContainer($container);
|
||||
$listItem->setRequestBuilder($requestBuilder);
|
||||
// Create session Object
|
||||
$container->set('session', fn () => (new SessionFactory())->newInstance($_COOKIE));
|
||||
|
||||
$model = new Kitsu\Model($listItem);
|
||||
$model->setContainer($container);
|
||||
$model->setRequestBuilder($requestBuilder);
|
||||
// Models
|
||||
$container->set('kitsu-model', static function($container): Kitsu\Model {
|
||||
$requestBuilder = new KitsuRequestBuilder($container);
|
||||
$requestBuilder->setLogger($container->getLogger('kitsu-request'));
|
||||
|
||||
$cache = $container->get('cache');
|
||||
$model->setCache($cache);
|
||||
return $model;
|
||||
});
|
||||
$container->set('anilist-model', static function ($container): Anilist\Model {
|
||||
$requestBuilder = new Anilist\AnilistRequestBuilder();
|
||||
$requestBuilder->setLogger($container->getLogger('anilist-request'));
|
||||
$listItem = new Kitsu\ListItem();
|
||||
$listItem->setContainer($container);
|
||||
$listItem->setRequestBuilder($requestBuilder);
|
||||
|
||||
$listItem = new Anilist\ListItem();
|
||||
$listItem->setContainer($container);
|
||||
$listItem->setRequestBuilder($requestBuilder);
|
||||
$model = new Kitsu\Model($listItem);
|
||||
$model->setContainer($container);
|
||||
$model->setRequestBuilder($requestBuilder);
|
||||
|
||||
$model = new Anilist\Model($listItem);
|
||||
$model->setContainer($container);
|
||||
$model->setRequestBuilder($requestBuilder);
|
||||
$cache = $container->get('cache');
|
||||
$model->setCache($cache);
|
||||
return $model;
|
||||
});
|
||||
$container->set('anilist-model', static function ($container): Anilist\Model {
|
||||
$requestBuilder = new Anilist\AnilistRequestBuilder();
|
||||
$requestBuilder->setLogger($container->getLogger('anilist-request'));
|
||||
|
||||
return $model;
|
||||
});
|
||||
$container->set('settings-model', static function($container): Model\Settings {
|
||||
$model = new Model\Settings($container->get('config'));
|
||||
$model->setContainer($container);
|
||||
return $model;
|
||||
});
|
||||
$listItem = new Anilist\ListItem();
|
||||
$listItem->setContainer($container);
|
||||
$listItem->setRequestBuilder($requestBuilder);
|
||||
|
||||
$container->set('auth', static function($container): Kitsu\Auth {
|
||||
return new Kitsu\Auth($container);
|
||||
});
|
||||
$model = new Anilist\Model($listItem);
|
||||
$model->setContainer($container);
|
||||
$model->setRequestBuilder($requestBuilder);
|
||||
|
||||
$container->set('url-generator', static function($container): UrlGenerator {
|
||||
return new UrlGenerator($container);
|
||||
});
|
||||
return $model;
|
||||
});
|
||||
$container->set('settings-model', static function($container): Model\Settings {
|
||||
$model = new Model\Settings($container->get('config'));
|
||||
$model->setContainer($container);
|
||||
return $model;
|
||||
});
|
||||
|
||||
$container->set('util', static function($container): Util {
|
||||
return new Util($container);
|
||||
});
|
||||
$container->set('auth', fn ($container) => new Kitsu\Auth($container));
|
||||
|
||||
return $container;
|
||||
};
|
||||
$container->set('url-generator', fn ($container) => new UrlGenerator($container));
|
||||
|
||||
return $di($configArray);
|
||||
$container->set('util', fn ($container) => new Util($container));
|
||||
|
||||
return $container;
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ namespace Aviat\AnimeClient\Command;
|
||||
|
||||
use Aviat\Ion\Di\Exception\ContainerException;
|
||||
use Aviat\Ion\Di\Exception\NotFoundException;
|
||||
use function Aviat\AnimeClient\clearCache;
|
||||
|
||||
/**
|
||||
* Clears the API Cache
|
||||
@ -36,8 +37,17 @@ final class CacheClear extends BaseCommand {
|
||||
{
|
||||
$this->setContainer($this->setupContainer());
|
||||
|
||||
$this->container->get('cache')->clear();
|
||||
$cache = $this->container->get('cache');
|
||||
|
||||
$this->echoBox('API Cache has been cleared.');
|
||||
$cleared = clearCache($cache);
|
||||
|
||||
if ($cleared)
|
||||
{
|
||||
$this->echoBox('API Cache has been cleared.');
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->echoErrorBox('Failed to clear cache.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,10 @@
|
||||
|
||||
namespace Aviat\AnimeClient\Command;
|
||||
|
||||
use Aviat\AnimeClient\API\Kitsu;
|
||||
use Aviat\Ion\Di\Exception\ContainerException;
|
||||
use Aviat\Ion\Di\Exception\NotFoundException;
|
||||
use function Aviat\AnimeClient\clearCache;
|
||||
|
||||
/**
|
||||
* Clears the API Cache
|
||||
@ -35,30 +37,25 @@ final class CachePrime extends BaseCommand {
|
||||
public function execute(array $args, array $options = []): void
|
||||
{
|
||||
$this->setContainer($this->setupContainer());
|
||||
|
||||
$cache = $this->container->get('cache');
|
||||
|
||||
// Save the user id, if it exists, for priming the cache
|
||||
$userIdItem = $cache->getItem('kitsu-auth-token');
|
||||
$userId = $userIdItem->isHit() ? $userIdItem->get() : null;
|
||||
|
||||
$cache->clear();
|
||||
$cleared = clearCache($cache);
|
||||
if ( ! $cleared)
|
||||
{
|
||||
$this->echoErrorBox('Failed to clear cache.');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->echoBox('Cache cleared, re-priming...');
|
||||
|
||||
if ($userId !== NULL)
|
||||
{
|
||||
$userIdItem = $cache->getItem('kitsu-auth-token');
|
||||
$userIdItem->set($userId);
|
||||
$userIdItem->save();
|
||||
}
|
||||
|
||||
$kitsuModel = $this->container->get('kitsu-model');
|
||||
|
||||
// Prime anime list cache
|
||||
// Prime anime list and history cache
|
||||
$kitsuModel->getAnimeHistory();
|
||||
$kitsuModel->getFullOrganizedAnimeList();
|
||||
|
||||
// Prime manga list cache
|
||||
$kitsuModel->getMangaHistory();
|
||||
$kitsuModel->getFullOrganizedMangaList();
|
||||
|
||||
$this->echoBox('API Cache has been primed.');
|
||||
|
@ -16,19 +16,18 @@
|
||||
|
||||
namespace Aviat\AnimeClient\Command;
|
||||
|
||||
use ConsoleKit\Widgets;
|
||||
|
||||
use Aviat\AnimeClient\API\{
|
||||
Anilist\MissingIdException,
|
||||
FailedResponseException,
|
||||
JsonAPI,
|
||||
ParallelAPIRequest
|
||||
};
|
||||
use Aviat\AnimeClient\API\Anilist\Transformer\{
|
||||
AnimeListTransformer as AALT,
|
||||
MangaListTransformer as AMLT
|
||||
};
|
||||
use Aviat\AnimeClient\API\Anilist\Model as AnilistModel;
|
||||
use Aviat\AnimeClient\API\Kitsu\Model as KitsuModel;
|
||||
use Aviat\AnimeClient\API\Anilist;
|
||||
use Aviat\AnimeClient\API\Kitsu;
|
||||
use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus};
|
||||
use Aviat\AnimeClient\Enum\{APISource, ListType, SyncAction};
|
||||
use Aviat\AnimeClient\Types\FormItem;
|
||||
use Aviat\Ion\Di\Exception\ContainerException;
|
||||
use Aviat\Ion\Di\Exception\NotFoundException;
|
||||
@ -44,18 +43,24 @@ final class SyncLists extends BaseCommand {
|
||||
|
||||
/**
|
||||
* Model for making requests to Anilist API
|
||||
* @var AnilistModel
|
||||
* @var Anilist\Model
|
||||
*/
|
||||
protected AnilistModel $anilistModel;
|
||||
private Anilist\Model $anilistModel;
|
||||
|
||||
/**
|
||||
* Model for making requests to Kitsu API
|
||||
* @var KitsuModel
|
||||
* @var Kitsu\Model
|
||||
*/
|
||||
protected KitsuModel $kitsuModel;
|
||||
private Kitsu\Model $kitsuModel;
|
||||
|
||||
/**
|
||||
* Run the Kitsu <=> Anilist sync script
|
||||
* Does the Kitsu API have valid authentication?
|
||||
* @var bool
|
||||
*/
|
||||
private bool $isKitsuAuthenticated = FALSE;
|
||||
|
||||
/**
|
||||
* Sync Kitsu <=> Anilist
|
||||
*
|
||||
* @param array $args
|
||||
* @param array $options
|
||||
@ -64,6 +69,31 @@ final class SyncLists extends BaseCommand {
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function execute(array $args, array $options = []): void
|
||||
{
|
||||
$this->init();
|
||||
|
||||
foreach ([ListType::ANIME, ListType::MANGA] as $type)
|
||||
{
|
||||
// Main Sync flow
|
||||
$this->fetchCount($type);
|
||||
$rawData = $this->fetch($type);
|
||||
$normalized = $this->transform($type, $rawData);
|
||||
$compared = $this->compare($type, $normalized);
|
||||
$this->update($type, $compared);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Main sync flow methods
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Set up dependencies
|
||||
*
|
||||
* @throws ContainerException
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
protected function init(): void
|
||||
{
|
||||
$this->setContainer($this->setupContainer());
|
||||
$this->setCache($this->container->get('cache'));
|
||||
@ -71,28 +101,198 @@ final class SyncLists extends BaseCommand {
|
||||
$config = $this->container->get('config');
|
||||
$anilistEnabled = $config->get(['anilist', 'enabled']);
|
||||
|
||||
// We can't sync kitsu against itself!
|
||||
if ( ! $anilistEnabled)
|
||||
{
|
||||
$this->echoBox('Anlist API is not enabled. Can not sync.');
|
||||
return;
|
||||
$this->echoErrorBox('Anlist API is not enabled. Can not sync.');
|
||||
die();
|
||||
}
|
||||
|
||||
// Authentication is required to update Kitsu
|
||||
$this->isKitsuAuthenticated = $this->container->get('auth')->isAuthenticated();
|
||||
if ( ! $this->isKitsuAuthenticated)
|
||||
{
|
||||
$this->echoWarningBox('Kitsu is not authenticated. Kitsu list can not be updated.');
|
||||
}
|
||||
|
||||
$this->anilistModel = $this->container->get('anilist-model');
|
||||
$this->kitsuModel = $this->container->get('kitsu-model');
|
||||
|
||||
$this->sync('anime');
|
||||
$this->sync('manga');
|
||||
|
||||
$this->echoBox('Finished syncing lists');
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to synchronize external APIs
|
||||
* Get and display the count of items for each API
|
||||
*
|
||||
* @param string $type
|
||||
*/
|
||||
protected function fetchCount(string $type): void
|
||||
{
|
||||
$this->echo('Fetching List Counts');
|
||||
$progress = new Widgets\ProgressBar($this->getConsole(), 2, 50, FALSE);
|
||||
|
||||
$displayLines = [];
|
||||
|
||||
$kitsuCount = $this->fetchKitsuCount($type);
|
||||
$displayLines[] = "Number of Kitsu {$type} list items: {$kitsuCount}";
|
||||
$progress->incr();
|
||||
|
||||
$anilistCount = $this->fetchAnilistCount($type);
|
||||
$displayLines[] = "Number of Anilist {$type} list items: {$anilistCount}";
|
||||
$progress->incr();
|
||||
|
||||
$this->clearLine();
|
||||
|
||||
$this->echoBox($displayLines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list data
|
||||
*
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
protected function fetch(string $type): array
|
||||
{
|
||||
$this->echo('Fetching List Data');
|
||||
$progress = new Widgets\ProgressBar($this->getConsole(), 2, 50, FALSE);
|
||||
|
||||
$anilist = $this->fetchAnilist($type);
|
||||
$progress->incr();
|
||||
|
||||
$kitsu = $this->fetchKitsu($type);
|
||||
$progress->incr();
|
||||
|
||||
$this->clearLine();
|
||||
|
||||
return [
|
||||
'anilist' => $anilist,
|
||||
'kitsu' => $kitsu,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the list data for comparison
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
protected function transform(string $type, array $data): array
|
||||
{
|
||||
$this->echo('Normalizing List Data');
|
||||
$progress = new Widgets\ProgressBar($this->getConsole(), 2, 50, FALSE);
|
||||
|
||||
$kitsu = $this->transformKitsu($type, $data['kitsu']);
|
||||
$progress->incr();
|
||||
|
||||
$anilist = $this->transformAnilist($type, $data['anilist']);
|
||||
$progress->incr();
|
||||
|
||||
$this->clearLine();
|
||||
|
||||
return [
|
||||
'anilist' => $anilist,
|
||||
'kitsu' => $kitsu,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare the lists data
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $data
|
||||
* @return array|array[]
|
||||
*/
|
||||
protected function compare(string $type, array $data): array
|
||||
{
|
||||
$this->echo('Comparing List Items');
|
||||
|
||||
return $this->compareLists($type, $data['anilist'], $data['kitsu']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updated outdated list items
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $data
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function sync(string $type): void
|
||||
protected function update(string $type, array $data)
|
||||
{
|
||||
if ( ! empty($data['addToAnilist']))
|
||||
{
|
||||
$count = count($data['addToAnilist']);
|
||||
$this->echoBox("Adding {$count} missing {$type} list items to Anilist");
|
||||
$this->updateAnilistListItems($data['addToAnilist'], SyncAction::CREATE, $type);
|
||||
}
|
||||
|
||||
if ( ! empty($data['updateAnilist']))
|
||||
{
|
||||
$count = count($data['updateAnilist']);
|
||||
$this->echoBox("Updating {$count} outdated Anilist {$type} list items");
|
||||
$this->updateAnilistListItems($data['updateAnilist'], SyncAction::UPDATE, $type);
|
||||
}
|
||||
|
||||
if ($this->isKitsuAuthenticated)
|
||||
{
|
||||
if ( ! empty($data['addToKitsu']))
|
||||
{
|
||||
$count = count($data['addToKitsu']);
|
||||
$this->echoBox("Adding {$count} missing {$type} list items to Kitsu");
|
||||
$this->updateKitsuListItems($data['addToKitsu'], SyncAction::CREATE, $type);
|
||||
}
|
||||
|
||||
if ( ! empty($data['updateKitsu']))
|
||||
{
|
||||
$count = count($data['updateKitsu']);
|
||||
$this->echoBox("Updating {$count} outdated Kitsu {$type} list items");
|
||||
$this->updateKitsuListItems($data['updateKitsu'], SyncAction::UPDATE, $type);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->echoErrorBox('Kitsu is not authenticated, so lists can not be updated');
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Fetch helpers
|
||||
// ------------------------------------------------------------------------
|
||||
private function fetchAnilistCount(string $type)
|
||||
{
|
||||
$list = $this->fetchAnilist($type);
|
||||
|
||||
if ( ! isset($list['data']['MediaListCollection']['lists']))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
|
||||
foreach ($list['data']['MediaListCollection']['lists'] as $subList)
|
||||
{
|
||||
$count += array_reduce($subList, fn ($carry, $item) => $carry + count(array_values($item)), 0);
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function fetchAnilist(string $type): array
|
||||
{
|
||||
static $list = [
|
||||
ListType::ANIME => NULL,
|
||||
ListType::MANGA => NULL,
|
||||
];
|
||||
|
||||
// This uses a static so I don't have to fetch this list twice for a count
|
||||
if ($list[$type] === NULL)
|
||||
{
|
||||
$list[$type] = $this->anilistModel->getSyncList(strtoupper($type));
|
||||
}
|
||||
|
||||
return $list[$type];
|
||||
}
|
||||
|
||||
private function fetchKitsuCount(string $type): int
|
||||
{
|
||||
$uType = ucfirst($type);
|
||||
|
||||
@ -106,157 +306,31 @@ final class SyncLists extends BaseCommand {
|
||||
dump($e);
|
||||
}
|
||||
|
||||
|
||||
$this->echoBox("Number of Kitsu {$type} list items: {$kitsuCount}");
|
||||
|
||||
$data = $this->diffLists($type);
|
||||
|
||||
if ( ! empty($data['addToAnilist']))
|
||||
{
|
||||
$count = count($data['addToAnilist']);
|
||||
$this->echoBox("Adding {$count} missing {$type} list items to Anilist");
|
||||
$this->updateAnilistListItems($data['addToAnilist'], 'create', $type);
|
||||
}
|
||||
|
||||
if ( ! empty($data['updateAnilist']))
|
||||
{
|
||||
$count = count($data['updateAnilist']);
|
||||
$this->echoBox("Updating {$count} outdated Anilist {$type} list items");
|
||||
$this->updateAnilistListItems($data['updateAnilist'], 'update', $type);
|
||||
}
|
||||
|
||||
if ( ! empty($data['addToKitsu']))
|
||||
{
|
||||
$count = count($data['addToKitsu']);
|
||||
$this->echoBox("Adding {$count} missing {$type} list items to Kitsu");
|
||||
$this->updateKitsuListItems($data['addToKitsu'], 'create', $type);
|
||||
}
|
||||
|
||||
if ( ! empty($data['updateKitsu']))
|
||||
{
|
||||
$count = count($data['updateKitsu']);
|
||||
$this->echoBox("Updating {$count} outdated Kitsu {$type} list items");
|
||||
$this->updateKitsuListItems($data['updateKitsu'], 'update', $type);
|
||||
}
|
||||
return $kitsuCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter Kitsu mappings for the specified type
|
||||
*
|
||||
* @param array $includes
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
protected function filterMappings(array $includes, string $type = 'anime'): array
|
||||
private function fetchKitsu(string $type): array
|
||||
{
|
||||
$output = [];
|
||||
|
||||
foreach($includes as $id => $mapping)
|
||||
{
|
||||
if ($mapping['externalSite'] === "myanimelist/{$type}")
|
||||
{
|
||||
$output[$id] = $mapping;
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
return $this->kitsuModel->getSyncList($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an Anilist list for comparison
|
||||
*
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
protected function formatAnilistList(string $type): array
|
||||
// ------------------------------------------------------------------------
|
||||
// Transform Helpers
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
private function transformKitsu(string $type, array $data): array
|
||||
{
|
||||
$type = ucfirst($type);
|
||||
$method = "formatAnilist{$type}List";
|
||||
return $this->$method();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an Anilist anime list for comparison
|
||||
*
|
||||
* @return array
|
||||
* @throws ContainerException
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
protected function formatAnilistAnimeList(): array
|
||||
{
|
||||
$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 = [];
|
||||
foreach ($transformedAnilist as $item)
|
||||
{
|
||||
$output[$item['mal_id']] = $item->toArray();
|
||||
}
|
||||
|
||||
$count = count($output);
|
||||
$this->echoBox("Number of Anilist anime list items: {$count}");
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an Anilist manga list for comparison
|
||||
*
|
||||
* @return array
|
||||
* @throws ContainerException
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
protected function formatAnilistMangaList(): array
|
||||
{
|
||||
$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 = [];
|
||||
foreach ($transformedAnilist as $item)
|
||||
{
|
||||
$output[$item['mal_id']] = $item->toArray();
|
||||
}
|
||||
|
||||
$count = count($output);
|
||||
$this->echoBox("Number of Anilist manga list items: {$count}");
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a kitsu list for the sake of comparision
|
||||
*
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
protected function formatKitsuList(string $type = 'anime'): array
|
||||
{
|
||||
$method = 'getFullRaw' . ucfirst($type) . 'List';
|
||||
$data = $this->kitsuModel->$method();
|
||||
|
||||
if (empty($data))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if ( ! array_key_exists('included', $data))
|
||||
{
|
||||
dump($data);
|
||||
return [];
|
||||
}
|
||||
|
||||
$includes = JsonAPI::organizeIncludes($data['included']);
|
||||
$includes['mappings'] = $this->filterMappings($includes['mappings'], $type);
|
||||
|
||||
@ -271,7 +345,7 @@ final class SyncLists extends BaseCommand {
|
||||
|
||||
foreach ($potentialMappings as $mappingId)
|
||||
{
|
||||
if (\is_array($mappingId))
|
||||
if (is_array($mappingId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@ -298,21 +372,37 @@ final class SyncLists extends BaseCommand {
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Go through lists of the specified type, and determine what kind of action each item needs
|
||||
*
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
protected function diffLists(string $type = 'anime'): array
|
||||
private function transformAnilist(string $type, array $data): array
|
||||
{
|
||||
// Get libraryEntries with media.mappings from Kitsu
|
||||
// Organize mappings, and ignore entries without mappings
|
||||
$kitsuList = $this->formatKitsuList($type);
|
||||
$uType = ucfirst($type);
|
||||
$className = "\\Aviat\\AnimeClient\\API\\Anilist\\Transformer\\{$uType}ListTransformer";
|
||||
$transformer = new $className;
|
||||
|
||||
// Get Anilist list data
|
||||
$anilistList = $this->formatAnilistList($type);
|
||||
$firstTransformed = [];
|
||||
|
||||
foreach ($data['data']['MediaListCollection']['lists'] as $list)
|
||||
{
|
||||
$firstTransformed[] = $transformer->untransformCollection($list['entries']);
|
||||
}
|
||||
|
||||
$transformed = array_merge_recursive(...$firstTransformed);
|
||||
|
||||
// Key the array by mal_id
|
||||
$output = [];
|
||||
foreach ($transformed as $item)
|
||||
{
|
||||
$output[$item['mal_id']] = $item->toArray();
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Compare Helpers
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
private function compareLists(string $type, array $anilistList, array $kitsuList): array
|
||||
{
|
||||
$itemsToAddToAnilist = [];
|
||||
$itemsToAddToKitsu = [];
|
||||
$anilistUpdateItems = [];
|
||||
@ -320,15 +410,21 @@ final class SyncLists extends BaseCommand {
|
||||
|
||||
$malIds = array_keys($anilistList);
|
||||
$kitsuMalIds = array_map('intval', array_column($kitsuList, 'malId'));
|
||||
$missingMalIds = array_diff($malIds, $kitsuMalIds);
|
||||
$missingMalIds = array_filter(array_diff($kitsuMalIds, $malIds), fn ($id) => ! in_array($id, $kitsuMalIds));
|
||||
|
||||
// Add items on Anilist, but not Kitsu to Kitsu
|
||||
foreach($missingMalIds as $mid)
|
||||
{
|
||||
$itemsToAddToKitsu[] = array_merge($anilistList[$mid]['data'], [
|
||||
'id' => $this->kitsuModel->getKitsuIdFromMALId((string)$mid, $type),
|
||||
'type' => $type
|
||||
]);
|
||||
if ( ! array_key_exists($mid, $anilistList))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = $anilistList[$mid]['data'];
|
||||
$data['id'] = $this->kitsuModel->getKitsuIdFromMALId((string)$mid, $type);
|
||||
$data['type'] = $type;
|
||||
|
||||
$itemsToAddToKitsu[] = $data;
|
||||
}
|
||||
|
||||
foreach($kitsuList as $kitsuItem)
|
||||
@ -359,7 +455,7 @@ final class SyncLists extends BaseCommand {
|
||||
continue;
|
||||
}
|
||||
|
||||
$statusMap = ($type === 'anime') ? AnimeWatchingStatus::class : MangaReadingStatus::class;
|
||||
$statusMap = ($type === ListType::ANIME) ? AnimeWatchingStatus::class : MangaReadingStatus::class;
|
||||
|
||||
// Looks like this item only exists on Kitsu
|
||||
$kItem = $kitsuItem['data'];
|
||||
@ -392,7 +488,7 @@ final class SyncLists extends BaseCommand {
|
||||
* @param array $anilistItem
|
||||
* @return array|null
|
||||
*/
|
||||
protected function compareListItems(array $kitsuItem, array $anilistItem): ?array
|
||||
private function compareListItems(array $kitsuItem, array $anilistItem): ?array
|
||||
{
|
||||
$compareKeys = [
|
||||
'notes',
|
||||
@ -585,6 +681,10 @@ final class SyncLists extends BaseCommand {
|
||||
return $return;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Update Helpers
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create/Update list items on Kitsu
|
||||
*
|
||||
@ -593,23 +693,23 @@ final class SyncLists extends BaseCommand {
|
||||
* @param string $type
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function updateKitsuListItems(array $itemsToUpdate, string $action = 'update', string $type = 'anime'): void
|
||||
private function updateKitsuListItems(array $itemsToUpdate, string $action = SyncAction::UPDATE, string $type = ListType::ANIME): void
|
||||
{
|
||||
$requester = new ParallelAPIRequest();
|
||||
foreach($itemsToUpdate as $item)
|
||||
{
|
||||
if ($action === 'update')
|
||||
if ($action === SyncAction::UPDATE)
|
||||
{
|
||||
$requester->addRequest(
|
||||
$this->kitsuModel->updateListItem(FormItem::from($item))
|
||||
);
|
||||
}
|
||||
else if ($action === 'create')
|
||||
else if ($action === SyncAction::CREATE)
|
||||
{
|
||||
$maybeRequest = $this->kitsuModel->createListItem($item);
|
||||
if ($maybeRequest === NULL)
|
||||
{
|
||||
$this->echoBox("Skipped creating Kitsu {$type} due to missing id ¯\_(ツ)_/¯");
|
||||
$this->echoWarning("Skipped creating Kitsu {$type} due to missing id ¯\_(ツ)_/¯");
|
||||
continue;
|
||||
}
|
||||
$requester->addRequest($this->kitsuModel->createListItem($item));
|
||||
@ -625,8 +725,8 @@ final class SyncLists extends BaseCommand {
|
||||
$id = $itemsToUpdate[$key]['id'];
|
||||
if ( ! array_key_exists('errors', $responseData))
|
||||
{
|
||||
$verb = ($action === 'update') ? 'updated' : 'created';
|
||||
$this->echoBox("Successfully {$verb} Kitsu {$type} list item with id: {$id}");
|
||||
$verb = ($action === SyncAction::UPDATE) ? 'updated' : 'created';
|
||||
$this->echoSuccess("Successfully {$verb} Kitsu {$type} list item with id: {$id}");
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -637,14 +737,14 @@ final class SyncLists extends BaseCommand {
|
||||
|
||||
if ($errorTitle === 'cannot exceed length of media')
|
||||
{
|
||||
$this->echoBox("Skipped Kitsu {$type} {$id} due to episode count mismatch with other API");
|
||||
$this->echoWarning("Skipped Kitsu {$type} {$id} due to episode count mismatch with other API");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
dump($responseData);
|
||||
$verb = ($action === 'update') ? 'update' : 'create';
|
||||
$this->echoBox("Failed to {$verb} Kitsu {$type} list item with id: {$id}");
|
||||
$verb = ($action === SyncAction::UPDATE) ? SyncAction::UPDATE : SyncAction::CREATE;
|
||||
$this->echoError("Failed to {$verb} Kitsu {$type} list item with id: {$id}");
|
||||
|
||||
}
|
||||
}
|
||||
@ -657,19 +757,19 @@ final class SyncLists extends BaseCommand {
|
||||
* @param string $type
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function updateAnilistListItems(array $itemsToUpdate, string $action = 'update', string $type = 'anime'): void
|
||||
private function updateAnilistListItems(array $itemsToUpdate, string $action = SyncAction::UPDATE, string $type = ListType::ANIME): void
|
||||
{
|
||||
$requester = new ParallelAPIRequest();
|
||||
|
||||
foreach($itemsToUpdate as $item)
|
||||
{
|
||||
if ($action === 'update')
|
||||
if ($action === SyncAction::UPDATE)
|
||||
{
|
||||
$requester->addRequest(
|
||||
$this->anilistModel->updateListItem(FormItem::from($item), $type)
|
||||
);
|
||||
}
|
||||
else if ($action === 'create')
|
||||
else if ($action === SyncAction::CREATE)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -679,7 +779,7 @@ final class SyncLists extends BaseCommand {
|
||||
{
|
||||
// Case where there's a MAL mapping from Kitsu, but no equivalent Anlist item
|
||||
$id = $item['mal_id'];
|
||||
$this->echoBox("Skipping Anilist ${type} with mal_id: {$id} due to missing mapping");
|
||||
$this->echoWarning("Skipping Anilist ${type} with MAL id: {$id} due to missing mapping");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -694,15 +794,41 @@ final class SyncLists extends BaseCommand {
|
||||
|
||||
if ( ! array_key_exists('errors', $responseData))
|
||||
{
|
||||
$verb = ($action === 'update') ? 'updated' : 'created';
|
||||
$this->echoBox("Successfully {$verb} Anilist {$type} list item with id: {$id}");
|
||||
$verb = ($action === SyncAction::UPDATE) ? 'updated' : 'created';
|
||||
$this->echoSuccess("Successfully {$verb} Anilist {$type} list item with id: {$id}");
|
||||
}
|
||||
else
|
||||
{
|
||||
dump($responseData);
|
||||
$verb = ($action === 'update') ? 'update' : 'create';
|
||||
$this->echoBox("Failed to {$verb} Anilist {$type} list item with id: {$id}");
|
||||
$verb = ($action === SyncAction::UPDATE) ? SyncAction::UPDATE : SyncAction::CREATE;
|
||||
$this->echoError("Failed to {$verb} Anilist {$type} list item with id: {$id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Other Helpers
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Filter Kitsu mappings for the specified type
|
||||
*
|
||||
* @param array $includes
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
private function filterMappings(array $includes, string $type = ListType::ANIME): array
|
||||
{
|
||||
$output = [];
|
||||
|
||||
foreach($includes as $id => $mapping)
|
||||
{
|
||||
if ($mapping['externalSite'] === "myanimelist/{$type}")
|
||||
{
|
||||
$output[$id] = $mapping;
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
@ -18,13 +18,14 @@ namespace Aviat\AnimeClient;
|
||||
|
||||
use function Aviat\Ion\_dir;
|
||||
|
||||
use Aviat\AnimeClient\Enum\EventType;
|
||||
use Aura\Router\Generator;
|
||||
use Aura\Session\Segment;
|
||||
use Aviat\AnimeClient\API\Kitsu\Auth;
|
||||
use Aviat\Ion\ConfigInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
|
||||
use Aviat\Ion\Di\{
|
||||
ContainerAware,
|
||||
@ -32,6 +33,7 @@ use Aviat\Ion\Di\{
|
||||
Exception\ContainerException,
|
||||
Exception\NotFoundException
|
||||
};
|
||||
use Aviat\Ion\Event;
|
||||
use Aviat\Ion\Exception\DoubleRenderException;
|
||||
use Aviat\Ion\View\{HtmlView, HttpView, JsonView};
|
||||
use InvalidArgumentException;
|
||||
@ -51,9 +53,9 @@ class Controller {
|
||||
|
||||
/**
|
||||
* Cache manager
|
||||
* @var CacheItemPoolInterface
|
||||
* @var CacheInterface
|
||||
*/
|
||||
protected CacheItemPoolInterface $cache;
|
||||
protected CacheInterface $cache;
|
||||
|
||||
/**
|
||||
* The global configuration object
|
||||
@ -131,6 +133,10 @@ class Controller {
|
||||
'url_type' => 'anime',
|
||||
'urlGenerator' => $urlGenerator,
|
||||
];
|
||||
|
||||
// Set up 'global' events
|
||||
Event::on(EventType::CLEAR_CACHE, fn () => clearCache($this->cache));
|
||||
Event::on(EventType::RESET_CACHE_KEY, fn (string $key) => $this->cache->delete($key));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,6 +17,8 @@
|
||||
namespace Aviat\AnimeClient\Controller;
|
||||
|
||||
use Aviat\AnimeClient\Controller as BaseController;
|
||||
use Aviat\AnimeClient\Enum\EventType;
|
||||
use Aviat\Ion\Event;
|
||||
use Aviat\Ion\View\HtmlView;
|
||||
|
||||
/**
|
||||
@ -30,7 +32,10 @@ final class Misc extends BaseController {
|
||||
*/
|
||||
public function clearCache(): void
|
||||
{
|
||||
$this->cache->clear();
|
||||
$this->checkAuth();
|
||||
|
||||
Event::emit(EventType::CLEAR_CACHE);
|
||||
|
||||
$this->outputHTML('blank', [
|
||||
'title' => 'Cache cleared'
|
||||
]);
|
||||
@ -89,8 +94,6 @@ final class Misc extends BaseController {
|
||||
*/
|
||||
public function logout(): void
|
||||
{
|
||||
$this->checkAuth();
|
||||
|
||||
$auth = $this->container->get('auth');
|
||||
$auth->logout();
|
||||
|
||||
|
@ -16,12 +16,14 @@
|
||||
|
||||
namespace Aviat\AnimeClient;
|
||||
|
||||
use Aviat\AnimeClient\Enum\EventType;
|
||||
use function Aviat\Ion\_dir;
|
||||
|
||||
use Aura\Router\{Map, Matcher, Route, Rule};
|
||||
|
||||
use Aviat\AnimeClient\API\FailedResponseException;
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\Event;
|
||||
use Aviat\Ion\Friend;
|
||||
use Aviat\Ion\Type\StringType;
|
||||
use LogicException;
|
||||
@ -161,10 +163,7 @@ final class Dispatcher extends RoutingBase {
|
||||
throw new LogicException('Missing controller');
|
||||
}
|
||||
|
||||
if (array_key_exists('controller', $route->attributes))
|
||||
{
|
||||
$controllerName = $route->attributes['controller'];
|
||||
}
|
||||
$controllerName = $route->attributes['controller'];
|
||||
|
||||
// Get the full namespace for a controller if a short name is given
|
||||
if (strpos($controllerName, '\\') === FALSE)
|
||||
@ -283,7 +282,7 @@ final class Dispatcher extends RoutingBase {
|
||||
$logger->debug('Dispatcher - controller arguments', $params);
|
||||
}
|
||||
|
||||
\call_user_func_array([$controller, $method], $params);
|
||||
call_user_func_array([$controller, $method], $params);
|
||||
}
|
||||
catch (FailedResponseException $e)
|
||||
{
|
||||
@ -293,7 +292,14 @@ final class Dispatcher extends RoutingBase {
|
||||
'API request timed out',
|
||||
'Failed to retrieve data from API (╯°□°)╯︵ ┻━┻');
|
||||
}
|
||||
|
||||
/* finally
|
||||
{
|
||||
// Log out on session/api token expiration
|
||||
Event::on(EventType::UNAUTHORIZED, static function () {
|
||||
$controllerName = DEFAULT_CONTROLLER;
|
||||
(new $controllerName($this->container))->logout();
|
||||
});
|
||||
} */
|
||||
}
|
||||
|
||||
/**
|
||||
|
27
src/AnimeClient/Enum/APISource.php
Normal file
27
src/AnimeClient/Enum/APISource.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7.4
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2020 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 5
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\Enum;
|
||||
|
||||
use Aviat\Ion\Enum as BaseEnum;
|
||||
|
||||
/**
|
||||
* Types of lists
|
||||
*/
|
||||
final class APISource extends BaseEnum {
|
||||
public const KITSU = 'kitsu';
|
||||
public const ANILIST = 'anilist';
|
||||
}
|
25
src/AnimeClient/Enum/EventType.php
Normal file
25
src/AnimeClient/Enum/EventType.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7.4
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2020 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 5
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\Enum;
|
||||
|
||||
use Aviat\Ion\Enum as BaseEnum;
|
||||
|
||||
final class EventType extends BaseEnum {
|
||||
public const CLEAR_CACHE = '::clear-cache::';
|
||||
public const RESET_CACHE_KEY = '::reset-cache-key::';
|
||||
public const UNAUTHORIZED = '::unauthorized::';
|
||||
}
|
28
src/AnimeClient/Enum/ListType.php
Normal file
28
src/AnimeClient/Enum/ListType.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7.4
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2020 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 5
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\Enum;
|
||||
|
||||
use Aviat\Ion\Enum as BaseEnum;
|
||||
|
||||
/**
|
||||
* Types of lists
|
||||
*/
|
||||
final class ListType extends BaseEnum {
|
||||
public const ANIME = 'anime';
|
||||
public const DRAMA = 'drama';
|
||||
public const MANGA = 'manga';
|
||||
}
|
28
src/AnimeClient/Enum/SyncAction.php
Normal file
28
src/AnimeClient/Enum/SyncAction.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7.4
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2020 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 5
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\Enum;
|
||||
|
||||
use Aviat\Ion\Enum as BaseEnum;
|
||||
|
||||
/**
|
||||
* Types of actions when syncing lists from different APIs
|
||||
*/
|
||||
final class SyncAction extends BaseEnum {
|
||||
public const CREATE = 'create';
|
||||
public const UPDATE = 'update';
|
||||
public const DELETE = 'delete';
|
||||
}
|
@ -105,7 +105,10 @@ final class MenuGenerator extends UrlGenerator {
|
||||
$has = StringType::from($this->path())->contains($path);
|
||||
$selected = ($has && mb_strlen($this->path()) >= mb_strlen($path));
|
||||
|
||||
$link = $this->helper->a($this->url($path), $title);
|
||||
$linkAttrs = ($selected)
|
||||
? ['aria-current' => 'location']
|
||||
: [];
|
||||
$link = $this->helper->a($this->url($path), $title, $linkAttrs);
|
||||
|
||||
$attrs = $selected
|
||||
? ['class' => 'selected']
|
||||
|
@ -97,6 +97,11 @@ class Anime extends AbstractType {
|
||||
*/
|
||||
public array $titles = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public array $titles_more = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
|
@ -23,10 +23,10 @@ final class AnimePage extends Anime {
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $characters;
|
||||
public array $characters = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $staff;
|
||||
public array $staff = [];
|
||||
}
|
@ -28,7 +28,7 @@ class FormItemData extends AbstractType {
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public bool $private = FALSE;
|
||||
public ?bool $private = FALSE;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
|
@ -54,6 +54,29 @@ class Util {
|
||||
$this->setContainer($container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolutely equal?
|
||||
*
|
||||
* @param $left
|
||||
* @param $right
|
||||
* @return bool
|
||||
*/
|
||||
public static function eq($left, $right): bool
|
||||
{
|
||||
return $left === $right;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set aria-current attribute based on a condition check
|
||||
*
|
||||
* @param bool $condition
|
||||
* @return string
|
||||
*/
|
||||
public static function ariaCurrent(bool $condition): string
|
||||
{
|
||||
return $condition ? 'true' : 'false';
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML selection helper function
|
||||
*
|
||||
@ -63,7 +86,7 @@ class Util {
|
||||
*/
|
||||
public static function isSelected(string $left, string $right): string
|
||||
{
|
||||
return ($left === $right) ? 'selected' : '';
|
||||
return static::eq($left, $right) ? 'selected' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -92,7 +92,6 @@ const SETTINGS_MAP = [
|
||||
'title' => 'Cache Type',
|
||||
'description' => 'The Cache backend',
|
||||
'options' => [
|
||||
'APCu' => 'apcu',
|
||||
'Memcached' => 'memcached',
|
||||
'Redis' => 'redis',
|
||||
'No Cache' => 'null'
|
||||
|
@ -26,7 +26,7 @@ trait ContainerAware {
|
||||
*
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
protected $container;
|
||||
protected ContainerInterface $container;
|
||||
|
||||
/**
|
||||
* Set the container for the current object
|
||||
|
@ -17,6 +17,7 @@
|
||||
namespace Aviat\Ion;
|
||||
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
|
||||
/**
|
||||
* Class emulating an enumeration type
|
||||
@ -27,7 +28,7 @@ abstract class Enum {
|
||||
* Return the list of constant values for the Enum
|
||||
*
|
||||
* @return array
|
||||
* @throws \ReflectionException
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public static function getConstList(): array
|
||||
{
|
||||
@ -48,12 +49,12 @@ abstract class Enum {
|
||||
*
|
||||
* @param mixed $key
|
||||
* @return boolean
|
||||
* @throws \ReflectionException
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public static function isValid($key): bool
|
||||
{
|
||||
$values = array_values(static::getConstList());
|
||||
return \in_array($key, $values, TRUE);
|
||||
return in_array($key, $values, TRUE);
|
||||
}
|
||||
}
|
||||
// End of Enum.php
|
55
src/Ion/Event.php
Normal file
55
src/Ion/Event.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7.4
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2020 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 5
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\Ion;
|
||||
|
||||
/**
|
||||
* A basic event handler
|
||||
*/
|
||||
class Event {
|
||||
private static array $eventMap = [];
|
||||
|
||||
/**
|
||||
* Subscribe to an event
|
||||
*
|
||||
* @param string $eventName
|
||||
* @param callable $handler
|
||||
*/
|
||||
public static function on(string $eventName, callable $handler): void
|
||||
{
|
||||
if ( ! array_key_exists($eventName, static::$eventMap))
|
||||
{
|
||||
static::$eventMap[$eventName] = [];
|
||||
}
|
||||
|
||||
static::$eventMap[$eventName][] = $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire off an event
|
||||
*
|
||||
* @param string $eventName
|
||||
* @param array $args
|
||||
*/
|
||||
public static function emit(string $eventName, array $args = []): void
|
||||
{
|
||||
// Call each subscriber with the provided arguments
|
||||
if (array_key_exists($eventName, static::$eventMap))
|
||||
{
|
||||
array_walk(static::$eventMap[$eventName], fn ($fn) => $fn(...$args));
|
||||
}
|
||||
}
|
||||
}
|
@ -29,5 +29,8 @@ titles:
|
||||
- 'Attack on Titan'
|
||||
- 'Shingeki no Kyojin'
|
||||
- 進撃の巨人
|
||||
titles_more:
|
||||
2: 'Shingeki no Kyojin'
|
||||
3: 進撃の巨人
|
||||
trailer_id: n4Nj6Y_SNYI
|
||||
url: 'https://kitsu.io/anime/attack-on-titan'
|
||||
|
@ -55,7 +55,7 @@ class MenuHelperTest extends AnimeClientTestCase {
|
||||
$expected['no selection'] = $this->helper->ul()->__toString();
|
||||
|
||||
// selected
|
||||
$link = $this->helper->a($this->urlGenerator->url('/foobar'), 'Index');
|
||||
$link = $this->helper->a($this->urlGenerator->url('/foobar'), 'Index', ['aria-current' => 'location']);
|
||||
$this->helper->ul()->rawItem($link, ['class' => 'selected']);
|
||||
$expected['selected'] = $this->helper->ul()->__toString();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user