Merge branch 'develop' of timw4mail/HummingBirdAnimeClient into master

This commit is contained in:
Timothy Warren 2018-12-13 14:51:01 -05:00 committed by GitLab
commit 87d0960424
97 changed files with 1710 additions and 2588 deletions

View File

@ -181,6 +181,7 @@ $routes = [
'user_info' => [ 'user_info' => [
'path' => '/user/{username}', 'path' => '/user/{username}',
'controller' => 'user', 'controller' => 'user',
'action' => 'about',
'tokens' => [ 'tokens' => [
'username' => '.*?' 'username' => '.*?'
] ]

View File

@ -20,5 +20,3 @@ host = "127.0.0.1"
# Database number # Database number
database = 2 database = 2
[options]

View File

@ -2,52 +2,55 @@
<main class="details fixed"> <main class="details fixed">
<section class="flex"> <section class="flex">
<aside class="info"> <aside class="info">
<?= $helper->picture("images/anime/{$show_data['id']}-original.webp") ?> <?= $helper->picture("images/anime/{$data['id']}-original.webp") ?>
<br /> <br />
<table class="media-details"> <table class="media-details">
<tr> <tr>
<td class="align-right">Airing Status</td> <td class="align-right">Airing Status</td>
<td><?= $show_data['status'] ?></td> <td><?= $data['status'] ?></td>
</tr> </tr>
<tr> <tr>
<td>Show Type</td> <td>Show Type</td>
<td><?= $show_data['show_type'] ?></td> <td><?= $data['show_type'] ?></td>
</tr> </tr>
<tr> <tr>
<td>Episode Count</td> <td>Episode Count</td>
<td><?= $show_data['episode_count'] ?? '-' ?></td> <td><?= $data['episode_count'] ?? '-' ?></td>
</tr> </tr>
<?php if ( ! empty($show_data['episode_length'])): ?> <?php if ( ! empty($data['episode_length'])): ?>
<tr> <tr>
<td>Episode Length</td> <td>Episode Length</td>
<td><?= $show_data['episode_length'] ?> minutes</td> <td><?= $data['episode_length'] ?> minutes</td>
</tr> </tr>
<?php endif ?> <?php endif ?>
<?php if ( ! empty($show_data['age_rating'])): ?> <?php if ( ! empty($data['age_rating'])): ?>
<tr> <tr>
<td>Age Rating</td> <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> </td>
</tr> </tr>
<?php endif ?> <?php endif ?>
<tr> <tr>
<td>Genres</td> <td>Genres</td>
<td> <td>
<?= implode(', ', $show_data['genres']) ?> <?= implode(', ', $data['genres']) ?>
</td> </td>
</tr> </tr>
</table> </table>
<br />
</aside> </aside>
<article class="text"> <article class="text">
<h2 class="toph"><a rel="external" href="<?= $show_data['url'] ?>"><?= $show_data['title'] ?></a></h2> <h2 class="toph"><a rel="external" href="<?= $data['url'] ?>"><?= $data['title'] ?></a></h2>
<?php foreach ($show_data['titles'] as $title): ?> <?php foreach ($data['titles'] as $title): ?>
<h3><?= $title ?></h3> <h3><?= $title ?></h3>
<?php endforeach ?> <?php endforeach ?>
<br /> <br />
<p class="description"><?= nl2br($show_data['synopsis']) ?></p> <p class="description"><?= nl2br($data['synopsis']) ?></p>
<?php if (count($show_data['streaming_links']) > 0): ?> <?php if (count($data['streaming_links']) > 0): ?>
<hr /> <hr />
<h4>Streaming on:</h4> <h4>Streaming on:</h4>
<table class="full-width invisible streaming-links"> <table class="full-width invisible streaming-links">
@ -59,13 +62,13 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($show_data['streaming_links'] as $link): ?> <?php foreach ($data['streaming_links'] as $link): ?>
<tr> <tr>
<td class="align-left"> <td class="align-left">
<?php if ($link['meta']['link'] !== FALSE): ?> <?php if ($link['meta']['link'] !== FALSE): ?>
<a <a
href="<?= $link['link'] ?>" 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', [ <?= $helper->picture("images/{$link['meta']['image']}", 'svg', [
'class' => 'streaming-logo', 'class' => 'streaming-logo',
@ -92,13 +95,13 @@
</tbody> </tbody>
</table> </table>
<?php endif ?> <?php endif ?>
<?php if ( ! empty($show_data['trailer_id'])): ?> <?php if ( ! empty($data['trailer_id'])): ?>
<div class="responsive-iframe"> <div class="responsive-iframe">
<h4>Trailer</h4> <h4>Trailer</h4>
<iframe <iframe
width="560" width="560"
height="315" height="315"
src="https://www.youtube.com/embed/<?= $show_data['trailer_id'] ?>" src="https://www.youtube.com/embed/<?= $data['trailer_id'] ?>"
frameborder="0" frameborder="0"
allow="autoplay; encrypted-media" allow="autoplay; encrypted-media"
allowfullscreen allowfullscreen
@ -108,13 +111,13 @@
</article> </article>
</section> </section>
<?php if (count($characters) > 0): ?> <?php if (count($data['characters']) > 0): ?>
<section> <section>
<h2>Characters</h2> <h2>Characters</h2>
<div class="tabs"> <div class="tabs">
<?php $i = 0 ?> <?php $i = 0 ?>
<?php foreach ($characters as $role => $list): ?> <?php foreach ($data['characters'] as $role => $list): ?>
<input <input
type="radio" name="character-types" type="radio" name="character-types"
id="character-types-<?= $i ?>" <?= ($i === 0) ? 'checked' : '' ?> /> id="character-types-<?= $i ?>" <?= ($i === 0) ? 'checked' : '' ?> />
@ -140,14 +143,13 @@
</section> </section>
<?php endif ?> <?php endif ?>
<?php if (count($staff) > 0): ?> <?php if (count($data['staff']) > 0): ?>
<?php //dump($staff); ?>
<section> <section>
<h2>Staff</h2> <h2>Staff</h2>
<div class="vertical-tabs"> <div class="vertical-tabs">
<?php $i = 0; ?> <?php $i = 0; ?>
<?php foreach ($staff as $role => $people): ?> <?php foreach ($data['staff'] as $role => $people): ?>
<div class="tab"> <div class="tab">
<input type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> /> <input type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />
<label for="staff-role<?= $i ?>"><?= $role ?></label> <label for="staff-role<?= $i ?>"><?= $role ?></label>

View File

@ -7,10 +7,10 @@ use Aviat\AnimeClient\API\Kitsu;
<main class="details fixed"> <main class="details fixed">
<section class="flex flex-no-wrap"> <section class="flex flex-no-wrap">
<div> <div>
<?= $helper->picture("images/characters/{$data[0]['id']}-original.webp") ?> <?= $helper->picture("images/characters/{$data['id']}-original.webp") ?>
<?php if ( ! empty($data[0]['attributes']['otherNames'])): ?> <?php if ( ! empty($data['otherNames'])): ?>
<h3>Nicknames / Other names</h3> <h3>Nicknames / Other names</h3>
<?php foreach ($data[0]['attributes']['otherNames'] as $name): ?> <?php foreach ($data['otherNames'] as $name): ?>
<h4><?= $name ?></h4> <h4><?= $name ?></h4>
<?php endforeach ?> <?php endforeach ?>
<?php endif ?> <?php endif ?>
@ -23,19 +23,19 @@ use Aviat\AnimeClient\API\Kitsu;
<hr /> <hr />
<p class="description"><?= $data[0]['attributes']['description'] ?></p> <p class="description"><?= $data['description'] ?></p>
</div> </div>
</section> </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> <h3>Media</h3>
<div class="tabs"> <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" /> <input checked="checked" type="radio" id="media-anime" name="media-tabs" />
<label for="media-anime">Anime</label> <label for="media-anime">Anime</label>
<section class="media-wrap content"> <section class="media-wrap content">
<?php foreach ($data['included']['anime'] as $id => $anime): ?> <?php foreach ($data['media']['anime'] as $id => $anime): ?>
<article class="media"> <article class="media">
<?php <?php
$link = $url->generate('anime.details', ['id' => $anime['attributes']['slug']]); $link = $url->generate('anime.details', ['id' => $anime['attributes']['slug']]);
@ -58,12 +58,12 @@ use Aviat\AnimeClient\API\Kitsu;
</section> </section>
<?php endif ?> <?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" /> <input type="radio" id="media-manga" name="media-tabs" />
<label for="media-manga">Manga</label> <label for="media-manga">Manga</label>
<section class="media-wrap content"> <section class="media-wrap content">
<?php foreach ($data['included']['manga'] as $id => $manga): ?> <?php foreach ($data['media']['manga'] as $id => $manga): ?>
<article class="media"> <article class="media">
<?php <?php
$link = $url->generate('manga.details', ['id' => $manga['attributes']['slug']]); $link = $url->generate('manga.details', ['id' => $manga['attributes']['slug']]);
@ -89,14 +89,68 @@ use Aviat\AnimeClient\API\Kitsu;
<?php endif ?> <?php endif ?>
<section> <section>
<?php if ($castCount > 0): ?> <?php if (count($data['castings']) > 0): ?>
<h3>Castings</h3> <h3>Castings</h3>
<?php <?php
$vas = $castings['Voice Actor']; $vas = $data['castings']['Voice Actor'];
unset($castings['Voice Actor']); unset($data['castings']['Voice Actor']);
ksort($vas) 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)): ?> <?php if ( ! empty($vas)): ?>
<h4>Voice Actors</h4> <h4>Voice Actors</h4>
@ -161,61 +215,6 @@ use Aviat\AnimeClient\API\Kitsu;
<?php endforeach ?> <?php endforeach ?>
</div> </div>
<?php endif ?> <?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 ?> <?php endif ?>
</section> </section>
</main> </main>

View File

@ -2,7 +2,7 @@
<main> <main>
<h2>Edit Anime Collection Item</h2> <h2>Edit Anime Collection Item</h2>
<form action="<?= $action_url ?>" method="post"> <form action="<?= $action_url ?>" method="post">
<table class="invisible form" style="border:0"> <table class="invisible form">
<tbody> <tbody>
<tr> <tr>
<td rowspan="6" class="align-center"> <td rowspan="6" class="align-center">
@ -28,7 +28,7 @@
<td class="align-left"> <td class="align-left">
<select name="media_id" id="media_id"> <select name="media_id" id="media_id">
<?php foreach($media_items as $id => $name): ?> <?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 ?> <?php endforeach ?>
</select> </select>
</td> </td>

View File

@ -9,7 +9,7 @@
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>"> <a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
<?= $item['title'] ?> <?= $item['title'] ?>
</a> </a>
<?= (!empty($item['alternate_title'])) ? " <br /><small> " . $item['alternate_title'] . "</small>" : "" ?> <?= ! empty($item['alternate_title']) ? ' <br /><small> ' . $item['alternate_title'] . '</small>' : '' ?>
</td> </td>
<td><?= $item['episode_count'] ?></td> <td><?= $item['episode_count'] ?></td>
<td><?= $item['episode_length'] ?></td> <td><?= $item['episode_length'] ?></td>

View File

@ -7,6 +7,7 @@
<meta http-equiv="Content-Security-Policy" content="script-src 'self'" /> <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" /> <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="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="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="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') ?>"> <link rel="apple-touch-icon" sizes="60x60" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-60x60.png') ?>">
@ -38,4 +39,5 @@
} }
} }
?> ?>
</header> </header>

View File

@ -25,6 +25,8 @@
</td> </td>
</tr> </tr>
</table> </table>
<br />
</aside> </aside>
<article class="text"> <article class="text">
<h2 class="toph"><a rel="external" href="<?= $data['url'] ?>"><?= $data['title'] ?></a></h2> <h2 class="toph"><a rel="external" href="<?= $data['url'] ?>"><?= $data['title'] ?></a></h2>
@ -37,11 +39,12 @@
</article> </article>
</section> </section>
<?php if (count($characters) > 0): ?> <?php if (count($data['characters']) > 0): ?>
<h2>Characters</h2> <h2>Characters</h2>
<div class="tabs"> <div class="tabs">
<?php $i = 0 ?> <?php $i = 0 ?>
<?php foreach ($characters as $role => $list): ?> <?php foreach ($data['characters'] as $role => $list): ?>
<input <input
type="radio" name="character-role-tabs" type="radio" name="character-role-tabs"
id="character-tabs<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> /> id="character-tabs<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />
@ -66,12 +69,12 @@
</div> </div>
<?php endif ?> <?php endif ?>
<?php if (count($staff) > 0): ?> <?php if (count($data['staff']) > 0): ?>
<h2>Staff</h2> <h2>Staff</h2>
<div class="vertical-tabs"> <div class="vertical-tabs">
<?php $i = 0 ?> <?php $i = 0 ?>
<?php foreach ($staff as $role => $people): ?> <?php foreach ($data['staff'] as $role => $people): ?>
<div class="tab"> <div class="tab">
<input <input
type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> /> type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />

View File

@ -5,7 +5,7 @@ use Aviat\AnimeClient\API\Kitsu;
<h3>Voice Acting Roles</h3> <h3>Voice Acting Roles</h3>
<div class="tabs"> <div class="tabs">
<?php $i = 0; ?> <?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 ?>" /> <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> <label for="character-type-<?= $i ?>"><h5><?= ucfirst($role) ?></h5></label>
<section class="content"> <section class="content">
@ -16,7 +16,7 @@ use Aviat\AnimeClient\API\Kitsu;
</tr> </tr>
<?php foreach ($characterList as $cid => $character): ?> <?php foreach ($characterList as $cid => $character): ?>
<tr> <tr>
<td style="width:229px"> <td>
<article class="character"> <article class="character">
<?php <?php
$link = $url->generate('character', ['slug' => $character['character']['slug']]); $link = $url->generate('character', ['slug' => $character['character']['slug']]);

View File

@ -9,16 +9,16 @@ use Aviat\AnimeClient\API\Kitsu;
<?= $helper->picture("images/people/{$data['id']}-original.webp", 'jpg', ['class' => 'cover' ]) ?> <?= $helper->picture("images/people/{$data['id']}-original.webp", 'jpg', ['class' => 'cover' ]) ?>
</div> </div>
<div> <div>
<h2 class="toph"><?= $data['attributes']['name'] ?></h2> <h2 class="toph"><?= $data['name'] ?></h2>
</div> </div>
</section> </section>
<?php if ( ! empty($staff)): ?> <?php if ( ! empty($data['staff'])): ?>
<section> <section>
<h3>Castings</h3> <h3>Castings</h3>
<div class="vertical-tabs"> <div class="vertical-tabs">
<?php $i = 0 ?> <?php $i = 0 ?>
<?php foreach ($staff as $role => $entries): ?> <?php foreach ($data['staff'] as $role => $entries): ?>
<div class="tab"> <div class="tab">
<input <input
type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> /> type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />
@ -59,7 +59,7 @@ use Aviat\AnimeClient\API\Kitsu;
</section> </section>
<?php endif ?> <?php endif ?>
<?php if ( ! (empty($characters['main']) || empty($characters['supporting']))): ?> <?php if ( ! (empty($data['characters']['main']) || empty($data['characters']['supporting']))): ?>
<section> <section>
<?php include 'character-mapping.php' ?> <?php include 'character-mapping.php' ?>
</section> </section>

View File

@ -1,45 +1,39 @@
<?php <?php
use function Aviat\AnimeClient\getLocalImg;
use Aviat\AnimeClient\API\Kitsu; use Aviat\AnimeClient\API\Kitsu;
?> ?>
<main class="user-page details"> <main class="user-page details">
<h2 class="toph"> <h2 class="toph">
<?= $helper->a( <?= $helper->a(
"https://kitsu.io/users/{$attributes['slug']}", "https://kitsu.io/users/{$data['slug']}",
$attributes['name'], [ $data['name'], [
'title' => 'View profile on Kitsu' 'title' => 'View profile on Kitsu'
]) ])
?> ?>
</h2> </h2>
<p><?= $escape->html($attributes['about']) ?></p> <p><?= $escape->html($data['about']) ?></p>
<section class="flex flex-no-wrap"> <section class="flex flex-no-wrap">
<aside class="info"> <aside class="info">
<center> <center>
<?php <?= $helper->img($urlGenerator->assetUrl($data['avatar']), ['alt' => '']); ?>
$avatar = $urlGenerator->assetUrl(
getLocalImg($attributes['avatar']['original'], FALSE)
);
echo $helper->img($avatar, ['alt' => '']);
?>
</center> </center>
<br /> <br />
<table class="media-details"> <table class="media-details">
<tr> <tr>
<td>Location</td> <td>Location</td>
<td><?= $attributes['location'] ?></td> <td><?= $data['location'] ?></td>
</tr> </tr>
<tr> <tr>
<td>Website</td> <td>Website</td>
<td><?= $helper->a($attributes['website'], $attributes['website']) ?></td> <td><?= $helper->a($data['website'], $data['website']) ?></td>
</tr> </tr>
<?php if (array_key_exists('waifu', $relationships)): ?> <?php if ( ! empty($data['waifu'])): ?>
<tr> <tr>
<td><?= $escape->html($attributes['waifuOrHusbando']) ?></td> <td><?= $escape->html($data['waifu']['label']) ?></td>
<td> <td>
<?php <?php
$character = $relationships['waifu']['attributes']; $character = $data['waifu']['character'];
echo $helper->a( echo $helper->a(
$url->generate('character', ['slug' => $character['slug']]), $url->generate('character', ['slug' => $character['slug']]),
$character['canonicalName'] $character['canonicalName']
@ -52,42 +46,24 @@ use Aviat\AnimeClient\API\Kitsu;
<h3>User Stats</h3><br /> <h3>User Stats</h3><br />
<table class="media-details"> <table class="media-details">
<?php foreach($data['stats'] as $label => $stat): ?>
<tr> <tr>
<td>Time spent watching anime:</td> <td><?= $label ?></td>
<td><?= $timeOnAnime ?></td> <td><?= $stat ?></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>
</tr> </tr>
<?php endforeach ?>
</table> </table>
</aside> </aside>
<article> <article>
<?php if ( ! empty($favorites)): ?> <?php if ( ! empty($data['favorites'])): ?>
<h3>Favorites</h3> <h3>Favorites</h3>
<div class="tabs"> <div class="tabs">
<?php $i = 0 ?> <?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' : '' ?> /> <input type="radio" name="user-favorites" id="user-fav-chars" <?= $i === 0 ? 'checked' : '' ?> />
<label for="user-fav-chars">Characters</label> <label for="user-fav-chars">Characters</label>
<section class="content full-width media-wrap"> <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'])): ?> <?php if ( ! empty($char['image']['original'])): ?>
<article class="character"> <article class="character">
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?> <?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
@ -101,11 +77,11 @@ use Aviat\AnimeClient\API\Kitsu;
</section> </section>
<?php $i++; ?> <?php $i++; ?>
<?php endif ?> <?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' : '' ?> /> <input type="radio" name="user-favorites" id="user-fav-anime" <?= $i === 0 ? 'checked' : '' ?> />
<label for="user-fav-anime">Anime</label> <label for="user-fav-anime">Anime</label>
<section class="content full-width media-wrap"> <section class="content full-width media-wrap">
<?php foreach($favorites['anime'] as $anime): ?> <?php foreach($data['favorites']['anime'] as $anime): ?>
<article class="media"> <article class="media">
<?php <?php
$link = $url->generate('anime.details', ['id' => $anime['slug']]); $link = $url->generate('anime.details', ['id' => $anime['slug']]);
@ -127,11 +103,11 @@ use Aviat\AnimeClient\API\Kitsu;
</section> </section>
<?php $i++; ?> <?php $i++; ?>
<?php endif ?> <?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' : '' ?> /> <input type="radio" name="user-favorites" id="user-fav-manga" <?= $i === 0 ? 'checked' : '' ?> />
<label for="user-fav-manga">Manga</label> <label for="user-fav-manga">Manga</label>
<section class="content full-width media-wrap"> <section class="content full-width media-wrap">
<?php foreach($favorites['manga'] as $manga): ?> <?php foreach($data['favorites']['manga'] as $manga): ?>
<article class="media"> <article class="media">
<?php <?php
$link = $url->generate('manga.details', ['id' => $manga['slug']]); $link = $url->generate('manga.details', ['id' => $manga['slug']]);

View File

@ -22,7 +22,7 @@
"aura/html": "^2.0", "aura/html": "^2.0",
"aura/router": "^3.0", "aura/router": "^3.0",
"aura/session": "^2.0", "aura/session": "^2.0",
"aviat/banker": "^1.0.0", "aviat/banker": "^2.0.0",
"aviat/ion": "^2.4.1", "aviat/ion": "^2.4.1",
"ext-iconv": "*", "ext-iconv": "*",
"ext-json": "*", "ext-json": "*",
@ -37,28 +37,30 @@
}, },
"require-dev": { "require-dev": {
"consolidation/robo": "~1.0", "consolidation/robo": "~1.0",
"filp/whoops": "^2.1",
"henrikbjorn/lurker": "^1.1.0", "henrikbjorn/lurker": "^1.1.0",
"pdepend/pdepend": "^2.2", "pdepend/pdepend": "^2.2",
"phploc/phploc": "^4.0", "phploc/phploc": "^4.0",
"phpmd/phpmd": "^2.4", "phpmd/phpmd": "^2.4",
"phpstan/phpstan": "^0.9.1", "phpstan/phpstan": "^0.10.5",
"phpunit/phpunit": "^6.0", "phpunit/phpunit": "^7.4.3",
"roave/security-advisories": "dev-master", "roave/security-advisories": "dev-master",
"robmorgan/phinx": "^0.10.6", "robmorgan/phinx": "^0.10.6",
"sebastian/phpcpd": "^3.0", "sebastian/phpcpd": "^4.1.0",
"spatie/phpunit-snapshot-assertions": "^1.2.0", "spatie/phpunit-snapshot-assertions": "^1.2.0",
"squizlabs/php_codesniffer": "^3.2.2", "squizlabs/php_codesniffer": "^3.2.2",
"symfony/var-dumper": "^4.0.1", "symfony/var-dumper": "^4.0.1",
"theseer/phpdox": "^0.11.0", "theseer/phpdox": "*"
"filp/whoops": "^2.1"
}, },
"scripts": { "scripts": {
"build": "vendor/bin/robo build", "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", "clean": "vendor/bin/robo clean",
"coverage": "phpdbg -qrr -- vendor/bin/phpunit -c build", "coverage": "phpdbg -qrr -- vendor/bin/phpunit -c build",
"phpstan": "phpstan analyse -l 4 -c phpstan.neon src tests ./console index.php", "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" "test": "vendor/bin/phpunit"
}, },
"scripts-descriptions": { "scripts-descriptions": {

View File

@ -47,12 +47,12 @@ $CONF_DIR = _dir($APP_DIR, 'config');
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Dependency Injection setup // Dependency Injection setup
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
$baseConfig = require $APPCONF_DIR . '/base_config.php'; $baseConfig = require "{$APPCONF_DIR}/base_config.php";
$di = require $APP_DIR . '/bootstrap.php'; $di = require "{$APP_DIR}/bootstrap.php";
$config = loadToml($CONF_DIR); $config = loadToml($CONF_DIR);
$overrideFile = $CONF_DIR . '/admin-override.toml'; $overrideFile = "{$CONF_DIR}/admin-override.toml";
$overrideConfig = file_exists($overrideFile) $overrideConfig = file_exists($overrideFile)
? loadTomlFile($overrideFile) ? loadTomlFile($overrideFile)
: []; : [];

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
undefined

View 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
View 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}

View File

@ -359,7 +359,7 @@ td .media-wrap-flex {
display: inline-block; display: inline-block;
text-align: center; text-align: center;
width: 220px; width: 220px;
height: 311px; height: 312px;
margin: var(--normal-padding); margin: var(--normal-padding);
z-index: 0; z-index: 0;
background: rgba(0, 0, 0, 0.15); background: rgba(0, 0, 0, 0.15);
@ -423,7 +423,7 @@ picture.cover {
background: var(--title-overlay); */ background: var(--title-overlay); */
content: ''; content: '';
display: block; display: block;
height: 311px; height: 312px;
left: 0; left: 0;
position: absolute; position: absolute;
top: 0; top: 0;

View File

@ -39,23 +39,26 @@
table .align-right, table .align-right,
table.align-center { table.align-center {
border: 0; border: 0;
display: block; /* display: block; */
margin: 0 auto; margin-left: auto;
margin-right: auto;
text-align: left; text-align: left;
width: 100%; width: 100%;
} }
table tbody {
width: 100%;
}
table td { table td {
display: inline-block; display: inline-block;
} }
table tbody,
table.media-details {
width: 100%;
}
table.media-details td { table.media-details td {
display: block; display: block;
text-align: left !important; text-align: left !important;
width: 100%;
} }
table thead { table thead {

View File

@ -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= "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)}); 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(); 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_'+ 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=
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'+ 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/'+
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; 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/'+
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/'+ 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="'+
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')}); 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+
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, '</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(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, 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=
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 "+ 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=
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()}})}); "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]);
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= 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,
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", 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=
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=== 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")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()}})})})(); 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 //# sourceMappingURL=scripts-authed.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -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= "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)}); 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(); 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 //# sourceMappingURL=scripts.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,10 @@
import './base/events.js'; import './base/events.js';
/* if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(reg => { navigator.serviceWorker.register('/sw.js').then(reg => {
console.log('Service worker registered', reg.scope); console.log('Service worker registered', reg.scope);
}).catch(error => { }).catch(error => {
console.error('Failed to register service worker', error); console.error('Failed to register service worker', error);
}); });
} */ }

View File

@ -8,7 +8,9 @@ const cssNext = require('postcss-cssnext');
const cssNano = require('cssnano'); const cssNano = require('cssnano');
const css = fs.readFileSync('css/all.css', 'utf-8'); const css = fs.readFileSync('css/all.css', 'utf-8');
const darkCss = fs.readFileSync('css/dark-override.css', 'utf-8');
// Basic theme
postcss() postcss()
.use(atImport()) .use(atImport())
.use(cssNext()) .use(cssNext())
@ -24,6 +26,24 @@ postcss()
from: 'css/all.css', from: 'css/all.css',
to: 'css/app.min.css' to: 'css/app.min.css'
}).then(result => { }).then(result => {
fs.writeFileSync('css/app.min.css', result.css); fs.writeFileSync('css/app.min.css', result.css);
fs.writeFileSync('css/app.min.css.map', result.map); });
});
// 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);
});

View File

@ -17,6 +17,7 @@
namespace Aviat\AnimeClient\API; namespace Aviat\AnimeClient\API;
use function Amp\Promise\wait; use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use Amp; use Amp;
use Amp\Artax\{FormBody, Request}; use Amp\Artax\{FormBody, Request};
@ -250,7 +251,7 @@ class APIRequestBuilder {
*/ */
public function getResponseData(Request $request) public function getResponseData(Request $request)
{ {
$response = wait((new HummingbirdClient)->request($request)); $response = getResponse($request);
return wait($response->getBody()); return wait($response->getBody());
} }
@ -315,7 +316,7 @@ class APIRequestBuilder {
* @param string $type * @param string $type
* @return void * @return void
*/ */
private function resetState($url, $type = 'GET') private function resetState($url, $type = 'GET'): void
{ {
$requestUrl = $url ?: $this->baseUrl; $requestUrl = $url ?: $this->baseUrl;

View File

@ -65,7 +65,7 @@ final class Anilist {
MangaReadingStatus::PLAN_TO_READ => KMRS::PLAN_TO_READ, MangaReadingStatus::PLAN_TO_READ => KMRS::PLAN_TO_READ,
]; ];
public static function getIdToWatchingStatusMap() public static function getIdToWatchingStatusMap(): array
{ {
return [ return [
'CURRENT' => AnimeWatchingStatus::WATCHING, 'CURRENT' => AnimeWatchingStatus::WATCHING,
@ -77,7 +77,7 @@ final class Anilist {
]; ];
} }
public static function getIdToReadingStatusMap() public static function getIdToReadingStatusMap(): array
{ {
return [ return [
'CURRENT' => MangaReadingStatus::READING, 'CURRENT' => MangaReadingStatus::READING,

View File

@ -19,14 +19,12 @@ namespace Aviat\AnimeClient\API\Anilist;
use const Aviat\AnimeClient\USER_AGENT; use const Aviat\AnimeClient\USER_AGENT;
use function Amp\Promise\wait; use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use Amp\Artax\Request; use Amp\Artax\Request;
use Amp\Artax\Response; use Amp\Artax\Response;
use Aviat\AnimeClient\API\{ use Aviat\AnimeClient\API\Anilist;
Anilist,
HummingbirdClient
};
use Aviat\Ion\Json; use Aviat\Ion\Json;
use Aviat\Ion\Di\ContainerAware; use Aviat\Ion\Di\ContainerAware;
@ -200,7 +198,7 @@ trait AnilistTrait {
} }
$request = $this->setUpRequest($url, $options); $request = $this->setUpRequest($url, $options);
$response = wait((new HummingbirdClient)->request($request)); $response = getResponse($request);
$logger->debug('Anilist response', [ $logger->debug('Anilist response', [
'status' => $response->getStatus(), 'status' => $response->getStatus(),
@ -221,7 +219,7 @@ trait AnilistTrait {
$logger = $this->container->getLogger('anilist-request'); $logger = $this->container->getLogger('anilist-request');
} }
$response = wait((new HummingbirdClient)->request($request)); $response = getResponse($request);
$logger->debug('Anilist response', [ $logger->debug('Anilist response', [
'status' => $response->getStatus(), 'status' => $response->getStatus(),

View File

@ -100,7 +100,7 @@ final class Model
$config = $this->container->get('config'); $config = $this->container->get('config');
$anilistUser = $config->get(['anilist', 'username']); $anilistUser = $config->get(['anilist', 'username']);
if ( ! is_string($anilistUser)) if ( ! \is_string($anilistUser))
{ {
throw new InvalidArgumentException('Anilist username is not defined in config'); 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 * Create a list item with all the relevant data
* *
* @param array $data * @param array $data
* @param string $type
* @return Request * @return Request
*/ */
public function createFullListItem(array $data, string $type = 'anime'): Request public function createFullListItem(array $data): Request
{ {
$createData = $data['data']; $createData = $data['data'];
$mediaId = $this->getMediaIdFromMalId($data['mal_id']); $mediaId = $this->getMediaIdFromMalId($data['mal_id']);
@ -168,6 +167,7 @@ final class Model
* Get the data for a specific list item, generally for editing * Get the data for a specific list item, generally for editing
* *
* @param string $malId - The unique identifier of that list item * @param string $malId - The unique identifier of that list item
* @param string $type - Them media type (anime/manga)
* @return mixed * @return mixed
*/ */
public function getListItem(string $malId, string $type): array public function getListItem(string $malId, string $type): array
@ -185,6 +185,7 @@ final class Model
* Increase the watch count for the current list item * Increase the watch count for the current list item
* *
* @param FormItem $data * @param FormItem $data
* @param string $type - Them media type (anime/manga)
* @return Request * @return Request
*/ */
public function incrementListItem(FormItem $data, string $type): Request public function incrementListItem(FormItem $data, string $type): Request
@ -198,7 +199,7 @@ final class Model
* Modify a list item * Modify a list item
* *
* @param FormItem $data * @param FormItem $data
* @param int [$id] * @param string $type - Them media type (anime/manga)
* @return Request * @return Request
*/ */
public function updateListItem(FormItem $data, string $type): 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 * Get the id of the specific list entry from the malId
* *
* @param string $malId * @param string $malId
* @param string $type - The media type (anime/manga)
* @return string * @return string
*/ */
public function getListIdFromMalId(string $malId, string $type): ?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 * this way is more accurate than getting the list item id
* directly from the MAL id * directly from the MAL id
*/ */
@ -248,13 +250,6 @@ final class Model
'userName' => $anilistUser, 'userName' => $anilistUser,
]); ]);
/* dump([
'media_id' => $mediaId,
'userName' => $anilistUser,
'response' => $info,
]);
die(); */
return (string)$info['data']['MediaList']['id']; return (string)$info['data']['MediaList']['id'];
} }
@ -272,12 +267,6 @@ final class Model
'type' => mb_strtoupper($type), 'type' => mb_strtoupper($type),
]); ]);
/* dump([
'mal_id' => $malId,
'response' => $info,
]);
die(); */
return (string)$info['data']['Media']['id']; return (string)$info['data']['Media']['id'];
} }
} }

View File

@ -17,6 +17,7 @@
namespace Aviat\AnimeClient\API\Anilist\Transformer; namespace Aviat\AnimeClient\API\Anilist\Transformer;
use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\Anilist as AnilistStatus; 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\API\Mapping\AnimeWatchingStatus;
use Aviat\AnimeClient\Types\{AnimeListItem, FormItem}; use Aviat\AnimeClient\Types\{AnimeListItem, FormItem};
@ -39,6 +40,8 @@ class AnimeListTransformer extends AbstractTransformer {
*/ */
public function untransform(array $item): FormItem public function untransform(array $item): FormItem
{ {
$reconsuming = $item['status'] === AnilistStatus::REPEATING;
return new FormItem([ return new FormItem([
'id' => $item['id'], 'id' => $item['id'],
'mal_id' => $item['media']['idMal'], 'mal_id' => $item['media']['idMal'],
@ -46,26 +49,16 @@ class AnimeListTransformer extends AbstractTransformer {
'notes' => $item['notes'] ?? '', 'notes' => $item['notes'] ?? '',
'private' => $item['private'], 'private' => $item['private'],
'progress' => $item['progress'], 'progress' => $item['progress'],
'rating' => $item['score'], 'rating' => $item['score'] ?? NULL,
'reconsumeCount' => $item['repeat'], 'reconsumeCount' => $item['repeat'],
'reconsuming' => $item['status'] === AnilistStatus::REPEATING, 'reconsuming' => $reconsuming,
'status' => AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']], 'status' => $reconsuming
? KitsuStatus::WATCHING
:AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']],
'updatedAt' => (new DateTime()) 'updatedAt' => (new DateTime())
->setTimestamp($item['updatedAt']) ->setTimestamp($item['updatedAt'])
->format(DateTime::W3C) ->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);
}
} }

View File

@ -18,6 +18,7 @@ namespace Aviat\AnimeClient\API\Anilist\Transformer;
use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Anilist as AnilistStatus; use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Anilist as AnilistStatus;
use Aviat\AnimeClient\API\Mapping\MangaReadingStatus; use Aviat\AnimeClient\API\Mapping\MangaReadingStatus;
use Aviat\AnimeClient\Types\MangaListItem;
use Aviat\AnimeClient\Types\FormItem; use Aviat\AnimeClient\Types\FormItem;
use Aviat\Ion\Transformer\AbstractTransformer; use Aviat\Ion\Transformer\AbstractTransformer;
@ -28,7 +29,7 @@ class MangaListTransformer extends AbstractTransformer {
public function transform($item) 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);
}
} }

View File

@ -22,10 +22,10 @@ use Aviat\Ion\Enum;
* Possible values for watching status for the current anime * Possible values for watching status for the current anime
*/ */
final class Anilist extends Enum { final class Anilist extends Enum {
const WATCHING = 'CURRENT'; public const WATCHING = 'CURRENT';
const COMPLETED = 'COMPLETED'; public const COMPLETED = 'COMPLETED';
const ON_HOLD = 'PAUSED'; public const ON_HOLD = 'PAUSED';
const DROPPED = 'DROPPED'; public const DROPPED = 'DROPPED';
const PLAN_TO_WATCH = 'PLANNING'; public const PLAN_TO_WATCH = 'PLANNING';
const REPEATING = 'REPEATING'; public const REPEATING = 'REPEATING';
} }

View File

@ -22,9 +22,9 @@ use Aviat\Ion\Enum;
* Possible values for watching status for the current anime * Possible values for watching status for the current anime
*/ */
final class Kitsu extends Enum { final class Kitsu extends Enum {
const WATCHING = 'current'; public const WATCHING = 'current';
const PLAN_TO_WATCH = 'planned'; public const PLAN_TO_WATCH = 'planned';
const ON_HOLD = 'on_hold'; public const ON_HOLD = 'on_hold';
const DROPPED = 'dropped'; public const DROPPED = 'dropped';
const COMPLETED = 'completed'; public const COMPLETED = 'completed';
} }

View File

@ -16,16 +16,16 @@
namespace Aviat\AnimeClient\API\Enum\AnimeWatchingStatus; namespace Aviat\AnimeClient\API\Enum\AnimeWatchingStatus;
use Aviat\Ion\Enum as Enum; use Aviat\Ion\Enum;
/** /**
* Possible values for current watching status of anime * Possible values for current watching status of anime
*/ */
final class Route extends Enum { final class Route extends Enum {
const ALL = 'all'; public const ALL = 'all';
const WATCHING = 'watching'; public const WATCHING = 'watching';
const PLAN_TO_WATCH = 'plan_to_watch'; public const PLAN_TO_WATCH = 'plan_to_watch';
const DROPPED = 'dropped'; public const DROPPED = 'dropped';
const ON_HOLD = 'on_hold'; public const ON_HOLD = 'on_hold';
const COMPLETED = 'completed'; public const COMPLETED = 'completed';
} }

View File

@ -16,16 +16,16 @@
namespace Aviat\AnimeClient\API\Enum\AnimeWatchingStatus; namespace Aviat\AnimeClient\API\Enum\AnimeWatchingStatus;
use Aviat\Ion\Enum as Enum; use Aviat\Ion\Enum;
/** /**
* Possible values for current watching status of anime * Possible values for current watching status of anime
*/ */
final class Title extends Enum { final class Title extends Enum {
const ALL = 'All'; public const ALL = 'All';
const WATCHING = 'Currently Watching'; public const WATCHING = 'Currently Watching';
const PLAN_TO_WATCH = 'Plan to Watch'; public const PLAN_TO_WATCH = 'Plan to Watch';
const DROPPED = 'Dropped'; public const DROPPED = 'Dropped';
const ON_HOLD = 'On Hold'; public const ON_HOLD = 'On Hold';
const COMPLETED = 'Completed'; public const COMPLETED = 'Completed';
} }

View File

@ -22,10 +22,10 @@ use Aviat\Ion\Enum;
* Possible values for watching status for the current anime * Possible values for watching status for the current anime
*/ */
final class Anilist extends Enum { final class Anilist extends Enum {
const READING = 'CURRENT'; public const READING = 'CURRENT';
const COMPLETED = 'COMPLETED'; public const COMPLETED = 'COMPLETED';
const ON_HOLD = 'PAUSED'; public const ON_HOLD = 'PAUSED';
const DROPPED = 'DROPPED'; public const DROPPED = 'DROPPED';
const PLAN_TO_READ = 'PLANNING'; public const PLAN_TO_READ = 'PLANNING';
const REPEATING = 'REPEATING'; public const REPEATING = 'REPEATING';
} }

View File

@ -22,9 +22,9 @@ use Aviat\Ion\Enum;
* Possible values for current reading status of manga * Possible values for current reading status of manga
*/ */
final class Kitsu extends Enum { final class Kitsu extends Enum {
const READING = 'current'; public const READING = 'current';
const PLAN_TO_READ = 'planned'; public const PLAN_TO_READ = 'planned';
const DROPPED = 'dropped'; public const DROPPED = 'dropped';
const ON_HOLD = 'on_hold'; public const ON_HOLD = 'on_hold';
const COMPLETED = 'completed'; public const COMPLETED = 'completed';
} }

View File

@ -22,10 +22,10 @@ use Aviat\Ion\Enum;
* Possible values for current reading status of manga * Possible values for current reading status of manga
*/ */
final class Route extends Enum { final class Route extends Enum {
const ALL = 'all'; public const ALL = 'all';
const READING = 'reading'; public const READING = 'reading';
const PLAN_TO_READ = 'plan_to_read'; public const PLAN_TO_READ = 'plan_to_read';
const DROPPED = 'dropped'; public const DROPPED = 'dropped';
const ON_HOLD = 'on_hold'; public const ON_HOLD = 'on_hold';
const COMPLETED = 'completed'; public const COMPLETED = 'completed';
} }

View File

@ -22,10 +22,10 @@ use Aviat\Ion\Enum;
* Possible values for current reading status of manga * Possible values for current reading status of manga
*/ */
final class Title extends Enum { final class Title extends Enum {
const ALL = 'All'; public const ALL = 'All';
const READING = 'Currently Reading'; public const READING = 'Currently Reading';
const PLAN_TO_READ = 'Plan to Read'; public const PLAN_TO_READ = 'Plan to Read';
const DROPPED = 'Dropped'; public const DROPPED = 'Dropped';
const ON_HOLD = 'On Hold'; public const ON_HOLD = 'On Hold';
const COMPLETED = 'Completed'; public const COMPLETED = 'Completed';
} }

File diff suppressed because it is too large Load Diff

View File

@ -21,9 +21,7 @@ namespace Aviat\AnimeClient\API;
*/ */
final class JsonAPI { final class JsonAPI {
/** /*
* The full data array
*
* Basic structure is generally like so: * Basic structure is generally like so:
* [ * [
* 'id' => '12016665', * 'id' => '12016665',
@ -35,10 +33,7 @@ final class JsonAPI {
* *
* ] * ]
* ] * ]
*
* @var array
*/ */
protected $data = [];
/** /**
* Inline all included data * Inline all included data
@ -214,8 +209,7 @@ final class JsonAPI {
$dataType = $props['data']['type']; $dataType = $props['data']['type'];
$relationship =& $organized[$type][$id]['relationships'][$relType]; $relationship =& $organized[$type][$id]['relationships'][$relType];
unset($relationship['links']); unset($relationship['links'], $relationship['data']);
unset($relationship['data']);
if ($relType === $dataType) if ($relType === $dataType)
{ {

View File

@ -23,11 +23,11 @@ use DateTimeImmutable;
* Data massaging helpers for the Kitsu API * Data massaging helpers for the Kitsu API
*/ */
final class Kitsu { final class Kitsu {
const AUTH_URL = 'https://kitsu.io/api/oauth/token'; public const AUTH_URL = 'https://kitsu.io/api/oauth/token';
const AUTH_USER_ID_KEY = 'kitsu-auth-userid'; public const AUTH_USER_ID_KEY = 'kitsu-auth-userid';
const AUTH_TOKEN_CACHE_KEY = 'kitsu-auth-token'; public const AUTH_TOKEN_CACHE_KEY = 'kitsu-auth-token';
const AUTH_TOKEN_EXP_CACHE_KEY = 'kitsu-auth-token-expires'; public const AUTH_TOKEN_EXP_CACHE_KEY = 'kitsu-auth-token-expires';
const AUTH_TOKEN_REFRESH_CACHE_KEY = 'kitsu-auth-token-refresh'; public const AUTH_TOKEN_REFRESH_CACHE_KEY = 'kitsu-auth-token-refresh';
/** /**
* Determine whether an anime is airing, finished airing, or has not yet aired * Determine whether an anime is airing, finished airing, or has not yet aired
@ -163,7 +163,7 @@ final class Kitsu {
'dubs' => $streamingLink['dubs'] 'dubs' => $streamingLink['dubs']
]; ];
} }
usort($links, function ($a, $b) { usort($links, function ($a, $b) {
return $a['meta']['name'] <=> $b['meta']['name']; return $a['meta']['name'] <=> $b['meta']['name'];
}); });

View File

@ -22,8 +22,8 @@ use Aviat\Ion\Enum as BaseEnum;
* Status of when anime is being/was/will be aired * Status of when anime is being/was/will be aired
*/ */
final class AnimeAiringStatus extends BaseEnum { final class AnimeAiringStatus extends BaseEnum {
const NOT_YET_AIRED = 'Not Yet Aired'; public const NOT_YET_AIRED = 'Not Yet Aired';
const AIRING = 'Currently Airing'; public const AIRING = 'Currently Airing';
const FINISHED_AIRING = 'Finished Airing'; public const FINISHED_AIRING = 'Finished Airing';
} }
// End of AnimeAiringStatus.php // End of AnimeAiringStatus.php

View File

@ -16,6 +16,7 @@
namespace Aviat\AnimeClient\API\Kitsu; namespace Aviat\AnimeClient\API\Kitsu;
use const Aviat\AnimeClient\USER_AGENT;
use Aviat\AnimeClient\API\APIRequestBuilder; use Aviat\AnimeClient\API\APIRequestBuilder;
final class KitsuRequestBuilder extends APIRequestBuilder { final class KitsuRequestBuilder extends APIRequestBuilder {
@ -32,7 +33,7 @@ final class KitsuRequestBuilder extends APIRequestBuilder {
* @var array * @var array
*/ */
protected $defaultHeaders = [ protected $defaultHeaders = [
'User-Agent' => "Tim's Anime Client/4.0", 'User-Agent' => USER_AGENT,
'Accept' => 'application/vnd.api+json', 'Accept' => 'application/vnd.api+json',
'Content-Type' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json',
'CLIENT_ID' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd', 'CLIENT_ID' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd',

View File

@ -19,12 +19,12 @@ namespace Aviat\AnimeClient\API\Kitsu;
use const Aviat\AnimeClient\SESSION_SEGMENT; use const Aviat\AnimeClient\SESSION_SEGMENT;
use function Amp\Promise\wait; use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use Amp\Artax\Request; use Amp\Artax\Request;
use Aviat\AnimeClient\AnimeClient; use Amp\Artax\Response;
use Aviat\AnimeClient\API\{ use Aviat\AnimeClient\API\{
FailedResponseException, FailedResponseException,
HummingbirdClient,
Kitsu as K Kitsu as K
}; };
use Aviat\Ion\Json; use Aviat\Ion\Json;
@ -121,7 +121,7 @@ trait KitsuTrait {
* @param array $options * @param array $options
* @return Response * @return Response
*/ */
private function getResponse(string $type, string $url, array $options = []) private function getResponse(string $type, string $url, array $options = []): Response
{ {
$logger = NULL; $logger = NULL;
if ($this->getContainer()) if ($this->getContainer())
@ -131,7 +131,7 @@ trait KitsuTrait {
$request = $this->setUpRequest($type, $url, $options); $request = $this->setUpRequest($type, $url, $options);
$response = wait((new HummingbirdClient)->request($request)); $response = getResponse($request);
if ($logger) if ($logger)
{ {

View File

@ -19,12 +19,10 @@ namespace Aviat\AnimeClient\API\Kitsu;
use const Aviat\AnimeClient\SESSION_SEGMENT; use const Aviat\AnimeClient\SESSION_SEGMENT;
use function Amp\Promise\wait; use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use Amp\Artax\Request; use Amp\Artax\Request;
use Aviat\AnimeClient\API\{ use Aviat\AnimeClient\API\ListItemInterface;
HummingbirdClient,
ListItemInterface
};
use Aviat\AnimeClient\Types\FormItemData; use Aviat\AnimeClient\Types\FormItemData;
use Aviat\Ion\Di\ContainerAware; use Aviat\Ion\Di\ContainerAware;
use Aviat\Ion\Json; use Aviat\Ion\Json;
@ -37,7 +35,7 @@ final class ListItem implements ListItemInterface {
use KitsuTrait; use KitsuTrait;
public function create(array $data): Request public function create(array $data): Request
{ {
$body = [ $body = [
'data' => [ 'data' => [
'type' => 'libraryEntries', 'type' => 'libraryEntries',
@ -61,7 +59,7 @@ final class ListItem implements ListItemInterface {
] ]
] ]
]; ];
if (array_key_exists('notes', $data)) if (array_key_exists('notes', $data))
{ {
$body['data']['attributes']['notes'] = $data['notes']; $body['data']['attributes']['notes'] = $data['notes'];
@ -78,8 +76,6 @@ final class ListItem implements ListItemInterface {
return $request->setJsonBody($body) return $request->setJsonBody($body)
->getFullRequest(); ->getFullRequest();
// return ($response->getStatus() === 201);
} }
public function delete(string $id): Request public function delete(string $id): Request
@ -93,8 +89,6 @@ final class ListItem implements ListItemInterface {
} }
return $request->getFullRequest(); return $request->getFullRequest();
// return ($response->getStatus() === 204);
} }
public function get(string $id): array public function get(string $id): array
@ -112,8 +106,7 @@ final class ListItem implements ListItemInterface {
} }
$request = $request->getFullRequest(); $request = $request->getFullRequest();
$response = getResponse($request);
$response = wait((new HummingbirdClient)->request($request));
return Json::decode(wait($response->getBody())); return Json::decode(wait($response->getBody()));
} }

View File

@ -91,9 +91,10 @@ final class Model {
{ {
$this->animeTransformer = new AnimeTransformer(); $this->animeTransformer = new AnimeTransformer();
$this->animeListTransformer = new AnimeListTransformer(); $this->animeListTransformer = new AnimeListTransformer();
$this->listItem = $listItem;
$this->mangaTransformer = new MangaTransformer(); $this->mangaTransformer = new MangaTransformer();
$this->mangaListTransformer = new MangaListTransformer(); $this->mangaListTransformer = new MangaListTransformer();
$this->listItem = $listItem;
} }
/** /**
@ -265,7 +266,7 @@ final class Model {
public function getUserData(string $username): array public function getUserData(string $username): array
{ {
// $userId = $this->getUserIdByUsername($username); // $userId = $this->getUserIdByUsername($username);
$data = $this->getRequest("users", [ $data = $this->getRequest('users', [
'query' => [ 'query' => [
'filter' => [ 'filter' => [
'name' => $username, 'name' => $username,
@ -334,7 +335,7 @@ final class Model {
* @param string $type "anime" or "manga" * @param string $type "anime" or "manga"
* @return string|NULL * @return string|NULL
*/ */
public function getKitsuIdFromMALId(string $malId, string $type="anime") public function getKitsuIdFromMALId(string $malId, string $type='anime'): ?string
{ {
$options = [ $options = [
'query' => [ 'query' => [
@ -369,7 +370,7 @@ final class Model {
* @param string $slug * @param string $slug
* @return Anime * @return Anime
*/ */
public function getAnime(string $slug) public function getAnime(string $slug): Anime
{ {
$baseData = $this->getRawMediaData('anime', $slug); $baseData = $this->getRawMediaData('anime', $slug);
@ -523,7 +524,7 @@ final class Model {
* @param string $kitsuAnimeId The id of the anime on Kitsu * @param string $kitsuAnimeId The id of the anime on Kitsu
* @return string|null Returns the mal id if it exists, otherwise null * @return string|null Returns the mal id if it exists, otherwise null
*/ */
public function getMalIdForAnime(string $kitsuAnimeId) public function getMalIdForAnime(string $kitsuAnimeId): ?string
{ {
$options = [ $options = [
'query' => [ 'query' => [
@ -625,7 +626,7 @@ final class Model {
* Get information about a particular manga * Get information about a particular manga
* *
* @param string $mangaId * @param string $mangaId
* @return array * @return MangaPage
*/ */
public function getMangaById(string $mangaId): MangaPage public function getMangaById(string $mangaId): MangaPage
{ {
@ -808,7 +809,7 @@ final class Model {
* @param string $kitsuMangaId The id of the manga on Kitsu * @param string $kitsuMangaId The id of the manga on Kitsu
* @return string|null Returns the mal id if it exists, otherwise null * @return string|null Returns the mal id if it exists, otherwise null
*/ */
public function getMalIdForManga(string $kitsuMangaId) public function getMalIdForManga(string $kitsuMangaId): ?string
{ {
$options = [ $options = [
'query' => [ '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 $type
* @param string $id * @param string $id

View File

@ -18,7 +18,6 @@ namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\Kitsu; use Aviat\AnimeClient\API\Kitsu;
use Aviat\AnimeClient\Types\{ use Aviat\AnimeClient\Types\{
Anime,
FormItem, FormItem,
AnimeListItem AnimeListItem
}; };
@ -95,7 +94,7 @@ final class AnimeListTransformer extends AbstractTransformer {
'started' => $anime['startDate'], 'started' => $anime['startDate'],
'ended' => $anime['endDate'] 'ended' => $anime['endDate']
], ],
'anime' => new Anime([ 'anime' => [
'id' => $animeId, 'id' => $animeId,
'age_rating' => $anime['ageRating'], 'age_rating' => $anime['ageRating'],
'title' => $title, 'title' => $title,
@ -105,7 +104,7 @@ final class AnimeListTransformer extends AbstractTransformer {
'cover_image' => $anime['posterImage']['small'], 'cover_image' => $anime['posterImage']['small'],
'genres' => $genres, 'genres' => $genres,
'streaming_links' => $streamingLinks, 'streaming_links' => $streamingLinks,
]), ],
'watching_status' => $item['attributes']['status'], 'watching_status' => $item['attributes']['status'],
'notes' => $item['attributes']['notes'], 'notes' => $item['attributes']['notes'],
'rewatching' => (bool) $item['attributes']['reconsuming'], 'rewatching' => (bool) $item['attributes']['reconsuming'],

View File

@ -17,7 +17,7 @@
namespace Aviat\AnimeClient\API\Kitsu\Transformer; namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\{JsonAPI, Kitsu}; use Aviat\AnimeClient\API\{JsonAPI, Kitsu};
use Aviat\AnimeClient\Types\Anime; use Aviat\AnimeClient\Types\AnimePage;
use Aviat\Ion\Transformer\AbstractTransformer; use Aviat\Ion\Transformer\AbstractTransformer;
/** /**
@ -30,9 +30,9 @@ final class AnimeTransformer extends AbstractTransformer {
* logical and workable structure * logical and workable structure
* *
* @param array $item API library item * @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']); $item['included'] = JsonAPI::organizeIncludes($item['included']);
$genres = $item['included']['categories'] ?? []; $genres = $item['included']['categories'] ?? [];
@ -40,13 +40,74 @@ final class AnimeTransformer extends AbstractTransformer {
sort($item['genres']); sort($item['genres']);
$title = $item['canonicalTitle']; $title = $item['canonicalTitle'];
$titles = Kitsu::filterTitles($item); $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' => $item['ageRating'],
'age_rating_guide' => $item['ageRatingGuide'], 'age_rating_guide' => $item['ageRatingGuide'],
'characters' => $characters,
'cover_image' => $item['posterImage']['small'], 'cover_image' => $item['posterImage']['small'],
'episode_count' => $item['episodeCount'], 'episode_count' => $item['episodeCount'],
'episode_length' => $item['episodeLength'], 'episode_length' => $item['episodeLength'],
@ -55,6 +116,7 @@ final class AnimeTransformer extends AbstractTransformer {
'included' => $item['included'], 'included' => $item['included'],
'show_type' => $this->string($item['showType'])->upperCaseFirst()->__toString(), 'show_type' => $this->string($item['showType'])->upperCaseFirst()->__toString(),
'slug' => $item['slug'], 'slug' => $item['slug'],
'staff' => $staff,
'status' => Kitsu::getAiringStatus($item['startDate'], $item['endDate']), 'status' => Kitsu::getAiringStatus($item['startDate'], $item['endDate']),
'streaming_links' => Kitsu::parseStreamingLinks($item['included']), 'streaming_links' => Kitsu::parseStreamingLinks($item['included']),
'synopsis' => $item['synopsis'], 'synopsis' => $item['synopsis'],

View 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;
}
}

View File

@ -51,13 +51,77 @@ final class MangaTransformer extends AbstractTransformer {
$rawTitles = array_values($item['titles']); $rawTitles = array_values($item['titles']);
$titles = array_unique(array_diff($rawTitles, [$title])); $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([ return new MangaPage([
'characters' => $characters,
'chapter_count' => $this->count($item['chapterCount']), 'chapter_count' => $this->count($item['chapterCount']),
'cover_image' => $item['posterImage']['small'], 'cover_image' => $item['posterImage']['small'],
'genres' => $genres, 'genres' => $genres,
'id' => $item['id'], 'id' => $item['id'],
'included' => $item['included'], 'included' => $item['included'],
'manga_type' => $item['mangaType'], 'manga_type' => $item['mangaType'],
'staff' => $staff,
'synopsis' => $item['synopsis'], 'synopsis' => $item['synopsis'],
'title' => $title, 'title' => $title,
'titles' => $titles, 'titles' => $titles,

View 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;
}
}

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

View File

@ -24,7 +24,7 @@ use Aviat\Ion\Enum;
* and url route segments * and url route segments
*/ */
final class AnimeWatchingStatus extends Enum { final class AnimeWatchingStatus extends Enum {
const ANILIST_TO_KITSU = [ public const ANILIST_TO_KITSU = [
Anilist::WATCHING => Kitsu::WATCHING, Anilist::WATCHING => Kitsu::WATCHING,
Anilist::PLAN_TO_WATCH => Kitsu::PLAN_TO_WATCH, Anilist::PLAN_TO_WATCH => Kitsu::PLAN_TO_WATCH,
Anilist::COMPLETED => Kitsu::COMPLETED, Anilist::COMPLETED => Kitsu::COMPLETED,
@ -32,7 +32,7 @@ final class AnimeWatchingStatus extends Enum {
Anilist::DROPPED => Kitsu::DROPPED Anilist::DROPPED => Kitsu::DROPPED
]; ];
const KITSU_TO_ANILIST = [ public const KITSU_TO_ANILIST = [
Kitsu::WATCHING => Anilist::WATCHING, Kitsu::WATCHING => Anilist::WATCHING,
Kitsu::PLAN_TO_WATCH => Anilist::PLAN_TO_WATCH, Kitsu::PLAN_TO_WATCH => Anilist::PLAN_TO_WATCH,
Kitsu::COMPLETED => Anilist::COMPLETED, Kitsu::COMPLETED => Anilist::COMPLETED,
@ -40,7 +40,7 @@ final class AnimeWatchingStatus extends Enum {
Kitsu::DROPPED => Anilist::DROPPED Kitsu::DROPPED => Anilist::DROPPED
]; ];
const KITSU_TO_TITLE = [ public const KITSU_TO_TITLE = [
Kitsu::WATCHING => Title::WATCHING, Kitsu::WATCHING => Title::WATCHING,
Kitsu::PLAN_TO_WATCH => Title::PLAN_TO_WATCH, Kitsu::PLAN_TO_WATCH => Title::PLAN_TO_WATCH,
Kitsu::ON_HOLD => Title::ON_HOLD, Kitsu::ON_HOLD => Title::ON_HOLD,
@ -48,7 +48,7 @@ final class AnimeWatchingStatus extends Enum {
Kitsu::COMPLETED => Title::COMPLETED Kitsu::COMPLETED => Title::COMPLETED
]; ];
const ROUTE_TO_KITSU = [ public const ROUTE_TO_KITSU = [
Route::WATCHING => Kitsu::WATCHING, Route::WATCHING => Kitsu::WATCHING,
Route::PLAN_TO_WATCH => Kitsu::PLAN_TO_WATCH, Route::PLAN_TO_WATCH => Kitsu::PLAN_TO_WATCH,
Route::ON_HOLD => Kitsu::ON_HOLD, Route::ON_HOLD => Kitsu::ON_HOLD,
@ -56,7 +56,7 @@ final class AnimeWatchingStatus extends Enum {
Route::COMPLETED => Kitsu::COMPLETED Route::COMPLETED => Kitsu::COMPLETED
]; ];
const ROUTE_TO_TITLE = [ public const ROUTE_TO_TITLE = [
Route::ALL => Title::ALL, Route::ALL => Title::ALL,
Route::WATCHING => Title::WATCHING, Route::WATCHING => Title::WATCHING,
Route::PLAN_TO_WATCH => Title::PLAN_TO_WATCH, Route::PLAN_TO_WATCH => Title::PLAN_TO_WATCH,
@ -65,7 +65,7 @@ final class AnimeWatchingStatus extends Enum {
Route::COMPLETED => Title::COMPLETED Route::COMPLETED => Title::COMPLETED
]; ];
const TITLE_TO_ROUTE = [ public const TITLE_TO_ROUTE = [
Title::ALL => Route::ALL, Title::ALL => Route::ALL,
Title::WATCHING => Route::WATCHING, Title::WATCHING => Route::WATCHING,
Title::PLAN_TO_WATCH => Route::PLAN_TO_WATCH, Title::PLAN_TO_WATCH => Route::PLAN_TO_WATCH,

View File

@ -24,7 +24,7 @@ use Aviat\Ion\Enum;
* and url route segments * and url route segments
*/ */
final class MangaReadingStatus extends Enum { final class MangaReadingStatus extends Enum {
const ANILIST_TO_KITSU = [ public const ANILIST_TO_KITSU = [
Anilist::READING => Kitsu::READING, Anilist::READING => Kitsu::READING,
Anilist::PLAN_TO_READ => Kitsu::PLAN_TO_READ, Anilist::PLAN_TO_READ => Kitsu::PLAN_TO_READ,
Anilist::COMPLETED => Kitsu::COMPLETED, Anilist::COMPLETED => Kitsu::COMPLETED,
@ -32,7 +32,7 @@ final class MangaReadingStatus extends Enum {
Anilist::DROPPED => Kitsu::DROPPED Anilist::DROPPED => Kitsu::DROPPED
]; ];
const KITSU_TO_ANILIST = [ public const KITSU_TO_ANILIST = [
Kitsu::READING => Anilist::READING, Kitsu::READING => Anilist::READING,
Kitsu::PLAN_TO_READ => Anilist::PLAN_TO_READ, Kitsu::PLAN_TO_READ => Anilist::PLAN_TO_READ,
Kitsu::COMPLETED => Anilist::COMPLETED, Kitsu::COMPLETED => Anilist::COMPLETED,
@ -40,7 +40,7 @@ final class MangaReadingStatus extends Enum {
Kitsu::DROPPED => Anilist::DROPPED Kitsu::DROPPED => Anilist::DROPPED
]; ];
const KITSU_TO_TITLE = [ public const KITSU_TO_TITLE = [
Kitsu::READING => Title::READING, Kitsu::READING => Title::READING,
Kitsu::PLAN_TO_READ => Title::PLAN_TO_READ, Kitsu::PLAN_TO_READ => Title::PLAN_TO_READ,
Kitsu::COMPLETED => Title::COMPLETED, Kitsu::COMPLETED => Title::COMPLETED,
@ -48,7 +48,7 @@ final class MangaReadingStatus extends Enum {
Kitsu::DROPPED => Title::DROPPED, Kitsu::DROPPED => Title::DROPPED,
]; ];
const ROUTE_TO_KITSU = [ public const ROUTE_TO_KITSU = [
Route::PLAN_TO_READ => Kitsu::PLAN_TO_READ, Route::PLAN_TO_READ => Kitsu::PLAN_TO_READ,
Route::READING => Kitsu::READING, Route::READING => Kitsu::READING,
Route::COMPLETED => Kitsu::COMPLETED, Route::COMPLETED => Kitsu::COMPLETED,
@ -56,7 +56,7 @@ final class MangaReadingStatus extends Enum {
Route::ON_HOLD => Kitsu::ON_HOLD, Route::ON_HOLD => Kitsu::ON_HOLD,
]; ];
const ROUTE_TO_TITLE = [ public const ROUTE_TO_TITLE = [
Route::ALL => Title::ALL, Route::ALL => Title::ALL,
Route::PLAN_TO_READ => Title::PLAN_TO_READ, Route::PLAN_TO_READ => Title::PLAN_TO_READ,
Route::READING => Title::READING, Route::READING => Title::READING,
@ -65,7 +65,7 @@ final class MangaReadingStatus extends Enum {
Route::ON_HOLD => Title::ON_HOLD, Route::ON_HOLD => Title::ON_HOLD,
]; ];
const TITLE_TO_KITSU = [ public const TITLE_TO_KITSU = [
Title::PLAN_TO_READ => Kitsu::PLAN_TO_READ, Title::PLAN_TO_READ => Kitsu::PLAN_TO_READ,
Title::READING => Kitsu::READING, Title::READING => Kitsu::READING,
Title::COMPLETED => Kitsu::COMPLETED, Title::COMPLETED => Kitsu::COMPLETED,

View File

@ -18,6 +18,7 @@ namespace Aviat\AnimeClient\API;
use function Amp\call; use function Amp\call;
use function Amp\Promise\{all, wait}; use function Amp\Promise\{all, wait};
use function Aviat\AnimeClient\getApiClient;
/** /**
* Class to simplify making and validating simultaneous requests * Class to simplify making and validating simultaneous requests
@ -70,7 +71,8 @@ final class ParallelAPIRequest {
*/ */
public function makeRequests(): array public function makeRequests(): array
{ {
$client = new HummingbirdClient(); $client = getApiClient();
$promises = []; $promises = [];
foreach ($this->requests as $key => $url) foreach ($this->requests as $key => $url)
@ -92,7 +94,8 @@ final class ParallelAPIRequest {
*/ */
public function getResponses(): array public function getResponses(): array
{ {
$client = new HummingbirdClient(); $client = getApiClient();
$promises = []; $promises = [];
foreach ($this->requests as $key => $url) foreach ($this->requests as $key => $url)

View File

@ -16,6 +16,10 @@
namespace Aviat\AnimeClient; namespace Aviat\AnimeClient;
use function Amp\Promise\wait;
use Amp\Artax\{Client, DefaultClient, Response};
use Aviat\Ion\ConfigInterface; use Aviat\Ion\ConfigInterface;
use Yosymfony\Toml\{Toml, TomlBuilder}; use Yosymfony\Toml\{Toml, TomlBuilder};
@ -203,6 +207,37 @@ function checkFolderPermissions(ConfigInterface $config): array
return $errors; 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 * 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 int $height
* @param string $text * @param string $text
*/ */
function createPlaceholderImage ($path, $width, $height, $text = 'Image Unavailable') function createPlaceholderImage ($path, $width, $height, $text = 'Image Unavailable'): void
{ {
$width = $width ?? 200; $width = $width ?? 200;
$height = $height ?? 200; $height = $height ?? 200;
@ -259,7 +294,7 @@ function createPlaceholderImage ($path, $width, $height, $text = 'Image Unavaila
// Background is the first color by default // Background is the first color by default
$fillColor = imagecolorallocatealpha($img, 255, 255, 255, 127); $fillColor = imagecolorallocatealpha($img, 255, 255, 255, 127);
imagefill($img, 0, 0, $fillColor); imagefill($img, 0, 0, $fillColor);
$textColor = imagecolorallocate($img, 64, 64, 64); $textColor = imagecolorallocate($img, 64, 64, 64);
imagealphablending($img, TRUE); imagealphablending($img, TRUE);
@ -268,7 +303,7 @@ function createPlaceholderImage ($path, $width, $height, $text = 'Image Unavaila
$fontSize = 10; $fontSize = 10;
$fontWidth = imagefontwidth($fontSize); $fontWidth = imagefontwidth($fontSize);
$fontHeight = imagefontheight($fontSize); $fontHeight = imagefontheight($fontSize);
$length = strlen($text); $length = \strlen($text);
$textWidth = $length * $fontWidth; $textWidth = $length * $fontWidth;
$fxPos = (int) ceil((imagesx($img) - $textWidth) / 2); $fxPos = (int) ceil((imagesx($img) - $textWidth) / 2);
$fyPos = (int) ceil((imagesy($img) - $fontHeight) / 2); $fyPos = (int) ceil((imagesy($img) - $fontHeight) / 2);
@ -280,11 +315,11 @@ function createPlaceholderImage ($path, $width, $height, $text = 'Image Unavaila
imagesavealpha($img, TRUE); imagesavealpha($img, TRUE);
imagepng($img, $path . '/placeholder.png', 9); imagepng($img, $path . '/placeholder.png', 9);
imagedestroy($img); imagedestroy($img);
$pngImage = imagecreatefrompng($path . '/placeholder.png'); $pngImage = imagecreatefrompng($path . '/placeholder.png');
imagealphablending($pngImage, TRUE); imagealphablending($pngImage, TRUE);
imagesavealpha($pngImage, TRUE); imagesavealpha($pngImage, TRUE);
imagewebp($pngImage, $path . '/placeholder.webp'); imagewebp($pngImage, $path . '/placeholder.webp');
imagedestroy($pngImage); imagedestroy($pngImage);

View File

@ -21,13 +21,9 @@ use function Aviat\AnimeClient\loadTomlFile;
use Aura\Router\RouterContainer; use Aura\Router\RouterContainer;
use Aura\Session\SessionFactory; use Aura\Session\SessionFactory;
use Aviat\AnimeClient\UrlGenerator; use Aviat\AnimeClient\{Model, UrlGenerator, Util};
use Aviat\AnimeClient\Util; use Aviat\AnimeClient\API\{Anilist, CacheTrait, Kitsu};
use Aviat\AnimeClient\API\CacheTrait;
use Aviat\AnimeClient\API\Anilist;
use Aviat\AnimeClient\API\Kitsu;
use Aviat\AnimeClient\API\Kitsu\KitsuRequestBuilder; use Aviat\AnimeClient\API\Kitsu\KitsuRequestBuilder;
use Aviat\AnimeClient\Model;
use Aviat\Banker\Pool; use Aviat\Banker\Pool;
use Aviat\Ion\Config; use Aviat\Ion\Config;
use Aviat\Ion\Di\{Container, ContainerAware}; use Aviat\Ion\Di\{Container, ContainerAware};

View File

@ -1,59 +1,59 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
/** /**
* Hummingbird Anime List Client * Hummingbird Anime List Client
* *
* An API client for Kitsu to manage anime and manga watch lists * An API client for Kitsu to manage anime and manga watch lists
* *
* PHP version 7.1 * PHP version 7.1
* *
* @package HummingbirdAnimeClient * @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.1 * @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */
namespace Aviat\AnimeClient\Command; namespace Aviat\AnimeClient\Command;
/** /**
* Clears out image cache directories * Clears out image cache directories
*/ */
class ClearThumbnails extends BaseCommand { class ClearThumbnails extends BaseCommand {
public function execute(array $args, array $options = []): void public function execute(array $args, array $options = []): void
{ {
$this->clearThumbs(); $this->clearThumbs();
$this->echoBox('All cached images have been removed'); $this->echoBox('All cached images have been removed');
} }
public function clearThumbs() private function clearThumbs(): void
{ {
$imgDir = realpath(__DIR__ . '/../../public/images'); $imgDir = realpath(__DIR__ . '/../../public/images');
$paths = [ $paths = [
'avatars/*.gif', 'avatars/*.gif',
'avatars/*.jpg', 'avatars/*.jpg',
'avatars/*.png', 'avatars/*.png',
'avatars/*.webp', 'avatars/*.webp',
'anime/*.jpg', 'anime/*.jpg',
'anime/*.png', 'anime/*.png',
'anime/*.webp', 'anime/*.webp',
'manga/*.jpg', 'manga/*.jpg',
'manga/*.png', 'manga/*.png',
'manga/*.webp', 'manga/*.webp',
'characters/*.jpg', 'characters/*.jpg',
'characters/*.png', 'characters/*.png',
'characters/*.webp', 'characters/*.webp',
'people/*.jpg', 'people/*.jpg',
'people/*.png', 'people/*.png',
'people/*.webp', 'people/*.webp',
]; ];
foreach($paths as $path) foreach($paths as $path)
{ {
$cmd = "rm -rf {$imgDir}/{$path}"; $cmd = "rm -rf {$imgDir}/{$path}";
exec($cmd); exec($cmd);
} }
} }
} }

View File

@ -264,6 +264,11 @@ final class SyncLists extends BaseCommand {
foreach ($potentialMappings as $mappingId) foreach ($potentialMappings as $mappingId)
{ {
if (\is_array($mappingId))
{
continue;
}
if (array_key_exists($mappingId, $includes['mappings'])) if (array_key_exists($mappingId, $includes['mappings']))
{ {
$malId = $includes['mappings'][$mappingId]['externalId']; $malId = $includes['mappings'][$mappingId]['externalId'];
@ -505,12 +510,16 @@ final class SyncLists extends BaseCommand {
// Use the first set rating, otherwise use the newer rating // Use the first set rating, otherwise use the newer rating
if ( ! $sameRating) 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']; $update['data']['ratingTwenty'] = $kitsuItem['data']['ratingTwenty'];
$return['updateType'][] = 'anilist'; $return['updateType'][] = 'anilist';
} }
else if($dateDiff === -1) else if($dateDiff === -1 && $anilistItem['data']['rating'] !== 0)
{ {
$update['data']['ratingTwenty'] = $anilistItem['data']['rating'] * 2; $update['data']['ratingTwenty'] = $anilistItem['data']['rating'] * 2;
$return['updateType'][] = 'kitsu'; $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'] = [ $return['meta'] = [
'kitsu' => $kitsuItem['data'], 'kitsu' => $kitsuItem['data'],
'anilist' => $anilistItem['data'], 'anilist' => $anilistItem['data'],

View File

@ -183,7 +183,7 @@ class Controller {
* @throws \Aviat\Ion\Di\Exception\NotFoundException * @throws \Aviat\Ion\Di\Exception\NotFoundException
* @return void * @return void
*/ */
public function sessionRedirect() public function sessionRedirect(): void
{ {
$target = $this->session->get('redirect_url'); $target = $this->session->get('redirect_url');
if (empty($target)) if (empty($target))
@ -208,7 +208,7 @@ class Controller {
* @throws \Aviat\Ion\Di\Exception\NotFoundException * @throws \Aviat\Ion\Di\Exception\NotFoundException
* @return string * @return string
*/ */
protected function loadPartial($view, string $template, array $data = []) protected function loadPartial($view, string $template, array $data = []): string
{ {
$router = $this->container->get('dispatcher'); $router = $this->container->get('dispatcher');
@ -242,7 +242,7 @@ class Controller {
* @throws \Aviat\Ion\Di\Exception\NotFoundException * @throws \Aviat\Ion\Di\Exception\NotFoundException
* @return void * @return void
*/ */
protected function renderFullPage($view, string $template, array $data) protected function renderFullPage($view, string $template, array $data): void
{ {
$csp = [ $csp = [
"default-src 'self'", "default-src 'self'",
@ -275,7 +275,7 @@ class Controller {
public function notFound( public function notFound(
string $title = 'Sorry, page not found', string $title = 'Sorry, page not found',
string $message = 'Page Not Found' string $message = 'Page Not Found'
) ): void
{ {
$this->outputHTML('404', [ $this->outputHTML('404', [
'title' => $title, 'title' => $title,
@ -383,7 +383,7 @@ class Controller {
* @throws \Aviat\Ion\Di\Exception\NotFoundException * @throws \Aviat\Ion\Di\Exception\NotFoundException
* @return void * @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) if (null === $view)
{ {

View File

@ -23,15 +23,11 @@ use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
use Aviat\AnimeClient\Types\FormItem; use Aviat\AnimeClient\Types\FormItem;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Json; use Aviat\Ion\Json;
use Aviat\Ion\StringWrapper;
/** /**
* Controller for Anime-related pages * Controller for Anime-related pages
*/ */
final class Anime extends BaseController { final class Anime extends BaseController {
use StringWrapper;
/** /**
* The anime list model * The anime list model
* @var \Aviat\AnimeClient\Model\Anime $model * @var \Aviat\AnimeClient\Model\Anime $model
@ -276,8 +272,6 @@ final class Anime extends BaseController {
public function details(string $animeId): void public function details(string $animeId): void
{ {
$data = $this->model->getAnime($animeId); $data = $this->model->getAnime($animeId);
$characters = [];
$staff = [];
if (empty($data)) if (empty($data))
{ {
@ -291,77 +285,13 @@ final class Anime extends BaseController {
return; 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', [ $this->outputHTML('anime/details', [
'title' => $this->formatTitle( 'title' => $this->formatTitle(
$this->config->get('whose_list') . "'s Anime List", $this->config->get('whose_list') . "'s Anime List",
'Anime', 'Anime',
$data->title $data->title
), ),
'characters' => $characters, 'data' => $data,
'show_data' => $data,
'staff' => $staff,
]); ]);
} }

View File

@ -68,7 +68,7 @@ final class AnimeCollection extends BaseController {
* @throws \Aviat\Ion\Exception\DoubleRenderException * @throws \Aviat\Ion\Exception\DoubleRenderException
* @return void * @return void
*/ */
public function search() public function search(): void
{ {
$queryParams = $this->request->getQueryParams(); $queryParams = $this->request->getQueryParams();
$query = $queryParams['query']; $query = $queryParams['query'];
@ -84,7 +84,7 @@ final class AnimeCollection extends BaseController {
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
* @return void * @return void
*/ */
public function index($view) public function index($view): void
{ {
$viewMap = [ $viewMap = [
'' => 'cover', '' => 'cover',
@ -110,7 +110,7 @@ final class AnimeCollection extends BaseController {
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
* @return void * @return void
*/ */
public function form($id = NULL) public function form($id = NULL): void
{ {
$this->setSessionRedirect(); $this->setSessionRedirect();
@ -137,7 +137,7 @@ final class AnimeCollection extends BaseController {
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
* @return void * @return void
*/ */
public function edit() public function edit(): void
{ {
$data = $this->request->getParsedBody(); $data = $this->request->getParsedBody();
if (array_key_exists('hummingbird_id', $data)) if (array_key_exists('hummingbird_id', $data))
@ -161,7 +161,7 @@ final class AnimeCollection extends BaseController {
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
* @return void * @return void
*/ */
public function add() public function add(): void
{ {
$data = $this->request->getParsedBody(); $data = $this->request->getParsedBody();
if (array_key_exists('id', $data)) if (array_key_exists('id', $data))
@ -182,7 +182,7 @@ final class AnimeCollection extends BaseController {
* *
* @return void * @return void
*/ */
public function delete() public function delete(): void
{ {
$data = $this->request->getParsedBody(); $data = $this->request->getParsedBody();
if ( ! array_key_exists('hummingbird_id', $data)) if ( ! array_key_exists('hummingbird_id', $data))

View File

@ -17,30 +17,42 @@
namespace Aviat\AnimeClient\Controller; namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Controller as BaseController; use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\API\JsonAPI; use Aviat\AnimeClient\API\Kitsu\Transformer\CharacterTransformer;
use Aviat\Ion\ArrayWrapper;
use Aviat\Ion\Di\ContainerInterface;
/** /**
* Controller for character description pages * Controller for character description pages
*/ */
class Character extends BaseController { 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 * Show information about a character
* *
* @param string $slug * @param string $slug
* @throws \Aviat\Ion\Di\ContainerException
* @throws \Aviat\Ion\Di\NotFoundException
* @throws \InvalidArgumentException
* @return void * @return void
*/ */
public function index(string $slug): void public function index(string $slug): void
{ {
$model = $this->container->get('kitsu-model'); $rawData = $this->model->getCharacter($slug);
$rawData = $model->getCharacter($slug);
if (( ! array_key_exists('data', $rawData)) || empty($rawData['data'])) if (( ! array_key_exists('data', $rawData)) || empty($rawData['data']))
{ {
@ -55,167 +67,14 @@ class Character extends BaseController {
return; return;
} }
$data = JsonAPI::organizeData($rawData); $data = (new CharacterTransformer())->transform($rawData)->toArray();
$data['names'] = array_unique( $this->outputHTML('character/details', [
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 = [
'title' => $this->formatTitle( 'title' => $this->formatTitle(
'Characters', 'Characters',
$data[0]['attributes']['name'] $data['name']
), ),
'data' => $data, '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;
} }
} }

View File

@ -1,198 +1,195 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
/** /**
* Hummingbird Anime List Client * Hummingbird Anime List Client
* *
* An API client for Kitsu to manage anime and manga watch lists * An API client for Kitsu to manage anime and manga watch lists
* *
* PHP version 7.1 * PHP version 7.1
* *
* @package HummingbirdAnimeClient * @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net> * @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2018 Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.1 * @version 4.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/ */
namespace Aviat\AnimeClient\Controller; namespace Aviat\AnimeClient\Controller;
use function Aviat\AnimeClient\createPlaceholderImage; use function Amp\Promise\wait;
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\AnimeClient\Controller as BaseController;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\View\HtmlView; /**
* Controller for handling routes that don't fit elsewhere
/** */
* Controller for handling routes that don't fit elsewhere final class Images extends BaseController {
*/ /**
final class Images extends BaseController { * Get image covers from kitsu
/** *
* Get image covers from kitsu * @param string $type The category of image
* * @param string $file The filename to look for
* @param string $type The category of image * @param bool $display Whether to output the image to the server
* @param string $file The filename to look for * @throws \Aviat\Ion\Di\ContainerException
* @param bool $display Whether to output the image to the server * @throws \Aviat\Ion\Di\NotFoundException
* @throws \Aviat\Ion\Di\ContainerException * @throws \InvalidArgumentException
* @throws \Aviat\Ion\Di\NotFoundException * @throws \TypeError
* @throws \InvalidArgumentException * @throws \Error
* @throws \TypeError * @throws \Throwable
* @throws \Error * @return void
* @throws \Throwable */
* @return void public function cache(string $type, string $file, $display = TRUE): void
*/ {
public function cache(string $type, string $file, $display = TRUE): void $currentUrl = $this->request->getUri()->__toString();
{
$currentUrl = $this->request->getUri()->__toString(); $kitsuUrl = 'https://media.kitsu.io/';
$fileName = str_replace('-original', '', $file);
$kitsuUrl = 'https://media.kitsu.io/'; [$id, $ext] = explode('.', basename($fileName));
$fileName = str_replace('-original', '', $file);
[$id, $ext] = explode('.', basename($fileName)); $baseSavePath = $this->config->get('img_cache_path');
$baseSavePath = $this->config->get('img_cache_path'); // Kitsu doesn't serve webp, but for most use cases,
// jpg is a safe assumption
// Kitsu doesn't serve webp, but for most use cases, $tryJpg = ['anime','characters','manga','people'];
// jpg is a safe assumption if ($ext === 'webp' && \in_array($type, $tryJpg, TRUE))
$tryJpg = ['anime','characters','manga','people']; {
if ($ext === 'webp' && in_array($type, $tryJpg, TRUE)) $ext = 'jpg';
{ $currentUrl = str_replace('webp', 'jpg', $currentUrl);
$ext = 'jpg'; }
$currentUrl = str_replace('webp', 'jpg', $currentUrl);
} $typeMap = [
'anime' => [
$typeMap = [ 'kitsuUrl' => "anime/poster_images/{$id}/medium.{$ext}",
'anime' => [ 'width' => 220,
'kitsuUrl' => "anime/poster_images/{$id}/medium.{$ext}", 'height' => 312,
'width' => 220, ],
'height' => 312, 'avatars' => [
], 'kitsuUrl' => "users/avatars/{$id}/original.{$ext}",
'avatars' => [ 'width' => null,
'kitsuUrl' => "users/avatars/{$id}/original.{$ext}", 'height' => null,
'width' => null, ],
'height' => null, 'characters' => [
], 'kitsuUrl' => "characters/images/{$id}/original.{$ext}",
'characters' => [ 'width' => 225,
'kitsuUrl' => "characters/images/{$id}/original.{$ext}", 'height' => 350,
'width' => 225, ],
'height' => 350, 'manga' => [
], 'kitsuUrl' => "manga/poster_images/{$id}/medium.{$ext}",
'manga' => [ 'width' => 220,
'kitsuUrl' => "manga/poster_images/{$id}/medium.{$ext}", 'height' => 312,
'width' => 220, ],
'height' => 312, 'people' => [
], 'kitsuUrl' => "people/images/{$id}/original.{$ext}",
'people' => [ 'width' => null,
'kitsuUrl' => "people/images/{$id}/original.{$ext}", 'height' => null,
'width' => null, ],
'height' => null, ];
],
]; $imageType = $typeMap[$type] ?? NULL;
$imageType = $typeMap[$type] ?? NULL; if (NULL === $imageType)
{
if (NULL === $imageType) $this->getPlaceholder($baseSavePath, 200, 200);
{ return;
$this->getPlaceholder($baseSavePath, 200, 200); }
return;
} $kitsuUrl .= $imageType['kitsuUrl'];
$width = $imageType['width'];
$kitsuUrl .= $imageType['kitsuUrl']; $height = $imageType['height'];
$width = $imageType['width']; $filePrefix = "{$baseSavePath}/{$type}/{$id}";
$height = $imageType['height'];
$filePrefix = "{$baseSavePath}/{$type}/{$id}"; $response = getResponse($kitsuUrl);
$promise = (new HummingbirdClient)->request($kitsuUrl); if ($response->getStatus() !== 200)
$response = wait($promise); {
// Try a few different file types before giving up
if ($response->getStatus() !== 200) // webm => jpg => png => gif
{ $nextType = [
// Try a few different file types before giving up 'jpg' => 'png',
// webm => jpg => png => gif 'png' => 'gif',
$nextType = [ ];
'jpg' => 'png',
'png' => 'gif', if (array_key_exists($ext, $nextType))
]; {
$newUrl = str_replace($ext, $nextType[$ext], $currentUrl);
if (array_key_exists($ext, $nextType)) $this->redirect($newUrl, 303);
{ return;
$newUrl = str_replace($ext, $nextType[$ext], $currentUrl); }
$this->redirect($newUrl, 303);
return; if ($display)
} {
$this->getPlaceholder("{$baseSavePath}/{$type}", $width, $height);
if ($display) }
{ else
$this->getPlaceholder("{$baseSavePath}/{$type}", $width, $height); {
} createPlaceholderImage("{$baseSavePath}/{$type}", $width, $height);
else }
{ return;
createPlaceholderImage("{$baseSavePath}/{$type}", $width, $height); }
}
return; $data = wait($response->getBody());
}
$data = wait($response->getBody());
[$origWidth] = getimagesizefromstring($data);
$gdImg = imagecreatefromstring($data);
$resizedImg = imagescale($gdImg, $width ?? $origWidth);
[$origWidth] = getimagesizefromstring($data);
$gdImg = imagecreatefromstring($data); if ($ext === 'gif')
$resizedImg = imagescale($gdImg, $width ?? $origWidth); {
file_put_contents("{$filePrefix}.gif", $data);
if ($ext === 'gif') \imagepalletetotruecolor($gdImg);
{ }
file_put_contents("{$filePrefix}.gif", $data);
imagepalletetotruecolor($gdImg); // save the webp versions
} imagewebp($gdImg, "{$filePrefix}-original.webp");
imagewebp($resizedImg, "{$filePrefix}.webp");
// save the webp versions
imagewebp($gdImg, "{$filePrefix}-original.webp"); // save the scaled jpeg file
imagewebp($resizedImg, "{$filePrefix}.webp"); imagejpeg($resizedImg, "{$filePrefix}.jpg");
// save the scaled jpeg file // And the original
imagejpeg($resizedImg, "{$filePrefix}.jpg"); file_put_contents("{$filePrefix}-original.jpg", $data);
// And the original imagedestroy($gdImg);
file_put_contents("{$filePrefix}-original.jpg", $data); imagedestroy($resizedImg);
imagedestroy($gdImg); if ($display)
imagedestroy($resizedImg); {
$contentType = ($ext === 'webp')
if ($display) ? 'image/webp'
{ : $response->getHeader('content-type')[0];
$contentType = ($ext === 'webp')
? "image/webp" $outputFile = (strpos($file, '-original') !== FALSE)
: $response->getHeader('content-type')[0]; ? "{$filePrefix}-original.{$ext}"
: "{$filePrefix}.{$ext}";
$outputFile = (strpos($file, '-original') !== FALSE)
? "{$filePrefix}-original.{$ext}" header("Content-Type: {$contentType}");
: "{$filePrefix}.{$ext}"; echo file_get_contents($outputFile);
}
header("Content-Type: {$contentType}"); }
echo file_get_contents($outputFile);
} /**
} * Get a placeholder for a missing image
*
/** * @param string $path
* Get a placeholder for a missing image * @param int|null $width
* * @param int|null $height
* @param string $path */
* @param int|null $width private function getPlaceholder (string $path, ?int $width = 200, ?int $height = NULL): void
* @param int|null $height {
*/ $height = $height ?? $width;
private function getPlaceholder (string $path, ?int $width = 200, ?int $height = NULL): void
{ $filename = $path . '/placeholder.png';
$height = $height ?? $width;
if ( ! file_exists($path . '/placeholder.png'))
$filename = $path . '/placeholder.png'; {
createPlaceholderImage($path, $width, $height);
if ( ! file_exists($path . '/placeholder.png')) }
{
createPlaceholderImage($path, $width, $height); header('Content-Type: image/png');
} echo file_get_contents($filename);
}
header('Content-Type: image/png');
echo file_get_contents($filename);
}
} }

View File

@ -294,65 +294,6 @@ final class Manga extends Controller {
return; 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', [ $this->outputHTML('manga/details', [
'title' => $this->formatTitle( 'title' => $this->formatTitle(
$this->config->get('whose_list') . "'s Manga List", $this->config->get('whose_list') . "'s Manga List",

View File

@ -138,7 +138,7 @@ final class MangaCollection extends BaseController {
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
* @return void * @return void
*/ */
public function edit() public function edit(): void
{ {
$data = $this->request->getParsedBody(); $data = $this->request->getParsedBody();
if (array_key_exists('hummingbird_id', $data)) if (array_key_exists('hummingbird_id', $data))
@ -162,7 +162,7 @@ final class MangaCollection extends BaseController {
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
* @return void * @return void
*/ */
public function add() public function add(): void
{ {
$data = $this->request->getParsedBody(); $data = $this->request->getParsedBody();
if (array_key_exists('id', $data)) if (array_key_exists('id', $data))
@ -183,7 +183,7 @@ final class MangaCollection extends BaseController {
* *
* @return void * @return void
*/ */
public function delete() public function delete(): void
{ {
$data = $this->request->getParsedBody(); $data = $this->request->getParsedBody();
if ( ! array_key_exists('hummingbird_id', $data)) if ( ! array_key_exists('hummingbird_id', $data))

View File

@ -17,7 +17,6 @@
namespace Aviat\AnimeClient\Controller; namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Controller as BaseController; use Aviat\AnimeClient\Controller as BaseController;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\View\HtmlView; use Aviat\Ion\View\HtmlView;
/** /**
@ -29,7 +28,7 @@ final class Misc extends BaseController {
* *
* @return void * @return void
*/ */
public function clearCache() public function clearCache(): void
{ {
$this->cache->clear(); $this->cache->clear();
$this->outputHTML('blank', [ $this->outputHTML('blank', [
@ -43,7 +42,7 @@ final class Misc extends BaseController {
* @param string $status * @param string $status
* @return void * @return void
*/ */
public function login(string $status = '') public function login(string $status = ''): void
{ {
$message = ''; $message = '';
@ -68,7 +67,7 @@ final class Misc extends BaseController {
* *
* @return void * @return void
*/ */
public function loginAction() public function loginAction(): void
{ {
$auth = $this->container->get('auth'); $auth = $this->container->get('auth');
$post = $this->request->getParsedBody(); $post = $this->request->getParsedBody();
@ -88,7 +87,7 @@ final class Misc extends BaseController {
* *
* @return void * @return void
*/ */
public function logout() public function logout(): void
{ {
$auth = $this->container->get('auth'); $auth = $this->container->get('auth');
$auth->logout(); $auth->logout();

View File

@ -17,12 +17,33 @@
namespace Aviat\AnimeClient\Controller; namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Controller as BaseController; 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 * Controller for People pages
*/ */
final class People extends BaseController { 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 * Show information about a person
* *
@ -31,9 +52,8 @@ final class People extends BaseController {
*/ */
public function index(string $id): void public function index(string $id): void
{ {
$model = $this->container->get('kitsu-model'); $rawData = $this->model->getPerson($id);
$data = (new PersonTransformer())->transform($rawData)->toArray();
$rawData = $model->getPerson($id);
if (( ! array_key_exists('data', $rawData)) || empty($rawData['data'])) if (( ! array_key_exists('data', $rawData)) || empty($rawData['data']))
{ {
@ -48,114 +68,12 @@ final class People extends BaseController {
return; return;
} }
$data = JsonAPI::organizeData($rawData); $this->outputHTML('person/details', [
$included = JsonAPI::organizeIncludes($rawData['included']);
$orgData = $this->organizeData($included);
$viewData = [
'included' => $included,
'title' => $this->formatTitle( 'title' => $this->formatTitle(
'People', 'People',
$data['attributes']['name'] $data['name']
), ),
'data' => $data, '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;
} }
} }

View File

@ -24,7 +24,7 @@ use Aviat\Ion\Di\ContainerInterface;
*/ */
final class Settings extends BaseController { final class Settings extends BaseController {
/** /**
* @var \Aviat\API\Anilist\Model * @var \Aviat\AnimeClient\API\Anilist\Model
*/ */
private $anilistModel; private $anilistModel;
@ -33,6 +33,13 @@ final class Settings extends BaseController {
*/ */
private $settingsModel; private $settingsModel;
/**
* Settings constructor.
*
* @param ContainerInterface $container
* @throws \Aviat\Ion\Di\Exception\ContainerException
* @throws \Aviat\Ion\Di\Exception\NotFoundException
*/
public function __construct(ContainerInterface $container) public function __construct(ContainerInterface $container)
{ {
parent::__construct($container); parent::__construct($container);
@ -44,7 +51,7 @@ final class Settings extends BaseController {
/** /**
* Show the user settings, if logged in * Show the user settings, if logged in
*/ */
public function index() public function index(): void
{ {
$auth = $this->container->get('auth'); $auth = $this->container->get('auth');
$form = $this->settingsModel->getSettingsForm(); $form = $this->settingsModel->getSettingsForm();
@ -66,7 +73,7 @@ final class Settings extends BaseController {
* *
* @throws \Aura\Router\Exception\RouteNotFound * @throws \Aura\Router\Exception\RouteNotFound
*/ */
public function update() public function update(): void
{ {
$post = $this->request->getParsedBody(); $post = $this->request->getParsedBody();
unset($post['settings-tabs']); unset($post['settings-tabs']);
@ -88,14 +95,15 @@ final class Settings extends BaseController {
/** /**
* Redirect to Anilist to start Oauth flow * Redirect to Anilist to start Oauth flow
*/ */
public function anilistRedirect() public function anilistRedirect(): void
{ {
$redirectUrl = 'https://anilist.co/api/v2/oauth/authorize?' . $query = http_build_query([
http_build_query([ 'client_id' => $this->config->get(['anilist', 'client_id']),
'client_id' => $this->config->get(['anilist', 'client_id']), 'redirect_uri' => $this->urlGenerator->url('/anilist-oauth'),
'redirect_uri' => $this->urlGenerator->url('/anilist-oauth'), 'response_type' => 'code',
'response_type' => 'code', ]);
]);
$redirectUrl = "https://anilist.co/api/v2/oauth/authorize?{$query}";
$this->redirect($redirectUrl, 303); $this->redirect($redirectUrl, 303);
} }
@ -103,7 +111,7 @@ final class Settings extends BaseController {
/** /**
* Oauth callback for Anilist API * Oauth callback for Anilist API
*/ */
public function anilistCallback() public function anilistCallback(): void
{ {
$query = $this->request->getQueryParams(); $query = $this->request->getQueryParams();
$authCode = $query['code']; $authCode = $query['code'];

View File

@ -16,8 +16,9 @@
namespace Aviat\AnimeClient\Controller; namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\API\Kitsu\Transformer\UserTransformer;
use Aviat\AnimeClient\Controller as BaseController; use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\API\JsonAPI;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
/** /**
@ -25,8 +26,18 @@ use Aviat\Ion\Di\ContainerInterface;
*/ */
final class User extends BaseController { final class User extends BaseController {
/**
* @var \Aviat\AnimeClient\API\Kitsu\Model
*/
private $kitsuModel; private $kitsuModel;
/**
* User constructor.
*
* @param ContainerInterface $container
* @throws \Aviat\Ion\Di\Exception\ContainerException
* @throws \Aviat\Ion\Di\Exception\NotFoundException
*/
public function __construct(ContainerInterface $container) public function __construct(ContainerInterface $container)
{ {
parent::__construct($container); parent::__construct($container);
@ -56,103 +67,16 @@ final class User extends BaseController {
? $this->config->get(['kitsu_username']) ? $this->config->get(['kitsu_username'])
: $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 $whom = $isMainUser
? $this->config->get('whose_list') ? $this->config->get('whose_list')
: $username; : $username;
$rawData = $this->kitsuModel->getUserData($username);
$data = (new UserTransformer())->transform($rawData)->toArray();
$this->outputHTML('user/details', [ $this->outputHTML('user/details', [
'title' => 'About ' . $whom, 'title' => 'About ' . $whom,
'data' => $orgData, 'data' => $data,
'attributes' => $orgData['attributes'],
'relationships' => $rels,
'favorites' => $this->organizeFavorites($favorites),
'stats' => $stats,
'timeOnAnime' => $timeOnAnime,
]); ]);
} }
/**
* 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;
}
} }

View File

@ -64,8 +64,9 @@ final class Dispatcher extends RoutingBase {
public function __construct(ContainerInterface $container) public function __construct(ContainerInterface $container)
{ {
parent::__construct($container); parent::__construct($container);
$this->router = $container->get('aura-router')->getMap(); $router = $this->container->get('aura-router');
$this->matcher = $container->get('aura-router')->getMatcher(); $this->router = $router->getMap();
$this->matcher = $router->getMatcher();
$this->request = $container->get('request'); $this->request = $container->get('request');
$this->outputRoutes = $this->setupRoutes(); $this->outputRoutes = $this->setupRoutes();
@ -99,7 +100,7 @@ final class Dispatcher extends RoutingBase {
* *
* @return array * @return array
*/ */
public function getOutputRoutes() public function getOutputRoutes(): array
{ {
return $this->outputRoutes; return $this->outputRoutes;
} }
@ -171,7 +172,7 @@ final class Dispatcher extends RoutingBase {
$controllerName = $map[$controllerName]; $controllerName = $map[$controllerName];
} }
$actionMethod = (array_key_exists('action', $route->attributes)) $actionMethod = array_key_exists('action', $route->attributes)
? $route->attributes['action'] ? $route->attributes['action']
: NOT_FOUND_METHOD; : NOT_FOUND_METHOD;
@ -205,9 +206,9 @@ final class Dispatcher extends RoutingBase {
* *
* @return string * @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(); $requestUri = $this->request->getUri()->getPath();
$path = trim($requestUri, '/'); $path = trim($requestUri, '/');
@ -225,7 +226,7 @@ final class Dispatcher extends RoutingBase {
$controller = $routeType; $controller = $routeType;
} }
return $controller; return $controller ?? '';
} }
/** /**
@ -233,11 +234,13 @@ final class Dispatcher extends RoutingBase {
* *
* @return array * @return array
*/ */
public function getControllerList() public function getControllerList(): array
{ {
$defaultNamespace = DEFAULT_CONTROLLER_NAMESPACE; $defaultNamespace = DEFAULT_CONTROLLER_NAMESPACE;
$path = str_replace('\\', '/', $defaultNamespace); $find = ['\\', 'Aviat/AnimeClient/'];
$path = str_replace('Aviat/AnimeClient/', '', $path); $replace = ['/', ''];
$path = str_replace($find, $replace, $defaultNamespace);
$path = trim($path, '/'); $path = trim($path, '/');
$actualPath = realpath(_dir(SRC_DIR, $path)); $actualPath = realpath(_dir(SRC_DIR, $path));
$classFiles = glob("{$actualPath}/*.php"); $classFiles = glob("{$actualPath}/*.php");
@ -265,7 +268,7 @@ final class Dispatcher extends RoutingBase {
* @param array $params * @param array $params
* @return void * @return void
*/ */
protected function call($controllerName, $method, array $params) protected function call($controllerName, $method, array $params): void
{ {
$logger = $this->container->getLogger('default'); $logger = $this->container->getLogger('default');
@ -347,7 +350,7 @@ final class Dispatcher extends RoutingBase {
* *
* @return array * @return array
*/ */
protected function setupRoutes() protected function setupRoutes(): array
{ {
$routeType = $this->getController(); $routeType = $this->getController();
@ -359,7 +362,7 @@ final class Dispatcher extends RoutingBase {
unset($route['path']); unset($route['path']);
$controllerMap = $this->getControllerList(); $controllerMap = $this->getControllerList();
$controllerClass = (array_key_exists($routeType, $controllerMap)) $controllerClass = array_key_exists($routeType, $controllerMap)
? $controllerMap[$routeType] ? $controllerMap[$routeType]
: DEFAULT_CONTROLLER; : DEFAULT_CONTROLLER;

View File

@ -16,35 +16,28 @@
namespace Aviat\AnimeClient; namespace Aviat\AnimeClient;
use Aviat\Ion\
{
ArrayWrapper, StringWrapper
};
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
/** /**
* Helper object to manage form generation, especially for config editing * Helper object to manage form generation, especially for config editing
*/ */
final class FormGenerator { final class FormGenerator {
use ArrayWrapper;
use StringWrapper;
/**
* Injection Container
* @var ContainerInterface $container
*/
protected $container;
/** /**
* Html generation helper * Html generation helper
* *
* @var \Aura\Html\HelperLocator * @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) public function __construct(ContainerInterface $container)
{ {
$this->container = $container;
$this->helper = $container->get('html-helper'); $this->helper = $container->get('html-helper');
} }
@ -55,7 +48,7 @@ final class FormGenerator {
* @param array $form * @param array $form
* @return string * @return string
*/ */
public function generate(string $name, array $form) public function generate(string $name, array $form): string
{ {
$type = $form['type']; $type = $form['type'];
@ -105,6 +98,6 @@ final class FormGenerator {
} }
} }
return $this->helper->input($params); return (string)$this->helper->input($params);
} }
} }

View File

@ -45,9 +45,11 @@ final class MenuGenerator extends UrlGenerator {
protected $request; protected $request;
/** /**
* Create menu generator * MenuGenerator constructor.
* *
* @param ContainerInterface $container * @param ContainerInterface $container
* @throws \Aviat\Ion\Di\Exception\ContainerException
* @throws \Aviat\Ion\Di\Exception\NotFoundException
*/ */
public function __construct(ContainerInterface $container) public function __construct(ContainerInterface $container)
{ {
@ -106,7 +108,7 @@ final class MenuGenerator extends UrlGenerator {
$link = $this->helper->a($this->url($path), $title); $link = $this->helper->a($this->url($path), $title);
$attrs = ($selected) $attrs = $selected
? ['class' => 'selected'] ? ['class' => 'selected']
: []; : [];

View File

@ -16,14 +16,10 @@
namespace Aviat\AnimeClient\Model; namespace Aviat\AnimeClient\Model;
use Aviat\Ion\StringWrapper;
/** /**
* Base model for api interaction * Base model for api interaction
*/ */
class API { class API {
use StringWrapper;
/** /**
* Sort the list entries by their title * Sort the list entries by their title
* *
@ -31,7 +27,7 @@ class API {
* @param string $sortKey * @param string $sortKey
* @return void * @return void
*/ */
protected function sortByName(array &$array, string $sortKey) protected function sortByName(array &$array, string $sortKey): void
{ {
$sort = []; $sort = [];

View File

@ -108,7 +108,7 @@ class Anime extends API {
* @param string $slug * @param string $slug
* @return AnimeType * @return AnimeType
*/ */
public function getAnime(string $slug) public function getAnime(string $slug): AnimeType
{ {
return $this->kitsuModel->getAnime($slug); return $this->kitsuModel->getAnime($slug);
} }
@ -147,7 +147,7 @@ class Anime extends API {
$item = $this->kitsuModel->getListItem($itemId); $item = $this->kitsuModel->getListItem($itemId);
$array = $item->toArray(); $array = $item->toArray();
if (is_array($array['notes'])) if (\is_array($array['notes']))
{ {
$array['notes'] = ''; $array['notes'] = '';
} }

View File

@ -226,6 +226,11 @@ final class AnimeCollection extends Collection {
return $query->fetch(PDO::FETCH_ASSOC); return $query->fetch(PDO::FETCH_ASSOC);
} }
/**
* Get the list of genres from the database
*
* @return array
*/
private function getGenresForList(): array private function getGenresForList(): array
{ {
$query = $this->db->select('hummingbird_id, genre') $query = $this->db->select('hummingbird_id, genre')

View File

@ -83,11 +83,11 @@ class Collection extends DB {
if ( ! empty($filter)) if ( ! empty($filter))
{ {
$this->db->where_in('hummingbird_id', $filter); $this->db->whereIn('hummingbird_id', $filter);
} }
$query = $this->db->order_by('hummingbird_id') $query = $this->db->orderBy('hummingbird_id')
->order_by('genre') ->orderBy('genre')
->get(); ->get();
$output = []; $output = [];

View File

@ -17,15 +17,12 @@
namespace Aviat\AnimeClient\Model; namespace Aviat\AnimeClient\Model;
use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
use Aviat\Ion\{ArrayWrapper, StringWrapper};
/** /**
* Base model for database interaction * Base model for database interaction
*/ */
class DB { class DB {
use ArrayWrapper;
use ContainerAware; use ContainerAware;
use StringWrapper;
/** /**
* The query builder object * The query builder object

View File

@ -41,7 +41,7 @@ final class Settings {
$this->config = $config; $this->config = $config;
} }
public function getSettings() public function getSettings(): array
{ {
$settings = [ $settings = [
'config' => [], 'config' => [],
@ -66,7 +66,7 @@ final class Settings {
return $settings; return $settings;
} }
public function getSettingsForm() public function getSettingsForm(): array
{ {
$output = []; $output = [];
@ -124,7 +124,7 @@ final class Settings {
return $output; return $output;
} }
public function validateSettings(array $settings) public function validateSettings(array $settings): array
{ {
$config = (new Config($settings))->toArray(); $config = (new Config($settings))->toArray();
@ -150,7 +150,7 @@ final class Settings {
$looseConfig[$key] = $val; $looseConfig[$key] = $val;
} }
} }
elseif (is_array($val) && ! empty($val)) elseif (\is_array($val) && ! empty($val))
{ {
foreach($val as $k => $v) foreach($val as $k => $v)
{ {
@ -204,7 +204,6 @@ final class Settings {
{ {
dump($e); dump($e);
dump($settings); dump($settings);
die();
return FALSE; return FALSE;
} }

View File

@ -59,20 +59,6 @@ class RoutingBase {
$this->routes = $this->config->get('routes'); $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 * Get the current url path
* @throws \Aviat\Ion\Di\ContainerException * @throws \Aviat\Ion\Di\ContainerException

View File

@ -17,9 +17,9 @@
namespace Aviat\AnimeClient\Types; 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;
public $age_rating_guide; public $age_rating_guide;
public $cover_image; public $cover_image;

View File

@ -17,7 +17,7 @@
namespace Aviat\AnimeClient\Types; namespace Aviat\AnimeClient\Types;
/** /**
* Type representing an Anime object for display * Type representing an anime watch list item
*/ */
final class AnimeListItem extends AbstractType { final class AnimeListItem extends AbstractType {
public $id; public $id;
@ -40,4 +40,9 @@ final class AnimeListItem extends AbstractType {
public $rewatched; public $rewatched;
public $user_rating; public $user_rating;
public $watching_status; public $watching_status;
public function setAnime($anime): void
{
$this->anime = new Anime($anime);
}
} }

25
src/Types/AnimePage.php Normal file
View 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
View 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;
};
}
}

View File

@ -24,6 +24,7 @@ class Config extends AbstractType {
// Settings in config.toml // Settings in config.toml
public $asset_path; // Path to public folder for urls public $asset_path; // Path to public folder for urls
public $dark_theme;
public $default_anime_list_path; public $default_anime_list_path;
public $default_list; public $default_list;
public $default_manga_list_path; public $default_manga_list_path;

View File

@ -20,12 +20,14 @@ namespace Aviat\AnimeClient\Types;
* Type representing an Anime object for display * Type representing an Anime object for display
*/ */
final class MangaPage extends AbstractType { final class MangaPage extends AbstractType {
public $characters;
public $chapter_count; public $chapter_count;
public $cover_image; public $cover_image;
public $genres; public $genres;
public $id; public $id;
public $included; public $included;
public $manga_type; public $manga_type;
public $staff;
public $synopsis; public $synopsis;
public $title; public $title;
public $titles; public $titles;

35
src/Types/Person.php Normal file
View 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
View 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;
}

View File

@ -47,13 +47,13 @@ class UrlGenerator extends RoutingBase {
/** /**
* Get the base url for css/js/images * Get the base url for css/js/images
* *
* @param string ...$args * @param string[] $args
* @return string * @return string
*/ */
public function assetUrl(string ...$args): string public function assetUrl(string ...$args): string
{ {
$baseUrl = rtrim($this->url(''), '/') $baseUrl = rtrim($this->url(''), '/')
. $this->__get('asset_path'); . $this->config->get('asset_path');
array_unshift($args, $baseUrl); array_unshift($args, $baseUrl);
@ -82,7 +82,7 @@ class UrlGenerator extends RoutingBase {
{ {
if ( ! array_key_exists($i + 1, $segments)) if ( ! array_key_exists($i + 1, $segments))
{ {
$segments[$i + 1] = ""; $segments[$i + 1] = '';
} }
$path_segments[$i] = preg_replace('`{.*?}`', $segments[$i + 1], $path_segments[$i]); $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 public function defaultUrl(string $type): string
{ {
$type = trim($type); $type = trim($type);
$defaultPath = $this->__get("default_{$type}_list_path"); $defaultPath = $this->config->get("default_{$type}_list_path");
if ($defaultPath !== NULL) if ($defaultPath !== NULL)
{ {

View File

@ -16,7 +16,6 @@
namespace Aviat\AnimeClient; namespace Aviat\AnimeClient;
use Aviat\Ion\ConfigInterface;
use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
/** /**
@ -42,12 +41,6 @@ class Util {
'me' 'me'
]; ];
/**
* The config manager
* @var ConfigInterface
*/
private $config;
/** /**
* Set up the Util class * Set up the Util class
* *
@ -58,7 +51,6 @@ class Util {
public function __construct(ContainerInterface $container) public function __construct(ContainerInterface $container)
{ {
$this->setContainer($container); $this->setContainer($container);
$this->config = $container->get('config');
} }
/** /**
@ -68,7 +60,7 @@ class Util {
* @param string $b - Second item to compare * @param string $b - Second item to compare
* @return string * @return string
*/ */
public static function isSelected($a, $b) public static function isSelected(string $a, string $b): string
{ {
return ($a === $b) ? 'selected' : ''; return ($a === $b) ? 'selected' : '';
} }
@ -80,7 +72,7 @@ class Util {
* @param string $b - Second item to compare * @param string $b - Second item to compare
* @return string * @return string
*/ */
public static function isNotSelected($a, $b) public static function isNotSelected(string $a, string $b): string
{ {
return ($a !== $b) ? 'selected' : ''; return ($a !== $b) ? 'selected' : '';
} }
@ -108,7 +100,7 @@ class Util {
* *
* @throws \Aviat\Ion\Di\ContainerException * @throws \Aviat\Ion\Di\ContainerException
* @throws \Aviat\Ion\Di\NotFoundException * @throws \Aviat\Ion\Di\NotFoundException
* @return boolean * @return bool
*/ */
public function isFormPage(): bool public function isFormPage(): bool
{ {

View File

@ -153,6 +153,12 @@ const SETTINGS_MAP = [
'default' => 'Somebody', 'default' => 'Somebody',
'description' => 'Name of the owner of the list data.', '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' => [ 'show_anime_collection' => [
'type' => 'boolean', 'type' => 'boolean',
'title' => 'Show Anime Collection', 'title' => 'Show Anime Collection',

31
sw.js
View File

@ -5,36 +5,17 @@ async function fromCache (request) {
return await cache.match(request); return await cache.match(request);
} }
async function fromNetwork (request) { async function updateCache (request) {
return await fetch(request);
}
async function update (request) {
const cache = await caches.open(CACHE_NAME); const cache = await caches.open(CACHE_NAME);
const response = await fetch(request); const response = await fetch(request);
if (request.url.includes('/public/images/')) { if (request.url.includes('/public/images/')) {
console.log('Saving to cache: ', request.url);
await cache.put(request, response.clone()); await cache.put(request, response.clone());
} }
return response; 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 => { self.addEventListener('install', event => {
console.log('Public Folder Worker installed'); console.log('Public Folder Worker installed');
@ -55,8 +36,8 @@ self.addEventListener('install', event => {
) )
}); });
self.addEventListener('activate', event => { self.addEventListener('activate', () => {
console.log('Public Folder Worker activated'); console.info('Public Folder Worker activated');
}); });
// Pull css, images, and javascript from cache // Pull css, images, and javascript from cache
@ -71,11 +52,7 @@ self.addEventListener('fetch', event => {
if (cached !== undefined) { if (cached !== undefined) {
event.respondWith(cached); event.respondWith(cached);
} else { } else {
event.respondWith(fromNetwork(event.request)); event.respondWith(updateCache(event.request));
} }
}); });
event.waitUntil(
update(event.request)
);
}); });

View File

@ -17,7 +17,9 @@
namespace Aviat\AnimeClient\Tests\API; namespace Aviat\AnimeClient\Tests\API;
use function Amp\Promise\wait; 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 Aviat\Ion\Json;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
@ -37,35 +39,35 @@ class APIRequestBuilderTest extends TestCase {
$this->builder->setLogger(new NullLogger); $this->builder->setLogger(new NullLogger);
} }
public function testGzipRequest() public function testGzipRequest(): void
{ {
$request = $this->builder->newRequest('GET', 'gzip') $request = $this->builder->newRequest('GET', 'gzip')
->getFullRequest(); ->getFullRequest();
$response = wait((new HummingbirdClient)->request($request)); $response = getResponse($request);
$body = Json::decode(wait($response->getBody())); $body = Json::decode(wait($response->getBody()));
$this->assertEquals(1, $body['gzipped']); $this->assertEquals(1, $body['gzipped']);
} }
public function testInvalidRequestMethod() public function testInvalidRequestMethod(): void
{ {
$this->expectException(\InvalidArgumentException::class); $this->expectException(\InvalidArgumentException::class);
$this->builder->newRequest('FOO', 'gzip') $this->builder->newRequest('FOO', 'gzip')
->getFullRequest(); ->getFullRequest();
} }
public function testRequestWithBasicAuth() public function testRequestWithBasicAuth(): void
{ {
$request = $this->builder->newRequest('GET', 'headers') $request = $this->builder->newRequest('GET', 'headers')
->setBasicAuth('username', 'password') ->setBasicAuth('username', 'password')
->getFullRequest(); ->getFullRequest();
$response = wait((new HummingbirdClient)->request($request)); $response = getResponse($request);
$body = Json::decode(wait($response->getBody())); $body = Json::decode(wait($response->getBody()));
$this->assertEquals('Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $body['headers']['Authorization']); $this->assertEquals('Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $body['headers']['Authorization']);
} }
public function testRequestWithQueryString() public function testRequestWithQueryString(): void
{ {
$query = [ $query = [
'foo' => 'bar', 'foo' => 'bar',
@ -87,13 +89,13 @@ class APIRequestBuilderTest extends TestCase {
->setQuery($query) ->setQuery($query)
->getFullRequest(); ->getFullRequest();
$response = wait((new HummingbirdClient)->request($request)); $response = getResponse($request);
$body = Json::decode(wait($response->getBody())); $body = Json::decode(wait($response->getBody()));
$this->assertEquals($expected, $body['args']); $this->assertEquals($expected, $body['args']);
} }
public function testFormValueRequest() public function testFormValueRequest(): void
{ {
$formValues = [ $formValues = [
'foo' => 'bar', 'foo' => 'bar',
@ -104,13 +106,13 @@ class APIRequestBuilderTest extends TestCase {
->setFormFields($formValues) ->setFormFields($formValues)
->getFullRequest(); ->getFullRequest();
$response = wait((new HummingbirdClient)->request($request)); $response = getResponse($request);
$body = Json::decode(wait($response->getBody())); $body = Json::decode(wait($response->getBody()));
$this->assertEquals($formValues, $body['form']); $this->assertEquals($formValues, $body['form']);
} }
public function testFullUrlRequest() public function testFullUrlRequest(): void
{ {
$data = [ $data = [
'foo' => [ 'foo' => [
@ -128,7 +130,7 @@ class APIRequestBuilderTest extends TestCase {
->setJsonBody($data) ->setJsonBody($data)
->getFullRequest(); ->getFullRequest();
$response = wait((new HummingbirdClient)->request($request)); $response = getResponse($request);
$body = Json::decode(wait($response->getBody())); $body = Json::decode(wait($response->getBody()));
$this->assertEquals($data, $body['json']); $this->assertEquals($data, $body['json']);

View File

@ -18,7 +18,6 @@ namespace Aviat\AnimeClient\Tests\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\Kitsu\Transformer\AnimeTransformer; use Aviat\AnimeClient\API\Kitsu\Transformer\AnimeTransformer;
use Aviat\AnimeClient\Tests\AnimeClientTestCase; use Aviat\AnimeClient\Tests\AnimeClientTestCase;
use Aviat\Ion\Friend;
use Aviat\Ion\Json; use Aviat\Ion\Json;
class AnimeTransformerTest extends AnimeClientTestCase { class AnimeTransformerTest extends AnimeClientTestCase {

View File

@ -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' => 'R',
'age_rating_guide' => 'Violence, Profanity', 'age_rating_guide' => 'Violence, Profanity',
'cover_image' => 'https://media.kitsu.io/anime/poster_images/7442/small.jpg?1418580054', 'cover_image' => 'https://media.kitsu.io/anime/poster_images/7442/small.jpg?1418580054',

View File

@ -1,4 +1,7 @@
<?php return Aviat\AnimeClient\Types\MangaPage::__set_state(array( <?php return Aviat\AnimeClient\Types\MangaPage::__set_state(array(
'characters' =>
array (
),
'chapter_count' => '-', 'chapter_count' => '-',
'cover_image' => 'https://media.kitsu.io/manga/poster_images/20286/small.jpg?1434293999', 'cover_image' => 'https://media.kitsu.io/manga/poster_images/20286/small.jpg?1434293999',
'genres' => 'genres' =>
@ -68,6 +71,9 @@
), ),
), ),
'manga_type' => 'manga', '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! '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)', (Source: Kirei Cake)',
'title' => 'Bokura wa Minna Kawaisou', 'title' => 'Bokura wa Minna Kawaisou',