Version 5.1 - All the GraphQL #32
@ -3,7 +3,10 @@
|
||||
## Version 4
|
||||
* Updated to use Kitsu API after discontinuation of Hummingbird
|
||||
* Added streaming links to list entries from the Kitsu API
|
||||
* Added simple integration with MyAnimeList, so an update can cross-post to both Kitsu and MyAnimeList
|
||||
* Added simple integration with MyAnimeList, so an update can cross-post to both Kitsu and MyAnimeList (anime and manga)
|
||||
* Added console command to sync Kitsu and MyAnimeList data
|
||||
|
||||
* Added character pages
|
||||
|
||||
## Version 3
|
||||
* Converted user configuration to toml files
|
||||
|
@ -14,6 +14,7 @@
|
||||
* @link https://github.com/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
use function Aviat\AnimeClient\loadToml;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Lower level configuration
|
||||
@ -23,7 +24,9 @@
|
||||
$APP_DIR = realpath(__DIR__ . '/../');
|
||||
$ROOT_DIR = realpath("{$APP_DIR}/../");
|
||||
|
||||
$base_config = [
|
||||
$tomlConfig = loadToml(__DIR__);
|
||||
|
||||
$base_config = array_merge($tomlConfig, [
|
||||
'asset_dir' => "{$ROOT_DIR}/public",
|
||||
|
||||
// Template file path
|
||||
@ -34,6 +37,5 @@ $base_config = [
|
||||
'img_cache_path' => "{$ROOT_DIR}/public/images",
|
||||
|
||||
// Included config files
|
||||
'menus' => require 'menus.php',
|
||||
'routes' => require 'routes.php',
|
||||
];
|
||||
]);
|
@ -1,41 +0,0 @@
|
||||
<?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 - 2017 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 4.0
|
||||
* @link https://github.com/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
|
||||
return [
|
||||
'anime_list' => [
|
||||
'route_prefix' => '/anime',
|
||||
'items' => [
|
||||
'watching' => '/watching',
|
||||
'plan_to_watch' => '/plan_to_watch',
|
||||
'on_hold' => '/on_hold',
|
||||
'dropped' => '/dropped',
|
||||
'completed' => '/completed',
|
||||
'all' => '/all'
|
||||
]
|
||||
],
|
||||
'manga_list' => [
|
||||
'route_prefix' => '/manga',
|
||||
'items' => [
|
||||
'reading' => '/reading',
|
||||
'plan_to_read' => '/plan_to_read',
|
||||
'on_hold' => '/on_hold',
|
||||
'dropped' => '/dropped',
|
||||
'completed' => '/completed',
|
||||
'all' => '/all'
|
||||
]
|
||||
]
|
||||
];
|
19
app/appConf/menus.toml
Normal file
19
app/appConf/menus.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[anime_list]
|
||||
route_prefix = "/anime"
|
||||
[anime_list.items]
|
||||
watching = '/watching'
|
||||
plan_to_watch = '/plan_to_watch'
|
||||
on_hold = '/on_hold'
|
||||
dropped = '/dropped'
|
||||
completed = '/completed'
|
||||
all = '/all'
|
||||
|
||||
[manga_list]
|
||||
route_prefix = "/manga"
|
||||
[manga_list.items]
|
||||
reading = '/reading'
|
||||
plan_to_read = '/plan_to_read'
|
||||
on_hold = '/on_hold'
|
||||
dropped = '/dropped'
|
||||
completed = '/completed'
|
||||
all = '/all'
|
19
app/appConf/route_config.toml
Normal file
19
app/appConf/route_config.toml
Normal file
@ -0,0 +1,19 @@
|
||||
################################################################################
|
||||
# Route config
|
||||
#
|
||||
# Default views and paths
|
||||
################################################################################
|
||||
|
||||
# Path to public directory, where images/css/javascript are located,
|
||||
# appended to the url
|
||||
asset_path = "/public"
|
||||
|
||||
# Which list should be the default?
|
||||
default_list = "anime" # anime or manga
|
||||
|
||||
# Default pages for anime/manga
|
||||
default_anime_list_path = "watching" # watching|plan_to_watch|on_hold|dropped|completed|all
|
||||
default_manga_list_path = "reading" # reading|plan_to_read|on_hold|dropped|completed|all
|
||||
|
||||
# Default view type (cover_view/list_view)
|
||||
default_view_type = "cover_view"
|
@ -17,38 +17,17 @@
|
||||
|
||||
use const Aviat\AnimeClient\{
|
||||
DEFAULT_CONTROLLER_METHOD,
|
||||
DEFAULT_CONTROLLER_NAMESPACE
|
||||
DEFAULT_CONTROLLER
|
||||
};
|
||||
|
||||
use Aviat\AnimeClient\AnimeClient;
|
||||
|
||||
return [
|
||||
// -------------------------------------------------------------------------
|
||||
// Routing options
|
||||
//
|
||||
// Specify default paths and views
|
||||
// -------------------------------------------------------------------------
|
||||
'route_config' => [
|
||||
// Path to public directory, where images/css/javascript are located,
|
||||
// appended to the url
|
||||
'asset_path' => '/public',
|
||||
|
||||
// Which list should be the default?
|
||||
'default_list' => 'anime', // anime or manga
|
||||
|
||||
// Default pages for anime/manga
|
||||
'default_anime_list_path' => "watching", // watching|plan_to_watch|on_hold|dropped|completed|all
|
||||
'default_manga_list_path' => "reading", // reading|plan_to_read|on_hold|dropped|completed|all
|
||||
|
||||
// Default view type (cover_view/list_view)
|
||||
'default_view_type' => 'cover_view',
|
||||
],
|
||||
// -------------------------------------------------------------------------
|
||||
// Routing Config
|
||||
//
|
||||
// Maps paths to controlers and methods
|
||||
// -------------------------------------------------------------------------
|
||||
'routes' => [
|
||||
return [
|
||||
// ---------------------------------------------------------------------
|
||||
// Anime List Routes
|
||||
// ---------------------------------------------------------------------
|
||||
@ -171,25 +150,25 @@ return [
|
||||
'cache_purge' => [
|
||||
'path' => '/cache_purge',
|
||||
'action' => 'clearCache',
|
||||
'controller' => DEFAULT_CONTROLLER_NAMESPACE,
|
||||
'controller' => DEFAULT_CONTROLLER,
|
||||
'verb' => 'get',
|
||||
],
|
||||
'login' => [
|
||||
'path' => '/login',
|
||||
'action' => 'login',
|
||||
'controller' => DEFAULT_CONTROLLER_NAMESPACE,
|
||||
'controller' => DEFAULT_CONTROLLER,
|
||||
'verb' => 'get',
|
||||
],
|
||||
'login.post' => [
|
||||
'path' => '/login',
|
||||
'action' => 'loginAction',
|
||||
'controller' => DEFAULT_CONTROLLER_NAMESPACE,
|
||||
'controller' => DEFAULT_CONTROLLER,
|
||||
'verb' => 'post',
|
||||
],
|
||||
'logout' => [
|
||||
'path' => '/logout',
|
||||
'action' => 'logout',
|
||||
'controller' => DEFAULT_CONTROLLER_NAMESPACE,
|
||||
'controller' => DEFAULT_CONTROLLER,
|
||||
],
|
||||
'update' => [
|
||||
'path' => '/{controller}/update',
|
||||
@ -225,8 +204,7 @@ return [
|
||||
],
|
||||
'index_redirect' => [
|
||||
'path' => '/',
|
||||
'controller' => DEFAULT_CONTROLLER_NAMESPACE,
|
||||
'controller' => DEFAULT_CONTROLLER,
|
||||
'action' => 'redirectToDefaultRoute',
|
||||
],
|
||||
],
|
||||
];
|
19
app/config/route_config.toml.example
Normal file
19
app/config/route_config.toml.example
Normal file
@ -0,0 +1,19 @@
|
||||
################################################################################
|
||||
# Route config
|
||||
#
|
||||
# Default views and paths
|
||||
################################################################################
|
||||
|
||||
# Path to public directory, where images/css/javascript are located,
|
||||
# appended to the url
|
||||
asset_path = "/public"
|
||||
|
||||
# Which list should be the default?
|
||||
default_list = "anime" # anime or manga
|
||||
|
||||
# Default pages for anime/manga
|
||||
default_anime_list_path = "watching" # watching|plan_to_watch|on_hold|dropped|completed|all
|
||||
default_manga_list_path = "reading" # reading|plan_to_read|on_hold|dropped|completed|all
|
||||
|
||||
# Default view type (cover_view/list_view)
|
||||
default_view_type = "cover_view"
|
@ -1,4 +1,4 @@
|
||||
<main class="details">
|
||||
<main class="details fixed">
|
||||
<section class="flex flex-no-wrap">
|
||||
<div>
|
||||
<img class="cover" width="402" height="284" src="<?= $data['cover_image'] ?>" alt="" />
|
||||
@ -74,30 +74,27 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif ?>
|
||||
|
||||
|
||||
<?php /* <pre><?= print_r($characters, TRUE) ?></pre> */ ?>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
|
||||
<?php if (count($characters) > 0): ?>
|
||||
<h2>Characters</h2>
|
||||
<div class="flex flex-wrap">
|
||||
<section class="media-wrap">
|
||||
<?php foreach($characters as $char): ?>
|
||||
<?php if ( ! empty($char['image']['original'])): ?>
|
||||
<div class="character">
|
||||
<article class="character">
|
||||
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
|
||||
<div class="name">
|
||||
<?= $helper->a($link, $char['name']); ?>
|
||||
<br />
|
||||
</div>
|
||||
<a href="<?= $link ?>">
|
||||
<?= $helper->img($char['image']['original'], [
|
||||
'width' => '225'
|
||||
]) ?>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
</section>
|
||||
<?php endif ?>
|
||||
</main>
|
@ -30,7 +30,11 @@
|
||||
<tr id="a-<?= $item['id'] ?>">
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<td>
|
||||
<a class="bracketed" href="<?= $urlGenerator->url("/anime/edit/{$item['id']}/{$item['watching_status']}") ?>">Edit</a>
|
||||
<a class="bracketed" href="<?= $url->generate('edit', [
|
||||
'controller' => 'anime',
|
||||
'id' => $item['id'],
|
||||
'status' => $item['watching_status']
|
||||
]) ?>">Edit</a>
|
||||
</td>
|
||||
<?php endif ?>
|
||||
<td class="justify">
|
||||
|
@ -1,7 +1,7 @@
|
||||
<main class="details">
|
||||
<main class="details fixed">
|
||||
<section class="flex flex-no-wrap">
|
||||
<div>
|
||||
<img class="cover" width="402" height="284" src="<?= $data['image']['original'] ?>" alt="" />
|
||||
<img class="cover" width="284" src="<?= $data['image']['original'] ?>" alt="" />
|
||||
</div>
|
||||
<div>
|
||||
<h2><?= $data['name'] ?></h2>
|
||||
|
@ -21,8 +21,11 @@
|
||||
<div class="table">
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<div class="row">
|
||||
<span class="edit"><a class="bracketed" href="<?= $urlGenerator->url("collection/edit/{$item['hummingbird_id']}") ?>">Edit</a></span>
|
||||
<?php /*<span class="delete"><a class="bracketed" href="<?= $urlGenerator->url("collection/delete/{$item['hummingbird_id']}") ?>">Delete</a></span> */ ?>
|
||||
<span class="edit">
|
||||
<a class="bracketed" href="<?= $url->generate('collection.edit.get', [
|
||||
'id' => $item['hummingbird_id']
|
||||
]) ?>">Edit</a>
|
||||
</span>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<div class="row">
|
||||
|
@ -1,6 +1,6 @@
|
||||
<main>
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<a class="bracketed" href="<?= $urlGenerator->fullUrl('collection/add', 'anime') ?>">Add Item</a>
|
||||
<a class="bracketed" href="<?= $url->generate('collection.add.get') ?>">Add Item</a>
|
||||
<?php endif ?>
|
||||
<?php if (empty($sections)): ?>
|
||||
<h3>There's nothing here!</h3>
|
||||
@ -26,12 +26,11 @@
|
||||
<tr>
|
||||
<?php if($auth->isAuthenticated()): ?>
|
||||
<td>
|
||||
<a class="bracketed" href="<?= $urlGenerator->fullUrl("collection/edit/{$item['hummingbird_id']}") ?>">Edit</a>
|
||||
<?php /*<a class="bracketed" href="<?= $urlGenerator->fullUrl("collection/delete/{$item['hummingbird_id']}") ?>">Delete</a>*/ ?>
|
||||
<a class="bracketed" href="<?= $url->generate('collection.edit.get', ['id' => $item['hummingbird_id']]) ?>">Edit</a>
|
||||
</td>
|
||||
<?php endif ?>
|
||||
<td class="align_left">
|
||||
<a href="https://hummingbird.me/anime/<?= $item['slug'] ?>">
|
||||
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
|
||||
<?= $item['title'] ?>
|
||||
</a>
|
||||
<?= ( ! empty($item['alternate_title'])) ? " <br /><small> " . $item['alternate_title'] . "</small>" : "" ?>
|
||||
|
@ -6,11 +6,11 @@
|
||||
<?= $config->get('whose_list') ?>'s <?= ucfirst($url_type) ?> List
|
||||
</a>
|
||||
<?php if($config->get("show_{$url_type}_collection")): ?>
|
||||
[<a href="<?= $urlGenerator->url('collection/view') ?>"><?= ucfirst($url_type) ?> Collection</a>]
|
||||
[<a href="<?= $url->generate('collection.view') ?>"><?= ucfirst($url_type) ?> Collection</a>]
|
||||
<?php endif ?>
|
||||
[<a href="<?= $urlGenerator->defaultUrl($other_type) ?>"><?= ucfirst($other_type) ?> List</a>]
|
||||
<?php else: ?>
|
||||
<a href="<?= $urlGenerator->url('collection/view') ?>">
|
||||
<a href="<?= $url->generate('collection.view') ?>">
|
||||
<?= $config->get('whose_list') ?>'s <?= ucfirst($url_type) ?> Collection
|
||||
</a>
|
||||
[<a href="<?= $urlGenerator->defaultUrl('anime') ?>">Anime List</a>]
|
||||
|
@ -1,6 +1,6 @@
|
||||
<main>
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<a class="bracketed" href="<?= $urlGenerator->url('manga/add') ?>">Add Item</a>
|
||||
<a class="bracketed" href="<?= $url->generate('manga.add.get') ?>">Add Item</a>
|
||||
<?php endif ?>
|
||||
<?php if (empty($sections)): ?>
|
||||
<h3>There's nothing here!</h3>
|
||||
@ -10,10 +10,11 @@
|
||||
<h2><?= $escape->html($name) ?></h2>
|
||||
<section class="media-wrap">
|
||||
<?php foreach($items as $item): ?>
|
||||
<article class="media" id="manga-<?= $item['id'] ?>">
|
||||
<article class="media" data-kitsu-id="<?= $item['id'] ?>" data-mal-id="<?= $item['mal_id'] ?>">
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<div class="edit_buttons" hidden>
|
||||
<button class="plus_one_chapter">+1 Chapter</button>
|
||||
<?php /* <button class="plus_one_volume">+1 Volume</button> */ ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<img src="<?= $escape->attr($item['manga']['image']) ?>" />
|
||||
@ -29,13 +30,38 @@
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<div class="row">
|
||||
<span class="edit">
|
||||
<a class="bracketed" title="Edit information about this manga" href="<?= $urlGenerator->url("manga/edit/{$item['id']}/{$name}") ?>">Edit</a>
|
||||
<a class="bracketed"
|
||||
title="Edit information about this manga"
|
||||
href="<?= $url->generate('edit', [
|
||||
'controller' => 'manga',
|
||||
'id' => $item['id'],
|
||||
'status' => $name
|
||||
]) ?>">
|
||||
Edit
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<div class="row">
|
||||
<div class="user_rating">Rating: <?= $item['user_rating'] ?> / 10</div>
|
||||
</div>
|
||||
|
||||
<?php if ($item['rereading']): ?>
|
||||
<div class="row">
|
||||
<?php foreach(['rereading'] as $attr): ?>
|
||||
<?php if($item[$attr]): ?>
|
||||
<span class="item-<?= $attr ?>"><?= ucfirst($attr) ?></span>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ($item['reread'] > 0): ?>
|
||||
<div class="row">
|
||||
<div>Reread <?= $item['reread'] ?> time(s)</div>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="chapter_completion">
|
||||
Chapters: <span class="chapters_read"><?= $item['chapters']['read'] ?></span> /
|
||||
|
@ -1,4 +1,4 @@
|
||||
<main class="details">
|
||||
<main class="details fixed">
|
||||
<section class="flex flex-no-wrap">
|
||||
<div>
|
||||
<img class="cover" src="<?= $data['cover_image'] ?>" alt="<?= $data['title'] ?> cover image" />
|
||||
@ -35,26 +35,25 @@
|
||||
<p><?= nl2br($data['synopsis']) ?></p>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
|
||||
<?php if (count($characters) > 0): ?>
|
||||
<h2>Characters</h2>
|
||||
<div class="flex flex-wrap">
|
||||
<section class="media-wrap">
|
||||
<?php foreach($characters as $char): ?>
|
||||
<?php if ( ! empty($char['image']['original'])): ?>
|
||||
<div class="character">
|
||||
<article class="character">
|
||||
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
|
||||
<div class="name">
|
||||
<?= $helper->a($link, $char['name']); ?>
|
||||
<br />
|
||||
</div>
|
||||
<a href="<?= $link ?>">
|
||||
<?= $helper->img($char['image']['original'], [
|
||||
'width' => '225'
|
||||
]) ?>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
</section>
|
||||
|
||||
<?php endif ?>
|
||||
</main>
|
@ -74,6 +74,7 @@
|
||||
<td> </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" />
|
||||
@ -92,6 +93,7 @@
|
||||
<td> </td>
|
||||
<td>
|
||||
<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>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<main>
|
||||
<?php if ($auth->isAuthenticated()): ?>
|
||||
<a class="bracketed" href="<?= $urlGenerator->url('manga/add') ?>">Add Item</a>
|
||||
<a class="bracketed" href="<?= $url->generate('manga.add.get') ?>">Add Item</a>
|
||||
<?php endif ?>
|
||||
<?php if (empty($sections)): ?>
|
||||
<h3>There's nothing here!</h3>
|
||||
@ -17,7 +17,9 @@
|
||||
<th>Rating</th>
|
||||
<th>Completed Chapters</th>
|
||||
<th># of Volumes</th>
|
||||
<th>Attributes</th>
|
||||
<th>Type</th>
|
||||
<th>Genres</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -25,7 +27,11 @@
|
||||
<tr id="manga-<?= $item['id'] ?>">
|
||||
<?php if($auth->isAuthenticated()): ?>
|
||||
<td>
|
||||
<a class="bracketed" href="<?= $urlGenerator->url("manga/edit/{$item['id']}/{$name}") ?>">Edit</a>
|
||||
<a class="bracketed" href="<?= $url->generate('edit', [
|
||||
'controller' => 'manga',
|
||||
'id' => $item['id'],
|
||||
'status' => $name
|
||||
]) ?>">Edit</a>
|
||||
</td>
|
||||
<?php endif ?>
|
||||
<td class="align_left">
|
||||
@ -39,7 +45,22 @@
|
||||
<td><?= $item['user_rating'] ?> / 10</td>
|
||||
<td><?= $item['chapters']['read'] ?> / <?= $item['chapters']['total'] ?></td>
|
||||
<td><?= $item['volumes']['total'] ?></td>
|
||||
<td>
|
||||
<ul>
|
||||
<?php if ($item['reread'] > 0): ?>
|
||||
<li>Reread <?= $item['reread'] ?> time(s)</li>
|
||||
<?php endif ?>
|
||||
<?php foreach(['rereading'] as $attr): ?>
|
||||
<?php if($item[$attr]): ?>
|
||||
<li><?= ucfirst($attr); ?></li>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
</td>
|
||||
<td><?= $item['manga']['type'] ?></td>
|
||||
<td class="align_left">
|
||||
<?= implode(', ', $item['manga']['genres']) ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</tbody>
|
||||
|
@ -1,11 +1,22 @@
|
||||
<main class="details">
|
||||
<?php use Aviat\AnimeClient\API\Kitsu; ?>
|
||||
<main class="user-page details">
|
||||
<section class="flex flex-no-wrap">
|
||||
<div>
|
||||
<h2><?= $attributes['name'] ?></h2>
|
||||
<center>
|
||||
<h2>
|
||||
<a title='View profile on Kisu'
|
||||
href="https://kitsu.io/users/<?= $attributes['name'] ?>">
|
||||
<?= $attributes['name'] ?>
|
||||
</a>
|
||||
</h2>
|
||||
<img src="<?= $attributes['avatar']['original'] ?>" alt="" />
|
||||
</center>
|
||||
<br />
|
||||
<br />
|
||||
<table class="media_details">
|
||||
<tr>
|
||||
<th colspan="2">General</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Location</td>
|
||||
<td><?= $attributes['location'] ?></td>
|
||||
@ -28,6 +39,21 @@
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif ?>
|
||||
<tr>
|
||||
<th colspan="2">User Stats</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td># of Posts</td>
|
||||
<td><?= $attributes['postsCount'] ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td># of Comments</td>
|
||||
<td><?= $attributes['commentsCount'] ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td># of Media Rated</td>
|
||||
<td><?= $attributes['ratingsCount'] ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
@ -35,9 +61,72 @@
|
||||
<dt>About:</dt>
|
||||
<dd><?= $escape->html($attributes['bio']) ?></dd>
|
||||
</dl>
|
||||
<?php /* <pre><?= json_encode($attributes, \JSON_PRETTY_PRINT) ?></pre>
|
||||
<pre><?= json_encode($relationships, \JSON_PRETTY_PRINT) ?></pre>
|
||||
<pre><?= json_encode($included, \JSON_PRETTY_PRINT) ?></pre> */ ?>
|
||||
<?php if ( ! empty($favorites)): ?>
|
||||
<?php if ( ! empty($favorites['characters'])): ?>
|
||||
<h4>Favorite Characters</h4>
|
||||
<section class="media-wrap">
|
||||
<?php foreach($favorites['characters'] as $char): ?>
|
||||
<?php if ( ! empty($char['image']['original'])): ?>
|
||||
<article class="small_character">
|
||||
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
|
||||
<div class="name"><?= $helper->a($link, $char['name']); ?></div>
|
||||
<a href="<?= $link ?>">
|
||||
<?= $helper->img($char['image']['original']) ?>
|
||||
</a>
|
||||
</article>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
</section>
|
||||
<?php endif ?>
|
||||
<?php if ( ! empty($favorites['anime'])): ?>
|
||||
<h4>Favorite Anime</h4>
|
||||
<section class="media-wrap">
|
||||
<?php foreach($favorites['anime'] as $anime): ?>
|
||||
<article class="media">
|
||||
<?php
|
||||
$link = $url->generate('anime.details', ['id' => $anime['slug']]);
|
||||
$titles = Kitsu::filterTitles($anime);
|
||||
?>
|
||||
<a href="<?= $link ?>">
|
||||
<img src="<?= $anime['posterImage']['small'] ?>" width="220" alt="" />
|
||||
</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>
|
||||
<?php endif ?>
|
||||
<?php if ( ! empty($favorites['manga'])): ?>
|
||||
<h4>Favorite Manga</h4>
|
||||
<section class="media-wrap">
|
||||
<?php foreach($favorites['manga'] as $manga): ?>
|
||||
<article class="media">
|
||||
<?php
|
||||
$link = $url->generate('manga.details', ['id' => $manga['slug']]);
|
||||
$titles = Kitsu::filterTitles($manga);
|
||||
?>
|
||||
<a href="<?= $link ?>">
|
||||
<img src="<?= $manga['posterImage']['small'] ?>" width="220" alt="" />
|
||||
</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>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
@ -7,7 +7,9 @@ $file_patterns = [
|
||||
'src/**/*.php',
|
||||
'src/*.php',
|
||||
'tests/**/*.php',
|
||||
'tests/*.php'
|
||||
'tests/*.php',
|
||||
'index.php',
|
||||
'Robofile.php'
|
||||
];
|
||||
|
||||
if ( ! function_exists('glob_recursive'))
|
||||
|
@ -21,7 +21,7 @@
|
||||
"aura/router": "^3.0",
|
||||
"aura/session": "^2.0",
|
||||
"aviat/banker": "^1.0.0",
|
||||
"aviat/ion": "dev-master",
|
||||
"aviat/ion": "^2.0.0",
|
||||
"monolog/monolog": "^1.0",
|
||||
"psr/http-message": "~1.0",
|
||||
"psr/log": "~1.0",
|
||||
|
18
index.php
18
index.php
@ -1,23 +1,23 @@
|
||||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime Client
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Hummingbird to manage anime and manga watch lists
|
||||
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 5.6
|
||||
* PHP version 7
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2016 Timothy J. Warren
|
||||
* @copyright 2015 - 2017 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 3.1
|
||||
* @link https://github.com/timw4mail/HummingBirdAnimeClient
|
||||
* @version 4.0
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient;
|
||||
|
||||
use function Aviat\AnimeClient\loadToml;
|
||||
|
||||
use Aviat\AnimeClient\AnimeClient;
|
||||
use function Aviat\Ion\_dir;
|
||||
|
||||
// Work around the silly timezone error
|
||||
$timezone = ini_get('date.timezone');
|
||||
|
@ -14,7 +14,6 @@
|
||||
* @link https://github.com/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
|
||||
namespace Aviat\EasyMin;
|
||||
|
||||
require_once('./min.php');
|
||||
|
@ -1025,7 +1025,7 @@ a:hover, a:active {
|
||||
Base list styles
|
||||
------------------------------------------------------------------------------*/
|
||||
|
||||
.media {
|
||||
.media, .character, .small_character {
|
||||
position:relative;
|
||||
vertical-align:top;
|
||||
display:inline-block;
|
||||
@ -1035,7 +1035,9 @@ a:hover, a:active {
|
||||
margin:0.25em 0.125em;
|
||||
}
|
||||
|
||||
.media > img {
|
||||
.media > img,
|
||||
.character > img,
|
||||
.small_character > img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -1076,6 +1078,8 @@ a:hover, a:active {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.small_character:hover > .name,
|
||||
.character:hover > .name,
|
||||
.media:hover > .name,
|
||||
.media:hover > .media_metadata > div,
|
||||
.media:hover > .medium_metadata > div,
|
||||
@ -1094,6 +1098,10 @@ a:hover, a:active {
|
||||
display:block;
|
||||
}
|
||||
|
||||
.small_character > .name a,
|
||||
.small_character > .name a small,
|
||||
.character > .name a,
|
||||
.character > .name a small,
|
||||
.media > .name a,
|
||||
.media > .name a small
|
||||
{
|
||||
@ -1250,17 +1258,20 @@ a:hover, a:active {
|
||||
.details {
|
||||
margin:15px auto 0 auto;
|
||||
margin: 1.5rem auto 0 auto;
|
||||
max-width:930px;
|
||||
max-width:93rem;
|
||||
padding:10px;
|
||||
padding:1rem;
|
||||
font-size:inherit;
|
||||
}
|
||||
|
||||
.details.fixed {
|
||||
max-width:930px;
|
||||
max-width:93rem;
|
||||
}
|
||||
|
||||
.details .cover {
|
||||
display: block;
|
||||
width: 284px;
|
||||
height: 402px;
|
||||
/* height: 402px; */
|
||||
}
|
||||
|
||||
.details h2 {
|
||||
@ -1295,6 +1306,58 @@ a:hover, a:active {
|
||||
text-align:left;
|
||||
}
|
||||
|
||||
.character,
|
||||
.small_character {
|
||||
background: rgba(0, 0, 0, .5);
|
||||
width: 225px;
|
||||
height: 350px;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.small_character a {
|
||||
display:inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.small_character .name,
|
||||
.character .name {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.small_character img,
|
||||
.character img {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
-webkit-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
z-index: 5;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
User page styles
|
||||
-----------------------------------------------------------------------------*/
|
||||
|
||||
.small_character {
|
||||
width: 160px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.user-page .media-wrap {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.media a {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
Viewport-based styles
|
||||
-----------------------------------------------------------------------------*/
|
||||
|
@ -297,7 +297,7 @@ a:hover, a:active {
|
||||
Base list styles
|
||||
------------------------------------------------------------------------------*/
|
||||
|
||||
.media {
|
||||
.media, .character, .small_character {
|
||||
position:relative;
|
||||
vertical-align:top;
|
||||
display:inline-block;
|
||||
@ -307,7 +307,9 @@ a:hover, a:active {
|
||||
margin: var(--normal-padding);
|
||||
}
|
||||
|
||||
.media > img {
|
||||
.media > img,
|
||||
.character > img,
|
||||
.small_character > img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -347,7 +349,8 @@ a:hover, a:active {
|
||||
position:absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.small_character:hover > .name,
|
||||
.character:hover > .name,
|
||||
.media:hover > .name,
|
||||
.media:hover > .media_metadata > div,
|
||||
.media:hover > .medium_metadata > div,
|
||||
@ -364,6 +367,10 @@ a:hover, a:active {
|
||||
display:block;
|
||||
}
|
||||
|
||||
.small_character > .name a,
|
||||
.small_character > .name a small,
|
||||
.character > .name a,
|
||||
.character > .name a small,
|
||||
.media > .name a,
|
||||
.media > .name a small
|
||||
{
|
||||
@ -510,15 +517,18 @@ a:hover, a:active {
|
||||
-----------------------------------------------------------------------------*/
|
||||
.details {
|
||||
margin: 1.5rem auto 0 auto;
|
||||
max-width:93rem;
|
||||
padding:1rem;
|
||||
font-size:inherit;
|
||||
}
|
||||
|
||||
.details.fixed {
|
||||
max-width:93rem;
|
||||
}
|
||||
|
||||
.details .cover {
|
||||
display: block;
|
||||
width: 284px;
|
||||
height: 402px;
|
||||
/* height: 402px; */
|
||||
}
|
||||
|
||||
.details h2 {
|
||||
@ -549,6 +559,55 @@ a:hover, a:active {
|
||||
text-align:left;
|
||||
}
|
||||
|
||||
.character,
|
||||
.small_character {
|
||||
background: rgba(0,0,0,0.5);
|
||||
width: 225px;
|
||||
height: 350px;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.small_character a {
|
||||
display:inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.small_character .name,
|
||||
.character .name {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.small_character img,
|
||||
.character img {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 5;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
User page styles
|
||||
-----------------------------------------------------------------------------*/
|
||||
.small_character {
|
||||
width: 160px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.user-page .media-wrap {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.media a {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
Viewport-based styles
|
||||
-----------------------------------------------------------------------------*/
|
||||
|
@ -8,6 +8,11 @@
|
||||
searchResults = JSON.parse(searchResults);
|
||||
_.$('.cssload-loader')[0].setAttribute('hidden', 'hidden');
|
||||
|
||||
// Give mustache a key to iterate over
|
||||
searchResults = {
|
||||
data: searchResults.data
|
||||
};
|
||||
|
||||
Mustache.parse(tempHtml);
|
||||
_.$('#series_list')[0].innerHTML = Mustache.render(tempHtml, searchResults);
|
||||
});
|
||||
|
@ -8,7 +8,6 @@
|
||||
_.on('.manga.list', 'click', '.edit_buttons button', (e) => {
|
||||
let thisSel = e.target;
|
||||
let parentSel = _.closestParent(e.target, 'article');
|
||||
let mangaId = parentSel.id.replace('manga-', '');
|
||||
let type = thisSel.classList.contains('plus_one_chapter') ? 'chapter' : 'volume';
|
||||
let completed = parseInt(_.$(`.${type}s_read`, parentSel)[0].textContent, 10);
|
||||
let total = parseInt(_.$(`.${type}_count`, parentSel)[0].textContent, 10);
|
||||
@ -20,7 +19,8 @@
|
||||
|
||||
// Setup the update data
|
||||
let data = {
|
||||
id: mangaId,
|
||||
id: parentSel.dataset.kitsuId,
|
||||
mal_id: parentSel.dataset.malId,
|
||||
data: {
|
||||
progress: completed
|
||||
}
|
||||
|
@ -40,6 +40,201 @@ class JsonAPI {
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* Inline all included data
|
||||
*
|
||||
* @param array $data - The raw JsonAPI response data
|
||||
* @return data
|
||||
*/
|
||||
public static function organizeData(array $data): array
|
||||
{
|
||||
// relationships that have singular data
|
||||
$singular = [
|
||||
'waifu'
|
||||
];
|
||||
|
||||
// Reorganize included data
|
||||
$included = static::organizeIncluded($data['included']);
|
||||
|
||||
// Inline organized data
|
||||
foreach($data['data'] as $i => $item)
|
||||
{
|
||||
if (array_key_exists('relationships', $item))
|
||||
{
|
||||
foreach($item['relationships'] as $relType => $props)
|
||||
{
|
||||
|
||||
if (array_keys($props) === ['links'])
|
||||
{
|
||||
unset($data['data'][$i]['relationships'][$relType]);
|
||||
|
||||
if (empty($data['data'][$i]['relationships']))
|
||||
{
|
||||
unset($data['data'][$i]['relationships']);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (array_key_exists('links', $props))
|
||||
{
|
||||
unset($data['data'][$i]['relationships'][$relType]['links']);
|
||||
}
|
||||
|
||||
if (array_key_exists('data', $props))
|
||||
{
|
||||
if (empty($props['data']))
|
||||
{
|
||||
unset($data['data'][$i]['relationships'][$relType]['data']);
|
||||
|
||||
if (empty($data['data'][$i]['relationships'][$relType]))
|
||||
{
|
||||
unset($data['data'][$i]['relationships'][$relType]);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
// Single data item
|
||||
else if (array_key_exists('id', $props['data']))
|
||||
{
|
||||
$idKey = $props['data']['id'];
|
||||
$typeKey = $props['data']['type'];
|
||||
$relationship =& $data['data'][$i]['relationships'][$relType];
|
||||
unset($relationship['data']);
|
||||
|
||||
if (in_array($relType, $singular))
|
||||
{
|
||||
$relationship = $included[$typeKey][$idKey];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($relType === $typeKey)
|
||||
{
|
||||
$relationship[$idKey] = $included[$typeKey][$idKey];
|
||||
continue;
|
||||
}
|
||||
|
||||
$relationship[$typeKey][$idKey] = $included[$typeKey][$idKey];
|
||||
}
|
||||
// Multiple data items
|
||||
else
|
||||
{
|
||||
foreach($props['data'] as $j => $datum)
|
||||
{
|
||||
$idKey = $props['data'][$j]['id'];
|
||||
$typeKey = $props['data'][$j]['type'];
|
||||
$relationship =& $data['data'][$i]['relationships'][$relType];
|
||||
|
||||
unset($relationship['data'][$j]);
|
||||
|
||||
if (empty($relationship['data']))
|
||||
{
|
||||
unset($relationship['data']);
|
||||
}
|
||||
|
||||
if ($relType === $typeKey)
|
||||
{
|
||||
$relationship[$idKey] = $included[$typeKey][$idKey];
|
||||
continue;
|
||||
}
|
||||
|
||||
$relationship[$typeKey][$idKey] = array_merge(
|
||||
$included[$typeKey][$idKey],
|
||||
$relationship[$typeKey][$idKey] ?? []
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data['data'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Restructure included data to make it simpler to inline
|
||||
*
|
||||
* @param array $included
|
||||
* @return array
|
||||
*/
|
||||
public static function organizeIncluded(array $included): array
|
||||
{
|
||||
$organized = [];
|
||||
|
||||
// First pass, create [ type => items[] ] structure
|
||||
foreach($included as &$item)
|
||||
{
|
||||
$type = $item['type'];
|
||||
$id = $item['id'];
|
||||
$organized[$type] = $organized[$type] ?? [];
|
||||
$newItem = [];
|
||||
|
||||
foreach(['attributes', 'relationships'] as $key)
|
||||
{
|
||||
if (array_key_exists($key, $item))
|
||||
{
|
||||
// Remove 'links' type relationships
|
||||
if ($key === 'relationships')
|
||||
{
|
||||
foreach($item['relationships'] as $relType => $props)
|
||||
{
|
||||
if (array_keys($props) === ['links'])
|
||||
{
|
||||
unset($item['relationships'][$relType]);
|
||||
if (empty($item['relationships']))
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$newItem[$key] = $item[$key];
|
||||
}
|
||||
}
|
||||
|
||||
$organized[$type][$id] = $newItem;
|
||||
}
|
||||
|
||||
// Second pass, go through and fill missing relationships in the first pass
|
||||
foreach($organized as $type => $items)
|
||||
{
|
||||
foreach($items as $id => $item)
|
||||
{
|
||||
if (array_key_exists('relationships', $item))
|
||||
{
|
||||
foreach($item['relationships'] as $relType => $props)
|
||||
{
|
||||
if (array_key_exists('data', $props))
|
||||
{
|
||||
if (array_key_exists($props['data']['id'], $organized[$props['data']['type']]))
|
||||
{
|
||||
$idKey = $props['data']['id'];
|
||||
$typeKey = $props['data']['type'];
|
||||
|
||||
|
||||
$relationship =& $organized[$type][$id]['relationships'][$relType];
|
||||
unset($relationship['links']);
|
||||
unset($relationship['data']);
|
||||
|
||||
if ($relType === $typeKey)
|
||||
{
|
||||
$relationship[$idKey] = $included[$typeKey][$idKey];
|
||||
continue;
|
||||
}
|
||||
|
||||
$relationship[$typeKey][$idKey] = $organized[$typeKey][$idKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $organized;
|
||||
}
|
||||
|
||||
public static function inlineRawIncludes(array &$data, string $key): array
|
||||
{
|
||||
foreach($data['data'] as $i => &$item)
|
||||
@ -118,27 +313,7 @@ class JsonAPI {
|
||||
*/
|
||||
public static function lightlyOrganizeIncludes(array $includes): array
|
||||
{
|
||||
$organized = [];
|
||||
|
||||
foreach($includes as $item)
|
||||
{
|
||||
$type = $item['type'];
|
||||
$id = $item['id'];
|
||||
$organized[$type] = $organized[$type] ?? [];
|
||||
$newItem = [];
|
||||
|
||||
foreach(['attributes', 'relationships'] as $key)
|
||||
{
|
||||
if (array_key_exists($key, $item))
|
||||
{
|
||||
$newItem[$key] = $item[$key];
|
||||
}
|
||||
}
|
||||
|
||||
$organized[$type][$id] = $newItem;
|
||||
}
|
||||
|
||||
return $organized;
|
||||
return static::organizeIncluded($includes);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -33,6 +33,8 @@ class ListItem extends AbstractListItem {
|
||||
|
||||
private function getAuthHeader()
|
||||
{
|
||||
$cache = $this->getContainer()->get('cache');
|
||||
$cacheItem = $cache->getItem('kitsu-auth-token');
|
||||
$sessionSegment = $this->getContainer()
|
||||
->get('session')
|
||||
->getSegment(SESSION_SEGMENT);
|
||||
@ -43,6 +45,12 @@ class ListItem extends AbstractListItem {
|
||||
return "bearer {$token}";
|
||||
}
|
||||
|
||||
if ($cacheItem->isHit())
|
||||
{
|
||||
$token = $cacheItem->get();
|
||||
return "bearer {$token}";
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,12 @@ namespace Aviat\AnimeClient\API\Kitsu;
|
||||
use function Amp\{all, wait};
|
||||
|
||||
use Amp\Artax\{Client, Request};
|
||||
use Aviat\AnimeClient\API\{CacheTrait, JsonAPI, Kitsu as K};
|
||||
use Aviat\AnimeClient\API\{
|
||||
CacheTrait,
|
||||
JsonAPI,
|
||||
Kitsu as K,
|
||||
ParallelAPIRequest
|
||||
};
|
||||
use Aviat\AnimeClient\API\Enum\{
|
||||
AnimeWatchingStatus\Title,
|
||||
AnimeWatchingStatus\Kitsu as KitsuWatchingStatus,
|
||||
@ -73,7 +78,6 @@ class Model {
|
||||
*/
|
||||
protected $mangaListTransformer;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
@ -88,6 +92,34 @@ class Model {
|
||||
$this->mangaListTransformer = new MangaListTransformer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the access token from the Kitsu API
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @return bool|string
|
||||
*/
|
||||
public function authenticate(string $username, string $password)
|
||||
{
|
||||
$response = $this->getResponse('POST', K::AUTH_URL, [
|
||||
'headers' => [],
|
||||
'form_params' => [
|
||||
'grant_type' => 'password',
|
||||
'username' => $username,
|
||||
'password' => $password
|
||||
]
|
||||
]);
|
||||
|
||||
$data = Json::decode((string)$response->getBody());
|
||||
|
||||
if (array_key_exists('access_token', $data))
|
||||
{
|
||||
return $data;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the userid for a username from Kitsu
|
||||
*
|
||||
@ -132,7 +164,7 @@ class Model {
|
||||
$data = $this->getRequest('/characters', [
|
||||
'query' => [
|
||||
'filter' => [
|
||||
'slug' => $slug
|
||||
'name' => $slug
|
||||
],
|
||||
// 'include' => 'primaryMedia,castings'
|
||||
]
|
||||
@ -149,45 +181,86 @@ class Model {
|
||||
*/
|
||||
public function getUserData(string $username): array
|
||||
{
|
||||
$userId = $this->getUserIdByUsername($username);
|
||||
$data = $this->getRequest("/users/{$userId}", [
|
||||
// $userId = $this->getUserIdByUsername($username);
|
||||
$data = $this->getRequest("/users", [
|
||||
'query' => [
|
||||
'include' => 'waifu,pinnedPost,blocks,linkedAccounts,profileLinks,profileLinks.profileLinkSite,mediaFollows,userRoles'
|
||||
'filter' => [
|
||||
'name' => $username,
|
||||
],
|
||||
'fields' => [
|
||||
// 'anime' => 'slug,name,canonicalTitle',
|
||||
'characters' => 'slug,name,image'
|
||||
],
|
||||
'include' => 'waifu,pinnedPost,blocks,linkedAccounts,profileLinks,profileLinks.profileLinkSite,mediaFollows,userRoles,favorites.item'
|
||||
]
|
||||
]);
|
||||
// $data['included'] = JsonAPI::organizeIncludes($data['included']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the access token from the Kitsu API
|
||||
* Search for an anime or manga
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @return bool|string
|
||||
* @param string $type - 'anime' or 'manga'
|
||||
* @param string $query - name of the item to search for
|
||||
* @return array
|
||||
*/
|
||||
public function authenticate(string $username, string $password)
|
||||
public function search(string $type, string $query): array
|
||||
{
|
||||
$response = $this->getResponse('POST', K::AUTH_URL, [
|
||||
'headers' => [],
|
||||
'form_params' => [
|
||||
'grant_type' => 'password',
|
||||
'username' => $username,
|
||||
'password' => $password
|
||||
$options = [
|
||||
'query' => [
|
||||
'filter' => [
|
||||
'text' => $query
|
||||
],
|
||||
'page' => [
|
||||
'offset' => 0,
|
||||
'limit' => 20
|
||||
],
|
||||
]
|
||||
]);
|
||||
];
|
||||
|
||||
$data = Json::decode((string)$response->getBody());
|
||||
$raw = $this->getRequest($type, $options);
|
||||
|
||||
if (array_key_exists('access_token', $data))
|
||||
foreach ($raw['data'] as &$item)
|
||||
{
|
||||
return $data;
|
||||
$item['attributes']['titles'] = K::filterTitles($item['attributes']);
|
||||
array_shift($item['attributes']['titles']);
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
return $raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a media item on Kitsu by its associated MAL id
|
||||
*
|
||||
* @param string $malId
|
||||
* @param string $type "anime" or "manga"
|
||||
* @return string
|
||||
*/
|
||||
public function getKitsuIdFromMALId(string $malId, string $type="anime"): string
|
||||
{
|
||||
$options = [
|
||||
'query' => [
|
||||
'filter' => [
|
||||
'external_site' => "myanimelist/{$type}",
|
||||
'external_id' => $malId
|
||||
],
|
||||
'fields' => [
|
||||
'media' => 'id,slug'
|
||||
],
|
||||
'include' => 'media'
|
||||
]
|
||||
];
|
||||
|
||||
$raw = $this->getRequest('mappings', $options);
|
||||
|
||||
return $raw['included'][0]['id'];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ! Anime-specific methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get information about a particular anime
|
||||
*
|
||||
@ -220,202 +293,6 @@ class Model {
|
||||
return $this->animeTransformer->transform($baseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mal id for the anime represented by the kitsu id
|
||||
* to enable updating MyAnimeList
|
||||
*
|
||||
* @param string $kitsuAnimeId The id of the anime on Kitsu
|
||||
* @return string|null Returns the mal id if it exists, otherwise null
|
||||
*/
|
||||
public function getMalIdForAnime(string $kitsuAnimeId)
|
||||
{
|
||||
$options = [
|
||||
'query' => [
|
||||
'include' => 'mappings'
|
||||
]
|
||||
];
|
||||
$data = $this->getRequest("anime/{$kitsuAnimeId}", $options);
|
||||
$mappings = array_column($data['included'], 'attributes');
|
||||
|
||||
foreach($mappings as $map)
|
||||
{
|
||||
if ($map['externalSite'] === 'myanimelist/anime')
|
||||
{
|
||||
return $map['externalId'];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about a particular manga
|
||||
*
|
||||
* @param string $mangaId
|
||||
* @return array
|
||||
*/
|
||||
public function getManga(string $mangaId): array
|
||||
{
|
||||
$baseData = $this->getRawMediaData('manga', $mangaId);
|
||||
|
||||
if (empty($baseData))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$transformed = $this->mangaTransformer->transform($baseData);
|
||||
$transformed['included'] = $baseData['included'];
|
||||
return $transformed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of anime list items
|
||||
*
|
||||
* @param string $status - Optional status to filter by
|
||||
* @return int
|
||||
*/
|
||||
public function getAnimeListCount(string $status = '') : int
|
||||
{
|
||||
$options = [
|
||||
'query' => [
|
||||
'filter' => [
|
||||
'user_id' => $this->getUserIdByUsername(),
|
||||
'media_type' => 'Anime'
|
||||
],
|
||||
'page' => [
|
||||
'limit' => 1
|
||||
],
|
||||
'sort' => '-updated_at'
|
||||
]
|
||||
];
|
||||
|
||||
if ( ! empty($status))
|
||||
{
|
||||
$options['query']['filter']['status'] = $status;
|
||||
}
|
||||
|
||||
$response = $this->getRequest('library-entries', $options);
|
||||
|
||||
return $response['meta']['count'];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full anime list in paginated form
|
||||
*
|
||||
* @param int $limit
|
||||
* @param int $offset
|
||||
* @param array $options
|
||||
* @return Request
|
||||
*/
|
||||
public function getPagedAnimeList(int $limit = 100, int $offset = 0, array $options = [
|
||||
'include' => 'anime.mappings'
|
||||
]): Request
|
||||
{
|
||||
$defaultOptions = [
|
||||
'filter' => [
|
||||
'user_id' => $this->getUserIdByUsername($this->getUsername()),
|
||||
'media_type' => 'Anime'
|
||||
],
|
||||
'page' => [
|
||||
'offset' => $offset,
|
||||
'limit' => $limit
|
||||
],
|
||||
'sort' => '-updated_at'
|
||||
];
|
||||
$options = array_merge($defaultOptions, $options);
|
||||
|
||||
return $this->setUpRequest('GET', 'library-entries', ['query' => $options]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full anime list
|
||||
*
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
public function getFullAnimeList(array $options = [
|
||||
'include' => 'anime.mappings'
|
||||
]): array
|
||||
{
|
||||
$status = $options['filter']['status'] ?? '';
|
||||
$count = $this->getAnimeListCount($status);
|
||||
$size = 100;
|
||||
$pages = ceil($count / $size);
|
||||
|
||||
$requests = [];
|
||||
|
||||
// Set up requests
|
||||
for ($i = 0; $i < $pages; $i++)
|
||||
{
|
||||
$offset = $i * $size;
|
||||
$requests[] = $this->getPagedAnimeList($size, $offset, $options);
|
||||
}
|
||||
|
||||
$promiseArray = (new Client())->requestMulti($requests);
|
||||
|
||||
$responses = wait(all($promiseArray));
|
||||
$output = [];
|
||||
|
||||
foreach($responses as $response)
|
||||
{
|
||||
$data = Json::decode($response->getBody());
|
||||
$output = array_merge_recursive($output, $data);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw (unorganized) anime list for the configured user
|
||||
*
|
||||
* @param string $status - The watching status to filter the list with
|
||||
* @return array
|
||||
*/
|
||||
public function getRawAnimeList(string $status): array
|
||||
{
|
||||
|
||||
$options = [
|
||||
'filter' => [
|
||||
'user_id' => $this->getUserIdByUsername($this->getUsername()),
|
||||
'media_type' => 'Anime',
|
||||
'status' => $status,
|
||||
],
|
||||
'include' => 'media,media.genres,media.mappings,anime.streamingLinks',
|
||||
'sort' => '-updated_at'
|
||||
];
|
||||
|
||||
return $this->getFullAnimeList($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the anine entries, that are organized for output to html
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFullOrganizedAnimeList(): array
|
||||
{
|
||||
$cacheItem = $this->cache->getItem(self::FULL_TRANSFORMED_LIST_CACHE_KEY);
|
||||
|
||||
if ( ! $cacheItem->isHit())
|
||||
{
|
||||
$output = [];
|
||||
|
||||
$statuses = KitsuWatchingStatus::getConstList();
|
||||
|
||||
foreach ($statuses as $key => $status)
|
||||
{
|
||||
$mappedStatus = AnimeWatchingStatus::KITSU_TO_TITLE[$status];
|
||||
$output[$mappedStatus] = $this->getAnimeList($status) ?? [];
|
||||
}
|
||||
|
||||
$cacheItem->set($output);
|
||||
$cacheItem->save();
|
||||
}
|
||||
|
||||
return $cacheItem->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the anime list for the configured user
|
||||
*
|
||||
@ -455,23 +332,200 @@ class Model {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Manga lists
|
||||
* Get the number of anime list items
|
||||
*
|
||||
* @param string $status - Optional status to filter by
|
||||
* @return int
|
||||
*/
|
||||
public function getAnimeListCount(string $status = '') : int
|
||||
{
|
||||
$options = [
|
||||
'query' => [
|
||||
'filter' => [
|
||||
'user_id' => $this->getUserIdByUsername(),
|
||||
'media_type' => 'Anime'
|
||||
],
|
||||
'page' => [
|
||||
'limit' => 1
|
||||
],
|
||||
'sort' => '-updated_at'
|
||||
]
|
||||
];
|
||||
|
||||
if ( ! empty($status))
|
||||
{
|
||||
$options['query']['filter']['status'] = $status;
|
||||
}
|
||||
|
||||
$response = $this->getRequest('library-entries', $options);
|
||||
|
||||
return $response['meta']['count'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full anime list
|
||||
*
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
public function getFullOrganizedMangaList(): array
|
||||
public function getFullAnimeList(array $options = [
|
||||
'include' => 'anime.mappings'
|
||||
]): array
|
||||
{
|
||||
$statuses = KitsuReadingStatus::getConstList();
|
||||
$status = $options['filter']['status'] ?? '';
|
||||
$count = $this->getAnimeListCount($status);
|
||||
$size = 100;
|
||||
$pages = ceil($count / $size);
|
||||
|
||||
$requester = new ParallelAPIRequest();
|
||||
|
||||
// Set up requests
|
||||
for ($i = 0; $i < $pages; $i++)
|
||||
{
|
||||
$offset = $i * $size;
|
||||
$requester->addRequest($this->getPagedAnimeList($size, $offset, $options));
|
||||
}
|
||||
|
||||
$responses = $requester->makeRequests();
|
||||
$output = [];
|
||||
foreach ($statuses as $status)
|
||||
|
||||
foreach($responses as $response)
|
||||
{
|
||||
$mappedStatus = MangaReadingStatus::KITSU_TO_TITLE[$status];
|
||||
$output[$mappedStatus] = $this->getMangaList($status);
|
||||
$data = Json::decode($response->getBody());
|
||||
$output = array_merge_recursive($output, $data);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the anine entries, that are organized for output to html
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFullOrganizedAnimeList(): array
|
||||
{
|
||||
$output = [];
|
||||
|
||||
$statuses = KitsuWatchingStatus::getConstList();
|
||||
|
||||
foreach ($statuses as $key => $status)
|
||||
{
|
||||
$mappedStatus = AnimeWatchingStatus::KITSU_TO_TITLE[$status];
|
||||
$output[$mappedStatus] = $this->getAnimeList($status) ?? [];
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mal id for the anime represented by the kitsu id
|
||||
* to enable updating MyAnimeList
|
||||
*
|
||||
* @param string $kitsuAnimeId The id of the anime on Kitsu
|
||||
* @return string|null Returns the mal id if it exists, otherwise null
|
||||
*/
|
||||
public function getMalIdForAnime(string $kitsuAnimeId)
|
||||
{
|
||||
$options = [
|
||||
'query' => [
|
||||
'include' => 'mappings'
|
||||
]
|
||||
];
|
||||
$data = $this->getRequest("anime/{$kitsuAnimeId}", $options);
|
||||
|
||||
if ( ! array_key_exists('included', $data))
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$mappings = array_column($data['included'], 'attributes');
|
||||
|
||||
foreach($mappings as $map)
|
||||
{
|
||||
if ($map['externalSite'] === 'myanimelist/anime')
|
||||
{
|
||||
return $map['externalId'];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full anime list in paginated form
|
||||
*
|
||||
* @param int $limit
|
||||
* @param int $offset
|
||||
* @param array $options
|
||||
* @return Request
|
||||
*/
|
||||
public function getPagedAnimeList(int $limit = 100, int $offset = 0, array $options = [
|
||||
'include' => 'anime.mappings'
|
||||
]): Request
|
||||
{
|
||||
$defaultOptions = [
|
||||
'filter' => [
|
||||
'user_id' => $this->getUserIdByUsername($this->getUsername()),
|
||||
'media_type' => 'Anime'
|
||||
],
|
||||
'page' => [
|
||||
'offset' => $offset,
|
||||
'limit' => $limit
|
||||
],
|
||||
'sort' => '-updated_at'
|
||||
];
|
||||
$options = array_merge($defaultOptions, $options);
|
||||
|
||||
return $this->setUpRequest('GET', 'library-entries', ['query' => $options]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw (unorganized) anime list for the configured user
|
||||
*
|
||||
* @param string $status - The watching status to filter the list with
|
||||
* @return array
|
||||
*/
|
||||
public function getRawAnimeList(string $status): array
|
||||
{
|
||||
|
||||
$options = [
|
||||
'filter' => [
|
||||
'user_id' => $this->getUserIdByUsername($this->getUsername()),
|
||||
'media_type' => 'Anime',
|
||||
'status' => $status,
|
||||
],
|
||||
'include' => 'media,media.genres,media.mappings,anime.streamingLinks',
|
||||
'sort' => '-updated_at'
|
||||
];
|
||||
|
||||
return $this->getFullAnimeList($options);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ! Manga-specific methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get information about a particular manga
|
||||
*
|
||||
* @param string $slug
|
||||
* @return array
|
||||
*/
|
||||
public function getManga(string $slug): array
|
||||
{
|
||||
$baseData = $this->getRawMediaData('manga', $slug);
|
||||
|
||||
if (empty($baseData))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$transformed = $this->mangaTransformer->transform($baseData);
|
||||
$transformed['included'] = $baseData['included'];
|
||||
return $transformed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the manga list for the configured user
|
||||
*
|
||||
@ -489,7 +543,7 @@ class Model {
|
||||
'media_type' => 'Manga',
|
||||
'status' => $status,
|
||||
],
|
||||
'include' => 'media',
|
||||
'include' => 'media,media.genres,media.mappings',
|
||||
'page' => [
|
||||
'offset' => $offset,
|
||||
'limit' => $limit
|
||||
@ -503,9 +557,16 @@ class Model {
|
||||
if ( ! $cacheItem->isHit())
|
||||
{
|
||||
$data = $this->getRequest('library-entries', $options);
|
||||
$data = JsonAPI::inlineRawIncludes($data, 'manga');
|
||||
|
||||
$transformed = $this->mangaListTransformer->transformCollection($data);
|
||||
$included = JsonAPI::organizeIncludes($data['included']);
|
||||
$included = JsonAPI::inlineIncludedRelationships($included, 'manga');
|
||||
|
||||
foreach($data['data'] as $i => &$item)
|
||||
{
|
||||
$item['included'] = $included;
|
||||
}
|
||||
|
||||
$transformed = $this->mangaListTransformer->transformCollection($data['data']);
|
||||
|
||||
$cacheItem->set($transformed);
|
||||
$cacheItem->save();
|
||||
@ -515,37 +576,150 @@ class Model {
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for an anime or manga
|
||||
* Get the number of manga list items
|
||||
*
|
||||
* @param string $type - 'anime' or 'manga'
|
||||
* @param string $query - name of the item to search for
|
||||
* @return array
|
||||
* @param string $status - Optional status to filter by
|
||||
* @return int
|
||||
*/
|
||||
public function search(string $type, string $query): array
|
||||
public function getMangaListCount(string $status = '') : int
|
||||
{
|
||||
$options = [
|
||||
'query' => [
|
||||
'filter' => [
|
||||
'text' => $query
|
||||
'user_id' => $this->getUserIdByUsername(),
|
||||
'media_type' => 'Manga'
|
||||
],
|
||||
'page' => [
|
||||
'offset' => 0,
|
||||
'limit' => 20
|
||||
'limit' => 1
|
||||
],
|
||||
'sort' => '-updated_at'
|
||||
]
|
||||
];
|
||||
|
||||
$raw = $this->getRequest($type, $options);
|
||||
|
||||
foreach ($raw['data'] as &$item)
|
||||
if ( ! empty($status))
|
||||
{
|
||||
$item['attributes']['titles'] = K::filterTitles($item['attributes']);
|
||||
array_shift($item['attributes']['titles']);
|
||||
$options['query']['filter']['status'] = $status;
|
||||
}
|
||||
|
||||
return $raw;
|
||||
$response = $this->getRequest('library-entries', $options);
|
||||
|
||||
return $response['meta']['count'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full manga list
|
||||
*
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
public function getFullMangaList(array $options = [
|
||||
'include' => 'manga.mappings'
|
||||
]): array
|
||||
{
|
||||
$status = $options['filter']['status'] ?? '';
|
||||
$count = $this->getMangaListCount($status);
|
||||
$size = 100;
|
||||
$pages = ceil($count / $size);
|
||||
|
||||
$requester = new ParallelAPIRequest();
|
||||
|
||||
// Set up requests
|
||||
for ($i = 0; $i < $pages; $i++)
|
||||
{
|
||||
$offset = $i * $size;
|
||||
$requester->addRequest($this->getPagedMangaList($size, $offset, $options));
|
||||
}
|
||||
|
||||
$responses = $requester->makeRequests();
|
||||
$output = [];
|
||||
|
||||
foreach($responses as $response)
|
||||
{
|
||||
$data = Json::decode($response->getBody());
|
||||
$output = array_merge_recursive($output, $data);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Manga lists
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFullOrganizedMangaList(): array
|
||||
{
|
||||
$statuses = KitsuReadingStatus::getConstList();
|
||||
$output = [];
|
||||
foreach ($statuses as $status)
|
||||
{
|
||||
$mappedStatus = MangaReadingStatus::KITSU_TO_TITLE[$status];
|
||||
$output[$mappedStatus] = $this->getMangaList($status);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full manga list in paginated form
|
||||
*
|
||||
* @param int $limit
|
||||
* @param int $offset
|
||||
* @param array $options
|
||||
* @return Request
|
||||
*/
|
||||
public function getPagedMangaList(int $limit = 100, int $offset = 0, array $options = [
|
||||
'include' => 'manga.mappings'
|
||||
]): Request
|
||||
{
|
||||
$defaultOptions = [
|
||||
'filter' => [
|
||||
'user_id' => $this->getUserIdByUsername($this->getUsername()),
|
||||
'media_type' => 'Manga'
|
||||
],
|
||||
'page' => [
|
||||
'offset' => $offset,
|
||||
'limit' => $limit
|
||||
],
|
||||
'sort' => '-updated_at'
|
||||
];
|
||||
$options = array_merge($defaultOptions, $options);
|
||||
|
||||
return $this->setUpRequest('GET', 'library-entries', ['query' => $options]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mal id for the manga represented by the kitsu id
|
||||
* to enable updating MyAnimeList
|
||||
*
|
||||
* @param string $kitsuAnimeId The id of the anime on Kitsu
|
||||
* @return string|null Returns the mal id if it exists, otherwise null
|
||||
*/
|
||||
public function getMalIdForManga(string $kitsuMangaId)
|
||||
{
|
||||
$options = [
|
||||
'query' => [
|
||||
'include' => 'mappings'
|
||||
]
|
||||
];
|
||||
$data = $this->getRequest("manga/{$kitsuMangaId}", $options);
|
||||
$mappings = array_column($data['included'], 'attributes');
|
||||
|
||||
foreach($mappings as $map)
|
||||
{
|
||||
if ($map['externalSite'] === 'myanimelist/manga')
|
||||
{
|
||||
return $map['externalId'];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ! Generic API calls
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a list item
|
||||
*
|
||||
@ -665,6 +839,9 @@ class Model {
|
||||
'filter' => [
|
||||
'slug' => $slug
|
||||
],
|
||||
'fields' => [
|
||||
'characters' => 'slug,name,image'
|
||||
],
|
||||
'include' => ($type === 'anime')
|
||||
? 'genres,mappings,streamingLinks,animeCharacters.character'
|
||||
: 'genres,mappings,mangaCharacters.character,castings.character',
|
||||
|
@ -123,7 +123,7 @@ class AnimeListTransformer extends AbstractTransformer {
|
||||
]
|
||||
];
|
||||
|
||||
if ( ! empty($item['user_rating']))
|
||||
if (is_numeric($item['user_rating']))
|
||||
{
|
||||
$untransformed['data']['rating'] = $item['user_rating'] / 2;
|
||||
}
|
||||
|
@ -35,22 +35,42 @@ class MangaListTransformer extends AbstractTransformer {
|
||||
*/
|
||||
public function transform($item)
|
||||
{
|
||||
$manga =& $item['manga'];
|
||||
$included = $item['included'];
|
||||
$mangaId = $item['relationships']['media']['data']['id'];
|
||||
$manga = $included['manga'][$mangaId];
|
||||
|
||||
$genres = array_column($manga['relationships']['genres'], 'name') ?? [];
|
||||
sort($genres);
|
||||
|
||||
$rating = (is_numeric($item['attributes']['rating']))
|
||||
? intval(2 * $item['attributes']['rating'])
|
||||
: '-';
|
||||
|
||||
$totalChapters = ($manga['attributes']['chapterCount'] > 0)
|
||||
? $manga['attributes']['chapterCount']
|
||||
$totalChapters = ($manga['chapterCount'] > 0)
|
||||
? $manga['chapterCount']
|
||||
: '-';
|
||||
|
||||
$totalVolumes = ($manga['attributes']['volumeCount'] > 0)
|
||||
? $manga['attributes']['volumeCount']
|
||||
$totalVolumes = ($manga['volumeCount'] > 0)
|
||||
? $manga['volumeCount']
|
||||
: '-';
|
||||
|
||||
$MALid = NULL;
|
||||
|
||||
if (array_key_exists('mappings', $manga['relationships']))
|
||||
{
|
||||
foreach ($manga['relationships']['mappings'] as $mapping)
|
||||
{
|
||||
if ($mapping['externalSite'] === 'myanimelist/manga')
|
||||
{
|
||||
$MALid = $mapping['externalId'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$map = [
|
||||
'id' => $item['id'],
|
||||
'mal_id' => $MALid,
|
||||
'chapters' => [
|
||||
'read' => $item['attributes']['progress'],
|
||||
'total' => $totalChapters
|
||||
@ -60,13 +80,13 @@ class MangaListTransformer extends AbstractTransformer {
|
||||
'total' => $totalVolumes
|
||||
],
|
||||
'manga' => [
|
||||
'titles' => Kitsu::filterTitles($manga['attributes']),
|
||||
'titles' => Kitsu::filterTitles($manga),
|
||||
'alternate_title' => NULL,
|
||||
'slug' => $manga['attributes']['slug'],
|
||||
'url' => 'https://kitsu.io/manga/' . $manga['attributes']['slug'],
|
||||
'type' => $manga['attributes']['mangaType'],
|
||||
'image' => $manga['attributes']['posterImage']['small'],
|
||||
'genres' => [], //$manga['genres'],
|
||||
'slug' => $manga['slug'],
|
||||
'url' => 'https://kitsu.io/manga/' . $manga['slug'],
|
||||
'type' => $manga['mangaType'],
|
||||
'image' => $manga['posterImage']['small'],
|
||||
'genres' => $genres,
|
||||
],
|
||||
'reading_status' => $item['attributes']['status'],
|
||||
'notes' => $item['attributes']['notes'],
|
||||
@ -90,16 +110,21 @@ class MangaListTransformer extends AbstractTransformer {
|
||||
|
||||
$map = [
|
||||
'id' => $item['id'],
|
||||
'mal_id' => $item['mal_id'],
|
||||
'data' => [
|
||||
'status' => $item['status'],
|
||||
'progress' => (int)$item['chapters_read'],
|
||||
'reconsuming' => $rereading,
|
||||
'reconsumeCount' => (int)$item['reread_count'],
|
||||
'notes' => $item['notes'],
|
||||
'rating' => $item['new_rating'] / 2
|
||||
],
|
||||
];
|
||||
|
||||
if (is_numeric($item['new_rating']))
|
||||
{
|
||||
$map['data']['rating'] = $item['new_rating'] / 2;
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,14 @@ class ListItem {
|
||||
use ContainerAware;
|
||||
use MALTrait;
|
||||
|
||||
public function create(array $data): Request
|
||||
/**
|
||||
* 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 = [
|
||||
@ -42,17 +49,24 @@ class ListItem {
|
||||
|
||||
$config = $this->container->get('config');
|
||||
|
||||
return $this->requestBuilder->newRequest('POST', "animelist/add/{$id}.xml")
|
||||
return $this->requestBuilder->newRequest('POST', "{$type}list/add/{$id}.xml")
|
||||
->setFormFields($createData)
|
||||
->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password']))
|
||||
->getFullRequest();
|
||||
}
|
||||
|
||||
public function delete(string $id): Request
|
||||
/**
|
||||
* 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', "animelist/delete/{$id}.xml")
|
||||
return $this->requestBuilder->newRequest('DELETE', "{$type}list/delete/{$id}.xml")
|
||||
->setFormFields([
|
||||
'id' => $id
|
||||
])
|
||||
@ -67,7 +81,15 @@ class ListItem {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function update(string $id, array $data): Request
|
||||
/**
|
||||
* Update a list item
|
||||
*
|
||||
* @param string $id
|
||||
* @param array $data
|
||||
* @param string $type
|
||||
* @return Request
|
||||
*/
|
||||
public function update(string $id, array $data, string $type = 'anime'): Request
|
||||
{
|
||||
$config = $this->container->get('config');
|
||||
|
||||
@ -76,7 +98,7 @@ class ListItem {
|
||||
->addField('id', $id)
|
||||
->addField('data', $xml);
|
||||
|
||||
return $this->requestBuilder->newRequest('POST', "animelist/update/{$id}.xml")
|
||||
return $this->requestBuilder->newRequest('POST', "{$type}list/update/{$id}.xml")
|
||||
->setFormFields([
|
||||
'id' => $id,
|
||||
'data' => $xml
|
||||
|
@ -64,21 +64,6 @@ trait MALTrait {
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unencode the dual-encoded ampersands in the body
|
||||
*
|
||||
* This is a dirty hack until I can fully track down where
|
||||
* the dual-encoding happens
|
||||
*
|
||||
* @param FormBody $formBody The form builder object to fix
|
||||
* @return string
|
||||
*/
|
||||
private function fixBody(FormBody $formBody): string
|
||||
{
|
||||
$rawBody = \Amp\wait($formBody->getBody());
|
||||
return html_entity_decode($rawBody, \ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a request object
|
||||
*
|
||||
|
@ -17,10 +17,13 @@
|
||||
namespace Aviat\AnimeClient\API\MAL;
|
||||
|
||||
use Amp\Artax\Request;
|
||||
use Aviat\AnimeClient\API\MAL\ListItem;
|
||||
use Aviat\AnimeClient\API\MAL\Transformer\AnimeListTransformer;
|
||||
use Aviat\AnimeClient\API\MAL\{
|
||||
ListItem,
|
||||
Transformer\AnimeListTransformer,
|
||||
Transformer\MangaListTransformer
|
||||
};
|
||||
use Aviat\AnimeClient\API\XML;
|
||||
use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
|
||||
use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus};
|
||||
use Aviat\Ion\Di\ContainerAware;
|
||||
|
||||
/**
|
||||
@ -48,15 +51,25 @@ class Model {
|
||||
public function __construct(ListItem $listItem)
|
||||
{
|
||||
$this->animeListTransformer = new AnimeListTransformer();
|
||||
$this->mangaListTransformer = new MangaListTransformer();
|
||||
$this->listItem = $listItem;
|
||||
}
|
||||
|
||||
public function createFullListItem(array $data): Request
|
||||
/**
|
||||
* Create a list item on MAL
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $type "anime" or "manga"
|
||||
* @return Request
|
||||
*/
|
||||
public function createFullListItem(array $data, string $type = 'anime'): Request
|
||||
{
|
||||
return $this->listItem->create($data);
|
||||
return $this->listItem->create($data, $type);
|
||||
}
|
||||
|
||||
public function createListItem(array $data): Request
|
||||
public function createListItem(array $data, string $type = 'anime'): Request
|
||||
{
|
||||
if ($type === 'anime')
|
||||
{
|
||||
$createData = [
|
||||
'id' => $data['id'],
|
||||
@ -64,11 +77,55 @@ class Model {
|
||||
'status' => AnimeWatchingStatus::KITSU_TO_MAL[$data['status']]
|
||||
]
|
||||
];
|
||||
|
||||
return $this->listItem->create($createData);
|
||||
}
|
||||
elseif ($type === 'manga')
|
||||
{
|
||||
$createData = [
|
||||
'id' => $data['id'],
|
||||
'data' => [
|
||||
'status' => MangaReadingStatus::KITSU_TO_MAL[$data['status']]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function getFullList(): array
|
||||
return $this->listItem->create($createData, $type);
|
||||
}
|
||||
|
||||
public function getMangaList(): array
|
||||
{
|
||||
return $this->getList('manga');
|
||||
}
|
||||
|
||||
public function getAnimeList(): array
|
||||
{
|
||||
return $this->getList('anime');
|
||||
}
|
||||
|
||||
public function getListItem(string $listId): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function updateListItem(array $data, string $type = 'anime'): Request
|
||||
{
|
||||
if ($type === 'anime')
|
||||
{
|
||||
$updateData = $this->animeListTransformer->untransform($data);
|
||||
}
|
||||
else if ($type === 'manga')
|
||||
{
|
||||
$updateData = $this->mangaListTransformer->untransform($data);
|
||||
}
|
||||
|
||||
return $this->listItem->update($updateData['id'], $updateData['data'], $type);
|
||||
}
|
||||
|
||||
public function deleteListItem(string $id, string $type = 'anime'): Request
|
||||
{
|
||||
return $this->listItem->delete($id, $type);
|
||||
}
|
||||
|
||||
private function getList(string $type): array
|
||||
{
|
||||
$config = $this->container->get('config');
|
||||
$userName = $config->get(['mal', 'username']);
|
||||
@ -78,26 +135,11 @@ class Model {
|
||||
],
|
||||
'query' => [
|
||||
'u' => $userName,
|
||||
'status' => 'all'
|
||||
'status' => 'all',
|
||||
'type' => $type
|
||||
]
|
||||
]);
|
||||
|
||||
return $list['myanimelist']['anime'];
|
||||
}
|
||||
|
||||
public function getListItem(string $listId): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function updateListItem(array $data): Request
|
||||
{
|
||||
$updateData = $this->animeListTransformer->untransform($data);
|
||||
return $this->listItem->update($updateData['id'], $updateData['data']);
|
||||
}
|
||||
|
||||
public function deleteListItem(string $id): Request
|
||||
{
|
||||
return $this->listItem->delete($id);
|
||||
return $list['myanimelist'][$type];
|
||||
}
|
||||
}
|
@ -24,26 +24,14 @@ use Aviat\Ion\Transformer\AbstractTransformer;
|
||||
*/
|
||||
class AnimeListTransformer extends AbstractTransformer {
|
||||
/**
|
||||
* Transform MAL episode data to Kitsu episode data
|
||||
* Identity transformation
|
||||
*
|
||||
* @param array $item
|
||||
* @return array
|
||||
*/
|
||||
public function transform($item)
|
||||
{
|
||||
$rewatching = (array_key_exists('rewatching', $item) && $item['rewatching']);
|
||||
|
||||
return [
|
||||
'id' => $item['mal_id'],
|
||||
'data' => [
|
||||
'status' => AnimeWatchingStatus::KITSU_TO_MAL[$item['watching_status']],
|
||||
'rating' => $item['user_rating'],
|
||||
'rewatch_value' => (int) $rewatching,
|
||||
'times_rewatched' => $item['rewatched'],
|
||||
'comments' => $item['notes'],
|
||||
'episode' => $item['episodes_watched']
|
||||
]
|
||||
];
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
|
85
src/API/MAL/Transformer/MangaListTransformer.php
Normal file
85
src/API/MAL/Transformer/MangaListTransformer.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?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 - 2017 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\Transformer;
|
||||
|
||||
use Aviat\AnimeClient\API\Mapping\MangaReadingStatus;
|
||||
use Aviat\Ion\Transformer\AbstractTransformer;
|
||||
|
||||
/**
|
||||
* Transformer for updating MAL List
|
||||
*/
|
||||
class MangaListTransformer extends AbstractTransformer {
|
||||
/**
|
||||
* Identity transformation
|
||||
*
|
||||
* @param array $item
|
||||
* @return array
|
||||
*/
|
||||
public function transform($item)
|
||||
{
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform Kitsu data to MAL data
|
||||
*
|
||||
* @param array $item
|
||||
* @return array
|
||||
*/
|
||||
public function untransform(array $item): array
|
||||
{
|
||||
$map = [
|
||||
'id' => $item['mal_id'],
|
||||
'data' => [
|
||||
'chapter' => $item['data']['progress']
|
||||
]
|
||||
];
|
||||
|
||||
$data =& $item['data'];
|
||||
|
||||
foreach($item['data'] as $key => $value)
|
||||
{
|
||||
switch($key)
|
||||
{
|
||||
case 'notes':
|
||||
$map['data']['comments'] = $value;
|
||||
break;
|
||||
|
||||
case 'rating':
|
||||
$map['data']['score'] = $value * 2;
|
||||
break;
|
||||
|
||||
case 'reconsuming':
|
||||
$map['data']['enable_rereading'] = (bool) $value;
|
||||
break;
|
||||
|
||||
case 'reconsumeCount':
|
||||
$map['data']['times_reread'] = $value;
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
$map['data']['status'] = MangaReadingStatus::KITSU_TO_MAL[$value];
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@ use Aviat\Ion\Enum;
|
||||
* and url route segments
|
||||
*/
|
||||
class MangaReadingStatus extends Enum {
|
||||
const MAL_TO_KITSU = [
|
||||
const KITSU_TO_MAL = [
|
||||
Kitsu::READING => MAL::READING,
|
||||
Kitsu::PLAN_TO_READ => MAL::PLAN_TO_READ,
|
||||
Kitsu::COMPLETED => MAL::COMPLETED,
|
||||
@ -37,12 +37,17 @@ class MangaReadingStatus extends Enum {
|
||||
Kitsu::DROPPED => MAL::DROPPED
|
||||
];
|
||||
|
||||
const KITSU_TO_MAL = [
|
||||
const MAL_TO_KITSU = [
|
||||
'1' => Kitsu::READING,
|
||||
'2' => Kitsu::COMPLETED,
|
||||
'3' => Kitsu::ON_HOLD,
|
||||
'4' => Kitsu::DROPPED,
|
||||
'6' => Kitsu::PLAN_TO_READ,
|
||||
MAL::READING => Kitsu::READING,
|
||||
MAL::PLAN_TO_READ => Kitsu::PLAN_TO_READ,
|
||||
MAL::COMPLETED => Kitsu::COMPLETED,
|
||||
MAL::ON_HOLD => Kitsu::ON_HOLD,
|
||||
MAL::DROPPED => Kitsu::DROPPED
|
||||
MAL::DROPPED => Kitsu::DROPPED,
|
||||
MAL::PLAN_TO_READ => Kitsu::PLAN_TO_READ,
|
||||
];
|
||||
|
||||
const KITSU_TO_TITLE = [
|
||||
@ -50,7 +55,7 @@ class MangaReadingStatus extends Enum {
|
||||
Kitsu::PLAN_TO_READ => Title::PLAN_TO_READ,
|
||||
Kitsu::COMPLETED => Title::COMPLETED,
|
||||
Kitsu::ON_HOLD => Title::ON_HOLD,
|
||||
Kitsu::DROPPED => Title::DROPPED
|
||||
Kitsu::DROPPED => Title::DROPPED,
|
||||
];
|
||||
|
||||
const ROUTE_TO_KITSU = [
|
||||
@ -58,7 +63,7 @@ class MangaReadingStatus extends Enum {
|
||||
Route::READING => Kitsu::READING,
|
||||
Route::COMPLETED => Kitsu::COMPLETED,
|
||||
Route::DROPPED => Kitsu::DROPPED,
|
||||
Route::ON_HOLD => Kitsu::ON_HOLD
|
||||
Route::ON_HOLD => Kitsu::ON_HOLD,
|
||||
];
|
||||
|
||||
const ROUTE_TO_TITLE = [
|
||||
@ -67,7 +72,7 @@ class MangaReadingStatus extends Enum {
|
||||
Route::READING => Title::READING,
|
||||
Route::COMPLETED => Title::COMPLETED,
|
||||
Route::DROPPED => Title::DROPPED,
|
||||
Route::ON_HOLD => Title::ON_HOLD
|
||||
Route::ON_HOLD => Title::ON_HOLD,
|
||||
];
|
||||
|
||||
const TITLE_TO_KITSU = [
|
||||
@ -75,6 +80,6 @@ class MangaReadingStatus extends Enum {
|
||||
Title::READING => Kitsu::READING,
|
||||
Title::COMPLETED => Kitsu::COMPLETED,
|
||||
Title::DROPPED => Kitsu::DROPPED,
|
||||
Title::ON_HOLD => Kitsu::ON_HOLD
|
||||
Title::ON_HOLD => Kitsu::ON_HOLD,
|
||||
];
|
||||
}
|
@ -21,25 +21,14 @@ use Yosymfony\Toml\Toml;
|
||||
define('SRC_DIR', realpath(__DIR__));
|
||||
|
||||
const SESSION_SEGMENT = 'Aviat\AnimeClient\Auth';
|
||||
const DEFAULT_CONTROLLER = 'Aviat\AnimeClient\Controller\Index';
|
||||
const DEFAULT_CONTROLLER_NAMESPACE = 'Aviat\AnimeClient\Controller';
|
||||
const DEFAULT_CONTROLLER = 'Aviat\AnimeClient\Controller\Anime';
|
||||
const DEFAULT_LIST_CONTROLLER = 'Aviat\AnimeClient\Controller\Anime';
|
||||
const DEFAULT_CONTROLLER_METHOD = 'index';
|
||||
const NOT_FOUND_METHOD = 'notFound';
|
||||
const ERROR_MESSAGE_METHOD = 'errorPage';
|
||||
const SRC_DIR = SRC_DIR;
|
||||
|
||||
/**
|
||||
* Joins paths together. Variadic to take an
|
||||
* arbitrary number of arguments
|
||||
*
|
||||
* @param string[] ...$args
|
||||
* @return string
|
||||
*/
|
||||
function _dir(...$args)
|
||||
{
|
||||
return implode(DIRECTORY_SEPARATOR, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration options from .toml files
|
||||
*
|
||||
|
@ -19,8 +19,16 @@ namespace Aviat\AnimeClient\Command;
|
||||
use function Amp\{all, wait};
|
||||
|
||||
use Amp\Artax\Client;
|
||||
use Aviat\AnimeClient\API\{JsonAPI, Mapping\AnimeWatchingStatus};
|
||||
use Aviat\AnimeClient\API\MAL\Transformer\AnimeListTransformer as ALT;
|
||||
use Aviat\AnimeClient\API\{
|
||||
JsonAPI,
|
||||
ParallelAPIRequest,
|
||||
Mapping\AnimeWatchingStatus,
|
||||
Mapping\MangaReadingStatus
|
||||
};
|
||||
use Aviat\AnimeClient\API\MAL\Transformer\{
|
||||
AnimeListTransformer as ALT,
|
||||
MangaListTransformer as MLT
|
||||
};
|
||||
use Aviat\Ion\Json;
|
||||
|
||||
/**
|
||||
@ -55,63 +63,71 @@ class SyncKitsuWithMal extends BaseCommand {
|
||||
$this->kitsuModel = $this->container->get('kitsu-model');
|
||||
$this->malModel = $this->container->get('mal-model');
|
||||
|
||||
$malCount = count($this->getMALAnimeList());
|
||||
$kitsuCount = $this->getKitsuAnimeListPageCount();
|
||||
$this->syncAnime();
|
||||
$this->syncManga();
|
||||
}
|
||||
|
||||
$this->echoBox("Number of MAL list items: {$malCount}");
|
||||
$this->echoBox("Number of Kitsu list items: {$kitsuCount}");
|
||||
public function syncAnime()
|
||||
{
|
||||
$malCount = count($this->malModel->getAnimeList());
|
||||
$kitsuCount = $this->kitsuModel->getAnimeListCount();
|
||||
|
||||
$this->echoBox("Number of MAL anime list items: {$malCount}");
|
||||
$this->echoBox("Number of Kitsu anime list items: {$kitsuCount}");
|
||||
|
||||
$data = $this->diffAnimeLists();
|
||||
$this->echoBox("Number of items that need to be added to MAL: " . count($data));
|
||||
|
||||
$this->echoBox("Number of anime items that need to be added to MAL: " . count($data['addToMAL']));
|
||||
|
||||
if ( ! empty($data['addToMAL']))
|
||||
{
|
||||
$this->echoBox("Adding missing list items to MAL");
|
||||
$this->createMALAnimeListItems($data['addToMAL']);
|
||||
}
|
||||
$this->echoBox("Adding missing anime list items to MAL");
|
||||
$this->createMALListItems($data['addToMAL'], 'anime');
|
||||
}
|
||||
|
||||
public function getKitsuAnimeList()
|
||||
$this->echoBox('Number of anime items that need to be added to Kitsu: ' . count($data['addToKitsu']));
|
||||
|
||||
if ( ! empty($data['addToKitsu']))
|
||||
{
|
||||
$count = $this->getKitsuAnimeListPageCount();
|
||||
$size = 100;
|
||||
$pages = ceil($count / $size);
|
||||
$this->echoBox("Adding missing anime list items to Kitsu");
|
||||
$this->createKitusListItems($data['addToKitsu'], 'anime');
|
||||
}
|
||||
}
|
||||
|
||||
$requests = [];
|
||||
|
||||
// Set up requests
|
||||
for ($i = 0; $i < $pages; $i++)
|
||||
public function syncManga()
|
||||
{
|
||||
$offset = $i * $size;
|
||||
$requests[] = $this->kitsuModel->getPagedAnimeList($size, $offset);
|
||||
}
|
||||
$malCount = count($this->malModel->getMangaList());
|
||||
$kitsuCount = $this->kitsuModel->getMangaListCount();
|
||||
|
||||
$promiseArray = (new Client())->requestMulti($requests);
|
||||
$this->echoBox("Number of MAL manga list items: {$malCount}");
|
||||
$this->echoBox("Number of Kitsu manga list items: {$kitsuCount}");
|
||||
|
||||
$responses = wait(all($promiseArray));
|
||||
$output = [];
|
||||
$data = $this->diffMangaLists();
|
||||
|
||||
foreach($responses as $response)
|
||||
$this->echoBox("Number of manga items that need to be added to MAL: " . count($data['addToMAL']));
|
||||
|
||||
if ( ! empty($data['addToMAL']))
|
||||
{
|
||||
$data = Json::decode($response->getBody());
|
||||
$output = array_merge_recursive($output, $data);
|
||||
$this->echoBox("Adding missing manga list items to MAL");
|
||||
$this->createMALListItems($data['addToMAL'], 'manga');
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
$this->echoBox('Number of manga items that need to be added to Kitsu: ' . count($data['addToKitsu']));
|
||||
|
||||
public function getMALAnimeList()
|
||||
if ( ! empty($data['addToKitsu']))
|
||||
{
|
||||
return $this->malModel->getFullList();
|
||||
$this->echoBox("Adding missing manga list items to Kitsu");
|
||||
$this->createKitsuListItems($data['addToKitsu'], 'manga');
|
||||
}
|
||||
}
|
||||
|
||||
public function filterMappings(array $includes): array
|
||||
public function filterMappings(array $includes, string $type = 'anime'): array
|
||||
{
|
||||
$output = [];
|
||||
|
||||
foreach($includes as $id => $mapping)
|
||||
{
|
||||
if ($mapping['externalSite'] === 'myanimelist/anime')
|
||||
if ($mapping['externalSite'] === "myanimelist/{$type}")
|
||||
{
|
||||
$output[$id] = $mapping;
|
||||
}
|
||||
@ -122,7 +138,7 @@ class SyncKitsuWithMal extends BaseCommand {
|
||||
|
||||
public function formatMALAnimeList()
|
||||
{
|
||||
$orig = $this->getMALAnimeList();
|
||||
$orig = $this->malModel->getAnimeList();
|
||||
$output = [];
|
||||
|
||||
foreach($orig as $item)
|
||||
@ -137,7 +153,37 @@ class SyncKitsuWithMal extends BaseCommand {
|
||||
? $item['times_rewatched']
|
||||
: 0,
|
||||
// 'notes' => ,
|
||||
'rating' => $item['my_score'],
|
||||
'rating' => $item['my_score'] / 2,
|
||||
'updatedAt' => (new \DateTime())
|
||||
->setTimestamp((int)$item['my_last_updated'])
|
||||
->format(\DateTime::W3C),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function formatMALMangaList()
|
||||
{
|
||||
$orig = $this->malModel->getMangaList();
|
||||
$output = [];
|
||||
|
||||
foreach($orig as $item)
|
||||
{
|
||||
$output[$item['series_mangadb_id']] = [
|
||||
'id' => $item['series_mangadb_id'],
|
||||
'data' => [
|
||||
'my_status' => $item['my_status'],
|
||||
'status' => MangaReadingStatus::MAL_TO_KITSU[$item['my_status']],
|
||||
'progress' => $item['my_read_chapters'],
|
||||
'volumes' => $item['my_read_volumes'],
|
||||
'reconsuming' => (bool) $item['my_rereadingg'],
|
||||
/* 'reconsumeCount' => array_key_exists('times_rewatched', $item)
|
||||
? $item['times_rewatched']
|
||||
: 0, */
|
||||
// 'notes' => ,
|
||||
'rating' => $item['my_score'] / 2,
|
||||
'updatedAt' => (new \DateTime())
|
||||
->setTimestamp((int)$item['my_last_updated'])
|
||||
->format(\DateTime::W3C),
|
||||
@ -186,9 +232,84 @@ class SyncKitsuWithMal extends BaseCommand {
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function getKitsuAnimeListPageCount()
|
||||
public function filterKitsuMangaList()
|
||||
{
|
||||
return $this->kitsuModel->getAnimeListCount();
|
||||
$data = $this->kitsuModel->getFullMangaList();
|
||||
$includes = JsonAPI::organizeIncludes($data['included']);
|
||||
$includes['mappings'] = $this->filterMappings($includes['mappings'], 'manga');
|
||||
|
||||
$output = [];
|
||||
|
||||
foreach($data['data'] as $listItem)
|
||||
{
|
||||
$mangaId = $listItem['relationships']['manga']['data']['id'];
|
||||
$potentialMappings = $includes['manga'][$mangaId]['relationships']['mappings'];
|
||||
$malId = NULL;
|
||||
|
||||
foreach ($potentialMappings as $mappingId)
|
||||
{
|
||||
if (array_key_exists($mappingId, $includes['mappings']))
|
||||
{
|
||||
$malId = $includes['mappings'][$mappingId]['externalId'];
|
||||
}
|
||||
}
|
||||
|
||||
// Skip to the next item if there isn't a MAL ID
|
||||
if (is_null($malId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$output[$listItem['id']] = [
|
||||
'id' => $listItem['id'],
|
||||
'malId' => $malId,
|
||||
'data' => $listItem['attributes'],
|
||||
];
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function diffMangaLists()
|
||||
{
|
||||
$kitsuList = $this->filterKitsuMangaList();
|
||||
$malList = $this->formatMALMangaList();
|
||||
|
||||
$itemsToAddToMAL = [];
|
||||
$itemsToAddToKitsu = [];
|
||||
|
||||
$malIds = array_column($malList, 'id');
|
||||
$kitsuMalIds = array_column($kitsuList, 'malId');
|
||||
$missingMalIds = array_diff($malIds, $kitsuMalIds);
|
||||
|
||||
foreach($missingMalIds as $mid)
|
||||
{
|
||||
$itemsToAddToKitsu[] = array_merge($malList[$mid]['data'], [
|
||||
'id' => $this->kitsuModel->getKitsuIdFromMALId($mid, 'manga'),
|
||||
'type' => 'manga'
|
||||
]);
|
||||
}
|
||||
|
||||
foreach($kitsuList as $kitsuItem)
|
||||
{
|
||||
if (in_array($kitsuItem['malId'], $malIds))
|
||||
{
|
||||
// Eventually, compare the list entries, and determine which
|
||||
// needs to be updated
|
||||
continue;
|
||||
}
|
||||
|
||||
// Looks like this item only exists on Kitsu
|
||||
$itemsToAddToMAL[] = [
|
||||
'mal_id' => $kitsuItem['malId'],
|
||||
'data' => $kitsuItem['data']
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'addToMAL' => $itemsToAddToMAL,
|
||||
'addToKitsu' => $itemsToAddToKitsu
|
||||
];
|
||||
}
|
||||
|
||||
public function diffAnimeLists()
|
||||
@ -201,10 +322,26 @@ class SyncKitsuWithMal extends BaseCommand {
|
||||
$malList = $this->formatMALAnimeList();
|
||||
|
||||
$itemsToAddToMAL = [];
|
||||
$itemsToAddToKitsu = [];
|
||||
$malUpdateItems = [];
|
||||
$kitsuUpdateItems = [];
|
||||
|
||||
$malIds = array_column($malList, 'id');
|
||||
$kitsuMalIds = array_column($kitsuList, 'malId');
|
||||
$missingMalIds = array_diff($malIds, $kitsuMalIds);
|
||||
|
||||
foreach($missingMalIds as $mid)
|
||||
{
|
||||
// print_r($malList[$mid]);
|
||||
$itemsToAddToKitsu[] = array_merge($malList[$mid]['data'], [
|
||||
'id' => $this->kitsuModel->getKitsuIdFromMALId($mid),
|
||||
'type' => 'anime'
|
||||
]);
|
||||
}
|
||||
|
||||
foreach($kitsuList as $kitsuItem)
|
||||
{
|
||||
if (array_key_exists($kitsuItem['malId'], $malList))
|
||||
if (in_array($kitsuItem['malId'], $malIds))
|
||||
{
|
||||
// Eventually, compare the list entries, and determine which
|
||||
// needs to be updated
|
||||
@ -230,34 +367,60 @@ class SyncKitsuWithMal extends BaseCommand {
|
||||
|
||||
return [
|
||||
'addToMAL' => $itemsToAddToMAL,
|
||||
'updateMAL' => $malUpdateItems,
|
||||
'addToKitsu' => $itemsToAddToKitsu,
|
||||
'updateKitsu' => $kitsuUpdateItems
|
||||
];
|
||||
}
|
||||
|
||||
public function createMALAnimeListItems($itemsToAdd)
|
||||
public function createKitusAnimeListItems($itemsToAdd, $type = 'anime')
|
||||
{
|
||||
$requester = new ParallelAPIRequest();
|
||||
foreach($itemsToAdd as $item)
|
||||
{
|
||||
$requester->addRequest($this->kitsuModel->createListItem($item));
|
||||
}
|
||||
|
||||
$responses = $requester->makeRequests();
|
||||
|
||||
foreach($responses as $key => $response)
|
||||
{
|
||||
$id = $itemsToAdd[$key]['id'];
|
||||
if ($response->getStatus() === 201)
|
||||
{
|
||||
$this->echoBox("Successfully created Kitsu {$type} list item with id: {$id}");
|
||||
}
|
||||
else
|
||||
{
|
||||
echo $response->getBody();
|
||||
$this->echoBox("Failed to create Kitsu {$type} list item with id: {$id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function createMALListItems($itemsToAdd, $type = 'anime')
|
||||
{
|
||||
$transformer = new ALT();
|
||||
$requests = [];
|
||||
$requester = new ParallelAPIRequest();
|
||||
|
||||
foreach($itemsToAdd as $item)
|
||||
{
|
||||
$data = $transformer->untransform($item);
|
||||
$requests[] = $this->malModel->createFullListItem($data);
|
||||
$requester->addRequest($this->malModel->createFullListItem($data, $type));
|
||||
}
|
||||
|
||||
$promiseArray = (new Client())->requestMulti($requests);
|
||||
|
||||
$responses = wait(all($promiseArray));
|
||||
$responses = $requester->makeRequests();
|
||||
|
||||
foreach($responses as $key => $response)
|
||||
{
|
||||
$id = $itemsToAdd[$key]['mal_id'];
|
||||
if ($response->getBody() === 'Created')
|
||||
{
|
||||
$this->echoBox("Successfully create list item with id: {$id}");
|
||||
$this->echoBox("Successfully created MAL {$type} list item with id: {$id}");
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->echoBox("Failed to create list item with id: {$id}");
|
||||
$this->echoBox("Failed to create MAL {$type} list item with id: {$id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ namespace Aviat\AnimeClient;
|
||||
|
||||
use const Aviat\AnimeClient\SESSION_SEGMENT;
|
||||
|
||||
use function Aviat\AnimeClient\_dir;
|
||||
use function Aviat\Ion\_dir;
|
||||
|
||||
use Aviat\AnimeClient\API\JsonAPI;
|
||||
use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
|
||||
@ -31,7 +31,66 @@ use InvalidArgumentException;
|
||||
* @property Response object $response
|
||||
*/
|
||||
class Controller {
|
||||
use ControllerTrait;
|
||||
|
||||
use ContainerAware;
|
||||
|
||||
/**
|
||||
* Cache manager
|
||||
* @var \Psr\Cache\CacheItemPoolInterface
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* The global configuration object
|
||||
* @var \Aviat\Ion\ConfigInterface $config
|
||||
*/
|
||||
public $config;
|
||||
|
||||
/**
|
||||
* Request object
|
||||
* @var object $request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* Response object
|
||||
* @var object $response
|
||||
*/
|
||||
public $response;
|
||||
|
||||
/**
|
||||
* The api model for the current controller
|
||||
* @var object
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* Url generation class
|
||||
* @var UrlGenerator
|
||||
*/
|
||||
protected $urlGenerator;
|
||||
|
||||
/**
|
||||
* Aura url generator
|
||||
* @var \Aura\Router\Generator
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* Session segment
|
||||
* @var \Aura\Session\Segment
|
||||
*/
|
||||
protected $session;
|
||||
|
||||
/**
|
||||
* Common data to be sent to views
|
||||
* @var array
|
||||
*/
|
||||
protected $baseData = [
|
||||
'url_type' => 'anime',
|
||||
'other_type' => 'manga',
|
||||
'menu_name' => ''
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@ -55,6 +114,7 @@ class Controller {
|
||||
'config' => $this->config
|
||||
]);
|
||||
|
||||
$this->url = $auraUrlGenerator;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
|
||||
$session = $container->get('session');
|
||||
@ -72,81 +132,267 @@ class Controller {
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the user profile page
|
||||
* Redirect to the previous page
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function me()
|
||||
public function redirectToPrevious()
|
||||
{
|
||||
$username = $this->config->get(['kitsu_username']);
|
||||
$model = $this->container->get('kitsu-model');
|
||||
$data = $model->getUserData($username);
|
||||
$included = JsonAPI::lightlyOrganizeIncludes($data['included']);
|
||||
$relationships = JsonAPI::fillRelationshipsFromIncludes($data['data']['relationships'], $included);
|
||||
$this->outputHTML('me', [
|
||||
'title' => 'About' . $this->config->get('whose_list'),
|
||||
'attributes' => $data['data']['attributes'],
|
||||
'relationships' => $relationships,
|
||||
'included' => $included
|
||||
$previous = $this->session->getFlash('previous');
|
||||
$this->redirect($previous, 303);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current url in the session as the target of a future redirect
|
||||
*
|
||||
* @param string|null $url
|
||||
* @return void
|
||||
*/
|
||||
public function setSessionRedirect(string $url = NULL)
|
||||
{
|
||||
$serverParams = $this->request->getServerParams();
|
||||
|
||||
if ( ! array_key_exists('HTTP_REFERER', $serverParams))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$util = $this->container->get('util');
|
||||
$doubleFormPage = $serverParams['HTTP_REFERER'] === $this->request->getUri();
|
||||
|
||||
// Don't attempt to set the redirect url if
|
||||
// the page is one of the form type pages,
|
||||
// and the previous page is also a form type page_segments
|
||||
if ($doubleFormPage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_null($url))
|
||||
{
|
||||
$url = $util->isViewPage()
|
||||
? $this->request->url->get()
|
||||
: $serverParams['HTTP_REFERER'];
|
||||
}
|
||||
|
||||
$this->session->set('redirect_url', $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the url previously set in the session
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function sessionRedirect()
|
||||
{
|
||||
$target = $this->session->get('redirect_url');
|
||||
if (empty($target))
|
||||
{
|
||||
$this->notFound();
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->redirect($target, 303);
|
||||
$this->session->set('redirect_url', NULL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string output of a partial template
|
||||
*
|
||||
* @param HtmlView $view
|
||||
* @param string $template
|
||||
* @param array $data
|
||||
* @throws InvalidArgumentException
|
||||
* @return string
|
||||
*/
|
||||
protected function loadPartial($view, string $template, array $data = [])
|
||||
{
|
||||
$router = $this->container->get('dispatcher');
|
||||
|
||||
if (isset($this->baseData))
|
||||
{
|
||||
$data = array_merge($this->baseData, $data);
|
||||
}
|
||||
|
||||
$route = $router->getRoute();
|
||||
$data['route_path'] = $route ? $router->getRoute()->path : '';
|
||||
|
||||
|
||||
$templatePath = _dir($this->config->get('view_path'), "{$template}.php");
|
||||
|
||||
if ( ! is_file($templatePath))
|
||||
{
|
||||
throw new InvalidArgumentException("Invalid template : {$template}");
|
||||
}
|
||||
|
||||
return $view->renderTemplate($templatePath, (array)$data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a template with header and footer
|
||||
*
|
||||
* @param HtmlView $view
|
||||
* @param string $template
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
protected function renderFullPage($view, string $template, array $data)
|
||||
{
|
||||
$view->appendOutput($this->loadPartial($view, 'header', $data));
|
||||
|
||||
if (array_key_exists('message', $data) && is_array($data['message']))
|
||||
{
|
||||
$view->appendOutput($this->loadPartial($view, 'message', $data['message']));
|
||||
}
|
||||
|
||||
$view->appendOutput($this->loadPartial($view, $template, $data));
|
||||
$view->appendOutput($this->loadPartial($view, 'footer', $data));
|
||||
}
|
||||
|
||||
/**
|
||||
* 404 action
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function notFound(
|
||||
string $title = 'Sorry, page not found',
|
||||
string $message = 'Page Not Found'
|
||||
)
|
||||
{
|
||||
$this->outputHTML('404', [
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
], NULL, 404);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a generic error page
|
||||
*
|
||||
* @param int $httpCode
|
||||
* @param string $title
|
||||
* @param string $message
|
||||
* @param string $long_message
|
||||
* @return void
|
||||
*/
|
||||
public function errorPage(int $httpCode, string $title, string $message, string $long_message = "")
|
||||
{
|
||||
$this->outputHTML('error', [
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
'long_message' => $long_message
|
||||
], NULL, $httpCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the default controller/url from an empty path
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function redirectToDefaultRoute()
|
||||
{
|
||||
$defaultType = $this->config->get(['routes', 'route_config', 'default_list']) ?? 'anime';
|
||||
$this->redirect($this->urlGenerator->defaultUrl($defaultType), 303);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a session flash variable to display a message on
|
||||
* next page load
|
||||
*
|
||||
* @param string $message
|
||||
* @param string $type
|
||||
* @return void
|
||||
*/
|
||||
public function setFlashMessage(string $message, string $type = "info")
|
||||
{
|
||||
static $messages;
|
||||
|
||||
if ( ! $messages)
|
||||
{
|
||||
$messages = [];
|
||||
}
|
||||
|
||||
$messages[] = [
|
||||
'message_type' => $type,
|
||||
'message' => $message
|
||||
];
|
||||
|
||||
$this->session->setFlash('message', $messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for consistent page titles
|
||||
*
|
||||
* @param string ...$parts Title segements
|
||||
* @return string
|
||||
*/
|
||||
public function formatTitle(string ...$parts) : string
|
||||
{
|
||||
return implode(' · ', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message box to the page
|
||||
*
|
||||
* @param HtmlView $view
|
||||
* @param string $type
|
||||
* @param string $message
|
||||
* @return string
|
||||
*/
|
||||
protected function showMessage($view, string $type, string $message): string
|
||||
{
|
||||
return $this->loadPartial($view, 'message', [
|
||||
'message_type' => $type,
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the login form
|
||||
* Output a template to HTML, using the provided data
|
||||
*
|
||||
* @param string $status
|
||||
* @param string $template
|
||||
* @param array $data
|
||||
* @param HtmlView|null $view
|
||||
* @param int $code
|
||||
* @return void
|
||||
*/
|
||||
public function login(string $status = '')
|
||||
protected function outputHTML(string $template, array $data = [], $view = NULL, int $code = 200)
|
||||
{
|
||||
if (is_null($view))
|
||||
{
|
||||
$message = '';
|
||||
|
||||
$view = new HtmlView($this->container);
|
||||
|
||||
if ($status !== '')
|
||||
{
|
||||
$message = $this->showMessage($view, 'error', $status);
|
||||
}
|
||||
|
||||
// Set the redirect url
|
||||
$this->setSessionRedirect();
|
||||
|
||||
$this->outputHTML('login', [
|
||||
'title' => 'Api login',
|
||||
'message' => $message
|
||||
], $view);
|
||||
$view->setStatusCode($code);
|
||||
$this->renderFullPage($view, $template, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt login authentication
|
||||
* Output a JSON Response
|
||||
*
|
||||
* @param mixed $data
|
||||
* @param int $code - the http status code
|
||||
* @return void
|
||||
*/
|
||||
public function loginAction()
|
||||
protected function outputJSON($data = 'Empty response', int $code = 200)
|
||||
{
|
||||
$auth = $this->container->get('auth');
|
||||
$post = $this->request->getParsedBody();
|
||||
if ($auth->authenticate($post['password']))
|
||||
{
|
||||
$this->sessionRedirect();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setFlashMessage('Invalid username or password.');
|
||||
$this->redirect($this->urlGenerator->url('login'), 303);
|
||||
(new JsonView($this->container))
|
||||
->setStatusCode($code)
|
||||
->setOutput($data)
|
||||
->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deauthorize the current user
|
||||
* Redirect to the selected page
|
||||
*
|
||||
* @param string $url
|
||||
* @param int $code
|
||||
* @return void
|
||||
*/
|
||||
public function logout()
|
||||
protected function redirect(string $url, int $code)
|
||||
{
|
||||
$auth = $this->container->get('auth');
|
||||
$auth->logout();
|
||||
|
||||
$this->redirectToDefaultRoute();
|
||||
$http = new HttpView($this->container);
|
||||
$http->redirect($url, $code);
|
||||
}
|
||||
}
|
||||
// End of BaseController.php
|
@ -116,7 +116,7 @@ class Anime extends BaseController {
|
||||
$this->config->get('whose_list') . "'s Anime List",
|
||||
'Add'
|
||||
),
|
||||
'action_url' => $this->urlGenerator->url('anime/add'),
|
||||
'action_url' => $this->url->generate('anime.add.post'),
|
||||
'status_list' => AnimeWatchingStatus::KITSU_TO_TITLE
|
||||
]);
|
||||
}
|
||||
@ -168,8 +168,9 @@ class Anime extends BaseController {
|
||||
),
|
||||
'item' => $item,
|
||||
'statuses' => AnimeWatchingStatus::KITSU_TO_TITLE,
|
||||
'action' => $this->container->get('url-generator')
|
||||
->url('/anime/update_form'),
|
||||
'action' => $this->url->generate('update.post', [
|
||||
'controller' => 'anime'
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -41,18 +41,6 @@ class Collection extends BaseController {
|
||||
*/
|
||||
private $animeModel;
|
||||
|
||||
/**
|
||||
* Data to be sent to all routes in this controller
|
||||
* @var array $baseData
|
||||
*/
|
||||
protected $baseData;
|
||||
|
||||
/**
|
||||
* Url Generator class
|
||||
* @var UrlGenerator
|
||||
*/
|
||||
protected $urlGenerator;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
@ -62,7 +50,6 @@ class Collection extends BaseController {
|
||||
{
|
||||
parent::__construct($container);
|
||||
|
||||
$this->urlGenerator = $container->get('url-generator');
|
||||
$this->animeModel = $container->get('anime-model');
|
||||
$this->animeCollectionModel = $container->get('anime-collection-model');
|
||||
$this->baseData = array_merge($this->baseData, [
|
||||
@ -118,10 +105,11 @@ class Collection extends BaseController {
|
||||
$this->setSessionRedirect();
|
||||
|
||||
$action = (is_null($id)) ? "Add" : "Edit";
|
||||
$urlAction = strtolower($action);
|
||||
|
||||
$this->outputHTML('collection/' . strtolower($action), [
|
||||
$this->outputHTML('collection/' . $urlAction, [
|
||||
'action' => $action,
|
||||
'action_url' => $this->urlGenerator->fullUrl('collection/' . strtolower($action)),
|
||||
'action_url' => $this->url->generate("collection.{$urlAction}.post"),
|
||||
'title' => $this->formatTitle(
|
||||
$this->config->get('whose_list') . "'s Anime Collection",
|
||||
$action
|
||||
|
138
src/Controller/Index.php
Normal file
138
src/Controller/Index.php
Normal file
@ -0,0 +1,138 @@
|
||||
<?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 - 2017 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\Controller;
|
||||
|
||||
use Aviat\AnimeClient\Controller as BaseController;
|
||||
use Aviat\AnimeClient\API\JsonAPI;
|
||||
use Aviat\Ion\View\HtmlView;
|
||||
|
||||
class Index extends BaseController {
|
||||
|
||||
/**
|
||||
* Purges the API cache
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearCache()
|
||||
{
|
||||
$this->cache->clear();
|
||||
$this->outputHTML('blank', [
|
||||
'title' => 'Cache cleared'
|
||||
], NULL, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the login form
|
||||
*
|
||||
* @param string $status
|
||||
* @return void
|
||||
*/
|
||||
public function login(string $status = '')
|
||||
{
|
||||
$message = '';
|
||||
|
||||
$view = new HtmlView($this->container);
|
||||
|
||||
if ($status !== '')
|
||||
{
|
||||
$message = $this->showMessage($view, 'error', $status);
|
||||
}
|
||||
|
||||
// Set the redirect url
|
||||
$this->setSessionRedirect();
|
||||
|
||||
$this->outputHTML('login', [
|
||||
'title' => 'Api login',
|
||||
'message' => $message
|
||||
], $view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt login authentication
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function loginAction()
|
||||
{
|
||||
$auth = $this->container->get('auth');
|
||||
$post = $this->request->getParsedBody();
|
||||
if ($auth->authenticate($post['password']))
|
||||
{
|
||||
$this->sessionRedirect();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setFlashMessage('Invalid username or password.');
|
||||
$this->redirect($this->url->generate('login'), 303);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deauthorize the current user
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
$auth = $this->container->get('auth');
|
||||
$auth->logout();
|
||||
|
||||
$this->redirectToDefaultRoute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the user profile page
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function me()
|
||||
{
|
||||
$username = $this->config->get(['kitsu_username']);
|
||||
$model = $this->container->get('kitsu-model');
|
||||
$data = $model->getUserData($username);
|
||||
$orgData = JsonAPI::organizeData($data);
|
||||
$this->outputHTML('me', [
|
||||
'title' => 'About' . $this->config->get('whose_list'),
|
||||
'data' => $orgData[0],
|
||||
'attributes' => $orgData[0]['attributes'],
|
||||
'relationships' => $orgData[0]['relationships'],
|
||||
'favorites' => $this->organizeFavorites($orgData[0]['relationships']['favorites']),
|
||||
]);
|
||||
}
|
||||
|
||||
private function organizeFavorites(array $rawfavorites): array
|
||||
{
|
||||
// return $rawfavorites;
|
||||
$output = [];
|
||||
|
||||
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] = $data['attributes'];
|
||||
}
|
||||
}
|
||||
|
||||
ksort($output[$key]);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
@ -98,17 +98,7 @@ class Manga extends Controller {
|
||||
*/
|
||||
public function addForm()
|
||||
{
|
||||
$raw_status_list = MangaReadingStatus::getConstList();
|
||||
|
||||
$statuses = [];
|
||||
|
||||
foreach ($raw_status_list as $status_item)
|
||||
{
|
||||
$statuses[$status_item] = (string)$this->string($status_item)
|
||||
->underscored()
|
||||
->humanize()
|
||||
->titleize();
|
||||
}
|
||||
$statuses = MangaReadingStatus::KITSU_TO_TITLE;
|
||||
|
||||
$this->setSessionRedirect();
|
||||
$this->outputHTML('manga/add', [
|
||||
@ -116,7 +106,7 @@ class Manga extends Controller {
|
||||
$this->config->get('whose_list') . "'s Manga List",
|
||||
'Add'
|
||||
),
|
||||
'action_url' => $this->urlGenerator->url('manga/add'),
|
||||
'action_url' => $this->url->generate('manga.add.post'),
|
||||
'status_list' => $statuses
|
||||
]);
|
||||
}
|
||||
@ -169,8 +159,9 @@ class Manga extends Controller {
|
||||
'title' => $title,
|
||||
'status_list' => MangaReadingStatus::KITSU_TO_TITLE,
|
||||
'item' => $item,
|
||||
'action' => $this->container->get('url-generator')
|
||||
->url('/manga/update_form'),
|
||||
'action' => $this->url->generate('update.post', [
|
||||
'controller' => 'manga'
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -221,7 +212,7 @@ class Manga extends Controller {
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
if ($this->request->getHeader('content-type')[0] === 'application/json')
|
||||
if (stripos($this->request->getHeader('content-type')[0], 'application/json') !== FALSE)
|
||||
{
|
||||
$data = Json::decode((string)$this->request->getBody());
|
||||
}
|
||||
@ -245,7 +236,8 @@ class Manga extends Controller {
|
||||
{
|
||||
$body = $this->request->getParsedBody();
|
||||
$id = $body['id'];
|
||||
$response = $this->model->deleteLibraryItem($id);
|
||||
$malId = $body['mal_id'];
|
||||
$response = $this->model->deleteLibraryItem($id, $malId);
|
||||
|
||||
if ($response)
|
||||
{
|
||||
|
@ -1,378 +0,0 @@
|
||||
<?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 - 2017 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;
|
||||
|
||||
use const Aviat\AnimeClient\SESSION_SEGMENT;
|
||||
|
||||
use function Aviat\AnimeClient\_dir;
|
||||
|
||||
use Aviat\AnimeClient\API\JsonAPI;
|
||||
use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
|
||||
use Aviat\Ion\View\{HtmlView, HttpView, JsonView};
|
||||
use InvalidArgumentException;
|
||||
|
||||
trait ControllerTrait {
|
||||
|
||||
use ContainerAware;
|
||||
|
||||
/**
|
||||
* Cache manager
|
||||
* @var \Psr\Cache\CacheItemPoolInterface
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* The global configuration object
|
||||
* @var \Aviat\Ion\ConfigInterface $config
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Request object
|
||||
* @var object $request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* Response object
|
||||
* @var object $response
|
||||
*/
|
||||
protected $response;
|
||||
|
||||
/**
|
||||
* The api model for the current controller
|
||||
* @var object
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* Url generation class
|
||||
* @var UrlGenerator
|
||||
*/
|
||||
protected $urlGenerator;
|
||||
|
||||
/**
|
||||
* Session segment
|
||||
* @var \Aura\Session\Segment
|
||||
*/
|
||||
protected $session;
|
||||
|
||||
/**
|
||||
* Common data to be sent to views
|
||||
* @var array
|
||||
*/
|
||||
protected $baseData = [
|
||||
'url_type' => 'anime',
|
||||
'other_type' => 'manga',
|
||||
'menu_name' => ''
|
||||
];
|
||||
|
||||
/**
|
||||
* Redirect to the default controller/url from an empty path
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function redirectToDefaultRoute()
|
||||
{
|
||||
$defaultType = $this->config->get(['routes', 'route_config', 'default_list']);
|
||||
$this->redirect($this->urlGenerator->defaultUrl($defaultType), 303);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the previous page
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function redirectToPrevious()
|
||||
{
|
||||
$previous = $this->session->getFlash('previous');
|
||||
$this->redirect($previous, 303);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current url in the session as the target of a future redirect
|
||||
*
|
||||
* @param string|null $url
|
||||
* @return void
|
||||
*/
|
||||
public function setSessionRedirect($url = NULL)
|
||||
{
|
||||
$serverParams = $this->request->getServerParams();
|
||||
|
||||
if ( ! array_key_exists('HTTP_REFERER', $serverParams))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$util = $this->container->get('util');
|
||||
$doubleFormPage = $serverParams['HTTP_REFERER'] === $this->request->getUri();
|
||||
|
||||
// Don't attempt to set the redirect url if
|
||||
// the page is one of the form type pages,
|
||||
// and the previous page is also a form type page_segments
|
||||
if ($doubleFormPage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_null($url))
|
||||
{
|
||||
$url = $util->isViewPage()
|
||||
? $this->request->url->get()
|
||||
: $serverParams['HTTP_REFERER'];
|
||||
}
|
||||
|
||||
$this->session->set('redirect_url', $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the url previously set in the session
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function sessionRedirect()
|
||||
{
|
||||
$target = $this->session->get('redirect_url');
|
||||
if (empty($target))
|
||||
{
|
||||
$this->notFound();
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->redirect($target, 303);
|
||||
$this->session->set('redirect_url', NULL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a class member
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get(string $key)
|
||||
{
|
||||
$allowed = ['response', 'config'];
|
||||
|
||||
if (in_array($key, $allowed))
|
||||
{
|
||||
return $this->$key;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string output of a partial template
|
||||
*
|
||||
* @param HtmlView $view
|
||||
* @param string $template
|
||||
* @param array $data
|
||||
* @throws InvalidArgumentException
|
||||
* @return string
|
||||
*/
|
||||
protected function loadPartial($view, $template, array $data = [])
|
||||
{
|
||||
$router = $this->container->get('dispatcher');
|
||||
|
||||
if (isset($this->baseData))
|
||||
{
|
||||
$data = array_merge($this->baseData, $data);
|
||||
}
|
||||
|
||||
$route = $router->getRoute();
|
||||
$data['route_path'] = $route ? $router->getRoute()->path : '';
|
||||
|
||||
|
||||
$templatePath = _dir($this->config->get('view_path'), "{$template}.php");
|
||||
|
||||
if ( ! is_file($templatePath))
|
||||
{
|
||||
throw new InvalidArgumentException("Invalid template : {$template}");
|
||||
}
|
||||
|
||||
return $view->renderTemplate($templatePath, (array)$data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a template with header and footer
|
||||
*
|
||||
* @param HtmlView $view
|
||||
* @param string $template
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
protected function renderFullPage($view, $template, array $data)
|
||||
{
|
||||
$view->appendOutput($this->loadPartial($view, 'header', $data));
|
||||
|
||||
if (array_key_exists('message', $data) && is_array($data['message']))
|
||||
{
|
||||
$view->appendOutput($this->loadPartial($view, 'message', $data['message']));
|
||||
}
|
||||
|
||||
$view->appendOutput($this->loadPartial($view, $template, $data));
|
||||
$view->appendOutput($this->loadPartial($view, 'footer', $data));
|
||||
}
|
||||
|
||||
/**
|
||||
* 404 action
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function notFound(
|
||||
string $title = 'Sorry, page not found',
|
||||
string $message = 'Page Not Found'
|
||||
)
|
||||
{
|
||||
$this->outputHTML('404', [
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
], NULL, 404);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a generic error page
|
||||
*
|
||||
* @param int $httpCode
|
||||
* @param string $title
|
||||
* @param string $message
|
||||
* @param string $long_message
|
||||
* @return void
|
||||
*/
|
||||
public function errorPage($httpCode, $title, $message, $long_message = "")
|
||||
{
|
||||
$this->outputHTML('error', [
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
'long_message' => $long_message
|
||||
], NULL, $httpCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a session flash variable to display a message on
|
||||
* next page load
|
||||
*
|
||||
* @param string $message
|
||||
* @param string $type
|
||||
* @return void
|
||||
*/
|
||||
public function setFlashMessage($message, $type = "info")
|
||||
{
|
||||
static $messages;
|
||||
|
||||
if ( ! $messages)
|
||||
{
|
||||
$messages = [];
|
||||
}
|
||||
|
||||
$messages[] = [
|
||||
'message_type' => $type,
|
||||
'message' => $message
|
||||
];
|
||||
|
||||
$this->session->setFlash('message', $messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purges the API cache
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearCache()
|
||||
{
|
||||
$this->cache->clear();
|
||||
$this->outputHTML('blank', [
|
||||
'title' => 'Cache cleared'
|
||||
], NULL, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for consistent page titles
|
||||
*
|
||||
* @param string ...$parts Title segements
|
||||
* @return string
|
||||
*/
|
||||
public function formatTitle(string ...$parts) : string
|
||||
{
|
||||
return implode(' · ', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message box to the page
|
||||
*
|
||||
* @param HtmlView $view
|
||||
* @param string $type
|
||||
* @param string $message
|
||||
* @return string
|
||||
*/
|
||||
protected function showMessage($view, $type, $message)
|
||||
{
|
||||
return $this->loadPartial($view, 'message', [
|
||||
'message_type' => $type,
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a template to HTML, using the provided data
|
||||
*
|
||||
* @param string $template
|
||||
* @param array $data
|
||||
* @param HtmlView|null $view
|
||||
* @param int $code
|
||||
* @return void
|
||||
*/
|
||||
protected function outputHTML($template, array $data = [], $view = NULL, $code = 200)
|
||||
{
|
||||
if (is_null($view))
|
||||
{
|
||||
$view = new HtmlView($this->container);
|
||||
}
|
||||
|
||||
$view->setStatusCode($code);
|
||||
$this->renderFullPage($view, $template, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a JSON Response
|
||||
*
|
||||
* @param mixed $data
|
||||
* @param int $code - the http status code
|
||||
* @return void
|
||||
*/
|
||||
protected function outputJSON($data = 'Empty response', int $code = 200)
|
||||
{
|
||||
(new JsonView($this->container))
|
||||
->setStatusCode($code)
|
||||
->setOutput($data)
|
||||
->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the selected page
|
||||
*
|
||||
* @param string $url
|
||||
* @param int $code
|
||||
* @return void
|
||||
*/
|
||||
protected function redirect($url, $code)
|
||||
{
|
||||
$http = new HttpView($this->container);
|
||||
$http->redirect($url, $code);
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ use const Aviat\AnimeClient\{
|
||||
SRC_DIR
|
||||
};
|
||||
|
||||
use function Aviat\AnimeClient\_dir;
|
||||
use function Aviat\Ion\_dir;
|
||||
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\Friend;
|
||||
|
@ -16,10 +16,7 @@
|
||||
|
||||
namespace Aviat\AnimeClient;
|
||||
|
||||
use Aviat\Ion\
|
||||
{
|
||||
ArrayWrapper, StringWrapper
|
||||
};
|
||||
use Aviat\Ion\{ArrayWrapper, StringWrapper};
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
|
||||
/**
|
||||
|
@ -21,6 +21,13 @@ namespace Aviat\AnimeClient\Model;
|
||||
*/
|
||||
class API extends AbstractModel {
|
||||
|
||||
/**
|
||||
* Whether to use the MAL api
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $useMALAPI;
|
||||
|
||||
/**
|
||||
* Sort the list entries by their title
|
||||
*
|
||||
|
@ -39,13 +39,6 @@ class Anime extends API {
|
||||
*/
|
||||
protected $malModel;
|
||||
|
||||
/**
|
||||
* Whether to use the MAL api
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $useMALAPI;
|
||||
|
||||
/**
|
||||
* Anime constructor.
|
||||
*
|
||||
@ -53,10 +46,10 @@ class Anime extends API {
|
||||
*/
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
$config = $container->get('config');
|
||||
$this->kitsuModel = $container->get('kitsu-model');
|
||||
$this->malModel = $container->get('mal-model');
|
||||
|
||||
$config = $container->get('config');
|
||||
$this->useMALAPI = $config->get(['use_mal_api']) === TRUE;
|
||||
}
|
||||
|
||||
@ -66,7 +59,7 @@ class Anime extends API {
|
||||
* @param string $status
|
||||
* @return array
|
||||
*/
|
||||
public function getList($status)
|
||||
public function getList($status): array
|
||||
{
|
||||
$data = $this->kitsuModel->getAnimeList($status);
|
||||
$this->sortByName($data, 'anime');
|
||||
@ -79,7 +72,12 @@ class Anime extends API {
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function getAllLists()
|
||||
/**
|
||||
* Get data for the 'all' anime page
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAllLists(): array
|
||||
{
|
||||
$data = $this->kitsuModel->getFullOrganizedAnimeList();
|
||||
|
||||
@ -97,7 +95,7 @@ class Anime extends API {
|
||||
* @param string $slug
|
||||
* @return array
|
||||
*/
|
||||
public function getAnime($slug)
|
||||
public function getAnime(string $slug): array
|
||||
{
|
||||
return $this->kitsuModel->getAnime($slug);
|
||||
}
|
||||
@ -108,7 +106,7 @@ class Anime extends API {
|
||||
* @param string $animeId
|
||||
* @return array
|
||||
*/
|
||||
public function getAnimeById($animeId)
|
||||
public function getAnimeById(string $animeId): array
|
||||
{
|
||||
return $this->kitsuModel->getAnimeById($animeId);
|
||||
}
|
||||
@ -119,7 +117,7 @@ class Anime extends API {
|
||||
* @param string $name
|
||||
* @return array
|
||||
*/
|
||||
public function search($name)
|
||||
public function search(string $name): array
|
||||
{
|
||||
return $this->kitsuModel->search('anime', $name);
|
||||
}
|
||||
|
@ -16,9 +16,13 @@
|
||||
|
||||
namespace Aviat\AnimeClient\Model;
|
||||
|
||||
use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Title;
|
||||
use Aviat\AnimeClient\API\Mapping\MangaReadingStatus;
|
||||
use Aviat\AnimeClient\API\{
|
||||
Enum\MangaReadingStatus\Title,
|
||||
Mapping\MangaReadingStatus,
|
||||
ParallelAPIRequest
|
||||
};
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use Aviat\Ion\Json;
|
||||
|
||||
/**
|
||||
* Model for handling requests dealing with the manga list
|
||||
@ -46,6 +50,9 @@ class Manga extends API
|
||||
{
|
||||
$this->kitsuModel = $container->get('kitsu-model');
|
||||
$this->malModel = $container->get('mal-model');
|
||||
|
||||
$config = $container->get('config');
|
||||
$this->useMALAPI = $config->get(['use_mal_api']) === TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,17 +84,6 @@ class Manga extends API
|
||||
return $this->kitsuModel->getManga($manga_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new manga list item
|
||||
*
|
||||
* @param array $data
|
||||
* @return bool
|
||||
*/
|
||||
public function createLibraryItem(array $data): bool
|
||||
{
|
||||
return $this->kitsuModel->createListItem($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about a specific list item
|
||||
* for editing/updating that item
|
||||
@ -100,6 +96,35 @@ class Manga extends API
|
||||
return $this->kitsuModel->getListItem($itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new manga list item
|
||||
*
|
||||
* @param array $data
|
||||
* @return bool
|
||||
*/
|
||||
public function createLibraryItem(array $data): bool
|
||||
{
|
||||
$requester = new ParallelAPIRequest();
|
||||
|
||||
if ($this->useMALAPI)
|
||||
{
|
||||
$malData = $data;
|
||||
$malId = $this->kitsuModel->getMalIdForManga($malData['id']);
|
||||
|
||||
if ( ! is_null($malId))
|
||||
{
|
||||
$malData['id'] = $malId;
|
||||
$requester->addRequest($this->malModel->createListItem($malData, 'manga'), 'mal');
|
||||
}
|
||||
}
|
||||
|
||||
$requester->addRequest($this->kitsuModel->createListItem($data), 'kitsu');
|
||||
|
||||
$results = $requester->makeRequests(TRUE);
|
||||
|
||||
return count($results[1]) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a list entry
|
||||
*
|
||||
@ -108,18 +133,44 @@ class Manga extends API
|
||||
*/
|
||||
public function updateLibraryItem(array $data): array
|
||||
{
|
||||
return $this->kitsuModel->updateListItem($data);
|
||||
$requester = new ParallelAPIRequest();
|
||||
|
||||
if ($this->useMALAPI)
|
||||
{
|
||||
$requester->addRequest($this->malModel->updateListItem($data, 'manga'), 'mal');
|
||||
}
|
||||
|
||||
$requester->addRequest($this->kitsuModel->updateListItem($data), 'kitsu');
|
||||
|
||||
$results = $requester->makeRequests(TRUE);
|
||||
|
||||
return [
|
||||
'body' => Json::decode($results[1]['kitsu']->getBody()),
|
||||
'statusCode' => $results[1]['kitsu']->getStatus()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a list entry
|
||||
* Delete a list entry
|
||||
*
|
||||
* @param string $itemId
|
||||
* @param string $id
|
||||
* @param string|null $malId
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteLibraryItem(string $itemId): bool
|
||||
public function deleteLibraryItem(string $id, string $malId = NULL): bool
|
||||
{
|
||||
return $this->kitsuModel->deleteListItem($itemId);
|
||||
$requester = new ParallelAPIRequest();
|
||||
|
||||
if ($this->useMALAPI && ! is_null($malId))
|
||||
{
|
||||
$requester->addRequest($this->malModel->deleteListItem($malId, 'manga'), 'MAL');
|
||||
}
|
||||
|
||||
$requester->addRequest($this->kitsuModel->deleteListItem($id), 'kitsu');
|
||||
|
||||
$results = $requester->makeRequests(TRUE);
|
||||
|
||||
return count($results[1]) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,9 +59,8 @@ class RoutingBase {
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->config = $container->get('config');
|
||||
$baseRoutes = $this->config->get('routes');
|
||||
$this->routes = $baseRoutes['routes'];
|
||||
$this->routeConfig = $baseRoutes['route_config'];
|
||||
$this->routes = $this->config->get('routes');
|
||||
$this->routeConfig = $this->config->get('route_config');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -72,11 +71,9 @@ class RoutingBase {
|
||||
*/
|
||||
public function __get($key)
|
||||
{
|
||||
$routingConfig =& $this->routeConfig;
|
||||
|
||||
if (array_key_exists($key, $routingConfig))
|
||||
if (array_key_exists($key, $this->routeConfig))
|
||||
{
|
||||
return $routingConfig[$key];
|
||||
return $this->routeConfig[$key];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,32 +108,5 @@ class UrlGenerator extends RoutingBase {
|
||||
|
||||
throw new InvalidArgumentException("Invalid default type: '{$type}'");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate full url path from the route path based on config
|
||||
*
|
||||
* @param string $path - (optional) The route path
|
||||
* @param string $type - (optional) The controller (anime or manga), defaults to anime
|
||||
* @return string
|
||||
*/
|
||||
public function fullUrl(string $path = "", string $type = "anime"): string
|
||||
{
|
||||
$configDefaultRoute = $this->__get("default_{$type}_path");
|
||||
|
||||
// Remove beginning/trailing slashes
|
||||
$path = trim($path, '/');
|
||||
|
||||
// Set the default view
|
||||
if ($path === '')
|
||||
{
|
||||
$path .= trim($configDefaultRoute, '/');
|
||||
if ($this->__get('default_to_list_view'))
|
||||
{
|
||||
$path .= '/list';
|
||||
}
|
||||
}
|
||||
|
||||
return $this->url($path);
|
||||
}
|
||||
}
|
||||
// End of UrlGenerator.php
|
@ -32,10 +32,21 @@ class MangaListTransformerTest extends AnimeClientTestCase {
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$kitsuModel = $this->container->get('kitsu-model');
|
||||
|
||||
$this->dir = AnimeClientTestCase::TEST_DATA_DIR . '/Kitsu';
|
||||
|
||||
// Prep for transform
|
||||
$rawBefore = Json::decodeFile("{$this->dir}/mangaListBeforeTransform.json");
|
||||
$this->beforeTransform = JsonAPI::inlineRawIncludes($rawBefore, 'manga');
|
||||
$included = JsonAPI::organizeIncludes($rawBefore['included']);
|
||||
$included = JsonAPI::inlineIncludedRelationships($included, 'manga');
|
||||
foreach($rawBefore['data'] as $i => &$item)
|
||||
{
|
||||
$item['included'] = $included;
|
||||
}
|
||||
|
||||
$this->beforeTransform = $rawBefore['data'];
|
||||
$this->afterTransform = Json::decodeFile("{$this->dir}/mangaListAfterTransform.json");
|
||||
|
||||
$this->transformer = new MangaListTransformer();
|
||||
@ -54,7 +65,8 @@ class MangaListTransformerTest extends AnimeClientTestCase {
|
||||
public function testUntransform()
|
||||
{
|
||||
$input = [
|
||||
'id' => "15084773",
|
||||
'id' => '15084773',
|
||||
'mal_id' => '26769',
|
||||
'chapters_read' => 67,
|
||||
'manga' => [
|
||||
'titles' => ["Bokura wa Minna Kawaisou"],
|
||||
@ -75,6 +87,7 @@ class MangaListTransformerTest extends AnimeClientTestCase {
|
||||
$actual = $this->transformer->untransform($input);
|
||||
$expected = [
|
||||
'id' => '15084773',
|
||||
'mal_id' => '26769',
|
||||
'data' => [
|
||||
'status' => 'current',
|
||||
'progress' => 67,
|
||||
|
40
tests/API/MAL/ListItemTest.php
Normal file
40
tests/API/MAL/ListItemTest.php
Normal 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 - 2017 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\Tests\API\MAL;
|
||||
|
||||
use Aviat\AnimeClient\API\MAL\ListItem;
|
||||
use Aviat\AnimeClient\API\MAL\MALRequestBuilder;
|
||||
use Aviat\AnimeClient\Tests\AnimeClientTestCase;
|
||||
use Aviat\Ion\Di\ContainerAware;
|
||||
|
||||
class ListItemTest extends AnimeClientTestCase {
|
||||
|
||||
protected $listItem;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$this->listItem = new ListItem();
|
||||
$this->listItem->setContainer($this->container);
|
||||
$this->listItem->setRequestBuilder(new MALRequestBuilder());
|
||||
}
|
||||
|
||||
public function testGet()
|
||||
{
|
||||
$this->assertEquals([], $this->listItem->get('foo'));
|
||||
}
|
||||
}
|
51
tests/API/MAL/MALTraitTest.php
Normal file
51
tests/API/MAL/MALTraitTest.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?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 - 2017 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\Tests\API\MAL;
|
||||
|
||||
use Aviat\AnimeClient\API\MAL\MALRequestBuilder;
|
||||
use Aviat\AnimeClient\API\MAL\MALTrait;
|
||||
use Aviat\AnimeClient\Tests\AnimeClientTestCase;
|
||||
use Aviat\Ion\Di\ContainerAware;
|
||||
|
||||
class MALTraitTest extends AnimeClientTestCase {
|
||||
|
||||
protected $obj;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$this->obj = new class {
|
||||
use ContainerAware;
|
||||
use MALTrait;
|
||||
};
|
||||
$this->obj->setContainer($this->container);
|
||||
$this->obj->setRequestBuilder(new MALRequestBuilder());
|
||||
}
|
||||
|
||||
public function testSetupRequest()
|
||||
{
|
||||
$request = $this->obj->setUpRequest('GET', 'foo', [
|
||||
'query' => [
|
||||
'foo' => 'bar'
|
||||
],
|
||||
'body' => ''
|
||||
]);
|
||||
$this->assertInstanceOf(\Amp\Artax\Request::class, $request);
|
||||
$this->assertEquals($request->getUri(), 'https://myanimelist.net/api/foo?foo=bar');
|
||||
$this->assertEquals($request->getBody(), '');
|
||||
}
|
||||
}
|
@ -14,26 +14,22 @@
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\Tests;
|
||||
namespace Aviat\AnimeClient\Tests\API\MAL;
|
||||
|
||||
use Aviat\AnimeClient\ControllerTrait;
|
||||
use Aviat\AnimeClient\Tests\AnimeClientTestCase;
|
||||
|
||||
class ControllerTraitTest extends AnimeClientTestCase {
|
||||
class ModelTest extends AnimeClientTestCase {
|
||||
|
||||
protected $model;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->controller = new class {
|
||||
use ControllerTrait;
|
||||
};
|
||||
$this->model = $this->container->get('mal-model');
|
||||
}
|
||||
|
||||
public function testFormatTitle()
|
||||
public function testGetListItem()
|
||||
{
|
||||
$this->assertEquals(
|
||||
$this->controller->formatTitle('foo', 'bar', 'baz'),
|
||||
'foo · bar · baz'
|
||||
);
|
||||
$this->assertEquals([], $this->model->getListItem('foo'));
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
<?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 - 2017 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\Tests;
|
||||
|
||||
use function Aviat\AnimeClient\_dir;
|
||||
|
||||
class AnimeClientTest extends AnimeClientTestCase {
|
||||
/**
|
||||
* Basic sanity test for _dir function
|
||||
*/
|
||||
public function testDir()
|
||||
{
|
||||
$this->assertEquals('foo' . \DIRECTORY_SEPARATOR . 'bar', _dir('foo', 'bar'));
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@ namespace Aviat\AnimeClient\Tests;
|
||||
|
||||
use const Aviat\AnimeClient\SRC_DIR;
|
||||
|
||||
use function Aviat\AnimeClient\_dir;
|
||||
use function Aviat\Ion\_dir;
|
||||
|
||||
use Aura\Web\WebFactory;
|
||||
use Aviat\Ion\Json;
|
||||
@ -95,13 +95,15 @@ class AnimeClientTestCase extends TestCase {
|
||||
'file' => ':memory:',
|
||||
]
|
||||
],
|
||||
'routes' => [
|
||||
'route_config' => [
|
||||
'asset_path' => '/assets'
|
||||
],
|
||||
'routes' => [
|
||||
|
||||
]
|
||||
],
|
||||
'mal' => [
|
||||
'username' => 'foo',
|
||||
'password' => 'bar'
|
||||
]
|
||||
];
|
||||
|
||||
|
@ -80,31 +80,12 @@ class ControllerTest extends AnimeClientTestCase {
|
||||
$this->assertTrue(is_object($this->BaseController));
|
||||
}
|
||||
|
||||
public function dataGet()
|
||||
public function testFormatTitle()
|
||||
{
|
||||
return [
|
||||
'response' => [
|
||||
'key' => 'response',
|
||||
],
|
||||
'config' => [
|
||||
'key' => 'config',
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataGet
|
||||
*/
|
||||
public function testGet($key)
|
||||
{
|
||||
$result = $this->BaseController->__get($key);
|
||||
$this->assertEquals($this->container->get($key), $result);
|
||||
}
|
||||
|
||||
public function testGetNull()
|
||||
{
|
||||
$result = $this->BaseController->__get('foo');
|
||||
$this->assertNull($result);
|
||||
$this->assertEquals(
|
||||
$this->BaseController->formatTitle('foo', 'bar', 'baz'),
|
||||
'foo · bar · baz'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -71,7 +71,6 @@ class DispatcherTest extends AnimeClientTestCase {
|
||||
public function dataRoute()
|
||||
{
|
||||
$defaultConfig = [
|
||||
'routes' => [
|
||||
'routes' => [
|
||||
'login_form' => [
|
||||
'path' => '/login',
|
||||
@ -104,7 +103,6 @@ class DispatcherTest extends AnimeClientTestCase {
|
||||
'manga_path' => 'manga',
|
||||
'default_list' => 'anime'
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
$data = [
|
||||
@ -134,8 +132,8 @@ class DispatcherTest extends AnimeClientTestCase {
|
||||
]
|
||||
];
|
||||
|
||||
$data['manga_default_routing_anime']['config']['routes']['route_config']['default_list'] = 'manga';
|
||||
$data['manga_default_routing_manga']['config']['routes']['route_config']['default_list'] = 'manga';
|
||||
$data['manga_default_routing_anime']['config']['route_config']['default_list'] = 'manga';
|
||||
$data['manga_default_routing_manga']['config']['route_config']['default_list'] = 'manga';
|
||||
|
||||
return $data;
|
||||
}
|
||||
@ -169,7 +167,6 @@ class DispatcherTest extends AnimeClientTestCase {
|
||||
public function testDefaultRoute()
|
||||
{
|
||||
$config = [
|
||||
'routes' => [
|
||||
'route_config' => [
|
||||
'anime_path' => 'anime',
|
||||
'manga_path' => 'manga',
|
||||
@ -201,7 +198,6 @@ class DispatcherTest extends AnimeClientTestCase {
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$this->doSetUp($config, "/", "localhost");
|
||||
@ -217,7 +213,6 @@ class DispatcherTest extends AnimeClientTestCase {
|
||||
return [
|
||||
'controller_list_sanity_check' => [
|
||||
'config' => [
|
||||
'routes' => [
|
||||
'routes' => [
|
||||
|
||||
],
|
||||
@ -228,18 +223,17 @@ class DispatcherTest extends AnimeClientTestCase {
|
||||
'default_manga_list_path' => 'all',
|
||||
'default_list' => 'manga'
|
||||
],
|
||||
]
|
||||
],
|
||||
'expected' => [
|
||||
'anime' => 'Aviat\AnimeClient\Controller\Anime',
|
||||
'manga' => 'Aviat\AnimeClient\Controller\Manga',
|
||||
'collection' => 'Aviat\AnimeClient\Controller\Collection',
|
||||
'character' => 'Aviat\AnimeClient\Controller\Character',
|
||||
'index' => 'Aviat\AnimeClient\Controller\Index',
|
||||
]
|
||||
],
|
||||
'empty_controller_list' => [
|
||||
'config' => [
|
||||
'routes' => [
|
||||
'routes' => [
|
||||
|
||||
],
|
||||
@ -250,13 +244,13 @@ class DispatcherTest extends AnimeClientTestCase {
|
||||
'default_manga_path' => '/manga/all',
|
||||
'default_list' => 'manga'
|
||||
],
|
||||
]
|
||||
],
|
||||
'expected' => [
|
||||
'anime' => 'Aviat\AnimeClient\Controller\Anime',
|
||||
'manga' => 'Aviat\AnimeClient\Controller\Manga',
|
||||
'collection' => 'Aviat\AnimeClient\Controller\Collection',
|
||||
'character' => 'Aviat\AnimeClient\Controller\Character',
|
||||
'index' => 'Aviat\AnimeClient\Controller\Index',
|
||||
]
|
||||
]
|
||||
];
|
||||
|
@ -49,60 +49,4 @@ class UrlGeneratorTest extends AnimeClientTestCase {
|
||||
$result = $urlGenerator->assetUrl(...$args);
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public function dataFullUrl()
|
||||
{
|
||||
return [
|
||||
'default_view' => [
|
||||
'config' => [
|
||||
'routes' => [
|
||||
'routes' => [],
|
||||
'route_config' => [
|
||||
'anime_path' => 'anime',
|
||||
'manga_path' => 'manga',
|
||||
'default_list' => 'manga',
|
||||
'default_anime_path' => '/anime/watching',
|
||||
'default_manga_path' => '/manga/all',
|
||||
'default_to_list_view' => FALSE,
|
||||
]
|
||||
],
|
||||
],
|
||||
'path' => '',
|
||||
'type' => 'manga',
|
||||
'expected' => '//localhost/manga/all',
|
||||
],
|
||||
'default_view_list' => [
|
||||
'config' => [
|
||||
'routes' => [
|
||||
'routes' => [],
|
||||
'route_config' => [
|
||||
'anime_path' => 'anime',
|
||||
'manga_path' => 'manga',
|
||||
'default_list' => 'manga',
|
||||
'default_anime_path' => '/anime/watching',
|
||||
'default_manga_path' => '/manga/all',
|
||||
'default_to_list_view' => TRUE,
|
||||
]
|
||||
],
|
||||
],
|
||||
'path' => '',
|
||||
'type' => 'manga',
|
||||
'expected' => '//localhost/manga/all/list',
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataFullUrl
|
||||
*/
|
||||
public function testFullUrl($config, $path, $type, $expected)
|
||||
{
|
||||
$config = new Config($config);
|
||||
$this->container->setInstance('config', $config);
|
||||
$urlGenerator = new UrlGenerator($this->container);
|
||||
|
||||
$result = $urlGenerator->fullUrl($path, $type);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
[{
|
||||
"id": "15084773",
|
||||
"mal_id": "26769",
|
||||
"chapters": {
|
||||
"read": 67,
|
||||
"total": "-"
|
||||
@ -15,7 +16,7 @@
|
||||
"url": "https:\/\/kitsu.io\/manga\/bokura-wa-minna-kawaisou",
|
||||
"type": "manga",
|
||||
"image": "https:\/\/media.kitsu.io\/manga\/poster_images\/20286\/small.jpg?1434293999",
|
||||
"genres": []
|
||||
"genres": ["Comedy", "Romance", "School", "Slice of Life", "Thriller"]
|
||||
},
|
||||
"reading_status": "current",
|
||||
"notes": "",
|
||||
@ -24,6 +25,7 @@
|
||||
"user_rating": 9
|
||||
}, {
|
||||
"id": "15085607",
|
||||
"mal_id": "16",
|
||||
"chapters": {
|
||||
"read": 17,
|
||||
"total": 120
|
||||
@ -39,7 +41,7 @@
|
||||
"url": "https:\/\/kitsu.io\/manga\/love-hina",
|
||||
"type": "manga",
|
||||
"image": "https:\/\/media.kitsu.io\/manga\/poster_images\/47\/small.jpg?1434249493",
|
||||
"genres": []
|
||||
"genres": ["Comedy", "Ecchi", "Harem", "Romance", "Sports"]
|
||||
},
|
||||
"reading_status": "current",
|
||||
"notes": "",
|
||||
@ -48,6 +50,7 @@
|
||||
"user_rating": 7
|
||||
}, {
|
||||
"id": "15084529",
|
||||
"mal_id": "35003",
|
||||
"chapters": {
|
||||
"read": 16,
|
||||
"total": "-"
|
||||
@ -63,7 +66,7 @@
|
||||
"url": "https:\/\/kitsu.io\/manga\/yamada-kun-to-7-nin-no-majo",
|
||||
"type": "manga",
|
||||
"image": "https:\/\/media.kitsu.io\/manga\/poster_images\/11777\/small.jpg?1438784325",
|
||||
"genres": []
|
||||
"genres": ["Comedy", "Ecchi", "Gender Bender", "Romance", "School", "Sports", "Supernatural"]
|
||||
},
|
||||
"reading_status": "current",
|
||||
"notes": "",
|
||||
@ -72,6 +75,7 @@
|
||||
"user_rating": 9
|
||||
}, {
|
||||
"id": "15312827",
|
||||
"mal_id": "78523",
|
||||
"chapters": {
|
||||
"read": 68,
|
||||
"total": "-"
|
||||
@ -87,7 +91,7 @@
|
||||
"url": "https:\/\/kitsu.io\/manga\/relife",
|
||||
"type": "manga",
|
||||
"image": "https:\/\/media.kitsu.io\/manga\/poster_images\/27175\/small.jpg?1464379411",
|
||||
"genres": []
|
||||
"genres": ["Romance", "School", "Slice of Life"]
|
||||
},
|
||||
"reading_status": "current",
|
||||
"notes": "",
|
||||
@ -95,33 +99,10 @@
|
||||
"reread": 0,
|
||||
"user_rating": "-"
|
||||
}, {
|
||||
"id": "15084772",
|
||||
"id": "15084769",
|
||||
"mal_id": "60815",
|
||||
"chapters": {
|
||||
"read": 28,
|
||||
"total": 62
|
||||
},
|
||||
"volumes": {
|
||||
"read": "-",
|
||||
"total": 10
|
||||
},
|
||||
"manga": {
|
||||
"titles": ["Usagi Drop", "Bunny Drop"],
|
||||
"alternate_title": null,
|
||||
"slug": "usagi-drop",
|
||||
"url": "https:\/\/kitsu.io\/manga\/usagi-drop",
|
||||
"type": "manga",
|
||||
"image": "https:\/\/media.kitsu.io\/manga\/poster_images\/7629\/small.jpg?1434265873",
|
||||
"genres": []
|
||||
},
|
||||
"reading_status": "on_hold",
|
||||
"notes": "",
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 8
|
||||
}, {
|
||||
"id": "15251749",
|
||||
"chapters": {
|
||||
"read": 1,
|
||||
"read": 43,
|
||||
"total": "-"
|
||||
},
|
||||
"volumes": {
|
||||
@ -129,111 +110,15 @@
|
||||
"total": "-"
|
||||
},
|
||||
"manga": {
|
||||
"titles": ["Shishunki Bitter Change"],
|
||||
"titles": ["Joshikausei"],
|
||||
"alternate_title": null,
|
||||
"slug": "shishunki-bitter-change",
|
||||
"url": "https:\/\/kitsu.io\/manga\/shishunki-bitter-change",
|
||||
"slug": "joshikausei",
|
||||
"url": "https:\/\/kitsu.io\/manga\/joshikausei",
|
||||
"type": "manga",
|
||||
"image": "https:\/\/media.kitsu.io\/manga\/poster_images\/25512\/small.jpg?1434305092",
|
||||
"genres": []
|
||||
"image": "https:\/\/media.kitsu.io\/manga\/poster_images\/25491\/small.jpg?1434305043",
|
||||
"genres": ["Comedy", "School", "Slice of Life"]
|
||||
},
|
||||
"reading_status": "planned",
|
||||
"notes": "",
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": "-"
|
||||
}, {
|
||||
"id": "15312881",
|
||||
"chapters": {
|
||||
"read": 0,
|
||||
"total": "-"
|
||||
},
|
||||
"volumes": {
|
||||
"read": "-",
|
||||
"total": "-"
|
||||
},
|
||||
"manga": {
|
||||
"titles": ["Kuragehime", "Princess Jellyfish"],
|
||||
"alternate_title": null,
|
||||
"slug": "kuragehime",
|
||||
"url": "https:\/\/kitsu.io\/manga\/kuragehime",
|
||||
"type": "manga",
|
||||
"image": "https:\/\/media.kitsu.io\/manga\/poster_images\/5531\/small.jpg?1434261214",
|
||||
"genres": []
|
||||
},
|
||||
"reading_status": "planned",
|
||||
"notes": "",
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": "-"
|
||||
}, {
|
||||
"id": "15315190",
|
||||
"chapters": {
|
||||
"read": 0,
|
||||
"total": 80
|
||||
},
|
||||
"volumes": {
|
||||
"read": "-",
|
||||
"total": 9
|
||||
},
|
||||
"manga": {
|
||||
"titles": ["Boku wa Mari no Naka", "Inside Mari"],
|
||||
"alternate_title": null,
|
||||
"slug": "boku-wa-mari-no-naka",
|
||||
"url": "https:\/\/kitsu.io\/manga\/boku-wa-mari-no-naka",
|
||||
"type": "manga",
|
||||
"image": "https:\/\/media.kitsu.io\/manga\/poster_images\/14261\/small.jpg?1434280674",
|
||||
"genres": []
|
||||
},
|
||||
"reading_status": "planned",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": "-"
|
||||
}, {
|
||||
"id": "15315189",
|
||||
"chapters": {
|
||||
"read": 0,
|
||||
"total": "-"
|
||||
},
|
||||
"volumes": {
|
||||
"read": "-",
|
||||
"total": "-"
|
||||
},
|
||||
"manga": {
|
||||
"titles": ["Aizawa-san Zoushoku"],
|
||||
"alternate_title": null,
|
||||
"slug": "aizawa-san-zoushoku",
|
||||
"url": "https:\/\/kitsu.io\/manga\/aizawa-san-zoushoku",
|
||||
"type": "manga",
|
||||
"image": "https:\/\/media.kitsu.io\/manga\/poster_images\/25316\/small.jpg?1434304656",
|
||||
"genres": []
|
||||
},
|
||||
"reading_status": "planned",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": "-"
|
||||
}, {
|
||||
"id": "15288185",
|
||||
"chapters": {
|
||||
"read": 28,
|
||||
"total": "-"
|
||||
},
|
||||
"volumes": {
|
||||
"read": "-",
|
||||
"total": "-"
|
||||
},
|
||||
"manga": {
|
||||
"titles": ["Tonari no Seki-kun", "My Neighbour Seki"],
|
||||
"alternate_title": null,
|
||||
"slug": "tonari-no-seki-kun",
|
||||
"url": "https:\/\/kitsu.io\/manga\/tonari-no-seki-kun",
|
||||
"type": "manga",
|
||||
"image": "https:\/\/media.kitsu.io\/manga\/poster_images\/21733\/small.jpg?1434297086",
|
||||
"genres": []
|
||||
},
|
||||
"reading_status": "on_hold",
|
||||
"reading_status": "current",
|
||||
"notes": "",
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user