Version 5.1 - All the GraphQL #32

Closed
timw4mail wants to merge 1160 commits from develop into master
109 changed files with 3021 additions and 1392 deletions
Showing only changes of commit 6df059fe75 - Show all commits

View File

@ -3,8 +3,6 @@
Update your anime/manga list on Kitsu.io and MyAnimeList.net
[![Build Status](https://travis-ci.org/timw4mail/HummingBirdAnimeClient.svg?branch=master)](https://travis-ci.org/timw4mail/HummingBirdAnimeClient)
[![build status](https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient/badges/develop/build.svg)](https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient/commits/develop)
[![coverage report](https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient/badges/develop/coverage.svg)](https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient/commits/develop)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/timw4mail/HummingBirdAnimeClient/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/timw4mail/HummingBirdAnimeClient/?branch=master)
[[Hosted Example](https://list.timshomepage.net)]
@ -52,12 +50,8 @@ Update your anime/manga list on Kitsu.io and MyAnimeList.net
* public/images/manga
5. Make sure the `console` script is executable
### Using MAL API
1. Update `app/config/mal.toml` with your username and password
2. Enable MAL api in `app/config/config.toml`
### Server Setup
See the [wiki](https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient/wikis/home)
See the [wiki](https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient/wiki)
for more in-depth information

View File

@ -8,13 +8,12 @@
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2017 Timothy J. Warren
* @copyright 2015 - 2018 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
use const Aviat\AnimeClient\{
DEFAULT_CONTROLLER_METHOD,
DEFAULT_CONTROLLER
@ -184,6 +183,16 @@ return [
// ---------------------------------------------------------------------
// Default / Shared routes
// ---------------------------------------------------------------------
'anilist-redirect' => [
'path' => '/anilist-redirect',
'action' => 'anilistRedirect',
'controller' => DEFAULT_CONTROLLER,
],
'anilist-oauth' => [
'path' => '/anilist-oauth',
'action' => 'anilistCallback',
'controller' => DEFAULT_CONTROLLER,
],
'image_proxy' => [
'path' => '/public/images/{type}/{file}',
'action' => 'images',

View File

@ -20,6 +20,7 @@ use Aura\Html\HelperLocatorFactory;
use Aura\Router\RouterContainer;
use Aura\Session\SessionFactory;
use Aviat\AnimeClient\API\{
Anilist,
Kitsu,
MAL,
Kitsu\KitsuRequestBuilder,
@ -45,11 +46,14 @@ return function (array $configArray = []) {
$appLogger = new Logger('animeclient');
$appLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/app.log', Logger::NOTICE));
$anilistRequestLogger = new Logger('anilist-request');
$anilistRequestLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/anilist_request.log', Logger::NOTICE));
$kitsuRequestLogger = new Logger('kitsu-request');
$kitsuRequestLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/kitsu_request.log', Logger::NOTICE));
$malRequestLogger = new Logger('mal-request');
$malRequestLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/mal_request.log', Logger::NOTICE));
$container->setLogger($appLogger);
$container->setLogger($anilistRequestLogger, 'anilist-request');
$container->setLogger($kitsuRequestLogger, 'kitsu-request');
$container->setLogger($malRequestLogger, 'mal-request');

View File

@ -11,8 +11,5 @@ whose_list = "Tim"
# do you wish to show the anime collection?
show_anime_collection = true
# do you have a My Anime List account set up in mal.toml?
use_mal_api = false
# path to public directory on the server
asset_dir = "/../../public"

View File

@ -1,6 +0,0 @@
################################################################################
# My Anime LIst Integration Config #
################################################################################
username = "timw4mail"
password = "mysecretpassword"

View File

@ -13,7 +13,7 @@
</section>
</section>
<br />
<table class="form">
<table class="invisible form">
<tbody>
<tr>
<td><label for="status">Watching Status</label></td>

View File

@ -1,4 +1,4 @@
<main>
<main class="media-list">
<?php if ($auth->isAuthenticated()): ?>
<a class="bracketed" href="<?= $url->generate('anime.add.get') ?>">Add Item</a>
<?php endif ?>
@ -24,27 +24,13 @@
<img src="<?= $urlGenerator->assetUrl("images/anime/{$item['anime']['id']}.jpg") ?>" alt="" />
<div class="name">
<a href="<?= $url->generate('anime.details', ['id' => $item['anime']['slug']]); ?>">
<?= array_shift($item['anime']['titles']) ?>
<span class="canonical"><?= $item['anime']['title'] ?></span>
<?php foreach ($item['anime']['titles'] as $title): ?>
<br /><small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
<div class="table">
<?php if ($auth->isAuthenticated()): ?>
<div class="row">
<span class="edit">
<a class="bracketed" title="Edit information about this anime" href="<?=
$url->generate('edit', [
'controller' => 'anime',
'id' => $item['id'],
'status' => $item['watching_status']
]);
?>">Edit</a>
</span>
</div>
<?php endif ?>
<?php if ($item['private'] || $item['rewatching']): ?>
<div class="row">
<?php foreach(['private', 'rewatching'] as $attr): ?>
@ -77,6 +63,20 @@
</div>
<?php endif ?>
<?php if ($auth->isAuthenticated()): ?>
<div class="row">
<span class="edit">
<a class="bracketed" title="Edit information about this anime" href="<?=
$url->generate('edit', [
'controller' => 'anime',
'id' => $item['id'],
'status' => $item['watching_status']
]);
?>">Edit</a>
</span>
</div>
<?php endif ?>
<div class="row">
<div class="user_rating">Rating: <?= $item['user_rating'] ?> / 10</div>
<div class="completion">Episodes:
@ -85,7 +85,7 @@
</div>
</div>
<div class="row">
<div class="media_type"><?= $escape->html($item['anime']['type']) ?></div>
<div class="media_type"><?= $escape->html($item['anime']['show_type']) ?></div>
<div class="airing_status"><?= $escape->html($item['airing']['status']) ?></div>
<div class="age_rating"><?= $escape->html($item['anime']['age_rating']) ?></div>
</div>

View File

@ -38,7 +38,7 @@
</table>
</div>
<div>
<h2><a rel="external" href="<?= $show_data['url'] ?>"><?= array_shift($show_data['titles']) ?></a></h2>
<h2><a rel="external" href="<?= $show_data['url'] ?>"><?= $show_data['title'] ?></a></h2>
<?php foreach ($show_data['titles'] as $title): ?>
<h3><?= $title ?></h3>
<?php endforeach ?>

View File

@ -2,23 +2,25 @@
<main>
<h2>Edit Anime List Item</h2>
<form action="<?= $action ?>" method="post">
<table class="form">
<table class="invisible form">
<thead>
<tr>
<th>
<h3><?= $escape->html(array_shift($item['anime']['titles'])) ?></h3>
<h3><?= $escape->html($item['anime']['title']) ?></h3>
<?php foreach($item['anime']['titles'] as $title): ?>
<h4><?= $escape->html($title) ?></h4>
<?php endforeach ?>
</th>
<th>
<article class="media">
<?= $helper->img($urlGenerator->assetUrl('images/anime', "{$item['anime']['id']}.jpg")) ?>
</article>
</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="9">
<article class="media">
<?= $helper->img($urlGenerator->assetUrl('images/anime', "{$item['anime']['id']}.jpg")) ?>
</article>
</td>
</tr>
<tr>
<td><label for="private">Is Private?</label></td>
<td>

View File

@ -1,4 +1,4 @@
<main>
<main class="media-list">
<?php if ($auth->isAuthenticated()): ?>
<a class="bracketed" href="<?= $url->generate('anime.add.get') ?>">Add Item</a>
<?php endif ?>
@ -42,15 +42,15 @@
<?php endif ?>
<td class="justify">
<a href="<?= $url->generate('anime.details', ['id' => $item['anime']['slug']]) ?>">
<?= array_shift($item['anime']['titles']) ?>
<?= $item['anime']['title'] ?>
</a>
<?php foreach($item['anime']['titles'] as $title): ?>
<br /><?= $title ?>
<?php foreach ($item['anime']['titles'] as $title): ?>
<br/><?= $title ?>
<?php endforeach ?>
</td>
<td><?= $item['airing']['status'] ?></td>
<td><?= $item['user_rating'] ?> / 10 </td>
<td><?= $item['anime']['type'] ?></td>
<td><?= $item['anime']['show_type'] ?></td>
<td id="<?= $item['anime']['slug'] ?>">
Episodes: <br />
<span class="completed_number"><?= $item['episodes']['watched'] ?></span>&nbsp;/&nbsp;<span class="total_number"><?= $item['episodes']['total'] ?></span>
@ -83,8 +83,8 @@
<p><?= $escape->html($item['notes']) ?></p>
</td>
<td class="align_left">
<?php sort($item['anime']['genres']) ?>
<?= implode(', ', $item['anime']['genres']) ?>
<?php sort($item['anime']->genres) ?>
<?= implode(', ', $item['anime']->genres) ?>
</td>
</tr>
<?php endforeach ?>

View File

@ -13,7 +13,7 @@
</section>
</section>
<br />
<table class="form">
<table class="invisible form">
<tbody>
<tr>
<td><label for="media_id">Media</label></td>

View File

@ -0,0 +1,27 @@
<article class="media" id="a-<?= $item['hummingbird_id'] ?>">
<img src="<?= $urlGenerator->assetUrl("images/anime/{$item['hummingbird_id']}.jpg") ?>"
alt="<?= $item['title'] ?> cover image"/>
<div class="name">
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
<?= $item['title'] ?>
<?= ($item['alternate_title'] != "") ? "<small><br />{$item['alternate_title']}</small>" : ""; ?>
</a>
</div>
<div class="table">
<?php if ($auth->isAuthenticated()): ?>
<div class="row">
<span class="edit">
<a class="bracketed"
href="<?= $url->generate($collection_type . '.collection.edit.get', [
'id' => $item['hummingbird_id']
]) ?>">Edit</a>
</span>
</div>
<?php endif ?>
<div class="row">
<div class="completion">Episodes: <?= $item['episode_count'] ?></div>
<div class="media_type"><?= $item['show_type'] ?></div>
<div class="age_rating"><?= $item['age_rating'] ?></div>
</div>
</div>
</article>

View File

@ -1,44 +1,25 @@
<main>
<main class="media-list">
<?php if ($auth->isAuthenticated()): ?>
<a class="bracketed" href="<?= $url->generate($collection_type . '.collection.add.get') ?>">Add Item</a>
<?php endif ?>
<?php if (empty($sections)): ?>
<h3>There's nothing here!</h3>
<?php else: ?>
<div class="tabs">
<?php $i = 0; ?>
<?php foreach ($sections as $name => $items): ?>
<section class="status">
<input <?= $i === 0 ? 'checked="checked"' : '' ?> type="radio" id="collection-tab-<?= $i ?>" name="collection-tabs" />
<label for="collection-tab-<?= $i ?>"><?= $name ?></label>
<div class="content">
<h2><?= $name ?></h2>
<section class="media-wrap">
<?php foreach($items as $item): ?>
<article class="media" id="a-<?= $item['hummingbird_id'] ?>">
<img src="<?= $urlGenerator->assetUrl("images/anime/{$item['hummingbird_id']}.jpg") ?>"
alt="<?= $item['title'] ?> cover image" />
<div class="name">
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
<?= $item['title'] ?>
<?= ($item['alternate_title'] != "") ? "<small><br />{$item['alternate_title']}</small>" : ""; ?>
</a>
</div>
<div class="table">
<?php if ($auth->isAuthenticated()): ?>
<div class="row">
<span class="edit">
<a class="bracketed" href="<?= $url->generate($collection_type . '.collection.edit.get', [
'id' => $item['hummingbird_id']
]) ?>">Edit</a>
</span>
</div>
<?php endif ?>
<div class="row">
<div class="completion">Episodes: <?= $item['episode_count'] ?></div>
<div class="media_type"><?= $item['show_type'] ?></div>
<div class="age_rating"><?= $item['age_rating'] ?></div>
</div>
</div>
</article>
<?php foreach ($items as $item): ?>
<?php include __DIR__ . '/cover-item.php'; ?>
<?php endforeach ?>
</section>
</section>
</div>
<?php $i++; ?>
<?php endforeach ?>
</div>
<?php endif ?>
</main>

View File

@ -2,7 +2,7 @@
<main>
<h2>Edit Anime Collection Item</h2>
<form action="<?= $action_url ?>" method="post">
<table class="form">
<table class="invisible form" style="border:0">
<thead>
<tr>
<th>
@ -11,17 +11,19 @@
<h4><?= $item['alternate_title'] ?></h4>
<?php endif ?>
</th>
<th>
<article class="media">
<?= $helper->img($urlGenerator->assetUrl("images/anime/{$item['hummingbird_id']}.jpg")); ?>
</article>
</th>
</tr>
</thead>
<tbody>
<tr>
<td><label for="media_id">Media</label></td>
<td>
<td rowspan="4" class="align_center">
<article class="media">
<?= $helper->img($urlGenerator->assetUrl("images/anime/{$item['hummingbird_id']}.jpg")); ?>
</article>
</td>
</tr>
<tr>
<td class="align_right"><label for="media_id">Media</label></td>
<td class="align_left">
<select name="media_id" id="media_id">
<?php foreach($media_items as $id => $name): ?>
<option <?= $item['media_id'] == $id ? 'selected="selected"' : '' ?> value="<?= $id ?>"><?= $name ?></option>

View File

@ -0,0 +1,19 @@
<tr>
<?php if ($auth->isAuthenticated()): ?>
<td>
<a class="bracketed"
href="<?= $url->generate($collection_type . '.collection.edit.get', ['id' => $item['hummingbird_id']]) ?>">Edit</a>
</td>
<?php endif ?>
<td class="align_left">
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
<?= $item['title'] ?>
</a>
<?= (!empty($item['alternate_title'])) ? " <br /><small> " . $item['alternate_title'] . "</small>" : "" ?>
</td>
<td><?= $item['episode_count'] ?></td>
<td><?= $item['episode_length'] ?></td>
<td><?= $item['show_type'] ?></td>
<td><?= $item['age_rating'] ?></td>
<td class="align_left"><?= $item['notes'] ?></td>
</tr>

View File

@ -1,17 +1,23 @@
<main>
<?php if ($auth->isAuthenticated()): ?>
<a class="bracketed" href="<?= $url->generate($collection_type . '.collection.add.get') ?>">Add Item</a>
<?php endif ?>
<?php if (empty($sections)): ?>
<h3>There's nothing here!</h3>
<?php else: ?>
<?php if ($auth->isAuthenticated()): ?>
<a class="bracketed" href="<?= $url->generate($collection_type . '.collection.add.get') ?>">Add Item</a>
<?php endif ?>
<?php if (empty($sections)): ?>
<h3>There's nothing here!</h3>
<?php else: ?>
<?php $i = 0; ?>
<div class="tabs">
<?php foreach ($sections as $name => $items): ?>
<input <?= $i === 0 ? 'checked="checked"' : '' ?> type="radio" id="collection-tab-<?= $i ?>"
name="collection-tabs"/>
<label for="collection-tab-<?= $i ?>"><?= $name ?></label>
<div class="content">
<h2><?= $name ?></h2>
<table>
<thead>
<tr>
<?php if($auth->isAuthenticated()): ?>
<th>Actions</th>
<?php if ($auth->isAuthenticated()): ?>
<td>Actions</td>
<?php endif ?>
<th>Title</th>
<th>Episode Count</th>
@ -22,30 +28,15 @@
</tr>
</thead>
<tbody>
<?php foreach($items as $item): ?>
<tr>
<?php if($auth->isAuthenticated()): ?>
<td>
<a class="bracketed" href="<?= $url->generate($collection_type . '.collection.edit.get', ['id' => $item['hummingbird_id']]) ?>">Edit</a>
</td>
<?php endif ?>
<td class="align_left">
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
<?= $item['title'] ?>
</a>
<?= ( ! empty($item['alternate_title'])) ? " <br /><small> " . $item['alternate_title'] . "</small>" : "" ?>
</td>
<td><?= $item['episode_count'] ?></td>
<td><?= $item['episode_length'] ?></td>
<td><?= $item['show_type'] ?></td>
<td><?= $item['age_rating'] ?></td>
<td class="align_left"><?= $item['notes'] ?></td>
</tr>
<?php foreach ($items as $item): ?>
<?php include __DIR__ . '/list-item.php' ?>
<?php endforeach ?>
</tbody>
</table>
<br />
</div>
<?php $i++ ?>
<?php endforeach ?>
<?php endif ?>
</div>
<?php endif ?>
</main>
<script defer="defer" src="<?= $urlGenerator->assetUrl('js.php/g/table') ?>"></script>

View File

@ -1,11 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title><?= $title ?></title>
<meta charset="utf-8" />
<meta http-equiv="cache-control" content="no-store" />
<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=0" />
<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="icon" href="<?= $urlGenerator->assetUrl('images/icons/favicon.ico') ?>" />
<link rel="apple-touch-icon" sizes="57x57" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-57x57.png') ?>">

View File

@ -7,7 +7,7 @@ $lastSegment = $urlGenerator->lastSegment();
$extraSegment = $lastSegment === 'list' ? '/list' : '';
?>
<h1 class="flex flex-align-end flex-wrap">
<div id="main-nav" class="flex flex-align-end flex-wrap">
<span class="flex-no-wrap grow-1">
<?php if(strpos($route_path, 'collection') === FALSE): ?>
<?= $helper->a(
@ -67,7 +67,7 @@ $extraSegment = $lastSegment === 'list' ? '/list' : '';
[<?= $helper->a($url->generate('login'), "{$whose} Login") ?>]
<?php endif ?>
</span>
</h1>
</div>
<nav>
<?php if ($container->get('util')->isViewPage()): ?>
<?= $helper->menu($menu_name) ?>

View File

@ -13,7 +13,7 @@
</section>
</section>
<br />
<table class="form">
<table class="invisible form">
<tbody>
<tr>
<td><label for="status">Reading Status</label></td>

View File

@ -1,4 +1,4 @@
<main>
<main class="media-list">
<?php if ($auth->isAuthenticated()): ?>
<a class="bracketed" href="<?= $url->generate('manga.add.get') ?>">Add Item</a>
<?php endif ?>
@ -26,7 +26,7 @@
<img src="<?= $urlGenerator->assetUrl('images/manga', "{$item['manga']['id']}.jpg") ?>" />
<div class="name">
<a href="<?= $url->generate('manga.details', ['id' => $item['manga']['slug']]) ?>">
<?= $escape->html(array_shift($item['manga']['titles'])) ?>
<?= $escape->html($item['manga']['title']) ?>
<?php foreach($item['manga']['titles'] as $title): ?>
<br /><small><?= $title ?></small>
<?php endforeach ?>
@ -73,8 +73,8 @@
Chapters: <span class="chapters_read"><?= $item['chapters']['read'] ?></span> /
<span class="chapter_count"><?= $item['chapters']['total'] ?></span>
</div>
</div>
<div class="row">
<?php /* </div>
<div class="row"> */ ?>
<div class="volume_completion">
Volumes: <span class="volume_count"><?= $item['volumes']['total'] ?></span>
</div>

View File

@ -1,32 +1,34 @@
<?php if ($auth->isAuthenticated()): ?>
<main>
<h1>
Edit <?= $item['manga']['titles'][0] ?>
</h1>
<h2>
Edit Manga List Item
</h2>
<form action="<?= $action ?>" method="post">
<table class="form">
<table class="invisible form">
<thead>
<tr>
<th>
<h3><?= $escape->html(array_shift($item['manga']['titles'])) ?></h3>
<?php foreach($item['manga']['titles'] as $title): ?>
<h3><?= $escape->html($item['manga']['title']) ?></h3>
<?php foreach ($item['manga']['titles'] as $title): ?>
<h4><?= $escape->html($title) ?></h4>
<?php endforeach ?>
</th>
<th>
<article class="media">
<?= $helper->img($urlGenerator->assetUrl('images/manga', "{$item['manga']['id']}.jpg")); ?>
</article>
</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="9">
<article class="media">
<?= $helper->img($urlGenerator->assetUrl('images/manga', "{$item['manga']['id']}.jpg")); ?>
</article>
</td>
</tr>
<tr>
<td><label for="status">Reading Status</label></td>
<td>
<select name="status" id="status">
<?php foreach($status_list as $val => $status): ?>
<option <?php if($item['reading_status'] === $val): ?>selected="selected"<?php endif ?>
<?php foreach ($status_list as $val => $status): ?>
<option <?php if ($item['reading_status'] === $val): ?>selected="selected"<?php endif ?>
value="<?= $val ?>"><?= $status ?></option>
<?php endforeach ?>
</select>
@ -35,33 +37,37 @@
<tr>
<td><label for="series_rating">Rating</label></td>
<td>
<input type="number" min="0" max="10" maxlength="2" name="new_rating" value="<?= $item['user_rating'] ?>" id="series_rating" size="2" /> / 10
<input type="number" min="0" max="10" maxlength="2" name="new_rating"
value="<?= $item['user_rating'] ?>" id="series_rating" size="2"/> / 10
</td>
</tr>
<tr>
<td><label for="chapters_read">Chapters Read</label></td>
<td>
<input type="number" min="0" name="chapters_read" id="chapters_read" value="<?= $item['chapters']['read'] ?>" /> / <?= $item['chapters']['total'] ?>
<input type="number" min="0" name="chapters_read" id="chapters_read"
value="<?= $item['chapters']['read'] ?>"/> / <?= $item['chapters']['total'] ?>
</td>
</tr>
<? /*<tr>
<tr>
<td><label for="volumes_read">Volumes Read</label></td>
<td>
<input type="number" min="0" name="volumes_read" id="volumes_read" value="<?= $item['volumes']['read'] ?>" /> / <?= $item['volumes']['total'] ?>
<?php /*<input type="number" disabled="disabled" min="0" name="volumes_read" id="volumes_read" value="" /> */ ?>
- / <?= $item['volumes']['total'] ?>
</td>
</tr> */ ?>
</tr>
<tr>
<td><label for="rereading_flag">Rereading?</label></td>
<td>
<input type="checkbox" name="rereading" id="rereading_flag"
<?php if($item['rereading'] === TRUE): ?>checked="checked"<?php endif ?>
<?php if ($item['rereading'] === TRUE): ?>checked="checked"<?php endif ?>
/>
</td>
</tr>
<tr>
<td><label for="reread_count">Reread Count</label></td>
<td>
<input type="number" min="0" id="reread_count" name="reread_count" value="<?= $item['reread'] ?>" />
<input type="number" min="0" id="reread_count" name="reread_count"
value="<?= $item['reread'] ?>"/>
</td>
</tr>
<tr>
@ -73,11 +79,11 @@
<tr>
<td>&nbsp;</td>
<td>
<input type="hidden" value="<?= $item['id'] ?>" name="id" />
<input type="hidden" value="<?= $item['mal_id'] ?>" name="mal_id" />
<input type="hidden" value="<?= $item['manga']['slug'] ?>" name="manga_id" />
<input type="hidden" value="<?= $item['user_rating'] ?>" name="old_rating" />
<input type="hidden" value="true" name="edit" />
<input type="hidden" value="<?= $item['id'] ?>" name="id"/>
<input type="hidden" value="<?= $item['mal_id'] ?>" name="mal_id"/>
<input type="hidden" value="<?= $item['manga']['slug'] ?>" name="manga_id"/>
<input type="hidden" value="<?= $item['user_rating'] ?>" name="old_rating"/>
<input type="hidden" value="true" name="edit"/>
<button type="submit">Submit</button>
</td>
</tr>
@ -92,8 +98,8 @@
<tr>
<td>&nbsp;</td>
<td>
<input type="hidden" value="<?= $item['id'] ?>" name="id" />
<input type="hidden" value="<?= $item['mal_id'] ?>" name="mal_id" />
<input type="hidden" value="<?= $item['id'] ?>" name="id"/>
<input type="hidden" value="<?= $item['mal_id'] ?>" name="mal_id"/>
<button type="submit" class="danger">Delete Entry</button>
</td>
</tr>

View File

@ -1,4 +1,4 @@
<main>
<main class="media-list">
<?php if ($auth->isAuthenticated()): ?>
<a class="bracketed" href="<?= $url->generate('manga.add.get') ?>">Add Item</a>
<?php endif ?>
@ -14,7 +14,7 @@
<thead>
<tr>
<?php if ($auth->isAuthenticated()): ?>
<th>&nbsp;</th>
<td>&nbsp;</td>
<?php endif ?>
<th>Title</th>
<th>Rating</th>
@ -39,7 +39,7 @@
<?php endif ?>
<td class="align_left">
<a href="<?= $url->generate('manga.details', ['id' => $item['manga']['slug']]) ?>">
<?= array_shift($item['manga']['titles']) ?>
<?= $item['manga']['title'] ?>
</a>
<?php foreach($item['manga']['titles'] as $title): ?>
<br /><?= $title ?>

View File

@ -39,6 +39,7 @@
"phpmd/phpmd": "^2.4",
"phpstan/phpstan": "^0.9.1",
"phpunit/phpunit": "^6.0",
"roave/security-advisories": "dev-master",
"robmorgan/phinx": "^0.9.1",
"sebastian/phpcpd": "^3.0",
"spatie/phpunit-snapshot-assertions": "^1.2.0",

View File

@ -28,9 +28,12 @@ if ($timezone === '' || $timezone === FALSE)
// Load composer autoloader
require_once __DIR__ . '/vendor/autoload.php';
$whoops = new \Whoops\Run;
$whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler);
$whoops->register();
// if (array_key_exists('ENV', $_ENV) && $_ENV['ENV'] === 'development')
{
$whoops = new \Whoops\Run;
$whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler);
$whoops->register();
}
// Define base directories
$APP_DIR = _dir(__DIR__, 'app');

View File

@ -7,7 +7,7 @@ const atImport = require('postcss-import');
const cssNext = require('postcss-cssnext');
const cssNano = require('cssnano');
const css = fs.readFileSync('css/base.css', 'utf8');
const css = fs.readFileSync('css/base.css', 'utf-8');
postcss()
.use(atImport())

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,7 @@ use Aviat\AnimeClient\API\HummingbirdClient;
use Aviat\Ion\{Json, JsonException};
// Include Amp and Artax
require_once('../vendor/autoload.php');
require_once '../vendor/autoload.php';
//Creative rewriting of /g/groupname to ?g=groupname
$pi = $_SERVER['PATH_INFO'];
@ -318,10 +318,11 @@ class JSMin {
$lastModifiedDate = gmdate('D, d M Y H:i:s', $lastModified);
$expiresDate = gmdate('D, d M Y H:i:s', $expires);
header("Content-Type: {$mimeType}; charset=utf8");
header("Cache-control: public, max-age=691200, must-revalidate");
header("Last-Modified: {$lastModifiedDate} GMT");
header("Content-Type: {$mimeType}; charset=utf-8");
header('Cache-control: public, max-age=691200, must-revalidate');
header("Expires: {$expiresDate} GMT");
header("Last-Modified: {$lastModifiedDate} GMT");
header('X-Content-Type-Options: no-sniff');
echo $content;
@ -335,7 +336,7 @@ class JSMin {
*/
public static function send304()
{
header("status: 304 Not Modified", true, 304);
header('status: 304 Not Modified', true, 304);
}
}

View File

@ -39,8 +39,17 @@
data,
dataType: 'json',
type: 'POST',
success: () => {
if (data.data.status === 'completed') {
success: (res) => {
const resData = JSON.parse(res);
if (resData.errors) {
_.hide(_.$('#loading-shadow')[ 0 ]);
_.showMessage('error', `Failed to update ${title}. `);
_.scrollToTop();
return;
}
if (resData.data.attributes.status === 'completed') {
_.hide(parentSel);
}

View File

@ -273,7 +273,7 @@ var AnimeClient = (function(w) {
responseText = request.responseText;
}
if (request.status > 400) {
if (request.status > 299) {
config.error.call(null, request.status, responseText, request.response);
} else {
config.success.call(null, responseText, request.status);

View File

@ -1,16 +1,15 @@
const LightTableSorter = (function() {
let _cellIndex, _onClickEvent, _order, _reset, _sort, _text, _th, _toggle;
_th = null;
_cellIndex = null;
_order = '';
_text = function(row) {
return row.cells.item(_cellIndex).textContent.toLowerCase();
'use strict';
const LightTableSorter = (() => {
let th = null;
let cellIndex = null;
let order = '';
const text = (row) => {
return row.cells.item(cellIndex).textContent.toLowerCase();
};
_sort = function(a, b) {
let n, textA, textB;
textA = _text(a);
textB = _text(b);
n = parseInt(textA, 10);
const sort = (a, b) => {
let textA = text(a);
let textB = text(b);
const n = parseInt(textA, 10);
if (n) {
textA = n;
textB = parseInt(textB, 10);
@ -23,51 +22,49 @@ const LightTableSorter = (function() {
}
return 0;
};
_toggle = function() {
let c;
c = _order !== 'sorting_asc' ? 'sorting_asc' : 'sorting_desc';
_th.className = (_th.className.replace(_order, '') + ' ' + c).trim();
return _order = c;
const toggle = () => {
const c = order !== 'sorting_asc' ? 'sorting_asc' : 'sorting_desc';
th.className = (th.className.replace(order, '') + ' ' + c).trim();
return order = c;
};
_reset = function() {
_th.className = _th.className.replace('sorting_asc', 'sorting').replace('sorting_desc', 'sorting');
return _order = '';
const reset = () => {
th.classList.remove('sorting_asc', 'sorting_desc');
th.classList.add('sorting');
return order = '';
};
_onClickEvent = function(e) {
let row, rows, tbody, _i, _len;
if (_th && (_cellIndex !== e.target.cellIndex)) {
_reset();
const onClickEvent = (e) => {
if (th && (cellIndex !== e.target.cellIndex)) {
reset();
}
_th = e.target;
if (_th.nodeName.toLowerCase() === 'th') {
_cellIndex = _th.cellIndex;
tbody = _th.offsetParent.getElementsByTagName('tbody')[0];
rows = tbody.rows;
th = e.target;
if (th.nodeName.toLowerCase() === 'th') {
cellIndex = th.cellIndex;
const tbody = th.offsetParent.getElementsByTagName('tbody')[0];
let rows = Array.from(tbody.rows);
if (rows) {
rows = Array.prototype.slice.call(rows, 0);
rows = Array.prototype.sort.call(rows, _sort);
if (_order === 'sorting_asc') {
Array.prototype.reverse.call(rows);
rows.sort(sort);
if (order === 'sorting_asc') {
rows.reverse();
}
_toggle();
toggle();
tbody.innerHtml = '';
for (_i = 0, _len = rows.length; _i < _len; _i++) {
row = rows[_i];
rows.forEach(row => {
tbody.appendChild(row);
}
});
}
}
};
return {
init: function() {
init: () => {
let ths = document.getElementsByTagName('th');
let _results = [];
for (let _i = 0, _len = ths.length; _i < _len; _i++) {
let th = ths[_i];
th.className = 'sorting';
_results.push(th.onclick = _onClickEvent);
let results = [];
for (let i = 0, len = ths.length; i < len; i++) {
let th = ths[i];
th.classList.add('sorting');
results.push(th.onclick = onClickEvent);
}
return _results;
return results;
}
};
})();

0
public/js/cache/.gitkeep vendored Normal file → Executable file
View File

View File

@ -4,10 +4,10 @@
"watch": "watch 'npm run build' --filter=./cssfilter.js"
},
"devDependencies": {
"cssnano": "^3.10.0",
"cssnano": "^4.0.5",
"postcss-cachify": "^1.3.1",
"postcss-cssnext": "^3.0.0",
"postcss-import": "^10.0.0",
"postcss-import": "^12.0.0",
"watch": "^1.0.2"
}
}

File diff suppressed because it is too large Load Diff

73
src/API/Anilist.php Normal file
View File

@ -0,0 +1,73 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API;
use Aviat\AnimeClient\API\Enum\{
AnimeWatchingStatus\Kitsu as KAWS,
MangaReadingStatus\Kitsu as KMRS
};
use Aviat\AnimeClient\API\Enum\{
AnimeWatchingStatus\Anilist as AnimeWatchingStatus,
MangaReadingStatus\Anilist as MangaReadingStatus
};
/**
* Constants and mappings for the Anilist API
*/
final class Anilist {
const AUTH_URL = 'https://anilist.co/api/v2/oauth/authorize';
const BASE_URL = 'https://graphql.anilist.co';
const KITSU_ANILIST_WATCHING_STATUS_MAP = [
KAWS::WATCHING => AnimeWatchingStatus::WATCHING,
KAWS::COMPLETED => AnimeWatchingStatus::COMPLETED,
KAWS::ON_HOLD => AnimeWatchingStatus::ON_HOLD,
KAWS::DROPPED => AnimeWatchingStatus::DROPPED,
KAWS::PLAN_TO_WATCH => AnimeWatchingStatus::PLAN_TO_WATCH,
];
const ANILIST_KITSU_WATCHING_STATUS_MAP = [
'CURRENT' => KAWS::WATCHING,
'COMPLETED' => KAWS::COMPLETED,
'PAUSED' => KAWS::ON_HOLD,
'DROPPED' => KAWS::DROPPED,
'PLANNING' => KAWS::PLAN_TO_WATCH,
];
public static function getIdToWatchingStatusMap()
{
return [
'CURRENT' => AnimeWatchingStatus::WATCHING,
'COMPLETED' => AnimeWatchingStatus::COMPLETED,
'PAUSED' => AnimeWatchingStatus::ON_HOLD,
'DROPPED' => AnimeWatchingStatus::DROPPED,
'PLANNING' => AnimeWatchingStatus::PLAN_TO_WATCH,
'REPEATING' => AnimeWatchingStatus::WATCHING,
];
}
public static function getIdToReadingStatusMap()
{
return [
'CURRENT' => MangaReadingStatus::READING,
'COMPLETED' => MangaReadingStatus::COMPLETED,
'PAUSED' => MangaReadingStatus::ON_HOLD,
'DROPPED' => MangaReadingStatus::DROPPED,
'PLANNING' => MangaReadingStatus::PLAN_TO_READ
];
}
}

View File

@ -0,0 +1,49 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Anilist;
use const Aviat\AnimeClient\USER_AGENT;
use Aviat\AnimeClient\API\APIRequestBuilder;
final class AnilistRequestBuilder extends APIRequestBuilder {
/**
* The base url for api requests
* @var string $base_url
*/
protected $baseUrl = 'https://kitsu.io/api/edge/';
/**
* Valid HTTP request methods
* @var array
*/
protected $validMethods = ['POST'];
/**
* HTTP headers to send with every request
*
* @var array
*/
protected $defaultHeaders = [
'User-Agent' => USER_AGENT,
'Accept' => 'application/vnd.api+json',
'Content-Type' => 'application/vnd.api+json',
'CLIENT_ID' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd',
'CLIENT_SECRET' => '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151',
];
}

View File

@ -0,0 +1,179 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\MAL;
use function Amp\Promise\wait;
use Aviat\AnimeClient\API\{
Anilist,
HummingbirdClient
};
trait AnilistTrait {
/**
* The request builder for the MAL API
* @var AnilistRequestBuilder
*/
protected $requestBuilder;
/**
* The base url for api requests
* @var string $base_url
*/
protected $baseUrl = Anilist::BASE_URL;
/**
* HTTP headers to send with every request
*
* @var array
*/
protected $defaultHeaders = [
'Accept' => 'application/json',
'Accept-Encoding' => 'gzip',
'Content-type' => 'application/json',
'User-Agent' => "Tim's Anime Client/4.0"
];
/**
* Set the request builder object
*
* @param MALRequestBuilder $requestBuilder
* @return self
*/
public function setRequestBuilder($requestBuilder): self
{
$this->requestBuilder = $requestBuilder;
return $this;
}
/**
* Create a request object
*
* @param string $type
* @param string $url
* @param array $options
* @return \Amp\Artax\Response
*/
public function setUpRequest(string $type, string $url, array $options = [])
{
$config = $this->container->get('config');
$request = $this->requestBuilder
->newRequest($type, $url)
->setBasicAuth($config->get(['mal','username']), $config->get(['mal','password']));
if (array_key_exists('query', $options))
{
$request = $request->setQuery($options['query']);
}
if (array_key_exists('body', $options))
{
$request = $request->setBody($options['body']);
}
return $request->getFullRequest();
}
/**
* Make a request
*
* @param string $type
* @param string $url
* @param array $options
* @return \Amp\Artax\Response
*/
private function getResponse(string $type, string $url, array $options = [])
{
$logger = NULL;
if ($this->getContainer())
{
$logger = $this->container->getLogger('mal-request');
}
$request = $this->setUpRequest($type, $url, $options);
$response = wait((new HummingbirdClient)->request($request));
$logger->debug('MAL api response', [
'status' => $response->getStatus(),
'reason' => $response->getReason(),
'body' => $response->getBody(),
'headers' => $response->getHeaders(),
'requestHeaders' => $request->getHeaders(),
]);
return $response;
}
/**
* Make a request
*
* @param string $type
* @param string $url
* @param array $options
* @return array
*/
private function request(string $type, string $url, array $options = []): array
{
$logger = NULL;
if ($this->getContainer())
{
$logger = $this->container->getLogger('anilist-request');
}
$response = $this->getResponse($type, $url, $options);
if ((int) $response->getStatus() > 299 OR (int) $response->getStatus() < 200)
{
if ($logger)
{
$logger->warning('Non 200 response for api call', (array)$response->getBody());
}
}
return XML::toArray(wait($response->getBody()));
}
/**
* Remove some boilerplate for post requests
*
* @param mixed ...$args
* @return array
*/
protected function postRequest(...$args): array
{
$logger = NULL;
if ($this->getContainer())
{
$logger = $this->container->getLogger('anilist-request');
}
$response = $this->getResponse('POST', ...$args);
$validResponseCodes = [200, 201];
if ( ! \in_array((int) $response->getStatus(), $validResponseCodes, TRUE))
{
if ($logger)
{
$logger->warning('Non 201 response for POST api call', (array)$response->getBody());
}
}
return XML::toArray($response->getBody());
}
}

View File

@ -0,0 +1,109 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Anilist;
use Amp\Artax\{FormBody, Request};
use Aviat\AnimeClient\API\{
XML
};
use Aviat\AnimeClient\Types\AbstractType;
use Aviat\Ion\Di\ContainerAware;
/**
* CRUD operations for MAL list items
*/
final class ListItem {
use ContainerAware;
use AnilistTrait;
/**
* Create a list item
*
* @param array $data
* @param string $type
* @return Request
*/
public function create(array $data, string $type = 'anime'): Request
{
$id = $data['id'];
$createData = [
'id' => $id,
'data' => XML::toXML([
'entry' => $data['data']
])
];
$config = $this->container->get('config');
return $this->requestBuilder->newRequest('POST', "{$type}list/add/{$id}.xml")
->setFormFields($createData)
->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password']))
->getFullRequest();
}
/**
* Delete a list item
*
* @param string $id
* @param string $type
* @return Request
*/
public function delete(string $id, string $type = 'anime'): Request
{
$config = $this->container->get('config');
return $this->requestBuilder->newRequest('DELETE', "{$type}list/delete/{$id}.xml")
->setFormFields([
'id' => $id
])
->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password']))
->getFullRequest();
// return $response->getBody() === 'Deleted'
}
public function get(string $id): array
{
return [];
}
/**
* Update a list item
*
* @param string $id
* @param AbstractType $data
* @param string $type
* @return Request
*/
public function update(string $id, AbstractType $data, string $type = 'anime'): Request
{
$config = $this->container->get('config');
$xml = XML::toXML(['entry' => $data]);
$body = new FormBody();
$body->addField('id', $id);
$body->addField('data', $xml);
return $this->requestBuilder->newRequest('POST', "{$type}list/update/{$id}.xml")
->setFormFields([
'id' => $id,
'data' => $xml
])
->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password']))
->getFullRequest();
}
}

23
src/API/Anilist/Model.php Normal file
View File

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Anilist;
/**
* Anilist API Model
*/
final class Model {
}

View File

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Enum\AnimeWatchingStatus;
use Aviat\Ion\Enum;
/**
* Possible values for watching status for the current anime
*/
final class Anilist extends Enum {
const WATCHING = 'CURRENT';
const COMPLETED = 'COMPLETED';
const ON_HOLD = 'PAUSED';
const DROPPED = 'DROPPED';
const PLAN_TO_WATCH = 'PLANNING';
const REPEATING = 'REPEATING';
}

View File

@ -21,7 +21,7 @@ use Aviat\Ion\Enum;
/**
* Possible values for watching status for the current anime
*/
class Kitsu extends Enum {
final class Kitsu extends Enum {
const WATCHING = 'current';
const PLAN_TO_WATCH = 'planned';
const ON_HOLD = 'on_hold';

View File

@ -21,7 +21,7 @@ use Aviat\Ion\Enum;
/**
* Possible values for watching status for the current anime
*/
class MAL extends Enum {
final class MAL extends Enum {
const WATCHING = 1;
const COMPLETED = 2;
const ON_HOLD = 3;

View File

@ -21,7 +21,7 @@ use Aviat\Ion\Enum as Enum;
/**
* Possible values for current watching status of anime
*/
class Route extends Enum {
final class Route extends Enum {
const ALL = 'all';
const WATCHING = 'watching';
const PLAN_TO_WATCH = 'plan_to_watch';

View File

@ -21,7 +21,7 @@ use Aviat\Ion\Enum as Enum;
/**
* Possible values for current watching status of anime
*/
class Title extends Enum {
final class Title extends Enum {
const ALL = 'All';
const WATCHING = 'Currently Watching';
const PLAN_TO_WATCH = 'Plan to Watch';

View File

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Enum\MangaReadingStatus;
use Aviat\Ion\Enum;
/**
* Possible values for watching status for the current anime
*/
final class Anilist extends Enum {
const WATCHING = 'CURRENT';
const COMPLETED = 'COMPLETED';
const ON_HOLD = 'PAUSED';
const DROPPED = 'DROPPED';
const PLAN_TO_WATCH = 'PLANNING';
const REPEATING = 'REPEATING';
}

View File

@ -21,7 +21,7 @@ use Aviat\Ion\Enum;
/**
* Possible values for current reading status of manga
*/
class Kitsu extends Enum {
final class Kitsu extends Enum {
const READING = 'current';
const PLAN_TO_READ = 'planned';
const DROPPED = 'dropped';

View File

@ -21,7 +21,7 @@ use Aviat\Ion\Enum;
/**
* Possible values for watching status for the current anime
*/
class MAL extends Enum {
final class MAL extends Enum {
const READING = 'reading';
const COMPLETED = 'completed';
const ON_HOLD = 'onhold';

View File

@ -16,12 +16,12 @@
namespace Aviat\AnimeClient\API\Enum\MangaReadingStatus;
use Aviat\Ion\Enum as Enum;
use Aviat\Ion\Enum;
/**
* Possible values for current reading status of manga
*/
class Route extends Enum {
final class Route extends Enum {
const ALL = 'all';
const READING = 'reading';
const PLAN_TO_READ = 'plan_to_read';

View File

@ -16,12 +16,12 @@
namespace Aviat\AnimeClient\API\Enum\MangaReadingStatus;
use Aviat\Ion\Enum as Enum;
use Aviat\Ion\Enum;
/**
* Possible values for current reading status of manga
*/
class Title extends Enum {
final class Title extends Enum {
const ALL = 'All';
const READING = 'Currently Reading';
const PLAN_TO_READ = 'Plan to Read';

View File

@ -19,7 +19,7 @@ namespace Aviat\AnimeClient\API;
/**
* Class encapsulating Json API data structure for a request or response
*/
class JsonAPI {
final class JsonAPI {
/**
* The full data array

View File

@ -22,7 +22,7 @@ use DateTimeImmutable;
/**
* Data massaging helpers for the Kitsu API
*/
class Kitsu {
final class Kitsu {
const AUTH_URL = 'https://kitsu.io/api/oauth/token';
const AUTH_USER_ID_KEY = 'kitsu-auth-userid';
const AUTH_TOKEN_CACHE_KEY = 'kitsu-auth-token';
@ -143,7 +143,10 @@ class Kitsu {
*/
public static function parseStreamingLinks(array $included): array
{
if ( ! array_key_exists('streamingLinks', $included))
if (
( ! array_key_exists('streamingLinks', $included)) ||
count($included['streamingLinks']) === 0
)
{
return [];
}
@ -152,7 +155,16 @@ class Kitsu {
foreach ($included['streamingLinks'] as $streamingLink)
{
$host = parse_url($streamingLink['url'], \PHP_URL_HOST);
$url = $streamingLink['url'];
// 'Fix' links that start with the hostname,
// rather than a protocol
if (strpos($url, '//') === FALSE)
{
$url = '//' . $url;
}
$host = parse_url($url, \PHP_URL_HOST);
$links[] = [
'meta' => static::getServiceMetaData($host),
@ -182,17 +194,7 @@ class Kitsu {
if (count($anime['relationships']['streamingLinks']) > 0)
{
foreach ($anime['relationships']['streamingLinks'] as $streamingLink)
{
$host = parse_url($streamingLink['url'], \PHP_URL_HOST);
$links[] = [
'meta' => static::getServiceMetaData($host),
'link' => $streamingLink['url'],
'subs' => $streamingLink['subs'],
'dubs' => $streamingLink['dubs']
];
}
return static::parseStreamingLinks($anime['relationships']);
}
return $links;
@ -243,10 +245,9 @@ class Kitsu {
foreach($existingTitles as $existing)
{
$isSubset = mb_substr_count($existing, $title) > 0;
$diff = levenshtein($existing, $title);
$onlydifferentCase = (mb_strtolower($existing) === mb_strtolower($title));
$diff = levenshtein(mb_strtolower($existing), mb_strtolower($title));
if ($diff <= 3 OR $isSubset OR $onlydifferentCase OR mb_strlen($title) > 55 OR mb_strlen($existing) > 60)
if ($diff <= 4 || $isSubset || mb_strlen($title) > 45 || mb_strlen($existing) > 50)
{
return FALSE;
}

View File

@ -28,7 +28,7 @@ use Exception;
/**
* Kitsu API Authentication
*/
class Auth {
final class Auth {
use CacheTrait;
use ContainerAware;
@ -37,14 +37,14 @@ class Auth {
*
* @var Model
*/
protected $model;
private $model;
/**
* Session object
*
* @var \Aura\Session\Segment
*/
protected $segment;
private $segment;
/**
* Constructor

View File

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

View File

@ -18,7 +18,7 @@ namespace Aviat\AnimeClient\API\Kitsu;
use Aviat\AnimeClient\API\APIRequestBuilder;
class KitsuRequestBuilder extends APIRequestBuilder {
final class KitsuRequestBuilder extends APIRequestBuilder {
/**
* The base url for api requests

View File

@ -25,39 +25,17 @@ use Aviat\AnimeClient\API\{
HummingbirdClient,
ListItemInterface
};
use Aviat\AnimeClient\Types\FormItemData;
use Aviat\Ion\Di\ContainerAware;
use Aviat\Ion\Json;
/**
* CRUD operations for Kitsu list items
*/
class ListItem implements ListItemInterface {
final class ListItem implements ListItemInterface {
use ContainerAware;
use KitsuTrait;
private function getAuthHeader()
{
$cache = $this->getContainer()->get('cache');
$cacheItem = $cache->getItem('kitsu-auth-token');
$sessionSegment = $this->getContainer()
->get('session')
->getSegment(SESSION_SEGMENT);
if ($sessionSegment->get('auth_token') !== NULL)
{
$token = $sessionSegment->get('auth_token');
return "bearer {$token}";
}
if ($cacheItem->isHit())
{
$token = $cacheItem->get();
return "bearer {$token}";
}
return FALSE;
}
public function create(array $data): Request
{
$body = [
@ -134,7 +112,7 @@ class ListItem implements ListItemInterface {
return Json::decode(wait($response->getBody()));
}
public function update(string $id, array $data): Request
public function update(string $id, FormItemData $data): Request
{
$authHeader = $this->getAuthHeader();
$requestData = [
@ -155,4 +133,25 @@ class ListItem implements ListItemInterface {
return $request->getFullRequest();
}
private function getAuthHeader()
{
$cache = $this->getContainer()->get('cache');
$cacheItem = $cache->getItem('kitsu-auth-token');
$sessionSegment = $this->getContainer()
->get('session')
->getSegment(SESSION_SEGMENT);
if ($sessionSegment->get('auth_token') !== NULL) {
$token = $sessionSegment->get('auth_token');
return "bearer {$token}";
}
if ($cacheItem->isHit()) {
$token = $cacheItem->get();
return "bearer {$token}";
}
return FALSE;
}
}

View File

@ -26,7 +26,6 @@ use Aviat\AnimeClient\API\{
ParallelAPIRequest
};
use Aviat\AnimeClient\API\Enum\{
AnimeWatchingStatus\Title,
AnimeWatchingStatus\Kitsu as KitsuWatchingStatus,
MangaReadingStatus\Kitsu as KitsuReadingStatus
};
@ -37,17 +36,25 @@ use Aviat\AnimeClient\API\Kitsu\Transformer\{
MangaTransformer,
MangaListTransformer
};
use Aviat\AnimeClient\Types\{
AbstractType,
Anime,
FormItem,
FormItemData,
AnimeListItem,
MangaPage
};
use Aviat\Ion\{Di\ContainerAware, Json};
/**
* Kitsu API Model
*/
class Model {
final class Model {
use CacheTrait;
use ContainerAware;
use KitsuTrait;
const LIST_PAGE_SIZE = 100;
private const LIST_PAGE_SIZE = 100;
/**
* Class to map anime list items
@ -56,27 +63,27 @@ class Model {
*
* @var AnimeListTransformer
*/
protected $animeListTransformer;
private $animeListTransformer;
/**
* @var AnimeTransformer
*/
protected $animeTransformer;
private $animeTransformer;
/**
* @var ListItem
*/
protected $listItem;
private $listItem;
/**
* @var MangaTransformer
*/
protected $mangaTransformer;
private $mangaTransformer;
/**
* @var MangaListTransformer
*/
protected $mangaListTransformer;
private $mangaListTransformer;
/**
* Constructor
@ -314,15 +321,15 @@ class Model {
* Get information about a particular anime
*
* @param string $slug
* @return array
* @return Anime
*/
public function getAnime(string $slug): array
public function getAnime(string $slug): Anime
{
$baseData = $this->getRawMediaData('anime', $slug);
if (empty($baseData))
{
return [];
return new Anime();
}
$transformed = $this->animeTransformer->transform($baseData);
@ -417,7 +424,7 @@ class Model {
* @param array $options
* @return array
*/
public function getFullAnimeList(array $options = [
public function getFullRawAnimeList(array $options = [
'include' => 'anime.mappings'
]): array
{
@ -441,10 +448,10 @@ class Model {
foreach($responses as $response)
{
$data = Json::decode($response);
$output = array_merge_recursive($output, $data);
$output[] = $data;
}
return $output;
return array_merge_recursive(...$output);
}
/**
@ -547,7 +554,7 @@ class Model {
'sort' => '-updated_at'
];
return $this->getFullAnimeList($options);
return $this->getFullRawAnimeList($options);
}
// -------------------------------------------------------------------------
@ -558,15 +565,15 @@ class Model {
* Get information about a particular manga
*
* @param string $slug
* @return array
* @return MangaPage
*/
public function getManga(string $slug): array
public function getManga(string $slug): MangaPage
{
$baseData = $this->getRawMediaData('manga', $slug);
if (empty($baseData))
{
return [];
return new MangaPage([]);
}
$transformed = $this->mangaTransformer->transform($baseData);
@ -580,7 +587,7 @@ class Model {
* @param string $mangaId
* @return array
*/
public function getMangaById(string $mangaId): array
public function getMangaById(string $mangaId): MangaPage
{
$baseData = $this->getRawMediaDataById('manga', $mangaId);
return $this->mangaTransformer->transform($baseData);
@ -680,7 +687,7 @@ class Model {
* @param array $options
* @return array
*/
public function getFullMangaList(array $options = [
public function getFullRawMangaList(array $options = [
'include' => 'manga.mappings'
]): array
{
@ -704,10 +711,10 @@ class Model {
foreach($responses as $response)
{
$data = Json::decode($response);
$output = array_merge_recursive($output, $data);
$output[] = $data;
}
return $output;
return array_merge_recursive(...$output);
}
/**
@ -804,9 +811,9 @@ class Model {
* Get the data for a specific list item, generally for editing
*
* @param string $listId - The unique identifier of that list item
* @return array
* @return mixed
*/
public function getListItem(string $listId): array
public function getListItem(string $listId)
{
$baseData = $this->listItem->get($listId);
$included = JsonAPI::organizeIncludes($baseData['included']);
@ -814,12 +821,12 @@ class Model {
switch (TRUE)
{
case in_array('anime', array_keys($included)):
case array_key_exists('anime', $included): // in_array('anime', array_keys($included)):
$included = JsonAPI::inlineIncludedRelationships($included, 'anime');
$baseData['data']['included'] = $included;
return $this->animeListTransformer->transform($baseData['data']);
case in_array('manga', array_keys($included)):
case array_key_exists('manga', $included): // in_array('manga', array_keys($included)):
$included = JsonAPI::inlineIncludedRelationships($included, 'manga');
$baseData['data']['included'] = $included;
$baseData['data']['manga'] = $baseData['included'][0];
@ -833,10 +840,10 @@ class Model {
/**
* Modify a list item
*
* @param array $data
* @param FormItem $data
* @return Request
*/
public function updateListItem(array $data): Request
public function updateListItem(FormItem $data): Request
{
return $this->listItem->update($data['id'], $data['data']);
}

View File

@ -17,21 +17,27 @@
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\Kitsu;
use Aviat\AnimeClient\Types\{
Anime,
AnimeFormItem,
AnimeFormItemData,
AnimeListItem
};
use Aviat\Ion\Transformer\AbstractTransformer;
/**
* Transformer for anime list
*/
class AnimeListTransformer extends AbstractTransformer {
final class AnimeListTransformer extends AbstractTransformer {
/**
* Convert raw api response to a more
* logical and workable structure
*
* @param array $item API library item
* @return array
* @return AnimeListItem
*/
public function transform($item): array
public function transform($item): AnimeListItem
{
$included = $item['included'];
$animeId = $item['relationships']['media']['data']['id'];
@ -66,7 +72,10 @@ class AnimeListTransformer extends AbstractTransformer {
? Kitsu::parseListItemStreamingLinks($included, $animeId)
: [];
return [
$titles = Kitsu::filterTitles($anime);
$title = array_shift($titles);
return new AnimeListItem([
'id' => $item['id'],
'mal_id' => $MALid,
'episodes' => [
@ -81,24 +90,24 @@ class AnimeListTransformer extends AbstractTransformer {
'started' => $anime['startDate'],
'ended' => $anime['endDate']
],
'anime' => [
'anime' => new Anime([
'id' => $animeId,
'age_rating' => $anime['ageRating'],
'title' => $anime['canonicalTitle'],
'titles' => Kitsu::filterTitles($anime),
'title' => $title,
'titles' => $titles,
'slug' => $anime['slug'],
'type' => $this->string($anime['showType'])->upperCaseFirst()->__toString(),
'image' => $anime['posterImage']['small'],
'show_type' => $this->string($anime['showType'])->upperCaseFirst()->__toString(),
'cover_image' => $anime['posterImage']['small'],
'genres' => $genres,
'streaming_links' => $streamingLinks,
],
]),
'watching_status' => $item['attributes']['status'],
'notes' => $item['attributes']['notes'],
'rewatching' => (bool) $item['attributes']['reconsuming'],
'rewatched' => (int) $item['attributes']['reconsumeCount'],
'user_rating' => $rating,
'private' => $item['attributes']['private'] ?? FALSE,
];
]);
}
/**
@ -106,14 +115,14 @@ class AnimeListTransformer extends AbstractTransformer {
* api response format
*
* @param array $item Transformed library item
* @return array API library item
* @return AnimeFormItem API library item
*/
public function untransform($item): array
public function untransform($item): AnimeFormItem
{
$privacy = (array_key_exists('private', $item) && $item['private']);
$rewatching = (array_key_exists('rewatching', $item) && $item['rewatching']);
$untransformed = [
$untransformed = new AnimeFormItem([
'id' => $item['id'],
'mal_id' => $item['mal_id'] ?? NULL,
'data' => [
@ -123,7 +132,7 @@ class AnimeListTransformer extends AbstractTransformer {
'notes' => $item['notes'],
'private' => $privacy
]
];
]);
if (is_numeric($item['episodes_watched']) && $item['episodes_watched'] > 0)
{

View File

@ -17,31 +17,32 @@
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\{JsonAPI, Kitsu};
use Aviat\AnimeClient\Types\Anime;
use Aviat\Ion\Transformer\AbstractTransformer;
/**
* Transformer for anime description page
*/
class AnimeTransformer extends AbstractTransformer {
final class AnimeTransformer extends AbstractTransformer {
/**
* Convert raw api response to a more
* logical and workable structure
*
* @param array $item API library item
* @return array
* @return Anime
*/
public function transform($item): array
public function transform($item): Anime
{
$item['included'] = JsonAPI::organizeIncludes($item['included']);
$genres = $item['included']['categories'] ?? [];
$item['genres'] = array_column($genres, 'title') ?? [];
sort($item['genres']);
$titles = Kitsu::filterTitles($item);
$title = $item['canonicalTitle'];
$titles = array_diff($item['titles'], [$title]);
return [
return new Anime([
'age_rating' => $item['ageRating'],
'age_rating_guide' => $item['ageRatingGuide'],
'cover_image' => $item['posterImage']['small'],
@ -54,10 +55,10 @@ class AnimeTransformer extends AbstractTransformer {
'status' => Kitsu::getAiringStatus($item['startDate'], $item['endDate']),
'streaming_links' => Kitsu::parseStreamingLinks($item['included']),
'synopsis' => $item['synopsis'],
'title' => $titles[0],
'title' => $title,
'titles' => $titles,
'trailer_id' => $item['youtubeVideoId'],
'url' => "https://kitsu.io/anime/{$item['slug']}",
];
]);
}
}

View File

@ -17,13 +17,17 @@
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\Kitsu;
use Aviat\AnimeClient\Types\{
MangaFormItem, MangaFormItemData,
MangaListItem, MangaListItemDetail
};
use Aviat\Ion\StringWrapper;
use Aviat\Ion\Transformer\AbstractTransformer;
/**
* Data transformation class for zippered Hummingbird manga
*/
class MangaListTransformer extends AbstractTransformer {
final class MangaListTransformer extends AbstractTransformer {
use StringWrapper;
@ -31,9 +35,9 @@ class MangaListTransformer extends AbstractTransformer {
* Remap zipped anime data to a more logical form
*
* @param array $item manga entry item
* @return array
* @return MangaListItem
*/
public function transform($item): array
public function transform($item): MangaListItem
{
$included = $item['included'];
$mangaId = $item['relationships']['media']['data']['id'];
@ -72,7 +76,10 @@ class MangaListTransformer extends AbstractTransformer {
}
}
$map = [
$titles = Kitsu::filterTitles($manga);
$title = array_shift($titles);
$map = new MangaListItem([
'id' => $item['id'],
'mal_id' => $MALid,
'chapters' => [
@ -83,22 +90,22 @@ class MangaListTransformer extends AbstractTransformer {
'read' => '-', //$item['attributes']['volumes_read'],
'total' => $totalVolumes
],
'manga' => [
'id' => $mangaId,
'titles' => Kitsu::filterTitles($manga),
'alternate_title' => NULL,
'slug' => $manga['slug'],
'url' => 'https://kitsu.io/manga/' . $manga['slug'],
'type' => $manga['mangaType'],
'image' => $manga['posterImage']['small'],
'manga' => new MangaListItemDetail([
'genres' => $genres,
],
'id' => $mangaId,
'image' => $manga['posterImage']['small'],
'slug' => $manga['slug'],
'title' => $title,
'titles' => $titles,
'type' => $manga['mangaType'],
'url' => 'https://kitsu.io/manga/' . $manga['slug'],
]),
'reading_status' => $item['attributes']['status'],
'notes' => $item['attributes']['notes'],
'rereading' => (bool)$item['attributes']['reconsuming'],
'reread' => $item['attributes']['reconsumeCount'],
'user_rating' => $rating,
];
]);
return $map;
}
@ -107,22 +114,22 @@ class MangaListTransformer extends AbstractTransformer {
* Untransform data to update the api
*
* @param array $item
* @return array
* @return MangaFormItem
*/
public function untransform($item): array
public function untransform($item): MangaFormItem
{
$rereading = array_key_exists('rereading', $item) && (bool)$item['rereading'];
$map = [
$map = new MangaFormItem([
'id' => $item['id'],
'mal_id' => $item['mal_id'],
'data' => [
'data' => new MangaFormItemData([
'status' => $item['status'],
'reconsuming' => $rereading,
'reconsumeCount' => (int)$item['reread_count'],
'notes' => $item['notes'],
],
];
]),
]);
if (is_numeric($item['chapters_read']) && $item['chapters_read'] > 0)
{

View File

@ -16,35 +16,37 @@
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\Types\MangaPage;
use Aviat\Ion\Transformer\AbstractTransformer;
/**
* Transformer for anime description page
*/
class MangaTransformer extends AbstractTransformer {
final class MangaTransformer extends AbstractTransformer {
/**
* Convert raw api response to a more
* logical and workable structure
*
* @param array $item API library item
* @return array
* @return MangaPage
*/
public function transform($item)
public function transform($item): MangaPage
{
// \dump($item);
$genres = [];
foreach($item['included'] as $included)
{
if ($included['type'] === 'genres')
if ($included['type'] === 'categories')
{
$genres[] = $included['attributes']['name'];
$genres[] = $included['attributes']['title'];
}
}
sort($genres);
return [
return new MangaPage([
'id' => $item['id'],
'title' => $item['canonicalTitle'],
'en_title' => $item['titles']['en'],
@ -56,7 +58,7 @@ class MangaTransformer extends AbstractTransformer {
'synopsis' => $item['synopsis'],
'url' => "https://kitsu.io/manga/{$item['slug']}",
'genres' => $genres,
];
]);
}
private function count(int $value = NULL)

View File

@ -17,6 +17,7 @@
namespace Aviat\AnimeClient\API;
use Amp\Artax\Request;
use Aviat\AnimeClient\Types\FormItemData;
/**
* Common interface for anime and manga list item CRUD
@ -43,10 +44,10 @@ interface ListItemInterface {
* Update a list item
*
* @param string $id - The id of the list item to update
* @param array $data - The data with which to update the list item
* @param FormItemData $data - The data with which to update the list item
* @return Request
*/
public function update(string $id, array $data): Request;
public function update(string $id, FormItemData $data): Request;
/**
* Delete a list item

View File

@ -28,7 +28,7 @@ use Aviat\AnimeClient\API\Enum\{
/**
* Constants and mappings for the My Anime List API
*/
class MAL {
final class MAL {
const AUTH_URL = 'https://myanimelist.net/api/account/verify_credentials.xml';
const BASE_URL = 'https://myanimelist.net/api/';

View File

@ -20,12 +20,13 @@ use Amp\Artax\{FormBody, Request};
use Aviat\AnimeClient\API\{
XML
};
use Aviat\AnimeClient\Types\AbstractType;
use Aviat\Ion\Di\ContainerAware;
/**
* CRUD operations for MAL list items
*/
class ListItem {
final class ListItem {
use ContainerAware;
use MALTrait;
@ -84,11 +85,11 @@ class ListItem {
* Update a list item
*
* @param string $id
* @param array $data
* @param AbstractType $data
* @param string $type
* @return Request
*/
public function update(string $id, array $data, string $type = 'anime'): Request
public function update(string $id, AbstractType $data, string $type = 'anime'): Request
{
$config = $this->container->get('config');

View File

@ -16,12 +16,14 @@
namespace Aviat\AnimeClient\API\MAL;
use const Aviat\AnimeClient\USER_AGENT;
use Aviat\AnimeClient\API\{
APIRequestBuilder,
MAL as M
};
class MALRequestBuilder extends APIRequestBuilder {
final class MALRequestBuilder extends APIRequestBuilder {
/**
* The base url for api requests
@ -38,7 +40,7 @@ class MALRequestBuilder extends APIRequestBuilder {
'Accept' => 'text/xml',
'Accept-Encoding' => 'gzip',
'Content-type' => 'application/x-www-form-urlencoded',
'User-Agent' => "Tim's Anime Client/4.0"
'User-Agent' => USER_AGENT,
];
/**

View File

@ -24,12 +24,13 @@ use Aviat\AnimeClient\API\MAL\{
};
use Aviat\AnimeClient\API\XML;
use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus};
use Aviat\AnimeClient\Types\{Anime, FormItem};
use Aviat\Ion\Di\ContainerAware;
/**
* MyAnimeList API Model
*/
class Model {
final class Model {
use ContainerAware;
use MALTrait;
@ -147,11 +148,11 @@ class Model {
/**
* Update a list item
*
* @param array $data
* @param FormItem $data
* @param string $type "anime" or "manga"
* @return Request
*/
public function updateListItem(array $data, string $type = 'anime'): Request
public function updateListItem(FormItem $data, string $type = 'anime'): Request
{
$updateData = [];

View File

@ -17,12 +17,13 @@
namespace Aviat\AnimeClient\API\MAL\Transformer;
use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
use Aviat\AnimeClient\Types\{AnimeFormItem, AnimeFormItemData};
use Aviat\Ion\Transformer\AbstractTransformer;
/**
* Transformer for updating MAL List
*/
class AnimeListTransformer extends AbstractTransformer {
final class AnimeListTransformer extends AbstractTransformer {
/**
* Identity transformation
*
@ -38,16 +39,14 @@ class AnimeListTransformer extends AbstractTransformer {
* Transform Kitsu episode data to MAL episode data
*
* @param array $item
* @return array
* @return AnimeFormItem
*/
public function untransform(array $item): array
public function untransform(array $item): AnimeFormItem
{
$map = [
$map = new AnimeFormItem([
'id' => $item['mal_id'],
'data' => []
];
$data =& $item['data'];
'data' => new AnimeFormItemData([]),
]);
foreach($item['data'] as $key => $value)
{

View File

@ -22,7 +22,7 @@ use Aviat\Ion\Transformer\AbstractTransformer;
/**
* Transformer for updating MAL List
*/
class MangaListTransformer extends AbstractTransformer {
final class MangaListTransformer extends AbstractTransformer {
/**
* Identity transformation
*

View File

@ -16,14 +16,30 @@
namespace Aviat\AnimeClient\API\Mapping;
use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\{Kitsu, MAL, Route, Title};
use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\{Anilist, Kitsu, MAL, Route, Title};
use Aviat\Ion\Enum;
/**
* Anime watching status mappings, among Kitsu, MAL, Page titles
* and url route segments
*/
class AnimeWatchingStatus extends Enum {
final class AnimeWatchingStatus extends Enum {
const ANILIST_TO_KITSU = [
Anilist::WATCHING => Kitsu::WATCHING,
Anilist::PLAN_TO_WATCH => Kitsu::PLAN_TO_WATCH,
Anilist::COMPLETED => Kitsu::COMPLETED,
Anilist::ON_HOLD => Kitsu::ON_HOLD,
Anilist::DROPPED => Kitsu::DROPPED
];
const KITSU_TO_ANILIST = [
Kitsu::WATCHING => Anilist::WATCHING,
Kitsu::PLAN_TO_WATCH => Anilist::PLAN_TO_WATCH,
Kitsu::COMPLETED => Anilist::COMPLETED,
Kitsu::ON_HOLD => Anilist::ON_HOLD,
Kitsu::DROPPED => Anilist::DROPPED
];
const KITSU_TO_MAL = [
Kitsu::WATCHING => MAL::WATCHING,
Kitsu::PLAN_TO_WATCH => MAL::PLAN_TO_WATCH,

View File

@ -16,14 +16,31 @@
namespace Aviat\AnimeClient\API\Mapping;
use Aviat\AnimeClient\API\Enum\MangaReadingStatus\{Kitsu, MAL, Title, Route};
use Aviat\AnimeClient\API\Enum\MangaReadingStatus\{Anilist, Kitsu, MAL, Title, Route};
use Aviat\Ion\Enum;
/**
* Manga reading status mappings, among Kitsu, MAL, Page titles
* and url route segments
*/
class MangaReadingStatus extends Enum {
final class MangaReadingStatus extends Enum {
const ANILIST_TO_KITSU = [
Anilist::READING => Kitsu::READING,
Anilist::PLAN_TO_READ => Kitsu::PLAN_TO_READ,
Anilist::COMPLETED => Kitsu::COMPLETED,
Anilist::ON_HOLD => Kitsu::ON_HOLD,
Anilist::DROPPED => Kitsu::DROPPED
];
const KITSU_TO_ANILIST = [
Kitsu::READING => Anilist::READING,
Kitsu::PLAN_TO_READ => Anilist::PLAN_TO_READ,
Kitsu::COMPLETED => Anilist::COMPLETED,
Kitsu::ON_HOLD => Anilist::ON_HOLD,
Kitsu::DROPPED => Anilist::DROPPED
];
const KITSU_TO_MAL = [
Kitsu::READING => MAL::READING,
Kitsu::PLAN_TO_READ => MAL::PLAN_TO_READ,

View File

@ -22,14 +22,14 @@ use function Amp\Promise\{all, wait};
/**
* Class to simplify making and validating simultaneous requests
*/
class ParallelAPIRequest {
final class ParallelAPIRequest {
/**
* Set of requests to make in parallel
*
* @var array
*/
protected $requests = [];
private $requests = [];
/**
* Add a request
@ -76,9 +76,7 @@ class ParallelAPIRequest {
{
$promises[$key] = call(function () use ($client, $url) {
$response = yield $client->request($url);
$body = yield $response->getBody();
return $body;
return yield $response->getBody();
});
}

View File

@ -21,7 +21,7 @@ use DOMDocument, DOMNode, DOMNodeList, InvalidArgumentException;
/**
* XML <=> PHP Array codec
*/
class XML {
final class XML {
/**
* XML representation of the data

View File

@ -19,7 +19,7 @@ namespace Aviat\AnimeClient\Command;
/**
* Clears the API Cache
*/
class CacheClear extends BaseCommand {
final class CacheClear extends BaseCommand {
/**
* Clear the API cache
*

View File

@ -19,7 +19,7 @@ namespace Aviat\AnimeClient\Command;
/**
* Clears the API Cache
*/
class CachePrime extends BaseCommand {
final class CachePrime extends BaseCommand {
/**
* Clear, then prime the API cache
*

View File

@ -32,7 +32,7 @@ use DateTime;
/**
* Clears the API Cache
*/
class SyncLists extends BaseCommand {
final class SyncLists extends BaseCommand {
/**
* Model for making requests to Kitsu API

View File

@ -20,6 +20,7 @@ use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\API\Kitsu\Transformer\AnimeListTransformer;
use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\Kitsu as KitsuWatchingStatus;
use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
use Aviat\AnimeClient\Types\AnimeFormItem;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Json;
use Aviat\Ion\StringWrapper;
@ -27,7 +28,7 @@ use Aviat\Ion\StringWrapper;
/**
* Controller for Anime-related pages
*/
class Anime extends BaseController {
final class Anime extends BaseController {
use StringWrapper;
@ -201,7 +202,7 @@ class Anime extends BaseController {
// large form-based updates
$transformer = new AnimeListTransformer();
$postData = $transformer->untransform($data);
$fullResult = $this->model->updateLibraryItem($postData);
$fullResult = $this->model->updateLibraryItem(new AnimeFormItem($postData));
if ($fullResult['statusCode'] === 200)
{
@ -232,7 +233,7 @@ class Anime extends BaseController {
$data = $this->request->getParsedBody();
}
$response = $this->model->updateLibraryItem($data);
$response = $this->model->updateLibraryItem(new AnimeFormItem($data));
$this->cache->clear();
$this->outputJSON($response['body'], $response['statusCode']);
@ -277,7 +278,7 @@ class Anime extends BaseController {
$show_data = $this->model->getAnime($animeId);
$characters = [];
if (empty($show_data))
if ($show_data->title === '')
{
$this->notFound(
$this->config->get('whose_list') .
@ -301,7 +302,7 @@ class Anime extends BaseController {
'title' => $this->formatTitle(
$this->config->get('whose_list') . "'s Anime List",
'Anime',
$show_data['titles'][0]
$show_data->title
),
'characters' => $characters,
'show_data' => $show_data,

View File

@ -26,7 +26,7 @@ use Aviat\Ion\Di\ContainerInterface;
/**
* Controller for Anime collection pages
*/
class AnimeCollection extends BaseController {
final class AnimeCollection extends BaseController {
/**
* The anime collection model

View File

@ -23,7 +23,7 @@ use Aviat\Ion\ArrayWrapper;
/**
* Controller for character description pages
*/
class Character extends BaseController {
final class Character extends BaseController {
use ArrayWrapper;

View File

@ -25,7 +25,7 @@ use Aviat\Ion\View\HtmlView;
/**
* Controller for handling routes that don't fit elsewhere
*/
class Index extends BaseController {
final class Index extends BaseController {
/**
* Purges the API cache
@ -72,6 +72,24 @@ class Index extends BaseController {
], $view);
}
/**
* Redirect to Anilist to start Oauth flow
*/
public function anilistRedirect()
{
}
/**
* Oauth callback for Anilist API
*/
public function anilistCallback()
{
$this->outputHTML('blank', [
'title' => 'Oauth!'
]);
}
/**
* Attempt login authentication
*

View File

@ -20,13 +20,14 @@ use Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\API\Kitsu\Transformer\MangaListTransformer;
use Aviat\AnimeClient\API\Mapping\MangaReadingStatus;
use Aviat\AnimeClient\Model\Manga as MangaModel;
use Aviat\AnimeClient\Types\MangaFormItem;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\{Json, StringWrapper};
/**
* Controller for manga list
*/
class Manga extends Controller {
final class Manga extends Controller {
use StringWrapper;
@ -200,7 +201,7 @@ class Manga extends Controller {
// large form-based updates
$transformer = new MangaListTransformer();
$post_data = $transformer->untransform($data);
$full_result = $this->model->updateLibraryItem($post_data);
$full_result = $this->model->updateLibraryItem(new MangaFormItem($post_data));
if ($full_result['statusCode'] === 200)
{
@ -234,7 +235,7 @@ class Manga extends Controller {
$data = $this->request->getParsedBody();
}
$response = $this->model->updateLibraryItem($data);
$response = $this->model->updateLibraryItem(new MangaFormItem($data));
$this->cache->clear();
$this->outputJSON($response['body'], $response['statusCode']);

View File

@ -26,7 +26,7 @@ use Aviat\Ion\Di\ContainerInterface;
/**
* Controller for manga collection pages
*/
class MangaCollection extends BaseController {
final class MangaCollection extends BaseController {
/**
* The manga collection model

View File

@ -28,7 +28,7 @@ use Aviat\Ion\StringWrapper;
/**
* Basic routing/ dispatch
*/
class Dispatcher extends RoutingBase {
final class Dispatcher extends RoutingBase {
use StringWrapper;

View File

@ -17,13 +17,14 @@
namespace Aviat\AnimeClient\Helper;
use Aviat\AnimeClient\MenuGenerator;
use Aviat\Ion\Di\ContainerAware;
/**
* MenuGenerator helper wrapper
*/
class Menu {
final class Menu {
use \Aviat\Ion\Di\ContainerAware;
use ContainerAware;
/**
* Create the html for the selected menu

View File

@ -25,7 +25,7 @@ use Aviat\Ion\Exception\ConfigException;
/**
* Helper object to manage menu creation and selection
*/
class MenuGenerator extends UrlGenerator {
final class MenuGenerator extends UrlGenerator {
use ArrayWrapper;
use StringWrapper;

View File

@ -44,7 +44,7 @@ class API {
foreach ($array as $key => $item)
{
$sort[$key] = $item[$sortKey]['titles'][0];
$sort[$key] = $item[$sortKey]['title'];
}
array_multisort($sort, SORT_ASC, $array);

View File

@ -18,6 +18,11 @@ namespace Aviat\AnimeClient\Model;
use Aviat\AnimeClient\API\ParallelAPIRequest;
use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
use Aviat\AnimeClient\Types\{
Anime as AnimeType,
AnimeFormItem,
AnimeListItem
};
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Json;
@ -93,9 +98,9 @@ class Anime extends API {
* Get information about an anime from its slug
*
* @param string $slug
* @return array
* @return AnimeType
*/
public function getAnime(string $slug): array
public function getAnime(string $slug): AnimeType
{
return $this->kitsuModel->getAnime($slug);
}
@ -127,9 +132,9 @@ class Anime extends API {
* for editing/updating that item
*
* @param string $itemId
* @return array
* @return AnimeListItem
*/
public function getLibraryItem(string $itemId): array
public function getLibraryItem(string $itemId): AnimeListItem
{
return $this->kitsuModel->getListItem($itemId);
}
@ -166,10 +171,10 @@ class Anime extends API {
/**
* Update a list entry
*
* @param array $data
* @param AnimeFormItem $data
* @return array
*/
public function updateLibraryItem(array $data): array
public function updateLibraryItem(AnimeFormItem $data): array
{
$requester = new ParallelAPIRequest();

View File

@ -22,7 +22,7 @@ use PDO;
/**
* Model for getting anime collection data
*/
class AnimeCollection extends Collection {
final class AnimeCollection extends Collection {
/**
* Anime API Model

View File

@ -21,14 +21,18 @@ use Aviat\AnimeClient\API\{
Mapping\MangaReadingStatus,
ParallelAPIRequest
};
use Aviat\AnimeClient\Types\{
MangaFormItem,
MangaListItem,
MangaPage
};
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Json;
/**
* Model for handling requests dealing with the manga list
*/
class Manga extends API
{
class Manga extends API {
/**
* Model for making requests to Kitsu API
* @var \Aviat\AnimeClient\API\Kitsu\Model
@ -67,21 +71,28 @@ class Manga extends API
{
if ($status === 'All')
{
return $this->kitsuModel->getFullOrganizedMangaList();
$data = $this->kitsuModel->getFullOrganizedMangaList();
foreach($data as &$section)
{
$this->sortByName($section, 'manga');
}
return $data;
}
$APIstatus = MangaReadingStatus::TITLE_TO_KITSU[$status];
$data = $this->kitsuModel->getMangaList($APIstatus);
return $this->mapByStatus($data)[$status];
$data = $this->mapByStatus($this->kitsuModel->getMangaList($APIstatus));
$this->sortByName($data[$status], 'manga');
return $data[$status];
}
/**
* Get the details of a manga
*
* @param string $manga_id
* @return array
* @return MangaPage
*/
public function getManga($manga_id): array
public function getManga($manga_id): MangaPage
{
return $this->kitsuModel->getManga($manga_id);
}
@ -90,9 +101,9 @@ class Manga extends API
* Get anime by its kitsu id
*
* @param string $animeId
* @return array
* @return MangaPage
*/
public function getMangaById(string $animeId): array
public function getMangaById(string $animeId): MangaPage
{
return $this->kitsuModel->getMangaById($animeId);
}
@ -102,9 +113,9 @@ class Manga extends API
* for editing/updating that item
*
* @param string $itemId
* @return array
* @return MangaListItem
*/
public function getLibraryItem(string $itemId): array
public function getLibraryItem(string $itemId): MangaListItem
{
return $this->kitsuModel->getListItem($itemId);
}
@ -141,10 +152,10 @@ class Manga extends API
/**
* Update a list entry
*
* @param array $data
* @param MangaFormItem $data
* @return array
*/
public function updateLibraryItem(array $data): array
public function updateLibraryItem(MangaFormItem $data): array
{
$requester = new ParallelAPIRequest();
@ -221,9 +232,7 @@ class Manga extends API
$output[$key][] = $entry;
}
foreach ($output as &$val) {
$this->sortByName($val, 'manga');
}
unset($entry);
return $output;
}

View File

@ -22,7 +22,7 @@ use PDO;
/**
* Model for getting anime collection data
*/
class MangaCollection extends Collection {
final class MangaCollection extends Collection {
/**
* Manga API Model

157
src/Types/AbstractType.php Normal file
View File

@ -0,0 +1,157 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
use ArrayAccess;
use LogicException;
abstract class AbstractType implements ArrayAccess {
/**
* Populate values for unserializing data
*
* @param $properties
* @return mixed
*/
public static function __set_state($properties)
{
return new static($properties);
}
/**
* Sets the properties by using the constructor
*
* @param mixed $data
*/
public function __construct($data = [])
{
$typeKeys = array_keys((array)$this);
$dataKeys = array_keys((array)$data);
$unsetKeys = array_diff($typeKeys, $dataKeys);
foreach ($data as $key => $value)
{
$this->__set($key, $value);
}
// Remove unset keys so that they aren't serialized
foreach ($unsetKeys as $k)
{
unset($this->$k);
}
}
/**
* See if a property is set
*
* @param $name
* @return bool
*/
public function __isset($name): bool
{
return property_exists($this, $name);
}
/**
* Set a property on the type object
*
* @param string $name
* @param mixed $value
* @return void
*/
public function __set($name, $value): void
{
$setterMethod = 'set' . ucfirst($name);
if (method_exists($this, $setterMethod))
{
$this->$setterMethod($value);
return;
}
if (!property_exists($this, $name))
{
$existing = json_encode($this);
throw new LogicException("Trying to set non-existent property: '$name'. Existing properties: $existing");
}
$this->$name = $value;
}
/**
* Get a property from the type object
*
* @param string $name
* @return mixed
*/
public function __get($name)
{
if (property_exists($this, $name))
{
return $this->$name;
}
throw new LogicException("Trying to get non-existent property: '$name'");
}
/**
* Implementing ArrayAccess
*
* @param $offset
* @return bool
*/
public function offsetExists($offset): bool
{
return $this->__isset($offset);
}
/**
* Implementing ArrayAccess
*
* @param $offset
* @return mixed
*/
public function offsetGet($offset)
{
return $this->__get($offset);
}
/**
* Implementing ArrayAccess
*
* @param $offset
* @param $value
*/
public function offsetSet($offset, $value): void
{
$this->__set($offset, $value);
}
/**
* Implementing ArrayAccess
*
* @param $offset
*/
public function offsetUnset($offset): void
{
if ($this->offsetExists($offset))
{
unset($this->$offset);
}
}
}

40
src/Types/Anime.php Normal file
View File

@ -0,0 +1,40 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for display
*/
final class Anime extends AbstractType {
public $age_rating;
public $age_rating_guide;
public $cover_image;
public $episode_count;
public $episode_length;
public $genres;
public $id;
public $included;
public $show_type;
public $slug;
public $status;
public $streaming_links;
public $synopsis;
public $title;
public $titles;
public $trailer_id;
public $url;
}

View File

@ -0,0 +1,27 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for display
*/
final class AnimeFormItem extends FormItem {
public function setData($value): void
{
$this->data = new AnimeFormItemData($value);
}
}

View File

@ -0,0 +1,22 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for display
*/
final class AnimeFormItemData extends FormItemData {}

View File

@ -0,0 +1,42 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for display
*/
final class AnimeListItem extends AbstractType {
public $id;
public $mal_id;
public $episodes = [
'length' => 0,
'total' => 0,
'watched' => '',
];
public $airing = [
'status' => '',
'started' => '',
'ended' => '',
];
public $anime;
public $watching_status;
public $notes;
public $rewatching;
public $rewatched;
public $user_rating;
public $private;
}

29
src/Types/FormItem.php Normal file
View File

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for display
*/
abstract class FormItem extends AbstractType {
public $id;
public $mal_id;
public $data;
abstract public function setData($value): void;
}

View File

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for display
*/
abstract class FormItemData extends AbstractType {
public $notes;
public $private;
public $progress;
public $rating;
public $reconsumeCount;
public $reconsuming;
public $status;
}

View File

@ -0,0 +1,28 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Form data for updating a Manga List item
*/
final class MangaFormItem extends FormItem {
public function setData($value): void
{
$this->data = new MangaFormItemData($value);
}
}

View File

@ -0,0 +1,19 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
final class MangaFormItemData extends FormItemData {}

View File

@ -0,0 +1,40 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for display
*/
final class MangaListItem extends AbstractType {
public $id;
public $mal_id;
public $chapters = [
'read' => 0,
'total' => 0,
];
public $volumes = [
'read' => '-',
'total' => 0,
];
public $manga;
public $reading_status;
public $notes;
public $rereading;
public $reread;
public $user_rating;
}

View File

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing the manga represented by the list item
*/
final class MangaListItemDetail extends AbstractType {
public $genres;
public $id;
public $image;
public $slug;
public $title;
public $titles;
public $type;
public $url;
}

35
src/Types/MangaPage.php Normal file
View File

@ -0,0 +1,35 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @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.0
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
/**
* Type representing an Anime object for display
*/
final class MangaPage extends AbstractType {
public $chapter_count;
public $cover_image;
public $en_title;
public $genres;
public $id;
public $included;
public $jp_title;
public $manga_type;
public $synopsis;
public $title;
public $url;
public $volume_count;
}

Some files were not shown because too many files have changed in this diff Show More