Version 5.1 - All the GraphQL #32
@ -181,6 +181,7 @@ $routes = [
|
||||
'user_info' => [
|
||||
'path' => '/user/{username}',
|
||||
'controller' => 'user',
|
||||
'action' => 'about',
|
||||
'tokens' => [
|
||||
'username' => '.*?'
|
||||
]
|
||||
|
@ -20,5 +20,3 @@ host = "127.0.0.1"
|
||||
|
||||
# Database number
|
||||
database = 2
|
||||
|
||||
[options]
|
@ -2,52 +2,55 @@
|
||||
<main class="details fixed">
|
||||
<section class="flex">
|
||||
<aside class="info">
|
||||
<?= $helper->picture("images/anime/{$show_data['id']}-original.webp") ?>
|
||||
<?= $helper->picture("images/anime/{$data['id']}-original.webp") ?>
|
||||
|
||||
<br />
|
||||
|
||||
<table class="media-details">
|
||||
<tr>
|
||||
<td class="align-right">Airing Status</td>
|
||||
<td><?= $show_data['status'] ?></td>
|
||||
<td><?= $data['status'] ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Show Type</td>
|
||||
<td><?= $show_data['show_type'] ?></td>
|
||||
<td><?= $data['show_type'] ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Episode Count</td>
|
||||
<td><?= $show_data['episode_count'] ?? '-' ?></td>
|
||||
<td><?= $data['episode_count'] ?? '-' ?></td>
|
||||
</tr>
|
||||
<?php if ( ! empty($show_data['episode_length'])): ?>
|
||||
<?php if ( ! empty($data['episode_length'])): ?>
|
||||
<tr>
|
||||
<td>Episode Length</td>
|
||||
<td><?= $show_data['episode_length'] ?> minutes</td>
|
||||
<td><?= $data['episode_length'] ?> minutes</td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
<?php if ( ! empty($show_data['age_rating'])): ?>
|
||||
<?php if ( ! empty($data['age_rating'])): ?>
|
||||
<tr>
|
||||
<td>Age Rating</td>
|
||||
<td><abbr title="<?= $show_data['age_rating_guide'] ?>"><?= $show_data['age_rating'] ?></abbr>
|
||||
<td><abbr title="<?= $data['age_rating_guide'] ?>"><?= $data['age_rating'] ?></abbr>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
<tr>
|
||||
<td>Genres</td>
|
||||
<td>
|
||||
<?= implode(', ', $show_data['genres']) ?>
|
||||
<?= implode(', ', $data['genres']) ?>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
</aside>
|
||||
<article class="text">
|
||||
<h2 class="toph"><a rel="external" href="<?= $show_data['url'] ?>"><?= $show_data['title'] ?></a></h2>
|
||||
<?php foreach ($show_data['titles'] as $title): ?>
|
||||
<h2 class="toph"><a rel="external" href="<?= $data['url'] ?>"><?= $data['title'] ?></a></h2>
|
||||
<?php foreach ($data['titles'] as $title): ?>
|
||||
<h3><?= $title ?></h3>
|
||||
<?php endforeach ?>
|
||||
<br />
|
||||
<p class="description"><?= nl2br($show_data['synopsis']) ?></p>
|
||||
<?php if (count($show_data['streaming_links']) > 0): ?>
|
||||
<p class="description"><?= nl2br($data['synopsis']) ?></p>
|
||||
<?php if (count($data['streaming_links']) > 0): ?>
|
||||
<hr />
|
||||
<h4>Streaming on:</h4>
|
||||
<table class="full-width invisible streaming-links">
|
||||
@ -59,13 +62,13 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($show_data['streaming_links'] as $link): ?>
|
||||
<?php foreach ($data['streaming_links'] as $link): ?>
|
||||
<tr>
|
||||
<td class="align-left">
|
||||
<?php if ($link['meta']['link'] !== FALSE): ?>
|
||||
<a
|
||||
href="<?= $link['link'] ?>"
|
||||
title="Stream '<?= $show_data['title'] ?>' on <?= $link['meta']['name'] ?>"
|
||||
title="Stream '<?= $data['title'] ?>' on <?= $link['meta']['name'] ?>"
|
||||
>
|
||||
<?= $helper->picture("images/{$link['meta']['image']}", 'svg', [
|
||||
'class' => 'streaming-logo',
|
||||
@ -92,13 +95,13 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif ?>
|
||||
<?php if ( ! empty($show_data['trailer_id'])): ?>
|
||||
<?php if ( ! empty($data['trailer_id'])): ?>
|
||||
<div class="responsive-iframe">
|
||||
<h4>Trailer</h4>
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://www.youtube.com/embed/<?= $show_data['trailer_id'] ?>"
|
||||
src="https://www.youtube.com/embed/<?= $data['trailer_id'] ?>"
|
||||
frameborder="0"
|
||||
allow="autoplay; encrypted-media"
|
||||
allowfullscreen
|
||||
@ -108,13 +111,13 @@
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<?php if (count($characters) > 0): ?>
|
||||
<?php if (count($data['characters']) > 0): ?>
|
||||
<section>
|
||||
<h2>Characters</h2>
|
||||
|
||||
<div class="tabs">
|
||||
<?php $i = 0 ?>
|
||||
<?php foreach ($characters as $role => $list): ?>
|
||||
<?php foreach ($data['characters'] as $role => $list): ?>
|
||||
<input
|
||||
type="radio" name="character-types"
|
||||
id="character-types-<?= $i ?>" <?= ($i === 0) ? 'checked' : '' ?> />
|
||||
@ -140,14 +143,13 @@
|
||||
</section>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (count($staff) > 0): ?>
|
||||
<?php //dump($staff); ?>
|
||||
<?php if (count($data['staff']) > 0): ?>
|
||||
<section>
|
||||
<h2>Staff</h2>
|
||||
|
||||
<div class="vertical-tabs">
|
||||
<?php $i = 0; ?>
|
||||
<?php foreach ($staff as $role => $people): ?>
|
||||
<?php foreach ($data['staff'] as $role => $people): ?>
|
||||
<div class="tab">
|
||||
<input type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />
|
||||
<label for="staff-role<?= $i ?>"><?= $role ?></label>
|
||||
|
@ -7,10 +7,10 @@ use Aviat\AnimeClient\API\Kitsu;
|
||||
<main class="details fixed">
|
||||
<section class="flex flex-no-wrap">
|
||||
<div>
|
||||
<?= $helper->picture("images/characters/{$data[0]['id']}-original.webp") ?>
|
||||
<?php if ( ! empty($data[0]['attributes']['otherNames'])): ?>
|
||||
<?= $helper->picture("images/characters/{$data['id']}-original.webp") ?>
|
||||
<?php if ( ! empty($data['otherNames'])): ?>
|
||||
<h3>Nicknames / Other names</h3>
|
||||
<?php foreach ($data[0]['attributes']['otherNames'] as $name): ?>
|
||||
<?php foreach ($data['otherNames'] as $name): ?>
|
||||
<h4><?= $name ?></h4>
|
||||
<?php endforeach ?>
|
||||
<?php endif ?>
|
||||
@ -23,19 +23,19 @@ use Aviat\AnimeClient\API\Kitsu;
|
||||
|
||||
<hr />
|
||||
|
||||
<p class="description"><?= $data[0]['attributes']['description'] ?></p>
|
||||
<p class="description"><?= $data['description'] ?></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if (array_key_exists('anime', $data['included']) || array_key_exists('manga', $data['included'])): ?>
|
||||
<?php if ( ! (empty($data['media']['anime']) || empty($data['media']['manga']))): ?>
|
||||
<h3>Media</h3>
|
||||
<div class="tabs">
|
||||
<?php if (array_key_exists('anime', $data['included'])): ?>
|
||||
<?php if ( ! empty($data['media']['anime'])): ?>
|
||||
<input checked="checked" type="radio" id="media-anime" name="media-tabs" />
|
||||
<label for="media-anime">Anime</label>
|
||||
|
||||
<section class="media-wrap content">
|
||||
<?php foreach ($data['included']['anime'] as $id => $anime): ?>
|
||||
<?php foreach ($data['media']['anime'] as $id => $anime): ?>
|
||||
<article class="media">
|
||||
<?php
|
||||
$link = $url->generate('anime.details', ['id' => $anime['attributes']['slug']]);
|
||||
@ -58,12 +58,12 @@ use Aviat\AnimeClient\API\Kitsu;
|
||||
</section>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (array_key_exists('manga', $data['included'])): ?>
|
||||
<?php if ( ! empty($data['media']['manga'])): ?>
|
||||
<input type="radio" id="media-manga" name="media-tabs" />
|
||||
<label for="media-manga">Manga</label>
|
||||
|
||||
<section class="media-wrap content">
|
||||
<?php foreach ($data['included']['manga'] as $id => $manga): ?>
|
||||
<?php foreach ($data['media']['manga'] as $id => $manga): ?>
|
||||
<article class="media">
|
||||
<?php
|
||||
$link = $url->generate('manga.details', ['id' => $manga['attributes']['slug']]);
|
||||
@ -89,14 +89,68 @@ use Aviat\AnimeClient\API\Kitsu;
|
||||
<?php endif ?>
|
||||
|
||||
<section>
|
||||
<?php if ($castCount > 0): ?>
|
||||
<?php if (count($data['castings']) > 0): ?>
|
||||
<h3>Castings</h3>
|
||||
<?php
|
||||
$vas = $castings['Voice Actor'];
|
||||
unset($castings['Voice Actor']);
|
||||
ksort($vas)
|
||||
$vas = $data['castings']['Voice Actor'];
|
||||
unset($data['castings']['Voice Actor']);
|
||||
ksort($vas)
|
||||
?>
|
||||
|
||||
<?php foreach ($data['castings'] as $role => $entries): ?>
|
||||
<h4><?= $role ?></h4>
|
||||
<?php foreach ($entries as $language => $casting): ?>
|
||||
<h5><?= $language ?></h5>
|
||||
<table class="min-table">
|
||||
<tr>
|
||||
<th>Cast Member</th>
|
||||
<th>Series</th>
|
||||
</tr>
|
||||
<?php foreach ($casting as $cid => $c): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<article class="character">
|
||||
<?php
|
||||
$link = $url->generate('person', ['id' => $c['person']['id']]);
|
||||
?>
|
||||
<a href="<?= $link ?>">
|
||||
<?= $helper->picture(getLocalImg($c['person']['image'], TRUE)) ?>
|
||||
<div class="name">
|
||||
<?= $c['person']['name'] ?>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
</td>
|
||||
<td>
|
||||
<section class="align-left media-wrap">
|
||||
<?php foreach ($c['series'] as $series): ?>
|
||||
<article class="media">
|
||||
<?php
|
||||
$link = $url->generate('anime.details', ['id' => $series['attributes']['slug']]);
|
||||
$titles = Kitsu::filterTitles($series['attributes']);
|
||||
?>
|
||||
<a href="<?= $link ?>">
|
||||
<?= $helper->picture(getLocalImg($series['attributes']['posterImage']['small'], TRUE)) ?>
|
||||
</a>
|
||||
<div class="name">
|
||||
<a href="<?= $link ?>">
|
||||
<?= array_shift($titles) ?>
|
||||
<?php foreach ($titles as $title): ?>
|
||||
<br />
|
||||
<small><?= $title ?></small>
|
||||
<?php endforeach ?>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach ?>
|
||||
</section>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<?php endforeach ?>
|
||||
<?php endforeach ?>
|
||||
|
||||
<?php if ( ! empty($vas)): ?>
|
||||
<h4>Voice Actors</h4>
|
||||
|
||||
@ -161,61 +215,6 @@ use Aviat\AnimeClient\API\Kitsu;
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
|
||||
<?php foreach ($castings as $role => $entries): ?>
|
||||
<h4><?= $role ?></h4>
|
||||
<?php foreach ($entries as $language => $casting): ?>
|
||||
<h5><?= $language ?></h5>
|
||||
<table class="min-table">
|
||||
<tr>
|
||||
<th>Cast Member</th>
|
||||
<th>Series</th>
|
||||
</tr>
|
||||
<?php foreach ($casting as $cid => $c): ?>
|
||||
<tr>
|
||||
<td style="width:229px">
|
||||
<article class="character">
|
||||
<?php
|
||||
$link = $url->generate('person', ['id' => $c['person']['id']]);
|
||||
?>
|
||||
<a href="<?= $link ?>">
|
||||
<?= $helper->picture(getLocalImg($c['person']['image'], TRUE)) ?>
|
||||
<div class="name">
|
||||
<?= $c['person']['name'] ?>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
</td>
|
||||
<td>
|
||||
<section class="align-left media-wrap">
|
||||
<?php foreach ($c['series'] as $series): ?>
|
||||
<article class="media">
|
||||
<?php
|
||||
$link = $url->generate('anime.details', ['id' => $series['attributes']['slug']]);
|
||||
$titles = Kitsu::filterTitles($series['attributes']);
|
||||
?>
|
||||
<a href="<?= $link ?>">
|
||||
<?= $helper->picture(getLocalImg($series['attributes']['posterImage']['small'], TRUE)) ?>
|
||||
</a>
|
||||
<div class="name">
|
||||
<a href="<?= $link ?>">
|
||||
<?= array_shift($titles) ?>
|
||||
<?php foreach ($titles as $title): ?>
|
||||
<br />
|
||||
<small><?= $title ?></small>
|
||||
<?php endforeach ?>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach ?>
|
||||
</section>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<?php endforeach ?>
|
||||
<?php endforeach ?>
|
||||
<?php endif ?>
|
||||
</section>
|
||||
</main>
|
@ -2,7 +2,7 @@
|
||||
<main>
|
||||
<h2>Edit Anime Collection Item</h2>
|
||||
<form action="<?= $action_url ?>" method="post">
|
||||
<table class="invisible form" style="border:0">
|
||||
<table class="invisible form">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="6" class="align-center">
|
||||
@ -28,7 +28,7 @@
|
||||
<td class="align-left">
|
||||
<select name="media_id" id="media_id">
|
||||
<?php foreach($media_items as $id => $name): ?>
|
||||
<option <?= $item['media_id'] == $id ? 'selected="selected"' : '' ?> value="<?= $id ?>"><?= $name ?></option>
|
||||
<option <?= $item['media_id'] === $id ? 'selected="selected"' : '' ?> value="<?= $id ?>"><?= $name ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
</td>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
|
||||
<?= $item['title'] ?>
|
||||
</a>
|
||||
<?= (!empty($item['alternate_title'])) ? " <br /><small> " . $item['alternate_title'] . "</small>" : "" ?>
|
||||
<?= ! empty($item['alternate_title']) ? ' <br /><small> ' . $item['alternate_title'] . '</small>' : '' ?>
|
||||
</td>
|
||||
<td><?= $item['episode_count'] ?></td>
|
||||
<td><?= $item['episode_length'] ?></td>
|
||||
|
@ -7,6 +7,7 @@
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self'" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=1" />
|
||||
<link rel="stylesheet" href="<?= $urlGenerator->assetUrl('css/app.min.css') ?>" />
|
||||
<link rel="<?= $config->get('dark_theme') ? '' : 'alternate ' ?>stylesheet" title="Dark Theme" href="<?= $urlGenerator->assetUrl('css/dark.min.css') ?>" />
|
||||
<link rel="icon" href="<?= $urlGenerator->assetUrl('images/icons/favicon.ico') ?>" />
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-57x57.png') ?>">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-60x60.png') ?>">
|
||||
@ -38,4 +39,5 @@
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
</header>
|
@ -25,6 +25,8 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
</aside>
|
||||
<article class="text">
|
||||
<h2 class="toph"><a rel="external" href="<?= $data['url'] ?>"><?= $data['title'] ?></a></h2>
|
||||
@ -37,11 +39,12 @@
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<?php if (count($characters) > 0): ?>
|
||||
<?php if (count($data['characters']) > 0): ?>
|
||||
<h2>Characters</h2>
|
||||
|
||||
<div class="tabs">
|
||||
<?php $i = 0 ?>
|
||||
<?php foreach ($characters as $role => $list): ?>
|
||||
<?php foreach ($data['characters'] as $role => $list): ?>
|
||||
<input
|
||||
type="radio" name="character-role-tabs"
|
||||
id="character-tabs<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />
|
||||
@ -66,12 +69,12 @@
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (count($staff) > 0): ?>
|
||||
<?php if (count($data['staff']) > 0): ?>
|
||||
<h2>Staff</h2>
|
||||
|
||||
<div class="vertical-tabs">
|
||||
<?php $i = 0 ?>
|
||||
<?php foreach ($staff as $role => $people): ?>
|
||||
<?php foreach ($data['staff'] as $role => $people): ?>
|
||||
<div class="tab">
|
||||
<input
|
||||
type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />
|
||||
|
@ -5,7 +5,7 @@ use Aviat\AnimeClient\API\Kitsu;
|
||||
<h3>Voice Acting Roles</h3>
|
||||
<div class="tabs">
|
||||
<?php $i = 0; ?>
|
||||
<?php foreach($characters as $role => $characterList): ?>
|
||||
<?php foreach($data['characters'] as $role => $characterList): ?>
|
||||
<input <?= $i === 0 ? 'checked="checked"' : '' ?> type="radio" name="character-type-tabs" id="character-type-<?= $i ?>" />
|
||||
<label for="character-type-<?= $i ?>"><h5><?= ucfirst($role) ?></h5></label>
|
||||
<section class="content">
|
||||
@ -16,7 +16,7 @@ use Aviat\AnimeClient\API\Kitsu;
|
||||
</tr>
|
||||
<?php foreach ($characterList as $cid => $character): ?>
|
||||
<tr>
|
||||
<td style="width:229px">
|
||||
<td>
|
||||
<article class="character">
|
||||
<?php
|
||||
$link = $url->generate('character', ['slug' => $character['character']['slug']]);
|
||||
|
@ -9,16 +9,16 @@ use Aviat\AnimeClient\API\Kitsu;
|
||||
<?= $helper->picture("images/people/{$data['id']}-original.webp", 'jpg', ['class' => 'cover' ]) ?>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="toph"><?= $data['attributes']['name'] ?></h2>
|
||||
<h2 class="toph"><?= $data['name'] ?></h2>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if ( ! empty($staff)): ?>
|
||||
<?php if ( ! empty($data['staff'])): ?>
|
||||
<section>
|
||||
<h3>Castings</h3>
|
||||
<div class="vertical-tabs">
|
||||
<?php $i = 0 ?>
|
||||
<?php foreach ($staff as $role => $entries): ?>
|
||||
<?php foreach ($data['staff'] as $role => $entries): ?>
|
||||
<div class="tab">
|
||||
<input
|
||||
type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />
|
||||
@ -59,7 +59,7 @@ use Aviat\AnimeClient\API\Kitsu;
|
||||
</section>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ( ! (empty($characters['main']) || empty($characters['supporting']))): ?>
|
||||
<?php if ( ! (empty($data['characters']['main']) || empty($data['characters']['supporting']))): ?>
|
||||
<section>
|
||||
<?php include 'character-mapping.php' ?>
|
||||
</section>
|
||||
|
@ -1,45 +1,39 @@
|
||||
<?php
|
||||
use function Aviat\AnimeClient\getLocalImg;
|
||||
use Aviat\AnimeClient\API\Kitsu;
|
||||
?>
|
||||
<main class="user-page details">
|
||||
<h2 class="toph">
|
||||
<?= $helper->a(
|
||||
"https://kitsu.io/users/{$attributes['slug']}",
|
||||
$attributes['name'], [
|
||||
"https://kitsu.io/users/{$data['slug']}",
|
||||
$data['name'], [
|
||||
'title' => 'View profile on Kitsu'
|
||||
])
|
||||
?>
|
||||
</h2>
|
||||
|
||||
<p><?= $escape->html($attributes['about']) ?></p>
|
||||
<p><?= $escape->html($data['about']) ?></p>
|
||||
|
||||
<section class="flex flex-no-wrap">
|
||||
<aside class="info">
|
||||
<center>
|
||||
<?php
|
||||
$avatar = $urlGenerator->assetUrl(
|
||||
getLocalImg($attributes['avatar']['original'], FALSE)
|
||||
);
|
||||
echo $helper->img($avatar, ['alt' => '']);
|
||||
?>
|
||||
<?= $helper->img($urlGenerator->assetUrl($data['avatar']), ['alt' => '']); ?>
|
||||
</center>
|
||||
<br />
|
||||
<table class="media-details">
|
||||
<tr>
|
||||
<td>Location</td>
|
||||
<td><?= $attributes['location'] ?></td>
|
||||
<td><?= $data['location'] ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Website</td>
|
||||
<td><?= $helper->a($attributes['website'], $attributes['website']) ?></td>
|
||||
<td><?= $helper->a($data['website'], $data['website']) ?></td>
|
||||
</tr>
|
||||
<?php if (array_key_exists('waifu', $relationships)): ?>
|
||||
<?php if ( ! empty($data['waifu'])): ?>
|
||||
<tr>
|
||||
<td><?= $escape->html($attributes['waifuOrHusbando']) ?></td>
|
||||
<td><?= $escape->html($data['waifu']['label']) ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$character = $relationships['waifu']['attributes'];
|
||||
$character = $data['waifu']['character'];
|
||||
echo $helper->a(
|
||||
$url->generate('character', ['slug' => $character['slug']]),
|
||||
$character['canonicalName']
|
||||
@ -52,42 +46,24 @@ use Aviat\AnimeClient\API\Kitsu;
|
||||
|
||||
<h3>User Stats</h3><br />
|
||||
<table class="media-details">
|
||||
<?php foreach($data['stats'] as $label => $stat): ?>
|
||||
<tr>
|
||||
<td>Time spent watching anime:</td>
|
||||
<td><?= $timeOnAnime ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td># of Anime episodes watched</td>
|
||||
<td><?= number_format($stats['anime-amount-consumed']['units']) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td># of Manga chapters read</td>
|
||||
<td><?= number_format($stats['manga-amount-consumed']['units']) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td># of Posts</td>
|
||||
<td><?= number_format($attributes['postsCount']) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td># of Comments</td>
|
||||
<td><?= number_format($attributes['commentsCount']) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td># of Media Rated</td>
|
||||
<td><?= number_format($attributes['ratingsCount']) ?></td>
|
||||
<td><?= $label ?></td>
|
||||
<td><?= $stat ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
</aside>
|
||||
<article>
|
||||
<?php if ( ! empty($favorites)): ?>
|
||||
<?php if ( ! empty($data['favorites'])): ?>
|
||||
<h3>Favorites</h3>
|
||||
<div class="tabs">
|
||||
<?php $i = 0 ?>
|
||||
<?php if ( ! empty($favorites['characters'])): ?>
|
||||
<?php if ( ! empty($data['favorites']['characters'])): ?>
|
||||
<input type="radio" name="user-favorites" id="user-fav-chars" <?= $i === 0 ? 'checked' : '' ?> />
|
||||
<label for="user-fav-chars">Characters</label>
|
||||
<section class="content full-width media-wrap">
|
||||
<?php foreach($favorites['characters'] as $id => $char): ?>
|
||||
<?php foreach($data['favorites']['characters'] as $id => $char): ?>
|
||||
<?php if ( ! empty($char['image']['original'])): ?>
|
||||
<article class="character">
|
||||
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
|
||||
@ -101,11 +77,11 @@ use Aviat\AnimeClient\API\Kitsu;
|
||||
</section>
|
||||
<?php $i++; ?>
|
||||
<?php endif ?>
|
||||
<?php if ( ! empty($favorites['anime'])): ?>
|
||||
<?php if ( ! empty($data['favorites']['anime'])): ?>
|
||||
<input type="radio" name="user-favorites" id="user-fav-anime" <?= $i === 0 ? 'checked' : '' ?> />
|
||||
<label for="user-fav-anime">Anime</label>
|
||||
<section class="content full-width media-wrap">
|
||||
<?php foreach($favorites['anime'] as $anime): ?>
|
||||
<?php foreach($data['favorites']['anime'] as $anime): ?>
|
||||
<article class="media">
|
||||
<?php
|
||||
$link = $url->generate('anime.details', ['id' => $anime['slug']]);
|
||||
@ -127,11 +103,11 @@ use Aviat\AnimeClient\API\Kitsu;
|
||||
</section>
|
||||
<?php $i++; ?>
|
||||
<?php endif ?>
|
||||
<?php if ( ! empty($favorites['manga'])): ?>
|
||||
<?php if ( ! empty($data['favorites']['manga'])): ?>
|
||||
<input type="radio" name="user-favorites" id="user-fav-manga" <?= $i === 0 ? 'checked' : '' ?> />
|
||||
<label for="user-fav-manga">Manga</label>
|
||||
<section class="content full-width media-wrap">
|
||||
<?php foreach($favorites['manga'] as $manga): ?>
|
||||
<?php foreach($data['favorites']['manga'] as $manga): ?>
|
||||
<article class="media">
|
||||
<?php
|
||||
$link = $url->generate('manga.details', ['id' => $manga['slug']]);
|
||||
|
@ -22,7 +22,7 @@
|
||||
"aura/html": "^2.0",
|
||||
"aura/router": "^3.0",
|
||||
"aura/session": "^2.0",
|
||||
"aviat/banker": "^1.0.0",
|
||||
"aviat/banker": "^2.0.0",
|
||||
"aviat/ion": "^2.4.1",
|
||||
"ext-iconv": "*",
|
||||
"ext-json": "*",
|
||||
@ -37,28 +37,30 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"consolidation/robo": "~1.0",
|
||||
"filp/whoops": "^2.1",
|
||||
"henrikbjorn/lurker": "^1.1.0",
|
||||
"pdepend/pdepend": "^2.2",
|
||||
"phploc/phploc": "^4.0",
|
||||
"phpmd/phpmd": "^2.4",
|
||||
"phpstan/phpstan": "^0.9.1",
|
||||
"phpunit/phpunit": "^6.0",
|
||||
"phpstan/phpstan": "^0.10.5",
|
||||
"phpunit/phpunit": "^7.4.3",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"robmorgan/phinx": "^0.10.6",
|
||||
"sebastian/phpcpd": "^3.0",
|
||||
"sebastian/phpcpd": "^4.1.0",
|
||||
"spatie/phpunit-snapshot-assertions": "^1.2.0",
|
||||
"squizlabs/php_codesniffer": "^3.2.2",
|
||||
"symfony/var-dumper": "^4.0.1",
|
||||
"theseer/phpdox": "^0.11.0",
|
||||
"filp/whoops": "^2.1"
|
||||
"theseer/phpdox": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vendor/bin/robo build",
|
||||
"build:css": "cd public && npm run build && cd ..",
|
||||
"build:css": "cd public && npm run build:css && cd ..",
|
||||
"build:js": "cd public && npm run build:js && cd ..",
|
||||
"clean": "vendor/bin/robo clean",
|
||||
"coverage": "phpdbg -qrr -- vendor/bin/phpunit -c build",
|
||||
"phpstan": "phpstan analyse -l 4 -c phpstan.neon src tests ./console index.php",
|
||||
"watch:css": "cd public && npm run watch",
|
||||
"watch:css": "cd public && npm run watch:css",
|
||||
"watch:js": "cd public && npm run watch:js",
|
||||
"test": "vendor/bin/phpunit"
|
||||
},
|
||||
"scripts-descriptions": {
|
||||
|
@ -47,12 +47,12 @@ $CONF_DIR = _dir($APP_DIR, 'config');
|
||||
// -----------------------------------------------------------------------------
|
||||
// Dependency Injection setup
|
||||
// -----------------------------------------------------------------------------
|
||||
$baseConfig = require $APPCONF_DIR . '/base_config.php';
|
||||
$di = require $APP_DIR . '/bootstrap.php';
|
||||
$baseConfig = require "{$APPCONF_DIR}/base_config.php";
|
||||
$di = require "{$APP_DIR}/bootstrap.php";
|
||||
|
||||
$config = loadToml($CONF_DIR);
|
||||
|
||||
$overrideFile = $CONF_DIR . '/admin-override.toml';
|
||||
$overrideFile = "{$CONF_DIR}/admin-override.toml";
|
||||
$overrideConfig = file_exists($overrideFile)
|
||||
? loadTomlFile($overrideFile)
|
||||
: [];
|
||||
|
2
public/css/app.min.css
vendored
2
public/css/app.min.css
vendored
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
undefined
|
135
public/css/dark-override.css
Normal file
135
public/css/dark-override.css
Normal file
@ -0,0 +1,135 @@
|
||||
a {
|
||||
color: rgb(25, 120, 226);
|
||||
text-shadow: var(--link-shadow);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #9e34fd;
|
||||
}
|
||||
|
||||
body,
|
||||
legend,
|
||||
nav ul li a {
|
||||
background: #333;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
nav a:hover, nav li.selected a {
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
header button {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
table {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
td, th {
|
||||
border-color: #111;
|
||||
}
|
||||
|
||||
thead td,
|
||||
thead th {
|
||||
background: #333;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
tbody > tr:nth-child(2n) {
|
||||
background: #555;
|
||||
color: #eee;
|
||||
}
|
||||
tbody > tr:nth-child(2n+1) {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
footer, legend, hr {
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
small {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.message, .static-message {
|
||||
text-shadow: var(--white-link-shadow);
|
||||
}
|
||||
|
||||
.message.success, .static-message.success {
|
||||
background: #1f8454;
|
||||
border-color: #70dda9;
|
||||
}
|
||||
.message.error, .static-message.error {
|
||||
border-color:#f3e6e6;
|
||||
background: #924949;
|
||||
}
|
||||
.message.info, .static-message.info {
|
||||
border-color: #FFFFCC;
|
||||
background: #bfbe3a;
|
||||
}
|
||||
|
||||
.invisible tr,
|
||||
.invisible td,
|
||||
.invisible th,
|
||||
.invisible tbody > tr:nth-child(2n),
|
||||
.invisible tbody > tr:nth-child(2n+1) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#main-nav {
|
||||
border-bottom: .1rem solid #ddd;
|
||||
}
|
||||
|
||||
.tabs,
|
||||
.vertical-tabs{
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.tabs > label,
|
||||
.vertical-tabs .tab label {
|
||||
background: #222;
|
||||
border: 0;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.vertical-tabs .tab label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabs > label:hover,
|
||||
.vertical-tabs .tab > label:hover {
|
||||
background: #888;
|
||||
}
|
||||
|
||||
.tabs > label:active,
|
||||
.vertical-tabs .tab > label:active {
|
||||
background: #999;
|
||||
}
|
||||
|
||||
.tabs > [type="radio"]:checked + label,
|
||||
.tabs > [type="radio"]:checked + label + .content,
|
||||
.vertical-tabs [type="radio"]:checked + label,
|
||||
.vertical-tabs [type="radio"]:checked ~ .content {
|
||||
/* border-color: #333; */
|
||||
border: 0;
|
||||
background: #666;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.vertical-tabs {
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
.vertical-tabs .tab {
|
||||
background: #666;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
|
||||
|
1
public/css/dark.min.css
vendored
Normal file
1
public/css/dark.min.css
vendored
Normal file
@ -0,0 +1 @@
|
||||
a{color:#1978e2;text-shadow:var(--link-shadow)}a:hover{color:#9e34fd}body,legend,nav ul li a{background:#333;color:#eee}nav a:hover,nav li.selected a{border-color:#fff}header button{background:transparent}table{-webkit-box-shadow:none;box-shadow:none}td,th{border-color:#111}thead td,thead th{background:#333;color:#eee}tbody>tr:nth-child(2n){background:#555;color:#eee}tbody>tr:nth-child(odd){background:#333}footer,hr,legend{border-color:#ddd}small{color:#fff}input,select,textarea{color:#111}.message,.static-message{text-shadow:var(--white-link-shadow)}.message.success,.static-message.success{background:#1f8454;border-color:#70dda9}.message.error,.static-message.error{border-color:#f3e6e6;background:#924949}.message.info,.static-message.info{border-color:#ffc;background:#bfbe3a}.invisible tbody>tr:nth-child(2n),.invisible tbody>tr:nth-child(odd),.invisible td,.invisible th,.invisible tr{background:transparent}#main-nav{border-bottom:.1rem solid #ddd}.tabs,.vertical-tabs{background:#333}.tabs>label,.vertical-tabs .tab label{background:#222;border:0;color:#eee}.vertical-tabs .tab label{width:100%}.tabs>label:hover,.vertical-tabs .tab>label:hover{background:#888}.tabs>label:active,.vertical-tabs .tab>label:active{background:#999}.tabs>[type=radio]:checked+label,.tabs>[type=radio]:checked+label+.content,.vertical-tabs [type=radio]:checked+label,.vertical-tabs [type=radio]:checked~.content{border:0;background:#666;color:#eee}.vertical-tabs{background:#222;border:1px solid #444}.vertical-tabs .tab{background:#666;border-bottom:1px solid #444}
|
@ -359,7 +359,7 @@ td .media-wrap-flex {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
width: 220px;
|
||||
height: 311px;
|
||||
height: 312px;
|
||||
margin: var(--normal-padding);
|
||||
z-index: 0;
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
@ -423,7 +423,7 @@ picture.cover {
|
||||
background: var(--title-overlay); */
|
||||
content: '';
|
||||
display: block;
|
||||
height: 311px;
|
||||
height: 312px;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -39,23 +39,26 @@
|
||||
table .align-right,
|
||||
table.align-center {
|
||||
border: 0;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
/* display: block; */
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table tbody {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table td {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
table tbody,
|
||||
table.media-details {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table.media-details td {
|
||||
display: block;
|
||||
text-align: left !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table thead {
|
||||
|
27
public/js/scripts-authed.min.js
vendored
27
public/js/scripts-authed.min.js
vendored
@ -7,17 +7,18 @@ sel.addEventListener(event,listener,false)}function delegateEvent(sel,target,eve
|
||||
"GET")url+=url.match(/\?/)?ajaxSerialize(config.data):"?"+ajaxSerialize(config.data);request.open(method,url);request.onreadystatechange=function(){if(request.readyState===4){var responseText="";if(request.responseType==="json")responseText=JSON.parse(request.responseText);else responseText=request.responseText;if(request.status>299)config.error.call(null,request.status,responseText,request.response);else config.success.call(null,responseText,request.status)}};if(config.dataType==="json"){config.data=
|
||||
JSON.stringify(config.data);config.mimeType="application/json"}else config.data=ajaxSerialize(config.data);request.setRequestHeader("Content-Type",config.mimeType);switch(method){case "GET":request.send(null);break;default:request.send(config.data);break}};AnimeClient.get=function(url,data,callback){callback=callback===undefined?null:callback;if(callback===null){callback=data;data={}}return AnimeClient.ajax(url,{data:data,success:callback})};AnimeClient.on("header","click",".message",function(e){AnimeClient.hide(e.target)});
|
||||
AnimeClient.on("form.js-delete","submit",function(event){var proceed=confirm("Are you ABSOLUTELY SURE you want to delete this item?");if(proceed===false){event.preventDefault();event.stopPropagation()}});AnimeClient.on(".js-clear-cache","click",function(){AnimeClient.get("/cache_purge",function(){AnimeClient.showMessage("success","Successfully purged api cache")})});AnimeClient.on(".vertical-tabs input","change",function(event){var el=event.currentTarget.parentElement;var rect=el.getBoundingClientRect();
|
||||
var top=rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})});AnimeClient.on("main","change",".big-check",function(e){var id=e.target.id;document.getElementById("mal_"+id).checked=true});function renderAnimeSearchResults(data){var results=[];data.forEach(function(x){var item=x.attributes;var titles=item.titles.reduce(function(prev,current){return prev+(current+"<br />")},[]);results.push('\n\t\t\t<article class="media search">\n\t\t\t\t<div class="name">\n\t\t\t\t\t<input type="radio" class="mal-check" id="mal_'+
|
||||
item.slug+'" name="mal_id" value="'+x.mal_id+'" />\n\t\t\t\t\t<input type="radio" class="big-check" id="'+item.slug+'" name="id" value="'+x.id+'" />\n\t\t\t\t\t<label for="'+item.slug+'">\n\t\t\t\t\t\t<picture width="220">\n\t\t\t\t\t\t\t<source srcset="/public/images/anime/'+x.id+'.webp" type="image/webp" />\n\t\t\t\t\t\t\t<source srcset="/public/images/anime/'+x.id+'.jpg" type="image/jpeg" />\n\t\t\t\t\t\t\t<img src="/public/images/anime/'+x.id+'.jpg" alt="" width="220" />\n\t\t\t\t\t\t</picture>\n\t\t\t\t\t\t\n\t\t\t\t\t\t<span class="name">\n\t\t\t\t\t\t\t'+
|
||||
item.canonicalTitle+"<br />\n\t\t\t\t\t\t\t<small>"+titles+'</small>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t\t<div class="table">\n\t\t\t\t\t<div class="row">\n\t\t\t\t\t\t<span class="edit">\n\t\t\t\t\t\t\t<a class="bracketed" href="/anime/details/'+item.slug+'">Info Page</a>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</article>\n\t\t')});return results.join("")}function renderMangaSearchResults(data){var results=[];data.forEach(function(x){var item=x.attributes;
|
||||
var titles=item.titles.reduce(function(prev,current){return prev+(current+"<br />")},[]);results.push('\n\t\t\t<article class="media search">\n\t\t\t\t<div class="name">\n\t\t\t\t\t<input type="radio" id="mal_'+item.slug+'" name="mal_id" value="'+x.mal_id+'" />\n\t\t\t\t\t<input type="radio" class="big-check" id="'+item.slug+'" name="id" value="'+x.id+'" />\n\t\t\t\t\t<label for="'+item.slug+'">\n\t\t\t\t\t\t<picture width="220">\n\t\t\t\t\t\t\t<source srcset="/public/images/manga/'+x.id+'.webp" type="image/webp" />\n\t\t\t\t\t\t\t<source srcset="/public/images/manga/'+
|
||||
x.id+'.jpg" type="image/jpeg" />\n\t\t\t\t\t\t\t<img src="/public/images/manga/'+x.id+'.jpg" alt="" width="220" />\n\t\t\t\t\t\t</picture>\n\t\t\t\t\t\t<span class="name">\n\t\t\t\t\t\t\t'+item.canonicalTitle+"<br />\n\t\t\t\t\t\t\t<small>"+titles+'</small>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t\t<div class="table">\n\t\t\t\t\t<div class="row">\n\t\t\t\t\t\t<span class="edit">\n\t\t\t\t\t\t\t<a class="bracketed" href="/manga/details/'+item.slug+'">Info Page</a>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</article>\n\t\t')});
|
||||
return results.join("")}var search=function(query){AnimeClient.$(".cssload-loader")[0].removeAttribute("hidden");AnimeClient.get(AnimeClient.url("/anime-collection/search"),{query:query},function(searchResults,status){searchResults=JSON.parse(searchResults);AnimeClient.$(".cssload-loader")[0].setAttribute("hidden","hidden");AnimeClient.$("#series-list")[0].innerHTML=renderAnimeSearchResults(searchResults.data)})};if(AnimeClient.hasElement(".anime #search"))AnimeClient.on("#search","keyup",AnimeClient.throttle(250,
|
||||
function(e){var query=encodeURIComponent(e.target.value);if(query==="")return;search(query)}));AnimeClient.on("body.anime.list","click",".plus-one",function(e){var parentSel=AnimeClient.closestParent(e.target,"article");var watchedCount=parseInt(AnimeClient.$(".completed_number",parentSel)[0].textContent,10)||0;var totalCount=parseInt(AnimeClient.$(".total_number",parentSel)[0].textContent,10);var title=AnimeClient.$(".name a",parentSel)[0].textContent;var data={id:parentSel.dataset.kitsuId,mal_id:parentSel.dataset.malId,
|
||||
data:{progress:watchedCount+1}};if(isNaN(watchedCount)||watchedCount===0)data.data.status="current";if(!isNaN(watchedCount)&&watchedCount+1===totalCount)data.data.status="completed";AnimeClient.show(AnimeClient.$("#loading-shadow")[0]);AnimeClient.ajax(AnimeClient.url("/anime/increment"),{data:data,dataType:"json",type:"POST",success:function(res){var resData=JSON.parse(res);if(resData.errors){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+
|
||||
title+". ");AnimeClient.scrollToTop();return}if(resData.data.attributes.status==="completed")AnimeClient.hide(parentSel);AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("success","Successfully updated "+title);AnimeClient.$(".completed_number",parentSel)[0].textContent=++watchedCount;AnimeClient.scrollToTop()},error:function(){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+title+". ");AnimeClient.scrollToTop()}})});
|
||||
var search$1=function(query){AnimeClient.$(".cssload-loader")[0].removeAttribute("hidden");AnimeClient.get(AnimeClient.url("/manga/search"),{query:query},function(searchResults,status){searchResults=JSON.parse(searchResults);AnimeClient.$(".cssload-loader")[0].setAttribute("hidden","hidden");AnimeClient.$("#series-list")[0].innerHTML=renderMangaSearchResults(searchResults.data)})};if(AnimeClient.hasElement(".manga #search"))AnimeClient.on("#search","keyup",AnimeClient.throttle(250,function(e){var query=
|
||||
encodeURIComponent(e.target.value);if(query==="")return;search$1(query)}));AnimeClient.on(".manga.list","click",".edit-buttons button",function(e){var thisSel=e.target;var parentSel=AnimeClient.closestParent(e.target,"article");var type=thisSel.classList.contains("plus-one-chapter")?"chapter":"volume";var completed=parseInt(AnimeClient.$("."+type+"s_read",parentSel)[0].textContent,10)||0;var total=parseInt(AnimeClient.$("."+type+"_count",parentSel)[0].textContent,10);var mangaName=AnimeClient.$(".name",
|
||||
parentSel)[0].textContent;if(isNaN(completed))completed=0;var data={id:parentSel.dataset.kitsuId,mal_id:parentSel.dataset.malId,data:{progress:completed}};if(isNaN(completed)||completed===0)data.data.status="current";if(!isNaN(completed)&&completed+1===total)data.data.status="completed";data.data.progress=++completed;AnimeClient.show(AnimeClient.$("#loading-shadow")[0]);AnimeClient.ajax(AnimeClient.url("/manga/increment"),{data:data,dataType:"json",type:"POST",mimeType:"application/json",success:function(){if(data.data.status===
|
||||
"completed")AnimeClient.hide(parentSel);AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.$("."+type+"s_read",parentSel)[0].textContent=completed;AnimeClient.showMessage("success","Successfully updated "+mangaName);AnimeClient.scrollToTop()},error:function(){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+mangaName);AnimeClient.scrollToTop()}})})})();
|
||||
var top=rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})});if("serviceWorker"in navigator)navigator.serviceWorker.register("/sw.js").then(function(reg){console.log("Service worker registered",reg.scope)})["catch"](function(error){console.error("Failed to register service worker",error)});AnimeClient.on("main","change",".big-check",function(e){var id=e.target.id;document.getElementById("mal_"+id).checked=true});function renderAnimeSearchResults(data){var results=[];data.forEach(function(x){var item=
|
||||
x.attributes;var titles=item.titles.reduce(function(prev,current){return prev+(current+"<br />")},[]);results.push('\n\t\t\t<article class="media search">\n\t\t\t\t<div class="name">\n\t\t\t\t\t<input type="radio" class="mal-check" id="mal_'+item.slug+'" name="mal_id" value="'+x.mal_id+'" />\n\t\t\t\t\t<input type="radio" class="big-check" id="'+item.slug+'" name="id" value="'+x.id+'" />\n\t\t\t\t\t<label for="'+item.slug+'">\n\t\t\t\t\t\t<picture width="220">\n\t\t\t\t\t\t\t<source srcset="/public/images/anime/'+
|
||||
x.id+'.webp" type="image/webp" />\n\t\t\t\t\t\t\t<source srcset="/public/images/anime/'+x.id+'.jpg" type="image/jpeg" />\n\t\t\t\t\t\t\t<img src="/public/images/anime/'+x.id+'.jpg" alt="" width="220" />\n\t\t\t\t\t\t</picture>\n\t\t\t\t\t\t\n\t\t\t\t\t\t<span class="name">\n\t\t\t\t\t\t\t'+item.canonicalTitle+"<br />\n\t\t\t\t\t\t\t<small>"+titles+'</small>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t\t<div class="table">\n\t\t\t\t\t<div class="row">\n\t\t\t\t\t\t<span class="edit">\n\t\t\t\t\t\t\t<a class="bracketed" href="/anime/details/'+
|
||||
item.slug+'">Info Page</a>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</article>\n\t\t')});return results.join("")}function renderMangaSearchResults(data){var results=[];data.forEach(function(x){var item=x.attributes;var titles=item.titles.reduce(function(prev,current){return prev+(current+"<br />")},[]);results.push('\n\t\t\t<article class="media search">\n\t\t\t\t<div class="name">\n\t\t\t\t\t<input type="radio" id="mal_'+item.slug+'" name="mal_id" value="'+x.mal_id+'" />\n\t\t\t\t\t<input type="radio" class="big-check" id="'+
|
||||
item.slug+'" name="id" value="'+x.id+'" />\n\t\t\t\t\t<label for="'+item.slug+'">\n\t\t\t\t\t\t<picture width="220">\n\t\t\t\t\t\t\t<source srcset="/public/images/manga/'+x.id+'.webp" type="image/webp" />\n\t\t\t\t\t\t\t<source srcset="/public/images/manga/'+x.id+'.jpg" type="image/jpeg" />\n\t\t\t\t\t\t\t<img src="/public/images/manga/'+x.id+'.jpg" alt="" width="220" />\n\t\t\t\t\t\t</picture>\n\t\t\t\t\t\t<span class="name">\n\t\t\t\t\t\t\t'+item.canonicalTitle+"<br />\n\t\t\t\t\t\t\t<small>"+titles+
|
||||
'</small>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t\t<div class="table">\n\t\t\t\t\t<div class="row">\n\t\t\t\t\t\t<span class="edit">\n\t\t\t\t\t\t\t<a class="bracketed" href="/manga/details/'+item.slug+'">Info Page</a>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</article>\n\t\t')});return results.join("")}var search=function(query){AnimeClient.$(".cssload-loader")[0].removeAttribute("hidden");AnimeClient.get(AnimeClient.url("/anime-collection/search"),{query:query},
|
||||
function(searchResults,status){searchResults=JSON.parse(searchResults);AnimeClient.$(".cssload-loader")[0].setAttribute("hidden","hidden");AnimeClient.$("#series-list")[0].innerHTML=renderAnimeSearchResults(searchResults.data)})};if(AnimeClient.hasElement(".anime #search"))AnimeClient.on("#search","keyup",AnimeClient.throttle(250,function(e){var query=encodeURIComponent(e.target.value);if(query==="")return;search(query)}));AnimeClient.on("body.anime.list","click",".plus-one",function(e){var parentSel=
|
||||
AnimeClient.closestParent(e.target,"article");var watchedCount=parseInt(AnimeClient.$(".completed_number",parentSel)[0].textContent,10)||0;var totalCount=parseInt(AnimeClient.$(".total_number",parentSel)[0].textContent,10);var title=AnimeClient.$(".name a",parentSel)[0].textContent;var data={id:parentSel.dataset.kitsuId,mal_id:parentSel.dataset.malId,data:{progress:watchedCount+1}};if(isNaN(watchedCount)||watchedCount===0)data.data.status="current";if(!isNaN(watchedCount)&&watchedCount+1===totalCount)data.data.status=
|
||||
"completed";AnimeClient.show(AnimeClient.$("#loading-shadow")[0]);AnimeClient.ajax(AnimeClient.url("/anime/increment"),{data:data,dataType:"json",type:"POST",success:function(res){var resData=JSON.parse(res);if(resData.errors){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+title+". ");AnimeClient.scrollToTop();return}if(resData.data.attributes.status==="completed")AnimeClient.hide(parentSel);AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);
|
||||
AnimeClient.showMessage("success","Successfully updated "+title);AnimeClient.$(".completed_number",parentSel)[0].textContent=++watchedCount;AnimeClient.scrollToTop()},error:function(){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+title+". ");AnimeClient.scrollToTop()}})});var search$1=function(query){AnimeClient.$(".cssload-loader")[0].removeAttribute("hidden");AnimeClient.get(AnimeClient.url("/manga/search"),{query:query},function(searchResults,
|
||||
status){searchResults=JSON.parse(searchResults);AnimeClient.$(".cssload-loader")[0].setAttribute("hidden","hidden");AnimeClient.$("#series-list")[0].innerHTML=renderMangaSearchResults(searchResults.data)})};if(AnimeClient.hasElement(".manga #search"))AnimeClient.on("#search","keyup",AnimeClient.throttle(250,function(e){var query=encodeURIComponent(e.target.value);if(query==="")return;search$1(query)}));AnimeClient.on(".manga.list","click",".edit-buttons button",function(e){var thisSel=e.target;var parentSel=
|
||||
AnimeClient.closestParent(e.target,"article");var type=thisSel.classList.contains("plus-one-chapter")?"chapter":"volume";var completed=parseInt(AnimeClient.$("."+type+"s_read",parentSel)[0].textContent,10)||0;var total=parseInt(AnimeClient.$("."+type+"_count",parentSel)[0].textContent,10);var mangaName=AnimeClient.$(".name",parentSel)[0].textContent;if(isNaN(completed))completed=0;var data={id:parentSel.dataset.kitsuId,mal_id:parentSel.dataset.malId,data:{progress:completed}};if(isNaN(completed)||
|
||||
completed===0)data.data.status="current";if(!isNaN(completed)&&completed+1===total)data.data.status="completed";data.data.progress=++completed;AnimeClient.show(AnimeClient.$("#loading-shadow")[0]);AnimeClient.ajax(AnimeClient.url("/manga/increment"),{data:data,dataType:"json",type:"POST",mimeType:"application/json",success:function(){if(data.data.status==="completed")AnimeClient.hide(parentSel);AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.$("."+type+"s_read",parentSel)[0].textContent=
|
||||
completed;AnimeClient.showMessage("success","Successfully updated "+mangaName);AnimeClient.scrollToTop()},error:function(){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+mangaName);AnimeClient.scrollToTop()}})})})();
|
||||
//# sourceMappingURL=scripts-authed.min.js.map
|
||||
|
File diff suppressed because one or more lines are too long
2
public/js/scripts.min.js
vendored
2
public/js/scripts.min.js
vendored
@ -7,5 +7,5 @@ sel.addEventListener(event,listener,false)}function delegateEvent(sel,target,eve
|
||||
"GET")url+=url.match(/\?/)?ajaxSerialize(config.data):"?"+ajaxSerialize(config.data);request.open(method,url);request.onreadystatechange=function(){if(request.readyState===4){var responseText="";if(request.responseType==="json")responseText=JSON.parse(request.responseText);else responseText=request.responseText;if(request.status>299)config.error.call(null,request.status,responseText,request.response);else config.success.call(null,responseText,request.status)}};if(config.dataType==="json"){config.data=
|
||||
JSON.stringify(config.data);config.mimeType="application/json"}else config.data=ajaxSerialize(config.data);request.setRequestHeader("Content-Type",config.mimeType);switch(method){case "GET":request.send(null);break;default:request.send(config.data);break}};AnimeClient.get=function(url,data,callback){callback=callback===undefined?null:callback;if(callback===null){callback=data;data={}}return AnimeClient.ajax(url,{data:data,success:callback})};AnimeClient.on("header","click",".message",function(e){AnimeClient.hide(e.target)});
|
||||
AnimeClient.on("form.js-delete","submit",function(event){var proceed=confirm("Are you ABSOLUTELY SURE you want to delete this item?");if(proceed===false){event.preventDefault();event.stopPropagation()}});AnimeClient.on(".js-clear-cache","click",function(){AnimeClient.get("/cache_purge",function(){AnimeClient.showMessage("success","Successfully purged api cache")})});AnimeClient.on(".vertical-tabs input","change",function(event){var el=event.currentTarget.parentElement;var rect=el.getBoundingClientRect();
|
||||
var top=rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})})})();
|
||||
var top=rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})});if("serviceWorker"in navigator)navigator.serviceWorker.register("/sw.js").then(function(reg){console.log("Service worker registered",reg.scope)})["catch"](function(error){console.error("Failed to register service worker",error)})})();
|
||||
//# sourceMappingURL=scripts.min.js.map
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,10 +1,10 @@
|
||||
import './base/events.js';
|
||||
|
||||
/* if ('serviceWorker' in navigator) {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/sw.js').then(reg => {
|
||||
console.log('Service worker registered', reg.scope);
|
||||
}).catch(error => {
|
||||
console.error('Failed to register service worker', error);
|
||||
});
|
||||
} */
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,9 @@ const cssNext = require('postcss-cssnext');
|
||||
const cssNano = require('cssnano');
|
||||
|
||||
const css = fs.readFileSync('css/all.css', 'utf-8');
|
||||
const darkCss = fs.readFileSync('css/dark-override.css', 'utf-8');
|
||||
|
||||
// Basic theme
|
||||
postcss()
|
||||
.use(atImport())
|
||||
.use(cssNext())
|
||||
@ -24,6 +26,24 @@ postcss()
|
||||
from: 'css/all.css',
|
||||
to: 'css/app.min.css'
|
||||
}).then(result => {
|
||||
fs.writeFileSync('css/app.min.css', result.css);
|
||||
fs.writeFileSync('css/app.min.css.map', result.map);
|
||||
});
|
||||
fs.writeFileSync('css/app.min.css', result.css);
|
||||
});
|
||||
|
||||
// Dark theme
|
||||
postcss()
|
||||
.use(atImport())
|
||||
.use(cssNext())
|
||||
.use(cssNano({
|
||||
autoprefixer: false,
|
||||
colormin: false,
|
||||
minifyFontValues: false,
|
||||
options: {
|
||||
sourcemap: false
|
||||
}
|
||||
}))
|
||||
.process(darkCss, {
|
||||
from: 'css/dark-override.css',
|
||||
to: 'css/dark.min.css'
|
||||
}).then(result => {
|
||||
fs.writeFileSync('css/dark.min.css', result.css);
|
||||
});
|
@ -17,6 +17,7 @@
|
||||
namespace Aviat\AnimeClient\API;
|
||||
|
||||
use function Amp\Promise\wait;
|
||||
use function Aviat\AnimeClient\getResponse;
|
||||
|
||||
use Amp;
|
||||
use Amp\Artax\{FormBody, Request};
|
||||
@ -250,7 +251,7 @@ class APIRequestBuilder {
|
||||
*/
|
||||
public function getResponseData(Request $request)
|
||||
{
|
||||
$response = wait((new HummingbirdClient)->request($request));
|
||||
$response = getResponse($request);
|
||||
return wait($response->getBody());
|
||||
}
|
||||
|
||||
@ -315,7 +316,7 @@ class APIRequestBuilder {
|
||||
* @param string $type
|
||||
* @return void
|
||||
*/
|
||||
private function resetState($url, $type = 'GET')
|
||||
private function resetState($url, $type = 'GET'): void
|
||||
{
|
||||
$requestUrl = $url ?: $this->baseUrl;
|
||||
|
||||
|
@ -65,7 +65,7 @@ final class Anilist {
|
||||
MangaReadingStatus::PLAN_TO_READ => KMRS::PLAN_TO_READ,
|
||||
];
|
||||
|
||||
public static function getIdToWatchingStatusMap()
|
||||
public static function getIdToWatchingStatusMap(): array
|
||||
{
|
||||
return [
|
||||
'CURRENT' => AnimeWatchingStatus::WATCHING,
|
||||
@ -77,7 +77,7 @@ final class Anilist {
|
||||
];
|
||||
}
|
||||
|
||||
public static function getIdToReadingStatusMap()
|
||||
public static function getIdToReadingStatusMap(): array
|
||||
{
|
||||
return [
|
||||
'CURRENT' => MangaReadingStatus::READING,
|
||||
|
@ -19,14 +19,12 @@ namespace Aviat\AnimeClient\API\Anilist;
|
||||
use const Aviat\AnimeClient\USER_AGENT;
|
||||
|
||||
use function Amp\Promise\wait;
|
||||
use function Aviat\AnimeClient\getResponse;
|
||||
|
||||
use Amp\Artax\Request;
|
||||
use Amp\Artax\Response;
|
||||
|
||||
use Aviat\AnimeClient\API\{
|
||||
Anilist,
|
||||
HummingbirdClient
|
||||
};
|
||||
use Aviat\AnimeClient\API\Anilist;
|
||||
use Aviat\Ion\Json;
|
||||
use Aviat\Ion\Di\ContainerAware;
|
||||
|
||||
@ -200,7 +198,7 @@ trait AnilistTrait {
|
||||
}
|
||||
|
||||
$request = $this->setUpRequest($url, $options);
|
||||
$response = wait((new HummingbirdClient)->request($request));
|
||||
$response = getResponse($request);
|
||||
|
||||
$logger->debug('Anilist response', [
|
||||
'status' => $response->getStatus(),
|
||||
@ -221,7 +219,7 @@ trait AnilistTrait {
|
||||
$logger = $this->container->getLogger('anilist-request');
|
||||
}
|
||||
|
||||
$response = wait((new HummingbirdClient)->request($request));
|
||||
$response = getResponse($request);
|
||||
|
||||
$logger->debug('Anilist response', [
|
||||
'status' => $response->getStatus(),
|
||||
|
@ -100,7 +100,7 @@ final class Model
|
||||
$config = $this->container->get('config');
|
||||
$anilistUser = $config->get(['anilist', 'username']);
|
||||
|
||||
if ( ! is_string($anilistUser))
|
||||
if ( ! \is_string($anilistUser))
|
||||
{
|
||||
throw new InvalidArgumentException('Anilist username is not defined in config');
|
||||
}
|
||||
@ -151,10 +151,9 @@ final class Model
|
||||
* Create a list item with all the relevant data
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $type
|
||||
* @return Request
|
||||
*/
|
||||
public function createFullListItem(array $data, string $type = 'anime'): Request
|
||||
public function createFullListItem(array $data): Request
|
||||
{
|
||||
$createData = $data['data'];
|
||||
$mediaId = $this->getMediaIdFromMalId($data['mal_id']);
|
||||
@ -168,6 +167,7 @@ final class Model
|
||||
* Get the data for a specific list item, generally for editing
|
||||
*
|
||||
* @param string $malId - The unique identifier of that list item
|
||||
* @param string $type - Them media type (anime/manga)
|
||||
* @return mixed
|
||||
*/
|
||||
public function getListItem(string $malId, string $type): array
|
||||
@ -185,6 +185,7 @@ final class Model
|
||||
* Increase the watch count for the current list item
|
||||
*
|
||||
* @param FormItem $data
|
||||
* @param string $type - Them media type (anime/manga)
|
||||
* @return Request
|
||||
*/
|
||||
public function incrementListItem(FormItem $data, string $type): Request
|
||||
@ -198,7 +199,7 @@ final class Model
|
||||
* Modify a list item
|
||||
*
|
||||
* @param FormItem $data
|
||||
* @param int [$id]
|
||||
* @param string $type - Them media type (anime/manga)
|
||||
* @return Request
|
||||
*/
|
||||
public function updateListItem(FormItem $data, string $type): Request
|
||||
@ -225,6 +226,7 @@ final class Model
|
||||
* Get the id of the specific list entry from the malId
|
||||
*
|
||||
* @param string $malId
|
||||
* @param string $type - The media type (anime/manga)
|
||||
* @return string
|
||||
*/
|
||||
public function getListIdFromMalId(string $malId, string $type): ?string
|
||||
@ -234,7 +236,7 @@ final class Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Anilist media id from its MAL id
|
||||
* Get the Anilist list item id from the media id from its MAL id
|
||||
* this way is more accurate than getting the list item id
|
||||
* directly from the MAL id
|
||||
*/
|
||||
@ -248,13 +250,6 @@ final class Model
|
||||
'userName' => $anilistUser,
|
||||
]);
|
||||
|
||||
/* dump([
|
||||
'media_id' => $mediaId,
|
||||
'userName' => $anilistUser,
|
||||
'response' => $info,
|
||||
]);
|
||||
die(); */
|
||||
|
||||
return (string)$info['data']['MediaList']['id'];
|
||||
}
|
||||
|
||||
@ -272,12 +267,6 @@ final class Model
|
||||
'type' => mb_strtoupper($type),
|
||||
]);
|
||||
|
||||
/* dump([
|
||||
'mal_id' => $malId,
|
||||
'response' => $info,
|
||||
]);
|
||||
die(); */
|
||||
|
||||
return (string)$info['data']['Media']['id'];
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
namespace Aviat\AnimeClient\API\Anilist\Transformer;
|
||||
|
||||
use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\Anilist as AnilistStatus;
|
||||
use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\Kitsu as KitsuStatus;
|
||||
use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
|
||||
use Aviat\AnimeClient\Types\{AnimeListItem, FormItem};
|
||||
|
||||
@ -39,6 +40,8 @@ class AnimeListTransformer extends AbstractTransformer {
|
||||
*/
|
||||
public function untransform(array $item): FormItem
|
||||
{
|
||||
$reconsuming = $item['status'] === AnilistStatus::REPEATING;
|
||||
|
||||
return new FormItem([
|
||||
'id' => $item['id'],
|
||||
'mal_id' => $item['media']['idMal'],
|
||||
@ -46,26 +49,16 @@ class AnimeListTransformer extends AbstractTransformer {
|
||||
'notes' => $item['notes'] ?? '',
|
||||
'private' => $item['private'],
|
||||
'progress' => $item['progress'],
|
||||
'rating' => $item['score'],
|
||||
'rating' => $item['score'] ?? NULL,
|
||||
'reconsumeCount' => $item['repeat'],
|
||||
'reconsuming' => $item['status'] === AnilistStatus::REPEATING,
|
||||
'status' => AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']],
|
||||
'reconsuming' => $reconsuming,
|
||||
'status' => $reconsuming
|
||||
? KitsuStatus::WATCHING
|
||||
:AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']],
|
||||
'updatedAt' => (new DateTime())
|
||||
->setTimestamp($item['updatedAt'])
|
||||
->format(DateTime::W3C)
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a set of structures
|
||||
*
|
||||
* @param array|object $collection
|
||||
* @return array
|
||||
*/
|
||||
public function untransformCollection($collection): array
|
||||
{
|
||||
$list = (array)$collection;
|
||||
return array_map([$this, 'untransform'], $list);
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ namespace Aviat\AnimeClient\API\Anilist\Transformer;
|
||||
|
||||
use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Anilist as AnilistStatus;
|
||||
use Aviat\AnimeClient\API\Mapping\MangaReadingStatus;
|
||||
use Aviat\AnimeClient\Types\MangaListItem;
|
||||
use Aviat\AnimeClient\Types\FormItem;
|
||||
|
||||
use Aviat\Ion\Transformer\AbstractTransformer;
|
||||
@ -28,7 +29,7 @@ class MangaListTransformer extends AbstractTransformer {
|
||||
|
||||
public function transform($item)
|
||||
{
|
||||
|
||||
return new MangaListItem([]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -56,16 +57,4 @@ class MangaListTransformer extends AbstractTransformer {
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a set of structures
|
||||
*
|
||||
* @param array|object $collection
|
||||
* @return array
|
||||
*/
|
||||
public function untransformCollection($collection): array
|
||||
{
|
||||
$list = (array)$collection;
|
||||
return array_map([$this, 'untransform'], $list);
|
||||
}
|
||||
}
|
@ -22,10 +22,10 @@ use Aviat\Ion\Enum;
|
||||
* Possible values for watching status for the current anime
|
||||
*/
|
||||
final class Anilist extends Enum {
|
||||
const WATCHING = 'CURRENT';
|
||||
const COMPLETED = 'COMPLETED';
|
||||
const ON_HOLD = 'PAUSED';
|
||||
const DROPPED = 'DROPPED';
|
||||
const PLAN_TO_WATCH = 'PLANNING';
|
||||
const REPEATING = 'REPEATING';
|
||||
public const WATCHING = 'CURRENT';
|
||||
public const COMPLETED = 'COMPLETED';
|
||||
public const ON_HOLD = 'PAUSED';
|
||||
public const DROPPED = 'DROPPED';
|
||||
public const PLAN_TO_WATCH = 'PLANNING';
|
||||
public const REPEATING = 'REPEATING';
|
||||
}
|
@ -22,9 +22,9 @@ use Aviat\Ion\Enum;
|
||||
* Possible values for watching status for the current anime
|
||||
*/
|
||||
final class Kitsu extends Enum {
|
||||
const WATCHING = 'current';
|
||||
const PLAN_TO_WATCH = 'planned';
|
||||
const ON_HOLD = 'on_hold';
|
||||
const DROPPED = 'dropped';
|
||||
const COMPLETED = 'completed';
|
||||
public const WATCHING = 'current';
|
||||
public const PLAN_TO_WATCH = 'planned';
|
||||
public const ON_HOLD = 'on_hold';
|
||||
public const DROPPED = 'dropped';
|
||||
public const COMPLETED = 'completed';
|
||||
}
|
@ -16,16 +16,16 @@
|
||||
|
||||
namespace Aviat\AnimeClient\API\Enum\AnimeWatchingStatus;
|
||||
|
||||
use Aviat\Ion\Enum as Enum;
|
||||
use Aviat\Ion\Enum;
|
||||
|
||||
/**
|
||||
* Possible values for current watching status of anime
|
||||
*/
|
||||
final class Route extends Enum {
|
||||
const ALL = 'all';
|
||||
const WATCHING = 'watching';
|
||||
const PLAN_TO_WATCH = 'plan_to_watch';
|
||||
const DROPPED = 'dropped';
|
||||
const ON_HOLD = 'on_hold';
|
||||
const COMPLETED = 'completed';
|
||||
public const ALL = 'all';
|
||||
public const WATCHING = 'watching';
|
||||
public const PLAN_TO_WATCH = 'plan_to_watch';
|
||||
public const DROPPED = 'dropped';
|
||||
public const ON_HOLD = 'on_hold';
|
||||
public const COMPLETED = 'completed';
|
||||
}
|
||||
|
@ -16,16 +16,16 @@
|
||||
|
||||
namespace Aviat\AnimeClient\API\Enum\AnimeWatchingStatus;
|
||||
|
||||
use Aviat\Ion\Enum as Enum;
|
||||
use Aviat\Ion\Enum;
|
||||
|
||||
/**
|
||||
* Possible values for current watching status of anime
|
||||
*/
|
||||
final class Title extends Enum {
|
||||
const ALL = 'All';
|
||||
const WATCHING = 'Currently Watching';
|
||||
const PLAN_TO_WATCH = 'Plan to Watch';
|
||||
const DROPPED = 'Dropped';
|
||||
const ON_HOLD = 'On Hold';
|
||||
const COMPLETED = 'Completed';
|
||||
public const ALL = 'All';
|
||||
public const WATCHING = 'Currently Watching';
|
||||
public const PLAN_TO_WATCH = 'Plan to Watch';
|
||||
public const DROPPED = 'Dropped';
|
||||
public const ON_HOLD = 'On Hold';
|
||||
public const COMPLETED = 'Completed';
|
||||
}
|
||||
|
@ -22,10 +22,10 @@ use Aviat\Ion\Enum;
|
||||
* Possible values for watching status for the current anime
|
||||
*/
|
||||
final class Anilist extends Enum {
|
||||
const READING = 'CURRENT';
|
||||
const COMPLETED = 'COMPLETED';
|
||||
const ON_HOLD = 'PAUSED';
|
||||
const DROPPED = 'DROPPED';
|
||||
const PLAN_TO_READ = 'PLANNING';
|
||||
const REPEATING = 'REPEATING';
|
||||
public const READING = 'CURRENT';
|
||||
public const COMPLETED = 'COMPLETED';
|
||||
public const ON_HOLD = 'PAUSED';
|
||||
public const DROPPED = 'DROPPED';
|
||||
public const PLAN_TO_READ = 'PLANNING';
|
||||
public const REPEATING = 'REPEATING';
|
||||
}
|
@ -22,9 +22,9 @@ use Aviat\Ion\Enum;
|
||||
* Possible values for current reading status of manga
|
||||
*/
|
||||
final class Kitsu extends Enum {
|
||||
const READING = 'current';
|
||||
const PLAN_TO_READ = 'planned';
|
||||
const DROPPED = 'dropped';
|
||||
const ON_HOLD = 'on_hold';
|
||||
const COMPLETED = 'completed';
|
||||
public const READING = 'current';
|
||||
public const PLAN_TO_READ = 'planned';
|
||||
public const DROPPED = 'dropped';
|
||||
public const ON_HOLD = 'on_hold';
|
||||
public const COMPLETED = 'completed';
|
||||
}
|
@ -22,10 +22,10 @@ use Aviat\Ion\Enum;
|
||||
* Possible values for current reading status of manga
|
||||
*/
|
||||
final class Route extends Enum {
|
||||
const ALL = 'all';
|
||||
const READING = 'reading';
|
||||
const PLAN_TO_READ = 'plan_to_read';
|
||||
const DROPPED = 'dropped';
|
||||
const ON_HOLD = 'on_hold';
|
||||
const COMPLETED = 'completed';
|
||||
public const ALL = 'all';
|
||||
public const READING = 'reading';
|
||||
public const PLAN_TO_READ = 'plan_to_read';
|
||||
public const DROPPED = 'dropped';
|
||||
public const ON_HOLD = 'on_hold';
|
||||
public const COMPLETED = 'completed';
|
||||
}
|
||||
|
@ -22,10 +22,10 @@ use Aviat\Ion\Enum;
|
||||
* Possible values for current reading status of manga
|
||||
*/
|
||||
final class Title extends Enum {
|
||||
const ALL = 'All';
|
||||
const READING = 'Currently Reading';
|
||||
const PLAN_TO_READ = 'Plan to Read';
|
||||
const DROPPED = 'Dropped';
|
||||
const ON_HOLD = 'On Hold';
|
||||
const COMPLETED = 'Completed';
|
||||
public const ALL = 'All';
|
||||
public const READING = 'Currently Reading';
|
||||
public const PLAN_TO_READ = 'Plan to Read';
|
||||
public const DROPPED = 'Dropped';
|
||||
public const ON_HOLD = 'On Hold';
|
||||
public const COMPLETED = 'Completed';
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -21,9 +21,7 @@ namespace Aviat\AnimeClient\API;
|
||||
*/
|
||||
final class JsonAPI {
|
||||
|
||||
/**
|
||||
* The full data array
|
||||
*
|
||||
/*
|
||||
* Basic structure is generally like so:
|
||||
* [
|
||||
* 'id' => '12016665',
|
||||
@ -35,10 +33,7 @@ final class JsonAPI {
|
||||
*
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* Inline all included data
|
||||
@ -214,8 +209,7 @@ final class JsonAPI {
|
||||
$dataType = $props['data']['type'];
|
||||
|
||||
$relationship =& $organized[$type][$id]['relationships'][$relType];
|
||||
unset($relationship['links']);
|
||||
unset($relationship['data']);
|
||||
unset($relationship['links'], $relationship['data']);
|
||||
|
||||
if ($relType === $dataType)
|
||||
{
|
||||
|
@ -23,11 +23,11 @@ use DateTimeImmutable;
|
||||
* Data massaging helpers for the Kitsu API
|
||||
*/
|
||||
final class Kitsu {
|
||||
const AUTH_URL = 'https://kitsu.io/api/oauth/token';
|
||||
const AUTH_USER_ID_KEY = 'kitsu-auth-userid';
|
||||
const AUTH_TOKEN_CACHE_KEY = 'kitsu-auth-token';
|
||||
const AUTH_TOKEN_EXP_CACHE_KEY = 'kitsu-auth-token-expires';
|
||||
const AUTH_TOKEN_REFRESH_CACHE_KEY = 'kitsu-auth-token-refresh';
|
||||
public const AUTH_URL = 'https://kitsu.io/api/oauth/token';
|
||||
public const AUTH_USER_ID_KEY = 'kitsu-auth-userid';
|
||||
public const AUTH_TOKEN_CACHE_KEY = 'kitsu-auth-token';
|
||||
public const AUTH_TOKEN_EXP_CACHE_KEY = 'kitsu-auth-token-expires';
|
||||
public const AUTH_TOKEN_REFRESH_CACHE_KEY = 'kitsu-auth-token-refresh';
|
||||
|
||||
/**
|
||||
* Determine whether an anime is airing, finished airing, or has not yet aired
|
||||
|
@ -22,8 +22,8 @@ use Aviat\Ion\Enum as BaseEnum;
|
||||
* Status of when anime is being/was/will be aired
|
||||
*/
|
||||
final class AnimeAiringStatus extends BaseEnum {
|
||||
const NOT_YET_AIRED = 'Not Yet Aired';
|
||||
const AIRING = 'Currently Airing';
|
||||
const FINISHED_AIRING = 'Finished Airing';
|
||||
public const NOT_YET_AIRED = 'Not Yet Aired';
|
||||
public const AIRING = 'Currently Airing';
|
||||
public const FINISHED_AIRING = 'Finished Airing';
|
||||
}
|
||||
// End of AnimeAiringStatus.php
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
namespace Aviat\AnimeClient\API\Kitsu;
|
||||
|
||||
use const Aviat\AnimeClient\USER_AGENT;
|
||||
use Aviat\AnimeClient\API\APIRequestBuilder;
|
||||
|
||||
final class KitsuRequestBuilder extends APIRequestBuilder {
|
||||
@ -32,7 +33,7 @@ final class KitsuRequestBuilder extends APIRequestBuilder {
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultHeaders = [
|
||||
'User-Agent' => "Tim's Anime Client/4.0",
|
||||
'User-Agent' => USER_AGENT,
|
||||
'Accept' => 'application/vnd.api+json',
|
||||
'Content-Type' => 'application/vnd.api+json',
|
||||
'CLIENT_ID' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd',
|
||||
|
@ -19,12 +19,12 @@ namespace Aviat\AnimeClient\API\Kitsu;
|
||||
use const Aviat\AnimeClient\SESSION_SEGMENT;
|
||||
|
||||
use function Amp\Promise\wait;
|
||||
use function Aviat\AnimeClient\getResponse;
|
||||
|
||||
use Amp\Artax\Request;
|
||||
use Aviat\AnimeClient\AnimeClient;
|
||||
use Amp\Artax\Response;
|
||||
use Aviat\AnimeClient\API\{
|
||||
FailedResponseException,
|
||||
HummingbirdClient,
|
||||
Kitsu as K
|
||||
};
|
||||
use Aviat\Ion\Json;
|
||||
@ -121,7 +121,7 @@ trait KitsuTrait {
|
||||
* @param array $options
|
||||
* @return Response
|
||||
*/
|
||||
private function getResponse(string $type, string $url, array $options = [])
|
||||
private function getResponse(string $type, string $url, array $options = []): Response
|
||||
{
|
||||
$logger = NULL;
|
||||
if ($this->getContainer())
|
||||
@ -131,7 +131,7 @@ trait KitsuTrait {
|
||||
|
||||
$request = $this->setUpRequest($type, $url, $options);
|
||||
|
||||
$response = wait((new HummingbirdClient)->request($request));
|
||||
$response = getResponse($request);
|
||||
|
||||
if ($logger)
|
||||
{
|
||||
|
@ -19,12 +19,10 @@ namespace Aviat\AnimeClient\API\Kitsu;
|
||||
use const Aviat\AnimeClient\SESSION_SEGMENT;
|
||||
|
||||
use function Amp\Promise\wait;
|
||||
use function Aviat\AnimeClient\getResponse;
|
||||
|
||||
use Amp\Artax\Request;
|
||||
use Aviat\AnimeClient\API\{
|
||||
HummingbirdClient,
|
||||
ListItemInterface
|
||||
};
|
||||
use Aviat\AnimeClient\API\ListItemInterface;
|
||||
use Aviat\AnimeClient\Types\FormItemData;
|
||||
use Aviat\Ion\Di\ContainerAware;
|
||||
use Aviat\Ion\Json;
|
||||
@ -78,8 +76,6 @@ final class ListItem implements ListItemInterface {
|
||||
|
||||
return $request->setJsonBody($body)
|
||||
->getFullRequest();
|
||||
|
||||
// return ($response->getStatus() === 201);
|
||||
}
|
||||
|
||||
public function delete(string $id): Request
|
||||
@ -93,8 +89,6 @@ final class ListItem implements ListItemInterface {
|
||||
}
|
||||
|
||||
return $request->getFullRequest();
|
||||
|
||||
// return ($response->getStatus() === 204);
|
||||
}
|
||||
|
||||
public function get(string $id): array
|
||||
@ -112,8 +106,7 @@ final class ListItem implements ListItemInterface {
|
||||
}
|
||||
|
||||
$request = $request->getFullRequest();
|
||||
|
||||
$response = wait((new HummingbirdClient)->request($request));
|
||||
$response = getResponse($request);
|
||||
return Json::decode(wait($response->getBody()));
|
||||
}
|
||||
|
||||
|
@ -91,9 +91,10 @@ final class Model {
|
||||
{
|
||||
$this->animeTransformer = new AnimeTransformer();
|
||||
$this->animeListTransformer = new AnimeListTransformer();
|
||||
$this->listItem = $listItem;
|
||||
$this->mangaTransformer = new MangaTransformer();
|
||||
$this->mangaListTransformer = new MangaListTransformer();
|
||||
|
||||
$this->listItem = $listItem;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -265,7 +266,7 @@ final class Model {
|
||||
public function getUserData(string $username): array
|
||||
{
|
||||
// $userId = $this->getUserIdByUsername($username);
|
||||
$data = $this->getRequest("users", [
|
||||
$data = $this->getRequest('users', [
|
||||
'query' => [
|
||||
'filter' => [
|
||||
'name' => $username,
|
||||
@ -334,7 +335,7 @@ final class Model {
|
||||
* @param string $type "anime" or "manga"
|
||||
* @return string|NULL
|
||||
*/
|
||||
public function getKitsuIdFromMALId(string $malId, string $type="anime")
|
||||
public function getKitsuIdFromMALId(string $malId, string $type='anime'): ?string
|
||||
{
|
||||
$options = [
|
||||
'query' => [
|
||||
@ -369,7 +370,7 @@ final class Model {
|
||||
* @param string $slug
|
||||
* @return Anime
|
||||
*/
|
||||
public function getAnime(string $slug)
|
||||
public function getAnime(string $slug): Anime
|
||||
{
|
||||
$baseData = $this->getRawMediaData('anime', $slug);
|
||||
|
||||
@ -523,7 +524,7 @@ final class Model {
|
||||
* @param string $kitsuAnimeId The id of the anime on Kitsu
|
||||
* @return string|null Returns the mal id if it exists, otherwise null
|
||||
*/
|
||||
public function getMalIdForAnime(string $kitsuAnimeId)
|
||||
public function getMalIdForAnime(string $kitsuAnimeId): ?string
|
||||
{
|
||||
$options = [
|
||||
'query' => [
|
||||
@ -625,7 +626,7 @@ final class Model {
|
||||
* Get information about a particular manga
|
||||
*
|
||||
* @param string $mangaId
|
||||
* @return array
|
||||
* @return MangaPage
|
||||
*/
|
||||
public function getMangaById(string $mangaId): MangaPage
|
||||
{
|
||||
@ -808,7 +809,7 @@ final class Model {
|
||||
* @param string $kitsuMangaId The id of the manga on Kitsu
|
||||
* @return string|null Returns the mal id if it exists, otherwise null
|
||||
*/
|
||||
public function getMalIdForManga(string $kitsuMangaId)
|
||||
public function getMalIdForManga(string $kitsuMangaId): ?string
|
||||
{
|
||||
$options = [
|
||||
'query' => [
|
||||
@ -920,7 +921,7 @@ final class Model {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw data for the anime id
|
||||
* Get the raw data for the anime/manga id
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $id
|
||||
|
@ -18,7 +18,6 @@ namespace Aviat\AnimeClient\API\Kitsu\Transformer;
|
||||
|
||||
use Aviat\AnimeClient\API\Kitsu;
|
||||
use Aviat\AnimeClient\Types\{
|
||||
Anime,
|
||||
FormItem,
|
||||
AnimeListItem
|
||||
};
|
||||
@ -95,7 +94,7 @@ final class AnimeListTransformer extends AbstractTransformer {
|
||||
'started' => $anime['startDate'],
|
||||
'ended' => $anime['endDate']
|
||||
],
|
||||
'anime' => new Anime([
|
||||
'anime' => [
|
||||
'id' => $animeId,
|
||||
'age_rating' => $anime['ageRating'],
|
||||
'title' => $title,
|
||||
@ -105,7 +104,7 @@ final class AnimeListTransformer extends AbstractTransformer {
|
||||
'cover_image' => $anime['posterImage']['small'],
|
||||
'genres' => $genres,
|
||||
'streaming_links' => $streamingLinks,
|
||||
]),
|
||||
],
|
||||
'watching_status' => $item['attributes']['status'],
|
||||
'notes' => $item['attributes']['notes'],
|
||||
'rewatching' => (bool) $item['attributes']['reconsuming'],
|
||||
|
@ -17,7 +17,7 @@
|
||||
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
|
||||
|
||||
use Aviat\AnimeClient\API\{JsonAPI, Kitsu};
|
||||
use Aviat\AnimeClient\Types\Anime;
|
||||
use Aviat\AnimeClient\Types\AnimePage;
|
||||
use Aviat\Ion\Transformer\AbstractTransformer;
|
||||
|
||||
/**
|
||||
@ -30,9 +30,9 @@ final class AnimeTransformer extends AbstractTransformer {
|
||||
* logical and workable structure
|
||||
*
|
||||
* @param array $item API library item
|
||||
* @return Anime
|
||||
* @return AnimePage
|
||||
*/
|
||||
public function transform($item): Anime
|
||||
public function transform($item): AnimePage
|
||||
{
|
||||
$item['included'] = JsonAPI::organizeIncludes($item['included']);
|
||||
$genres = $item['included']['categories'] ?? [];
|
||||
@ -40,13 +40,74 @@ final class AnimeTransformer extends AbstractTransformer {
|
||||
sort($item['genres']);
|
||||
|
||||
$title = $item['canonicalTitle'];
|
||||
|
||||
$titles = Kitsu::filterTitles($item);
|
||||
// $titles = array_unique(array_diff($item['titles'], [$title]));
|
||||
|
||||
return new Anime([
|
||||
$characters = [];
|
||||
$staff = [];
|
||||
|
||||
if (array_key_exists('animeCharacters', $item['included']))
|
||||
{
|
||||
$animeCharacters = $item['included']['animeCharacters'];
|
||||
|
||||
foreach ($animeCharacters as $rel)
|
||||
{
|
||||
$charId = $rel['relationships']['character']['data']['id'];
|
||||
$role = $rel['role'];
|
||||
|
||||
if (array_key_exists($charId, $item['included']['characters']))
|
||||
{
|
||||
$characters[$role][$charId] = $item['included']['characters'][$charId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('mediaStaff', $item['included']))
|
||||
{
|
||||
foreach ($item['included']['mediaStaff'] as $id => $staffing)
|
||||
{
|
||||
$personId = $staffing['relationships']['person']['data']['id'];
|
||||
$personDetails = $item['included']['people'][$personId];
|
||||
|
||||
$role = $staffing['role'];
|
||||
|
||||
if ( ! array_key_exists($role, $staff))
|
||||
{
|
||||
$staff[$role] = [];
|
||||
}
|
||||
|
||||
$staff[$role][$personId] = [
|
||||
'id' => $personId,
|
||||
'name' => $personDetails['name'] ?? '??',
|
||||
'image' => $personDetails['image'],
|
||||
];
|
||||
|
||||
usort($staff[$role], function ($a, $b) {
|
||||
return $a['name'] <=> $b['name'];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty($characters['main']))
|
||||
{
|
||||
uasort($characters['main'], function ($a, $b) {
|
||||
return $a['name'] <=> $b['name'];
|
||||
});
|
||||
}
|
||||
|
||||
if ( ! empty($characters['supporting']))
|
||||
{
|
||||
uasort($characters['supporting'], function ($a, $b) {
|
||||
return $a['name'] <=> $b['name'];
|
||||
});
|
||||
}
|
||||
|
||||
ksort($characters);
|
||||
ksort($staff);
|
||||
|
||||
return new AnimePage([
|
||||
'age_rating' => $item['ageRating'],
|
||||
'age_rating_guide' => $item['ageRatingGuide'],
|
||||
'characters' => $characters,
|
||||
'cover_image' => $item['posterImage']['small'],
|
||||
'episode_count' => $item['episodeCount'],
|
||||
'episode_length' => $item['episodeLength'],
|
||||
@ -55,6 +116,7 @@ final class AnimeTransformer extends AbstractTransformer {
|
||||
'included' => $item['included'],
|
||||
'show_type' => $this->string($item['showType'])->upperCaseFirst()->__toString(),
|
||||
'slug' => $item['slug'],
|
||||
'staff' => $staff,
|
||||
'status' => Kitsu::getAiringStatus($item['startDate'], $item['endDate']),
|
||||
'streaming_links' => Kitsu::parseStreamingLinks($item['included']),
|
||||
'synopsis' => $item['synopsis'],
|
||||
|
173
src/API/Kitsu/Transformer/CharacterTransformer.php
Normal file
173
src/API/Kitsu/Transformer/CharacterTransformer.php
Normal file
@ -0,0 +1,173 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7.1
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2018 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 4.1
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
|
||||
|
||||
use Aviat\AnimeClient\API\JsonAPI;
|
||||
use Aviat\AnimeClient\Types\Character;
|
||||
|
||||
use Aviat\Ion\Transformer\AbstractTransformer;
|
||||
|
||||
/**
|
||||
* Data transformation class for character pages
|
||||
*/
|
||||
final class CharacterTransformer extends AbstractTransformer {
|
||||
|
||||
public function transform($characterData): Character
|
||||
{
|
||||
$data = JsonAPI::organizeData($characterData);
|
||||
$attributes = $data[0]['attributes'];
|
||||
$castings = [];
|
||||
|
||||
$names = array_unique(
|
||||
array_merge(
|
||||
[$attributes['canonicalName']],
|
||||
$attributes['names']
|
||||
)
|
||||
);
|
||||
$name = array_shift($names);
|
||||
|
||||
if (array_key_exists('included', $data))
|
||||
{
|
||||
if (array_key_exists('anime', $data['included']))
|
||||
{
|
||||
uasort($data['included']['anime'], function ($a, $b) {
|
||||
return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle'];
|
||||
});
|
||||
}
|
||||
|
||||
if (array_key_exists('manga', $data['included']))
|
||||
{
|
||||
uasort($data['included']['manga'], function ($a, $b) {
|
||||
return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle'];
|
||||
});
|
||||
}
|
||||
|
||||
if (array_key_exists('castings', $data['included']))
|
||||
{
|
||||
$castings = $this->organizeCast($data['included']['castings']);
|
||||
}
|
||||
}
|
||||
|
||||
return new Character([
|
||||
'castings' => $castings,
|
||||
'description' => $attributes['description'],
|
||||
'id' => $data[0]['id'],
|
||||
'media' => [
|
||||
'anime' => $data['included']['anime'] ?? [],
|
||||
'manga' => $data['included']['manga'] ?? [],
|
||||
],
|
||||
'name' => $name,
|
||||
'names' => $names,
|
||||
'otherNames' => $attributes['otherNames'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Organize VA => anime relationships
|
||||
*
|
||||
* @param array $cast
|
||||
* @return array
|
||||
*/
|
||||
private function dedupeCast(array $cast): array
|
||||
{
|
||||
$output = [];
|
||||
$people = [];
|
||||
|
||||
$i = 0;
|
||||
foreach ($cast as &$role)
|
||||
{
|
||||
if (empty($role['attributes']['role']))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
$person = current($role['relationships']['person']['people'])['attributes'];
|
||||
$hasName = array_key_exists($person['name'], $people);
|
||||
|
||||
if ( ! $hasName)
|
||||
{
|
||||
$people[$person['name']] = $i;
|
||||
$role['relationships']['media']['anime'] = [current($role['relationships']['media']['anime'])];
|
||||
$output[$i] = $role;
|
||||
|
||||
$i++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (array_key_exists('anime', $role['relationships']['media']))
|
||||
{
|
||||
$key = $people[$person['name']];
|
||||
$output[$key]['relationships']['media']['anime'][] = current($role['relationships']['media']['anime']);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
protected function organizeCast(array $cast): array
|
||||
{
|
||||
$cast = $this->dedupeCast($cast);
|
||||
$output = [];
|
||||
|
||||
foreach ($cast as $id => $role)
|
||||
{
|
||||
if (empty($role['attributes']['role']))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$language = $role['attributes']['language'];
|
||||
$roleName = $role['attributes']['role'];
|
||||
$isVA = $role['attributes']['voiceActor'];
|
||||
|
||||
if ($isVA)
|
||||
{
|
||||
foreach ($role['relationships']['person']['people'] as $pid => $peoples)
|
||||
{
|
||||
$p = $peoples;
|
||||
}
|
||||
|
||||
$person = $p['attributes'];
|
||||
$person['id'] = $pid;
|
||||
$person['image'] = $person['image']['original'];
|
||||
|
||||
uasort($role['relationships']['media']['anime'], function ($a, $b) {
|
||||
return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle'];
|
||||
});
|
||||
|
||||
$item = [
|
||||
'person' => $person,
|
||||
'series' => $role['relationships']['media']['anime']
|
||||
];
|
||||
|
||||
$output[$roleName][$language][] = $item;
|
||||
} else
|
||||
{
|
||||
foreach ($role['relationships']['person']['people'] as $pid => $person)
|
||||
{
|
||||
$person['id'] = $pid;
|
||||
$output[$roleName][$pid] = $person;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
@ -51,13 +51,77 @@ final class MangaTransformer extends AbstractTransformer {
|
||||
$rawTitles = array_values($item['titles']);
|
||||
$titles = array_unique(array_diff($rawTitles, [$title]));
|
||||
|
||||
$characters = [];
|
||||
$staff = [];
|
||||
|
||||
if (array_key_exists('mediaCharacters', $item['included']))
|
||||
{
|
||||
$mediaCharacters = $item['included']['mediaCharacters'];
|
||||
|
||||
foreach ($mediaCharacters as $rel)
|
||||
{
|
||||
// dd($rel);
|
||||
// $charId = $rel['relationships']['character']['data']['id'];
|
||||
$role = $rel['attributes']['role'];
|
||||
|
||||
foreach ($rel['relationships']['character']['characters'] as $charId => $char)
|
||||
{
|
||||
if (array_key_exists($charId, $item['included']['characters']))
|
||||
{
|
||||
$characters[$role][$charId] = $char['attributes'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('mediaStaff', $item['included']))
|
||||
{
|
||||
foreach ($item['included']['mediaStaff'] as $id => $staffing)
|
||||
{
|
||||
$role = $staffing['attributes']['role'];
|
||||
|
||||
foreach ($staffing['relationships']['person']['people'] as $personId => $personDetails)
|
||||
{
|
||||
if ( ! array_key_exists($role, $staff))
|
||||
{
|
||||
$staff[$role] = [];
|
||||
}
|
||||
|
||||
$staff[$role][$personId] = [
|
||||
'id' => $personId,
|
||||
'name' => $personDetails['attributes']['name'] ?? '??',
|
||||
'image' => $personDetails['attributes']['image'],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty($characters['main']))
|
||||
{
|
||||
uasort($characters['main'], function ($a, $b) {
|
||||
return $a['name'] <=> $b['name'];
|
||||
});
|
||||
}
|
||||
|
||||
if ( ! empty($characters['supporting']))
|
||||
{
|
||||
uasort($characters['supporting'], function ($a, $b) {
|
||||
return $a['name'] <=> $b['name'];
|
||||
});
|
||||
}
|
||||
|
||||
ksort($characters);
|
||||
ksort($staff);
|
||||
|
||||
return new MangaPage([
|
||||
'characters' => $characters,
|
||||
'chapter_count' => $this->count($item['chapterCount']),
|
||||
'cover_image' => $item['posterImage']['small'],
|
||||
'genres' => $genres,
|
||||
'id' => $item['id'],
|
||||
'included' => $item['included'],
|
||||
'manga_type' => $item['mangaType'],
|
||||
'staff' => $staff,
|
||||
'synopsis' => $item['synopsis'],
|
||||
'title' => $title,
|
||||
'titles' => $titles,
|
||||
|
132
src/API/Kitsu/Transformer/PersonTransformer.php
Normal file
132
src/API/Kitsu/Transformer/PersonTransformer.php
Normal file
@ -0,0 +1,132 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7.1
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2018 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 4.1
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
|
||||
|
||||
use Aviat\AnimeClient\API\JsonAPI;
|
||||
use Aviat\AnimeClient\Types\Person;
|
||||
use Aviat\Ion\Transformer\AbstractTransformer;
|
||||
|
||||
/**
|
||||
* Data transformation class for people pages
|
||||
*/
|
||||
final class PersonTransformer extends AbstractTransformer {
|
||||
|
||||
public function transform($personData): Person
|
||||
{
|
||||
$data = JsonAPI::organizeData($personData);
|
||||
$included = JsonAPI::organizeIncludes($personData['included']);
|
||||
|
||||
$orgData = $this->organizeData($included);
|
||||
|
||||
return new Person([
|
||||
'id' => $data['id'],
|
||||
'name' => $data['attributes']['name'],
|
||||
'characters' => $orgData['characters'],
|
||||
'staff' => $orgData['staff'],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function organizeData(array $data): array
|
||||
{
|
||||
$output = [
|
||||
'characters' => [
|
||||
'main' => [],
|
||||
'supporting' => [],
|
||||
],
|
||||
'staff' => [],
|
||||
];
|
||||
|
||||
if (array_key_exists('characterVoices', $data))
|
||||
{
|
||||
foreach ($data['characterVoices'] as $cv)
|
||||
{
|
||||
$mcId = $cv['relationships']['mediaCharacter']['data']['id'];
|
||||
|
||||
if ( ! array_key_exists($mcId, $data['mediaCharacters']))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$mc = $data['mediaCharacters'][$mcId];
|
||||
|
||||
$role = $mc['role'];
|
||||
|
||||
$charId = $mc['relationships']['character']['data']['id'];
|
||||
$mediaId = $mc['relationships']['media']['data']['id'];
|
||||
|
||||
$existingMedia = array_key_exists($charId, $output['characters'][$role])
|
||||
? $output['characters'][$role][$charId]['media']
|
||||
: [];
|
||||
|
||||
$relatedMedia = [
|
||||
$mediaId => $data['anime'][$mediaId],
|
||||
];
|
||||
|
||||
$includedMedia = array_replace_recursive($existingMedia, $relatedMedia);
|
||||
|
||||
uasort($includedMedia, function ($a, $b) {
|
||||
return $a['canonicalTitle'] <=> $b['canonicalTitle'];
|
||||
});
|
||||
|
||||
$character = $data['characters'][$charId];
|
||||
|
||||
$output['characters'][$role][$charId] = [
|
||||
'character' => $character,
|
||||
'media' => $includedMedia,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('mediaStaff', $data))
|
||||
{
|
||||
foreach ($data['mediaStaff'] as $rid => $role)
|
||||
{
|
||||
$roleName = $role['role'];
|
||||
$mediaType = $role['relationships']['media']['data']['type'];
|
||||
$mediaId = $role['relationships']['media']['data']['id'];
|
||||
$media = $data[$mediaType][$mediaId];
|
||||
$output['staff'][$roleName][$mediaType][$mediaId] = $media;
|
||||
}
|
||||
}
|
||||
|
||||
uasort($output['characters']['main'], function ($a, $b) {
|
||||
return $a['character']['canonicalName'] <=> $b['character']['canonicalName'];
|
||||
});
|
||||
uasort($output['characters']['supporting'], function ($a, $b) {
|
||||
return $a['character']['canonicalName'] <=> $b['character']['canonicalName'];
|
||||
});
|
||||
ksort($output['staff']);
|
||||
foreach ($output['staff'] as $role => &$media)
|
||||
{
|
||||
if (array_key_exists('anime', $media))
|
||||
{
|
||||
uasort($media['anime'], function ($a, $b) {
|
||||
return $a['canonicalTitle'] <=> $b['canonicalTitle'];
|
||||
});
|
||||
}
|
||||
|
||||
if (array_key_exists('manga', $media))
|
||||
{
|
||||
uasort($media['manga'], function ($a, $b) {
|
||||
return $a['canonicalTitle'] <=> $b['canonicalTitle'];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
168
src/API/Kitsu/Transformer/UserTransformer.php
Normal file
168
src/API/Kitsu/Transformer/UserTransformer.php
Normal file
@ -0,0 +1,168 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7.1
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2018 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 4.1
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
|
||||
|
||||
use function Aviat\AnimeClient\getLocalImg;
|
||||
|
||||
use Aviat\AnimeClient\API\JsonAPI;
|
||||
use Aviat\AnimeClient\Types\User;
|
||||
use Aviat\Ion\Transformer\AbstractTransformer;
|
||||
|
||||
/**
|
||||
* Transform user profile data for display
|
||||
*/
|
||||
final class UserTransformer extends AbstractTransformer {
|
||||
public function transform($profileData): User
|
||||
{
|
||||
$orgData = JsonAPI::organizeData($profileData)[0];
|
||||
$attributes = $orgData['attributes'];
|
||||
|
||||
$rels = $orgData['relationships'] ?? [];
|
||||
$favorites = array_key_exists('favorites', $rels) ? $rels['favorites'] : [];
|
||||
|
||||
$stats = [];
|
||||
foreach ($rels['stats'] as $sid => &$item)
|
||||
{
|
||||
$key = $item['attributes']['kind'];
|
||||
$stats[$key] = $item['attributes']['statsData'];
|
||||
unset($item);
|
||||
}
|
||||
|
||||
$waifu = [];
|
||||
if (array_key_exists('waifu', $rels))
|
||||
{
|
||||
$waifu = [
|
||||
'label' => $attributes['waifuOrHusbando'],
|
||||
'character' => $rels['waifu']['attributes'],
|
||||
];
|
||||
}
|
||||
|
||||
return new User([
|
||||
'about' => $attributes['about'],
|
||||
'avatar' => getLocalImg($attributes['avatar']['original'], FALSE),
|
||||
'favorites' => $this->organizeFavorites($favorites),
|
||||
'location' => $attributes['location'],
|
||||
'name' => $attributes['name'],
|
||||
'slug' => $attributes['slug'],
|
||||
'stats' => $this->organizeStats($stats, $attributes),
|
||||
'waifu' => $waifu,
|
||||
'website' => $attributes['website'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorganize favorites data to be more useful
|
||||
*
|
||||
* @param array $rawFavorites
|
||||
* @return array
|
||||
*/
|
||||
private function organizeFavorites(array $rawFavorites): array
|
||||
{
|
||||
$output = [];
|
||||
|
||||
unset($rawFavorites['data']);
|
||||
|
||||
foreach ($rawFavorites as $item)
|
||||
{
|
||||
$rank = $item['attributes']['favRank'];
|
||||
foreach ($item['relationships']['item'] as $key => $fav)
|
||||
{
|
||||
$output[$key] = $output[$key] ?? [];
|
||||
foreach ($fav as $id => $data)
|
||||
{
|
||||
$output[$key][$rank] = array_merge(['id' => $id], $data['attributes']);
|
||||
}
|
||||
|
||||
ksort($output[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the time spent on anime in a more readable format
|
||||
*
|
||||
* @param int $seconds
|
||||
* @return string
|
||||
*/
|
||||
private function formatAnimeTime(int $seconds): string
|
||||
{
|
||||
// All the seconds left
|
||||
$remSeconds = $seconds % 60;
|
||||
$minutes = ($seconds - $remSeconds) / 60;
|
||||
|
||||
$minutesPerDay = 1440;
|
||||
$minutesPerYear = $minutesPerDay * 365;
|
||||
|
||||
// Minutes short of a year
|
||||
$years = (int)floor($minutes / $minutesPerYear);
|
||||
$minutes %= $minutesPerYear;
|
||||
|
||||
// Minutes short of a day
|
||||
$extraMinutes = $minutes % $minutesPerDay;
|
||||
$days = ($minutes - $extraMinutes) / $minutesPerDay;
|
||||
|
||||
// Minutes short of an hour
|
||||
$remMinutes = $extraMinutes % 60;
|
||||
$hours = ($extraMinutes - $remMinutes) / 60;
|
||||
|
||||
$output = "{$days} days, {$hours} hours, {$remMinutes} minutes, and {$remSeconds} seconds.";
|
||||
|
||||
if ($years > 0)
|
||||
{
|
||||
$output = "{$years} year(s),{$output}";
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function organizeStats($stats, $data = []): array
|
||||
{
|
||||
$animeStats = [];
|
||||
$mangaStats = [];
|
||||
$otherStats = [];
|
||||
|
||||
if (array_key_exists('anime-amount-consumed', $stats))
|
||||
{
|
||||
$animeStats = [
|
||||
'Time spent watching anime:' => $this->formatAnimeTime($stats['anime-amount-consumed']['time']),
|
||||
'Anime series watched:' => number_format($stats['anime-amount-consumed']['media']),
|
||||
'Anime episodes watched:' => number_format($stats['anime-amount-consumed']['units']),
|
||||
];
|
||||
}
|
||||
|
||||
if (array_key_exists('manga-amount-consumed', $stats))
|
||||
{
|
||||
$mangaStats = [
|
||||
'Manga series read:' => number_format($stats['manga-amount-consumed']['media']),
|
||||
'Manga chapters read:' => number_format($stats['manga-amount-consumed']['units']),
|
||||
];
|
||||
}
|
||||
|
||||
if ( ! empty($data))
|
||||
{
|
||||
$otherStats = [
|
||||
'Posts:' => number_format($data['postsCount']),
|
||||
'Comments:' => number_format($data['commentsCount']),
|
||||
'Media Rated:' => number_format($data['ratingsCount']),
|
||||
];
|
||||
}
|
||||
|
||||
return array_merge($animeStats, $mangaStats, $otherStats);
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ use Aviat\Ion\Enum;
|
||||
* and url route segments
|
||||
*/
|
||||
final class AnimeWatchingStatus extends Enum {
|
||||
const ANILIST_TO_KITSU = [
|
||||
public const ANILIST_TO_KITSU = [
|
||||
Anilist::WATCHING => Kitsu::WATCHING,
|
||||
Anilist::PLAN_TO_WATCH => Kitsu::PLAN_TO_WATCH,
|
||||
Anilist::COMPLETED => Kitsu::COMPLETED,
|
||||
@ -32,7 +32,7 @@ final class AnimeWatchingStatus extends Enum {
|
||||
Anilist::DROPPED => Kitsu::DROPPED
|
||||
];
|
||||
|
||||
const KITSU_TO_ANILIST = [
|
||||
public const KITSU_TO_ANILIST = [
|
||||
Kitsu::WATCHING => Anilist::WATCHING,
|
||||
Kitsu::PLAN_TO_WATCH => Anilist::PLAN_TO_WATCH,
|
||||
Kitsu::COMPLETED => Anilist::COMPLETED,
|
||||
@ -40,7 +40,7 @@ final class AnimeWatchingStatus extends Enum {
|
||||
Kitsu::DROPPED => Anilist::DROPPED
|
||||
];
|
||||
|
||||
const KITSU_TO_TITLE = [
|
||||
public const KITSU_TO_TITLE = [
|
||||
Kitsu::WATCHING => Title::WATCHING,
|
||||
Kitsu::PLAN_TO_WATCH => Title::PLAN_TO_WATCH,
|
||||
Kitsu::ON_HOLD => Title::ON_HOLD,
|
||||
@ -48,7 +48,7 @@ final class AnimeWatchingStatus extends Enum {
|
||||
Kitsu::COMPLETED => Title::COMPLETED
|
||||
];
|
||||
|
||||
const ROUTE_TO_KITSU = [
|
||||
public const ROUTE_TO_KITSU = [
|
||||
Route::WATCHING => Kitsu::WATCHING,
|
||||
Route::PLAN_TO_WATCH => Kitsu::PLAN_TO_WATCH,
|
||||
Route::ON_HOLD => Kitsu::ON_HOLD,
|
||||
@ -56,7 +56,7 @@ final class AnimeWatchingStatus extends Enum {
|
||||
Route::COMPLETED => Kitsu::COMPLETED
|
||||
];
|
||||
|
||||
const ROUTE_TO_TITLE = [
|
||||
public const ROUTE_TO_TITLE = [
|
||||
Route::ALL => Title::ALL,
|
||||
Route::WATCHING => Title::WATCHING,
|
||||
Route::PLAN_TO_WATCH => Title::PLAN_TO_WATCH,
|
||||
@ -65,7 +65,7 @@ final class AnimeWatchingStatus extends Enum {
|
||||
Route::COMPLETED => Title::COMPLETED
|
||||
];
|
||||
|
||||
const TITLE_TO_ROUTE = [
|
||||
public const TITLE_TO_ROUTE = [
|
||||
Title::ALL => Route::ALL,
|
||||
Title::WATCHING => Route::WATCHING,
|
||||
Title::PLAN_TO_WATCH => Route::PLAN_TO_WATCH,
|
||||
|
@ -24,7 +24,7 @@ use Aviat\Ion\Enum;
|
||||
* and url route segments
|
||||
*/
|
||||
final class MangaReadingStatus extends Enum {
|
||||
const ANILIST_TO_KITSU = [
|
||||
public const ANILIST_TO_KITSU = [
|
||||
Anilist::READING => Kitsu::READING,
|
||||
Anilist::PLAN_TO_READ => Kitsu::PLAN_TO_READ,
|
||||
Anilist::COMPLETED => Kitsu::COMPLETED,
|
||||
@ -32,7 +32,7 @@ final class MangaReadingStatus extends Enum {
|
||||
Anilist::DROPPED => Kitsu::DROPPED
|
||||
];
|
||||
|
||||
const KITSU_TO_ANILIST = [
|
||||
public const KITSU_TO_ANILIST = [
|
||||
Kitsu::READING => Anilist::READING,
|
||||
Kitsu::PLAN_TO_READ => Anilist::PLAN_TO_READ,
|
||||
Kitsu::COMPLETED => Anilist::COMPLETED,
|
||||
@ -40,7 +40,7 @@ final class MangaReadingStatus extends Enum {
|
||||
Kitsu::DROPPED => Anilist::DROPPED
|
||||
];
|
||||
|
||||
const KITSU_TO_TITLE = [
|
||||
public const KITSU_TO_TITLE = [
|
||||
Kitsu::READING => Title::READING,
|
||||
Kitsu::PLAN_TO_READ => Title::PLAN_TO_READ,
|
||||
Kitsu::COMPLETED => Title::COMPLETED,
|
||||
@ -48,7 +48,7 @@ final class MangaReadingStatus extends Enum {
|
||||
Kitsu::DROPPED => Title::DROPPED,
|
||||
];
|
||||
|
||||
const ROUTE_TO_KITSU = [
|
||||
public const ROUTE_TO_KITSU = [
|
||||
Route::PLAN_TO_READ => Kitsu::PLAN_TO_READ,
|
||||
Route::READING => Kitsu::READING,
|
||||
Route::COMPLETED => Kitsu::COMPLETED,
|
||||
@ -56,7 +56,7 @@ final class MangaReadingStatus extends Enum {
|
||||
Route::ON_HOLD => Kitsu::ON_HOLD,
|
||||
];
|
||||
|
||||
const ROUTE_TO_TITLE = [
|
||||
public const ROUTE_TO_TITLE = [
|
||||
Route::ALL => Title::ALL,
|
||||
Route::PLAN_TO_READ => Title::PLAN_TO_READ,
|
||||
Route::READING => Title::READING,
|
||||
@ -65,7 +65,7 @@ final class MangaReadingStatus extends Enum {
|
||||
Route::ON_HOLD => Title::ON_HOLD,
|
||||
];
|
||||
|
||||
const TITLE_TO_KITSU = [
|
||||
public const TITLE_TO_KITSU = [
|
||||
Title::PLAN_TO_READ => Kitsu::PLAN_TO_READ,
|
||||
Title::READING => Kitsu::READING,
|
||||
Title::COMPLETED => Kitsu::COMPLETED,
|
||||
|
@ -18,6 +18,7 @@ namespace Aviat\AnimeClient\API;
|
||||
|
||||
use function Amp\call;
|
||||
use function Amp\Promise\{all, wait};
|
||||
use function Aviat\AnimeClient\getApiClient;
|
||||
|
||||
/**
|
||||
* Class to simplify making and validating simultaneous requests
|
||||
@ -70,7 +71,8 @@ final class ParallelAPIRequest {
|
||||
*/
|
||||
public function makeRequests(): array
|
||||
{
|
||||
$client = new HummingbirdClient();
|
||||
$client = getApiClient();
|
||||
|
||||
$promises = [];
|
||||
|
||||
foreach ($this->requests as $key => $url)
|
||||
@ -92,7 +94,8 @@ final class ParallelAPIRequest {
|
||||
*/
|
||||
public function getResponses(): array
|
||||
{
|
||||
$client = new HummingbirdClient();
|
||||
$client = getApiClient();
|
||||
|
||||
$promises = [];
|
||||
|
||||
foreach ($this->requests as $key => $url)
|
||||
|
@ -16,6 +16,10 @@
|
||||
|
||||
namespace Aviat\AnimeClient;
|
||||
|
||||
use function Amp\Promise\wait;
|
||||
|
||||
use Amp\Artax\{Client, DefaultClient, Response};
|
||||
|
||||
use Aviat\Ion\ConfigInterface;
|
||||
use Yosymfony\Toml\{Toml, TomlBuilder};
|
||||
|
||||
@ -203,6 +207,37 @@ function checkFolderPermissions(ConfigInterface $config): array
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an API Client, with better defaults
|
||||
*
|
||||
* @return DefaultClient
|
||||
*/
|
||||
function getApiClient ()
|
||||
{
|
||||
static $client;
|
||||
|
||||
if ($client === NULL)
|
||||
{
|
||||
$client = new DefaultClient;
|
||||
$client->setOption(Client::OP_TRANSFER_TIMEOUT, 0);
|
||||
}
|
||||
|
||||
return $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplify making a request with Artax
|
||||
*
|
||||
* @param $request
|
||||
* @return Response
|
||||
* @throws \Throwable
|
||||
*/
|
||||
function getResponse ($request): Response
|
||||
{
|
||||
$client = getApiClient();
|
||||
return wait($client->request($request));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the path for the cached image from the original image
|
||||
*
|
||||
@ -246,7 +281,7 @@ function getLocalImg ($kitsuUrl, $webp = TRUE): string
|
||||
* @param int $height
|
||||
* @param string $text
|
||||
*/
|
||||
function createPlaceholderImage ($path, $width, $height, $text = 'Image Unavailable')
|
||||
function createPlaceholderImage ($path, $width, $height, $text = 'Image Unavailable'): void
|
||||
{
|
||||
$width = $width ?? 200;
|
||||
$height = $height ?? 200;
|
||||
@ -268,7 +303,7 @@ function createPlaceholderImage ($path, $width, $height, $text = 'Image Unavaila
|
||||
$fontSize = 10;
|
||||
$fontWidth = imagefontwidth($fontSize);
|
||||
$fontHeight = imagefontheight($fontSize);
|
||||
$length = strlen($text);
|
||||
$length = \strlen($text);
|
||||
$textWidth = $length * $fontWidth;
|
||||
$fxPos = (int) ceil((imagesx($img) - $textWidth) / 2);
|
||||
$fyPos = (int) ceil((imagesy($img) - $fontHeight) / 2);
|
||||
|
@ -21,13 +21,9 @@ use function Aviat\AnimeClient\loadTomlFile;
|
||||
|
||||
use Aura\Router\RouterContainer;
|
||||
use Aura\Session\SessionFactory;
|
||||
use Aviat\AnimeClient\UrlGenerator;
|
||||
use Aviat\AnimeClient\Util;
|
||||
use Aviat\AnimeClient\API\CacheTrait;
|
||||
use Aviat\AnimeClient\API\Anilist;
|
||||
use Aviat\AnimeClient\API\Kitsu;
|
||||
use Aviat\AnimeClient\{Model, UrlGenerator, Util};
|
||||
use Aviat\AnimeClient\API\{Anilist, CacheTrait, Kitsu};
|
||||
use Aviat\AnimeClient\API\Kitsu\KitsuRequestBuilder;
|
||||
use Aviat\AnimeClient\Model;
|
||||
use Aviat\Banker\Pool;
|
||||
use Aviat\Ion\Config;
|
||||
use Aviat\Ion\Di\{Container, ContainerAware};
|
||||
|
@ -27,7 +27,7 @@ class ClearThumbnails extends BaseCommand {
|
||||
$this->echoBox('All cached images have been removed');
|
||||
}
|
||||
|
||||
public function clearThumbs()
|
||||
private function clearThumbs(): void
|
||||
{
|
||||
$imgDir = realpath(__DIR__ . '/../../public/images');
|
||||
|
||||
|
@ -264,6 +264,11 @@ final class SyncLists extends BaseCommand {
|
||||
|
||||
foreach ($potentialMappings as $mappingId)
|
||||
{
|
||||
if (\is_array($mappingId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (array_key_exists($mappingId, $includes['mappings']))
|
||||
{
|
||||
$malId = $includes['mappings'][$mappingId]['externalId'];
|
||||
@ -505,12 +510,16 @@ final class SyncLists extends BaseCommand {
|
||||
// Use the first set rating, otherwise use the newer rating
|
||||
if ( ! $sameRating)
|
||||
{
|
||||
if ($kitsuItem['data']['rating'] !== 0 && $dateDiff === 1)
|
||||
if (
|
||||
$dateDiff === 1 &&
|
||||
$kitsuItem['data']['rating'] !== 0 &&
|
||||
$kitsuItem['data']['ratingTwenty'] !== 0
|
||||
)
|
||||
{
|
||||
$update['data']['ratingTwenty'] = $kitsuItem['data']['ratingTwenty'];
|
||||
$return['updateType'][] = 'anilist';
|
||||
}
|
||||
else if($dateDiff === -1)
|
||||
else if($dateDiff === -1 && $anilistItem['data']['rating'] !== 0)
|
||||
{
|
||||
$update['data']['ratingTwenty'] = $anilistItem['data']['rating'] * 2;
|
||||
$return['updateType'][] = 'kitsu';
|
||||
@ -547,6 +556,12 @@ final class SyncLists extends BaseCommand {
|
||||
}
|
||||
}
|
||||
|
||||
// No changes? Let's bail!
|
||||
if (empty($return['updateType']))
|
||||
{
|
||||
return $return;
|
||||
}
|
||||
|
||||
$return['meta'] = [
|
||||
'kitsu' => $kitsuItem['data'],
|
||||
'anilist' => $anilistItem['data'],
|
||||
|
@ -183,7 +183,7 @@ class Controller {
|
||||
* @throws \Aviat\Ion\Di\Exception\NotFoundException
|
||||
* @return void
|
||||
*/
|
||||
public function sessionRedirect()
|
||||
public function sessionRedirect(): void
|
||||
{
|
||||
$target = $this->session->get('redirect_url');
|
||||
if (empty($target))
|
||||
@ -208,7 +208,7 @@ class Controller {
|
||||
* @throws \Aviat\Ion\Di\Exception\NotFoundException
|
||||
* @return string
|
||||
*/
|
||||
protected function loadPartial($view, string $template, array $data = [])
|
||||
protected function loadPartial($view, string $template, array $data = []): string
|
||||
{
|
||||
$router = $this->container->get('dispatcher');
|
||||
|
||||
@ -242,7 +242,7 @@ class Controller {
|
||||
* @throws \Aviat\Ion\Di\Exception\NotFoundException
|
||||
* @return void
|
||||
*/
|
||||
protected function renderFullPage($view, string $template, array $data)
|
||||
protected function renderFullPage($view, string $template, array $data): void
|
||||
{
|
||||
$csp = [
|
||||
"default-src 'self'",
|
||||
@ -275,7 +275,7 @@ class Controller {
|
||||
public function notFound(
|
||||
string $title = 'Sorry, page not found',
|
||||
string $message = 'Page Not Found'
|
||||
)
|
||||
): void
|
||||
{
|
||||
$this->outputHTML('404', [
|
||||
'title' => $title,
|
||||
@ -383,7 +383,7 @@ class Controller {
|
||||
* @throws \Aviat\Ion\Di\Exception\NotFoundException
|
||||
* @return void
|
||||
*/
|
||||
protected function outputHTML(string $template, array $data = [], $view = NULL, int $code = 200)
|
||||
protected function outputHTML(string $template, array $data = [], $view = NULL, int $code = 200): void
|
||||
{
|
||||
if (null === $view)
|
||||
{
|
||||
|
@ -23,15 +23,11 @@ use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
|
||||
use Aviat\AnimeClient\Types\FormItem;
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\Json;
|
||||
use Aviat\Ion\StringWrapper;
|
||||
|
||||
/**
|
||||
* Controller for Anime-related pages
|
||||
*/
|
||||
final class Anime extends BaseController {
|
||||
|
||||
use StringWrapper;
|
||||
|
||||
/**
|
||||
* The anime list model
|
||||
* @var \Aviat\AnimeClient\Model\Anime $model
|
||||
@ -276,8 +272,6 @@ final class Anime extends BaseController {
|
||||
public function details(string $animeId): void
|
||||
{
|
||||
$data = $this->model->getAnime($animeId);
|
||||
$characters = [];
|
||||
$staff = [];
|
||||
|
||||
if (empty($data))
|
||||
{
|
||||
@ -291,77 +285,13 @@ final class Anime extends BaseController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (array_key_exists('animeCharacters', $data['included']))
|
||||
{
|
||||
$animeCharacters = $data['included']['animeCharacters'];
|
||||
|
||||
foreach ($animeCharacters as $rel)
|
||||
{
|
||||
$charId = $rel['relationships']['character']['data']['id'];
|
||||
$role = $rel['role'];
|
||||
|
||||
if (array_key_exists($charId, $data['included']['characters']))
|
||||
{
|
||||
$characters[$role][$charId] = $data['included']['characters'][$charId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('mediaStaff', $data['included']))
|
||||
{
|
||||
foreach ($data['included']['mediaStaff'] as $id => $staffing)
|
||||
{
|
||||
$personId = $staffing['relationships']['person']['data']['id'];
|
||||
$personDetails = $data['included']['people'][$personId];
|
||||
|
||||
$role = $staffing['role'];
|
||||
|
||||
if ( ! array_key_exists($role, $staff))
|
||||
{
|
||||
$staff[$role] = [];
|
||||
}
|
||||
|
||||
$staff[$role][$personId] = [
|
||||
'id' => $personId,
|
||||
'name' => $personDetails['name'] ?? '??',
|
||||
'image' => $personDetails['image'],
|
||||
];
|
||||
|
||||
usort($staff[$role], function ($a, $b) {
|
||||
return $a['name'] <=> $b['name'];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty($characters['main']))
|
||||
{
|
||||
uasort($characters['main'], function ($a, $b) {
|
||||
return $a['name'] <=> $b['name'];
|
||||
});
|
||||
}
|
||||
|
||||
if ( ! empty($characters['supporting']))
|
||||
{
|
||||
uasort($characters['supporting'], function ($a, $b) {
|
||||
return $a['name'] <=> $b['name'];
|
||||
});
|
||||
}
|
||||
|
||||
ksort($characters);
|
||||
ksort($staff);
|
||||
|
||||
// dump($characters);
|
||||
// dump($staff);
|
||||
|
||||
$this->outputHTML('anime/details', [
|
||||
'title' => $this->formatTitle(
|
||||
$this->config->get('whose_list') . "'s Anime List",
|
||||
'Anime',
|
||||
$data->title
|
||||
),
|
||||
'characters' => $characters,
|
||||
'show_data' => $data,
|
||||
'staff' => $staff,
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,7 @@ final class AnimeCollection extends BaseController {
|
||||
* @throws \Aviat\Ion\Exception\DoubleRenderException
|
||||
* @return void
|
||||
*/
|
||||
public function search()
|
||||
public function search(): void
|
||||
{
|
||||
$queryParams = $this->request->getQueryParams();
|
||||
$query = $queryParams['query'];
|
||||
@ -84,7 +84,7 @@ final class AnimeCollection extends BaseController {
|
||||
* @throws \InvalidArgumentException
|
||||
* @return void
|
||||
*/
|
||||
public function index($view)
|
||||
public function index($view): void
|
||||
{
|
||||
$viewMap = [
|
||||
'' => 'cover',
|
||||
@ -110,7 +110,7 @@ final class AnimeCollection extends BaseController {
|
||||
* @throws \InvalidArgumentException
|
||||
* @return void
|
||||
*/
|
||||
public function form($id = NULL)
|
||||
public function form($id = NULL): void
|
||||
{
|
||||
$this->setSessionRedirect();
|
||||
|
||||
@ -137,7 +137,7 @@ final class AnimeCollection extends BaseController {
|
||||
* @throws \InvalidArgumentException
|
||||
* @return void
|
||||
*/
|
||||
public function edit()
|
||||
public function edit(): void
|
||||
{
|
||||
$data = $this->request->getParsedBody();
|
||||
if (array_key_exists('hummingbird_id', $data))
|
||||
@ -161,7 +161,7 @@ final class AnimeCollection extends BaseController {
|
||||
* @throws \InvalidArgumentException
|
||||
* @return void
|
||||
*/
|
||||
public function add()
|
||||
public function add(): void
|
||||
{
|
||||
$data = $this->request->getParsedBody();
|
||||
if (array_key_exists('id', $data))
|
||||
@ -182,7 +182,7 @@ final class AnimeCollection extends BaseController {
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete()
|
||||
public function delete(): void
|
||||
{
|
||||
$data = $this->request->getParsedBody();
|
||||
if ( ! array_key_exists('hummingbird_id', $data))
|
||||
|
@ -17,30 +17,42 @@
|
||||
namespace Aviat\AnimeClient\Controller;
|
||||
|
||||
use Aviat\AnimeClient\Controller as BaseController;
|
||||
use Aviat\AnimeClient\API\JsonAPI;
|
||||
use Aviat\Ion\ArrayWrapper;
|
||||
use Aviat\AnimeClient\API\Kitsu\Transformer\CharacterTransformer;
|
||||
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Controller for character description pages
|
||||
*/
|
||||
class Character extends BaseController {
|
||||
|
||||
use ArrayWrapper;
|
||||
/**
|
||||
* @var \Aviat\AnimeClient\API\Kitsu\Model
|
||||
*/
|
||||
private $model;
|
||||
|
||||
/**
|
||||
* Character constructor.
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @throws \Aviat\Ion\Di\Exception\ContainerException
|
||||
* @throws \Aviat\Ion\Di\Exception\NotFoundException
|
||||
*/
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->model = $container->get('kitsu-model');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show information about a character
|
||||
*
|
||||
* @param string $slug
|
||||
* @throws \Aviat\Ion\Di\ContainerException
|
||||
* @throws \Aviat\Ion\Di\NotFoundException
|
||||
* @throws \InvalidArgumentException
|
||||
* @return void
|
||||
*/
|
||||
public function index(string $slug): void
|
||||
{
|
||||
$model = $this->container->get('kitsu-model');
|
||||
|
||||
$rawData = $model->getCharacter($slug);
|
||||
$rawData = $this->model->getCharacter($slug);
|
||||
|
||||
if (( ! array_key_exists('data', $rawData)) || empty($rawData['data']))
|
||||
{
|
||||
@ -55,167 +67,14 @@ class Character extends BaseController {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = JsonAPI::organizeData($rawData);
|
||||
$data = (new CharacterTransformer())->transform($rawData)->toArray();
|
||||
|
||||
$data['names'] = array_unique(
|
||||
array_merge(
|
||||
[ $data[0]['attributes']['canonicalName'] ],
|
||||
$data[0]['attributes']['names']
|
||||
)
|
||||
);
|
||||
$data['name'] = array_shift($data['names']);
|
||||
|
||||
if (array_key_exists('included', $data))
|
||||
{
|
||||
if (array_key_exists('anime', $data['included']))
|
||||
{
|
||||
uasort($data['included']['anime'], function ($a, $b) {
|
||||
return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle'];
|
||||
});
|
||||
}
|
||||
|
||||
if (array_key_exists('manga', $data['included']))
|
||||
{
|
||||
uasort($data['included']['manga'], function ($a, $b) {
|
||||
return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle'];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$viewData = [
|
||||
$this->outputHTML('character/details', [
|
||||
'title' => $this->formatTitle(
|
||||
'Characters',
|
||||
$data[0]['attributes']['name']
|
||||
$data['name']
|
||||
),
|
||||
'data' => $data,
|
||||
'castCount' => 0,
|
||||
'castings' => []
|
||||
];
|
||||
|
||||
if (array_key_exists('included', $data))
|
||||
{
|
||||
if (array_key_exists('castings', $data['included']))
|
||||
{
|
||||
$viewData['castings'] = $this->organizeCast($data['included']['castings']);
|
||||
$viewData['castCount'] = $this->getCastCount($viewData['castings']);
|
||||
}
|
||||
}
|
||||
|
||||
$this->outputHTML('character/details', $viewData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Organize VA => anime relationships
|
||||
*
|
||||
* @param array $cast
|
||||
* @return array
|
||||
*/
|
||||
private function dedupeCast(array $cast): array
|
||||
{
|
||||
$output = [];
|
||||
$people = [];
|
||||
|
||||
$i = 0;
|
||||
foreach ($cast as &$role)
|
||||
{
|
||||
if (empty($role['attributes']['role']))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
$person = current($role['relationships']['person']['people'])['attributes'];
|
||||
$hasName = array_key_exists($person['name'], $people);
|
||||
|
||||
if ( ! $hasName)
|
||||
{
|
||||
$people[$person['name']] = $i;
|
||||
$role['relationships']['media']['anime'] = [current($role['relationships']['media']['anime'])];
|
||||
$output[$i] = $role;
|
||||
|
||||
$i++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (array_key_exists('anime', $role['relationships']['media']))
|
||||
{
|
||||
$key = $people[$person['name']];
|
||||
$output[$key]['relationships']['media']['anime'][] = current($role['relationships']['media']['anime']);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
protected function getCastCount(array $cast): int
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
foreach($cast as $role)
|
||||
{
|
||||
$count++;
|
||||
/* if (
|
||||
array_key_exists('attributes', $role) &&
|
||||
array_key_exists('role', $role['attributes']) &&
|
||||
$role['attributes']['role'] !== NULL
|
||||
) {
|
||||
$count++;
|
||||
} */
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
protected function organizeCast(array $cast): array
|
||||
{
|
||||
$cast = $this->dedupeCast($cast);
|
||||
$output = [];
|
||||
|
||||
foreach($cast as $id => $role)
|
||||
{
|
||||
if (empty($role['attributes']['role']))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$language = $role['attributes']['language'];
|
||||
$roleName = $role['attributes']['role'];
|
||||
$isVA = $role['attributes']['voiceActor'];
|
||||
|
||||
if ($isVA)
|
||||
{
|
||||
foreach($role['relationships']['person']['people'] as $pid => $peoples)
|
||||
{
|
||||
$p = $peoples;
|
||||
}
|
||||
|
||||
$person = $p['attributes'];
|
||||
$person['id'] = $pid;
|
||||
$person['image'] = $person['image']['original'];
|
||||
|
||||
uasort($role['relationships']['media']['anime'], function ($a, $b) {
|
||||
return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle'];
|
||||
});
|
||||
|
||||
$item = [
|
||||
'person' => $person,
|
||||
'series' => $role['relationships']['media']['anime']
|
||||
];
|
||||
|
||||
$output[$roleName][$language][] = $item;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach($role['relationships']['person']['people'] as $pid => $person)
|
||||
{
|
||||
$person['id'] = $pid;
|
||||
$output[$roleName][$pid] = $person;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
]);
|
||||
}
|
||||
}
|
@ -16,13 +16,11 @@
|
||||
|
||||
namespace Aviat\AnimeClient\Controller;
|
||||
|
||||
use function Aviat\AnimeClient\createPlaceholderImage;
|
||||
use function Amp\Promise\wait;
|
||||
use function Aviat\AnimeClient\getResponse;
|
||||
use function Aviat\AnimeClient\createPlaceholderImage;
|
||||
|
||||
use Aviat\AnimeClient\Controller as BaseController;
|
||||
use Aviat\AnimeClient\API\{HummingbirdClient, JsonAPI};
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\View\HtmlView;
|
||||
|
||||
/**
|
||||
* Controller for handling routes that don't fit elsewhere
|
||||
@ -55,7 +53,7 @@ final class Images extends BaseController {
|
||||
// Kitsu doesn't serve webp, but for most use cases,
|
||||
// jpg is a safe assumption
|
||||
$tryJpg = ['anime','characters','manga','people'];
|
||||
if ($ext === 'webp' && in_array($type, $tryJpg, TRUE))
|
||||
if ($ext === 'webp' && \in_array($type, $tryJpg, TRUE))
|
||||
{
|
||||
$ext = 'jpg';
|
||||
$currentUrl = str_replace('webp', 'jpg', $currentUrl);
|
||||
@ -102,8 +100,7 @@ final class Images extends BaseController {
|
||||
$height = $imageType['height'];
|
||||
$filePrefix = "{$baseSavePath}/{$type}/{$id}";
|
||||
|
||||
$promise = (new HummingbirdClient)->request($kitsuUrl);
|
||||
$response = wait($promise);
|
||||
$response = getResponse($kitsuUrl);
|
||||
|
||||
if ($response->getStatus() !== 200)
|
||||
{
|
||||
@ -143,7 +140,7 @@ final class Images extends BaseController {
|
||||
if ($ext === 'gif')
|
||||
{
|
||||
file_put_contents("{$filePrefix}.gif", $data);
|
||||
imagepalletetotruecolor($gdImg);
|
||||
\imagepalletetotruecolor($gdImg);
|
||||
}
|
||||
|
||||
// save the webp versions
|
||||
@ -162,7 +159,7 @@ final class Images extends BaseController {
|
||||
if ($display)
|
||||
{
|
||||
$contentType = ($ext === 'webp')
|
||||
? "image/webp"
|
||||
? 'image/webp'
|
||||
: $response->getHeader('content-type')[0];
|
||||
|
||||
$outputFile = (strpos($file, '-original') !== FALSE)
|
||||
|
@ -294,65 +294,6 @@ final class Manga extends Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
if (array_key_exists('mediaCharacters', $data['included']))
|
||||
{
|
||||
$mediaCharacters = $data['included']['mediaCharacters'];
|
||||
|
||||
foreach ($mediaCharacters as $rel)
|
||||
{
|
||||
// dd($rel);
|
||||
// $charId = $rel['relationships']['character']['data']['id'];
|
||||
$role = $rel['attributes']['role'];
|
||||
|
||||
foreach($rel['relationships']['character']['characters'] as $charId => $char)
|
||||
{
|
||||
if (array_key_exists($charId, $data['included']['characters']))
|
||||
{
|
||||
$characters[$role][$charId] = $char['attributes'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('mediaStaff', $data['included']))
|
||||
{
|
||||
foreach ($data['included']['mediaStaff'] as $id => $staffing)
|
||||
{
|
||||
$role = $staffing['attributes']['role'];
|
||||
|
||||
foreach($staffing['relationships']['person']['people'] as $personId => $personDetails)
|
||||
{
|
||||
if ( ! array_key_exists($role, $staff))
|
||||
{
|
||||
$staff[$role] = [];
|
||||
}
|
||||
|
||||
$staff[$role][$personId] = [
|
||||
'id' => $personId,
|
||||
'name' => $personDetails['attributes']['name'] ?? '??',
|
||||
'image' => $personDetails['attributes']['image'],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty($characters['main']))
|
||||
{
|
||||
uasort($characters['main'], function ($a, $b) {
|
||||
return $a['name'] <=> $b['name'];
|
||||
});
|
||||
}
|
||||
|
||||
if ( ! empty($characters['supporting']))
|
||||
{
|
||||
uasort($characters['supporting'], function ($a, $b) {
|
||||
return $a['name'] <=> $b['name'];
|
||||
});
|
||||
}
|
||||
|
||||
ksort($characters);
|
||||
ksort($staff);
|
||||
|
||||
$this->outputHTML('manga/details', [
|
||||
'title' => $this->formatTitle(
|
||||
$this->config->get('whose_list') . "'s Manga List",
|
||||
|
@ -138,7 +138,7 @@ final class MangaCollection extends BaseController {
|
||||
* @throws \InvalidArgumentException
|
||||
* @return void
|
||||
*/
|
||||
public function edit()
|
||||
public function edit(): void
|
||||
{
|
||||
$data = $this->request->getParsedBody();
|
||||
if (array_key_exists('hummingbird_id', $data))
|
||||
@ -162,7 +162,7 @@ final class MangaCollection extends BaseController {
|
||||
* @throws \InvalidArgumentException
|
||||
* @return void
|
||||
*/
|
||||
public function add()
|
||||
public function add(): void
|
||||
{
|
||||
$data = $this->request->getParsedBody();
|
||||
if (array_key_exists('id', $data))
|
||||
@ -183,7 +183,7 @@ final class MangaCollection extends BaseController {
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete()
|
||||
public function delete(): void
|
||||
{
|
||||
$data = $this->request->getParsedBody();
|
||||
if ( ! array_key_exists('hummingbird_id', $data))
|
||||
|
@ -17,7 +17,6 @@
|
||||
namespace Aviat\AnimeClient\Controller;
|
||||
|
||||
use Aviat\AnimeClient\Controller as BaseController;
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\View\HtmlView;
|
||||
|
||||
/**
|
||||
@ -29,7 +28,7 @@ final class Misc extends BaseController {
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearCache()
|
||||
public function clearCache(): void
|
||||
{
|
||||
$this->cache->clear();
|
||||
$this->outputHTML('blank', [
|
||||
@ -43,7 +42,7 @@ final class Misc extends BaseController {
|
||||
* @param string $status
|
||||
* @return void
|
||||
*/
|
||||
public function login(string $status = '')
|
||||
public function login(string $status = ''): void
|
||||
{
|
||||
$message = '';
|
||||
|
||||
@ -68,7 +67,7 @@ final class Misc extends BaseController {
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function loginAction()
|
||||
public function loginAction(): void
|
||||
{
|
||||
$auth = $this->container->get('auth');
|
||||
$post = $this->request->getParsedBody();
|
||||
@ -88,7 +87,7 @@ final class Misc extends BaseController {
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function logout()
|
||||
public function logout(): void
|
||||
{
|
||||
$auth = $this->container->get('auth');
|
||||
$auth->logout();
|
||||
|
@ -17,12 +17,33 @@
|
||||
namespace Aviat\AnimeClient\Controller;
|
||||
|
||||
use Aviat\AnimeClient\Controller as BaseController;
|
||||
use Aviat\AnimeClient\API\JsonAPI;
|
||||
use Aviat\AnimeClient\API\Kitsu\Transformer\PersonTransformer;
|
||||
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Controller for People pages
|
||||
*/
|
||||
final class People extends BaseController {
|
||||
|
||||
/**
|
||||
* @var \Aviat\AnimeClient\API\Kitsu\Model
|
||||
*/
|
||||
private $model;
|
||||
|
||||
/**
|
||||
* People constructor.
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @throws \Aviat\Ion\Di\Exception\ContainerException
|
||||
* @throws \Aviat\Ion\Di\Exception\NotFoundException
|
||||
*/
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->model = $container->get('kitsu-model');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show information about a person
|
||||
*
|
||||
@ -31,9 +52,8 @@ final class People extends BaseController {
|
||||
*/
|
||||
public function index(string $id): void
|
||||
{
|
||||
$model = $this->container->get('kitsu-model');
|
||||
|
||||
$rawData = $model->getPerson($id);
|
||||
$rawData = $this->model->getPerson($id);
|
||||
$data = (new PersonTransformer())->transform($rawData)->toArray();
|
||||
|
||||
if (( ! array_key_exists('data', $rawData)) || empty($rawData['data']))
|
||||
{
|
||||
@ -48,114 +68,12 @@ final class People extends BaseController {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = JsonAPI::organizeData($rawData);
|
||||
$included = JsonAPI::organizeIncludes($rawData['included']);
|
||||
|
||||
$orgData = $this->organizeData($included);
|
||||
|
||||
$viewData = [
|
||||
'included' => $included,
|
||||
$this->outputHTML('person/details', [
|
||||
'title' => $this->formatTitle(
|
||||
'People',
|
||||
$data['attributes']['name']
|
||||
$data['name']
|
||||
),
|
||||
'data' => $data,
|
||||
'castCount' => 0,
|
||||
'castings' => [],
|
||||
'characters' => $orgData['characters'],
|
||||
'staff' => $orgData['staff'],
|
||||
];
|
||||
|
||||
$this->outputHTML('person/details', $viewData);
|
||||
}
|
||||
|
||||
protected function organizeData(array $data): array
|
||||
{
|
||||
$output = [
|
||||
'characters' => [
|
||||
'main' => [],
|
||||
'supporting' => [],
|
||||
],
|
||||
'staff' => [],
|
||||
];
|
||||
|
||||
if (array_key_exists('characterVoices', $data))
|
||||
{
|
||||
foreach ($data['characterVoices'] as $cv)
|
||||
{
|
||||
$mcId = $cv['relationships']['mediaCharacter']['data']['id'];
|
||||
|
||||
if ( ! array_key_exists($mcId, $data['mediaCharacters']))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$mc = $data['mediaCharacters'][$mcId];
|
||||
|
||||
$role = $mc['role'];
|
||||
|
||||
$charId = $mc['relationships']['character']['data']['id'];
|
||||
$mediaId = $mc['relationships']['media']['data']['id'];
|
||||
|
||||
$existingMedia = array_key_exists($charId, $output['characters'][$role])
|
||||
? $output['characters'][$role][$charId]['media']
|
||||
: [];
|
||||
|
||||
$relatedMedia = [
|
||||
$mediaId => $data['anime'][$mediaId],
|
||||
];
|
||||
|
||||
$includedMedia = array_replace_recursive($existingMedia, $relatedMedia);
|
||||
|
||||
uasort($includedMedia, function ($a, $b) {
|
||||
return $a['canonicalTitle'] <=> $b['canonicalTitle'];
|
||||
});
|
||||
|
||||
$character = $data['characters'][$charId];
|
||||
|
||||
$output['characters'][$role][$charId] = [
|
||||
'character' => $character,
|
||||
'media' => $includedMedia,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('mediaStaff', $data))
|
||||
{
|
||||
foreach($data['mediaStaff'] as $rid => $role)
|
||||
{
|
||||
$roleName = $role['role'];
|
||||
$mediaType = $role['relationships']['media']['data']['type'];
|
||||
$mediaId = $role['relationships']['media']['data']['id'];
|
||||
$media = $data[$mediaType][$mediaId];
|
||||
$output['staff'][$roleName][$mediaType][$mediaId] = $media;
|
||||
}
|
||||
}
|
||||
|
||||
uasort($output['characters']['main'], function ($a, $b) {
|
||||
return $a['character']['canonicalName'] <=> $b['character']['canonicalName'];
|
||||
});
|
||||
uasort($output['characters']['supporting'], function ($a, $b) {
|
||||
return $a['character']['canonicalName'] <=> $b['character']['canonicalName'];
|
||||
});
|
||||
ksort($output['staff']);
|
||||
foreach($output['staff'] as $role => &$media)
|
||||
{
|
||||
if (array_key_exists('anime', $media))
|
||||
{
|
||||
uasort($media['anime'], function ($a, $b) {
|
||||
return $a['canonicalTitle'] <=> $b['canonicalTitle'];
|
||||
});
|
||||
}
|
||||
|
||||
if (array_key_exists('manga', $media))
|
||||
{
|
||||
uasort($media['manga'], function ($a, $b) {
|
||||
return $a['canonicalTitle'] <=> $b['canonicalTitle'];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
]);
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ use Aviat\Ion\Di\ContainerInterface;
|
||||
*/
|
||||
final class Settings extends BaseController {
|
||||
/**
|
||||
* @var \Aviat\API\Anilist\Model
|
||||
* @var \Aviat\AnimeClient\API\Anilist\Model
|
||||
*/
|
||||
private $anilistModel;
|
||||
|
||||
@ -33,6 +33,13 @@ final class Settings extends BaseController {
|
||||
*/
|
||||
private $settingsModel;
|
||||
|
||||
/**
|
||||
* Settings constructor.
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @throws \Aviat\Ion\Di\Exception\ContainerException
|
||||
* @throws \Aviat\Ion\Di\Exception\NotFoundException
|
||||
*/
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
@ -44,7 +51,7 @@ final class Settings extends BaseController {
|
||||
/**
|
||||
* Show the user settings, if logged in
|
||||
*/
|
||||
public function index()
|
||||
public function index(): void
|
||||
{
|
||||
$auth = $this->container->get('auth');
|
||||
$form = $this->settingsModel->getSettingsForm();
|
||||
@ -66,7 +73,7 @@ final class Settings extends BaseController {
|
||||
*
|
||||
* @throws \Aura\Router\Exception\RouteNotFound
|
||||
*/
|
||||
public function update()
|
||||
public function update(): void
|
||||
{
|
||||
$post = $this->request->getParsedBody();
|
||||
unset($post['settings-tabs']);
|
||||
@ -88,14 +95,15 @@ final class Settings extends BaseController {
|
||||
/**
|
||||
* Redirect to Anilist to start Oauth flow
|
||||
*/
|
||||
public function anilistRedirect()
|
||||
public function anilistRedirect(): void
|
||||
{
|
||||
$redirectUrl = 'https://anilist.co/api/v2/oauth/authorize?' .
|
||||
http_build_query([
|
||||
'client_id' => $this->config->get(['anilist', 'client_id']),
|
||||
'redirect_uri' => $this->urlGenerator->url('/anilist-oauth'),
|
||||
'response_type' => 'code',
|
||||
]);
|
||||
$query = http_build_query([
|
||||
'client_id' => $this->config->get(['anilist', 'client_id']),
|
||||
'redirect_uri' => $this->urlGenerator->url('/anilist-oauth'),
|
||||
'response_type' => 'code',
|
||||
]);
|
||||
|
||||
$redirectUrl = "https://anilist.co/api/v2/oauth/authorize?{$query}";
|
||||
|
||||
$this->redirect($redirectUrl, 303);
|
||||
}
|
||||
@ -103,7 +111,7 @@ final class Settings extends BaseController {
|
||||
/**
|
||||
* Oauth callback for Anilist API
|
||||
*/
|
||||
public function anilistCallback()
|
||||
public function anilistCallback(): void
|
||||
{
|
||||
$query = $this->request->getQueryParams();
|
||||
$authCode = $query['code'];
|
||||
|
@ -16,8 +16,9 @@
|
||||
|
||||
namespace Aviat\AnimeClient\Controller;
|
||||
|
||||
use Aviat\AnimeClient\API\Kitsu\Transformer\UserTransformer;
|
||||
use Aviat\AnimeClient\Controller as BaseController;
|
||||
use Aviat\AnimeClient\API\JsonAPI;
|
||||
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
|
||||
/**
|
||||
@ -25,8 +26,18 @@ use Aviat\Ion\Di\ContainerInterface;
|
||||
*/
|
||||
final class User extends BaseController {
|
||||
|
||||
/**
|
||||
* @var \Aviat\AnimeClient\API\Kitsu\Model
|
||||
*/
|
||||
private $kitsuModel;
|
||||
|
||||
/**
|
||||
* User constructor.
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @throws \Aviat\Ion\Di\Exception\ContainerException
|
||||
* @throws \Aviat\Ion\Di\Exception\NotFoundException
|
||||
*/
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
@ -56,103 +67,16 @@ final class User extends BaseController {
|
||||
? $this->config->get(['kitsu_username'])
|
||||
: $username;
|
||||
|
||||
$data = $this->kitsuModel->getUserData($username);
|
||||
$orgData = JsonAPI::organizeData($data)[0];
|
||||
$rels = $orgData['relationships'] ?? [];
|
||||
$favorites = array_key_exists('favorites', $rels) ? $rels['favorites'] : [];
|
||||
|
||||
$stats = [];
|
||||
foreach ($rels['stats'] as $sid => &$item)
|
||||
{
|
||||
$key = $item['attributes']['kind'];
|
||||
$stats[$key] = $item['attributes']['statsData'];
|
||||
unset($item);
|
||||
}
|
||||
|
||||
//dump($orgData);
|
||||
// dump($stats);
|
||||
|
||||
// $timeOnAnime = $this->formatAnimeTime($orgData['attributes']['lifeSpentOnAnime']);
|
||||
$timeOnAnime = $this->formatAnimeTime($stats['anime-amount-consumed']['time']);
|
||||
|
||||
|
||||
$whom = $isMainUser
|
||||
? $this->config->get('whose_list')
|
||||
: $username;
|
||||
|
||||
$rawData = $this->kitsuModel->getUserData($username);
|
||||
$data = (new UserTransformer())->transform($rawData)->toArray();
|
||||
|
||||
$this->outputHTML('user/details', [
|
||||
'title' => 'About ' . $whom,
|
||||
'data' => $orgData,
|
||||
'attributes' => $orgData['attributes'],
|
||||
'relationships' => $rels,
|
||||
'favorites' => $this->organizeFavorites($favorites),
|
||||
'stats' => $stats,
|
||||
'timeOnAnime' => $timeOnAnime,
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorganize favorites data to be more useful
|
||||
*
|
||||
* @param array $rawFavorites
|
||||
* @return array
|
||||
*/
|
||||
private function organizeFavorites(array $rawFavorites): array
|
||||
{
|
||||
$output = [];
|
||||
|
||||
unset($rawFavorites['data']);
|
||||
|
||||
foreach ($rawFavorites as $item)
|
||||
{
|
||||
$rank = $item['attributes']['favRank'];
|
||||
foreach ($item['relationships']['item'] as $key => $fav)
|
||||
{
|
||||
$output[$key] = $output[$key] ?? [];
|
||||
foreach ($fav as $id => $data)
|
||||
{
|
||||
$output[$key][$rank] = array_merge(['id' => $id], $data['attributes']);
|
||||
}
|
||||
}
|
||||
|
||||
ksort($output[$key]);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the time spent on anime in a more readable format
|
||||
*
|
||||
* @param int $minutes
|
||||
* @return string
|
||||
*/
|
||||
private function formatAnimeTime(int $minutes): string
|
||||
{
|
||||
$minutesPerDay = 1440;
|
||||
$minutesPerYear = $minutesPerDay * 365;
|
||||
|
||||
// Minutes short of a year
|
||||
$years = (int)floor($minutes / $minutesPerYear);
|
||||
$minutes %= $minutesPerYear;
|
||||
|
||||
// Minutes short of a day
|
||||
$extraMinutes = $minutes % $minutesPerDay;
|
||||
|
||||
$days = ($minutes - $extraMinutes) / $minutesPerDay;
|
||||
|
||||
// Minutes short of an hour
|
||||
$remMinutes = $extraMinutes % 60;
|
||||
|
||||
$hours = ($extraMinutes - $remMinutes) / 60;
|
||||
|
||||
$output = "{$days} days, {$hours} hours, and {$remMinutes} minutes.";
|
||||
|
||||
if ($years > 0)
|
||||
{
|
||||
$output = "{$years} year(s),{$output}";
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
@ -64,8 +64,9 @@ final class Dispatcher extends RoutingBase {
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->router = $container->get('aura-router')->getMap();
|
||||
$this->matcher = $container->get('aura-router')->getMatcher();
|
||||
$router = $this->container->get('aura-router');
|
||||
$this->router = $router->getMap();
|
||||
$this->matcher = $router->getMatcher();
|
||||
$this->request = $container->get('request');
|
||||
|
||||
$this->outputRoutes = $this->setupRoutes();
|
||||
@ -99,7 +100,7 @@ final class Dispatcher extends RoutingBase {
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getOutputRoutes()
|
||||
public function getOutputRoutes(): array
|
||||
{
|
||||
return $this->outputRoutes;
|
||||
}
|
||||
@ -171,7 +172,7 @@ final class Dispatcher extends RoutingBase {
|
||||
$controllerName = $map[$controllerName];
|
||||
}
|
||||
|
||||
$actionMethod = (array_key_exists('action', $route->attributes))
|
||||
$actionMethod = array_key_exists('action', $route->attributes)
|
||||
? $route->attributes['action']
|
||||
: NOT_FOUND_METHOD;
|
||||
|
||||
@ -205,9 +206,9 @@ final class Dispatcher extends RoutingBase {
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getController()
|
||||
public function getController(): string
|
||||
{
|
||||
$routeType = $this->__get('default_list');
|
||||
$routeType = $this->config->get('default_list');
|
||||
$requestUri = $this->request->getUri()->getPath();
|
||||
$path = trim($requestUri, '/');
|
||||
|
||||
@ -225,7 +226,7 @@ final class Dispatcher extends RoutingBase {
|
||||
$controller = $routeType;
|
||||
}
|
||||
|
||||
return $controller;
|
||||
return $controller ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -233,11 +234,13 @@ final class Dispatcher extends RoutingBase {
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getControllerList()
|
||||
public function getControllerList(): array
|
||||
{
|
||||
$defaultNamespace = DEFAULT_CONTROLLER_NAMESPACE;
|
||||
$path = str_replace('\\', '/', $defaultNamespace);
|
||||
$path = str_replace('Aviat/AnimeClient/', '', $path);
|
||||
$find = ['\\', 'Aviat/AnimeClient/'];
|
||||
$replace = ['/', ''];
|
||||
|
||||
$path = str_replace($find, $replace, $defaultNamespace);
|
||||
$path = trim($path, '/');
|
||||
$actualPath = realpath(_dir(SRC_DIR, $path));
|
||||
$classFiles = glob("{$actualPath}/*.php");
|
||||
@ -265,7 +268,7 @@ final class Dispatcher extends RoutingBase {
|
||||
* @param array $params
|
||||
* @return void
|
||||
*/
|
||||
protected function call($controllerName, $method, array $params)
|
||||
protected function call($controllerName, $method, array $params): void
|
||||
{
|
||||
$logger = $this->container->getLogger('default');
|
||||
|
||||
@ -347,7 +350,7 @@ final class Dispatcher extends RoutingBase {
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function setupRoutes()
|
||||
protected function setupRoutes(): array
|
||||
{
|
||||
$routeType = $this->getController();
|
||||
|
||||
@ -359,7 +362,7 @@ final class Dispatcher extends RoutingBase {
|
||||
unset($route['path']);
|
||||
|
||||
$controllerMap = $this->getControllerList();
|
||||
$controllerClass = (array_key_exists($routeType, $controllerMap))
|
||||
$controllerClass = array_key_exists($routeType, $controllerMap)
|
||||
? $controllerMap[$routeType]
|
||||
: DEFAULT_CONTROLLER;
|
||||
|
||||
|
@ -16,35 +16,28 @@
|
||||
|
||||
namespace Aviat\AnimeClient;
|
||||
|
||||
use Aviat\Ion\
|
||||
{
|
||||
ArrayWrapper, StringWrapper
|
||||
};
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Helper object to manage form generation, especially for config editing
|
||||
*/
|
||||
final class FormGenerator {
|
||||
use ArrayWrapper;
|
||||
use StringWrapper;
|
||||
|
||||
/**
|
||||
* Injection Container
|
||||
* @var ContainerInterface $container
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* Html generation helper
|
||||
*
|
||||
* @var \Aura\Html\HelperLocator
|
||||
*/
|
||||
protected $helper;
|
||||
private $helper;
|
||||
|
||||
/**
|
||||
* FormGenerator constructor.
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @throws \Aviat\Ion\Di\Exception\ContainerException
|
||||
* @throws \Aviat\Ion\Di\Exception\NotFoundException
|
||||
*/
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->helper = $container->get('html-helper');
|
||||
}
|
||||
|
||||
@ -55,7 +48,7 @@ final class FormGenerator {
|
||||
* @param array $form
|
||||
* @return string
|
||||
*/
|
||||
public function generate(string $name, array $form)
|
||||
public function generate(string $name, array $form): string
|
||||
{
|
||||
$type = $form['type'];
|
||||
|
||||
@ -105,6 +98,6 @@ final class FormGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
return $this->helper->input($params);
|
||||
return (string)$this->helper->input($params);
|
||||
}
|
||||
}
|
@ -45,9 +45,11 @@ final class MenuGenerator extends UrlGenerator {
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* Create menu generator
|
||||
* MenuGenerator constructor.
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @throws \Aviat\Ion\Di\Exception\ContainerException
|
||||
* @throws \Aviat\Ion\Di\Exception\NotFoundException
|
||||
*/
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
@ -106,7 +108,7 @@ final class MenuGenerator extends UrlGenerator {
|
||||
|
||||
$link = $this->helper->a($this->url($path), $title);
|
||||
|
||||
$attrs = ($selected)
|
||||
$attrs = $selected
|
||||
? ['class' => 'selected']
|
||||
: [];
|
||||
|
||||
|
@ -16,14 +16,10 @@
|
||||
|
||||
namespace Aviat\AnimeClient\Model;
|
||||
|
||||
use Aviat\Ion\StringWrapper;
|
||||
|
||||
/**
|
||||
* Base model for api interaction
|
||||
*/
|
||||
class API {
|
||||
use StringWrapper;
|
||||
|
||||
/**
|
||||
* Sort the list entries by their title
|
||||
*
|
||||
@ -31,7 +27,7 @@ class API {
|
||||
* @param string $sortKey
|
||||
* @return void
|
||||
*/
|
||||
protected function sortByName(array &$array, string $sortKey)
|
||||
protected function sortByName(array &$array, string $sortKey): void
|
||||
{
|
||||
$sort = [];
|
||||
|
||||
|
@ -108,7 +108,7 @@ class Anime extends API {
|
||||
* @param string $slug
|
||||
* @return AnimeType
|
||||
*/
|
||||
public function getAnime(string $slug)
|
||||
public function getAnime(string $slug): AnimeType
|
||||
{
|
||||
return $this->kitsuModel->getAnime($slug);
|
||||
}
|
||||
@ -147,7 +147,7 @@ class Anime extends API {
|
||||
$item = $this->kitsuModel->getListItem($itemId);
|
||||
$array = $item->toArray();
|
||||
|
||||
if (is_array($array['notes']))
|
||||
if (\is_array($array['notes']))
|
||||
{
|
||||
$array['notes'] = '';
|
||||
}
|
||||
|
@ -226,6 +226,11 @@ final class AnimeCollection extends Collection {
|
||||
return $query->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of genres from the database
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getGenresForList(): array
|
||||
{
|
||||
$query = $this->db->select('hummingbird_id, genre')
|
||||
|
@ -83,11 +83,11 @@ class Collection extends DB {
|
||||
|
||||
if ( ! empty($filter))
|
||||
{
|
||||
$this->db->where_in('hummingbird_id', $filter);
|
||||
$this->db->whereIn('hummingbird_id', $filter);
|
||||
}
|
||||
|
||||
$query = $this->db->order_by('hummingbird_id')
|
||||
->order_by('genre')
|
||||
$query = $this->db->orderBy('hummingbird_id')
|
||||
->orderBy('genre')
|
||||
->get();
|
||||
|
||||
$output = [];
|
||||
|
@ -17,15 +17,12 @@
|
||||
namespace Aviat\AnimeClient\Model;
|
||||
|
||||
use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
|
||||
use Aviat\Ion\{ArrayWrapper, StringWrapper};
|
||||
|
||||
/**
|
||||
* Base model for database interaction
|
||||
*/
|
||||
class DB {
|
||||
use ArrayWrapper;
|
||||
use ContainerAware;
|
||||
use StringWrapper;
|
||||
|
||||
/**
|
||||
* The query builder object
|
||||
|
@ -41,7 +41,7 @@ final class Settings {
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function getSettings()
|
||||
public function getSettings(): array
|
||||
{
|
||||
$settings = [
|
||||
'config' => [],
|
||||
@ -66,7 +66,7 @@ final class Settings {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
public function getSettingsForm()
|
||||
public function getSettingsForm(): array
|
||||
{
|
||||
$output = [];
|
||||
|
||||
@ -124,7 +124,7 @@ final class Settings {
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function validateSettings(array $settings)
|
||||
public function validateSettings(array $settings): array
|
||||
{
|
||||
$config = (new Config($settings))->toArray();
|
||||
|
||||
@ -150,7 +150,7 @@ final class Settings {
|
||||
$looseConfig[$key] = $val;
|
||||
}
|
||||
}
|
||||
elseif (is_array($val) && ! empty($val))
|
||||
elseif (\is_array($val) && ! empty($val))
|
||||
{
|
||||
foreach($val as $k => $v)
|
||||
{
|
||||
@ -204,7 +204,6 @@ final class Settings {
|
||||
{
|
||||
dump($e);
|
||||
dump($settings);
|
||||
die();
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
@ -59,20 +59,6 @@ class RoutingBase {
|
||||
$this->routes = $this->config->get('routes');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the appropriate value for the routing key
|
||||
*
|
||||
* @param string|int|array $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get($key)
|
||||
{
|
||||
if ($this->config->has($key))
|
||||
{
|
||||
return $this->config->get($key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current url path
|
||||
* @throws \Aviat\Ion\Di\ContainerException
|
||||
|
@ -17,9 +17,9 @@
|
||||
namespace Aviat\AnimeClient\Types;
|
||||
|
||||
/**
|
||||
* Type representing an Anime object for display
|
||||
* Type representing an anime within a watch list
|
||||
*/
|
||||
final class Anime extends AbstractType {
|
||||
class Anime extends AbstractType {
|
||||
public $age_rating;
|
||||
public $age_rating_guide;
|
||||
public $cover_image;
|
||||
|
@ -17,7 +17,7 @@
|
||||
namespace Aviat\AnimeClient\Types;
|
||||
|
||||
/**
|
||||
* Type representing an Anime object for display
|
||||
* Type representing an anime watch list item
|
||||
*/
|
||||
final class AnimeListItem extends AbstractType {
|
||||
public $id;
|
||||
@ -40,4 +40,9 @@ final class AnimeListItem extends AbstractType {
|
||||
public $rewatched;
|
||||
public $user_rating;
|
||||
public $watching_status;
|
||||
|
||||
public function setAnime($anime): void
|
||||
{
|
||||
$this->anime = new Anime($anime);
|
||||
}
|
||||
}
|
||||
|
25
src/Types/AnimePage.php
Normal file
25
src/Types/AnimePage.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7.1
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2018 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 4.1
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\Types;
|
||||
|
||||
/**
|
||||
* Type representing an Anime object for a detail page
|
||||
*/
|
||||
final class AnimePage extends Anime {
|
||||
public $characters;
|
||||
public $staff;
|
||||
}
|
39
src/Types/Character.php
Normal file
39
src/Types/Character.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7.1
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2018 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 4.1
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\Types;
|
||||
|
||||
/**
|
||||
* Type representing a character for display
|
||||
*/
|
||||
final class Character extends AbstractType {
|
||||
public $castings;
|
||||
public $description;
|
||||
public $id;
|
||||
public $included;
|
||||
public $media;
|
||||
public $name;
|
||||
public $names;
|
||||
public $otherNames;
|
||||
|
||||
public function setMedia ($media): void
|
||||
{
|
||||
$this->media = new class($media) extends AbstractType {
|
||||
public $anime;
|
||||
public $manga;
|
||||
};
|
||||
}
|
||||
}
|
@ -24,6 +24,7 @@ class Config extends AbstractType {
|
||||
|
||||
// Settings in config.toml
|
||||
public $asset_path; // Path to public folder for urls
|
||||
public $dark_theme;
|
||||
public $default_anime_list_path;
|
||||
public $default_list;
|
||||
public $default_manga_list_path;
|
||||
|
@ -20,12 +20,14 @@ namespace Aviat\AnimeClient\Types;
|
||||
* Type representing an Anime object for display
|
||||
*/
|
||||
final class MangaPage extends AbstractType {
|
||||
public $characters;
|
||||
public $chapter_count;
|
||||
public $cover_image;
|
||||
public $genres;
|
||||
public $id;
|
||||
public $included;
|
||||
public $manga_type;
|
||||
public $staff;
|
||||
public $synopsis;
|
||||
public $title;
|
||||
public $titles;
|
||||
|
35
src/Types/Person.php
Normal file
35
src/Types/Person.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7.1
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2018 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 4.1
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\Types;
|
||||
|
||||
/**
|
||||
* Type representing a person for display
|
||||
*/
|
||||
final class Person extends AbstractType {
|
||||
public $id;
|
||||
public $name;
|
||||
public $characters;
|
||||
public $staff;
|
||||
|
||||
public function setCharacters($characters): void
|
||||
{
|
||||
$this->characters = new class($characters) extends AbstractType {
|
||||
public $main;
|
||||
public $supporting;
|
||||
};
|
||||
}
|
||||
}
|
32
src/Types/User.php
Normal file
32
src/Types/User.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7.1
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2018 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 4.1
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\Types;
|
||||
|
||||
/**
|
||||
* Type representing a Kitsu user for display
|
||||
*/
|
||||
final class User extends AbstractType {
|
||||
public $about;
|
||||
public $avatar;
|
||||
public $favorites;
|
||||
public $location;
|
||||
public $name;
|
||||
public $slug;
|
||||
public $stats;
|
||||
public $waifu;
|
||||
public $website;
|
||||
}
|
@ -47,13 +47,13 @@ class UrlGenerator extends RoutingBase {
|
||||
/**
|
||||
* Get the base url for css/js/images
|
||||
*
|
||||
* @param string ...$args
|
||||
* @param string[] $args
|
||||
* @return string
|
||||
*/
|
||||
public function assetUrl(string ...$args): string
|
||||
{
|
||||
$baseUrl = rtrim($this->url(''), '/')
|
||||
. $this->__get('asset_path');
|
||||
. $this->config->get('asset_path');
|
||||
|
||||
array_unshift($args, $baseUrl);
|
||||
|
||||
@ -82,7 +82,7 @@ class UrlGenerator extends RoutingBase {
|
||||
{
|
||||
if ( ! array_key_exists($i + 1, $segments))
|
||||
{
|
||||
$segments[$i + 1] = "";
|
||||
$segments[$i + 1] = '';
|
||||
}
|
||||
|
||||
$path_segments[$i] = preg_replace('`{.*?}`', $segments[$i + 1], $path_segments[$i]);
|
||||
@ -104,7 +104,7 @@ class UrlGenerator extends RoutingBase {
|
||||
public function defaultUrl(string $type): string
|
||||
{
|
||||
$type = trim($type);
|
||||
$defaultPath = $this->__get("default_{$type}_list_path");
|
||||
$defaultPath = $this->config->get("default_{$type}_list_path");
|
||||
|
||||
if ($defaultPath !== NULL)
|
||||
{
|
||||
|
14
src/Util.php
14
src/Util.php
@ -16,7 +16,6 @@
|
||||
|
||||
namespace Aviat\AnimeClient;
|
||||
|
||||
use Aviat\Ion\ConfigInterface;
|
||||
use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
|
||||
|
||||
/**
|
||||
@ -42,12 +41,6 @@ class Util {
|
||||
'me'
|
||||
];
|
||||
|
||||
/**
|
||||
* The config manager
|
||||
* @var ConfigInterface
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* Set up the Util class
|
||||
*
|
||||
@ -58,7 +51,6 @@ class Util {
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
$this->setContainer($container);
|
||||
$this->config = $container->get('config');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,7 +60,7 @@ class Util {
|
||||
* @param string $b - Second item to compare
|
||||
* @return string
|
||||
*/
|
||||
public static function isSelected($a, $b)
|
||||
public static function isSelected(string $a, string $b): string
|
||||
{
|
||||
return ($a === $b) ? 'selected' : '';
|
||||
}
|
||||
@ -80,7 +72,7 @@ class Util {
|
||||
* @param string $b - Second item to compare
|
||||
* @return string
|
||||
*/
|
||||
public static function isNotSelected($a, $b)
|
||||
public static function isNotSelected(string $a, string $b): string
|
||||
{
|
||||
return ($a !== $b) ? 'selected' : '';
|
||||
}
|
||||
@ -108,7 +100,7 @@ class Util {
|
||||
*
|
||||
* @throws \Aviat\Ion\Di\ContainerException
|
||||
* @throws \Aviat\Ion\Di\NotFoundException
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function isFormPage(): bool
|
||||
{
|
||||
|
@ -153,6 +153,12 @@ const SETTINGS_MAP = [
|
||||
'default' => 'Somebody',
|
||||
'description' => 'Name of the owner of the list data.',
|
||||
],
|
||||
'dark_theme' => [
|
||||
'type' => 'boolean',
|
||||
'title' => 'Use Dark Theme',
|
||||
'default' => FALSE,
|
||||
'description' => 'Use a darker background theme?',
|
||||
],
|
||||
'show_anime_collection' => [
|
||||
'type' => 'boolean',
|
||||
'title' => 'Show Anime Collection',
|
||||
|
31
sw.js
31
sw.js
@ -5,36 +5,17 @@ async function fromCache (request) {
|
||||
return await cache.match(request);
|
||||
}
|
||||
|
||||
async function fromNetwork (request) {
|
||||
return await fetch(request);
|
||||
}
|
||||
|
||||
async function update (request) {
|
||||
async function updateCache (request) {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const response = await fetch(request);
|
||||
|
||||
if (request.url.includes('/public/images/')) {
|
||||
console.log('Saving to cache: ', request.url);
|
||||
await cache.put(request, response.clone());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/* function refresh (response) {
|
||||
return self.clients.matchAll().then(clients => {
|
||||
clients.forEach(client => {
|
||||
const message = {
|
||||
type: 'refresh',
|
||||
url: response.url,
|
||||
eTag: response.headers.get('ETag')
|
||||
};
|
||||
|
||||
client.postMessage(JSON.stringify(message));
|
||||
})
|
||||
});
|
||||
} */
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
console.log('Public Folder Worker installed');
|
||||
|
||||
@ -55,8 +36,8 @@ self.addEventListener('install', event => {
|
||||
)
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
console.log('Public Folder Worker activated');
|
||||
self.addEventListener('activate', () => {
|
||||
console.info('Public Folder Worker activated');
|
||||
});
|
||||
|
||||
// Pull css, images, and javascript from cache
|
||||
@ -71,11 +52,7 @@ self.addEventListener('fetch', event => {
|
||||
if (cached !== undefined) {
|
||||
event.respondWith(cached);
|
||||
} else {
|
||||
event.respondWith(fromNetwork(event.request));
|
||||
event.respondWith(updateCache(event.request));
|
||||
}
|
||||
});
|
||||
|
||||
event.waitUntil(
|
||||
update(event.request)
|
||||
);
|
||||
});
|
@ -17,7 +17,9 @@
|
||||
namespace Aviat\AnimeClient\Tests\API;
|
||||
|
||||
use function Amp\Promise\wait;
|
||||
use Aviat\AnimeClient\API\{APIRequestBuilder, HummingbirdClient};
|
||||
use function Aviat\AnimeClient\getResponse;
|
||||
|
||||
use Aviat\AnimeClient\API\APIRequestBuilder;
|
||||
use Aviat\Ion\Json;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
@ -37,35 +39,35 @@ class APIRequestBuilderTest extends TestCase {
|
||||
$this->builder->setLogger(new NullLogger);
|
||||
}
|
||||
|
||||
public function testGzipRequest()
|
||||
public function testGzipRequest(): void
|
||||
{
|
||||
$request = $this->builder->newRequest('GET', 'gzip')
|
||||
->getFullRequest();
|
||||
$response = wait((new HummingbirdClient)->request($request));
|
||||
$response = getResponse($request);
|
||||
$body = Json::decode(wait($response->getBody()));
|
||||
$this->assertEquals(1, $body['gzipped']);
|
||||
}
|
||||
|
||||
public function testInvalidRequestMethod()
|
||||
public function testInvalidRequestMethod(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->builder->newRequest('FOO', 'gzip')
|
||||
->getFullRequest();
|
||||
}
|
||||
|
||||
public function testRequestWithBasicAuth()
|
||||
public function testRequestWithBasicAuth(): void
|
||||
{
|
||||
$request = $this->builder->newRequest('GET', 'headers')
|
||||
->setBasicAuth('username', 'password')
|
||||
->getFullRequest();
|
||||
|
||||
$response = wait((new HummingbirdClient)->request($request));
|
||||
$response = getResponse($request);
|
||||
$body = Json::decode(wait($response->getBody()));
|
||||
|
||||
$this->assertEquals('Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $body['headers']['Authorization']);
|
||||
}
|
||||
|
||||
public function testRequestWithQueryString()
|
||||
public function testRequestWithQueryString(): void
|
||||
{
|
||||
$query = [
|
||||
'foo' => 'bar',
|
||||
@ -87,13 +89,13 @@ class APIRequestBuilderTest extends TestCase {
|
||||
->setQuery($query)
|
||||
->getFullRequest();
|
||||
|
||||
$response = wait((new HummingbirdClient)->request($request));
|
||||
$response = getResponse($request);
|
||||
$body = Json::decode(wait($response->getBody()));
|
||||
|
||||
$this->assertEquals($expected, $body['args']);
|
||||
}
|
||||
|
||||
public function testFormValueRequest()
|
||||
public function testFormValueRequest(): void
|
||||
{
|
||||
$formValues = [
|
||||
'foo' => 'bar',
|
||||
@ -104,13 +106,13 @@ class APIRequestBuilderTest extends TestCase {
|
||||
->setFormFields($formValues)
|
||||
->getFullRequest();
|
||||
|
||||
$response = wait((new HummingbirdClient)->request($request));
|
||||
$response = getResponse($request);
|
||||
$body = Json::decode(wait($response->getBody()));
|
||||
|
||||
$this->assertEquals($formValues, $body['form']);
|
||||
}
|
||||
|
||||
public function testFullUrlRequest()
|
||||
public function testFullUrlRequest(): void
|
||||
{
|
||||
$data = [
|
||||
'foo' => [
|
||||
@ -128,7 +130,7 @@ class APIRequestBuilderTest extends TestCase {
|
||||
->setJsonBody($data)
|
||||
->getFullRequest();
|
||||
|
||||
$response = wait((new HummingbirdClient)->request($request));
|
||||
$response = getResponse($request);
|
||||
$body = Json::decode(wait($response->getBody()));
|
||||
|
||||
$this->assertEquals($data, $body['json']);
|
||||
|
@ -18,7 +18,6 @@ namespace Aviat\AnimeClient\Tests\API\Kitsu\Transformer;
|
||||
|
||||
use Aviat\AnimeClient\API\Kitsu\Transformer\AnimeTransformer;
|
||||
use Aviat\AnimeClient\Tests\AnimeClientTestCase;
|
||||
use Aviat\Ion\Friend;
|
||||
use Aviat\Ion\Json;
|
||||
|
||||
class AnimeTransformerTest extends AnimeClientTestCase {
|
||||
|
@ -1,4 +1,10 @@
|
||||
<?php return Aviat\AnimeClient\Types\Anime::__set_state(array(
|
||||
<?php return Aviat\AnimeClient\Types\AnimePage::__set_state(array(
|
||||
'characters' =>
|
||||
array (
|
||||
),
|
||||
'staff' =>
|
||||
array (
|
||||
),
|
||||
'age_rating' => 'R',
|
||||
'age_rating_guide' => 'Violence, Profanity',
|
||||
'cover_image' => 'https://media.kitsu.io/anime/poster_images/7442/small.jpg?1418580054',
|
||||
|
@ -1,4 +1,7 @@
|
||||
<?php return Aviat\AnimeClient\Types\MangaPage::__set_state(array(
|
||||
'characters' =>
|
||||
array (
|
||||
),
|
||||
'chapter_count' => '-',
|
||||
'cover_image' => 'https://media.kitsu.io/manga/poster_images/20286/small.jpg?1434293999',
|
||||
'genres' =>
|
||||
@ -68,6 +71,9 @@
|
||||
),
|
||||
),
|
||||
'manga_type' => 'manga',
|
||||
'staff' =>
|
||||
array (
|
||||
),
|
||||
'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!
|
||||
(Source: Kirei Cake)',
|
||||
'title' => 'Bokura wa Minna Kawaisou',
|
||||
|
Loading…
Reference in New Issue
Block a user