Merge remote-tracking branch 'origin/develop'
timw4mail/HummingBirdAnimeClient/pipeline/head This commit looks good Details

This commit is contained in:
Timothy Warren 2022-01-17 10:21:27 -05:00
commit dcc5b928c7
93 changed files with 749 additions and 383 deletions

View File

@ -1,10 +1,8 @@
# Changelog # Changelog
## Version 5.3
* Updated to support PHP 8.1
## Version 5.2 ## Version 5.2
* Updated PHP requirement to 8 * Updated PHP requirement to 8
* Updated to support PHP 8.1
## Version 5.1 ## Version 5.1
* Added session check, so when coming back to a page, if the session is expired, the page will refresh. * Added session check, so when coming back to a page, if the session is expired, the page will refresh.

View File

@ -6,7 +6,7 @@
<?php if ($auth->isAuthenticated()): ?> <?php if ($auth->isAuthenticated()): ?>
<button title="Increment episode count" class="plus-one" hidden>+1 Episode</button> <button title="Increment episode count" class="plus-one" hidden>+1 Episode</button>
<?php endif ?> <?php endif ?>
<?= $helper->picture("images/anime/{$item['anime']['id']}.webp") ?> <?= $helper->img($item['anime']['cover_image'], ['width' => 220, 'loading' => 'lazy']) ?>
<div class="name"> <div class="name">
<a href="<?= $url->generate('anime.details', ['id' => $item['anime']['slug']]) ?>"> <a href="<?= $url->generate('anime.details', ['id' => $item['anime']['slug']]) ?>">

View File

@ -4,7 +4,7 @@
<button class="plus-one-chapter">+1 Chapter</button> <button class="plus-one-chapter">+1 Chapter</button>
</div> </div>
<?php endif ?> <?php endif ?>
<?= $helper->picture("images/manga/{$item['manga']['id']}.webp") ?> <?= $helper->img($item['manga']['image'], ['width' => 220, 'loading' => 'lazy']) ?>
<div class="name"> <div class="name">
<a href="<?= $url->generate('manga.details', ['id' => $item['manga']['slug']]) ?>"> <a href="<?= $url->generate('manga.details', ['id' => $item['manga']['slug']]) ?>">
<?= $escape->html($item['manga']['title']) ?> <?= $escape->html($item['manga']['title']) ?>

View File

@ -1,13 +1,12 @@
<?php <?php
use Aviat\AnimeClient\Kitsu; use Aviat\AnimeClient\Kitsu;
use function Aviat\AnimeClient\getLocalImg;
?> ?>
<main class="details fixed"> <main class="details fixed">
<section class="flex" unselectable> <section class="flex" unselectable>
<aside class="info"> <aside class="info">
<?= $helper->picture("images/anime/{$data['id']}-original.webp") ?> <?= $helper->img($data['cover_image'], ['width' => '390']) ?>
<br /> <br />
@ -162,14 +161,14 @@ use function Aviat\AnimeClient\getLocalImg;
use ($component, $url, $helper) { use ($component, $url, $helper) {
$rendered = []; $rendered = [];
foreach ($characterList as $id => $character): foreach ($characterList as $id => $character):
if (empty($character['image']['original'])) if (empty($character['image']))
{ {
continue; continue;
} }
$rendered[] = $component->character( $rendered[] = $component->character(
$character['name'], $character['name'],
$url->generate('character', ['slug' => $character['slug']]), $url->generate('character', ['slug' => $character['slug']]),
$helper->picture("images/characters/{$id}.webp"), $helper->img($character['image']),
(strtolower($role) !== 'main') ? 'small-character' : 'character' (strtolower($role) !== 'main') ? 'small-character' : 'character'
); );
endforeach; endforeach;
@ -187,14 +186,14 @@ use function Aviat\AnimeClient\getLocalImg;
use ($component, $url, $helper) { use ($component, $url, $helper) {
$rendered = []; $rendered = [];
foreach ($staffList as $id => $person): foreach ($staffList as $id => $person):
if (empty($person['image']['original'])) if (empty($person['image']))
{ {
continue; continue;
} }
$rendered[] = $component->character( $rendered[] = $component->character(
$person['name'], $person['name'],
$url->generate('person', ['slug' => $person['slug']]), $url->generate('person', ['slug' => $person['slug']]),
$helper->picture(getLocalImg($person['image']['original'] ?? NULL)), $helper->img($person['image']),
'character small-person', 'character small-person',
); );
endforeach; endforeach;

View File

@ -16,7 +16,7 @@
<tbody> <tbody>
<tr> <tr>
<td rowspan="9"> <td rowspan="9">
<?= $helper->picture("images/anime/{$item['anime']['id']}-original.webp", "jpg", [], ["width" => "390"]) ?> <?= $helper->img($item['anime']['cover_image']) ?>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -25,10 +25,10 @@
<?php endif ?> <?php endif ?>
<th>Title</th> <th>Title</th>
<th>Airing Status</th> <th>Airing Status</th>
<th>Score</th> <th class='numeric'>Score</th>
<th>Type</th> <th>Type</th>
<th>Progress</th> <th class='numeric'>Progress</th>
<th>Rated</th> <th class='rating'>Age Rating</th>
<th>Attributes</th> <th>Attributes</th>
<?php if($hasNotes): ?><th>Notes</th><?php endif ?> <?php if($hasNotes): ?><th>Notes</th><?php endif ?>
</tr> </tr>

View File

@ -7,7 +7,7 @@ use Aviat\AnimeClient\Kitsu;
<main class="character-page details fixed"> <main class="character-page details fixed">
<section class="flex flex-no-wrap"> <section class="flex flex-no-wrap">
<aside> <aside>
<?= $helper->picture("images/characters/{$data['id']}-original.webp") ?> <?= $helper->img($data['image']) ?>
</aside> </aside>
<div> <div>
<h2 class="toph"><?= $data['name'] ?></h2> <h2 class="toph"><?= $data['name'] ?></h2>
@ -41,7 +41,7 @@ use Aviat\AnimeClient\Kitsu;
$rendered[] = $component->media( $rendered[] = $component->media(
array_merge([$item['title']], $item['titles']), array_merge([$item['title']], $item['titles']),
$url->generate("{$mediaType}.details", ['id' => $item['slug']]), $url->generate("{$mediaType}.details", ['id' => $item['slug']]),
$helper->picture("images/{$mediaType}/{$item['id']}.webp") $helper->img(Kitsu::getPosterImage($item), ['width' => 220, 'loading' => 'lazy']),
); );
} }
@ -75,7 +75,7 @@ use Aviat\AnimeClient\Kitsu;
$link = $url->generate('person', ['id' => $c['person']['id']]); $link = $url->generate('person', ['id' => $c['person']['id']]);
?> ?>
<a href="<?= $link ?>"> <a href="<?= $link ?>">
<?= $helper->picture(getLocalImg($c['person']['image'], TRUE)) ?> <?= $helper->img($c['person']['image']) ?>
<div class="name"> <div class="name">
<?= $c['person']['name'] ?> <?= $c['person']['name'] ?>
</div> </div>
@ -91,7 +91,7 @@ use Aviat\AnimeClient\Kitsu;
$titles = Kitsu::filterTitles($series['attributes']); $titles = Kitsu::filterTitles($series['attributes']);
?> ?>
<a href="<?= $link ?>"> <a href="<?= $link ?>">
<?= $helper->picture(getLocalImg($series['attributes']['posterImage']['small'], TRUE)) ?> <?= $helper->img(Kitsu::getPosterImage($series['attributes'])) ?>
</a> </a>
<div class="name"> <div class="name">
<a href="<?= $link ?>"> <a href="<?= $link ?>">
@ -121,12 +121,12 @@ use Aviat\AnimeClient\Kitsu;
$person = $component->character( $person = $component->character(
$c['person']['name'], $c['person']['name'],
$url->generate('person', ['slug' => $c['person']['slug']]), $url->generate('person', ['slug' => $c['person']['slug']]),
$helper->picture(getLocalImg($c['person']['image'])) $helper->img($c['person']['image']['original']['url']),
); );
$medias = array_map(fn ($series) => $component->media( $medias = array_map(fn ($series) => $component->media(
array_merge([$series['title']], $series['titles']), array_merge([$series['title']], $series['titles']),
$url->generate('anime.details', ['id' => $series['slug']]), $url->generate('anime.details', ['id' => $series['slug']]),
$helper->picture(getLocalImg($series['posterImage'], TRUE)) $helper->img(Kitsu::getPosterImage($series)),
), $c['series']); ), $c['series']);
$media = implode('', array_map('mb_trim', $medias)); $media = implode('', array_map('mb_trim', $medias));

View File

@ -9,7 +9,7 @@
<div class="cssload-inner cssload-two"></div> <div class="cssload-inner cssload-two"></div>
<div class="cssload-inner cssload-three"></div> <div class="cssload-inner cssload-three"></div>
</div> </div>
<label for="search">Search for <?= $collection_type ?> by name:&nbsp;&nbsp;&nbsp;&nbsp;<input type="search" id="search" name="search" /></label> <label for="search-anime-collection">Search for <?= $collection_type ?> by name:&nbsp;&nbsp;&nbsp;&nbsp;<input type="search" id="search-anime-collection" name="search" /></label>
<section id="series-list" class="media-wrap"> <section id="series-list" class="media-wrap">
</section> </section>
</section> </section>

View File

@ -38,10 +38,10 @@
{$firstTh} {$firstTh}
<th>Title</th> <th>Title</th>
{$mediaTh} {$mediaTh}
<th>Episode Count</th> <th class='numeric'>Episode Count</th>
<th>Episode Length</th> <th class='numeric'>Episode Length</th>
<th>Show Type</th> <th>Show Type</th>
<th>Age Rating</th> <th class='rating'>Age Rating</th>
{$noteTh} {$noteTh}
<th>Genres</th> <th>Genres</th>
</tr> </tr>

View File

@ -7,11 +7,9 @@
<article class="flex flex-no-wrap flex-justify-start"> <article class="flex flex-no-wrap flex-justify-start">
<section class="flex-self-center history-img"> <section class="flex-self-center history-img">
<a href="<?= $item['url'] ?>"> <a href="<?= $item['url'] ?>">
<?= $helper->picture( <?= $helper->img(
$item['coverImg'], $item['coverImg'],
'jpg',
['width' => '110px', 'height' => '156px'], ['width' => '110px', 'height' => '156px'],
['width' => '110px', 'height' => '156px']
) ?> ) ?>
</a> </a>
</section> </section>

View File

@ -1,7 +1,7 @@
<main class="details fixed"> <main class="details fixed">
<section class="flex flex-no-wrap"> <section class="flex flex-no-wrap">
<aside class="info"> <aside class="info">
<?= $helper->picture("images/manga/{$data['id']}-original.webp", 'jpg', ['class' => 'cover']) ?> <?= $helper->img($data['cover_image'], ['class' => 'cover', 'width' => '350']) ?>
<br /> <br />
@ -79,7 +79,7 @@
$rendered[] = $component->character( $rendered[] = $component->character(
$char['name'], $char['name'],
$url->generate('character', ['slug' => $char['slug']]), $url->generate('character', ['slug' => $char['slug']]),
$helper->picture("images/characters/{$id}.webp"), $helper->img($char['image'], ['loading' => 'lazy']),
($role !== 'main') ? 'small-character' : 'character' ($role !== 'main') ? 'small-character' : 'character'
); );
} }
@ -96,7 +96,7 @@
fn ($person) => $component->character( fn ($person) => $component->character(
$person['name'], $person['name'],
$url->generate('person', ['slug' => $person['slug']]), $url->generate('person', ['slug' => $person['slug']]),
$helper->picture("images/people/{$person['id']}.webp") $helper->img($person['image']),
), ),
$people $people
)) ))

View File

@ -18,7 +18,7 @@
<tbody> <tbody>
<tr> <tr>
<td rowspan="9"> <td rowspan="9">
<?= $helper->picture("images/manga/{$item['manga']['id']}-original.webp", "jpg", [], ["width" => "390"]) ?> <?= $helper->img($item['manga']['image']) ?>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -20,8 +20,8 @@
<td>&nbsp;</td> <td>&nbsp;</td>
<?php endif ?> <?php endif ?>
<th>Title</th> <th>Title</th>
<th>Rating</th> <th class='numeric'>Score</th>
<th>Completed Chapters</th> <th class='numeric'>Completed Chapters</th>
<th>Attributes</th> <th>Attributes</th>
<th>Type</th> <th>Type</th>
</tr> </tr>

View File

@ -1,10 +1,7 @@
<?php
use function Aviat\AnimeClient\getLocalImg;
?>
<main class="details fixed"> <main class="details fixed">
<section class="flex flex-no-wrap"> <section class="flex flex-no-wrap">
<div> <div>
<?= $helper->picture("images/people/{$data['id']}-original.webp", 'jpg', ['class' => 'cover' ]) ?> <?= $helper->img($data['image'], ['class' => 'cover' ]) ?>
</div> </div>
<div> <div>
<h2 class="toph"><?= $data['name'] ?></h2> <h2 class="toph"><?= $data['name'] ?></h2>
@ -40,7 +37,7 @@ use function Aviat\AnimeClient\getLocalImg;
<?= $component->media( <?= $component->media(
$series['titles'], $series['titles'],
$url->generate("{$mediaType}.details", ['id' => $series['slug']]), $url->generate("{$mediaType}.details", ['id' => $series['slug']]),
$helper->picture("images/{$type}/{$sid}.webp") $helper->img($series['image'], ['width' => 220, 'loading' => 'lazy'])
) ?> ) ?>
<?php endforeach; ?> <?php endforeach; ?>
</section> </section>
@ -61,7 +58,7 @@ use function Aviat\AnimeClient\getLocalImg;
$character = $component->character( $character = $component->character(
$item['character']['canonicalName'], $item['character']['canonicalName'],
$url->generate('character', ['slug' => $item['character']['slug']]), $url->generate('character', ['slug' => $item['character']['slug']]),
$helper->picture(getLocalImg($item['character']['image']['original'] ?? null)) $helper->img($item['character']['image'], ['loading' => 'lazy']),
); );
$medias = []; $medias = [];
foreach ($item['media'] as $sid => $series) foreach ($item['media'] as $sid => $series)
@ -69,7 +66,7 @@ use function Aviat\AnimeClient\getLocalImg;
$medias[] = $component->media( $medias[] = $component->media(
$series['titles'], $series['titles'],
$url->generate('anime.details', ['id' => $series['slug']]), $url->generate('anime.details', ['id' => $series['slug']]),
$helper->picture("images/anime/{$sid}.webp") $helper->img($series['image'], ['width' => 220, 'loading' => 'lazy'])
); );
} }
$media = implode('', array_map('mb_trim', $medias)); $media = implode('', array_map('mb_trim', $medias));

View File

@ -16,7 +16,7 @@ use Aviat\AnimeClient\Kitsu;
<section class="flex flex-no-wrap"> <section class="flex flex-no-wrap">
<aside class="info"> <aside class="info">
<center> <center>
<?= $helper->img($urlGenerator->assetUrl($data['avatar']), ['alt' => '']); ?> <?= $helper->img($data['avatar'], ['alt' => '']); ?>
</center> </center>
<br /> <br />
<table class="media-details"> <table class="media-details">
@ -75,7 +75,7 @@ use Aviat\AnimeClient\Kitsu;
$rendered[] = $component->character( $rendered[] = $component->character(
$item['names']['canonical'], $item['names']['canonical'],
$url->generate('character', ['slug' => $item['slug']]), $url->generate('character', ['slug' => $item['slug']]),
$helper->picture("images/characters/{$item['id']}.webp") $helper->img($item['image']['original']['url'])
); );
} }
else else
@ -86,7 +86,7 @@ use Aviat\AnimeClient\Kitsu;
Kitsu::getFilteredTitles($item['titles']), Kitsu::getFilteredTitles($item['titles']),
), ),
$url->generate("{$type}.details", ['id' => $item['slug']]), $url->generate("{$type}.details", ['id' => $item['slug']]),
$helper->picture("images/{$type}/{$item['id']}.webp"), $helper->img(Kitsu::getPosterImage($item), ['width' => 220]),
); );
} }
} }

View File

@ -9,8 +9,8 @@ use ConsoleKit\Console;
$GLOBALS['_SERVER']['HTTP_HOST'] = 'localhost'; $GLOBALS['_SERVER']['HTTP_HOST'] = 'localhost';
define('APP_DIR', __DIR__ . '/app'); const APP_DIR = __DIR__ . '/app';
define('TEMPLATE_DIR', APP_DIR . '/templates'); const TEMPLATE_DIR = APP_DIR . '/templates';
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Start console script // Start console script
@ -26,7 +26,7 @@ try
'sync:lists' => Command\SyncLists::class 'sync:lists' => Command\SyncLists::class
]))->run(); ]))->run();
} }
catch (\Throwable) catch (Throwable)
{ {
} }

View File

@ -1,7 +1,7 @@
import _ from './anime-client.js' import _ from './anime-client.js'
import { renderSearchResults } from './template-helpers.js' import { renderSearchResults } from './template-helpers.js'
const search = (query) => { const search = (query, isCollection = false) => {
// Show the loader // Show the loader
_.show('.cssload-loader'); _.show('.cssload-loader');
@ -13,10 +13,11 @@ const search = (query) => {
_.hide('.cssload-loader'); _.hide('.cssload-loader');
// Show the results // Show the results
_.$('#series-list')[ 0 ].innerHTML = renderSearchResults('anime', searchResults); _.$('#series-list')[ 0 ].innerHTML = renderSearchResults('anime', searchResults, isCollection);
}); });
}; };
// Anime list search
if (_.hasElement('.anime #search')) { if (_.hasElement('.anime #search')) {
let prevRequest = null; let prevRequest = null;
@ -34,6 +35,24 @@ if (_.hasElement('.anime #search')) {
})); }));
} }
// Anime collection search
if (_.hasElement('#search-anime-collection')) {
let prevRequest = null;
_.on('#search-anime-collection', 'input', _.throttle(250, (e) => {
const query = encodeURIComponent(e.target.value);
if (query === '') {
return;
}
if (prevRequest !== null) {
prevRequest.abort();
}
prevRequest = search(query, true);
}));
}
// Action to increment episode count // Action to increment episode count
_.on('body.anime.list', 'click', '.plus-one', (e) => { _.on('body.anime.list', 'click', '.plus-one', (e) => {
let parentSel = _.closestParent(e.target, 'article'); let parentSel = _.closestParent(e.target, 'article');

View File

@ -6,9 +6,22 @@ const LightTableSorter = (() => {
const sort = (a, b) => { const sort = (a, b) => {
let textA = text(a); let textA = text(a);
let textB = text(b); let textB = text(b);
const n = parseInt(textA, 10); console.log("Comparing " + textA + " and " + textB)
if (n) {
textA = n; if(th.classList.contains("numeric")){
let arrayA = textA.replace('episodes: ','').replace('-',0).split("/");
let arrayB = textB.replace('episodes: ','').replace('-',0).split("/");
if(arrayA.length > 1) {
textA = parseInt(arrayA[0],10) / parseInt(arrayA[1],10);
textB = parseInt(arrayB[0],10) / parseInt(arrayB[1],10);
}
else{
textA = parseInt(arrayA[0],10);
textB = parseInt(arrayB[0],10);
}
}
else if (parseInt(textA, 10)) {
textA = parseInt(textA, 10);
textB = parseInt(textB, 10); textB = parseInt(textB, 10);
} }
if (textA > textB) { if (textA > textB) {
@ -59,6 +72,7 @@ const LightTableSorter = (() => {
for (let i = 0, len = ths.length; i < len; i++) { for (let i = 0, len = ths.length; i < len; i++) {
let th = ths[i]; let th = ths[i];
th.classList.add('sorting'); th.classList.add('sorting');
th.classList.add('testing');
results.push(th.onclick = onClickEvent); results.push(th.onclick = onClickEvent);
} }
return results; return results;

View File

@ -13,10 +13,11 @@ _.on('main', 'change', '.big-check', (e) => {
* *
* @param {'anime'|'manga'} type * @param {'anime'|'manga'} type
* @param {Object} item * @param {Object} item
* @param isCollection
* @returns {String} * @returns {String}
*/ */
function renderEditLink (type, item) { function renderEditLink (type, item, isCollection = false) {
if (item.libraryEntry === null) { if (isCollection || item.libraryEntry === null) {
return ''; return '';
} }
@ -38,13 +39,18 @@ function renderEditLink (type, item) {
* *
* @param {'anime'|'manga'} type * @param {'anime'|'manga'} type
* @param {Object} data * @param {Object} data
* @param {boolean} isCollection
* @returns {String} * @returns {String}
*/ */
export function renderSearchResults (type, data) { export function renderSearchResults (type, data, isCollection = false) {
return data.map(item => { return data.map(item => {
const titles = item.titles.join('<br />'); const titles = item.titles.join('<br />');
const disabled = item.libraryEntry !== null ? 'disabled' : ''; let disabled = item.libraryEntry !== null ? 'disabled' : '';
const editLink = renderEditLink(type, item); const editLink = renderEditLink(type, item, isCollection);
if (isCollection) {
disabled = '';
}
return ` return `
<article class="media search ${disabled}"> <article class="media search ${disabled}">
@ -52,11 +58,7 @@ export function renderSearchResults (type, data) {
<input type="radio" class="mal-check" id="mal_${item.slug}" name="mal_id" value="${item.mal_id}" ${disabled} /> <input type="radio" class="mal-check" id="mal_${item.slug}" name="mal_id" value="${item.mal_id}" ${disabled} />
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${item.id}" ${disabled} /> <input type="radio" class="big-check" id="${item.slug}" name="id" value="${item.id}" ${disabled} />
<label for="${item.slug}"> <label for="${item.slug}">
<picture width="220"> <img src="${item.coverImage}" alt="" width="220" />
<source srcset="/public/images/${type}/${item.id}.webp" type="image/webp" />
<source srcset="/public/images/${type}/${item.id}.jpg" type="image/jpeg" />
<img src="/public/images/${type}/${item.id}.jpg" alt="" width="220" />
</picture>
<span class="name"> <span class="name">
${item.canonicalTitle}<br /> ${item.canonicalTitle}<br />
<small>${titles}</small> <small>${titles}</small>

View File

@ -25,7 +25,7 @@ setlocale(LC_CTYPE, 'en_US');
// Load composer autoloader // Load composer autoloader
require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';
Debugger::$strictMode = E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED; // all errors except deprecated notices Debugger::$strictMode = E_ALL & ~E_DEPRECATED; // all errors except deprecated notices
Debugger::$showBar = false; Debugger::$showBar = false;
Debugger::enable(Debugger::DEVELOPMENT, __DIR__ . '/app/logs'); Debugger::enable(Debugger::DEVELOPMENT, __DIR__ . '/app/logs');

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
var LightTableSorter=function(){var th=null;var cellIndex=null;var order='';var text=function(row){return row.cells.item(cellIndex).textContent.toLowerCase();};var sort=function(a,b){var textA=text(a);var textB=text(b);var n=parseInt(textA,10);if(n){textA=n;textB=parseInt(textB,10);}if(textA>textB)return 1;if(textA<textB)return -1;return 0;};var toggle=function(){var c=order!=='sorting-asc'?'sorting-asc':'sorting-desc';th.className=(th.className.replace(order,'')+' '+c).trim();return order=c;};var reset=function(){th.classList.remove('sorting-asc','sorting-desc');th.classList.add('sorting');return order='';};var onClickEvent=function(e){if(th&&cellIndex!==e.target.cellIndex)reset();th=e.target;if(th.nodeName.toLowerCase()==='th'){cellIndex=th.cellIndex;var tbody=th.offsetParent.getElementsByTagName('tbody')[0];var rows=Array.from(tbody.rows);if(rows){rows.sort(sort);if(order==='sorting-asc')rows.reverse();toggle();tbody.innerHtml='';rows.forEach(function(row){tbody.appendChild(row);});}}};return {init:function(){var ths=document.getElementsByTagName('th');var results=[];for(var i=0,len=ths.length;i<len;i++){var th1=ths[i];th1.classList.add('sorting');results.push(th1.onclick=onClickEvent);}return results;}};}();LightTableSorter.init(); var LightTableSorter=function(){var th=null;var cellIndex=null;var order='';var text=function(row){return row.cells.item(cellIndex).textContent.toLowerCase()};var sort=function(a,b){var textA=text(a);var textB=text(b);console.log("Comparing "+textA+" and "+textB);if(th.classList.contains("numeric")){var arrayA=textA.replace('episodes: ','').replace('-',0).split("/");var arrayB=textB.replace('episodes: ','').replace('-',0).split("/");if(arrayA.length>1){textA=parseInt(arrayA[0],10)/parseInt(arrayA[1],10);textB=parseInt(arrayB[0],10)/parseInt(arrayB[1],10)}else{textA=parseInt(arrayA[0],10);textB=parseInt(arrayB[0],10)}}else if(parseInt(textA,10)){textA=parseInt(textA,10);textB=parseInt(textB,10)}if(textA>textB)return 1;if(textA<textB)return -1;return 0};var toggle=function(){var c=order!=='sorting-asc'?'sorting-asc':'sorting-desc';th.className=(th.className.replace(order,'')+' '+c).trim();return order=c};var reset=function(){th.classList.remove('sorting-asc','sorting-desc');th.classList.add('sorting');return order=''};var onClickEvent=function(e){if(th&&cellIndex!==e.target.cellIndex)reset();th=e.target;if(th.nodeName.toLowerCase()==='th'){cellIndex=th.cellIndex;var tbody=th.offsetParent.getElementsByTagName('tbody')[0];var rows=Array.from(tbody.rows);if(rows){rows.sort(sort);if(order==='sorting-asc')rows.reverse();toggle();tbody.innerHtml='';rows.forEach(function(row){tbody.appendChild(row)})}}};return{init:function(){var ths=document.getElementsByTagName('th');var results=[];for(var i=0,len=ths.length;i<len;i++){var th=ths[i];th.classList.add('sorting');th.classList.add('testing');results.push(th.onclick=onClickEvent)}return results}}}();LightTableSorter.init()

File diff suppressed because one or more lines are too long

View File

@ -154,7 +154,7 @@ abstract class APIRequestBuilder {
* Set a request header * Set a request header
* *
* @param string $name * @param string $name
* @param string $value * @param string|null $value
* @return self * @return self
*/ */
public function setHeader(string $name, string $value = NULL): self public function setHeader(string $name, string $value = NULL): self

View File

@ -70,7 +70,7 @@ abstract class AbstractListItem {
* Delete a list item * Delete a list item
* *
* @param string $id - The id of the list item to delete * @param string $id - The id of the list item to delete
* @return Request * @return Request|null
*/ */
abstract public function delete(string $id):?Request; abstract public function delete(string $id):?Request;
} }

View File

@ -207,7 +207,7 @@ final class Model
* *
* @param FormItem $data * @param FormItem $data
* @param string $type - Them media type (anime/manga) * @param string $type - Them media type (anime/manga)
* @return Request * @return Request|null
*/ */
public function incrementListItem(FormItem $data, string $type): ?Request public function incrementListItem(FormItem $data, string $type): ?Request
{ {
@ -225,7 +225,7 @@ final class Model
* *
* @param FormItem $data * @param FormItem $data
* @param string $type - Them media type (anime/manga) * @param string $type - Them media type (anime/manga)
* @return Request * @return Request|null
*/ */
public function updateListItem(FormItem $data, string $type): ?Request public function updateListItem(FormItem $data, string $type): ?Request
{ {
@ -244,7 +244,7 @@ final class Model
* *
* @param string $malId - The id of the list item to remove * @param string $malId - The id of the list item to remove
* @param string $type - Them media type (anime/manga) * @param string $type - Them media type (anime/manga)
* @return Request * @return Request|null
*/ */
public function deleteListItem(string $malId, string $type): ?Request public function deleteListItem(string $malId, string $type): ?Request
{ {
@ -262,7 +262,7 @@ final class Model
* *
* @param string $malId * @param string $malId
* @param string $type - The media type (anime/manga) * @param string $type - The media type (anime/manga)
* @return string * @return string|null
*/ */
public function getListIdFromMalId(string $malId, string $type): ?string public function getListIdFromMalId(string $malId, string $type): ?string
{ {
@ -306,7 +306,7 @@ final class Model
* *
* @param string $malId * @param string $malId
* @param string $type * @param string $type
* @return string * @return string|null
*/ */
private function getMediaIdFromMalId(string $malId, string $type = 'ANIME'): ?string private function getMediaIdFromMalId(string $malId, string $type = 'ANIME'): ?string
{ {

View File

@ -257,24 +257,18 @@ final class RequestBuilder extends APIRequestBuilder {
$validResponseCodes = [200, 201]; $validResponseCodes = [200, 201];
$logger = $this->container->getLogger('anilist-request'); $logger = $this->container->getLogger('anilist-request');
if ($logger !== NULL) $logger?->debug('Anilist response', [
{ 'status' => $response->getStatus(),
$logger->debug('Anilist response', [ 'reason' => $response->getReason(),
'status' => $response->getStatus(), 'body' => $response->getBody(),
'reason' => $response->getReason(), 'headers' => $response->getHeaders(),
'body' => $response->getBody(), //'requestHeaders' => $request->getHeaders(),
'headers' => $response->getHeaders(), ]);
//'requestHeaders' => $request->getHeaders(),
]);
}
if ( ! \in_array($response->getStatus(), $validResponseCodes, TRUE)) if ( ! \in_array($response->getStatus(), $validResponseCodes, TRUE))
{ {
if ($logger !== NULL) $logger?->warning('Non 200 response for POST api call', (array)$response->getBody());
{
$logger->warning('Non 200 response for POST api call', (array)$response->getBody());
}
} }
$rawBody = wait($response->getBody()->buffer()); $rawBody = wait($response->getBody()->buffer());

View File

@ -24,6 +24,7 @@ use Aviat\AnimeClient\Types\{AnimeListItem, FormItem};
use Aviat\Ion\Transformer\AbstractTransformer; use Aviat\Ion\Transformer\AbstractTransformer;
use DateTime; use DateTime;
use DateTimeInterface;
class AnimeListTransformer extends AbstractTransformer { class AnimeListTransformer extends AbstractTransformer {
@ -57,7 +58,7 @@ class AnimeListTransformer extends AbstractTransformer {
: AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']], : AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']],
'updatedAt' => (new DateTime()) 'updatedAt' => (new DateTime())
->setTimestamp($item['updatedAt']) ->setTimestamp($item['updatedAt'])
->format(DateTime::W3C) ->format(DateTimeInterface::W3C)
], ],
]); ]);
} }

View File

@ -25,6 +25,7 @@ use Aviat\AnimeClient\Types\FormItem;
use Aviat\Ion\Transformer\AbstractTransformer; use Aviat\Ion\Transformer\AbstractTransformer;
use DateTime; use DateTime;
use DateTimeInterface;
class MangaListTransformer extends AbstractTransformer { class MangaListTransformer extends AbstractTransformer {
@ -58,7 +59,7 @@ class MangaListTransformer extends AbstractTransformer {
: MangaReadingStatus::ANILIST_TO_KITSU[$item['status']], : MangaReadingStatus::ANILIST_TO_KITSU[$item['status']],
'updatedAt' => (new DateTime()) 'updatedAt' => (new DateTime())
->setTimestamp($item['updatedAt']) ->setTimestamp($item['updatedAt'])
->format(DateTime::W3C), ->format(DateTimeInterface::W3C),
] ]
]); ]);
} }

View File

@ -19,10 +19,8 @@ namespace Aviat\AnimeClient\API\Anilist\Types;
use Aviat\AnimeClient\Types\AbstractType; use Aviat\AnimeClient\Types\AbstractType;
class MediaListEntry extends AbstractType { class MediaListEntry extends AbstractType {
/**
* @var int|string public int|string $id;
*/
public $id;
public ?string $notes; public ?string $notes;

View File

@ -12,7 +12,7 @@ union ActivityUnion = ListActivity | MessageActivity | TextActivity
union LikeableUnion = ActivityReply | ListActivity | MessageActivity | TextActivity | Thread | ThreadComment union LikeableUnion = ActivityReply | ListActivity | MessageActivity | TextActivity | Thread | ThreadComment
"Notification union type" "Notification union type"
union NotificationUnion = ActivityLikeNotification | ActivityMentionNotification | ActivityMessageNotification | ActivityReplyLikeNotification | ActivityReplyNotification | ActivityReplySubscribedNotification | AiringNotification | FollowingNotification | RelatedMediaAdditionNotification | ThreadCommentLikeNotification | ThreadCommentMentionNotification | ThreadCommentReplyNotification | ThreadCommentSubscribedNotification | ThreadLikeNotification union NotificationUnion = ActivityLikeNotification | ActivityMentionNotification | ActivityMessageNotification | ActivityReplyLikeNotification | ActivityReplyNotification | ActivityReplySubscribedNotification | AiringNotification | FollowingNotification | MediaDataChangeNotification | MediaDeletionNotification | MediaMergeNotification | RelatedMediaAdditionNotification | ThreadCommentLikeNotification | ThreadCommentMentionNotification | ThreadCommentReplyNotification | ThreadCommentSubscribedNotification | ThreadLikeNotification
"Notification for when a activity is liked" "Notification for when a activity is liked"
type ActivityLikeNotification { type ActivityLikeNotification {
@ -227,6 +227,8 @@ type AniChartUser {
type Character { type Character {
"The character's age. Note this is a string, not an int, it may contain further text and additional ages." "The character's age. Note this is a string, not an int, it may contain further text and additional ages."
age: String age: String
"The characters blood type"
bloodType: String
"The character's birth date" "The character's birth date"
dateOfBirth: FuzzyDate dateOfBirth: FuzzyDate
"A general description of the character" "A general description of the character"
@ -262,7 +264,7 @@ type Character {
name: CharacterName name: CharacterName
"The url for the character page on the AniList website" "The url for the character page on the AniList website"
siteUrl: String siteUrl: String
updatedAt: Int @deprecated(reason : "No data available") updatedAt: Int @deprecated(reason: "No data available")
} }
type CharacterConnection { type CharacterConnection {
@ -314,15 +316,21 @@ type CharacterName {
middle: String middle: String
"The character's full name in their native language" "The character's full name in their native language"
native: String native: String
"The currently authenticated users preferred name language. Default romaji for non-authenticated"
userPreferred: String
} }
"A submission for a character that features in an anime or manga" "A submission for a character that features in an anime or manga"
type CharacterSubmission { type CharacterSubmission {
"Data Mod assigned to handle the submission"
assignee: User
"Character that the submission is referencing" "Character that the submission is referencing"
character: Character character: Character
createdAt: Int createdAt: Int
"The id of the submission" "The id of the submission"
id: Int! id: Int!
"Whether the submission is locked"
locked: Boolean
"Inner details of submission status" "Inner details of submission status"
notes: String notes: String
source: String source: String
@ -543,6 +551,7 @@ type InternalPage {
sort: [AiringSort] sort: [AiringSort]
): [AiringSchedule] ): [AiringSchedule]
characterSubmissions( characterSubmissions(
assigneeId: Int,
characterId: Int, characterId: Int,
"The order the results will be returned in" "The order the results will be returned in"
sort: [SubmissionSort], sort: [SubmissionSort],
@ -654,6 +663,8 @@ type InternalPage {
id_not_in: [Int], id_not_in: [Int],
"Filter by if the media's intended for 18+ adult audiences" "Filter by if the media's intended for 18+ adult audiences"
isAdult: Boolean, isAdult: Boolean,
"If the media is officially licensed or a self-published doujin release"
isLicensed: Boolean,
"Filter media by sites with a online streaming or reading license" "Filter media by sites with a online streaming or reading license"
licensedBy: String, licensedBy: String,
"Filter media by sites with a online streaming or reading license" "Filter media by sites with a online streaming or reading license"
@ -772,6 +783,7 @@ type InternalPage {
userName: String userName: String
): [MediaList] ): [MediaList]
mediaSubmissions( mediaSubmissions(
assigneeId: Int,
mediaId: Int, mediaId: Int,
"The order the results will be returned in" "The order the results will be returned in"
sort: [SubmissionSort], sort: [SubmissionSort],
@ -864,7 +876,7 @@ type InternalPage {
"Filter by user who created the recommendation" "Filter by user who created the recommendation"
userId: Int userId: Int
): [Recommendation] ): [Recommendation]
reports: [Report] reports(reportedId: Int, reporterId: Int): [Report]
reviews( reviews(
"Filter by Review id" "Filter by Review id"
id: Int, id: Int,
@ -906,6 +918,7 @@ type InternalPage {
sort: [StaffSort] sort: [StaffSort]
): [Staff] ): [Staff]
staffSubmissions( staffSubmissions(
assigneeId: Int,
"The order the results will be returned in" "The order the results will be returned in"
sort: [SubmissionSort], sort: [SubmissionSort],
staffId: Int, staffId: Int,
@ -958,9 +971,15 @@ type InternalPage {
"Filter by the user id of the thread's creator" "Filter by the user id of the thread's creator"
userId: Int userId: Int
): [Thread] ): [Thread]
userBlockSearch(
"Filter by search query"
search: String
): [User]
users( users(
"Filter by the user id" "Filter by the user id"
id: Int, id: Int,
"Filter to moderators only if true"
isModerator: Boolean,
"Filter by the name of the user" "Filter by the name of the user"
name: String, name: String,
"Filter by search query" "Filter by search query"
@ -1073,6 +1092,8 @@ type Media {
isAdult: Boolean isAdult: Boolean
"If the media is marked as favourite by the current authenticated user" "If the media is marked as favourite by the current authenticated user"
isFavourite: Boolean! isFavourite: Boolean!
"If the media is blocked from being added to favourites"
isFavouriteBlocked: Boolean!
"If the media is officially licensed or a self-published doujin release" "If the media is officially licensed or a self-published doujin release"
isLicensed: Boolean isLicensed: Boolean
"Locked media may not be added to lists our favorited. This may be due to the entry pending for deletion or other reasons." "Locked media may not be added to lists our favorited. This may be due to the entry pending for deletion or other reasons."
@ -1120,7 +1141,7 @@ type Media {
siteUrl: String siteUrl: String
"Source type the media was adapted from." "Source type the media was adapted from."
source( source(
"Provide 2 to use new version 2 of sources enum" "Provide 2 or 3 to use new version 2 or 3 of sources enum"
version: Int version: Int
): MediaSource ): MediaSource
"The staff who produced the media" "The staff who produced the media"
@ -1205,6 +1226,40 @@ type MediaCoverImage {
medium: String medium: String
} }
"Notification for when a media entry's data was changed in a significant way impacting users' list tracking"
type MediaDataChangeNotification {
"The reason for the media data change"
context: String
"The time the notification was created at"
createdAt: Int
"The id of the Notification"
id: Int!
"The media that received data changes"
media: Media
"The id of the media that received data changes"
mediaId: Int!
"The reason for the media data change"
reason: String
"The type of notification"
type: NotificationType
}
"Notification for when a media tracked in a user's list is deleted from the site"
type MediaDeletionNotification {
"The reason for the media deletion"
context: String
"The time the notification was created at"
createdAt: Int
"The title of the deleted media"
deletedMediaTitle: String
"The id of the Notification"
id: Int!
"The reason for the media deletion"
reason: String
"The type of notification"
type: NotificationType
}
"Media connection edge" "Media connection edge"
type MediaEdge { type MediaEdge {
"Media specific character name" "Media specific character name"
@ -1298,13 +1353,13 @@ type MediaList {
"List of anime or manga" "List of anime or manga"
type MediaListCollection { type MediaListCollection {
"A map of media list entry arrays grouped by custom lists" "A map of media list entry arrays grouped by custom lists"
customLists(asArray: Boolean): [[MediaList]] @deprecated(reason : "Not GraphQL spec compliant, use lists field instead.") customLists(asArray: Boolean): [[MediaList]] @deprecated(reason: "Not GraphQL spec compliant, use lists field instead.")
"If there is another chunk" "If there is another chunk"
hasNextChunk: Boolean hasNextChunk: Boolean
"Grouped media list entries" "Grouped media list entries"
lists: [MediaListGroup] lists: [MediaListGroup]
"A map of media list entry arrays grouped by status" "A map of media list entry arrays grouped by status"
statusLists(asArray: Boolean): [[MediaList]] @deprecated(reason : "Not GraphQL spec compliant, use lists field instead.") statusLists(asArray: Boolean): [[MediaList]] @deprecated(reason: "Not GraphQL spec compliant, use lists field instead.")
"The owner of the list" "The owner of the list"
user: User user: User
} }
@ -1330,10 +1385,10 @@ type MediaListOptions {
"The score format the user is using for media lists" "The score format the user is using for media lists"
scoreFormat: ScoreFormat scoreFormat: ScoreFormat
"The list theme options for both lists" "The list theme options for both lists"
sharedTheme: Json @deprecated(reason : "No longer used") sharedTheme: Json @deprecated(reason: "No longer used")
"If the shared theme should be used instead of the individual list themes" "If the shared theme should be used instead of the individual list themes"
sharedThemeEnabled: Boolean @deprecated(reason : "No longer used") sharedThemeEnabled: Boolean @deprecated(reason: "No longer used")
useLegacyLists: Boolean @deprecated(reason : "No longer used") useLegacyLists: Boolean @deprecated(reason: "No longer used")
} }
"A user's list options for anime or manga lists" "A user's list options for anime or manga lists"
@ -1349,7 +1404,27 @@ type MediaListTypeOptions {
"If the completed sections of the list should be separated by format" "If the completed sections of the list should be separated by format"
splitCompletedSectionByFormat: Boolean splitCompletedSectionByFormat: Boolean
"The list theme options" "The list theme options"
theme: Json @deprecated(reason : "This field has not yet been fully implemented and may change without warning") theme: Json @deprecated(reason: "This field has not yet been fully implemented and may change without warning")
}
"Notification for when a media entry is merged into another for a user who had it on their list"
type MediaMergeNotification {
"The reason for the media data change"
context: String
"The time the notification was created at"
createdAt: Int
"The title of the deleted media"
deletedMediaTitles: [String]
"The id of the Notification"
id: Int!
"The media that was merged into"
media: Media
"The id of the media that was merged into"
mediaId: Int!
"The reason for the media merge"
reason: String
"The type of notification"
type: NotificationType
} }
"The ranking of a media in a particular time span and format compared to other media" "The ranking of a media in a particular time span and format compared to other media"
@ -1374,7 +1449,7 @@ type MediaRank {
"A media's statistics" "A media's statistics"
type MediaStats { type MediaStats {
airingProgression: [AiringProgression] @deprecated(reason : "Replaced by MediaTrends") airingProgression: [AiringProgression] @deprecated(reason: "Replaced by MediaTrends")
scoreDistribution: [ScoreDistribution] scoreDistribution: [ScoreDistribution]
statusDistribution: [StatusDistribution] statusDistribution: [StatusDistribution]
} }
@ -1393,12 +1468,16 @@ type MediaStreamingEpisode {
"Media submission" "Media submission"
type MediaSubmission { type MediaSubmission {
"Data Mod assigned to handle the submission"
assignee: User
changes: [String] changes: [String]
characters: [MediaSubmissionComparison] characters: [MediaSubmissionComparison]
createdAt: Int createdAt: Int
externalLinks: [MediaExternalLink] externalLinks: [MediaExternalLink]
"The id of the submission" "The id of the submission"
id: Int! id: Int!
"Whether the submission is locked"
locked: Boolean
media: Media media: Media
notes: String notes: String
relations: [MediaEdge] relations: [MediaEdge]
@ -1458,6 +1537,8 @@ type MediaTag {
name: String! name: String!
"The relevance ranking of the tag out of the 100 for this media" "The relevance ranking of the tag out of the 100 for this media"
rank: Int rank: Int
"The user who submitted the tag"
userId: Int
} }
"The official titles of the media in various languages" "The official titles of the media in various languages"
@ -1738,6 +1819,8 @@ type Mutation {
comment: String, comment: String,
"The comment id, required for updating" "The comment id, required for updating"
id: Int, id: Int,
"If the comment tree should be locked. (Mod Only)"
locked: Boolean,
"The id of thread comment to reply to" "The id of thread comment to reply to"
parentCommentId: Int, parentCommentId: Int,
"The id of thread the comment belongs to" "The id of thread the comment belongs to"
@ -1872,6 +1955,8 @@ type Mutation {
rowOrder: String, rowOrder: String,
"The user's list scoring system" "The user's list scoring system"
scoreFormat: ScoreFormat, scoreFormat: ScoreFormat,
"The language the user wants to see staff and character names in"
staffNameLanguage: UserStaffNameLanguage,
"Timezone offset format: -?HH:MM" "Timezone offset format: -?HH:MM"
timezone: String, timezone: String,
"User's title language" "User's title language"
@ -2094,6 +2179,8 @@ type Page {
id_not_in: [Int], id_not_in: [Int],
"Filter by if the media's intended for 18+ adult audiences" "Filter by if the media's intended for 18+ adult audiences"
isAdult: Boolean, isAdult: Boolean,
"If the media is officially licensed or a self-published doujin release"
isLicensed: Boolean,
"Filter media by sites with a online streaming or reading license" "Filter media by sites with a online streaming or reading license"
licensedBy: String, licensedBy: String,
"Filter media by sites with a online streaming or reading license" "Filter media by sites with a online streaming or reading license"
@ -2368,6 +2455,8 @@ type Page {
users( users(
"Filter by the user id" "Filter by the user id"
id: Int, id: Int,
"Filter to moderators only if true"
isModerator: Boolean,
"Filter by the name of the user" "Filter by the name of the user"
name: String, name: String,
"Filter by search query" "Filter by search query"
@ -2618,6 +2707,8 @@ type Query {
id_not_in: [Int], id_not_in: [Int],
"Filter by if the media's intended for 18+ adult audiences" "Filter by if the media's intended for 18+ adult audiences"
isAdult: Boolean, isAdult: Boolean,
"If the media is officially licensed or a self-published doujin release"
isLicensed: Boolean,
"Filter media by sites with a online streaming or reading license" "Filter media by sites with a online streaming or reading license"
licensedBy: String, licensedBy: String,
"Filter media by sites with a online streaming or reading license" "Filter media by sites with a online streaming or reading license"
@ -2958,6 +3049,8 @@ type Query {
User( User(
"Filter by the user id" "Filter by the user id"
id: Int, id: Int,
"Filter to moderators only if true"
isModerator: Boolean,
"Filter by the name of the user" "Filter by the name of the user"
name: String, name: String,
"Filter by search query" "Filter by search query"
@ -3014,6 +3107,7 @@ type RelatedMediaAdditionNotification {
} }
type Report { type Report {
cleared: Boolean
"When the entry data was created" "When the entry data was created"
createdAt: Int createdAt: Int
id: Int! id: Int!
@ -3179,6 +3273,8 @@ type SiteTrendEdge {
type Staff { type Staff {
"The person's age in years" "The person's age in years"
age: Int age: Int
"The persons blood type"
bloodType: String
"Media the actor voiced characters in. (Same data as characters with media as node instead of characters)" "Media the actor voiced characters in. (Same data as characters with media as node instead of characters)"
characterMedia( characterMedia(
onList: Boolean, onList: Boolean,
@ -3218,7 +3314,7 @@ type Staff {
"If the staff member is blocked from being added to favourites" "If the staff member is blocked from being added to favourites"
isFavouriteBlocked: Boolean! isFavouriteBlocked: Boolean!
"The primary language the staff member dub's in" "The primary language the staff member dub's in"
language: StaffLanguage @deprecated(reason : "Replaced with languageV2") language: StaffLanguage @deprecated(reason: "Replaced with languageV2")
"The primary language of the staff member. Current values: Japanese, English, Korean, Italian, Spanish, Portuguese, French, German, Hebrew, Hungarian, Chinese, Arabic, Filipino, Catalan" "The primary language of the staff member. Current values: Japanese, English, Korean, Italian, Spanish, Portuguese, French, German, Hebrew, Hungarian, Chinese, Arabic, Filipino, Catalan"
languageV2: String languageV2: String
"Notes for site moderators" "Notes for site moderators"
@ -3247,7 +3343,7 @@ type Staff {
submissionStatus: Int submissionStatus: Int
"Submitter for the submission" "Submitter for the submission"
submitter: User submitter: User
updatedAt: Int @deprecated(reason : "No data available") updatedAt: Int @deprecated(reason: "No data available")
"[startYear, endYear] (If the 2nd value is not present staff is still active)" "[startYear, endYear] (If the 2nd value is not present staff is still active)"
yearsActive: [Int] yearsActive: [Int]
} }
@ -3291,6 +3387,8 @@ type StaffName {
middle: String middle: String
"The person's full name in their native language" "The person's full name in their native language"
native: String native: String
"The currently authenticated users preferred name language. Default romaji for non-authenticated"
userPreferred: String
} }
"Voice actor role for a character" "Voice actor role for a character"
@ -3314,9 +3412,13 @@ type StaffStats {
"A submission for a staff that features in an anime or manga" "A submission for a staff that features in an anime or manga"
type StaffSubmission { type StaffSubmission {
"Data Mod assigned to handle the submission"
assignee: User
createdAt: Int createdAt: Int
"The id of the submission" "The id of the submission"
id: Int! id: Int!
"Whether the submission is locked"
locked: Boolean
"Inner details of submission status" "Inner details of submission status"
notes: String notes: String
source: String source: String
@ -3511,6 +3613,8 @@ type ThreadComment {
id: Int! id: Int!
"If the currently authenticated user liked the comment" "If the currently authenticated user liked the comment"
isLiked: Boolean isLiked: Boolean
"If the comment tree is locked and may not receive replies or edits"
isLocked: Boolean
"The amount of likes the comment has" "The amount of likes the comment has"
likeCount: Int! likeCount: Int!
"The users who liked the comment" "The users who liked the comment"
@ -3651,6 +3755,8 @@ type User {
"The user's banner images" "The user's banner images"
bannerImage: String bannerImage: String
bans: Json bans: Json
"When the user's account was created. (Does not exist for accounts created before 2020)"
createdAt: Int
"Custom donation badge text" "Custom donation badge text"
donatorBadge: String donatorBadge: String
"The donation tier of the user" "The donation tier of the user"
@ -3670,18 +3776,22 @@ type User {
isFollowing: Boolean isFollowing: Boolean
"The user's media list options" "The user's media list options"
mediaListOptions: MediaListOptions mediaListOptions: MediaListOptions
"The user's moderator roles if they are a site moderator"
moderatorRoles: [ModRole]
"If the user is a moderator or data moderator" "If the user is a moderator or data moderator"
moderatorStatus: String moderatorStatus: String @deprecated(reason: "Deprecated. Replaced with moderatorRoles field.")
"The name of the user" "The name of the user"
name: String! name: String!
"The user's general options" "The user's general options"
options: UserOptions options: UserOptions
"The user's previously used names."
previousNames: [UserPreviousName]
"The url for the user page on the AniList website" "The url for the user page on the AniList website"
siteUrl: String siteUrl: String
"The users anime & manga list statistics" "The users anime & manga list statistics"
statistics: UserStatisticTypes statistics: UserStatisticTypes
"The user's statistics" "The user's statistics"
stats: UserStats @deprecated(reason : "Deprecated. Replaced with statistics field.") stats: UserStats @deprecated(reason: "Deprecated. Replaced with statistics field.")
"The number of unread notifications the user has" "The number of unread notifications the user has"
unreadNotificationCount: Int unreadNotificationCount: Int
"When the user's data was last updated" "When the user's data was last updated"
@ -3747,7 +3857,9 @@ type UserModData {
alts: [User] alts: [User]
bans: Json bans: Json
counts: Json counts: Json
email: String
ip: Json ip: Json
privacy: Int
} }
"A user's general options" "A user's general options"
@ -3762,12 +3874,24 @@ type UserOptions {
notificationOptions: [NotificationOption] notificationOptions: [NotificationOption]
"Profile highlight color (blue, purple, pink, orange, red, green, gray)" "Profile highlight color (blue, purple, pink, orange, red, green, gray)"
profileColor: String profileColor: String
"The language the user wants to see staff and character names in"
staffNameLanguage: UserStaffNameLanguage
"The user's timezone offset (Auth user only)" "The user's timezone offset (Auth user only)"
timezone: String timezone: String
"The language the user wants to see media titles in" "The language the user wants to see media titles in"
titleLanguage: UserTitleLanguage titleLanguage: UserTitleLanguage
} }
"A user's previous name"
type UserPreviousName {
"When the user first changed from this name."
createdAt: Int
"A previous name of the user."
name: String
"When the user most recently changed from this name."
updatedAt: Int
}
type UserReleaseYearStatistic { type UserReleaseYearStatistic {
chaptersRead: Int! chaptersRead: Int!
count: Int! count: Int!
@ -4127,24 +4251,36 @@ enum MediaSort {
"Source type the media was adapted from" "Source type the media was adapted from"
enum MediaSource { enum MediaSource {
"Version 2 only. Japanese Anime" "Version 2+ only. Japanese Anime"
ANIME ANIME
"Version 2 only. Self-published works" "Version 3 only. Comics excluding manga"
COMIC
"Version 2+ only. Self-published works"
DOUJINSHI DOUJINSHI
"Version 3 only. Games excluding video games"
GAME
"Written work published in volumes" "Written work published in volumes"
LIGHT_NOVEL LIGHT_NOVEL
"Version 3 only. Live action media such as movies or TV show"
LIVE_ACTION
"Asian comic book" "Asian comic book"
MANGA MANGA
"Version 2 only. Written works not published in volumes" "Version 3 only. Multimedia project"
MULTIMEDIA_PROJECT
"Version 2+ only. Written works not published in volumes"
NOVEL NOVEL
"An original production not based of another work" "An original production not based of another work"
ORIGINAL ORIGINAL
"Other" "Other"
OTHER OTHER
"Version 3 only. Picture book"
PICTURE_BOOK
"Video game" "Video game"
VIDEO_GAME VIDEO_GAME
"Video game driven primary by text and narrative" "Video game driven primary by text and narrative"
VISUAL_NOVEL VISUAL_NOVEL
"Version 3 only. Written works published online"
WEB_NOVEL
} }
"The current releasing status of the media" "The current releasing status of the media"
@ -4198,6 +4334,36 @@ enum ModActionType {
RESET RESET
} }
"Mod role enums"
enum ModRole {
"An AniList administrator"
ADMIN
"An anime data moderator"
ANIME_DATA
"A community moderator"
COMMUNITY
"An AniList developer"
DEVELOPER
"A discord community moderator"
DISCORD_COMMUNITY
"A lead anime data moderator"
LEAD_ANIME_DATA
"A lead community moderator"
LEAD_COMMUNITY
"A head developer of AniList"
LEAD_DEVELOPER
"A lead manga data moderator"
LEAD_MANGA_DATA
"A lead social media moderator"
LEAD_SOCIAL_MEDIA
"A manga data moderator"
MANGA_DATA
"A retired moderator"
RETIRED
"A social media moderator"
SOCIAL_MEDIA
}
"Notification type enum" "Notification type enum"
enum NotificationType { enum NotificationType {
"A user has liked your activity" "A user has liked your activity"
@ -4216,6 +4382,12 @@ enum NotificationType {
AIRING AIRING
"A user has followed you" "A user has followed you"
FOLLOWING FOLLOWING
"An anime or manga has had a data change that affects how a user may track it in their lists"
MEDIA_DATA_CHANGE
"An anime or manga on the user's list has been deleted from the site"
MEDIA_DELETION
"Anime or manga entries on the user's list have been merged into a single entry"
MEDIA_MERGE
"A new anime or manga has been added to the site where its related media is on the user's list" "A new anime or manga has been added to the site where its related media is on the user's list"
RELATED_MEDIA_ADDITION RELATED_MEDIA_ADDITION
"A user has liked your forum comment" "A user has liked your forum comment"
@ -4399,6 +4571,16 @@ enum UserSort {
WATCHED_TIME_DESC WATCHED_TIME_DESC
} }
"The language the user wants to see staff and character names in"
enum UserStaffNameLanguage {
"The staff or character's name in their native language"
NATIVE
"The romanization of the staff or character's native name"
ROMAJI
"The romanization of the staff or character's native name, with western name ordering"
ROMAJI_WESTERN
}
"User statistics sort enum" "User statistics sort enum"
enum UserStatisticsSort { enum UserStatisticsSort {
COUNT COUNT
@ -4427,6 +4609,14 @@ enum UserTitleLanguage {
ROMAJI_STYLISED ROMAJI_STYLISED
} }
"ISO 3166-1 alpha-2 country code"
scalar CountryCode
"8 digit long date integer (YYYYMMDD). Unknown dates represented by 0. E.g. 2016: 20160000, May 1976: 19760500"
scalar FuzzyDateInt
scalar Json
input AiringScheduleInput { input AiringScheduleInput {
airingAt: Int airingAt: Int
episode: Int episode: Int
@ -4521,12 +4711,3 @@ input StaffNameInput {
"The person's full name in their native language" "The person's full name in their native language"
native: String native: String
} }
scalar Json
"ISO 3166-1 alpha-2 country code"
scalar CountryCode
"8 digit long date integer (YYYYMMDD). Unknown dates represented by 0. E.g. 2016: 20160000, May 1976: 19760500"
scalar FuzzyDateInt

View File

@ -17,7 +17,6 @@
namespace Aviat\AnimeClient\API; namespace Aviat\AnimeClient\API;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;
/** /**
* Helper methods for dealing with the Cache * Helper methods for dealing with the Cache

View File

@ -23,7 +23,6 @@ use const Aviat\AnimeClient\SESSION_SEGMENT;
use Aviat\AnimeClient\Kitsu as K; use Aviat\AnimeClient\Kitsu as K;
use Aviat\AnimeClient\API\CacheTrait; use Aviat\AnimeClient\API\CacheTrait;
use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
use Aviat\Ion\Event; use Aviat\Ion\Event;
/** /**
@ -129,11 +128,11 @@ final class Auth {
{ {
if (PHP_SAPI === 'cli') if (PHP_SAPI === 'cli')
{ {
return $this->segment->get('auth_token', NULL) return $this->segment->get('auth_token')
?? $this->cache->get(K::AUTH_TOKEN_CACHE_KEY, NULL); ?? $this->cache->get(K::AUTH_TOKEN_CACHE_KEY);
} }
return $this->segment->get('auth_token', NULL); return $this->segment->get('auth_token');
} }
/** /**
@ -146,7 +145,7 @@ final class Auth {
if (PHP_SAPI === 'cli') if (PHP_SAPI === 'cli')
{ {
return $this->segment->get('refresh_token') return $this->segment->get('refresh_token')
?? $this->cache->get(K::AUTH_TOKEN_REFRESH_CACHE_KEY, NULL); ?? $this->cache->get(K::AUTH_TOKEN_REFRESH_CACHE_KEY);
} }
return $this->segment->get('refresh_token'); return $this->segment->get('refresh_token');

View File

@ -16,17 +16,10 @@
namespace Aviat\AnimeClient\API\Kitsu; namespace Aviat\AnimeClient\API\Kitsu;
use Aviat\Ion\Di\Exception\ContainerException;
use Aviat\Ion\Di\Exception\NotFoundException;
use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use Amp\Http\Client\Request; use Amp\Http\Client\Request;
use Aviat\AnimeClient\API\AbstractListItem; use Aviat\AnimeClient\API\AbstractListItem;
use Aviat\AnimeClient\Types\FormItemData; use Aviat\AnimeClient\Types\FormItemData;
use Aviat\Ion\Di\ContainerAware; use Aviat\Ion\Di\ContainerAware;
use Aviat\Ion\Json;
use Throwable; use Throwable;

View File

@ -44,6 +44,7 @@ use Aviat\Ion\{
use Generator; use Generator;
use function Amp\Promise\wait; use function Amp\Promise\wait;
use function Aviat\AnimeClient\getApiClient; use function Aviat\AnimeClient\getApiClient;
use const Aviat\AnimeClient\SESSION_SEGMENT;
/** /**
* Kitsu API Model * Kitsu API Model
@ -533,6 +534,7 @@ final class Model {
$searchItem = [ $searchItem = [
'id' => $item['id'], 'id' => $item['id'],
'slug' => $item['slug'], 'slug' => $item['slug'],
'coverImage' => K::getPosterImage($item),
'canonicalTitle' => $item['titles']['canonical'], 'canonicalTitle' => $item['titles']['canonical'],
'titles' => array_values(K::getTitles($item['titles'])), 'titles' => array_values(K::getTitles($item['titles'])),
'libraryEntry' => $item['myLibraryEntry'], 'libraryEntry' => $item['myLibraryEntry'],
@ -707,9 +709,14 @@ final class Model {
$rawData = Json::decode($json); $rawData = Json::decode($json);
$data = $rawData['data']['findProfileBySlug']['library']['all'] ?? []; $data = $rawData['data']['findProfileBySlug']['library']['all'] ?? [];
$page = $data['pageInfo']; $page = $data['pageInfo'] ?? [];
if (empty($data)) if (empty($data))
{ {
// Clear session, in case the error is an invalid token.
$segment = $this->container->get('session')
->getSegment(SESSION_SEGMENT);
$segment->clear();
// @TODO Proper Error logging // @TODO Proper Error logging
dump($rawData); dump($rawData);
die(); die();
@ -719,7 +726,7 @@ final class Model {
yield $emit($data['nodes']); yield $emit($data['nodes']);
if ($page['hasNextPage'] === FALSE) if ($page['hasNextPage'] !== TRUE)
{ {
break; break;
} }

View File

@ -18,7 +18,6 @@ namespace Aviat\AnimeClient\API\Kitsu;
use Amp\Http\Client\Request; use Amp\Http\Client\Request;
use Aviat\AnimeClient\Types\FormItem; use Aviat\AnimeClient\Types\FormItem;
use Aviat\Banker\Exception\InvalidArgumentException;
/** /**
* Kitsu API calls that mutate data, C/U/D parts of CRUD * Kitsu API calls that mutate data, C/U/D parts of CRUD

View File

@ -40,12 +40,10 @@ query ($slug: String!) {
} }
type type
} }
voices(first: 100) { role
voices(first: 10) {
nodes { nodes {
id id
licensor {
name
}
locale locale
person { person {
id id

View File

@ -19,6 +19,11 @@ query ($slug: String!) {
id id
slug slug
posterImage { posterImage {
original {
width
height
url
}
views { views {
width width
height height

View File

@ -52,7 +52,7 @@ query ($slug: String!) {
} }
} }
} }
voices(first: 100) { voices(first: 500) {
nodes { nodes {
locale locale
mediaCharacter { mediaCharacter {

View File

@ -2,6 +2,14 @@ query ($query: String!) {
searchAnimeByTitle(title: $query, first: 20) { searchAnimeByTitle(title: $query, first: 20) {
nodes { nodes {
id id
posterImage {
original {
url
}
views {
url
}
}
mappings(first: 10) { mappings(first: 10) {
nodes { nodes {
externalId externalId

View File

@ -2,6 +2,14 @@ query ($query: String!) {
searchMangaByTitle(title: $query, first: 20) { searchMangaByTitle(title: $query, first: 20) {
nodes { nodes {
id id
posterImage {
original {
url
}
views {
url
}
}
mappings(first: 10) { mappings(first: 10) {
nodes { nodes {
externalId externalId

View File

@ -102,6 +102,11 @@ query ($slug: String!) {
original { original {
url url
} }
views {
url
height
width
}
} }
names { names {
alternatives alternatives

View File

@ -192,16 +192,13 @@ final class RequestBuilder extends APIRequestBuilder {
$request = $this->setUpRequest($type, $url, $options); $request = $this->setUpRequest($type, $url, $options);
$response = getResponse($request); $response = getResponse($request);
if ($logger !== NULL) $logger?->debug('Kitsu API Response', [
{ 'status' => $response->getStatus(),
$logger->debug('Kitsu API Response', [ 'reason' => $response->getReason(),
'status' => $response->getStatus(), 'body' => $response->getBody(),
'reason' => $response->getReason(), 'headers' => $response->getHeaders(),
'body' => $response->getBody(), 'requestHeaders' => $request->getHeaders(),
'headers' => $response->getHeaders(), ]);
'requestHeaders' => $request->getHeaders(),
]);
}
return $response; return $response;
} }

View File

@ -27,7 +27,7 @@ trait RequestBuilderTrait {
* Set the request builder object * Set the request builder object
* *
* @param RequestBuilder $requestBuilder * @param RequestBuilder $requestBuilder
* @return $this * @return RequestBuilderTrait|ListItem|Model
*/ */
public function setRequestBuilder(RequestBuilder $requestBuilder): self public function setRequestBuilder(RequestBuilder $requestBuilder): self
{ {

View File

@ -96,7 +96,7 @@ final class AnimeListTransformer extends AbstractTransformer {
'titles' => $titles, 'titles' => $titles,
'slug' => $anime['slug'], 'slug' => $anime['slug'],
'show_type' => (string)StringType::from($anime['subtype'])->upperCaseFirst(), 'show_type' => (string)StringType::from($anime['subtype'])->upperCaseFirst(),
'cover_image' => $anime['posterImage']['views'][1]['url'], 'cover_image' => Kitsu::getPosterImage($anime),
'genres' => $genres, 'genres' => $genres,
'streaming_links' => $streamingLinks, 'streaming_links' => $streamingLinks,
], ],

View File

@ -59,7 +59,7 @@ final class AnimeTransformer extends AbstractTransformer {
$details = $rawCharacter['character']; $details = $rawCharacter['character'];
$characters[$type][$details['id']] = [ $characters[$type][$details['id']] = [
'image' => $details['image'], 'image' => $details['image']['original']['url'] ?? '',
'name' => $details['names']['canonical'], 'name' => $details['names']['canonical'],
'slug' => $details['slug'], 'slug' => $details['slug'],
]; ];
@ -103,9 +103,7 @@ final class AnimeTransformer extends AbstractTransformer {
$staff[$role][$person['id']] = [ $staff[$role][$person['id']] = [
'id' => $person['id'], 'id' => $person['id'],
'name' => $name, 'name' => $name,
'image' => [ 'image' => $person['image']['original']['url'],
'original' => $person['image']['original']['url'] ?? '',
],
'slug' => $person['slug'], 'slug' => $person['slug'],
]; ];
@ -125,7 +123,7 @@ final class AnimeTransformer extends AbstractTransformer {
'age_rating' => $base['ageRating'], 'age_rating' => $base['ageRating'],
'age_rating_guide' => $base['ageRatingGuide'], 'age_rating_guide' => $base['ageRatingGuide'],
'characters' => $characters, 'characters' => $characters,
'cover_image' => $base['posterImage']['views'][1]['url'], 'cover_image' => Kitsu::getPosterImage($base),
'episode_count' => $base['episodeCount'], 'episode_count' => $base['episodeCount'],
'episode_length' => $base['episodeLength'], 'episode_length' => $base['episodeLength'],
'genres' => $genres, 'genres' => $genres,
@ -136,7 +134,7 @@ final class AnimeTransformer extends AbstractTransformer {
'show_type' => $base['subtype'], 'show_type' => $base['subtype'],
'status' => Kitsu::getAiringStatus($base['startDate'], $base['endDate']), 'status' => Kitsu::getAiringStatus($base['startDate'], $base['endDate']),
'streaming_links' => Kitsu::parseStreamingLinks($base['streamingLinks']['nodes'] ?? []), 'streaming_links' => Kitsu::parseStreamingLinks($base['streamingLinks']['nodes'] ?? []),
'synopsis' => $base['description']['en'], 'synopsis' => $base['description']['en'] ?? '',
'title' => $title, 'title' => $title,
'titles' => $titles, 'titles' => $titles,
'titles_more' => $titles_more, 'titles_more' => $titles_more,

View File

@ -58,6 +58,7 @@ final class CharacterTransformer extends AbstractTransformer {
'castings' => $castings, 'castings' => $castings,
'description' => $data['description']['en'], 'description' => $data['description']['en'],
'id' => $data['id'], 'id' => $data['id'],
'image' => $data['image']['original']['url'] ?? 'images/placeholder.png',
'media' => $media, 'media' => $media,
'name' => $name, 'name' => $name,
'names' => $names, 'names' => $names,
@ -147,7 +148,7 @@ final class CharacterTransformer extends AbstractTransformer {
'slug' => $voiceMap['media']['slug'], 'slug' => $voiceMap['media']['slug'],
'title' => $voiceMap['media']['titles']['canonical'], 'title' => $voiceMap['media']['titles']['canonical'],
'titles' => Kitsu::getFilteredTitles($voiceMap['media']['titles']), 'titles' => Kitsu::getFilteredTitles($voiceMap['media']['titles']),
'posterImage' => $voiceMap['media']['posterImage']['views'][1]['url'], 'posterImage' => Kitsu::getPosterImage($voiceMap['media']),
]; ];
uasort($castings['Voice Actor'][$lang][$id]['series'], $titleSort); uasort($castings['Voice Actor'][$lang][$id]['series'], $titleSort);

View File

@ -17,6 +17,7 @@
namespace Aviat\AnimeClient\API\Kitsu\Transformer; namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\Types\HistoryItem; use Aviat\AnimeClient\Types\HistoryItem;
use Aviat\AnimeClient\Kitsu;
use DateTimeImmutable; use DateTimeImmutable;
use DateTimeInterface; use DateTimeInterface;
use DateTimeZone; use DateTimeZone;
@ -184,10 +185,8 @@ abstract class HistoryTransformer {
protected function transformProgress (array $entry): ?HistoryItem protected function transformProgress (array $entry): ?HistoryItem
{ {
$id = $entry['media']['id'];
$data = $entry['media']; $data = $entry['media'];
$title = $this->linkTitle($data); $title = $this->linkTitle($data);
$imgUrl = "images/{$this->type}/{$id}.webp";
$item = end($entry['changedData']['progress']); $item = end($entry['changedData']['progress']);
// No showing episode 0 nonsense // No showing episode 0 nonsense
@ -215,7 +214,7 @@ abstract class HistoryTransformer {
return HistoryItem::from([ return HistoryItem::from([
'action' => $action, 'action' => $action,
'coverImg' => $imgUrl, 'coverImg' => Kitsu::getPosterImage($data, 0),
'kind' => 'progressed', 'kind' => 'progressed',
'original' => $entry, 'original' => $entry,
'title' => $title, 'title' => $title,
@ -226,10 +225,8 @@ abstract class HistoryTransformer {
protected function transformUpdated(array $entry): HistoryItem protected function transformUpdated(array $entry): HistoryItem
{ {
$id = $entry['media']['id'];
$data = $entry['media']; $data = $entry['media'];
$title = $this->linkTitle($data); $title = $this->linkTitle($data);
$imgUrl = "images/{$this->type}/{$id}.webp";
$kind = array_key_first($entry['changedData']); $kind = array_key_first($entry['changedData']);
@ -247,7 +244,7 @@ abstract class HistoryTransformer {
return HistoryItem::from([ return HistoryItem::from([
'action' => $statusName, 'action' => $statusName,
'coverImg' => $imgUrl, 'coverImg' => Kitsu::getPosterImage($data, 0),
'kind' => 'updated', 'kind' => 'updated',
'original' => $entry, 'original' => $entry,
'title' => $title, 'title' => $title,

View File

@ -102,7 +102,7 @@ final class LibraryEntryTransformer extends AbstractTransformer
'titles' => $titles, 'titles' => $titles,
'slug' => $anime['slug'], 'slug' => $anime['slug'],
'show_type' => (string)StringType::from($anime['subtype'])->upperCaseFirst(), 'show_type' => (string)StringType::from($anime['subtype'])->upperCaseFirst(),
'cover_image' => $anime['posterImage']['views'][1]['url'], 'cover_image' => Kitsu::getPosterImage($anime),
'genres' => $genres, 'genres' => $genres,
'streaming_links' => $streamingLinks, 'streaming_links' => $streamingLinks,
], ],
@ -167,7 +167,7 @@ final class LibraryEntryTransformer extends AbstractTransformer
'manga' => MangaListItemDetail::from([ 'manga' => MangaListItemDetail::from([
'genres' => $genres, 'genres' => $genres,
'id' => $mangaId, 'id' => $mangaId,
'image' => $manga['posterImage']['views'][1]['url'], 'image' => Kitsu::getPosterImage($manga),
'slug' => $manga['slug'], 'slug' => $manga['slug'],
'title' => $title, 'title' => $title,
'titles' => $titles, 'titles' => $titles,

View File

@ -91,7 +91,7 @@ final class MangaListTransformer extends AbstractTransformer {
'manga' => MangaListItemDetail::from([ 'manga' => MangaListItemDetail::from([
'genres' => $genres, 'genres' => $genres,
'id' => $mangaId, 'id' => $mangaId,
'image' => $manga['posterImage']['views'][1]['url'], 'image' => Kitsu::getPosterImage($manga),
'slug' => $manga['slug'], 'slug' => $manga['slug'],
'title' => $title, 'title' => $title,
'titles' => $titles, 'titles' => $titles,

View File

@ -58,7 +58,7 @@ final class MangaTransformer extends AbstractTransformer {
$details = $rawCharacter['character']; $details = $rawCharacter['character'];
$characters[$type][$details['id']] = [ $characters[$type][$details['id']] = [
'image' => $details['image'], 'image' => $details['image']['original']['url'],
'name' => $details['names']['canonical'], 'name' => $details['names']['canonical'],
'slug' => $details['slug'], 'slug' => $details['slug'],
]; ];
@ -103,9 +103,7 @@ final class MangaTransformer extends AbstractTransformer {
'id' => $person['id'], 'id' => $person['id'],
'slug' => $person['slug'], 'slug' => $person['slug'],
'name' => $name, 'name' => $name,
'image' => [ 'image' => $person['image']['original']['url'],
'original' => $person['image']['original']['url'] ?? '',
],
]; ];
usort($staff[$role], fn ($a, $b) => $a['name'] <=> $b['name']); usort($staff[$role], fn ($a, $b) => $a['name'] <=> $b['name']);
@ -125,7 +123,7 @@ final class MangaTransformer extends AbstractTransformer {
'characters' => $characters, 'characters' => $characters,
'chapter_count' => $base['chapterCount'], 'chapter_count' => $base['chapterCount'],
'volume_count' => $base['volumeCount'], 'volume_count' => $base['volumeCount'],
'cover_image' => $base['posterImage']['views'][1]['url'], 'cover_image' => Kitsu::getPosterImage($base),
'genres' => $genres, 'genres' => $genres,
'links' => $links, 'links' => $links,
'manga_type' => $base['subtype'], 'manga_type' => $base['subtype'],

View File

@ -41,6 +41,7 @@ final class PersonTransformer extends AbstractTransformer {
return Person::from([ return Person::from([
'id' => $data['id'], 'id' => $data['id'],
'name' => $canonicalName, 'name' => $canonicalName,
'image' => $data['image']['original']['url'],
'names' => array_diff($data['names']['localized'], [$canonicalName]), 'names' => array_diff($data['names']['localized'], [$canonicalName]),
'description' => $data['description']['en'] ?? '', 'description' => $data['description']['en'] ?? '',
'characters' => $orgData['characters'], 'characters' => $orgData['characters'],
@ -83,9 +84,7 @@ final class PersonTransformer extends AbstractTransformer {
'id' => $media['id'], 'id' => $media['id'],
'title' => $title, 'title' => $title,
'titles' => array_merge([$title], Kitsu::getFilteredTitles($media['titles'])), 'titles' => array_merge([$title], Kitsu::getFilteredTitles($media['titles'])),
'image' => [ 'image' => Kitsu::getPosterImage($media),
'original' => $media['posterImage']['views'][1]['url'] ?? '',
],
'slug' => $media['slug'], 'slug' => $media['slug'],
]; ];
@ -107,6 +106,7 @@ final class PersonTransformer extends AbstractTransformer {
$media = [ $media = [
'id' => $rawMedia['id'], 'id' => $rawMedia['id'],
'slug' => $rawMedia['slug'], 'slug' => $rawMedia['slug'],
'image' => Kitsu::getPosterImage($rawMedia),
'titles' => array_merge( 'titles' => array_merge(
[$rawMedia['titles']['canonical']], [$rawMedia['titles']['canonical']],
Kitsu::getFilteredTitles($rawMedia['titles']), Kitsu::getFilteredTitles($rawMedia['titles']),
@ -124,9 +124,7 @@ final class PersonTransformer extends AbstractTransformer {
'character' => [ 'character' => [
'id' => $character['id'], 'id' => $character['id'],
'slug' => $character['slug'], 'slug' => $character['slug'],
'image' => [ 'image' => $character['image']['original']['url'],
'original' => $character['image']['original']['url'] ?? '',
],
'canonicalName' => $character['names']['canonical'], 'canonicalName' => $character['names']['canonical'],
], ],
'media' => [ 'media' => [

View File

@ -42,7 +42,7 @@ final class UserTransformer extends AbstractTransformer {
return User::from([ return User::from([
'about' => $base['about'], 'about' => $base['about'],
'avatar' => getLocalImg($base['avatarImage']['original']['url'], FALSE), 'avatar' => $base['avatarImage']['original']['url'],
'favorites' => $this->organizeFavorites($favorites), 'favorites' => $this->organizeFavorites($favorites),
'location' => $base['location'], 'location' => $base['location'],
'name' => $base['name'], 'name' => $base['name'],

View File

@ -16,9 +16,8 @@
namespace Aviat\AnimeClient; namespace Aviat\AnimeClient;
use Aviat\AnimeClient\Kitsu; use Aviat\Ion\ImageBuilder;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;
use Amp\Http\Client\Request; use Amp\Http\Client\Request;
use Amp\Http\Client\Response; use Amp\Http\Client\Response;
@ -151,6 +150,21 @@ function tomlToArray(string $toml): array
//! Misc Functions //! Misc Functions
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
if ( ! function_exists('array_is_list'))
{
/**
* Polyfill for PHP 8
*
* @see https://www.php.net/manual/en/function.array-is-list
* @param array $a
* @return bool
*/
function array_is_list(array $a): bool
{
return $a === [] || (array_keys($a) === range(0, count($a) - 1));
}
}
/** /**
* Is the array sequential, not associative? * Is the array sequential, not associative?
* *
@ -164,15 +178,7 @@ function isSequentialArray(mixed $array): bool
return FALSE; return FALSE;
} }
$i = 0; return array_is_list($array);
foreach ($array as $k => $v)
{
if ($k !== $i++)
{
return FALSE;
}
}
return TRUE;
} }
/** /**
@ -263,7 +269,7 @@ function getResponse (Request|string $request): Response
* @param bool $webp * @param bool $webp
* @return string * @return string
*/ */
function getLocalImg (string $kitsuUrl, $webp = TRUE): string function getLocalImg (string $kitsuUrl, bool $webp = TRUE): string
{ {
if (empty($kitsuUrl) || ( ! is_string($kitsuUrl))) if (empty($kitsuUrl) || ( ! is_string($kitsuUrl)))
{ {
@ -296,71 +302,26 @@ function getLocalImg (string $kitsuUrl, $webp = TRUE): string
* *
* @codeCoverageIgnore * @codeCoverageIgnore
* @param string $path * @param string $path
* @param int|null $width * @param int $width
* @param int|null $height * @param int $height
* @param string $text * @param string $text
* @return bool * @return bool
*/ */
function createPlaceholderImage (string $path, ?int $width, ?int $height, $text = 'Image Unavailable'): bool function createPlaceholderImage (string $path, int $width = 200, int $height = 200, string $text = 'Image Unavailable'): bool
{ {
$width = $width ?? 200; $img = ImageBuilder::new($width, $height)
$height = $height ?? 200; ->enableAlphaBlending(TRUE)
->addBackgroundColor(255, 255, 255)
$img = imagecreatetruecolor($width, $height); ->addCenteredText($text, 64, 64, 64);
if ($img === FALSE)
{
return FALSE;
}
imagealphablending($img, TRUE);
$path = rtrim($path, '/'); $path = rtrim($path, '/');
// Background is the first color by default $savedPng = $img->savePng($path . '/placeholder.png');
$fillColor = imagecolorallocatealpha($img, 255, 255, 255, 127); $savedWebp = $img->saveWebp($path . '/placeholder.webp');
if ($fillColor === FALSE)
{
return FALSE;
}
imagefill($img, 0, 0, $fillColor);
$textColor = imagecolorallocate($img, 64, 64, 64); $img->cleanup();
if ($textColor === FALSE)
{
return FALSE;
}
imagealphablending($img, TRUE); return $savedPng && $savedWebp;
// Generate placeholder text
$fontSize = 10;
$fontWidth = imagefontwidth($fontSize);
$fontHeight = imagefontheight($fontSize);
$length = strlen($text);
$textWidth = $length * $fontWidth;
$fxPos = (int) ceil((imagesx($img) - $textWidth) / 2);
$fyPos = (int) ceil((imagesy($img) - $fontHeight) / 2);
// Add the image text
imagestring($img, $fontSize, $fxPos, $fyPos, $text, $textColor);
// Save the images
imagesavealpha($img, TRUE);
imagepng($img, $path . '/placeholder.png', 9);
imagedestroy($img);
$pngImage = imagecreatefrompng($path . '/placeholder.png');
if ($pngImage === FALSE)
{
return FALSE;
}
imagealphablending($pngImage, TRUE);
imagesavealpha($pngImage, TRUE);
imagewebp($pngImage, $path . '/placeholder.webp');
imagedestroy($pngImage);
return TRUE;
} }
/** /**

View File

@ -16,7 +16,6 @@
namespace Aviat\AnimeClient\Command; namespace Aviat\AnimeClient\Command;
use Aviat\AnimeClient\Kitsu;
use Aviat\Ion\Di\Exception\ContainerException; use Aviat\Ion\Di\Exception\ContainerException;
use Aviat\Ion\Di\Exception\NotFoundException; use Aviat\Ion\Di\Exception\NotFoundException;
use function Aviat\AnimeClient\clearCache; use function Aviat\AnimeClient\clearCache;

View File

@ -203,7 +203,7 @@ final class SyncLists extends BaseCommand {
* *
* @param string $type * @param string $type
* @param array $data * @param array $data
* @return array|array[] * @return array
*/ */
protected function compare(string $type, array $data): array protected function compare(string $type, array $data): array
{ {

View File

@ -239,7 +239,7 @@ class Controller {
protected function renderFullPage(HtmlView $view, string $template, array $data): HtmlView protected function renderFullPage(HtmlView $view, string $template, array $data): HtmlView
{ {
$csp = [ $csp = [
"default-src 'self'", "default-src 'self' media.kitsu.io kitsu-production-media.s3.us-west-002.backblazeb2.com",
"object-src 'none'", "object-src 'none'",
"child-src 'self' *.youtube.com polyfill.io", "child-src 'self' *.youtube.com polyfill.io",
]; ];
@ -375,10 +375,10 @@ class Controller {
* @param array $data * @param array $data
* @param HtmlView|NULL $view * @param HtmlView|NULL $view
* @param int $code * @param int $code
* @throws InvalidArgumentException
* @return void * @return void
*@throws InvalidArgumentException
*/ */
protected function outputHTML(string $template, array $data = [], $view = NULL, int $code = 200): void protected function outputHTML(string $template, array $data = [], HtmlView $view = NULL, int $code = 200): void
{ {
if (NULL === $view) if (NULL === $view)
{ {

View File

@ -66,15 +66,13 @@ final class Anime extends BaseController {
/** /**
* Show a portion, or all of the anime list * Show a portion, or all of the anime list
* *
* @param string|int $status - The section of the list * @param int|string $status - The section of the list
* @param string|null $view - List or cover view * @param string|null $view - List or cover view
* @throws ContainerException * @return void
* @throws NotFoundException
* @throws InvalidArgumentException * @throws InvalidArgumentException
* @throws Throwable * @throws Throwable
* @return void
*/ */
public function index($status = KitsuWatchingStatus::WATCHING, ?string $view = NULL): void public function index(int|string $status = KitsuWatchingStatus::WATCHING, ?string $view = NULL): void
{ {
if ( ! in_array($status, [ if ( ! in_array($status, [
'all', 'all',
@ -178,7 +176,7 @@ final class Anime extends BaseController {
* @param string $id * @param string $id
* @param string $status * @param string $status
*/ */
public function edit(string $id, $status = 'all'): void public function edit(string $id, string $status = 'all'): void
{ {
$this->checkAuth(); $this->checkAuth();

View File

@ -82,7 +82,7 @@ final class AnimeCollection extends BaseController {
{ {
$queryParams = $this->request->getQueryParams(); $queryParams = $this->request->getQueryParams();
$query = $queryParams['query']; $query = $queryParams['query'];
$this->outputJSON($this->animeModel->search($query), 200); $this->outputJSON($this->animeModel->search($query, inCollection: TRUE), 200);
} }
/** /**
@ -239,8 +239,6 @@ final class AnimeCollection extends BaseController {
* Update a collection item * Update a collection item
* *
* @param array $data * @param array $data
* @throws ContainerException
* @throws NotFoundException
*/ */
protected function update(array $data): void protected function update(array $data): void
{ {

View File

@ -78,8 +78,6 @@ final class History extends BaseController {
'url_type' => 'anime', 'url_type' => 'anime',
]); ]);
// $this->outputJSON($this->animeModel->getHistory());
// return;
$this->outputHTML('history', [ $this->outputHTML('history', [
'title' => $this->formatTitle( 'title' => $this->formatTitle(
$this->config->get('whose_list') . "'s Anime List", $this->config->get('whose_list') . "'s Anime List",
@ -98,8 +96,6 @@ final class History extends BaseController {
'url_type' => 'manga', 'url_type' => 'manga',
]); ]);
// $this->outputJSON($this->mangaModel->getHistory());
// return;
$this->outputHTML('history', [ $this->outputHTML('history', [
'title' => $this->formatTitle( 'title' => $this->formatTitle(
$this->config->get('whose_list') . "'s Manga List", $this->config->get('whose_list') . "'s Manga List",

View File

@ -16,8 +16,6 @@
namespace Aviat\AnimeClient\Controller; namespace Aviat\AnimeClient\Controller;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
use function Amp\Promise\wait; use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse; use function Aviat\AnimeClient\getResponse;
use function Aviat\AnimeClient\createPlaceholderImage; use function Aviat\AnimeClient\createPlaceholderImage;
@ -39,7 +37,7 @@ final class Images extends BaseController {
* @return void * @return void
* @throws Throwable * @throws Throwable
*/ */
public function cache(string $type, string $file, $display = TRUE): void public function cache(string $type, string $file, bool $display = TRUE): void
{ {
$currentUrl = (string)$this->request->getUri(); $currentUrl = (string)$this->request->getUri();

View File

@ -64,10 +64,10 @@ final class Manga extends Controller {
* *
* @param string $status * @param string $status
* @param string $view * @param string $view
* @throws InvalidArgumentException
* @return void * @return void
*@throws InvalidArgumentException
*/ */
public function index($status = 'all', $view = ''): void public function index(string $status = 'all', ?string $view = ''): void
{ {
if ( ! in_array($status, [ if ( ! in_array($status, [
'all', 'all',

View File

@ -52,8 +52,6 @@ final class People extends BaseController {
* *
* @param string $slug * @param string $slug
* @return void * @return void
* @throws ContainerException
* @throws NotFoundException
*/ */
public function index(string $slug): void public function index(string $slug): void
{ {

View File

@ -16,6 +16,8 @@
namespace Aviat\AnimeClient; namespace Aviat\AnimeClient;
use Aviat\AnimeClient\Enum\EventType;
use Aviat\Ion\Event;
use Aviat\Ion\Json; use Aviat\Ion\Json;
use Aura\Router\{ use Aura\Router\{
Map, Map,
@ -27,7 +29,6 @@ use Aviat\AnimeClient\API\FailedResponseException;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Friend; use Aviat\Ion\Friend;
use Aviat\Ion\Type\StringType; use Aviat\Ion\Type\StringType;
use JetBrains\PhpStorm\ArrayShape;
use LogicException; use LogicException;
use ReflectionException; use ReflectionException;
@ -117,7 +118,7 @@ final class Dispatcher extends RoutingBase {
* @return void * @return void
* @throws ReflectionException * @throws ReflectionException
*/ */
public function __invoke($route = NULL): void public function __invoke(object $route = NULL): void
{ {
$logger = $this->container->getLogger(); $logger = $this->container->getLogger();
@ -136,7 +137,7 @@ final class Dispatcher extends RoutingBase {
{ {
// If not route was matched, return an appropriate http // If not route was matched, return an appropriate http
// error message // error message
$errorRoute = (array)$this->getErrorParams(); $errorRoute = $this->getErrorParams();
$controllerName = DEFAULT_CONTROLLER; $controllerName = DEFAULT_CONTROLLER;
$actionMethod = $errorRoute['action_method']; $actionMethod = $errorRoute['action_method'];
$params = $errorRoute['params']; $params = $errorRoute['params'];
@ -278,17 +279,14 @@ final class Dispatcher extends RoutingBase {
*/ */
protected function call(string $controllerName, string $method, array $params): void protected function call(string $controllerName, string $method, array $params): void
{ {
$logger = $this->container->getLogger('default'); $logger = $this->container->getLogger();
try try
{ {
$controller = new $controllerName($this->container); $controller = new $controllerName($this->container);
// Run the appropriate controller method // Run the appropriate controller method
if ($logger !== NULL) $logger?->debug('Dispatcher - controller arguments', $params);
{
$logger->debug('Dispatcher - controller arguments', $params);
}
$params = array_values($params); $params = array_values($params);
$controller->$method(...$params); $controller->$method(...$params);
@ -360,7 +358,7 @@ final class Dispatcher extends RoutingBase {
} }
/** /**
* Select controller based on the current url, and apply its relevent routes * Select controller based on the current url, and apply its relevant routes
* *
* @return array * @return array
*/ */

View File

@ -336,6 +336,24 @@ final class Kitsu {
return $valid; return $valid;
} }
/**
* Get the url of the posterImage from Kitsu, with fallbacks
*
* @param array $base
* @param int $size
* @return string
*/
public static function getPosterImage(array $base, int $size = 1): string
{
$rawUrl = $base['posterImage']['views'][$size]['url']
?? $base['posterImage']['original']['url']
?? '/public/images/placeholder.png';
$parts = explode('?', $rawUrl);
return ( ! empty($parts)) ? $parts[0] : $rawUrl;
}
/** /**
* Get the name and logo for the streaming service of the current link * Get the name and logo for the streaming service of the current link
* *

View File

@ -16,17 +16,8 @@
namespace Aviat\AnimeClient\Model; namespace Aviat\AnimeClient\Model;
use Aviat\AnimeClient\API\ParallelAPIRequest;
use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus; use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
use Aviat\AnimeClient\Types\{ use Aviat\AnimeClient\Types\Anime as AnimeType;
Anime as AnimeType,
FormItem,
AnimeListItem
};
use Aviat\Ion\Json;
use Throwable;
use function is_array;
/** /**
* Model for handling requests dealing with the anime list * Model for handling requests dealing with the anime list
@ -64,7 +55,7 @@ class Anime extends API {
{ {
$data = $this->kitsuModel->getFullOrganizedAnimeList(); $data = $this->kitsuModel->getFullOrganizedAnimeList();
foreach($data as $section => &$list) foreach($data as &$list)
{ {
$this->sortByName($list, 'anime'); $this->sortByName($list, 'anime');
} }

View File

@ -578,7 +578,7 @@ final class AnimeCollection extends Collection {
* @param string $animeId The current anime * @param string $animeId The current anime
* @return void * @return void
*/ */
private function updateGenres($animeId): void private function updateGenres(string $animeId): void
{ {
if ($this->db === NULL) if ($this->db === NULL)
{ {

View File

@ -71,11 +71,19 @@ trait MediaTrait {
* Search for anime by name * Search for anime by name
* *
* @param string $name * @param string $name
* @param bool $inCollection
* @return array * @return array
*/ */
public function search(string $name): array public function search(string $name, bool $inCollection = false): array
{ {
return $this->kitsuModel->search($this->type, urldecode($name)); $data = $this->kitsuModel->search($this->type, urldecode($name));
if ($inCollection)
{
// @TODO: allow filtering collection search by existing items
}
return $data;
} }
/** /**

View File

@ -28,6 +28,8 @@ final class Character extends AbstractType {
public ?Media $media; public ?Media $media;
public string $image;
public ?string $name; public ?string $name;
public array $names = []; public array $names = [];

View File

@ -25,6 +25,8 @@ final class Person extends AbstractType {
public ?string $name; public ?string $name;
public string $image;
public array $names = []; public array $names = [];
public ?string $description; public ?string $description;

View File

@ -46,8 +46,6 @@ class Util {
* Set up the Util class * Set up the Util class
* *
* @param ContainerInterface $container * @param ContainerInterface $container
* @throws ContainerException
* @throws NotFoundException
*/ */
public function __construct(ContainerInterface $container) public function __construct(ContainerInterface $container)
{ {

View File

@ -45,10 +45,10 @@ class Config implements ConfigInterface {
/** /**
* Does the config item exist? * Does the config item exist?
* *
* @param string|int|array $key * @param array|int|string $key
* @return bool * @return bool
*/ */
public function has($key): bool public function has(array|int|string $key): bool
{ {
return $this->map->hasKey($key); return $this->map->hasKey($key);
} }
@ -60,7 +60,7 @@ class Config implements ConfigInterface {
* @return mixed * @return mixed
* @throws ConfigException * @throws ConfigException
*/ */
public function get($key = NULL) public function get(array|string $key = NULL): mixed
{ {
if (\is_array($key)) if (\is_array($key))
{ {
@ -73,10 +73,10 @@ class Config implements ConfigInterface {
/** /**
* Remove a config value * Remove a config value
* *
* @param string|array $key * @param array|string $key
* @return void * @return void
*/ */
public function delete($key): void public function delete(array|string $key): void
{ {
if (\is_array($key)) if (\is_array($key))
{ {
@ -92,12 +92,12 @@ class Config implements ConfigInterface {
/** /**
* Set a config value * Set a config value
* *
* @param integer|string|array $key * @param array|integer|string $key
* @param mixed $value * @param mixed $value
* @throws InvalidArgumentException
* @return ConfigInterface * @return ConfigInterface
*@throws InvalidArgumentException
*/ */
public function set($key, $value): ConfigInterface public function set(array|int|string $key, mixed $value): ConfigInterface
{ {
if (\is_array($key)) if (\is_array($key))
{ {

View File

@ -23,10 +23,10 @@ interface ConfigInterface {
/** /**
* Does the config item exist? * Does the config item exist?
* *
* @param string|int|array $key * @param array|int|string $key
* @return bool * @return bool
*/ */
public function has($key): bool; public function has(array|int|string $key): bool;
/** /**
* Get a config value * Get a config value
@ -34,23 +34,23 @@ interface ConfigInterface {
* @param array|string|null $key * @param array|string|null $key
* @return mixed * @return mixed
*/ */
public function get($key = NULL); public function get(array|string $key = NULL): mixed;
/** /**
* Set a config value * Set a config value
* *
* @param integer|string|array $key * @param array|integer|string $key
* @param mixed $value * @param mixed $value
* @throws \InvalidArgumentException
* @return ConfigInterface * @return ConfigInterface
* @throws \InvalidArgumentException
*/ */
public function set($key, $value): self; public function set(array|int|string $key, mixed $value): self;
/** /**
* Remove a config value * Remove a config value
* *
* @param string|array $key * @param array|string $key
* @return void * @return void
*/ */
public function delete($key): void; public function delete(array|string $key): void;
} }

View File

@ -51,7 +51,7 @@ abstract class Enum {
* @return boolean * @return boolean
* @throws ReflectionException * @throws ReflectionException
*/ */
public static function isValid($key): bool public static function isValid(mixed $key): bool
{ {
$values = array_values(static::getConstList()); $values = array_values(static::getConstList());
return in_array($key, $values, TRUE); return in_array($key, $values, TRUE);

View File

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\Ion\Exception;
use RuntimeException;
/**
* Exception for bad configuration
*/
class ImageCreationException extends RuntimeException {
}

View File

@ -31,7 +31,7 @@ class Friend {
* Object to create a friend of * Object to create a friend of
* @var mixed * @var mixed
*/ */
private $_friend_; private mixed $_friend_;
/** /**
* Reflection class of the object * Reflection class of the object
@ -46,7 +46,7 @@ class Friend {
* @throws InvalidArgumentException * @throws InvalidArgumentException
* @throws \ReflectionException * @throws \ReflectionException
*/ */
public function __construct($obj) public function __construct(mixed $obj)
{ {
if ( ! \is_object($obj)) if ( ! \is_object($obj))
{ {
@ -63,7 +63,7 @@ class Friend {
* @param string $key * @param string $key
* @return mixed * @return mixed
*/ */
public function __get(string $key) public function __get(string $key): mixed
{ {
if ($this->__isset($key)) if ($this->__isset($key))
{ {
@ -96,7 +96,7 @@ class Friend {
* @param mixed $value * @param mixed $value
* @return void * @return void
*/ */
public function __set(string $key, $value) public function __set(string $key, mixed $value)
{ {
if ($this->__isset($key)) if ($this->__isset($key))
{ {

167
src/Ion/ImageBuilder.php Normal file
View File

@ -0,0 +1,167 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\Ion;
use GdImage;
use Aviat\Ion\Exception\ImageCreationException;
/**
* A wrapper around GD functions to create images
*
* @property GdImage|false|null $img
*/
class ImageBuilder {
private GDImage|false|null $_img;
private int $fontSize = 10;
private function __construct(private int $width=200, private int $height=200 )
{
$this->_img = imagecreatetruecolor($this->width, $this->height);
}
public function __destruct()
{
$this->cleanup();
}
private function getImg(): GdImage
{
if ($this->_img instanceof GdImage)
{
return $this->_img;
}
throw new ImageCreationException('Invalid GD object');
}
public static function new(int $width=200, int $height=200): self
{
$i = new self($width, $height);
if ($i->_img === FALSE)
{
throw new ImageCreationException('Could not create image object');
}
return $i;
}
public function setFontSize(int $size): self
{
$this->fontSize = $size;
return $this;
}
public function enableAlphaBlending(bool $enable): self
{
$ab = imagealphablending($this->getImg(), $enable);
if ( ! $ab)
{
throw new ImageCreationException('Failed to toggle image alpha blending');
}
return $this;
}
public function addCenteredText(string $text, int $red, int $green, int $blue, int $alpha=-1): self
{
// Create the font color
$textColor = ($alpha > -1)
? imagecolorallocatealpha($this->getImg(), $red, $green, $blue, $alpha)
: imagecolorallocate($this->getImg(), $red, $green, $blue);
if ($textColor === FALSE)
{
throw new ImageCreationException('Could not create image text color');
}
// Generate placeholder text
$fontWidth = imagefontwidth($this->fontSize);
$fontHeight = imagefontheight($this->fontSize);
$length = strlen($text);
$textWidth = $length * $fontWidth;
$fxPos = (int) ceil((imagesx($this->getImg()) - $textWidth) / 2);
$fyPos = (int) ceil((imagesy($this->getImg()) - $fontHeight) / 2);
// Add the image text
imagestring($this->getImg(), $this->fontSize, $fxPos, $fyPos, $text, $textColor);
return $this;
}
public function addBackgroundColor(int $red, int $green, int $blue, int $alpha=-1): self
{
$fillColor = ($alpha > -1)
? imagecolorallocatealpha($this->getImg(), $red, $green, $blue, $alpha)
: imagecolorallocate($this->getImg(), $red, $green, $blue);
if ($fillColor === FALSE)
{
throw new ImageCreationException('Failed to create image fill color');
}
$hasFilled = imagefill($this->getImg(), 0, 0, $fillColor);
if ($hasFilled === FALSE)
{
throw new ImageCreationException('Failed to add background color to image');
}
return $this;
}
public function savePng(string $savePath, bool $saveAlpha = TRUE): bool
{
$setAlpha = imagesavealpha($this->getImg(), $saveAlpha);
if ($setAlpha === FALSE)
{
throw new ImageCreationException('Failed to set image save alpha flag');
}
return imagepng($this->getImg(), $savePath, 9);
}
public function saveWebp(string $savePath): bool
{
return imagewebp($this->getImg(), $savePath);
}
public function saveJpg(string $savePath): bool
{
return imagejpeg($this->getImg(), $savePath);
}
public function saveGif(string $savePath): bool
{
return imagegif($this->getImg(), $savePath);
}
public function cleanup(): void
{
$cleaned = FALSE;
if ($this->getImg() instanceof GdImage)
{
$cleaned = imagedestroy($this->getImg());
}
if ($cleaned === FALSE)
{
throw new ImageCreationException('Failed to clean up image resource');
}
}
}

View File

@ -93,7 +93,7 @@ class HttpView implements HttpViewInterface{
* @param string|string[] $value * @param string|string[] $value
* @return HttpView * @return HttpView
*/ */
public function addHeader(string $name, $value): self public function addHeader(string $name, array|string $value): self
{ {
$this->response = $this->response->withHeader($name, $value); $this->response = $this->response->withHeader($name, $value);
return $this; return $this;
@ -105,7 +105,7 @@ class HttpView implements HttpViewInterface{
* @param mixed $string * @param mixed $string
* @return HttpViewInterface * @return HttpViewInterface
*/ */
public function setOutput($string): HttpViewInterface public function setOutput(mixed $string): HttpViewInterface
{ {
$this->response->getBody()->write($string); $this->response->getBody()->write($string);

View File

@ -37,7 +37,7 @@ interface ViewInterface {
* @param mixed $string * @param mixed $string
* @return ViewInterface * @return ViewInterface
*/ */
public function setOutput($string): self; public function setOutput(mixed $string): self;
/** /**
* Append additional output. * Append additional output.
@ -54,7 +54,7 @@ interface ViewInterface {
* @param string|string[] $value * @param string|string[] $value
* @return ViewInterface * @return ViewInterface
*/ */
public function addHeader(string $name, $value): self; public function addHeader(string $name, array|string $value): self;
/** /**
* Get the current output as a string. Does not * Get the current output as a string. Does not

View File

@ -4,7 +4,7 @@
mal_id: '28091' mal_id: '28091'
chapters: { read: 94, total: '-' } chapters: { read: 94, total: '-' }
volumes: { read: '-', total: '-' } volumes: { read: '-', total: '-' }
manga: { empty: false, genres: { }, id: '21733', image: 'https://media.kitsu.io/manga/poster_images/21733/small.jpg?1496845097', slug: tonari-no-seki-kun, title: 'Tonari no Seki-kun', titles: ['My Neighbour Seki', となりの関くん], type: Manga, url: 'https://kitsu.io/manga/tonari-no-seki-kun' } manga: { empty: false, genres: { }, id: '21733', image: 'https://media.kitsu.io/manga/poster_images/21733/small.jpg', slug: tonari-no-seki-kun, title: 'Tonari no Seki-kun', titles: ['My Neighbour Seki', となりの関くん], type: Manga, url: 'https://kitsu.io/manga/tonari-no-seki-kun' }
reading_status: current reading_status: current
notes: '' notes: ''
rereading: false rereading: false
@ -16,7 +16,7 @@
mal_id: '60815' mal_id: '60815'
chapters: { read: 87, total: '-' } chapters: { read: 87, total: '-' }
volumes: { read: '-', total: '-' } volumes: { read: '-', total: '-' }
manga: { empty: false, genres: { }, id: '25491', image: 'https://media.kitsu.io/manga/poster_images/25491/small.jpg?1499026452', slug: joshikausei, title: Joshikausei, titles: [女子かう生], type: Manga, url: 'https://kitsu.io/manga/joshikausei' } manga: { empty: false, genres: { }, id: '25491', image: 'https://media.kitsu.io/manga/poster_images/25491/small.jpg', slug: joshikausei, title: Joshikausei, titles: [女子かう生], type: Manga, url: 'https://kitsu.io/manga/joshikausei' }
reading_status: current reading_status: current
notes: 'Wordless, and it works.' notes: 'Wordless, and it works.'
rereading: false rereading: false

View File

@ -2,10 +2,10 @@ empty: false
age_rating: PG age_rating: PG
age_rating_guide: '' age_rating_guide: ''
characters: characters:
main: { 40541: { image: { original: { height: null, name: original, url: 'https://media.kitsu.io/characters/images/40541/original.jpg?1483096805', width: null } }, name: 'Kazunari Usa', slug: kazunari-usa }, 40540: { image: { original: { height: null, name: original, url: 'https://media.kitsu.io/characters/images/40540/original.jpg?1483096805', width: null } }, name: 'Ritsu Kawai', slug: ritsu-kawai } } main: { 40541: { image: 'https://media.kitsu.io/characters/images/40541/original.jpg?1483096805', name: 'Kazunari Usa', slug: kazunari-usa }, 40540: { image: 'https://media.kitsu.io/characters/images/40540/original.jpg?1483096805', name: 'Ritsu Kawai', slug: ritsu-kawai } }
background: { 62591: { image: { original: { height: null, name: original, url: 'https://media.kitsu.io/characters/images/62591/original.jpg?1485073100', width: null } }, name: Chinatsu, slug: chinatsu }, 72839: { image: { original: { height: null, name: original, url: 'https://media.kitsu.io/characters/images/72839/original.jpg?1485079724', width: null } }, name: Hayashi, slug: hayashi-ec3a2705-5d5c-493c-b172-bbee2d04b5b9 }, 78362: { image: { original: { height: null, name: original, url: 'https://media.kitsu.io/characters/images/78362/original.jpg?1485081676', width: null } }, name: Houjou, slug: houjou }, 90353: { image: { original: { height: null, name: original, url: 'https://media.kitsu.io/characters/images/90353/original.jpg?1485086356', width: null } }, name: Kurokawa, slug: kurokawa-a493ddf6-0f02-4abf-8b18-ab6ae2198b6e }, 77996: { image: { original: { height: null, name: original, url: 'https://media.kitsu.io/characters/images/77996/original.jpg?1485081552', width: null } }, name: Maemura, slug: maemura }, 55132: { image: { original: { height: null, name: original, url: 'https://media.kitsu.io/characters/images/55132/original.jpg?1483096805', width: null } }, name: 'Mayumi Nishikino', slug: mayumi-nishikino }, 71479: { image: { original: { height: null, name: original, url: 'https://media.kitsu.io/characters/images/71479/original.jpg?1485079211', width: null } }, name: 'Miharu Tsuneda', slug: miharu-tsuneda }, 55134: { image: { original: { height: null, name: original, url: '/images/original/missing.png?1483096805', width: null } }, name: 'Mother Usa', slug: mother-usa }, 55135: { image: { original: { height: null, name: original, url: 'https://media.kitsu.io/characters/images/55135/original.jpg?1483096805', width: null } }, name: 'Sayaka Watanabe', slug: sayaka-watanabe }, 95032: { image: { original: { height: null, name: original, url: 'https://media.kitsu.io/characters/images/95032/original.jpg?1485088329', width: null } }, name: Shirosaki, slug: shirosaki-2ed2e15c-9cee-4756-92f1-027c4820e224 }, 55131: { image: { original: { height: null, name: original, url: 'https://media.kitsu.io/characters/images/55131/original.jpg?1483096805', width: null } }, name: 'Sumiko Kawai', slug: sumiko-kawai }, 74335: { image: { original: { height: null, name: original, url: 'https://media.kitsu.io/characters/images/74335/original.jpg?1485080245', width: null } }, name: 'Tae Shinohara', slug: tae-shinohara }, 55133: { image: { original: { height: null, name: original, url: 'https://media.kitsu.io/characters/images/55133/original.jpg?1483096805', width: null } }, name: Tagami, slug: tagami }, 83463: { image: { original: { height: null, name: original, url: /images/original/missing.png, width: null } }, name: 'Yoko Mabuchi', slug: yoko-mabuchi } } background: { 62591: { image: 'https://media.kitsu.io/characters/images/62591/original.jpg?1485073100', name: Chinatsu, slug: chinatsu }, 72839: { image: 'https://media.kitsu.io/characters/images/72839/original.jpg?1485079724', name: Hayashi, slug: hayashi-ec3a2705-5d5c-493c-b172-bbee2d04b5b9 }, 78362: { image: 'https://media.kitsu.io/characters/images/78362/original.jpg?1485081676', name: Houjou, slug: houjou }, 90353: { image: 'https://media.kitsu.io/characters/images/90353/original.jpg?1485086356', name: Kurokawa, slug: kurokawa-a493ddf6-0f02-4abf-8b18-ab6ae2198b6e }, 77996: { image: 'https://media.kitsu.io/characters/images/77996/original.jpg?1485081552', name: Maemura, slug: maemura }, 55132: { image: 'https://media.kitsu.io/characters/images/55132/original.jpg?1483096805', name: 'Mayumi Nishikino', slug: mayumi-nishikino }, 71479: { image: 'https://media.kitsu.io/characters/images/71479/original.jpg?1485079211', name: 'Miharu Tsuneda', slug: miharu-tsuneda }, 55134: { image: '/images/original/missing.png?1483096805', name: 'Mother Usa', slug: mother-usa }, 55135: { image: 'https://media.kitsu.io/characters/images/55135/original.jpg?1483096805', name: 'Sayaka Watanabe', slug: sayaka-watanabe }, 95032: { image: 'https://media.kitsu.io/characters/images/95032/original.jpg?1485088329', name: Shirosaki, slug: shirosaki-2ed2e15c-9cee-4756-92f1-027c4820e224 }, 55131: { image: 'https://media.kitsu.io/characters/images/55131/original.jpg?1483096805', name: 'Sumiko Kawai', slug: sumiko-kawai }, 74335: { image: 'https://media.kitsu.io/characters/images/74335/original.jpg?1485080245', name: 'Tae Shinohara', slug: tae-shinohara }, 55133: { image: 'https://media.kitsu.io/characters/images/55133/original.jpg?1483096805', name: Tagami, slug: tagami }, 83463: { image: /images/original/missing.png, name: 'Yoko Mabuchi', slug: yoko-mabuchi } }
chapter_count: 90 chapter_count: 90
cover_image: 'https://media.kitsu.io/manga/poster_images/20286/small.jpg?1434293999' cover_image: 'https://media.kitsu.io/manga/poster_images/20286/small.jpg'
genres: genres:
- Comedy - Comedy
- Romance - Romance
@ -19,7 +19,7 @@ id: '20286'
manga_type: MANGA manga_type: MANGA
status: Completed status: Completed
staff: staff:
'Story & Art': [{ id: '8712', slug: ruri-miyahara, name: 'Ruri Miyahara', image: { original: 'https://media.kitsu.io/people/images/8712/original.jpg?1533271952' } }] 'Story & Art': [{ id: '8712', slug: ruri-miyahara, name: 'Ruri Miyahara', image: 'https://media.kitsu.io/people/images/8712/original.jpg?1533271952' }]
synopsis: "Usa, a high-school student aspiring to begin a bachelor lifestyle, moves into a new apartment only to discover that he not only shares a room with a perverted roommate that has an obsession for underaged girls, but also that another girl, Ritsu, a love-at-first-sight, is living in the same building as well!\r\n(Source: Kirei Cake)" synopsis: "Usa, a high-school student aspiring to begin a bachelor lifestyle, moves into a new apartment only to discover that he not only shares a room with a perverted roommate that has an obsession for underaged girls, but also that another girl, Ritsu, a love-at-first-sight, is living in the same building as well!\r\n(Source: Kirei Cake)"
title: 'Bokura wa Minna Kawai-sou' title: 'Bokura wa Minna Kawai-sou'
titles: titles:

View File

@ -1,6 +1,6 @@
empty: false empty: false
about: 'Web Developer, Anime Fan, Reader of VNs, and web comics.' about: 'Web Developer, Anime Fan, Reader of VNs, and web comics.'
avatar: images/avatars/2644.gif avatar: 'https://media.kitsu.io/users/avatars/2644/original.gif?1491510751'
favorites: favorites:
anime: { 933073: { __typename: Anime, id: '14212', slug: hataraku-saibou-tv, posterImage: { original: { url: 'https://media.kitsu.io/anime/poster_images/14212/original.jpg?1597697195', height: 1050, width: 750 }, views: [{ url: 'https://media.kitsu.io/anime/poster_images/14212/tiny.jpg?1597697195', height: 156, width: 110 }, { url: 'https://media.kitsu.io/anime/poster_images/14212/small.jpg?1597697195', height: 402, width: 284 }, { url: 'https://media.kitsu.io/anime/poster_images/14212/medium.jpg?1597697195', height: 554, width: 390 }, { url: 'https://media.kitsu.io/anime/poster_images/14212/large.jpg?1597697195', height: 780, width: 550 }] }, titles: { canonical: 'Hataraku Saibou', localized: { en: 'Cells at Work!', en_jp: 'Hataraku Saibou', ja_jp: はたらく細胞 } } }, 586217: { __typename: Anime, id: '323', slug: fate-stay-night, posterImage: { original: { url: 'https://media.kitsu.io/anime/poster_images/323/original.jpg?1597698066', height: 1074, width: 760 }, views: [{ url: 'https://media.kitsu.io/anime/poster_images/323/tiny.jpg?1597698066', height: 156, width: 110 }, { url: 'https://media.kitsu.io/anime/poster_images/323/small.jpg?1597698066', height: 402, width: 284 }, { url: 'https://media.kitsu.io/anime/poster_images/323/medium.jpg?1597698066', height: 554, width: 390 }, { url: 'https://media.kitsu.io/anime/poster_images/323/large.jpg?1597698066', height: 780, width: 550 }] }, titles: { canonical: 'Fate/stay night', localized: { en: 'Fate/stay night', en_jp: 'Fate/stay night', en_us: 'Fate/stay night', ja_jp: 'Fate/stay night' } } }, 607473: { __typename: Anime, id: '310', slug: tsukuyomi-moon-phase, posterImage: { original: { url: 'https://media.kitsu.io/anime/poster_images/310/original.jpg?1597690591', height: 320, width: 225 }, views: [{ url: 'https://media.kitsu.io/anime/poster_images/310/tiny.jpg?1597690591', height: 156, width: 110 }, { url: 'https://media.kitsu.io/anime/poster_images/310/small.jpg?1597690591', height: 402, width: 284 }, { url: 'https://media.kitsu.io/anime/poster_images/310/medium.jpg?1597690591', height: 554, width: 390 }, { url: 'https://media.kitsu.io/anime/poster_images/310/large.jpg?1597690591', height: 780, width: 550 }] }, titles: { canonical: 'Tsukuyomi: Moon Phase', localized: { en: 'Tsukuyomi: Moon Phase', en_jp: 'Tsukuyomi: Moon Phase', en_us: 'Tsukuyomi: Moon Phase', ja_jp: '月詠 MOON PHASE' } } }, 607472: { __typename: Anime, id: '5992', slug: carnival-phantasm, posterImage: { original: { url: 'https://media.kitsu.io/anime/poster_images/5992/original.jpg?1597697878', height: 693, width: 533 }, views: [{ url: 'https://media.kitsu.io/anime/poster_images/5992/tiny.jpg?1597697878', height: 156, width: 110 }, { url: 'https://media.kitsu.io/anime/poster_images/5992/small.jpg?1597697878', height: 402, width: 284 }, { url: 'https://media.kitsu.io/anime/poster_images/5992/medium.jpg?1597697878', height: 554, width: 390 }, { url: 'https://media.kitsu.io/anime/poster_images/5992/large.jpg?1597697878', height: 780, width: 550 }] }, titles: { canonical: 'Carnival Phantasm', localized: { en_jp: 'Carnival Phantasm', ja_jp: カーニバル・ファンタズム } } }, 636892: { __typename: Anime, id: '6062', slug: nichijou, posterImage: { original: { url: 'https://media.kitsu.io/anime/poster_images/6062/original.jpg?1597696783', height: 2292, width: 1610 }, views: [{ url: 'https://media.kitsu.io/anime/poster_images/6062/tiny.jpg?1597696783', height: 156, width: 110 }, { url: 'https://media.kitsu.io/anime/poster_images/6062/small.jpg?1597696783', height: 402, width: 284 }, { url: 'https://media.kitsu.io/anime/poster_images/6062/medium.jpg?1597696783', height: 554, width: 390 }, { url: 'https://media.kitsu.io/anime/poster_images/6062/large.jpg?1597696783', height: 780, width: 550 }] }, titles: { canonical: Nichijou, localized: { en: 'Nichijou - My Ordinary Life', en_jp: Nichijou, en_us: 'Nichijou - My Ordinary Life', ja_jp: 日常 } } } } anime: { 933073: { __typename: Anime, id: '14212', slug: hataraku-saibou-tv, posterImage: { original: { url: 'https://media.kitsu.io/anime/poster_images/14212/original.jpg?1597697195', height: 1050, width: 750 }, views: [{ url: 'https://media.kitsu.io/anime/poster_images/14212/tiny.jpg?1597697195', height: 156, width: 110 }, { url: 'https://media.kitsu.io/anime/poster_images/14212/small.jpg?1597697195', height: 402, width: 284 }, { url: 'https://media.kitsu.io/anime/poster_images/14212/medium.jpg?1597697195', height: 554, width: 390 }, { url: 'https://media.kitsu.io/anime/poster_images/14212/large.jpg?1597697195', height: 780, width: 550 }] }, titles: { canonical: 'Hataraku Saibou', localized: { en: 'Cells at Work!', en_jp: 'Hataraku Saibou', ja_jp: はたらく細胞 } } }, 586217: { __typename: Anime, id: '323', slug: fate-stay-night, posterImage: { original: { url: 'https://media.kitsu.io/anime/poster_images/323/original.jpg?1597698066', height: 1074, width: 760 }, views: [{ url: 'https://media.kitsu.io/anime/poster_images/323/tiny.jpg?1597698066', height: 156, width: 110 }, { url: 'https://media.kitsu.io/anime/poster_images/323/small.jpg?1597698066', height: 402, width: 284 }, { url: 'https://media.kitsu.io/anime/poster_images/323/medium.jpg?1597698066', height: 554, width: 390 }, { url: 'https://media.kitsu.io/anime/poster_images/323/large.jpg?1597698066', height: 780, width: 550 }] }, titles: { canonical: 'Fate/stay night', localized: { en: 'Fate/stay night', en_jp: 'Fate/stay night', en_us: 'Fate/stay night', ja_jp: 'Fate/stay night' } } }, 607473: { __typename: Anime, id: '310', slug: tsukuyomi-moon-phase, posterImage: { original: { url: 'https://media.kitsu.io/anime/poster_images/310/original.jpg?1597690591', height: 320, width: 225 }, views: [{ url: 'https://media.kitsu.io/anime/poster_images/310/tiny.jpg?1597690591', height: 156, width: 110 }, { url: 'https://media.kitsu.io/anime/poster_images/310/small.jpg?1597690591', height: 402, width: 284 }, { url: 'https://media.kitsu.io/anime/poster_images/310/medium.jpg?1597690591', height: 554, width: 390 }, { url: 'https://media.kitsu.io/anime/poster_images/310/large.jpg?1597690591', height: 780, width: 550 }] }, titles: { canonical: 'Tsukuyomi: Moon Phase', localized: { en: 'Tsukuyomi: Moon Phase', en_jp: 'Tsukuyomi: Moon Phase', en_us: 'Tsukuyomi: Moon Phase', ja_jp: '月詠 MOON PHASE' } } }, 607472: { __typename: Anime, id: '5992', slug: carnival-phantasm, posterImage: { original: { url: 'https://media.kitsu.io/anime/poster_images/5992/original.jpg?1597697878', height: 693, width: 533 }, views: [{ url: 'https://media.kitsu.io/anime/poster_images/5992/tiny.jpg?1597697878', height: 156, width: 110 }, { url: 'https://media.kitsu.io/anime/poster_images/5992/small.jpg?1597697878', height: 402, width: 284 }, { url: 'https://media.kitsu.io/anime/poster_images/5992/medium.jpg?1597697878', height: 554, width: 390 }, { url: 'https://media.kitsu.io/anime/poster_images/5992/large.jpg?1597697878', height: 780, width: 550 }] }, titles: { canonical: 'Carnival Phantasm', localized: { en_jp: 'Carnival Phantasm', ja_jp: カーニバル・ファンタズム } } }, 636892: { __typename: Anime, id: '6062', slug: nichijou, posterImage: { original: { url: 'https://media.kitsu.io/anime/poster_images/6062/original.jpg?1597696783', height: 2292, width: 1610 }, views: [{ url: 'https://media.kitsu.io/anime/poster_images/6062/tiny.jpg?1597696783', height: 156, width: 110 }, { url: 'https://media.kitsu.io/anime/poster_images/6062/small.jpg?1597696783', height: 402, width: 284 }, { url: 'https://media.kitsu.io/anime/poster_images/6062/medium.jpg?1597696783', height: 554, width: 390 }, { url: 'https://media.kitsu.io/anime/poster_images/6062/large.jpg?1597696783', height: 780, width: 550 }] }, titles: { canonical: Nichijou, localized: { en: 'Nichijou - My Ordinary Life', en_jp: Nichijou, en_us: 'Nichijou - My Ordinary Life', ja_jp: 日常 } } } }
character: { 586219: { __typename: Character, id: '6553', slug: saber, image: { original: { url: 'https://media.kitsu.io/characters/images/6553/original.jpg?1483096805' } }, names: { alternatives: ['King of Knights'], canonical: Saber, canonicalLocale: null, localized: { en: Saber, ja_jp: セイバー } } }, 586218: { __typename: Character, id: '6556', slug: rin-tohsaka, image: { original: { url: 'https://media.kitsu.io/characters/images/6556/original.jpg?1483096805' } }, names: { alternatives: { }, canonical: 'Rin Toosaka', canonicalLocale: null, localized: { en: 'Rin Toosaka', ja_jp: '遠坂 凛' } } }, 611365: { __typename: Character, id: '32035', slug: nano-shinonome, image: { original: { url: 'https://media.kitsu.io/characters/images/32035/original.jpg?1483096805' } }, names: { alternatives: { }, canonical: 'Nano Shinonome', canonicalLocale: null, localized: { en: 'Nano Shinonome', ja_jp: '東雲 なの' } } }, 611364: { __typename: Character, id: '32034', slug: mio-naganohara, image: { original: { url: 'https://media.kitsu.io/characters/images/32034/original.jpg?1483096805' } }, names: { alternatives: { }, canonical: 'Mio Naganohara', canonicalLocale: null, localized: { en: 'Mio Naganohara', ja_jp: 長野原みお } } }, 636590: { __typename: Character, id: '31851', slug: aria-holmes-kanzaki, image: { original: { url: 'https://media.kitsu.io/characters/images/31851/original.jpg?1483096805' } }, names: { alternatives: ['Quadra Aria'], canonical: 'Aria Holmes Kanzaki', canonicalLocale: null, localized: { en: 'Aria Holmes Kanzaki', ja_jp: 神崎・H・アリア } } }, 636591: { __typename: Character, id: '25930', slug: taiga-aisaka, image: { original: { url: 'https://media.kitsu.io/characters/images/25930/original.jpg?1483096805' } }, names: { alternatives: ['Palmtop Tiger'], canonical: 'Taiga Aisaka', canonicalLocale: null, localized: { en: 'Taiga Aisaka', ja_jp: '逢坂 大河' } } }, 636593: { __typename: Character, id: '31625', slug: victorique-de-blois, image: { original: { url: 'https://media.kitsu.io/characters/images/31625/original.jpg?1483096805' } }, names: { alternatives: ['The Golden Fairy', 'Gray Wolf', 'Monstre Charmant'], canonical: 'Victorique de Blois', canonicalLocale: null, localized: { en: 'Victorique de Blois', ja_jp: ヴィクトリカ・ド・ブロワ } } } } character: { 586219: { __typename: Character, id: '6553', slug: saber, image: { original: { url: 'https://media.kitsu.io/characters/images/6553/original.jpg?1483096805' } }, names: { alternatives: ['King of Knights'], canonical: Saber, canonicalLocale: null, localized: { en: Saber, ja_jp: セイバー } } }, 586218: { __typename: Character, id: '6556', slug: rin-tohsaka, image: { original: { url: 'https://media.kitsu.io/characters/images/6556/original.jpg?1483096805' } }, names: { alternatives: { }, canonical: 'Rin Toosaka', canonicalLocale: null, localized: { en: 'Rin Toosaka', ja_jp: '遠坂 凛' } } }, 611365: { __typename: Character, id: '32035', slug: nano-shinonome, image: { original: { url: 'https://media.kitsu.io/characters/images/32035/original.jpg?1483096805' } }, names: { alternatives: { }, canonical: 'Nano Shinonome', canonicalLocale: null, localized: { en: 'Nano Shinonome', ja_jp: '東雲 なの' } } }, 611364: { __typename: Character, id: '32034', slug: mio-naganohara, image: { original: { url: 'https://media.kitsu.io/characters/images/32034/original.jpg?1483096805' } }, names: { alternatives: { }, canonical: 'Mio Naganohara', canonicalLocale: null, localized: { en: 'Mio Naganohara', ja_jp: 長野原みお } } }, 636590: { __typename: Character, id: '31851', slug: aria-holmes-kanzaki, image: { original: { url: 'https://media.kitsu.io/characters/images/31851/original.jpg?1483096805' } }, names: { alternatives: ['Quadra Aria'], canonical: 'Aria Holmes Kanzaki', canonicalLocale: null, localized: { en: 'Aria Holmes Kanzaki', ja_jp: 神崎・H・アリア } } }, 636591: { __typename: Character, id: '25930', slug: taiga-aisaka, image: { original: { url: 'https://media.kitsu.io/characters/images/25930/original.jpg?1483096805' } }, names: { alternatives: ['Palmtop Tiger'], canonical: 'Taiga Aisaka', canonicalLocale: null, localized: { en: 'Taiga Aisaka', ja_jp: '逢坂 大河' } } }, 636593: { __typename: Character, id: '31625', slug: victorique-de-blois, image: { original: { url: 'https://media.kitsu.io/characters/images/31625/original.jpg?1483096805' } }, names: { alternatives: ['The Golden Fairy', 'Gray Wolf', 'Monstre Charmant'], canonical: 'Victorique de Blois', canonicalLocale: null, localized: { en: 'Victorique de Blois', ja_jp: ヴィクトリカ・ド・ブロワ } } } }

View File

@ -20,7 +20,7 @@ use Aviat\Ion\Config;
class ConfigTest extends IonTestCase { class ConfigTest extends IonTestCase {
protected $config; protected Config $config;
public function setUp(): void public function setUp(): void
{ {
@ -66,12 +66,6 @@ class ConfigTest extends IonTestCase {
$this->assertEquals('great', $this->config->get(['apple', 'sauce', 'is']), "Array argument get for config failed."); $this->assertEquals('great', $this->config->get(['apple', 'sauce', 'is']), "Array argument get for config failed.");
} }
public function testConfigBadSet(): void
{
$this->expectException('InvalidArgumentException');
$this->config->set(NULL, FALSE);
}
public function dataConfigDelete(): array public function dataConfigDelete(): array
{ {
return [ return [