- 0): ?>li>Rewatched = $item['rewatched'] ?> time(s)
+ 0): ?>
+
+ - Rewatched once
+
+ - Rewatched twice
+
+ - Rewatched thrice
+
+ - Rewatched = $item['rewatched'] ?> times
+
+
- = ucfirst($attr); ?>
diff --git a/app/views/collection/list-all.php b/app/views/collection/list-all.php
index c7ff2d3d..b14db2b1 100644
--- a/app/views/collection/list-all.php
+++ b/app/views/collection/list-all.php
@@ -35,7 +35,7 @@
= $item['episode_length'] ?> |
= $item['show_type'] ?> |
= $item['age_rating'] ?> |
- = $item['notes'] ?> |
+ = nl2br($item['notes'], TRUE) ?> |
= implode(', ', $item['genres']) ?> |
diff --git a/app/views/collection/list-item.php b/app/views/collection/list-item.php
index d163ae42..9c8ac967 100644
--- a/app/views/collection/list-item.php
+++ b/app/views/collection/list-item.php
@@ -11,10 +11,10 @@
= ! empty($item['alternate_title']) ? '
' . $item['alternate_title'] . '' : '' ?>
- = $item['episode_count'] ?> |
= ($item['episode_count'] > 1) ? $item['episode_count'] : '-' ?> |
+ = $item['episode_length'] ?> |
= $item['show_type'] ?> |
= $item['age_rating'] ?> |
- = $item['notes'] ?> |
+ = nl2br($item['notes'], TRUE) ?> |
= implode(', ', $item['genres']) ?> |
\ No newline at end of file
diff --git a/app/views/main-menu.php b/app/views/main-menu.php
index 1f38d6ab..39d68032 100644
--- a/app/views/main-menu.php
+++ b/app/views/main-menu.php
@@ -14,7 +14,8 @@ $hasManga = stripos($_SERVER['REQUEST_URI'], 'manga') !== FALSE;
= $helper->a(
$urlGenerator->defaultUrl($url_type),
- $whose . ucfirst($url_type) . ' List'
+ $whose . ucfirst($url_type) . ' List',
+ ['aria-current'=> 'page']
) ?>
get("show_{$url_type}_collection")): ?>
[= $helper->a(
@@ -35,7 +36,8 @@ $hasManga = stripos($_SERVER['REQUEST_URI'], 'manga') !== FALSE;
= $helper->a(
$url->generate("{$url_type}.collection.view") . $extraSegment,
- $whose . ucfirst($url_type) . ' Collection'
+ $whose . ucfirst($url_type) . ' Collection',
+ ['aria-current'=> 'page']
) ?>
get("show_{$other_type}_collection")): ?>
[= $helper->a(
@@ -79,15 +81,22 @@ $hasManga = stripos($_SERVER['REQUEST_URI'], 'manga') !== FALSE;
+get('util')->isViewPage() && ($hasAnime || $hasManga)): ?>
+
diff --git a/app/views/manga/cover.php b/app/views/manga/cover.php
index 0f183334..7d3edbae 100644
--- a/app/views/manga/cover.php
+++ b/app/views/manga/cover.php
@@ -68,7 +68,15 @@
0): ?>
-
Reread = $item['reread'] ?> time(s)
+
+
Reread once
+
+
Reread twice
+
+
Reread thrice
+
+
Reread = $item['reread'] ?> times
+
diff --git a/app/views/manga/list.php b/app/views/manga/list.php
index 4cba2b88..02aa879d 100644
--- a/app/views/manga/list.php
+++ b/app/views/manga/list.php
@@ -53,8 +53,14 @@
= $item['volumes']['total'] ?> |
- 0): ?>
- - Reread = $item['reread'] ?> time(s)
+
+ - Reread once
+
+ - Reread twice
+
+ - Reread thrice
+ 3): ?>
+ - Reread = $item['reread'] ?> times
diff --git a/app/views/settings/_field.php b/app/views/settings/_field.php
new file mode 100644
index 00000000..94d5e031
--- /dev/null
+++ b/app/views/settings/_field.php
@@ -0,0 +1,5 @@
+
+
+ = $field['description'] ?>
+ = $helper->field($fieldName, $field); ?>
+
\ No newline at end of file
diff --git a/app/views/settings/_form.php b/app/views/settings/_form.php
index eccaf1ec..9112d175 100644
--- a/app/views/settings/_form.php
+++ b/app/views/settings/_form.php
@@ -6,19 +6,19 @@
?>
$field): ?>
-
+
-
-
- = $field['description'] ?>
- = $helper->field($fieldname, $field); ?>
-
+
- field($fieldname, $field); ?>
+ field($fieldName, $field); ?>
diff --git a/app/views/settings/_subfield.php b/app/views/settings/_subfield.php
new file mode 100644
index 00000000..0de2297c
--- /dev/null
+++ b/app/views/settings/_subfield.php
@@ -0,0 +1,20 @@
+
+
+ $field): ?>
+
+
+
+
+ field($fieldName, $field); ?>
+
+
\ No newline at end of file
diff --git a/composer.json b/composer.json
index b4b2c23c..cea18548 100644
--- a/composer.json
+++ b/composer.json
@@ -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": "*",
diff --git a/index.php b/index.php
index 93f533d9..be33f4aa 100644
--- a/index.php
+++ b/index.php
@@ -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);
diff --git a/src/AnimeClient/API/APIRequestBuilder.php b/src/AnimeClient/API/APIRequestBuilder.php
index 2e018fb0..4d4113f9 100644
--- a/src/AnimeClient/API/APIRequestBuilder.php
+++ b/src/AnimeClient/API/APIRequestBuilder.php
@@ -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);
}
diff --git a/src/AnimeClient/API/Anilist/Transformer/AnimeListTransformer.php b/src/AnimeClient/API/Anilist/Transformer/AnimeListTransformer.php
index 464d423c..c09f63c1 100644
--- a/src/AnimeClient/API/Anilist/Transformer/AnimeListTransformer.php
+++ b/src/AnimeClient/API/Anilist/Transformer/AnimeListTransformer.php
@@ -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)
diff --git a/src/AnimeClient/API/Anilist/Transformer/MangaListTransformer.php b/src/AnimeClient/API/Anilist/Transformer/MangaListTransformer.php
index 184dd73a..f60c7025 100644
--- a/src/AnimeClient/API/Anilist/Transformer/MangaListTransformer.php
+++ b/src/AnimeClient/API/Anilist/Transformer/MangaListTransformer.php
@@ -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),
diff --git a/src/AnimeClient/API/CacheTrait.php b/src/AnimeClient/API/CacheTrait.php
index bc1e945d..dbb01271 100644
--- a/src/AnimeClient/API/CacheTrait.php
+++ b/src/AnimeClient/API/CacheTrait.php
@@ -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,
];
diff --git a/src/AnimeClient/API/Kitsu.php b/src/AnimeClient/API/Kitsu.php
index d7c6c69e..3716d62e 100644
--- a/src/AnimeClient/API/Kitsu.php
+++ b/src/AnimeClient/API/Kitsu.php
@@ -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
*
diff --git a/src/AnimeClient/API/Kitsu/Auth.php b/src/AnimeClient/API/Kitsu/Auth.php
index c0997f6c..676fc141 100644
--- a/src/AnimeClient/API/Kitsu/Auth.php
+++ b/src/AnimeClient/API/Kitsu/Auth.php
@@ -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
\ No newline at end of file
diff --git a/src/AnimeClient/API/Kitsu/KitsuRequestBuilder.php b/src/AnimeClient/API/Kitsu/KitsuRequestBuilder.php
index aa105062..4e6b7c21 100644
--- a/src/AnimeClient/API/Kitsu/KitsuRequestBuilder.php
+++ b/src/AnimeClient/API/Kitsu/KitsuRequestBuilder.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();
+ }
+ }
+
+
}
\ No newline at end of file
diff --git a/src/AnimeClient/API/Kitsu/KitsuTrait.php b/src/AnimeClient/API/Kitsu/KitsuTrait.php
index 2503a25b..c4d477af 100644
--- a/src/AnimeClient/API/Kitsu/KitsuTrait.php
+++ b/src/AnimeClient/API/Kitsu/KitsuTrait.php
@@ -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);
- }
}
\ No newline at end of file
diff --git a/src/AnimeClient/API/Kitsu/ListItem.php b/src/AnimeClient/API/Kitsu/ListItem.php
index 4b11080e..76fc7fef 100644
--- a/src/AnimeClient/API/Kitsu/ListItem.php
+++ b/src/AnimeClient/API/Kitsu/ListItem.php
@@ -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;
}
}
\ No newline at end of file
diff --git a/src/AnimeClient/API/Kitsu/Model.php b/src/AnimeClient/API/Kitsu/Model.php
index 9b43dc1d..671a04d6 100644
--- a/src/AnimeClient/API/Kitsu/Model.php
+++ b/src/AnimeClient/API/Kitsu/Model.php
@@ -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]);
+ }
}
\ No newline at end of file
diff --git a/src/AnimeClient/API/Kitsu/Transformer/AnimeTransformer.php b/src/AnimeClient/API/Kitsu/Transformer/AnimeTransformer.php
index e0b049b0..838f6549 100644
--- a/src/AnimeClient/API/Kitsu/Transformer/AnimeTransformer.php
+++ b/src/AnimeClient/API/Kitsu/Transformer/AnimeTransformer.php
@@ -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']}",
]);
diff --git a/src/AnimeClient/API/Kitsu/Transformer/CharacterTransformer.php b/src/AnimeClient/API/Kitsu/Transformer/CharacterTransformer.php
index 17a259fc..49fb45f7 100644
--- a/src/AnimeClient/API/Kitsu/Transformer/CharacterTransformer.php
+++ b/src/AnimeClient/API/Kitsu/Transformer/CharacterTransformer.php
@@ -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'];
diff --git a/src/AnimeClient/API/Kitsu/Transformer/MangaTransformer.php b/src/AnimeClient/API/Kitsu/Transformer/MangaTransformer.php
index c5018318..2d428537 100644
--- a/src/AnimeClient/API/Kitsu/Transformer/MangaTransformer.php
+++ b/src/AnimeClient/API/Kitsu/Transformer/MangaTransformer.php
@@ -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);
diff --git a/src/AnimeClient/API/ParallelAPIRequest.php b/src/AnimeClient/API/ParallelAPIRequest.php
index 83389c92..d781471f 100644
--- a/src/AnimeClient/API/ParallelAPIRequest.php
+++ b/src/AnimeClient/API/ParallelAPIRequest.php
@@ -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));
diff --git a/src/AnimeClient/AnimeClient.php b/src/AnimeClient/AnimeClient.php
index 4fca5075..42eadf73 100644
--- a/src/AnimeClient/AnimeClient.php
+++ b/src/AnimeClient/AnimeClient.php
@@ -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';
}
@@ -345,4 +350,31 @@ 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;
}
\ No newline at end of file
diff --git a/src/AnimeClient/Command/BaseCommand.php b/src/AnimeClient/Command/BaseCommand.php
index 17e65b24..7f03656d 100644
--- a/src/AnimeClient/Command/BaseCommand.php
+++ b/src/AnimeClient/Command/BaseCommand.php
@@ -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;
}
}
\ No newline at end of file
diff --git a/src/AnimeClient/Command/CacheClear.php b/src/AnimeClient/Command/CacheClear.php
index 2d3495e5..f56a93ac 100644
--- a/src/AnimeClient/Command/CacheClear.php
+++ b/src/AnimeClient/Command/CacheClear.php
@@ -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.');
+ }
}
}
diff --git a/src/AnimeClient/Command/CachePrime.php b/src/AnimeClient/Command/CachePrime.php
index 2b25cca7..7072fda9 100644
--- a/src/AnimeClient/Command/CachePrime.php
+++ b/src/AnimeClient/Command/CachePrime.php
@@ -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.');
diff --git a/src/AnimeClient/Command/SyncLists.php b/src/AnimeClient/Command/SyncLists.php
index b29e1226..5daaa9a3 100644
--- a/src/AnimeClient/Command/SyncLists.php
+++ b/src/AnimeClient/Command/SyncLists.php
@@ -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;
+ }
}
diff --git a/src/AnimeClient/Controller.php b/src/AnimeClient/Controller.php
index 46ae7e4f..b15ed34f 100644
--- a/src/AnimeClient/Controller.php
+++ b/src/AnimeClient/Controller.php
@@ -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));
}
/**
diff --git a/src/AnimeClient/Controller/Misc.php b/src/AnimeClient/Controller/Misc.php
index 035248f4..0e5a5d42 100644
--- a/src/AnimeClient/Controller/Misc.php
+++ b/src/AnimeClient/Controller/Misc.php
@@ -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();
diff --git a/src/AnimeClient/Dispatcher.php b/src/AnimeClient/Dispatcher.php
index ecb650a3..e8b58546 100644
--- a/src/AnimeClient/Dispatcher.php
+++ b/src/AnimeClient/Dispatcher.php
@@ -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();
+ });
+ } */
}
/**
diff --git a/src/AnimeClient/Enum/APISource.php b/src/AnimeClient/Enum/APISource.php
new file mode 100644
index 00000000..65549401
--- /dev/null
+++ b/src/AnimeClient/Enum/APISource.php
@@ -0,0 +1,27 @@
+
+ * @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';
+}
\ No newline at end of file
diff --git a/src/AnimeClient/Enum/EventType.php b/src/AnimeClient/Enum/EventType.php
new file mode 100644
index 00000000..5edf0c9f
--- /dev/null
+++ b/src/AnimeClient/Enum/EventType.php
@@ -0,0 +1,25 @@
+
+ * @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::';
+}
\ No newline at end of file
diff --git a/src/AnimeClient/Enum/ListType.php b/src/AnimeClient/Enum/ListType.php
new file mode 100644
index 00000000..fc13ac37
--- /dev/null
+++ b/src/AnimeClient/Enum/ListType.php
@@ -0,0 +1,28 @@
+
+ * @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';
+}
\ No newline at end of file
diff --git a/src/AnimeClient/Enum/SyncAction.php b/src/AnimeClient/Enum/SyncAction.php
new file mode 100644
index 00000000..1fe2156b
--- /dev/null
+++ b/src/AnimeClient/Enum/SyncAction.php
@@ -0,0 +1,28 @@
+
+ * @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';
+}
\ No newline at end of file
diff --git a/src/AnimeClient/MenuGenerator.php b/src/AnimeClient/MenuGenerator.php
index a42a9787..d08aa64e 100644
--- a/src/AnimeClient/MenuGenerator.php
+++ b/src/AnimeClient/MenuGenerator.php
@@ -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']
diff --git a/src/AnimeClient/Types/Anime.php b/src/AnimeClient/Types/Anime.php
index b9435937..af6b649a 100644
--- a/src/AnimeClient/Types/Anime.php
+++ b/src/AnimeClient/Types/Anime.php
@@ -97,6 +97,11 @@ class Anime extends AbstractType {
*/
public array $titles = [];
+ /**
+ * @var array
+ */
+ public array $titles_more = [];
+
/**
* @var string
*/
diff --git a/src/AnimeClient/Types/AnimePage.php b/src/AnimeClient/Types/AnimePage.php
index b1a3b5d9..ef530373 100644
--- a/src/AnimeClient/Types/AnimePage.php
+++ b/src/AnimeClient/Types/AnimePage.php
@@ -23,10 +23,10 @@ final class AnimePage extends Anime {
/**
* @var array
*/
- public $characters;
+ public array $characters = [];
/**
* @var array
*/
- public $staff;
+ public array $staff = [];
}
\ No newline at end of file
diff --git a/src/AnimeClient/Types/FormItemData.php b/src/AnimeClient/Types/FormItemData.php
index 1058d0e8..e9956a92 100644
--- a/src/AnimeClient/Types/FormItemData.php
+++ b/src/AnimeClient/Types/FormItemData.php
@@ -28,7 +28,7 @@ class FormItemData extends AbstractType {
/**
* @var bool
*/
- public bool $private = FALSE;
+ public ?bool $private = FALSE;
/**
* @var int
diff --git a/src/AnimeClient/Util.php b/src/AnimeClient/Util.php
index 7e4d6f37..d427c3d3 100644
--- a/src/AnimeClient/Util.php
+++ b/src/AnimeClient/Util.php
@@ -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' : '';
}
/**
diff --git a/src/AnimeClient/constants.php b/src/AnimeClient/constants.php
index ffde78d4..51f994fa 100644
--- a/src/AnimeClient/constants.php
+++ b/src/AnimeClient/constants.php
@@ -92,7 +92,6 @@ const SETTINGS_MAP = [
'title' => 'Cache Type',
'description' => 'The Cache backend',
'options' => [
- 'APCu' => 'apcu',
'Memcached' => 'memcached',
'Redis' => 'redis',
'No Cache' => 'null'
diff --git a/src/Ion/Di/ContainerAware.php b/src/Ion/Di/ContainerAware.php
index 3b43a0cf..4da0f84d 100644
--- a/src/Ion/Di/ContainerAware.php
+++ b/src/Ion/Di/ContainerAware.php
@@ -26,7 +26,7 @@ trait ContainerAware {
*
* @var ContainerInterface
*/
- protected $container;
+ protected ContainerInterface $container;
/**
* Set the container for the current object
diff --git a/src/Ion/Enum.php b/src/Ion/Enum.php
index e51bfc6a..d0b3a610 100644
--- a/src/Ion/Enum.php
+++ b/src/Ion/Enum.php
@@ -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
\ No newline at end of file
diff --git a/src/Ion/Event.php b/src/Ion/Event.php
new file mode 100644
index 00000000..9cf226d7
--- /dev/null
+++ b/src/Ion/Event.php
@@ -0,0 +1,55 @@
+
+ * @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));
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/AnimeClient/API/Kitsu/Transformer/__snapshots__/AnimeTransformerTest__testTransform__1.yml b/tests/AnimeClient/API/Kitsu/Transformer/__snapshots__/AnimeTransformerTest__testTransform__1.yml
index f736c331..3e28a782 100644
--- a/tests/AnimeClient/API/Kitsu/Transformer/__snapshots__/AnimeTransformerTest__testTransform__1.yml
+++ b/tests/AnimeClient/API/Kitsu/Transformer/__snapshots__/AnimeTransformerTest__testTransform__1.yml
@@ -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'
diff --git a/tests/AnimeClient/Helper/MenuHelperTest.php b/tests/AnimeClient/Helper/MenuHelperTest.php
index 1e863b1d..537074ff 100644
--- a/tests/AnimeClient/Helper/MenuHelperTest.php
+++ b/tests/AnimeClient/Helper/MenuHelperTest.php
@@ -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();
|