Do you wish to register an account?
Browse Source

Merge remote-tracking branch 'origin/develop'

master
Timothy Warren 2 months ago
parent
commit
9297ff4b94
53 changed files with 1528 additions and 901 deletions
  1. +3
    -1
      .gitignore
  2. +5
    -4
      app/bootstrap.php
  3. +1
    -1
      app/config/cache.toml.example
  4. +9
    -1
      app/views/anime/cover-item.php
  5. +1
    -1
      app/views/anime/details.php
  6. +11
    -1
      app/views/anime/list.php
  7. +1
    -1
      app/views/collection/list-all.php
  8. +2
    -2
      app/views/collection/list-item.php
  9. +15
    -6
      app/views/main-menu.php
  10. +9
    -1
      app/views/manga/cover.php
  11. +8
    -2
      app/views/manga/list.php
  12. +5
    -0
      app/views/settings/_field.php
  13. +8
    -8
      app/views/settings/_form.php
  14. +20
    -0
      app/views/settings/_subfield.php
  15. +1
    -1
      composer.json
  16. +1
    -1
      index.php
  17. +5
    -2
      src/AnimeClient/API/APIRequestBuilder.php
  18. +2
    -2
      src/AnimeClient/API/Anilist/Transformer/AnimeListTransformer.php
  19. +7
    -2
      src/AnimeClient/API/Anilist/Transformer/MangaListTransformer.php
  20. +37
    -8
      src/AnimeClient/API/CacheTrait.php
  21. +88
    -68
      src/AnimeClient/API/Kitsu.php
  22. +73
    -74
      src/AnimeClient/API/Kitsu/Auth.php
  23. +229
    -0
      src/AnimeClient/API/Kitsu/KitsuRequestBuilder.php
  24. +0
    -222
      src/AnimeClient/API/Kitsu/KitsuTrait.php
  25. +9
    -19
      src/AnimeClient/API/Kitsu/ListItem.php
  26. +237
    -143
      src/AnimeClient/API/Kitsu/Model.php
  27. +2
    -0
      src/AnimeClient/API/Kitsu/Transformer/AnimeTransformer.php
  28. +1
    -1
      src/AnimeClient/API/Kitsu/Transformer/CharacterTransformer.php
  29. +2
    -6
      src/AnimeClient/API/Kitsu/Transformer/MangaTransformer.php
  30. +1
    -3
      src/AnimeClient/API/ParallelAPIRequest.php
  31. +34
    -2
      src/AnimeClient/AnimeClient.php
  32. +155
    -117
      src/AnimeClient/Command/BaseCommand.php
  33. +12
    -2
      src/AnimeClient/Command/CacheClear.php
  34. +11
    -14
      src/AnimeClient/Command/CachePrime.php
  35. +288
    -162
      src/AnimeClient/Command/SyncLists.php
  36. +9
    -3
      src/AnimeClient/Controller.php
  37. +6
    -3
      src/AnimeClient/Controller/Misc.php
  38. +12
    -6
      src/AnimeClient/Dispatcher.php
  39. +27
    -0
      src/AnimeClient/Enum/APISource.php
  40. +25
    -0
      src/AnimeClient/Enum/EventType.php
  41. +28
    -0
      src/AnimeClient/Enum/ListType.php
  42. +28
    -0
      src/AnimeClient/Enum/SyncAction.php
  43. +4
    -1
      src/AnimeClient/MenuGenerator.php
  44. +5
    -0
      src/AnimeClient/Types/Anime.php
  45. +2
    -2
      src/AnimeClient/Types/AnimePage.php
  46. +1
    -1
      src/AnimeClient/Types/FormItemData.php
  47. +24
    -1
      src/AnimeClient/Util.php
  48. +0
    -1
      src/AnimeClient/constants.php
  49. +1
    -1
      src/Ion/Di/ContainerAware.php
  50. +4
    -3
      src/Ion/Enum.php
  51. +55
    -0
      src/Ion/Event.php
  52. +3
    -0
      tests/AnimeClient/API/Kitsu/Transformer/__snapshots__/AnimeTransformerTest__testTransform__1.yml
  53. +1
    -1
      tests/AnimeClient/Helper/MenuHelperTest.php

+ 3
- 1
.gitignore View File

@@ -146,4 +146,6 @@ public/images/manga/**
public/images/characters/**
public/images/people/**
public/mal_mappings.json
.phpunit.result.cache
.phpunit.result.cache

.is-dev

+ 5
- 4
app/bootstrap.php View File

@@ -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();


+ 1
- 1
app/config/cache.toml.example View File

@@ -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"



+ 9
- 1
app/views/anime/cover-item.php View File

@@ -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 ?>



+ 1
- 1
app/views/anime/details.php View File

@@ -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 />


+ 11
- 1
app/views/anime/list.php View File

@@ -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 ?>


+ 1
- 1
app/views/collection/list-all.php View File

@@ -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 ?>


+ 2
- 2
app/views/collection/list-item.php View File

@@ -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>

+ 15
- 6
app/views/main-menu.php View File

@@ -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 ?>

+ 9
- 1
app/views/manga/cover.php View File

@@ -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 ?>



+ 8
- 2
app/views/manga/list.php View File

@@ -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
- 0
app/views/settings/_field.php View File

@@ -0,0 +1,5 @@
<article>
<label for="<?= $fieldName ?>"><?= $field['title'] ?></label><br />
<small><?= $field['description'] ?></small><br />
<?= $helper->field($fieldName, $field); ?>
</article>

+ 8
- 8
app/views/settings/_form.php View File

@@ -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
- 0
app/views/settings/_subfield.php View 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 ?>

+ 1
- 1
composer.json View File

@@ -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": "*",


+ 1
- 1
index.php View File

@@ -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);


+ 5
- 2
src/AnimeClient/API/APIRequestBuilder.php View File

@@ -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);
}

+ 2
- 2
src/AnimeClient/API/Anilist/Transformer/AnimeListTransformer.php View File

@@ -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)


+ 7
- 2
src/AnimeClient/API/Anilist/Transformer/MangaListTransformer.php View File

@@ -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),


+ 37
- 8
src/AnimeClient/API/CacheTrait.php View File

@@ -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,
];


+ 88
- 68
src/AnimeClient/API/Kitsu.php View File

@@ -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
*


+ 73
- 74
src/AnimeClient/API/Kitsu/Auth.php View File

@@ -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 getAuthToken(): ?string
{
if (PHP_SAPI === 'cli')
{
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
*/
public function get_auth_token()
private function getRefreshToken(): ?string
{
$now = time();
if (PHP_SAPI === 'cli')
{
return $this->segment->get('refresh_token')
?? $this->cache->get(K::AUTH_TOKEN_REFRESH_CACHE_KEY, NULL);
}

$token = $this->segment->get('auth_token', FALSE);
$refreshToken = $this->segment->get('refresh_token', FALSE);
$isExpired = time() > $this->segment->get('auth_token_expires', $now + 5000);
return $this->segment->get('refresh_token');
}

// Attempt to re-authenticate with refresh token
/* if ($isExpired && $refreshToken)
/**
* Save the new authentication information
*
* @param $auth
* @return bool
* @throws InvalidArgumentException
*/
private function storeAuth($auth): bool
{
if (FALSE !== $auth)
{
if ($this->reAuthenticate($refreshToken))
$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

+ 229
- 0
src/AnimeClient/API/Kitsu/KitsuRequestBuilder.php View File

@@ -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();
}
}


}

+ 0
- 222
src/AnimeClient/API/Kitsu/KitsuTrait.php View File

@@ -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);
}
}

+ 9
- 19
src/AnimeClient/API/Kitsu/ListItem.php View File

@@ -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);

if ($sessionSegment->get('auth_token') !== NULL) {
$token = $sessionSegment->get('auth_token');
return "bearer {$token}";
}
$auth = $this->getContainer()->get('auth');
$token = $auth->getAuthToken();

if ($cacheItem->isHit()) {
$token = $cacheItem->get();
if ( ! empty($token)) {
return "bearer {$token}";
}

return FALSE;
return NULL;
}
}

+ 237
- 143
src/AnimeClient/API/Kitsu/Model.php View File

@@ -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