commit
2f71a97327
@ -15,7 +15,7 @@ test:7:
|
||||
- php composer.phar install --no-dev
|
||||
image: php:7
|
||||
script:
|
||||
- phpunit -c build --coverage-text
|
||||
- phpunit -c build --coverage-text --colors=never
|
||||
|
||||
test:7.1:
|
||||
before_script:
|
||||
@ -24,9 +24,10 @@ test:7.1:
|
||||
- php composer.phar install --no-dev
|
||||
image: php:7.1
|
||||
script:
|
||||
- phpunit -c build --coverage-text
|
||||
- phpunit -c build --coverage-text --colors=never
|
||||
|
||||
test:hhvm:
|
||||
allow_failure: true
|
||||
before_script:
|
||||
- /usr/local/bin/composer self-update
|
||||
- curl -Lo /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar
|
||||
@ -34,4 +35,4 @@ test:hhvm:
|
||||
- composer install --no-dev
|
||||
image: 51systems/docker-gitlab-ci-runner-hhvm
|
||||
script:
|
||||
- hhvm -d hhvm.php7.all=true /usr/local/bin/phpunit -c build --coverage-text
|
||||
- hhvm -d hhvm.php7.all=true /usr/local/bin/phpunit -c build --coverage-text --colors=never
|
@ -3,6 +3,7 @@
|
||||
## 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
|
||||
|
||||
## Version 3
|
||||
* Converted user configuration to toml files
|
||||
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Timothy J Warren
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
10
README.md
10
README.md
@ -3,6 +3,8 @@
|
||||
A self-hosted client that allows custom formatting of data from the hummingbird api
|
||||
|
||||
[![Build Status](https://travis-ci.org/timw4mail/HummingBirdAnimeClient.svg?branch=master)](https://travis-ci.org/timw4mail/HummingBirdAnimeClient)
|
||||
[![build status](https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient/badges/develop/build.svg)](https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient/commits/develop)
|
||||
[![coverage report](https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient/badges/develop/coverage.svg)](https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient/commits/develop)
|
||||
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/timw4mail/HummingBirdAnimeClient/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/timw4mail/HummingBirdAnimeClient/?branch=master)
|
||||
|
||||
[[Hosted Example](https://list.timshomepage.net)]
|
||||
@ -50,6 +52,10 @@ or
|
||||
* public/js/cache
|
||||
5. Make sure the `console` script is executable
|
||||
|
||||
### Using MAL API
|
||||
1. Update `app/config/mal.toml` with your username and password
|
||||
2. Enable MAL api in `app/config/config.toml`
|
||||
|
||||
### Server Setup
|
||||
|
||||
#### Caching
|
||||
@ -88,8 +94,4 @@ include the contents of the `.htaccess` file in your Apache configuration.
|
||||
1. Login
|
||||
2. Use the form to select your media
|
||||
3. Save & Repeat as needed
|
||||
* For bulk importing anime:
|
||||
1. Find the anime you are looking for on the hummingbird search api page: `https://hummingbird.me/api/v1/search/anime?query=`
|
||||
2. Create an `import.json` file in the root of the app, with an array of objects from the search page that you want to import
|
||||
3. Go to the anime collection tab, and the import will be run
|
||||
|
||||
|
@ -48,10 +48,13 @@ return function(array $config_array = []) {
|
||||
|
||||
$app_logger = new Logger('animeclient');
|
||||
$app_logger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/app.log', Logger::NOTICE));
|
||||
$request_logger = new Logger('request');
|
||||
$request_logger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/request.log', Logger::NOTICE));
|
||||
$kitsu_request_logger = new Logger('kitsu_request');
|
||||
$kitsu_request_logger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/kitsu_request.log', Logger::NOTICE));
|
||||
$mal_request_logger = new Logger('mal_request');
|
||||
$mal_request_logger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/mal_request.log', Logger::NOTICE));
|
||||
$container->setLogger($app_logger, 'default');
|
||||
$container->setLogger($request_logger, 'request');
|
||||
$container->setLogger($kitsu_request_logger, 'kitsu_request');
|
||||
$container->setLogger($mal_request_logger, 'mal_request');
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Injected Objects
|
||||
|
@ -1,5 +1,4 @@
|
||||
<?php if ($auth->is_authenticated()): ?>
|
||||
<?php /* <pre><?= json_encode($item, \JSON_PRETTY_PRINT); ?></pre> */ ?>
|
||||
<main>
|
||||
<h2>Edit Anime List Item</h2>
|
||||
<form action="<?= $action ?>" method="post">
|
||||
@ -86,15 +85,20 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<br />
|
||||
<br />
|
||||
<fieldset>
|
||||
<legend>Danger Zone</legend>
|
||||
<form class="js-delete" action="<?= $url->generate('anime.delete') ?>" method="post">
|
||||
<table class="form invisible">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>
|
||||
<strong>Permanently</strong> remove this list item and <strong>all</strong> its data?
|
||||
</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>
|
||||
|
@ -53,7 +53,7 @@
|
||||
<tr>
|
||||
<td><label for="rereading_flag">Rereading?</label></td>
|
||||
<td>
|
||||
<input type="checkbox" name="reareading" id="rereading_flag"
|
||||
<input type="checkbox" name="rereading" id="rereading_flag"
|
||||
<?php if($item['rereading'] === TRUE): ?>checked="checked"<?php endif ?>
|
||||
/>
|
||||
</td>
|
||||
|
@ -14,24 +14,25 @@
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"aura/html": "2.*",
|
||||
"aura/router": "3.*",
|
||||
"aura/session": "2.*",
|
||||
"aura/html": "^2.0",
|
||||
"aura/router": "^3.0",
|
||||
"aura/session": "^2.0",
|
||||
"aviat/banker": "^1.0.0",
|
||||
"aviat/ion": "1.0.*",
|
||||
"filp/whoops": "2.0.*",
|
||||
"guzzlehttp/guzzle": "6.*",
|
||||
"monolog/monolog": "1.*",
|
||||
"filp/whoops": "^2.1.5",
|
||||
"guzzlehttp/guzzle": "^6.0",
|
||||
"monolog/monolog": "^1.0",
|
||||
"psr/http-message": "~1.0",
|
||||
"psr/log": "~1.0",
|
||||
"yosymfony/toml": "0.3.*",
|
||||
"zendframework/zend-diactoros": "1.3.*",
|
||||
"maximebf/consolekit": "^1.0"
|
||||
"yosymfony/toml": "^0.3",
|
||||
"zendframework/zend-diactoros": "^1.3",
|
||||
"maximebf/consolekit": "^1.0",
|
||||
"amphp/artax": "^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"pdepend/pdepend": "^2.2",
|
||||
"sebastian/phpcpd": "^2.0",
|
||||
"theseer/phpdox": "0.8.1.1",
|
||||
"sebastian/phpcpd": "^3.0",
|
||||
"theseer/phpdox": "^0.9.0",
|
||||
"phploc/phploc": "^3.0",
|
||||
"phpmd/phpmd": "^2.4",
|
||||
"phpunit/phpunit": "^5.7",
|
||||
|
@ -1317,3 +1317,8 @@ a:hover, a:active {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.cover_streaming_link .streaming-logo {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
@ -569,3 +569,8 @@ a:hover, a:active {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.cover_streaming_link .streaming-logo {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
@ -42,73 +42,14 @@ class JsonAPI {
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* Data array parsed out from a request
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $parsedData = [];
|
||||
|
||||
/**
|
||||
* Related objects included with the request
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $included = [];
|
||||
|
||||
/**
|
||||
* Pagination links
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $links = [];
|
||||
|
||||
/**
|
||||
* JsonAPI constructor
|
||||
*
|
||||
* @param array $initital
|
||||
*/
|
||||
public function __construct(array $initial = [])
|
||||
public static function inlineRawIncludes(array &$data, string $key): array
|
||||
{
|
||||
$this->data = $initial;
|
||||
}
|
||||
foreach($data['data'] as $i => &$item)
|
||||
{
|
||||
$item[$key] = $data['included'][$i];
|
||||
}
|
||||
|
||||
public function parseFromString(string $json)
|
||||
{
|
||||
$this->parse(Json::decode($json));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a JsonAPI response into its components
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
public function parse(array $data)
|
||||
{
|
||||
$this->included = static::organizeIncludes($data['included']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return data array after input is parsed
|
||||
* to inline includes inside of relationship objects
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getParsedData(): array
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Take inlined included data and inline it into the main object's relationships
|
||||
*
|
||||
* @param array $mainObject
|
||||
* @param array $included
|
||||
* @return array
|
||||
*/
|
||||
public static function inlineIncludedIntoMainObject(array $mainObject, array $included): array
|
||||
{
|
||||
$output = clone $mainObject;
|
||||
return $data['data'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -73,7 +73,7 @@ class Kitsu {
|
||||
public static function getAiringStatus(string $startDate = null, string $endDate = null): string
|
||||
{
|
||||
$startAirDate = new DateTimeImmutable($startDate ?? 'tomorrow');
|
||||
$endAirDate = new DateTimeImmutable($endDate ?? 'tomorrow');
|
||||
$endAirDate = new DateTimeImmutable($endDate ?? 'next year');
|
||||
$now = new DateTimeImmutable();
|
||||
|
||||
$isDoneAiring = $now > $endAirDate;
|
||||
@ -195,6 +195,8 @@ class Kitsu {
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -28,4 +28,3 @@ class AnimeWatchingStatus extends BaseEnum {
|
||||
const ON_HOLD = 'on_hold';
|
||||
const DROPPED = 'dropped';
|
||||
}
|
||||
// End of AnimeWatchingStatus.php
|
@ -93,7 +93,7 @@ trait KitsuTrait {
|
||||
'headers' => $this->defaultHeaders
|
||||
];
|
||||
|
||||
$logger = $this->container->getLogger('request');
|
||||
$logger = $this->container->getLogger('kitsu_request');
|
||||
$sessionSegment = $this->getContainer()
|
||||
->get('session')
|
||||
->getSegment(AnimeClient::SESSION_SEGMENT);
|
||||
@ -106,10 +106,19 @@ trait KitsuTrait {
|
||||
|
||||
$options = array_merge($defaultOptions, $options);
|
||||
|
||||
$logger->debug(Json::encode([$type, $url]));
|
||||
$logger->debug(Json::encode($options));
|
||||
$response = $this->client->request($type, $url, $options);
|
||||
|
||||
return $this->client->request($type, $url, $options);
|
||||
$logger->debug('Kitsu API request', [
|
||||
'requestParams' => [
|
||||
'type' => $type,
|
||||
'url' => $url,
|
||||
],
|
||||
'responseValues' => [
|
||||
'status' => $response->getStatusCode()
|
||||
]
|
||||
]);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -125,7 +134,7 @@ trait KitsuTrait {
|
||||
$logger = null;
|
||||
if ($this->getContainer())
|
||||
{
|
||||
$logger = $this->container->getLogger('request');
|
||||
$logger = $this->container->getLogger('kitsu_request');
|
||||
}
|
||||
|
||||
$response = $this->getResponse($type, $url, $options);
|
||||
@ -134,11 +143,8 @@ trait KitsuTrait {
|
||||
{
|
||||
if ($logger)
|
||||
{
|
||||
$logger->warning('Non 200 response for api call');
|
||||
$logger->warning($response->getBody());
|
||||
$logger->warning('Non 200 response for api call', $response->getBody());
|
||||
}
|
||||
|
||||
// throw new RuntimeException($response->getBody());
|
||||
}
|
||||
|
||||
return JSON::decode($response->getBody(), TRUE);
|
||||
@ -177,7 +183,7 @@ trait KitsuTrait {
|
||||
$logger = null;
|
||||
if ($this->getContainer())
|
||||
{
|
||||
$logger = $this->container->getLogger('request');
|
||||
$logger = $this->container->getLogger('kitsu_request');
|
||||
}
|
||||
|
||||
$response = $this->getResponse('POST', ...$args);
|
||||
@ -187,11 +193,8 @@ trait KitsuTrait {
|
||||
{
|
||||
if ($logger)
|
||||
{
|
||||
$logger->warning('Non 201 response for POST api call');
|
||||
$logger->warning($response->getBody());
|
||||
$logger->warning('Non 201 response for POST api call', $response->getBody());
|
||||
}
|
||||
|
||||
// throw new RuntimeException($response->getBody());
|
||||
}
|
||||
|
||||
return JSON::decode($response->getBody(), TRUE);
|
||||
|
@ -167,6 +167,34 @@ 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
|
||||
*
|
||||
@ -179,6 +207,16 @@ class Model {
|
||||
return $this->mangaTransformer->transform($baseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get and transform the entirety of the user's anime list
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFullAnimeList(): array
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw (unorganized) anime list for the configured user
|
||||
*
|
||||
@ -270,13 +308,9 @@ class Model {
|
||||
if ( ! $cacheItem->isHit())
|
||||
{
|
||||
$data = $this->getRequest('library-entries', $options);
|
||||
$data = JsonAPI::inlineRawIncludes($data, 'manga');
|
||||
|
||||
foreach($data['data'] as $i => &$item)
|
||||
{
|
||||
$item['manga'] = $data['included'][$i];
|
||||
}
|
||||
|
||||
$transformed = $this->mangaListTransformer->transformCollection($data['data']);
|
||||
$transformed = $this->mangaListTransformer->transformCollection($data);
|
||||
|
||||
$cacheItem->set($transformed);
|
||||
$cacheItem->save();
|
||||
|
@ -113,6 +113,7 @@ class AnimeListTransformer extends AbstractTransformer {
|
||||
|
||||
$untransformed = [
|
||||
'id' => $item['id'],
|
||||
'mal_id' => $item['mal_id'] ?? null,
|
||||
'data' => [
|
||||
'status' => $item['watching_status'],
|
||||
'rating' => $item['user_rating'] / 2,
|
||||
|
@ -97,16 +97,10 @@ class MangaListTransformer extends AbstractTransformer {
|
||||
'reconsuming' => $rereading,
|
||||
'reconsumeCount' => (int)$item['reread_count'],
|
||||
'notes' => $item['notes'],
|
||||
'rating' => $item['new_rating'] / 2
|
||||
],
|
||||
];
|
||||
|
||||
if ($item['new_rating'] !== $item['old_rating'] && $item['new_rating'] !== "")
|
||||
{
|
||||
$map['data']['rating'] = ($item['new_rating'] > 0)
|
||||
? $item['new_rating'] / 2
|
||||
: $item['old_rating'] / 2;
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,18 @@ class MangaTransformer extends AbstractTransformer {
|
||||
*/
|
||||
public function transform($item)
|
||||
{
|
||||
$genres = [];
|
||||
|
||||
foreach($item['included'] as $included)
|
||||
{
|
||||
if ($included['type'] === 'genres')
|
||||
{
|
||||
$genres[] = $included['attributes']['name'];
|
||||
}
|
||||
}
|
||||
|
||||
sort($genres);
|
||||
|
||||
return [
|
||||
'title' => $item['canonicalTitle'],
|
||||
'en_title' => $item['titles']['en'],
|
||||
@ -42,7 +54,7 @@ class MangaTransformer extends AbstractTransformer {
|
||||
'volume_count' => $this->count($item['volumeCount']),
|
||||
'synopsis' => $item['synopsis'],
|
||||
'url' => "https://kitsu.io/manga/{$item['slug']}",
|
||||
'genres' => $item['genres'],
|
||||
'genres' => $genres,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,10 @@
|
||||
|
||||
namespace Aviat\AnimeClient\API;
|
||||
|
||||
use Aviat\AnimeClient\API\Kitsu\Enum\{
|
||||
AnimeWatchingStatus as KAWS,
|
||||
MangaReadingStatus as KMRS
|
||||
};
|
||||
use Aviat\AnimeClient\API\MAL\Enum\{AnimeWatchingStatus, MangaReadingStatus};
|
||||
|
||||
/**
|
||||
@ -25,6 +29,14 @@ class MAL {
|
||||
const AUTH_URL = 'https://myanimelist.net/api/account/verify_credentials.xml';
|
||||
const BASE_URL = 'https://myanimelist.net/api/';
|
||||
|
||||
const KITSU_MAL_WATCHING_STATUS_MAP = [
|
||||
KAWS::WATCHING => AnimeWatchingStatus::WATCHING,
|
||||
KAWS::COMPLETED => AnimeWatchingStatus::COMPLETED,
|
||||
KAWS::ON_HOLD => AnimeWatchingStatus::ON_HOLD,
|
||||
KAWS::DROPPED => AnimeWatchingStatus::DROPPED,
|
||||
KAWS::PLAN_TO_WATCH => AnimeWatchingStatus::PLAN_TO_WATCH
|
||||
];
|
||||
|
||||
public static function getIdToWatchingStatusMap()
|
||||
{
|
||||
return [
|
||||
@ -32,7 +44,12 @@ class MAL {
|
||||
2 => AnimeWatchingStatus::COMPLETED,
|
||||
3 => AnimeWatchingStatus::ON_HOLD,
|
||||
4 => AnimeWatchingStatus::DROPPED,
|
||||
5 => AnimeWatchingStatus::PLAN_TO_WATCH
|
||||
6 => AnimeWatchingStatus::PLAN_TO_WATCH,
|
||||
'watching' => AnimeWatchingStatus::WATCHING,
|
||||
'completed' => AnimeWatchingStatus::COMPLETED,
|
||||
'onhold' => AnimeWatchingStatus::ON_HOLD,
|
||||
'dropped' => AnimeWatchingStatus::DROPPED,
|
||||
'plantowatch' => AnimeWatchingStatus::PLAN_TO_WATCH
|
||||
];
|
||||
}
|
||||
|
||||
@ -43,7 +60,12 @@ class MAL {
|
||||
2 => MangaReadingStatus::COMPLETED,
|
||||
3 => MangaReadingStatus::ON_HOLD,
|
||||
4 => MangaReadingStatus::DROPPED,
|
||||
5 => MangaReadingStatus::PLAN_TO_READ
|
||||
6 => MangaReadingStatus::PLAN_TO_READ,
|
||||
'reading' => MangaReadingStatus::READING,
|
||||
'completed' => MangaReadingStatus::COMPLETED,
|
||||
'onhold' => MangaReadingStatus::ON_HOLD,
|
||||
'dropped' => MangaReadingStatus::DROPPED,
|
||||
'plantoread' => MangaReadingStatus::PLAN_TO_WATCH
|
||||
];
|
||||
}
|
||||
}
|
@ -22,9 +22,9 @@ use Aviat\Ion\Enum as BaseEnum;
|
||||
* Possible values for watching status for the current anime
|
||||
*/
|
||||
class AnimeWatchingStatus extends BaseEnum {
|
||||
const WATCHING = 'watching';
|
||||
const COMPLETED = 'completed';
|
||||
const ON_HOLD = 'onhold';
|
||||
const DROPPED = 'dropped';
|
||||
const PLAN_TO_WATCH = 'plantowatch';
|
||||
const WATCHING = 1;
|
||||
const COMPLETED = 2;
|
||||
const ON_HOLD = 3;
|
||||
const DROPPED = 4;
|
||||
const PLAN_TO_WATCH = 6;
|
||||
}
|
@ -16,29 +16,44 @@
|
||||
|
||||
namespace Aviat\AnimeClient\API\MAL;
|
||||
|
||||
use Aviat\AnimeClient\API\AbstractListItem;
|
||||
use Amp\Artax\FormBody;
|
||||
use Aviat\AnimeClient\API\{
|
||||
AbstractListItem,
|
||||
XML
|
||||
};
|
||||
use Aviat\Ion\Di\ContainerAware;
|
||||
|
||||
/**
|
||||
* CRUD operations for MAL list items
|
||||
*/
|
||||
class ListItem extends AbstractListItem {
|
||||
class ListItem {
|
||||
use ContainerAware;
|
||||
use MALTrait;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->init();
|
||||
}
|
||||
|
||||
public function create(array $data): bool
|
||||
{
|
||||
return FALSE;
|
||||
$id = $data['id'];
|
||||
$createData = [
|
||||
'id' => $id,
|
||||
'data' => XML::toXML([
|
||||
'entry' => $data['data']
|
||||
])
|
||||
];
|
||||
|
||||
$response = $this->getResponse('POST', "animelist/add/{$id}.xml", [
|
||||
'body' => $this->fixBody((new FormBody)->addFields($createData))
|
||||
]);
|
||||
|
||||
return $response->getBody() === 'Created';
|
||||
}
|
||||
|
||||
public function delete(string $id): bool
|
||||
{
|
||||
return FALSE;
|
||||
$response = $this->getResponse('DELETE', "animelist/delete/{$id}.xml", [
|
||||
'body' => $this->fixBody((new FormBody)->addField('id', $id))
|
||||
]);
|
||||
|
||||
return $response->getBody() === 'Deleted';
|
||||
}
|
||||
|
||||
public function get(string $id): array
|
||||
@ -46,8 +61,15 @@ class ListItem extends AbstractListItem {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function update(string $id, array $data): Response
|
||||
public function update(string $id, array $data)
|
||||
{
|
||||
$xml = XML::toXML(['entry' => $data]);
|
||||
$body = (new FormBody)
|
||||
->addField('id', $id)
|
||||
->addField('data', $xml);
|
||||
|
||||
return $this->getResponse('POST', "animelist/update/{$id}.xml", [
|
||||
'body' => $this->fixBody($body)
|
||||
]);
|
||||
}
|
||||
}
|
@ -16,18 +16,15 @@
|
||||
|
||||
namespace Aviat\AnimeClient\API\MAL;
|
||||
|
||||
use Amp\Artax\{Client, FormBody, Request};
|
||||
use Aviat\AnimeClient\API\{
|
||||
GuzzleTrait,
|
||||
MAL as M,
|
||||
XML
|
||||
};
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Cookie\CookieJar;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Aviat\Ion\Json;
|
||||
use InvalidArgumentException;
|
||||
|
||||
trait MALTrait {
|
||||
use GuzzleTrait;
|
||||
|
||||
/**
|
||||
* The base url for api requests
|
||||
@ -41,30 +38,25 @@ trait MALTrait {
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultHeaders = [
|
||||
'Accept' => 'text/xml',
|
||||
'Accept-Encoding' => 'gzip',
|
||||
'Content-type' => 'application/x-www-form-urlencoded',
|
||||
'User-Agent' => "Tim's Anime Client/4.0"
|
||||
];
|
||||
|
||||
/**
|
||||
* Set up the class properties
|
||||
* Unencode the dual-encoded ampersands in the body
|
||||
*
|
||||
* @return void
|
||||
* 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
|
||||
*/
|
||||
protected function init()
|
||||
private function fixBody(FormBody $formBody): string
|
||||
{
|
||||
$defaults = [
|
||||
'cookies' => $this->cookieJar,
|
||||
'headers' => $this->defaultHeaders,
|
||||
'timeout' => 25,
|
||||
'connect_timeout' => 25
|
||||
];
|
||||
|
||||
$this->cookieJar = new CookieJar();
|
||||
$this->client = new Client([
|
||||
'base_uri' => $this->baseUrl,
|
||||
'cookies' => TRUE,
|
||||
'http_errors' => TRUE,
|
||||
'defaults' => $defaults
|
||||
]);
|
||||
$rawBody = \Amp\wait($formBody->getBody());
|
||||
return html_entity_decode($rawBody, \ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,8 +69,10 @@ trait MALTrait {
|
||||
*/
|
||||
private function getResponse(string $type, string $url, array $options = [])
|
||||
{
|
||||
$this->defaultHeaders['User-Agent'] = $_SERVER['HTTP_USER_AGENT'] ?? $this->defaultHeaders;
|
||||
|
||||
$type = strtoupper($type);
|
||||
$validTypes = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
|
||||
$validTypes = ['GET', 'POST', 'DELETE'];
|
||||
|
||||
if ( ! in_array($type, $validTypes))
|
||||
{
|
||||
@ -86,22 +80,49 @@ trait MALTrait {
|
||||
}
|
||||
|
||||
$config = $this->container->get('config');
|
||||
$logger = $this->container->getLogger('request');
|
||||
$logger = $this->container->getLogger('mal_request');
|
||||
|
||||
$defaultOptions = [
|
||||
'auth' => [
|
||||
$config->get(['mal','username']),
|
||||
$config->get(['mal','password'])
|
||||
],
|
||||
'headers' => $this->defaultHeaders
|
||||
];
|
||||
$headers = array_merge($this->defaultHeaders, $options['headers'] ?? [], [
|
||||
'Authorization' => 'Basic ' .
|
||||
base64_encode($config->get(['mal','username']) . ':' .$config->get(['mal','password']))
|
||||
]);
|
||||
|
||||
$options = array_merge($defaultOptions, $options);
|
||||
$query = $options['query'] ?? [];
|
||||
|
||||
$logger->debug(Json::encode([$type, $url]));
|
||||
$logger->debug(Json::encode($options));
|
||||
$url = (strpos($url, '//') !== FALSE)
|
||||
? $url
|
||||
: $this->baseUrl . $url;
|
||||
|
||||
return $this->client->request($type, $url, $options);
|
||||
if ( ! empty($query))
|
||||
{
|
||||
$url .= '?' . http_build_query($query);
|
||||
}
|
||||
|
||||
$request = (new Request)
|
||||
->setMethod($type)
|
||||
->setUri($url)
|
||||
->setProtocol('1.1')
|
||||
->setAllHeaders($headers);
|
||||
|
||||
if (array_key_exists('body', $options))
|
||||
{
|
||||
$request->setBody($options['body']);
|
||||
}
|
||||
|
||||
$response = \Amp\wait((new Client)->request($request));
|
||||
|
||||
$logger->debug('MAL api request', [
|
||||
'url' => $url,
|
||||
'status' => $response->getStatus(),
|
||||
'reason' => $response->getReason(),
|
||||
'headers' => $response->getAllHeaders(),
|
||||
'requestHeaders' => $request->getAllHeaders(),
|
||||
'requestBody' => $request->hasBody() ? $request->getBody() : 'No request body',
|
||||
'requestBodyBeforeEncode' => $request->hasBody() ? urldecode($request->getBody()) : '',
|
||||
'body' => $response->getBody()
|
||||
]);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -117,20 +138,17 @@ trait MALTrait {
|
||||
$logger = null;
|
||||
if ($this->getContainer())
|
||||
{
|
||||
$logger = $this->container->getLogger('request');
|
||||
$logger = $this->container->getLogger('mal_request');
|
||||
}
|
||||
|
||||
$response = $this->getResponse($type, $url, $options);
|
||||
|
||||
if ((int) $response->getStatusCode() > 299 || (int) $response->getStatusCode() < 200)
|
||||
if ((int) $response->getStatus() > 299 || (int) $response->getStatus() < 200)
|
||||
{
|
||||
if ($logger)
|
||||
{
|
||||
$logger->warning('Non 200 response for api call');
|
||||
$logger->warning($response->getBody());
|
||||
$logger->warning('Non 200 response for api call', $response->getBody());
|
||||
}
|
||||
|
||||
// throw new RuntimeException($response->getBody());
|
||||
}
|
||||
|
||||
return XML::toArray((string) $response->getBody());
|
||||
@ -158,33 +176,20 @@ trait MALTrait {
|
||||
$logger = null;
|
||||
if ($this->getContainer())
|
||||
{
|
||||
$logger = $this->container->getLogger('request');
|
||||
$logger = $this->container->getLogger('mal_request');
|
||||
}
|
||||
|
||||
$response = $this->getResponse('POST', ...$args);
|
||||
$validResponseCodes = [200, 201];
|
||||
|
||||
if ( ! in_array((int) $response->getStatusCode(), $validResponseCodes))
|
||||
if ( ! in_array((int) $response->getStatus(), $validResponseCodes))
|
||||
{
|
||||
if ($logger)
|
||||
{
|
||||
$logger->warning('Non 201 response for POST api call');
|
||||
$logger->warning($response->getBody());
|
||||
$logger->warning('Non 201 response for POST api call', $response->getBody());
|
||||
}
|
||||
}
|
||||
|
||||
return XML::toArray((string) $response->getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove some boilerplate for delete requests
|
||||
*
|
||||
* @param array $args
|
||||
* @return bool
|
||||
*/
|
||||
protected function deleteRequest(...$args): bool
|
||||
{
|
||||
$response = $this->getResponse('DELETE', ...$args);
|
||||
return ((int) $response->getStatusCode() === 204);
|
||||
return XML::toArray($response->getBody());
|
||||
}
|
||||
}
|
@ -17,10 +17,8 @@
|
||||
namespace Aviat\AnimeClient\API\MAL;
|
||||
|
||||
use Aviat\AnimeClient\API\MAL as M;
|
||||
use Aviat\AnimeClient\API\MAL\{
|
||||
AnimeListTransformer,
|
||||
ListItem
|
||||
};
|
||||
use Aviat\AnimeClient\API\MAL\ListItem;
|
||||
use Aviat\AnimeClient\API\MAL\Transformer\AnimeListTransformer;
|
||||
use Aviat\AnimeClient\API\XML;
|
||||
use Aviat\Ion\Di\ContainerAware;
|
||||
|
||||
@ -41,15 +39,37 @@ class Model {
|
||||
*/
|
||||
public function __construct(ListItem $listItem)
|
||||
{
|
||||
// Set up Guzzle trait
|
||||
$this->init();
|
||||
$this->animeListTransformer = new AnimeListTransformer();
|
||||
$this->listItem = $listItem;
|
||||
}
|
||||
|
||||
public function createListItem(array $data): bool
|
||||
{
|
||||
return FALSE;
|
||||
$createData = [
|
||||
'id' => $data['id'],
|
||||
'data' => [
|
||||
'status' => M::KITSU_MAL_WATCHING_STATUS_MAP[$data['status']]
|
||||
]
|
||||
];
|
||||
|
||||
return $this->listItem->create($createData);
|
||||
}
|
||||
|
||||
public function getFullList(): array
|
||||
{
|
||||
$config = $this->container->get('config');
|
||||
$userName = $config->get(['mal', 'username']);
|
||||
$list = $this->getRequest('https://myanimelist.net/malappinfo.php', [
|
||||
'headers' => [
|
||||
'Accept' => 'text/xml'
|
||||
],
|
||||
'query' => [
|
||||
'u' => $userName,
|
||||
'status' => 'all'
|
||||
]
|
||||
]);
|
||||
|
||||
return $list;//['anime'];
|
||||
}
|
||||
|
||||
public function getListItem(string $listId): array
|
||||
@ -59,12 +79,12 @@ class Model {
|
||||
|
||||
public function updateListItem(array $data)
|
||||
{
|
||||
$updateData = $this->animeListTransformer->transform($data['data']);
|
||||
return $this->listItem->update($data['mal_id'], $updateData);
|
||||
$updateData = $this->animeListTransformer->untransform($data);
|
||||
return $this->listItem->update($updateData['id'], $updateData['data']);
|
||||
}
|
||||
|
||||
public function deleteListItem(string $id): bool
|
||||
{
|
||||
|
||||
return $this->listItem->delete($id);
|
||||
}
|
||||
}
|
@ -14,8 +14,9 @@
|
||||
* @link https://github.com/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\API\MAL;
|
||||
namespace Aviat\AnimeClient\API\MAL\Transformer;
|
||||
|
||||
use Aviat\AnimeClient\API\Kitsu\Enum\AnimeWatchingStatus;
|
||||
use Aviat\Ion\Transformer\AbstractTransformer;
|
||||
|
||||
/**
|
||||
@ -23,18 +24,22 @@ use Aviat\Ion\Transformer\AbstractTransformer;
|
||||
*/
|
||||
class AnimeListTransformer extends AbstractTransformer {
|
||||
|
||||
const statusMap = [
|
||||
AnimeWatchingStatus::WATCHING => '1',
|
||||
AnimeWatchingStatus::COMPLETED => '2',
|
||||
AnimeWatchingStatus::ON_HOLD => '3',
|
||||
AnimeWatchingStatus::DROPPED => '4',
|
||||
AnimeWatchingStatus::PLAN_TO_WATCH => '6'
|
||||
];
|
||||
|
||||
public function transform($item)
|
||||
{
|
||||
$rewatching = 'false';
|
||||
if (array_key_exists('rewatching', $item) && $item['rewatching'])
|
||||
{
|
||||
$rewatching = 'true';
|
||||
}
|
||||
$rewatching = (array_key_exists('rewatching', $item) && $item['rewatching']);
|
||||
|
||||
return [
|
||||
'id' => $item['id'],
|
||||
'id' => $item['mal_id'],
|
||||
'data' => [
|
||||
'status' => $item['watching_status'],
|
||||
'status' => self::statusMap[$item['watching_status']],
|
||||
'rating' => $item['user_rating'],
|
||||
'rewatch_value' => (int) $rewatching,
|
||||
'times_rewatched' => $item['rewatched'],
|
||||
@ -43,4 +48,31 @@ class AnimeListTransformer extends AbstractTransformer {
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform Kitsu episode data to MAL episode data
|
||||
*
|
||||
* @param array $item
|
||||
* @return array
|
||||
*/
|
||||
public function untransform(array $item): array
|
||||
{
|
||||
$rewatching = (array_key_exists('reconsuming', $item['data']) && $item['data']['reconsuming']);
|
||||
|
||||
$map = [
|
||||
'id' => $item['mal_id'],
|
||||
'data' => [
|
||||
'episode' => $item['data']['progress'],
|
||||
'status' => self::statusMap[$item['data']['status']],
|
||||
'score' => (array_key_exists('rating', $item['data']))
|
||||
? $item['data']['rating'] * 2
|
||||
: "",
|
||||
// 'enable_rewatching' => $rewatching,
|
||||
// 'times_rewatched' => $item['data']['reconsumeCount'],
|
||||
// 'comments' => $item['data']['notes'],
|
||||
]
|
||||
];
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
@ -107,12 +107,7 @@ class XML {
|
||||
{
|
||||
$data = [];
|
||||
|
||||
// Get rid of unimportant text nodes by removing
|
||||
// whitespace characters from between xml tags,
|
||||
// except for the xml declaration tag, Which looks
|
||||
// something like:
|
||||
/* <?xml version="1.0" encoding="UTF-8"?> */
|
||||
$xml = preg_replace('/([^\?])>\s+</', '$1><', $xml);
|
||||
$xml = static::stripXMLWhitespace($xml);
|
||||
|
||||
$dom = new DOMDocument();
|
||||
$dom->loadXML($xml);
|
||||
@ -166,6 +161,16 @@ class XML {
|
||||
return static::toXML($this->getData());
|
||||
}
|
||||
|
||||
private static function stripXMLWhitespace(string $xml): string
|
||||
{
|
||||
// Get rid of unimportant text nodes by removing
|
||||
// whitespace characters from between xml tags,
|
||||
// except for the xml declaration tag, Which looks
|
||||
// something like:
|
||||
/* <?xml version="1.0" encoding="UTF-8"?> */
|
||||
return preg_replace('/([^\?])>\s+</', '$1><', $xml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively create array structure based on xml structure
|
||||
*
|
||||
|
@ -259,7 +259,7 @@ class Anime extends BaseController {
|
||||
$data = $this->request->getParsedBody();
|
||||
}
|
||||
|
||||
$response = $this->model->updateLibraryItem($data);
|
||||
$response = $this->model->updateLibraryItem($data, $data);
|
||||
|
||||
$this->cache->clear();
|
||||
$this->outputJSON($response['body'], $response['statusCode']);
|
||||
@ -273,7 +273,7 @@ class Anime extends BaseController {
|
||||
public function delete()
|
||||
{
|
||||
$body = $this->request->getParsedBody();
|
||||
$response = $this->model->deleteLibraryItem($body['id']);
|
||||
$response = $this->model->deleteLibraryItem($body['id'], $body['mal_id']);
|
||||
|
||||
if ((bool)$response === TRUE)
|
||||
{
|
||||
|
@ -43,6 +43,12 @@ class Anime extends API {
|
||||
AnimeWatchingStatus::COMPLETED => self::COMPLETED,
|
||||
];
|
||||
|
||||
protected $kitsuModel;
|
||||
|
||||
protected $malModel;
|
||||
|
||||
protected $useMALAPI;
|
||||
|
||||
/**
|
||||
* Anime constructor.
|
||||
* @param ContainerInterface $container
|
||||
@ -50,7 +56,11 @@ class Anime extends API {
|
||||
public function __construct(ContainerInterface $container) {
|
||||
parent::__construct($container);
|
||||
|
||||
$config = $container->get('config');
|
||||
$this->kitsuModel = $container->get('kitsu-model');
|
||||
$this->malModel = $container->get('mal-model');
|
||||
|
||||
$this->useMALAPI = $config->get(['use_mal_api']) === TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,8 +120,26 @@ class Anime extends API {
|
||||
return $this->kitsuModel->getListItem($itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an anime to your list
|
||||
*
|
||||
* @param array $data
|
||||
* @return bool
|
||||
*/
|
||||
public function createLibraryItem(array $data): bool
|
||||
{
|
||||
if ($this->useMALAPI)
|
||||
{
|
||||
$malData = $data;
|
||||
$malId = $this->kitsuModel->getMalIdForAnime($malData['id']);
|
||||
|
||||
if ( ! is_null($malId))
|
||||
{
|
||||
$malData['id'] = $malId;
|
||||
$this->malModel->createListItem($malData);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->kitsuModel->createListItem($data);
|
||||
}
|
||||
|
||||
@ -123,11 +151,28 @@ class Anime extends API {
|
||||
*/
|
||||
public function updateLibraryItem(array $data): array
|
||||
{
|
||||
if ($this->useMALAPI)
|
||||
{
|
||||
$this->malModel->updateListItem($data);
|
||||
}
|
||||
|
||||
return $this->kitsuModel->updateListItem($data);
|
||||
}
|
||||
|
||||
public function deleteLibraryItem($id): bool
|
||||
/**
|
||||
* Delete a list entry
|
||||
*
|
||||
* @param string $id
|
||||
* @param string|null $malId
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteLibraryItem(string $id, string $malId = null): bool
|
||||
{
|
||||
if ($this->useMALAPI && ! is_null($malId))
|
||||
{
|
||||
$this->malModel->deleteListItem($malId);
|
||||
}
|
||||
|
||||
return $this->kitsuModel->deleteListItem($id);
|
||||
}
|
||||
}
|
||||
|
28
tests/API/CacheTraitTest.php
Normal file
28
tests/API/CacheTraitTest.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Aviat\AnimeClient\Tests\API;
|
||||
|
||||
use Aviat\AnimeClient\API\CacheTrait;
|
||||
|
||||
class CacheTraitTest extends \AnimeClient_TestCase {
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$this->testClass = new class {
|
||||
use CacheTrait;
|
||||
};
|
||||
}
|
||||
|
||||
public function testSetGet()
|
||||
{
|
||||
$cachePool = $this->container->get('cache');
|
||||
$this->testClass->setCache($cachePool);
|
||||
$this->assertEquals($cachePool, $this->testClass->getCache());
|
||||
}
|
||||
|
||||
public function testGetHashForMethodCall()
|
||||
{
|
||||
$hash = $this->testClass->getHashForMethodCall($this, __METHOD__, []);
|
||||
$this->assertEquals('684ba0a5c29ffec452c5f6a07d2eee6932575490', $hash);
|
||||
}
|
||||
}
|
@ -44,6 +44,7 @@ class AnimeListTransformerTest extends AnimeClient_TestCase {
|
||||
],
|
||||
'expected' => [
|
||||
'id' => 14047981,
|
||||
'mal_id' => null,
|
||||
'data' => [
|
||||
'status' => 'current',
|
||||
'rating' => 4,
|
||||
@ -57,6 +58,7 @@ class AnimeListTransformerTest extends AnimeClient_TestCase {
|
||||
], [
|
||||
'input' => [
|
||||
'id' => 14047981,
|
||||
'mal_id' => '12345',
|
||||
'watching_status' => 'current',
|
||||
'user_rating' => 8,
|
||||
'episodes_watched' => 38,
|
||||
@ -68,6 +70,7 @@ class AnimeListTransformerTest extends AnimeClient_TestCase {
|
||||
],
|
||||
'expected' => [
|
||||
'id' => 14047981,
|
||||
'mal_id' => '12345',
|
||||
'data' => [
|
||||
'status' => 'current',
|
||||
'rating' => 4,
|
||||
|
71
tests/API/Kitsu/Transformer/MangaListTransformerTest.php
Normal file
71
tests/API/Kitsu/Transformer/MangaListTransformerTest.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Aviat\AnimeClient\Tests\API\Kitsu\Transformer;
|
||||
|
||||
use AnimeClient_TestCase;
|
||||
use Aviat\AnimeClient\API\JsonAPI;
|
||||
use Aviat\AnimeClient\API\Kitsu\Transformer\MangaListTransformer;
|
||||
use Aviat\Ion\Json;
|
||||
|
||||
class MangaListTransformerTest extends AnimeClient_TestCase {
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$this->dir = AnimeClient_TestCase::TEST_DATA_DIR . '/Kitsu';
|
||||
|
||||
$rawBefore = Json::decodeFile("{$this->dir}/mangaListBeforeTransform.json");
|
||||
$this->beforeTransform = JsonAPI::inlineRawIncludes($rawBefore, 'manga');
|
||||
$this->afterTransform = Json::decodeFile("{$this->dir}/mangaListAfterTransform.json");
|
||||
|
||||
$this->transformer = new MangaListTransformer();
|
||||
}
|
||||
|
||||
public function testTransform()
|
||||
{
|
||||
$expected = $this->afterTransform;
|
||||
$actual = $this->transformer->transformCollection($this->beforeTransform);
|
||||
|
||||
// Json::encodeFile("{$this->dir}/mangaListAfterTransform.json", $actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
public function testUntransform()
|
||||
{
|
||||
$input = [
|
||||
'id' => "15084773",
|
||||
'chapters_read' => 67,
|
||||
'manga' => [
|
||||
'titles' => ["Bokura wa Minna Kawaisou"],
|
||||
'alternate_title' => NULL,
|
||||
'slug' => "bokura-wa-minna-kawaisou",
|
||||
'url' => "https://kitsu.io/manga/bokura-wa-minna-kawaisou",
|
||||
'type' => 'manga',
|
||||
'image' => 'https://media.kitsu.io/manga/poster_images/20286/small.jpg?1434293999',
|
||||
'genres' => [],
|
||||
],
|
||||
'status' => 'current',
|
||||
'notes' => '',
|
||||
'rereading' => false,
|
||||
'reread_count' => 0,
|
||||
'new_rating' => 9,
|
||||
];
|
||||
|
||||
$actual = $this->transformer->untransform($input);
|
||||
$expected = [
|
||||
'id' => '15084773',
|
||||
'data' => [
|
||||
'status' => 'current',
|
||||
'progress' => 67,
|
||||
'reconsuming' => false,
|
||||
'reconsumeCount' => 0,
|
||||
'notes' => '',
|
||||
'rating' => 4.5
|
||||
]
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
}
|
34
tests/API/Kitsu/Transformer/MangaTransformerTest.php
Normal file
34
tests/API/Kitsu/Transformer/MangaTransformerTest.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Aviat\AnimeClient\Tests\API\Kitsu\Transformer;
|
||||
|
||||
use AnimeClient_TestCase;
|
||||
use Aviat\AnimeClient\API\JsonAPI;
|
||||
use Aviat\AnimeClient\API\Kitsu\Transformer\MangaTransformer;
|
||||
use Aviat\Ion\Json;
|
||||
|
||||
class MangaTransformerTest extends AnimeClient_TestCase {
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$this->dir = AnimeClient_TestCase::TEST_DATA_DIR . '/Kitsu';
|
||||
|
||||
$data = Json::decodeFile("{$this->dir}/mangaBeforeTransform.json");
|
||||
$baseData = $data['data'][0]['attributes'];
|
||||
$baseData['included'] = $data['included'];
|
||||
$this->beforeTransform = $baseData;
|
||||
$this->afterTransform = Json::decodeFile("{$this->dir}/mangaAfterTransform.json");
|
||||
|
||||
$this->transformer = new MangaTransformer();
|
||||
}
|
||||
|
||||
public function testTransform()
|
||||
{
|
||||
$actual = $this->transformer->transform($this->beforeTransform);
|
||||
$expected = $this->afterTransform;
|
||||
//Json::encodeFile("{$this->dir}/mangaAfterTransform.json", $actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
}
|
59
tests/API/KitsuTest.php
Normal file
59
tests/API/KitsuTest.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Aviat\AnimeClient\Tests\API;
|
||||
|
||||
use Aviat\AnimeClient\API\Kitsu;
|
||||
use Aviat\AnimeClient\API\Kitsu\Enum\{
|
||||
AnimeAiringStatus,
|
||||
AnimeWatchingStatus,
|
||||
MangaReadingStatus
|
||||
};
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class KitsuTest extends TestCase {
|
||||
public function testGetStatusToSelectMap()
|
||||
{
|
||||
$this->assertEquals([
|
||||
AnimeWatchingStatus::WATCHING => 'Currently Watching',
|
||||
AnimeWatchingStatus::PLAN_TO_WATCH => 'Plan to Watch',
|
||||
AnimeWatchingStatus::COMPLETED => 'Completed',
|
||||
AnimeWatchingStatus::ON_HOLD => 'On Hold',
|
||||
AnimeWatchingStatus::DROPPED => 'Dropped'
|
||||
], Kitsu::getStatusToSelectMap());
|
||||
}
|
||||
|
||||
public function testGetStatusToMangaSelectMap()
|
||||
{
|
||||
$this->assertEquals([
|
||||
MangaReadingStatus::READING => 'Currently Reading',
|
||||
MangaReadingStatus::PLAN_TO_READ => 'Plan to Read',
|
||||
MangaReadingStatus::COMPLETED => 'Completed',
|
||||
MangaReadingStatus::ON_HOLD => 'On Hold',
|
||||
MangaReadingStatus::DROPPED => 'Dropped'
|
||||
], Kitsu::getStatusToMangaSelectMap());
|
||||
}
|
||||
|
||||
public function testGetAiringStatus()
|
||||
{
|
||||
$actual = Kitsu::getAiringStatus('next week', 'next year');
|
||||
$this->assertEquals(AnimeAiringStatus::NOT_YET_AIRED, $actual);
|
||||
}
|
||||
|
||||
public function testParseStreamingLinksEmpty()
|
||||
{
|
||||
$this->assertEquals([], Kitsu::parseStreamingLinks([]));
|
||||
}
|
||||
|
||||
public function testTitleIsUniqueEmpty()
|
||||
{
|
||||
$actual = Kitsu::filterTitles([
|
||||
'canonicalTitle' => 'Foo',
|
||||
'titles' => [
|
||||
null,
|
||||
''
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(['Foo'], $actual);
|
||||
}
|
||||
}
|
@ -87,10 +87,6 @@ class AnimeClient_TestCase extends TestCase {
|
||||
'routes' => [
|
||||
|
||||
]
|
||||
],
|
||||
'redis' => [
|
||||
'host' => (array_key_exists('REDIS_HOST', $_ENV)) ? $_ENV['REDIS_HOST'] : 'localhost',
|
||||
'database' => 13
|
||||
]
|
||||
];
|
||||
|
||||
@ -157,9 +153,9 @@ class AnimeClient_TestCase extends TestCase {
|
||||
*
|
||||
* @return mixed - the decoded data
|
||||
*/
|
||||
public function getMockFileData()
|
||||
public function getMockFileData(...$args)
|
||||
{
|
||||
$rawData = call_user_func_array([$this, 'getMockFile'], func_get_args());
|
||||
$rawData = $this->getMockFile(...$args);
|
||||
|
||||
return Json::decode($rawData);
|
||||
}
|
||||
|
@ -194,7 +194,7 @@ class DispatcherTest extends AnimeClient_TestCase {
|
||||
$this->assertEquals('//localhost/manga/all', $this->urlGenerator->default_url('manga'), "Incorrect default url");
|
||||
$this->assertEquals('//localhost/anime/watching', $this->urlGenerator->default_url('anime'), "Incorrect default url");
|
||||
|
||||
$this->setExpectedException('\InvalidArgumentException');
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->urlGenerator->default_url('foo');
|
||||
}
|
||||
|
||||
|
12
tests/test_data/Kitsu/mangaAfterTransform.json
Normal file
12
tests/test_data/Kitsu/mangaAfterTransform.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"title": "Bokura wa Minna Kawaisou",
|
||||
"en_title": null,
|
||||
"jp_title": "Bokura wa Minna Kawaisou",
|
||||
"cover_image": "https:\/\/media.kitsu.io\/manga\/poster_images\/20286\/small.jpg?1434293999",
|
||||
"manga_type": "manga",
|
||||
"chapter_count": "-",
|
||||
"volume_count": "-",
|
||||
"synopsis": "Usa, a high-school student aspiring to begin a bachelor lifestyle, moves into a new apartment only to discover that he not only shares a room with a perverted roommate that has an obsession for underaged girls, but also that another girl, Ritsu, a love-at-first-sight, is living in the same building as well!\n(Source: Kirei Cake)",
|
||||
"url": "https:\/\/kitsu.io\/manga\/bokura-wa-minna-kawaisou",
|
||||
"genres": ["Comedy","Romance","School","Slice of Life","Thriller"]
|
||||
}
|
197
tests/test_data/Kitsu/mangaBeforeTransform.json
Normal file
197
tests/test_data/Kitsu/mangaBeforeTransform.json
Normal file
@ -0,0 +1,197 @@
|
||||
{
|
||||
"data": [{
|
||||
"id": "20286",
|
||||
"type": "manga",
|
||||
"links": {
|
||||
"self": "https://kitsu.io/api/edge/manga/20286"
|
||||
},
|
||||
"attributes": {
|
||||
"slug": "bokura-wa-minna-kawaisou",
|
||||
"synopsis": "Usa, a high-school student aspiring to begin a bachelor lifestyle, moves into a new apartment only to discover that he not only shares a room with a perverted roommate that has an obsession for underaged girls, but also that another girl, Ritsu, a love-at-first-sight, is living in the same building as well!\n(Source: Kirei Cake)",
|
||||
"coverImageTopOffset": 40,
|
||||
"titles": {
|
||||
"en": null,
|
||||
"en_jp": "Bokura wa Minna Kawaisou"
|
||||
},
|
||||
"canonicalTitle": "Bokura wa Minna Kawaisou",
|
||||
"abbreviatedTitles": null,
|
||||
"averageRating": 4.12281805954249,
|
||||
"ratingFrequencies": {
|
||||
"0.5": "0",
|
||||
"1.0": "1",
|
||||
"1.5": "0",
|
||||
"2.0": "1",
|
||||
"2.5": "2",
|
||||
"3.0": "6",
|
||||
"3.5": "21",
|
||||
"4.0": "38",
|
||||
"4.5": "35",
|
||||
"5.0": "43",
|
||||
"nil": "16"
|
||||
},
|
||||
"favoritesCount": 0,
|
||||
"startDate": "2010-01-01",
|
||||
"endDate": null,
|
||||
"popularityRank": 262,
|
||||
"ratingRank": 127,
|
||||
"ageRating": "PG",
|
||||
"ageRatingGuide": null,
|
||||
"posterImage": {
|
||||
"tiny": "https://media.kitsu.io/manga/poster_images/20286/tiny.jpg?1434293999",
|
||||
"small": "https://media.kitsu.io/manga/poster_images/20286/small.jpg?1434293999",
|
||||
"medium": "https://media.kitsu.io/manga/poster_images/20286/medium.jpg?1434293999",
|
||||
"large": "https://media.kitsu.io/manga/poster_images/20286/large.jpg?1434293999",
|
||||
"original": "https://media.kitsu.io/manga/poster_images/20286/original.jpg?1434293999"
|
||||
},
|
||||
"coverImage": {
|
||||
"small": "https://media.kitsu.io/manga/cover_images/20286/small.jpg?1430793688",
|
||||
"large": "https://media.kitsu.io/manga/cover_images/20286/large.jpg?1430793688",
|
||||
"original": "https://media.kitsu.io/manga/cover_images/20286/original.jpg?1430793688"
|
||||
},
|
||||
"subtype": "manga",
|
||||
"chapterCount": null,
|
||||
"volumeCount": 0,
|
||||
"serialization": "Young King Ours",
|
||||
"mangaType": "manga"
|
||||
},
|
||||
"relationships": {
|
||||
"genres": {
|
||||
"links": {
|
||||
"self": "https://kitsu.io/api/edge/manga/20286/relationships/genres",
|
||||
"related": "https://kitsu.io/api/edge/manga/20286/genres"
|
||||
},
|
||||
"data": [{
|
||||
"type": "genres",
|
||||
"id": "3"
|
||||
}, {
|
||||
"type": "genres",
|
||||
"id": "24"
|
||||
}, {
|
||||
"type": "genres",
|
||||
"id": "16"
|
||||
}, {
|
||||
"type": "genres",
|
||||
"id": "14"
|
||||
}, {
|
||||
"type": "genres",
|
||||
"id": "18"
|
||||
}]
|
||||
},
|
||||
"castings": {
|
||||
"links": {
|
||||
"self": "https://kitsu.io/api/edge/manga/20286/relationships/castings",
|
||||
"related": "https://kitsu.io/api/edge/manga/20286/castings"
|
||||
}
|
||||
},
|
||||
"installments": {
|
||||
"links": {
|
||||
"self": "https://kitsu.io/api/edge/manga/20286/relationships/installments",
|
||||
"related": "https://kitsu.io/api/edge/manga/20286/installments"
|
||||
}
|
||||
},
|
||||
"mappings": {
|
||||
"links": {
|
||||
"self": "https://kitsu.io/api/edge/manga/20286/relationships/mappings",
|
||||
"related": "https://kitsu.io/api/edge/manga/20286/mappings"
|
||||
},
|
||||
"data": [{
|
||||
"type": "mappings",
|
||||
"id": "48014"
|
||||
}]
|
||||
},
|
||||
"reviews": {
|
||||
"links": {
|
||||
"self": "https://kitsu.io/api/edge/manga/20286/relationships/reviews",
|
||||
"related": "https://kitsu.io/api/edge/manga/20286/reviews"
|
||||
}
|
||||
},
|
||||
"mediaRelationships": {
|
||||
"links": {
|
||||
"self": "https://kitsu.io/api/edge/manga/20286/relationships/media-relationships",
|
||||
"related": "https://kitsu.io/api/edge/manga/20286/media-relationships"
|
||||
}
|
||||
}
|
||||
}
|
||||
}],
|
||||
"included": [{
|
||||
"id": "3",
|
||||
"type": "genres",
|
||||
"links": {
|
||||
"self": "https://kitsu.io/api/edge/genres/3"
|
||||
},
|
||||
"attributes": {
|
||||
"name": "Comedy",
|
||||
"slug": "comedy",
|
||||
"description": null
|
||||
}
|
||||
}, {
|
||||
"id": "24",
|
||||
"type": "genres",
|
||||
"links": {
|
||||
"self": "https://kitsu.io/api/edge/genres/24"
|
||||
},
|
||||
"attributes": {
|
||||
"name": "School",
|
||||
"slug": "school",
|
||||
"description": null
|
||||
}
|
||||
}, {
|
||||
"id": "16",
|
||||
"type": "genres",
|
||||
"links": {
|
||||
"self": "https://kitsu.io/api/edge/genres/16"
|
||||
},
|
||||
"attributes": {
|
||||
"name": "Slice of Life",
|
||||
"slug": "slice-of-life",
|
||||
"description": ""
|
||||
}
|
||||
}, {
|
||||
"id": "14",
|
||||
"type": "genres",
|
||||
"links": {
|
||||
"self": "https://kitsu.io/api/edge/genres/14"
|
||||
},
|
||||
"attributes": {
|
||||
"name": "Romance",
|
||||
"slug": "romance",
|
||||
"description": ""
|
||||
}
|
||||
}, {
|
||||
"id": "18",
|
||||
"type": "genres",
|
||||
"links": {
|
||||
"self": "https://kitsu.io/api/edge/genres/18"
|
||||
},
|
||||
"attributes": {
|
||||
"name": "Thriller",
|
||||
"slug": "thriller",
|
||||
"description": null
|
||||
}
|
||||
}, {
|
||||
"id": "48014",
|
||||
"type": "mappings",
|
||||
"links": {
|
||||
"self": "https://kitsu.io/api/edge/mappings/48014"
|
||||
},
|
||||
"attributes": {
|
||||
"externalSite": "myanimelist/manga",
|
||||
"externalId": "26769"
|
||||
},
|
||||
"relationships": {
|
||||
"media": {
|
||||
"links": {
|
||||
"self": "https://kitsu.io/api/edge/mappings/48014/relationships/media",
|
||||
"related": "https://kitsu.io/api/edge/mappings/48014/media"
|
||||
}
|
||||
}
|
||||
}
|
||||
}],
|
||||
"meta": {
|
||||
"count": 1
|
||||
},
|
||||
"links": {
|
||||
"first": "https://kitsu.io/api/edge/manga?filter%5Bslug%5D=bokura-wa-minna-kawaisou&include=genres%2Cmappings&page%5Blimit%5D=10&page%5Boffset%5D=0",
|
||||
"last": "https://kitsu.io/api/edge/manga?filter%5Bslug%5D=bokura-wa-minna-kawaisou&include=genres%2Cmappings&page%5Blimit%5D=10&page%5Boffset%5D=0"
|
||||
}
|
||||
}
|
241
tests/test_data/Kitsu/mangaListAfterTransform.json
Normal file
241
tests/test_data/Kitsu/mangaListAfterTransform.json
Normal file
@ -0,0 +1,241 @@
|
||||
[{
|
||||
"id": "15084773",
|
||||
"chapters": {
|
||||
"read": 67,
|
||||
"total": "-"
|
||||
},
|
||||
"volumes": {
|
||||
"read": "-",
|
||||
"total": "-"
|
||||
},
|
||||
"manga": {
|
||||
"titles": ["Bokura wa Minna Kawaisou"],
|
||||
"alternate_title": null,
|
||||
"slug": "bokura-wa-minna-kawaisou",
|
||||
"url": "https:\/\/kitsu.io\/manga\/bokura-wa-minna-kawaisou",
|
||||
"type": "manga",
|
||||
"image": "https:\/\/media.kitsu.io\/manga\/poster_images\/20286\/small.jpg?1434293999",
|
||||
"genres": []
|
||||
},
|
||||
"reading_status": "current",
|
||||
"notes": "",
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 9
|
||||
}, {
|
||||
"id": "15085607",
|
||||
"chapters": {
|
||||
"read": 17,
|
||||
"total": 120
|
||||
},
|
||||
"volumes": {
|
||||
"read": "-",
|
||||
"total": 14
|
||||
},
|
||||
"manga": {
|
||||
"titles": ["Love Hina"],
|
||||
"alternate_title": null,
|
||||
"slug": "love-hina",
|
||||
"url": "https:\/\/kitsu.io\/manga\/love-hina",
|
||||
"type": "manga",
|
||||
"image": "https:\/\/media.kitsu.io\/manga\/poster_images\/47\/small.jpg?1434249493",
|
||||
"genres": []
|
||||
},
|
||||
"reading_status": "current",
|
||||
"notes": "",
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 7
|
||||
}, {
|
||||
"id": "15084529",
|
||||
"chapters": {
|
||||
"read": 16,
|
||||
"total": "-"
|
||||
},
|
||||
"volumes": {
|
||||
"read": "-",
|
||||
"total": "-"
|
||||
},
|
||||
"manga": {
|
||||
"titles": ["Yamada-kun to 7-nin no Majo", "Yamada-kun and the Seven Witches"],
|
||||
"alternate_title": null,
|
||||
"slug": "yamada-kun-to-7-nin-no-majo",
|
||||
"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": []
|
||||
},
|
||||
"reading_status": "current",
|
||||
"notes": "",
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 9
|
||||
}, {
|
||||
"id": "15312827",
|
||||
"chapters": {
|
||||
"read": 68,
|
||||
"total": "-"
|
||||
},
|
||||
"volumes": {
|
||||
"read": "-",
|
||||
"total": "-"
|
||||
},
|
||||
"manga": {
|
||||
"titles": ["ReLIFE"],
|
||||
"alternate_title": null,
|
||||
"slug": "relife",
|
||||
"url": "https:\/\/kitsu.io\/manga\/relife",
|
||||
"type": "manga",
|
||||
"image": "https:\/\/media.kitsu.io\/manga\/poster_images\/27175\/small.jpg?1464379411",
|
||||
"genres": []
|
||||
},
|
||||
"reading_status": "current",
|
||||
"notes": "",
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": "-"
|
||||
}, {
|
||||
"id": "15084772",
|
||||
"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,
|
||||
"total": "-"
|
||||
},
|
||||
"volumes": {
|
||||
"read": "-",
|
||||
"total": "-"
|
||||
},
|
||||
"manga": {
|
||||
"titles": ["Shishunki Bitter Change"],
|
||||
"alternate_title": null,
|
||||
"slug": "shishunki-bitter-change",
|
||||
"url": "https:\/\/kitsu.io\/manga\/shishunki-bitter-change",
|
||||
"type": "manga",
|
||||
"image": "https:\/\/media.kitsu.io\/manga\/poster_images\/25512\/small.jpg?1434305092",
|
||||
"genres": []
|
||||
},
|
||||
"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",
|
||||
"notes": "",
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 8
|
||||
}]
|
1648
tests/test_data/Kitsu/mangaListBeforeTransform.json
Normal file
1648
tests/test_data/Kitsu/mangaListBeforeTransform.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
[{"id":8710,"mal_id":24705,"slug":"ore-twintails-ni-narimasu","status":"Finished Airing","url":"https://hummingbird.me/anime/ore-twintails-ni-narimasu","title":"Ore, Twintails ni Narimasu.","alternate_title":"Gonna be the Twin-Tails!!","episode_count":12,"episode_length":24,"cover_image":"https://static.hummingbird.me/anime/poster_images/000/008/710/large/ore-twintails-ni-narimasu.jpg?1416244663","synopsis":"Mitsuka Souji is a first year high school student who greatly loves the \"twintails\" hairstyle. One day a beautiful girl, Twoearle, who comes from another world suddenly appeared in front of him and gave him the power to transform into the twintails warrior TailRed. Now Souji, with the help of his childhood friend Tsube Aika who can becomes the twintails warrior TailBlue, must fight in order to protect the peace on earth.\n\n(Source: Wikipedia)","show_type":"TV","started_airing":"2014-10-10","finished_airing":"2014-12-26","community_rating":3.32172789396078,"age_rating":"PG13","genres":[{"name":"Action"},{"name":"Comedy"},{"name":"Fantasy"},{"name":"Romance"},{"name":"School"},{"name":"Gender Bender"}]}]
|
File diff suppressed because one or more lines are too long
0
tests/test_data/cache/.gitkeep
vendored
0
tests/test_data/cache/.gitkeep
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,961 +0,0 @@
|
||||
[{
|
||||
"chapters": {
|
||||
"read": 6,
|
||||
"total": 120
|
||||
},
|
||||
"volumes": {
|
||||
"read": 1,
|
||||
"total": 14
|
||||
},
|
||||
"manga": {
|
||||
"title": "Love Hina",
|
||||
"alternate_title": null,
|
||||
"slug": "love-hina",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/love-hina",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/000\/047\/large\/52139.jpg?1434249493",
|
||||
"genres": ["Comedy", "Ecchi", "Harem", "Romance"]
|
||||
},
|
||||
"id": 401735,
|
||||
"reading_status": "Currently Reading",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": "-"
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 0,
|
||||
"total": "-"
|
||||
},
|
||||
"volumes": {
|
||||
"read": 0,
|
||||
"total": 2
|
||||
},
|
||||
"manga": {
|
||||
"title": "Murder Incarnation",
|
||||
"alternate_title": null,
|
||||
"slug": "murder-incarnation",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/murder-incarnation",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/026\/632\/large\/115683.jpg?1434307495",
|
||||
"genres": ["Psychological"]
|
||||
},
|
||||
"id": 400982,
|
||||
"reading_status": "Plan to Read",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": "-"
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 0,
|
||||
"total": 17
|
||||
},
|
||||
"volumes": {
|
||||
"read": 0,
|
||||
"total": 3
|
||||
},
|
||||
"manga": {
|
||||
"title": "DOLL The Hotel Detective",
|
||||
"alternate_title": null,
|
||||
"slug": "doll-the-hotel-detective",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/doll-the-hotel-detective",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/010\/076\/large\/34469.jpg?1434271227",
|
||||
"genres": ["Action"]
|
||||
},
|
||||
"id": 400980,
|
||||
"reading_status": "Plan to Read",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": "-"
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 0,
|
||||
"total": "-"
|
||||
},
|
||||
"volumes": {
|
||||
"read": 0,
|
||||
"total": "-"
|
||||
},
|
||||
"manga": {
|
||||
"title": "Fuuka",
|
||||
"alternate_title": null,
|
||||
"slug": "fuuka",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/fuuka",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/025\/292\/large\/4407752-04.jpg?1434304607",
|
||||
"genres": ["Ecchi", "Romance", "School"]
|
||||
},
|
||||
"id": 400978,
|
||||
"reading_status": "Plan to Read",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": "-"
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 0,
|
||||
"total": "-"
|
||||
},
|
||||
"volumes": {
|
||||
"read": 0,
|
||||
"total": "-"
|
||||
},
|
||||
"manga": {
|
||||
"title": "Yamada-kun to 7-nin no Majo",
|
||||
"alternate_title": "Yamada-kun and the Seven Witches",
|
||||
"slug": "yamada-kun-to-7-nin-no-majo",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/yamada-kun-to-7-nin-no-majo",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/011\/777\/large\/82656l.jpg?1438784325",
|
||||
"genres": ["Comedy", "Ecchi", "Gender Bender", "Romance", "School", "Supernatural"]
|
||||
},
|
||||
"id": 400977,
|
||||
"reading_status": "Plan to Read",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": "-"
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 86,
|
||||
"total": "-"
|
||||
},
|
||||
"volumes": {
|
||||
"read": 12,
|
||||
"total": "-"
|
||||
},
|
||||
"manga": {
|
||||
"title": "Yotsubato!",
|
||||
"alternate_title": null,
|
||||
"slug": "yotsubato",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/yotsubato",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/000\/272\/large\/Yotsuba-Cover-Images-yotsuba-and-5465671-434-600.jpg?1434249971",
|
||||
"genres": ["Comedy", "Slice of Life"]
|
||||
},
|
||||
"id": 400904,
|
||||
"reading_status": "On Hold",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 9
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 111,
|
||||
"total": 111
|
||||
},
|
||||
"volumes": {
|
||||
"read": 13,
|
||||
"total": 13
|
||||
},
|
||||
"manga": {
|
||||
"title": "Wagatsuma-san wa Ore no Yome",
|
||||
"alternate_title": "My Wife Is Wagatsuma-san",
|
||||
"slug": "wagatsuma-san-wa-ore-no-yome",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/wagatsuma-san-wa-ore-no-yome",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/005\/516\/large\/48035.jpg?1434261178",
|
||||
"genres": ["Comedy", "Romance", "School", "Slice of Life"]
|
||||
},
|
||||
"id": 400903,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 8
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 4,
|
||||
"total": 4
|
||||
},
|
||||
"volumes": {
|
||||
"read": 1,
|
||||
"total": 1
|
||||
},
|
||||
"manga": {
|
||||
"title": "Usotsuki Marriage",
|
||||
"alternate_title": "Deceitful Marriage",
|
||||
"slug": "usotsuki-marriage",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/usotsuki-marriage",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/023\/295\/large\/12823.jpg?1434300356",
|
||||
"genres": ["Comedy", "Romance"]
|
||||
},
|
||||
"id": 400902,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 8
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 28,
|
||||
"total": 62
|
||||
},
|
||||
"volumes": {
|
||||
"read": 1,
|
||||
"total": 10
|
||||
},
|
||||
"manga": {
|
||||
"title": "Usagi Drop",
|
||||
"alternate_title": "Bunny Drop",
|
||||
"slug": "usagi-drop",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/usagi-drop",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/007\/629\/large\/53493.jpg?1434265873",
|
||||
"genres": ["Comedy", "Drama", "Slice of Life"]
|
||||
},
|
||||
"id": 400901,
|
||||
"reading_status": "Currently Reading",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 8
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 269,
|
||||
"total": 366
|
||||
},
|
||||
"volumes": {
|
||||
"read": 12,
|
||||
"total": 34
|
||||
},
|
||||
"manga": {
|
||||
"title": "Urusei Yatsura",
|
||||
"alternate_title": "Those Obnoxious Aliens",
|
||||
"slug": "urusei-yatsura",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/urusei-yatsura",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/000\/702\/large\/3465.jpg?1434250836",
|
||||
"genres": ["Comedy", "Romance", "Sci-Fi"]
|
||||
},
|
||||
"id": 400900,
|
||||
"reading_status": "Dropped",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 8
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 6,
|
||||
"total": 6
|
||||
},
|
||||
"volumes": {
|
||||
"read": 1,
|
||||
"total": 1
|
||||
},
|
||||
"manga": {
|
||||
"title": "SOLD OUT!",
|
||||
"alternate_title": null,
|
||||
"slug": "sold-out",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/sold-out",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/009\/061\/large\/4134.jpg?1434268999",
|
||||
"genres": ["Drama", "Romance"]
|
||||
},
|
||||
"id": 400899,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 7
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 190,
|
||||
"total": "-"
|
||||
},
|
||||
"volumes": {
|
||||
"read": 30,
|
||||
"total": "-"
|
||||
},
|
||||
"manga": {
|
||||
"title": "Skip Beat!",
|
||||
"alternate_title": null,
|
||||
"slug": "skip-beat",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/skip-beat",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/001\/388\/large\/26110.jpg?1434252296",
|
||||
"genres": ["Comedy", "Drama", "Romance"]
|
||||
},
|
||||
"id": 400898,
|
||||
"reading_status": "On Hold",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 10
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 10,
|
||||
"total": 10
|
||||
},
|
||||
"volumes": {
|
||||
"read": 2,
|
||||
"total": 2
|
||||
},
|
||||
"manga": {
|
||||
"title": "Samurai Champloo",
|
||||
"alternate_title": null,
|
||||
"slug": "samurai-champloo",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/samurai-champloo",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/001\/171\/large\/56743.jpg?1434251812",
|
||||
"genres": ["Action", "Adventure", "Comedy"]
|
||||
},
|
||||
"id": 400897,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 8
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 407,
|
||||
"total": 407
|
||||
},
|
||||
"volumes": {
|
||||
"read": 38,
|
||||
"total": 38
|
||||
},
|
||||
"manga": {
|
||||
"title": "Ranma \u00bd",
|
||||
"alternate_title": null,
|
||||
"slug": "ranma",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/ranma",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/000\/062\/large\/5616.jpg?1434249522",
|
||||
"genres": ["Action", "Comedy", "Ecchi", "Gender Bender", "Harem", "Martial Arts", "Romance", "School"]
|
||||
},
|
||||
"id": 400896,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 9
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 5,
|
||||
"total": 5
|
||||
},
|
||||
"volumes": {
|
||||
"read": 1,
|
||||
"total": 1
|
||||
},
|
||||
"manga": {
|
||||
"title": "Otome no Iroha!",
|
||||
"alternate_title": null,
|
||||
"slug": "otome-no-iroha",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/otome-no-iroha",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/022\/436\/large\/12188.jpg?1434298544",
|
||||
"genres": ["Comedy", "Ecchi", "School"]
|
||||
},
|
||||
"id": 400895,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 7
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 26,
|
||||
"total": 26
|
||||
},
|
||||
"volumes": {
|
||||
"read": 4,
|
||||
"total": 4
|
||||
},
|
||||
"manga": {
|
||||
"title": "Ore no Imouto ga Konnani Kawaii Wake ga Nai",
|
||||
"alternate_title": "Oreimo",
|
||||
"slug": "ore-no-imouto-ga-konnani-kawaii-wake-ga-nai",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/ore-no-imouto-ga-konnani-kawaii-wake-ga-nai",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/000\/714\/large\/71Zqo5clDfL._SL1200_.jpg?1439399108",
|
||||
"genres": ["Comedy", "Drama", "Ecchi", "School"]
|
||||
},
|
||||
"id": 400894,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 10
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 44,
|
||||
"total": "-"
|
||||
},
|
||||
"volumes": {
|
||||
"read": 6,
|
||||
"total": "-"
|
||||
},
|
||||
"manga": {
|
||||
"title": "Onii-chan no Koto nanka Zenzen Suki Janain Dakara ne!!",
|
||||
"alternate_title": "I don't like you at all, Big Brother!!",
|
||||
"slug": "onii-chan-no-koto-nanka-zenzen-suki-janain-dakara-ne",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/onii-chan-no-koto-nanka-zenzen-suki-janain-dakara-ne",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/021\/900\/large\/11793.jpg?1434297436",
|
||||
"genres": ["Comedy", "Ecchi", "Harem", "Romance", "School"]
|
||||
},
|
||||
"id": 400893,
|
||||
"reading_status": "On Hold",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 9
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 3,
|
||||
"total": 3
|
||||
},
|
||||
"volumes": {
|
||||
"read": 1,
|
||||
"total": 1
|
||||
},
|
||||
"manga": {
|
||||
"title": "Onegai, Sensei",
|
||||
"alternate_title": "Please, Teacher",
|
||||
"slug": "onegai-sensei-manga",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/onegai-sensei-manga",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/019\/137\/large\/9625.jpg?1434291544",
|
||||
"genres": ["Romance", "School"]
|
||||
},
|
||||
"id": 400892,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 9
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 0,
|
||||
"total": 22
|
||||
},
|
||||
"volumes": {
|
||||
"read": 0,
|
||||
"total": 4
|
||||
},
|
||||
"manga": {
|
||||
"title": "Obaa-chan wa Idol",
|
||||
"alternate_title": null,
|
||||
"slug": "obaa-chan-wa-idol",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/obaa-chan-wa-idol",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/008\/089\/large\/3634.jpg?1434266860",
|
||||
"genres": ["Comedy", "Drama", "Romance", "Supernatural"]
|
||||
},
|
||||
"id": 400891,
|
||||
"reading_status": "Dropped",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": "-"
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 96,
|
||||
"total": 96
|
||||
},
|
||||
"volumes": {
|
||||
"read": 12,
|
||||
"total": 12
|
||||
},
|
||||
"manga": {
|
||||
"title": "Nazo no Kanojo X",
|
||||
"alternate_title": "Mysterious Girlfriend X",
|
||||
"slug": "nazo-no-kanojo-x",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/nazo-no-kanojo-x",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/004\/029\/large\/1926.jpg?1434257986",
|
||||
"genres": ["Ecchi", "Mystery", "Romance", "School"]
|
||||
},
|
||||
"id": 400890,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 9
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 7,
|
||||
"total": 7
|
||||
},
|
||||
"volumes": {
|
||||
"read": 2,
|
||||
"total": 2
|
||||
},
|
||||
"manga": {
|
||||
"title": "Milk to Vitamin",
|
||||
"alternate_title": null,
|
||||
"slug": "milk-to-vitamin",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/milk-to-vitamin",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/007\/616\/large\/3843.jpg?1434265843",
|
||||
"genres": ["Comedy", "Drama", "Romance"]
|
||||
},
|
||||
"id": 400889,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 7
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 12,
|
||||
"total": 12
|
||||
},
|
||||
"volumes": {
|
||||
"read": 2,
|
||||
"total": 2
|
||||
},
|
||||
"manga": {
|
||||
"title": "Mama wa Shougaku 4 Nensei",
|
||||
"alternate_title": null,
|
||||
"slug": "mama-wa-shougaku-4-nensei",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/mama-wa-shougaku-4-nensei",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/007\/618\/large\/3464.jpg?1434265846",
|
||||
"genres": ["Comedy", "Drama", "Sci-Fi"]
|
||||
},
|
||||
"id": 400888,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 9
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 13,
|
||||
"total": 14
|
||||
},
|
||||
"volumes": {
|
||||
"read": 2,
|
||||
"total": 2
|
||||
},
|
||||
"manga": {
|
||||
"title": "Maburaho",
|
||||
"alternate_title": null,
|
||||
"slug": "maburaho-7719eaec-27ba-4375-847e-7b140a29257a",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/maburaho-7719eaec-27ba-4375-847e-7b140a29257a",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/001\/649\/large\/5194.jpg?1434252868",
|
||||
"genres": ["Comedy", "Harem", "Magic", "Romance"]
|
||||
},
|
||||
"id": 400887,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 8
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 49,
|
||||
"total": 49
|
||||
},
|
||||
"volumes": {
|
||||
"read": 8,
|
||||
"total": 8
|
||||
},
|
||||
"manga": {
|
||||
"title": "Kono Onee-san wa Fiction desu!?",
|
||||
"alternate_title": "Is this Girl for Real!?",
|
||||
"slug": "kono-onee-san-wa-fiction-desu",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/kono-onee-san-wa-fiction-desu",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/015\/503\/large\/74249.jpg?1434283423",
|
||||
"genres": ["Comedy", "Ecchi", "Romance", "School"]
|
||||
},
|
||||
"id": 400886,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 8
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 5,
|
||||
"total": 5
|
||||
},
|
||||
"volumes": {
|
||||
"read": 1,
|
||||
"total": 1
|
||||
},
|
||||
"manga": {
|
||||
"title": "Kimi Dake no Devil",
|
||||
"alternate_title": "A Devil Just for You",
|
||||
"slug": "kimi-dake-no-devil",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/kimi-dake-no-devil",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/014\/606\/large\/7082.jpg?1434281449",
|
||||
"genres": ["Comedy", "Fantasy"]
|
||||
},
|
||||
"id": 400885,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 8
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 2,
|
||||
"total": 2
|
||||
},
|
||||
"volumes": {
|
||||
"read": 1,
|
||||
"total": "-"
|
||||
},
|
||||
"manga": {
|
||||
"title": "Kanaete Aizen",
|
||||
"alternate_title": null,
|
||||
"slug": "kanaete-aizen",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/kanaete-aizen",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/013\/080\/large\/66091.jpg?1434278021",
|
||||
"genres": ["Ecchi", "Romance", "School", "Supernatural"]
|
||||
},
|
||||
"id": 400884,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 9
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 34,
|
||||
"total": "-"
|
||||
},
|
||||
"volumes": {
|
||||
"read": 0,
|
||||
"total": "-"
|
||||
},
|
||||
"manga": {
|
||||
"title": "Joshikausei",
|
||||
"alternate_title": null,
|
||||
"slug": "joshikausei",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/joshikausei",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/025\/491\/large\/121107.jpg?1434305043",
|
||||
"genres": ["Comedy", "School", "Slice of Life"]
|
||||
},
|
||||
"id": 400883,
|
||||
"reading_status": "Currently Reading",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 8
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 558,
|
||||
"total": 558
|
||||
},
|
||||
"volumes": {
|
||||
"read": 56,
|
||||
"total": 56
|
||||
},
|
||||
"manga": {
|
||||
"title": "InuYasha",
|
||||
"alternate_title": null,
|
||||
"slug": "inuyasha",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/inuyasha",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/001\/531\/large\/32468.jpg?1434252597",
|
||||
"genres": ["Adventure", "Comedy", "Demons", "Drama", "Fantasy", "Historical", "Romance", "Supernatural"]
|
||||
},
|
||||
"id": 400882,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 7
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 19,
|
||||
"total": 19
|
||||
},
|
||||
"volumes": {
|
||||
"read": 3,
|
||||
"total": 3
|
||||
},
|
||||
"manga": {
|
||||
"title": "Inumimi",
|
||||
"alternate_title": null,
|
||||
"slug": "inumimi",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/inumimi",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/004\/025\/large\/1924.jpg?1434257977",
|
||||
"genres": ["Comedy", "Ecchi", "Harem", "Romance"]
|
||||
},
|
||||
"id": 400881,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 9
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 41,
|
||||
"total": 41
|
||||
},
|
||||
"volumes": {
|
||||
"read": 5,
|
||||
"total": 5
|
||||
},
|
||||
"manga": {
|
||||
"title": "Inu Neko Jump",
|
||||
"alternate_title": "Dog Cat Jump",
|
||||
"slug": "inu-neko-jump",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/inu-neko-jump",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/002\/136\/large\/978.jpg?1434253893",
|
||||
"genres": ["Comedy", "Romance"]
|
||||
},
|
||||
"id": 400880,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 8
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 9,
|
||||
"total": 9
|
||||
},
|
||||
"volumes": {
|
||||
"read": 2,
|
||||
"total": 2
|
||||
},
|
||||
"manga": {
|
||||
"title": "I \u2665 HS",
|
||||
"alternate_title": "I Love High School",
|
||||
"slug": "i-hs",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/i-hs",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/023\/195\/large\/17718.jpg?1434300143",
|
||||
"genres": ["Comedy", "Drama", "Romance", "School"]
|
||||
},
|
||||
"id": 400879,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 7
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 48,
|
||||
"total": 48
|
||||
},
|
||||
"volumes": {
|
||||
"read": 8,
|
||||
"total": 8
|
||||
},
|
||||
"manga": {
|
||||
"title": "Futaba-kun Change\u2661",
|
||||
"alternate_title": null,
|
||||
"slug": "futaba-kun-change",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/futaba-kun-change",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/004\/367\/large\/2114.jpg?1434258719",
|
||||
"genres": ["Comedy", "Ecchi", "Romance", "School"]
|
||||
},
|
||||
"id": 400878,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 8
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 58,
|
||||
"total": 58
|
||||
},
|
||||
"volumes": {
|
||||
"read": 9,
|
||||
"total": 9
|
||||
},
|
||||
"manga": {
|
||||
"title": "Full Metal Panic!",
|
||||
"alternate_title": null,
|
||||
"slug": "full-metal-panic",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/full-metal-panic",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/001\/735\/large\/5190.jpg?1434253058",
|
||||
"genres": ["Action", "Comedy", "Mecha", "Military", "Romance"]
|
||||
},
|
||||
"id": 400877,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 10
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 88,
|
||||
"total": 88
|
||||
},
|
||||
"volumes": {
|
||||
"read": 8,
|
||||
"total": 8
|
||||
},
|
||||
"manga": {
|
||||
"title": "Chobits",
|
||||
"alternate_title": null,
|
||||
"slug": "chobits",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/chobits",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/000\/278\/large\/19440.jpg?1434249984",
|
||||
"genres": ["Comedy", "Ecchi", "Psychological", "Romance", "Sci-Fi"]
|
||||
},
|
||||
"id": 400876,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 9
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 4,
|
||||
"total": 4
|
||||
},
|
||||
"volumes": {
|
||||
"read": 1,
|
||||
"total": 1
|
||||
},
|
||||
"manga": {
|
||||
"title": "Change 2!!",
|
||||
"alternate_title": null,
|
||||
"slug": "change-2",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/change-2",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/016\/486\/large\/8072.jpg?1434285566",
|
||||
"genres": ["Action", "Ecchi", "School"]
|
||||
},
|
||||
"id": 400875,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 10
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 62,
|
||||
"total": "-"
|
||||
},
|
||||
"volumes": {
|
||||
"read": 6,
|
||||
"total": "-"
|
||||
},
|
||||
"manga": {
|
||||
"title": "Bokura wa Minna Kawaisou",
|
||||
"alternate_title": null,
|
||||
"slug": "bokura-wa-minna-kawaisou",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/bokura-wa-minna-kawaisou",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/020\/286\/large\/17950117v1.jpg?1434293999",
|
||||
"genres": ["Comedy", "Romance", "School", "Slice of Life"]
|
||||
},
|
||||
"id": 400874,
|
||||
"reading_status": "Currently Reading",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 9
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 21,
|
||||
"total": 21
|
||||
},
|
||||
"volumes": {
|
||||
"read": 5,
|
||||
"total": 5
|
||||
},
|
||||
"manga": {
|
||||
"title": "Boku ni Natta Watashi",
|
||||
"alternate_title": "I Became a Boy",
|
||||
"slug": "boku-ni-natta-watashi",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/boku-ni-natta-watashi",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/001\/379\/large\/20745.jpg?1434252279",
|
||||
"genres": ["Drama", "Romance", "School"]
|
||||
},
|
||||
"id": 400873,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 8
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 30,
|
||||
"total": 61
|
||||
},
|
||||
"volumes": {
|
||||
"read": 5,
|
||||
"total": 18
|
||||
},
|
||||
"manga": {
|
||||
"title": "Bishoujo Senshi Sailor Moon",
|
||||
"alternate_title": "Sailor Moon",
|
||||
"slug": "bishoujo-senshi-sailor-moon",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/bishoujo-senshi-sailor-moon",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/000\/241\/large\/6601.jpg?1434249905",
|
||||
"genres": ["Drama", "Fantasy", "Magic", "Mahou Shoujo", "Romance"]
|
||||
},
|
||||
"id": 400872,
|
||||
"reading_status": "Dropped",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 8
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 4,
|
||||
"total": 4
|
||||
},
|
||||
"volumes": {
|
||||
"read": 1,
|
||||
"total": 1
|
||||
},
|
||||
"manga": {
|
||||
"title": "Anta Nanka Daikirai",
|
||||
"alternate_title": null,
|
||||
"slug": "anta-nanka-daikirai",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/anta-nanka-daikirai",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/024\/984\/large\/14617.jpg?1434303960",
|
||||
"genres": ["Drama", "Romance", "School"]
|
||||
},
|
||||
"id": 400871,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 7
|
||||
}, {
|
||||
"chapters": {
|
||||
"read": 10,
|
||||
"total": 10
|
||||
},
|
||||
"volumes": {
|
||||
"read": 2,
|
||||
"total": 2
|
||||
},
|
||||
"manga": {
|
||||
"title": "Akane-chan Overdrive",
|
||||
"alternate_title": null,
|
||||
"slug": "akane-chan-overdrive",
|
||||
"url": "https:\/\/hummingbird.me\/manga\/akane-chan-overdrive",
|
||||
"type": "Manga",
|
||||
"image": "https:\/\/static.hummingbird.me\/manga\/poster_images\/000\/001\/961\/large\/900.jpg?1434253542",
|
||||
"genres": ["Comedy", "Ecchi", "Gender Bender"]
|
||||
},
|
||||
"id": 400870,
|
||||
"reading_status": "Completed",
|
||||
"notes": null,
|
||||
"rereading": false,
|
||||
"reread": 0,
|
||||
"user_rating": 7
|
||||
}]
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user