Merge branch 'develop'

This commit is contained in:
Timothy Warren 2017-01-16 11:27:49 -05:00
commit 42f152b366
79 changed files with 19059 additions and 402 deletions

View File

@ -1,5 +1,8 @@
# Changelog # Changelog
## Version 4
* Updated to use Kitsu API after discontinuation of Hummingbird
## Version 3 ## Version 3
* Converted user configuration to toml files * Converted user configuration to toml files
* Added a caching layer for api calls, which resets upon updates from the * Added a caching layer for api calls, which resets upon updates from the

View File

@ -2,7 +2,6 @@
A self-hosted client that allows custom formatting of data from the hummingbird api A self-hosted client that allows custom formatting of data from the hummingbird api
[![Build Status](https://jenkins.timshomepage.net/buildStatus/icon?job=animeclient)](https://jenkins.timshomepage.net/job/animeclient/)
[![Build Status](https://travis-ci.org/timw4mail/HummingBirdAnimeClient.svg?branch=master)](https://travis-ci.org/timw4mail/HummingBirdAnimeClient) [![Build Status](https://travis-ci.org/timw4mail/HummingBirdAnimeClient.svg?branch=master)](https://travis-ci.org/timw4mail/HummingBirdAnimeClient)
[![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) [![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)

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient
@ -19,9 +19,15 @@ namespace Aviat\AnimeClient;
use Aura\Html\HelperLocatorFactory; use Aura\Html\HelperLocatorFactory;
use Aura\Router\RouterContainer; use Aura\Router\RouterContainer;
use Aura\Session\SessionFactory; use Aura\Session\SessionFactory;
use Aviat\AnimeClient\API\Kitsu\Auth as KitsuAuth; use Aviat\AnimeClient\API\Kitsu\{
use Aviat\AnimeClient\API\Kitsu\ListItem as KitsuListItem; Auth as KitsuAuth,
use Aviat\AnimeClient\API\Kitsu\KitsuModel; ListItem as KitsuListItem,
KitsuModel
};
use Aviat\AnimeClient\API\MAL\{
ListItem as MALListItem,
Model as MALModel
};
use Aviat\AnimeClient\Model; use Aviat\AnimeClient\Model;
use Aviat\Banker\Pool; use Aviat\Banker\Pool;
use Aviat\Ion\Config; use Aviat\Ion\Config;
@ -111,6 +117,15 @@ return function(array $config_array = []) {
$listItem->setContainer($container); $listItem->setContainer($container);
$model = new KitsuModel($listItem); $model = new KitsuModel($listItem);
$model->setContainer($container); $model->setContainer($container);
$cache = $container->get('cache');
$model->setCache($cache);
return $model;
});
$container->set('mal-model', function($container) {
$listItem = new MALListItem();
$listItem->setContainer($container);
$model = new MALModel($listItem);
$model->setContainer($container);
return $model; return $model;
}); });
$container->set('api-model', function($container) { $container->set('api-model', function($container) {

View File

@ -2,6 +2,10 @@
# Cache Setup # # Cache Setup #
################################################################################ ################################################################################
# See https://git.timshomepage.net/timw4mail/banker for more information
# Available drivers are memcache, memcached, redis or null
# Null cache driver means no caching
driver = "redis" driver = "redis"
[connection] [connection]

View File

@ -3,7 +3,7 @@
################################################################################ ################################################################################
# Username for anime and manga lists # Username for anime and manga lists
hummingbird_username = "timw4mail" kitsu_username = "timw4mail"
# Whose list is it? # Whose list is it?
whose_list = "Tim" whose_list = "Tim"
@ -14,6 +14,9 @@ show_anime_collection = true
# do you wish to show the manga collection? # do you wish to show the manga collection?
show_manga_collection = false show_manga_collection = false
# do you have a My Anime List account set up in mal.toml?
use_mal_api = false
# cache driver for api calls (NullDriver, SQLDriver, RedisDriver) # cache driver for api calls (NullDriver, SQLDriver, RedisDriver)
cache_driver = "NullDriver" cache_driver = "NullDriver"

View File

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

View File

@ -1,16 +0,0 @@
################################################################################
# Redis Cache Configuration #
################################################################################
# Host or socket to connect to
# Socket must be prefixed with 'unix:'
host = "127.0.0.1"
# Connection port
#port = 6379
# Connection password
#password = ""
# Database number
database = 13

View File

@ -11,11 +11,11 @@
<section class="media-wrap"> <section class="media-wrap">
<?php foreach($items as $item): ?> <?php foreach($items as $item): ?>
<?php if ($item['private'] && ! $auth->is_authenticated()) continue; ?> <?php if ($item['private'] && ! $auth->is_authenticated()) continue; ?>
<article class="media" id="<?= $item['id'] ?>"> <article class="media" data-kitsu-id="<?= $item['id'] ?>" data-mal-id="<?= $item['mal_id'] ?>">
<?php if ($auth->is_authenticated()): ?> <?php if ($auth->is_authenticated()): ?>
<button title="Increment episode count" class="plus_one" hidden>+1 Episode</button> <button title="Increment episode count" class="plus_one" hidden>+1 Episode</button>
<?php endif ?> <?php endif ?>
<?= $helper->img($item['anime']['image']); ?> <img src="<?= $item['anime']['image'] ?>" alt="" />
<div class="name"> <div class="name">
<a href="<?= $url->generate('anime.details', ['id' => $item['anime']['slug']]); ?>"> <a href="<?= $url->generate('anime.details', ['id' => $item['anime']['slug']]); ?>">
<?= array_shift($item['anime']['titles']) ?> <?= array_shift($item['anime']['titles']) ?>
@ -32,6 +32,7 @@
</span> </span>
</div> </div>
<?php endif ?> <?php endif ?>
<?php if ($item['private'] || $item['rewatching']): ?> <?php if ($item['private'] || $item['rewatching']): ?>
<div class="row"> <div class="row">
<?php foreach(['private', 'rewatching'] as $attr): ?> <?php foreach(['private', 'rewatching'] as $attr): ?>
@ -41,11 +42,29 @@
<?php endforeach ?> <?php endforeach ?>
</div> </div>
<?php endif ?> <?php endif ?>
<?php if ($item['rewatched'] > 0): ?> <?php if ($item['rewatched'] > 0): ?>
<div class="row"> <div class="row">
<div>Rewatched <?= $item['rewatched'] ?> time(s)</div> <div>Rewatched <?= $item['rewatched'] ?> time(s)</div>
</div> </div>
<?php endif ?> <?php endif ?>
<?php if (count($item['anime']['streaming_links']) > 0): ?>
<div class="row">
<?php foreach($item['anime']['streaming_links'] as $link): ?>
<div class="cover_streaming_link">
<?php if($link['meta']['link']): ?>
<a href="<?= $link['link']?>">
<?= $link['meta']['logo'] ?>
</a>
<?php else: ?>
<?= $link['meta']['logo'] ?>
<?php endif ?>
</div>
<?php endforeach ?>
</div>
<?php endif ?>
<div class="row"> <div class="row">
<div class="user_rating">Rating: <?= $item['user_rating'] ?> / 10</div> <div class="user_rating">Rating: <?= $item['user_rating'] ?> / 10</div>
<div class="completion">Episodes: <div class="completion">Episodes:

View File

@ -1,10 +1,10 @@
<main class="details"> <main class="details">
<section class="flex flex-no-wrap"> <section class="flex flex-no-wrap">
<div> <div>
<img class="cover" src="<?= $data['cover_image'] ?>" alt="<?= $data['title'] ?> cover image" /> <img class="cover" src="<?= $data['cover_image'] ?>" alt="" />
<br /> <br />
<br /> <br />
<table> <table class="media_details">
<tr> <tr>
<td class="align_right">Airing Status</td> <td class="align_right">Airing Status</td>
<td><?= $data['status'] ?></td> <td><?= $data['status'] ?></td>
@ -15,16 +15,18 @@
</tr> </tr>
<tr> <tr>
<td>Episode Count</td> <td>Episode Count</td>
<td><?= $data['episode_count'] ?></td> <td><?= $data['episode_count'] ?? '-' ?></td>
</tr> </tr>
<tr> <tr>
<td>Episode Length</td> <td>Episode Length</td>
<td><?= $data['episode_length'] ?> minutes</td> <td><?= $data['episode_length'] ?> minutes</td>
</tr> </tr>
<?php if ( ! empty($data['age_rating'])): ?>
<tr> <tr>
<td>Age Rating</td> <td>Age Rating</td>
<td><abbr title="<?= $data['age_rating_guide'] ?>"><?= $data['age_rating'] ?></abbr></td> <td><abbr title="<?= $data['age_rating_guide'] ?>"><?= $data['age_rating'] ?></abbr></td>
</tr> </tr>
<?php endif ?>
<tr> <tr>
<td>Genres</td> <td>Genres</td>
<td> <td>
@ -40,6 +42,39 @@
<?php endforeach ?> <?php endforeach ?>
<br /> <br />
<p><?= nl2br($data['synopsis']) ?></p> <p><?= nl2br($data['synopsis']) ?></p>
<?php if (count($data['streaming_links']) > 0): ?>
<hr />
<h4>Streaming on:</h4>
<table class="full_width invisible">
<thead>
<tr>
<th class="align_left">Service</th>
<th>Subtitles</th>
<th>Dubs</th>
</tr>
</thead>
<tbody>
<?php foreach($data['streaming_links'] as $streaming_link): ?>
<tr>
<td class="align_left">
<?php if ($streaming_link['meta']['link'] !== FALSE): ?>
<a href="<?= $streaming_link['link'] ?>">
<?= $streaming_link['meta']['logo'] ?>
&nbsp;&nbsp;<?= $streaming_link['meta']['name'] ?>
</a>
<?php else: ?>
<?= $streaming_link['meta']['logo'] ?>
&nbsp;&nbsp;<?= $streaming_link['meta']['name'] ?>
<?php endif ?>
</td>
<td><?= implode(', ', $streaming_link['subs']) ?></td>
<td><?= implode(', ', $streaming_link['dubs']) ?></td>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php endif ?>
<?php /*<pre><?= print_r($data, TRUE) ?></pre> */ ?>
</div> </div>
</section> </section>
</main> </main>

View File

@ -1,4 +1,5 @@
<?php if ($auth->is_authenticated()): ?> <?php if ($auth->is_authenticated()): ?>
<?php /* <pre><?= json_encode($item, \JSON_PRETTY_PRINT); ?></pre> */ ?>
<main> <main>
<h2>Edit Anime List Item</h2> <h2>Edit Anime List Item</h2>
<form action="<?= $action ?>" method="post"> <form action="<?= $action ?>" method="post">
@ -77,6 +78,7 @@
<td>&nbsp;</td> <td>&nbsp;</td>
<td> <td>
<input type="hidden" value="<?= $item['id'] ?>" name="id" /> <input type="hidden" value="<?= $item['id'] ?>" name="id" />
<input type="hidden" value="<?= $item['mal_id'] ?>" name="mal_id" />
<input type="hidden" value="true" name="edit" /> <input type="hidden" value="true" name="edit" />
<button type="submit">Submit</button> <button type="submit">Submit</button>
</td> </td>

View File

@ -19,7 +19,7 @@
<th>Type</th> <th>Type</th>
<th>Progress</th> <th>Progress</th>
<th>Rated</th> <th>Rated</th>
<th>Attributes</th> <th colspan="2">Attributes</th>
<th>Notes</th> <th>Notes</th>
<th>Genres</th> <th>Genres</th>
</tr> </tr>
@ -61,6 +61,17 @@
<?php endforeach ?> <?php endforeach ?>
</ul> </ul>
</td> </td>
<td>
<?php foreach($item['anime']['streaming_links'] as $link): ?>
<?php if ($link['meta']['link'] !== FALSE): ?>
<a href="<?= $link['link'] ?>">
<?= $link['meta']['logo'] ?>
</a>
<?php else: ?>
<?= $link['meta']['logo'] ?>
<?php endif ?>
<?php endforeach ?>
</td>
<td> <td>
<p><?= $escape->html($item['notes']) ?></p> <p><?= $escape->html($item['notes']) ?></p>
</td> </td>

View File

@ -17,13 +17,13 @@
[<a href="<?= $urlGenerator->default_url('manga') ?>">Manga List</a>] [<a href="<?= $urlGenerator->default_url('manga') ?>">Manga List</a>]
<?php endif ?> <?php endif ?>
</span> </span>
<?php /* if ($auth->is_authenticated()): ?> <?php if ($auth->is_authenticated()): ?>
<span class="flex-no-wrap">&nbsp;</span> <span class="flex-no-wrap">&nbsp;</span>
<span class="flex-no-wrap small-font"> <span class="flex-no-wrap small-font">
<button type="button" class="js-clear-cache user-btn">Clear API Cache</button> <button type="button" class="js-clear-cache user-btn">Clear API Cache</button>
</span> </span>
<span class="flex-no-wrap">&nbsp;</span> <span class="flex-no-wrap">&nbsp;</span>
<?php endif */ ?> <?php endif ?>
<span class="flex-no-wrap small-font"> <span class="flex-no-wrap small-font">
<?php if ($auth->is_authenticated()): ?> <?php if ($auth->is_authenticated()): ?>
<a class="bracketed" href="<?= $url->generate('logout') ?>">Logout</a> <a class="bracketed" href="<?= $url->generate('logout') ?>">Logout</a>

View File

@ -7,7 +7,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -4,7 +4,6 @@
stopOnFailure="false" stopOnFailure="false"
bootstrap="../tests/bootstrap.php" bootstrap="../tests/bootstrap.php"
beStrictAboutTestsThatDoNotTestAnything="true" beStrictAboutTestsThatDoNotTestAnything="true"
checkForUnintentionallyCoveredCode="true"
> >
<filter> <filter>
<whitelist> <whitelist>

View File

@ -14,7 +14,6 @@
} }
}, },
"require": { "require": {
"abeautifulsite/simpleimage": "2.5.*",
"aura/html": "2.*", "aura/html": "2.*",
"aura/router": "3.*", "aura/router": "3.*",
"aura/session": "2.*", "aura/session": "2.*",
@ -35,15 +34,16 @@
"theseer/phpdox": "0.8.1.1", "theseer/phpdox": "0.8.1.1",
"phploc/phploc": "^3.0", "phploc/phploc": "^3.0",
"phpmd/phpmd": "^2.4", "phpmd/phpmd": "^2.4",
"phpunit/phpunit": "^5.4", "phpunit/phpunit": "^5.7",
"robmorgan/phinx": "^0.6.4", "robmorgan/phinx": "^0.6.4",
"humbug/humbug": "~1.0@dev", "humbug/humbug": "~1.0@dev",
"consolidation/robo": "~1.0@RC", "consolidation/robo": "~1.0",
"henrikbjorn/lurker": "^1.1.0", "henrikbjorn/lurker": "^1.1.0",
"symfony/var-dumper": "^3.1", "symfony/var-dumper": "^3.1",
"squizlabs/php_codesniffer": "^3.0.0@beta" "squizlabs/php_codesniffer": "^3.0.0@beta"
}, },
"scripts": { "scripts": {
"build:css": "cd public && npm run build && cd .." "build:css": "cd public && npm run build && cd ..",
"watch:css": "cd public && npm run watch"
} }
} }

View File

@ -3,7 +3,6 @@
colors="true" colors="true"
stopOnFailure="false" stopOnFailure="false"
bootstrap="tests/bootstrap.php" bootstrap="tests/bootstrap.php"
beStrictAboutTestsThatDoNotTestAnything="true"
> >
<filter> <filter>
<whitelist> <whitelist>

View File

@ -794,6 +794,10 @@ a:hover, a:active {
background-color:#db7d12; background-color:#db7d12;
} }
.full_width {
width: 100%;
}
/* ----------------------------------------------------------------------------- /* -----------------------------------------------------------------------------
CSS loading icon CSS loading icon
------------------------------------------------------------------------------*/ ------------------------------------------------------------------------------*/
@ -1208,7 +1212,7 @@ a:hover, a:active {
text-align:center; text-align:center;
color: greenyellow; color: greenyellow;
position:absolute; position:absolute;
top:5px; top:147px;
left:0; left:0;
height:100%; height:100%;
width:100%; width:100%;
@ -1248,8 +1252,9 @@ a:hover, a:active {
} }
.details .cover { .details .cover {
max-width: 300px; display: block;
max-height: 435px; width: 284px;
height: 402px;
} }
.details h2 { .details h2 {
@ -1261,11 +1266,11 @@ a:hover, a:active {
margin: 1rem; margin: 1rem;
} }
.details table { .details .media_details {
max-width:300px; max-width:300px;
} }
.details td { .details .media_details td {
padding:0 15px; padding:0 15px;
padding:0 1.5rem; padding:0 1.5rem;
} }
@ -1274,13 +1279,13 @@ a:hover, a:active {
text-align:justify; text-align:justify;
} }
.details td:nth-child(odd) { .details .media_details td:nth-child(odd) {
width:1%; width:1%;
white-space:nowrap; white-space:nowrap;
text-align:right; text-align:right;
} }
.details td:nth-child(even) { .details .media_details td:nth-child(even) {
text-align:left; text-align:left;
} }

View File

@ -115,6 +115,10 @@ a:hover, a:active {
background-color: var(--edit-link-hover-color); background-color: var(--edit-link-hover-color);
} }
.full_width {
width: 100%;
}
/* ----------------------------------------------------------------------------- /* -----------------------------------------------------------------------------
CSS loading icon CSS loading icon
------------------------------------------------------------------------------*/ ------------------------------------------------------------------------------*/
@ -471,7 +475,7 @@ a:hover, a:active {
text-align:center; text-align:center;
color: greenyellow; color: greenyellow;
position:absolute; position:absolute;
top:5px; top:147px;
left:0; left:0;
height:100%; height:100%;
width:100%; width:100%;
@ -505,8 +509,9 @@ a:hover, a:active {
} }
.details .cover { .details .cover {
max-width: 300px; display: block;
max-height: 435px; width: 284px;
height: 402px;
} }
.details h2 { .details h2 {
@ -517,10 +522,10 @@ a:hover, a:active {
margin: 1rem; margin: 1rem;
} }
.details table { .details .media_details {
max-width:300px; max-width:300px;
} }
.details td { .details .media_details td {
padding:0 1.5rem; padding:0 1.5rem;
} }
@ -528,12 +533,12 @@ a:hover, a:active {
text-align:justify; text-align:justify;
} }
.details td:nth-child(odd) { .details .media_details td:nth-child(odd) {
width:1%; width:1%;
white-space:nowrap; white-space:nowrap;
text-align:right; text-align:right;
} }
.details td:nth-child(even) { .details .media_details td:nth-child(even) {
text-align:left; text-align:left;
} }

View File

@ -6,15 +6,16 @@
'use strict'; 'use strict';
// Action to increment episode count // Action to increment episode count
_.on('body.anime.list', 'click', '.plus_one', function() { _.on('body.anime.list', 'click', '.plus_one', e => {
let parent_sel = _.closestParent(this, 'article'); let parent_sel = _.closestParent(e.target, 'article');
let watched_count = parseInt(_.$('.completed_number', parent_sel)[0].textContent, 10); let watched_count = parseInt(_.$('.completed_number', parent_sel)[0].textContent, 10);
let total_count = parseInt(_.$('.total_number', parent_sel)[0].textContent, 10); let total_count = parseInt(_.$('.total_number', parent_sel)[0].textContent, 10);
let title = _.$('.name a', parent_sel)[0].textContent; let title = _.$('.name a', parent_sel)[0].textContent;
// Setup the update data // Setup the update data
let data = { let data = {
id: parent_sel.id, id: parent_sel.dataset.kitsuId,
mal_id: parent_sel.dataset.malId,
data: { data: {
progress: watched_count + 1 progress: watched_count + 1
} }
@ -41,7 +42,7 @@
_.hide(parent_sel); _.hide(parent_sel);
} }
_.showMessage('success', `Sucessfully updated ${title}`); _.showMessage('success', `Successfully updated ${title}`);
_.$('.completed_number', parent_sel)[0].textContent = ++watched_count; _.$('.completed_number', parent_sel)[0].textContent = ++watched_count;
_.scrollToTop(); _.scrollToTop();
}, },

View File

@ -5,9 +5,9 @@
'use strict'; 'use strict';
_.on('.manga.list', 'click', '.edit_buttons button', function() { _.on('.manga.list', 'click', '.edit_buttons button', e => {
let this_sel = this; let this_sel = e.target;
let parent_sel = _.closestParent(this, 'article'); let parent_sel = _.closestParent(e.target, 'article');
let manga_id = parent_sel.id.replace("manga-", ""); let manga_id = parent_sel.id.replace("manga-", "");
let type = this_sel.classList.contains("plus_one_chapter") ? 'chapter' : 'volume'; let type = this_sel.classList.contains("plus_one_chapter") ? 'chapter' : 'volume';
let completed = parseInt(_.$(`.${type}s_read`, parent_sel)[0].textContent, 10); let completed = parseInt(_.$(`.${type}s_read`, parent_sel)[0].textContent, 10);

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

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

@ -0,0 +1,73 @@
<?php declare(strict_types=1);
/**
* Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package AnimeListClient
* @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
*/
namespace Aviat\AnimeClient\API;
use Aviat\Banker\Pool;
use Aviat\Ion\Di\ContainerAware;
/**
* Helper methods for dealing with the Cache
*/
trait CacheTrait {
/**
* @var Aviat\Banker\Pool
*/
protected $cache;
/**
* Inject the cache object
*
* @param Pool $cache
* @return $this
*/
public function setCache(Pool $cache): self
{
$this->cache = $cache;
return $this;
}
/**
* Get the cache object if it exists
*
* @return Pool
*/
public function getCache()
{
return $this->cache;
}
/**
* Generate a hash as a cache key from the current method call
*
* @param object $object
* @param string $method
* @param array $args
* @return string
*/
public function getHashForMethodCall($object, string $method, array $args = []): string
{
$classname = get_class($object);
$keyObj = [
'class' => $classname,
'method' => $method,
'args' => $args,
];
$hash = sha1(json_encode($keyObj));
return $hash;
}
}

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient
@ -16,6 +16,8 @@
namespace Aviat\AnimeClient\API; namespace Aviat\AnimeClient\API;
use Aviat\Ion\Json;
/** /**
* Class encapsulating Json API data structure for a request or response * Class encapsulating Json API data structure for a request or response
*/ */
@ -40,12 +42,19 @@ class JsonAPI {
*/ */
protected $data = []; protected $data = [];
/**
* Data array parsed out from a request
*
* @var array
*/
protected $parsedData = [];
/** /**
* Related objects included with the request * Related objects included with the request
* *
* @var array * @var array
*/ */
protected $included = []; public $included = [];
/** /**
* Pagination links * Pagination links
@ -54,13 +63,142 @@ class JsonAPI {
*/ */
protected $links = []; protected $links = [];
/**
* JsonAPI constructor
*
* @param array $initital
*/
public function __construct(array $initial = [])
{
$this->data = $initial;
}
public function parseFromString(string $json)
{
$this->parse(Json::decode($json));
}
/** /**
* Parse a JsonAPI response into its components * Parse a JsonAPI response into its components
* *
* @param array $data * @param array $data
*/ */
public function parse(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;
}
/**
* Take organized includes and inline them, where applicable
*
* @param array $included
* @param string $key The key of the include to inline the other included values into
* @return array
*/
public static function inlineIncludedRelationships(array $included, string $key): array
{
$inlined = [
$key => []
];
foreach ($included[$key] as $itemId => $item)
{
// Duplicate the item for the output
$inlined[$key][$itemId] = $item;
foreach($item['relationships'] as $type => $ids)
{
$inlined[$key][$itemId]['relationships'][$type] = [];
foreach($ids as $id)
{
$inlined[$key][$itemId]['relationships'][$type][$id] = $included[$type][$id];
}
}
}
return $inlined;
}
/**
* Reorganizes 'included' data to be keyed by
* type => [
* id => data/attributes,
* ]
*
* @param array $includes
* @return array
*/
public static function organizeIncludes(array $includes): array
{
$organized = [];
foreach ($includes as $item)
{
$type = $item['type'];
$id = $item['id'];
$organized[$type] = $organized[$type] ?? [];
$organized[$type][$id] = $item['attributes'];
if (array_key_exists('relationships', $item))
{
$organized[$type][$id]['relationships'] = static::organizeRelationships($item['relationships']);
}
}
return $organized;
}
/**
* Reorganize relationship mappings to make them simpler to use
*
* Remove verbose structure, and just map:
* type => [ idArray ]
*
* @param array $relationships
* @return array
*/
public static function organizeRelationships(array $relationships): array
{
$organized = [];
foreach($relationships as $key => $data)
{
if ( ! array_key_exists('data', $data))
{
continue;
}
$organized[$key] = $organized[$key] ?? [];
foreach ($data['data'] as $item)
{
$organized[$key][] = $item['id'];
}
}
return $organized;
}
} }

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient
@ -24,7 +24,7 @@ use Aviat\AnimeClient\API\Kitsu\Enum\{
use DateTimeImmutable; use DateTimeImmutable;
/** /**
* Constants and mappings for the Kitsu API * Data massaging helpers for the Kitsu API
*/ */
class Kitsu { class Kitsu {
const AUTH_URL = 'https://kitsu.io/api/oauth/token'; const AUTH_URL = 'https://kitsu.io/api/oauth/token';
@ -45,6 +45,11 @@ class Kitsu {
]; ];
} }
/**
* Map of Kitsu Manga status to label for select menus
*
* @return array
*/
public static function getStatusToMangaSelectMap() public static function getStatusToMangaSelectMap()
{ {
return [ return [
@ -85,6 +90,78 @@ class Kitsu {
} }
} }
/**
* Get the name and logo for the streaming service of the current link
*
* @param string $hostname
* @return array
*/
protected static function getServiceMetaData(string $hostname = null): array
{
switch($hostname)
{
case 'www.crunchyroll.com':
return [
'name' => 'Crunchyroll',
'link' => true,
'logo' => '<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><g fill="#F78B24" fill-rule="evenodd"><path d="M22.549 49.145c-.815-.077-2.958-.456-3.753-.663-6.873-1.79-12.693-6.59-15.773-13.009C1.335 31.954.631 28.807.633 24.788c.003-4.025.718-7.235 2.38-10.686 1.243-2.584 2.674-4.609 4.706-6.66 3.8-3.834 8.614-6.208 14.067-6.936 1.783-.239 5.556-.161 7.221.148 3.463.642 6.571 1.904 9.357 3.797 5.788 3.934 9.542 9.951 10.52 16.861.21 1.48.332 4.559.19 4.816-.077.14-.117-.007-.167-.615-.25-3.015-1.528-6.66-3.292-9.388C40.253 7.836 30.249 4.32 20.987 7.467c-7.15 2.43-12.522 8.596-13.997 16.06-.73 3.692-.51 7.31.658 10.882a21.426 21.426 0 0 0 13.247 13.518c1.475.515 3.369.944 4.618 1.047 1.496.122 1.119.239-.727.224-1.006-.008-2.013-.032-2.237-.053z"></path><path d="M27.685 46.1c-7.731-.575-14.137-6.455-15.474-14.204-.243-1.41-.29-4.047-.095-5.345 1.16-7.706 6.97-13.552 14.552-14.639 1.537-.22 4.275-.143 5.746.162 1.28.266 2.7.737 3.814 1.266l.865.411-.814.392c-2.936 1.414-4.748 4.723-4.323 7.892.426 3.173 2.578 5.664 5.667 6.56 1.112.322 2.812.322 3.925 0 1.438-.417 2.566-1.1 3.593-2.173.346-.362.652-.621.68-.576.027.046.106.545.176 1.11.171 1.395.07 4.047-.204 5.371-.876 4.218-3.08 7.758-6.463 10.374-3.2 2.476-7.434 3.711-11.645 3.399z"></path></g></svg>'
];
case 'www.funimation.com':
return [
'name' => 'Funimation',
'link' => true,
'logo' => '<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="M24.066.017a24.922 24.922 0 0 1 13.302 3.286 25.098 25.098 0 0 1 7.833 7.058 24.862 24.862 0 0 1 4.207 9.575c.82 4.001.641 8.201-.518 12.117a24.946 24.946 0 0 1-4.868 9.009 24.98 24.98 0 0 1-7.704 6.118 24.727 24.727 0 0 1-10.552 2.718A24.82 24.82 0 0 1 13.833 47.3c-5.815-2.872-10.408-8.107-12.49-14.25-2.162-6.257-1.698-13.375 1.303-19.28C5.483 8.07 10.594 3.55 16.602 1.435A24.94 24.94 0 0 1 24.066.017zm-8.415 33.31c.464 2.284 1.939 4.358 3.99 5.48 2.174 1.217 4.765 1.444 7.202 1.181 2.002-.217 3.986-.992 5.455-2.397 1.173-1.151 2.017-2.648 2.33-4.267-1.189-.027-2.378 0-3.566-.03-.568.082-1.137-.048-1.705.014-1.232.012-2.465.003-3.697-.01-.655.066-1.309-.035-1.963.013-1.166-.053-2.334.043-3.5-.025-1.515.08-3.03-.035-4.546.042z" fill="#411299" fill-rule="evenodd"></path></svg>'
];
case 'www.hulu.com':
return [
'name' => 'Hulu',
'link' => true,
'logo' => '<svg width="50" height="50" viewBox="0 0 34 50" xmlns="http://www.w3.org/2000/svg"><path d="M22.222 13.889h-11.11V0H0v50h11.111V27.778c0-1.39 1.111-2.778 2.778-2.778h5.555c1.39 0 2.778 1.111 2.778 2.778V50h11.111V25c0-6.111-5-11.111-11.11-11.111z" fill="#8BC34A" fill-rule="evenodd"></path></svg>'
];
// Default to Netflix, because the API links are broken,
// and there's no other real identifier for Netflix
default:
return [
'name' => 'Netflix',
'link' => false,
'logo' => '<svg width="50" height="50" viewBox="0 0 26 50" xmlns="http://www.w3.org/2000/svg"><path d="M.057.258C2.518.253 4.982.263 7.446.253c2.858 7.76 5.621 15.556 8.456 23.324.523 1.441 1.003 2.897 1.59 4.312.078-9.209.01-18.42.034-27.631h7.763v46.36c-2.812.372-5.637.627-8.457.957-1.203-3.451-2.396-6.902-3.613-10.348-1.796-5.145-3.557-10.302-5.402-15.428.129 8.954.015 17.912.057 26.871-2.603.39-5.227.637-7.815 1.119C.052 33.279.06 16.768.057.258z" fill="#E21221" fill-rule="evenodd"></path></svg>'
];
}
}
/**
* Reorganize streaming links
*
* @param array $included
* @return array
*/
public static function parseStreamingLinks(array $included): array
{
if ( ! array_key_exists('streamingLinks', $included))
{
return [];
}
$links = [];
foreach ($included['streamingLinks'] as $streamingLink)
{
$host = parse_url($streamingLink['url'], \PHP_URL_HOST);
$links[] = [
'meta' => static::getServiceMetaData($host),
'link' => $streamingLink['url'],
'subs' => $streamingLink['subs'],
'dubs' => $streamingLink['dubs']
];
}
return $links;
}
/** /**
* Filter out duplicate and very similar names from * Filter out duplicate and very similar names from
* *
@ -110,66 +187,6 @@ class Kitsu {
return $valid; return $valid;
} }
/**
* Reorganizes 'included' data to be keyed by
* type => [
* id => data/attributes,
* ]
*
* @param array $includes
* @return array
*/
public static function organizeIncludes(array $includes): array
{
$organized = [];
foreach ($includes as $item)
{
$type = $item['type'];
$id = $item['id'];
$organized[$type] = $organized[$type] ?? [];
$organized[$type][$id] = $item['attributes'];
if (array_key_exists('relationships', $item))
{
$organized[$type][$id]['relationships'] = self::organizeRelationships($item['relationships']);
}
}
return $organized;
}
/**
* Reorganize relationship mappings to make them simpler to use
*
* Remove verbose structure, and just map:
* type => [ idArray ]
*
* @param array $relationships
* @return array
*/
public static function organizeRelationships(array $relationships): array
{
$organized = [];
foreach($relationships as $key => $data)
{
if ( ! array_key_exists('data', $data))
{
continue;
}
$organized[$key] = $organized[$key] ?? [];
foreach ($data['data'] as $item)
{
$organized[$key][] = $item['id'];
}
}
return $organized;
}
/** /**
* Determine if an alternate title is unique enough to list * Determine if an alternate title is unique enough to list
* *

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient
@ -18,6 +18,7 @@ namespace Aviat\AnimeClient\API\Kitsu;
use Aviat\AnimeClient\AnimeClient; use Aviat\AnimeClient\AnimeClient;
use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
use Exception;
/** /**
* Kitsu API Authentication * Kitsu API Authentication
@ -64,7 +65,16 @@ class Auth {
{ {
$config = $this->container->get('config'); $config = $this->container->get('config');
$username = $config->get(['kitsu_username']); $username = $config->get(['kitsu_username']);
try
{
$auth_token = $this->model->authenticate($username, $password); $auth_token = $this->model->authenticate($username, $password);
}
catch (Exception $e)
{
return FALSE;
}
if (FALSE !== $auth_token) if (FALSE !== $auth_token)
{ {

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient
@ -16,6 +16,8 @@
namespace Aviat\AnimeClient\API\Kitsu; namespace Aviat\AnimeClient\API\Kitsu;
use Aviat\AnimeClient\API\CacheTrait;
use Aviat\AnimeClient\API\JsonAPI;
use Aviat\AnimeClient\API\Kitsu as K; use Aviat\AnimeClient\API\Kitsu as K;
use Aviat\AnimeClient\API\Kitsu\Transformer\{ use Aviat\AnimeClient\API\Kitsu\Transformer\{
AnimeTransformer, AnimeListTransformer, MangaTransformer, MangaListTransformer AnimeTransformer, AnimeListTransformer, MangaTransformer, MangaListTransformer
@ -28,6 +30,7 @@ use GuzzleHttp\Exception\ClientException;
* Kitsu API Model * Kitsu API Model
*/ */
class KitsuModel { class KitsuModel {
use CacheTrait;
use ContainerAware; use ContainerAware;
use KitsuTrait; use KitsuTrait;
@ -60,6 +63,7 @@ class KitsuModel {
*/ */
protected $mangaListTransformer; protected $mangaListTransformer;
/** /**
* KitsuModel constructor. * KitsuModel constructor.
*/ */
@ -130,6 +134,7 @@ class KitsuModel {
*/ */
public function getAnime(string $animeId): array public function getAnime(string $animeId): array
{ {
// @TODO catch non-existent anime
$baseData = $this->getRawMediaData('anime', $animeId); $baseData = $this->getRawMediaData('anime', $animeId);
return $this->animeTransformer->transform($baseData); return $this->animeTransformer->transform($baseData);
} }
@ -146,7 +151,13 @@ class KitsuModel {
return $this->mangaTransformer->transform($baseData); return $this->mangaTransformer->transform($baseData);
} }
public function getAnimeList($status): array /**
* Get the anime list for the configured user
*
* @param string $status - The watching status to filter the list with
* @return array
*/
public function getAnimeList(string $status): array
{ {
$options = [ $options = [
'query' => [ 'query' => [
@ -155,33 +166,33 @@ class KitsuModel {
'media_type' => 'Anime', 'media_type' => 'Anime',
'status' => $status, 'status' => $status,
], ],
'include' => 'media,media.genres', 'include' => 'media,media.genres,media.mappings,anime.streamingLinks',
'page' => [ 'page' => [
'offset' => 0, 'offset' => 0,
'limit' => 1000 'limit' => 500
], ]
'sort' => '-updated_at'
] ]
]; ];
$cacheItem = $this->cache->getItem($this->getHashForMethodCall($this, __METHOD__, $options));
if ( ! $cacheItem->isHit())
{
$data = $this->getRequest('library-entries', $options); $data = $this->getRequest('library-entries', $options);
$included = K::organizeIncludes($data['included']); $included = JsonAPI::organizeIncludes($data['included']);
$included = JsonAPI::inlineIncludedRelationships($included, 'anime');
foreach($data['data'] as $i => &$item) foreach($data['data'] as $i => &$item)
{ {
$item['anime'] = $included['anime'][$item['relationships']['media']['data']['id']]; $item['included'] = $included;
$animeGenres = $item['anime']['relationships']['genres'];
foreach($animeGenres as $id)
{
$item['genres'][] = $included['genres'][$id]['name'];
} }
}
$transformed = $this->animeListTransformer->transformCollection($data['data']); $transformed = $this->animeListTransformer->transformCollection($data['data']);
return $transformed; $cacheItem->set($transformed);
$cacheItem->save();
}
return $cacheItem->get();
} }
public function getMangaList($status): array public function getMangaList($status): array
@ -248,19 +259,24 @@ class KitsuModel {
public function getListItem(string $listId): array public function getListItem(string $listId): array
{ {
$baseData = $this->listItem->get($listId); $baseData = $this->listItem->get($listId);
$included = JsonAPI::organizeIncludes($baseData['included']);
switch ($baseData['included'][0]['type'])
switch (TRUE)
{ {
case 'anime': case in_array('anime', array_keys($included)):
$baseData['data']['anime'] = $baseData['included'][0]; $included = JsonAPI::inlineIncludedRelationships($included, 'anime');
$baseData['data']['included'] = $included;
return $this->animeListTransformer->transform($baseData['data']); return $this->animeListTransformer->transform($baseData['data']);
case 'manga': case in_array('manga', array_keys($included)):
$included = JsonAPI::inlineIncludedRelationships($included, 'manga');
$baseData['data']['included'] = $included;
$baseData['data']['manga'] = $baseData['included'][0]; $baseData['data']['manga'] = $baseData['included'][0];
return $this->mangaListTransformer->transform($baseData['data']); return $this->mangaListTransformer->transform($baseData['data']);
default: default:
return $baseData['data']['attributes']; return $baseData['data'];
} }
} }
@ -309,11 +325,7 @@ class KitsuModel {
]; ];
$data = $this->getRequest($type, $options); $data = $this->getRequest($type, $options);
$baseData = $data['data'][0]['attributes']; $baseData = $data['data'][0]['attributes'];
$rawGenres = array_pluck($data['included'], 'attributes');
$genres = array_pluck($rawGenres, 'name');
$baseData['genres'] = $genres;
$baseData['included'] = $data['included']; $baseData['included'] = $data['included'];
return $baseData; return $baseData;
} }

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient
@ -37,7 +37,6 @@ class ListItem extends AbstractListItem {
public function create(array $data): bool public function create(array $data): bool
{ {
/*?><pre><?= print_r($data, TRUE) ?></pre><?php */
$response = $this->getResponse('POST', 'library-entries', [ $response = $this->getResponse('POST', 'library-entries', [
'body' => Json::encode([ 'body' => Json::encode([
'data' => [ 'data' => [
@ -77,7 +76,7 @@ class ListItem extends AbstractListItem {
{ {
return $this->getRequest("library-entries/{$id}", [ return $this->getRequest("library-entries/{$id}", [
'query' => [ 'query' => [
'include' => 'media' 'include' => 'media,media.genres,media.mappings'
] ]
]); ]);
} }

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient
@ -33,10 +33,13 @@ class AnimeListTransformer extends AbstractTransformer {
*/ */
public function transform($item) public function transform($item)
{ {
/* ?><pre><?= print_r($item, TRUE) ?></pre><?php /* ?><pre><?= json_encode($item, \JSON_PRETTY_PRINT) ?></pre><?php */
// die(); */ $included = $item['included'];
$anime = $item['anime']['attributes'] ?? $item['anime']; $animeId = $item['relationships']['media']['data']['id'];
$genres = $item['genres'] ?? []; $anime = $included['anime'][$animeId];
$genres = array_column($anime['relationships']['genres'], 'name') ?? [];
sort($genres);
$rating = (int) 2 * $item['attributes']['rating']; $rating = (int) 2 * $item['attributes']['rating'];
@ -44,10 +47,27 @@ class AnimeListTransformer extends AbstractTransformer {
? (int) $anime['episodeCount'] ? (int) $anime['episodeCount']
: '-'; : '-';
$MALid = NULL;
if (array_key_exists('mappings', $anime['relationships']))
{
foreach ($anime['relationships']['mappings'] as $mapping)
{
if ($mapping['externalSite'] === 'myanimelist/anime')
{
$MALid = $mapping['externalId'];
break;
}
}
}
return [ return [
'id' => $item['id'], 'id' => $item['id'],
'mal_id' => $MALid,
'episodes' => [ 'episodes' => [
'watched' => $item['attributes']['progress'], 'watched' => (int) $item['attributes']['progress'] !== '0'
? (int) $item['attributes']['progress']
: '-',
'total' => $total_episodes, 'total' => $total_episodes,
'length' => $anime['episodeLength'], 'length' => $anime['episodeLength'],
], ],
@ -60,16 +80,16 @@ class AnimeListTransformer extends AbstractTransformer {
'age_rating' => $anime['ageRating'], 'age_rating' => $anime['ageRating'],
'titles' => Kitsu::filterTitles($anime), 'titles' => Kitsu::filterTitles($anime),
'slug' => $anime['slug'], 'slug' => $anime['slug'],
'url' => $anime['url'] ?? '',
'type' => $this->string($anime['showType'])->upperCaseFirst()->__toString(), 'type' => $this->string($anime['showType'])->upperCaseFirst()->__toString(),
'image' => $anime['posterImage']['small'], 'image' => $anime['posterImage']['small'],
'genres' => $genres, 'genres' => $genres,
'streaming_links' => Kitsu::parseStreamingLinks($included),
], ],
'watching_status' => $item['attributes']['status'], 'watching_status' => $item['attributes']['status'],
'notes' => $item['attributes']['notes'], 'notes' => $item['attributes']['notes'],
'rewatching' => (bool) $item['attributes']['reconsuming'], 'rewatching' => (bool) $item['attributes']['reconsuming'],
'rewatched' => (int) $item['attributes']['reconsumeCount'], 'rewatched' => (int) $item['attributes']['reconsumeCount'],
'user_rating' => ($rating === 0) ? '-' : $rating, 'user_rating' => ($rating === 0) ? '-' : (int) $rating,
'private' => (bool) $item['attributes']['private'] ?? false, 'private' => (bool) $item['attributes']['private'] ?? false,
]; ];
} }
@ -83,18 +103,8 @@ class AnimeListTransformer extends AbstractTransformer {
*/ */
public function untransform($item) public function untransform($item)
{ {
// Messy mapping of boolean values to their API string equivalents $privacy = (array_key_exists('private', $item) && $item['private']);
$privacy = 'false'; $rewatching = (array_key_exists('rewatching', $item) && $item['rewatching']);
if (array_key_exists('private', $item) && $item['private'])
{
$privacy = 'true';
}
$rewatching = 'false';
if (array_key_exists('rewatching', $item) && $item['rewatching'])
{
$rewatching = 'true';
}
$untransformed = [ $untransformed = [
'id' => $item['id'], 'id' => $item['id'],

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient
@ -16,7 +16,7 @@
namespace Aviat\AnimeClient\API\Kitsu\Transformer; namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\Kitsu; use Aviat\AnimeClient\API\{JsonAPI, Kitsu};
use Aviat\Ion\Transformer\AbstractTransformer; use Aviat\Ion\Transformer\AbstractTransformer;
/** /**
@ -33,14 +33,18 @@ class AnimeTransformer extends AbstractTransformer {
*/ */
public function transform($item) public function transform($item)
{ {
$item['genres'] = $item['genres'] ?? []; $item['included'] = JsonAPI::organizeIncludes($item['included']);
$item['genres'] = array_column($item['included']['genres'], 'name') ?? [];
sort($item['genres']); sort($item['genres']);
$titles = Kitsu::filterTitles($item);
return [ return [
'titles' => Kitsu::filterTitles($item), 'title' => $titles[0],
'titles' => $titles,
'status' => Kitsu::getAiringStatus($item['startDate'], $item['endDate']), 'status' => Kitsu::getAiringStatus($item['startDate'], $item['endDate']),
'cover_image' => $item['posterImage']['small'], 'cover_image' => $item['posterImage']['small'],
'show_type' => $item['showType'], 'show_type' => $this->string($item['showType'])->upperCaseFirst()->__toString(),
'episode_count' => $item['episodeCount'], 'episode_count' => $item['episodeCount'],
'episode_length' => $item['episodeLength'], 'episode_length' => $item['episodeLength'],
'synopsis' => $item['synopsis'], 'synopsis' => $item['synopsis'],
@ -48,6 +52,7 @@ class AnimeTransformer extends AbstractTransformer {
'age_rating_guide' => $item['ageRatingGuide'], 'age_rating_guide' => $item['ageRatingGuide'],
'url' => "https://kitsu.io/anime/{$item['slug']}", 'url' => "https://kitsu.io/anime/{$item['slug']}",
'genres' => $item['genres'], 'genres' => $item['genres'],
'streaming_links' => Kitsu::parseStreamingLinks($item['included'])
]; ];
} }
} }

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

49
src/API/MAL.php Normal file
View File

@ -0,0 +1,49 @@
<?php declare(strict_types=1);
/**
* Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package AnimeListClient
* @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
*/
namespace Aviat\AnimeClient\API;
use Aviat\AnimeClient\API\MAL\Enum\{AnimeWatchingStatus, MangaReadingStatus};
/**
* Constants and mappings for the My Anime List API
*/
class MAL {
const AUTH_URL = 'https://myanimelist.net/api/account/verify_credentials.xml';
const BASE_URL = 'https://myanimelist.net/api/';
public static function getIdToWatchingStatusMap()
{
return [
1 => AnimeWatchingStatus::WATCHING,
2 => AnimeWatchingStatus::COMPLETED,
3 => AnimeWatchingStatus::ON_HOLD,
4 => AnimeWatchingStatus::DROPPED,
5 => AnimeWatchingStatus::PLAN_TO_WATCH
];
}
public static function getIdToReadingStatusMap()
{
return [
1 => MangaReadingStatus::READING,
2 => MangaReadingStatus::COMPLETED,
3 => MangaReadingStatus::ON_HOLD,
4 => MangaReadingStatus::DROPPED,
5 => MangaReadingStatus::PLAN_TO_READ
];
}
}

View File

@ -1,108 +0,0 @@
<?php declare(strict_types=1);
/**
* Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2016 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\MAL;
use Aviat\AnimeClient\AnimeClient;
use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
/**
* MAL API Authentication
*/
class Auth {
use \Aviat\Ion\Di\ContainerAware;
/**
* Anime API Model
*
* @var \Aviat\AnimeClient\Model\API
*/
protected $model;
/**
* Session object
*
* @var Aura\Session\Segment
*/
protected $segment;
/**
* Constructor
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->setContainer($container);
$this->segment = $container->get('session')
->getSegment(AnimeClient::SESSION_SEGMENT);
$this->model = $container->get('api-model');
}
/**
* Make the appropriate authentication call,
* and save the resulting auth token if successful
*
* @param string $password
* @return boolean
*/
public function authenticate($password)
{
$username = $this->container->get('config')
->get('hummingbird_username');
$auth_token = $this->model->authenticate($username, $password);
if (FALSE !== $auth_token)
{
$this->segment->set('auth_token', $auth_token);
return TRUE;
}
return FALSE;
}
/**
* Check whether the current user is authenticated
*
* @return boolean
*/
public function is_authenticated()
{
return ($this->get_auth_token() !== FALSE);
}
/**
* Clear authentication values
*
* @return void
*/
public function logout()
{
$this->segment->clear();
}
/**
* Retrieve the authentication token from the session
*
* @return string|false
*/
public function get_auth_token()
{
return $this->segment->get('auth_token', FALSE);
}
}
// End of KitsuAuth.php

View File

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
/**
* Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package AnimeListClient
* @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
*/
namespace Aviat\AnimeClient\API\MAL\Enum;
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';
}

View File

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
/**
* Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package AnimeListClient
* @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
*/
namespace Aviat\AnimeClient\API\MAL\Enum;
use Aviat\Ion\Enum as BaseEnum;
/**
* Possible values for watching status for the current anime
*/
class MangaReadingStatus extends BaseEnum {
const READING = 'reading';
const COMPLETED = 'completed';
const ON_HOLD = 'onhold';
const DROPPED = 'dropped';
const PLAN_TO_READ = 'plantoread';
}

53
src/API/MAL/ListItem.php Normal file
View File

@ -0,0 +1,53 @@
<?php declare(strict_types=1);
/**
* Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package AnimeListClient
* @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
*/
namespace Aviat\AnimeClient\API\MAL;
use Aviat\AnimeClient\API\AbstractListItem;
use Aviat\Ion\Di\ContainerAware;
/**
* CRUD operations for MAL list items
*/
class ListItem extends AbstractListItem {
use ContainerAware;
use MALTrait;
public function __construct()
{
$this->init();
}
public function create(array $data): bool
{
return FALSE;
}
public function delete(string $id): bool
{
return FALSE;
}
public function get(string $id): array
{
return [];
}
public function update(string $id, array $data): Response
{
}
}

190
src/API/MAL/MALTrait.php Normal file
View File

@ -0,0 +1,190 @@
<?php declare(strict_types=1);
/**
* Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package AnimeListClient
* @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
*/
namespace Aviat\AnimeClient\API\MAL;
use Aviat\AnimeClient\API\{
GuzzleTrait,
MAL as M,
XML
};
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Psr7\Response;
use InvalidArgumentException;
trait MALTrait {
use GuzzleTrait;
/**
* The base url for api requests
* @var string $base_url
*/
protected $baseUrl = M::BASE_URL;
/**
* HTTP headers to send with every request
*
* @var array
*/
protected $defaultHeaders = [
'User-Agent' => "Tim's Anime Client/4.0"
];
/**
* Set up the class properties
*
* @return void
*/
protected function init()
{
$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
]);
}
/**
* Make a request via Guzzle
*
* @param string $type
* @param string $url
* @param array $options
* @return Response
*/
private function getResponse(string $type, string $url, array $options = [])
{
$type = strtoupper($type);
$validTypes = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
if ( ! in_array($type, $validTypes))
{
throw new InvalidArgumentException('Invalid http request type');
}
$config = $this->container->get('config');
$logger = $this->container->getLogger('request');
$defaultOptions = [
'auth' => [
$config->get(['mal','username']),
$config->get(['mal','password'])
],
'headers' => $this->defaultHeaders
];
$options = array_merge($defaultOptions, $options);
$logger->debug(Json::encode([$type, $url]));
$logger->debug(Json::encode($options));
return $this->client->request($type, $url, $options);
}
/**
* Make a request via Guzzle
*
* @param string $type
* @param string $url
* @param array $options
* @return array
*/
private function request(string $type, string $url, array $options = []): array
{
$logger = null;
if ($this->getContainer())
{
$logger = $this->container->getLogger('request');
}
$response = $this->getResponse($type, $url, $options);
if ((int) $response->getStatusCode() > 299 || (int) $response->getStatusCode() < 200)
{
if ($logger)
{
$logger->warning('Non 200 response for api call');
$logger->warning($response->getBody());
}
// throw new RuntimeException($response->getBody());
}
return XML::toArray((string) $response->getBody());
}
/**
* Remove some boilerplate for get requests
*
* @param array $args
* @return array
*/
protected function getRequest(...$args): array
{
return $this->request('GET', ...$args);
}
/**
* Remove some boilerplate for post requests
*
* @param array $args
* @return array
*/
protected function postRequest(...$args): array
{
$logger = null;
if ($this->getContainer())
{
$logger = $this->container->getLogger('request');
}
$response = $this->getResponse('POST', ...$args);
$validResponseCodes = [200, 201];
if ( ! in_array((int) $response->getStatusCode(), $validResponseCodes))
{
if ($logger)
{
$logger->warning('Non 201 response for POST api call');
$logger->warning($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);
}
}

View File

@ -8,56 +8,63 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient
*/ */
namespace Aviat\AnimeClient\API\Kitsu; namespace Aviat\AnimeClient\API\MAL;
use Aviat\AnimeClient\Model\API; use Aviat\AnimeClient\API\MAL as M;
use Aviat\AnimeClient\API\MAL\{
AnimeListTransformer,
ListItem
};
use Aviat\AnimeClient\API\XML;
use Aviat\Ion\Di\ContainerAware;
/** /**
* MyAnimeList API Model * MyAnimeList API Model
*/ */
class Model extends API { class Model {
use ContainerAware;
use MALTrait;
/** /**
* Base url for Kitsu API * @var AnimeListTransformer
*/ */
protected $baseUrl = 'https://myanimelist.net/api/'; protected $animeListTransformer;
/** /**
* Default settings for Guzzle * KitsuModel constructor.
* @var array
*/ */
protected $connectionDefaults = []; public function __construct(ListItem $listItem)
/**
* 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->post('account/', [ // Set up Guzzle trait
'body' => http_build_query([ $this->init();
'grant_type' => 'password', $this->animeListTransformer = new AnimeListTransformer();
'username' => $username, $this->listItem = $listItem;
'password' => $password
])
]);
$info = $response->getBody();
if (array_key_exists('access_token', $info)) {
// @TODO save token
return true;
} }
return false; public function createListItem(array $data): bool
{
return FALSE;
}
public function getListItem(string $listId): array
{
return [];
}
public function updateListItem(array $data)
{
$updateData = $this->animeListTransformer->transform($data['data']);
return $this->listItem->update($data['mal_id'], $updateData);
}
public function deleteListItem(string $id): bool
{
} }
} }

View File

@ -0,0 +1,46 @@
<?php declare(strict_types=1);
/**
* Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package AnimeListClient
* @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
*/
namespace Aviat\AnimeClient\API\MAL;
use Aviat\Ion\Transformer\AbstractTransformer;
/**
* Transformer for updating MAL List
*/
class AnimeListTransformer extends AbstractTransformer {
public function transform($item)
{
$rewatching = 'false';
if (array_key_exists('rewatching', $item) && $item['rewatching'])
{
$rewatching = 'true';
}
return [
'id' => $item['id'],
'data' => [
'status' => $item['watching_status'],
'rating' => $item['user_rating'],
'rewatch_value' => (int) $rewatching,
'times_rewatched' => $item['rewatched'],
'comments' => $item['notes'],
'episode' => $item['episodes_watched']
]
];
}
}

View File

@ -0,0 +1,33 @@
<?php declare(strict_types=1);
/**
* Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package AnimeListClient
* @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
*/
namespace Aviat\AnimeClient\API\MAL;
use Aviat\Ion\Transformer\AbstractTransformer;
class MALToKitsuTransformer extends AbstractTransformer {
public function transform($item)
{
}
public function untransform($item)
{
}
}

220
src/API/XML.php Normal file
View File

@ -0,0 +1,220 @@
<?php declare(strict_types=1);
/**
* Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package AnimeListClient
* @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
*/
namespace Aviat\AnimeClient\API;
use DOMDocument, DOMNode, DOMNodelist;
/**
* XML <=> PHP Array codec
*/
class XML {
/**
* XML representation of the data
*
* @var string
*/
private $xml;
/**
* PHP array version of the data
*
* @var array
*/
private $data;
/**
* XML constructor
*/
public function __construct(string $xml = '', array $data = [])
{
$this->setXML($xml)->setData($data);
}
/**
* Serialize the data to an xml string
*/
public function __toString(): string
{
return static::toXML($this->getData());
}
/**
* Get the data parsed from the XML
*
* @return array
*/
public function getData(): array
{
return $this->data;
}
/**
* Set the data to create xml from
*
* @param array $data
* @return $this
*/
public function setData(array $data): self
{
$this->data = $data;
return $this;
}
/**
* Get the xml created from the data
*
* @return string
*/
public function getXML(): string
{
return $this->xml;
}
/**
* Set the xml to parse the data from
*
* @param string $xml
* @return $this
*/
public function setXML(string $xml): self
{
$this->xml = $xml;
return $this;
}
/**
* Parse an xml document string to a php array
*
* @param string $xml
* @return array
*/
public static function toArray(string $xml): array
{
$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);
$dom = new DOMDocument();
$dom->loadXML($xml);
$root = $dom->documentElement;
$data[$root->tagName] = [];
if ($root->hasChildNodes())
{
static::childNodesToArray($data[$root->tagName], $root->childNodes);
}
return $data;
}
/**
* Transform the array into XML
*
* @param array $data
* @return string
*/
public static function toXML(array $data): string
{
$dom = new DOMDocument();
$dom->encoding = 'UTF-8';
static::arrayPropertiesToXmlNodes($dom, $dom, $data);
return $dom->saveXML();
}
/**
* Parse the xml document string to a php array
*
* @return array
*/
public function parse(): array
{
$xml = $this->getXML();
$data = static::toArray($xml);
return $this->setData($data)->getData();
}
/**
* Transform the array into XML
*
* @return string
*/
public function createXML(): string
{
return static::toXML($this->getData());
}
/**
* Recursively create array structure based on xml structure
*
* @param array &$root A reference to the current array location
* @param DOMNodeList $nodeList The current NodeList object
* @return void
*/
private static function childNodesToArray(array &$root, DOMNodelist $nodeList)
{
$length = $nodeList->length;
for ($i = 0; $i < $length; $i++)
{
$el = $nodeList->item($i);
if (is_a($el->childNodes->item(0), 'DomText') || ( ! $el->hasChildNodes()))
{
$root[$el->nodeName] = $el->textContent;
}
else
{
$root[$el->nodeName] = [];
static::childNodesToArray($root[$el->nodeName], $el->childNodes);
}
}
}
/**
* Recursively create xml nodes from array properties
*
* @param DOMDocument $dom The current DOM object
* @param DOMNode $parent The parent element to append children to
* @param array $data The data for the current node
* @return void
*/
private static function arrayPropertiesToXmlNodes(DOMDocument &$dom, DOMNode &$parent, array $data)
{
foreach($data as $key => $props)
{
$node = $dom->createElement($key);
if (is_array($props))
{
static::arrayPropertiesToXmlNodes($dom, $node, $props);
}
else
{
$tNode = $dom->createTextNode((string)$props);
$node->appendChild($tNode);
}
$parent->appendChild($node);
}
}
}

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -304,7 +304,8 @@ class Controller {
return $this->session_redirect(); return $this->session_redirect();
} }
$this->login("Invalid username or password."); $this->set_flash_message('Invalid username or password.');
$this->redirect($this->urlGenerator->url('login'), 303);
} }
/** /**
@ -373,7 +374,7 @@ class Controller {
*/ */
public function clearCache() public function clearCache()
{ {
$this->cache->purge(); $this->cache->clear();
$this->outputHTML('blank', [ $this->outputHTML('blank', [
'title' => 'Cache cleared' 'title' => 'Cache cleared'
], NULL, 200); ], NULL, 200);

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient
@ -159,7 +159,7 @@ class Anime extends BaseController {
if ($result) if ($result)
{ {
$this->set_flash_message('Added new anime to list', 'success'); $this->set_flash_message('Added new anime to list', 'success');
// $this->cache->purge(); $this->cache->clear();
} }
else else
{ {
@ -233,7 +233,7 @@ class Anime extends BaseController {
if ($full_result['statusCode'] === 200) if ($full_result['statusCode'] === 200)
{ {
$this->set_flash_message("Successfully updated.", 'success'); $this->set_flash_message("Successfully updated.", 'success');
// $this->cache->purge(); $this->cache->clear();
} }
else else
{ {
@ -261,7 +261,7 @@ class Anime extends BaseController {
$response = $this->model->updateLibraryItem($data); $response = $this->model->updateLibraryItem($data);
// $this->cache->purge(); $this->cache->clear();
$this->outputJSON($response['body'], $response['statusCode']); $this->outputJSON($response['body'], $response['statusCode']);
} }
@ -278,7 +278,7 @@ class Anime extends BaseController {
if ((bool)$response === TRUE) if ((bool)$response === TRUE)
{ {
$this->set_flash_message("Successfully deleted anime.", 'success'); $this->set_flash_message("Successfully deleted anime.", 'success');
// $this->cache->purge(); $this->cache->clear();
} }
else else
{ {

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient
@ -142,7 +142,7 @@ class Manga extends Controller {
if ($result) if ($result)
{ {
$this->set_flash_message('Added new manga to list', 'success'); $this->set_flash_message('Added new manga to list', 'success');
// $this->cache->purge(); $this->cache->clear();
} }
else else
{ {
@ -203,7 +203,7 @@ class Manga extends Controller {
if ($full_result['statusCode'] === 200) if ($full_result['statusCode'] === 200)
{ {
$this->set_flash_message("Successfully updated manga.", 'success'); $this->set_flash_message("Successfully updated manga.", 'success');
// $this->cache->purge(); $this->cache->clear();
} }
else else
{ {
@ -232,7 +232,7 @@ class Manga extends Controller {
$response = $this->model->updateLibraryItem($data); $response = $this->model->updateLibraryItem($data);
// $this->cache->purge(); $this->cache->clear();
$this->outputJSON($response['body'], $response['statusCode']); $this->outputJSON($response['body'], $response['statusCode']);
} }
@ -250,7 +250,7 @@ class Manga extends Controller {
if ($response) if ($response)
{ {
$this->set_flash_message("Successfully deleted manga.", 'success'); $this->set_flash_message("Successfully deleted manga.", 'success');
//$this->cache->purge(); $this->cache->clear();
} }
else else
{ {

View File

@ -18,6 +18,7 @@ namespace Aviat\AnimeClient;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Friend; use Aviat\Ion\Friend;
use GuzzleHttp\Exception\ServerException;
/** /**
* Basic routing/ dispatch * Basic routing/ dispatch
@ -129,9 +130,23 @@ class Dispatcher extends RoutingBase {
$params = $error_route['params']; $params = $error_route['params'];
} }
// Try to catch API errors in a presentable fashion
try
{
// Actually instantiate the controller // Actually instantiate the controller
$this->call($controllerName, $actionMethod, $params); $this->call($controllerName, $actionMethod, $params);
} }
catch (ServerException $e)
{
$response = $e->getResponse();
$this->call(AnimeClient::DEFAULT_CONTROLLER, AnimeClient::ERROR_MESSAGE_METHOD, [
$response->getStatusCode(),
'API Error',
'There was a problem getting data from an external source.',
(string) $response->getBody()
]);
}
}
/** /**
* Parse out the arguments for the appropriate controller for * Parse out the arguments for the appropriate controller for

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -8,7 +8,7 @@
* *
* @package AnimeListClient * @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net> * @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 * @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0 * @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient * @link https://github.com/timw4mail/HummingBirdAnimeClient

View File

@ -16,7 +16,6 @@
namespace Aviat\AnimeClient; namespace Aviat\AnimeClient;
use abeautifulsite\SimpleImage;
use Aviat\Ion\ConfigInterface; use Aviat\Ion\ConfigInterface;
use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
use DomainException; use DomainException;

48
tests/API/JsonAPITest.php Normal file
View File

@ -0,0 +1,48 @@
<?php declare(strict_types=1);
/**
* Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package AnimeListClient
* @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
*/
namespace Aviat\AnimeClient\Tests\API;
use Aviat\AnimeClient\API\JsonAPI;
use Aviat\Ion\Json;
use PHPUnit\Framework\TestCase;
class JsonAPITest extends TestCase {
public function setUp()
{
$dir = __DIR__ . '/../test_data/JsonAPI';
$this->startData = Json::decodeFile("{$dir}/jsonApiExample.json");
$this->organizedIncludes = Json::decodeFile("{$dir}/organizedIncludes.json");
$this->inlineIncluded = Json::decodeFile("{$dir}/inlineIncluded.json");
}
public function testOrganizeIncludes()
{
$expected = $this->organizedIncludes;
$actual = JsonAPI::organizeIncludes($this->startData['included']);
$this->assertEquals($expected, $actual);
}
public function testInlineIncludedRelationships()
{
$expected = $this->inlineIncluded;
$actual = JsonAPI::inlineIncludedRelationships($this->organizedIncludes, 'anime');
$this->assertEquals($expected, $actual);
}
}

View File

@ -0,0 +1,92 @@
<?php declare(strict_types=1);
namespace Aviat\AnimeClient\Tests\API\Kitsu\Transformer;
use AnimeClient_TestCase;
use Aviat\AnimeClient\API\Kitsu\Transformer\AnimeListTransformer;
use Aviat\Ion\Friend;
use Aviat\Ion\Json;
class AnimeListTransformerTest extends AnimeClient_TestCase {
public function setUp()
{
parent::setUp();
$this->dir = AnimeClient_TestCase::TEST_DATA_DIR . '/Kitsu';
$this->beforeTransform = Json::decodeFile("{$this->dir}/animeListItemBeforeTransform.json");
$this->afterTransform = Json::decodeFile("{$this->dir}/animeListItemAfterTransform.json");
$this->transformer = new AnimeListTransformer();
}
public function testTransform()
{
$expected = $this->afterTransform;
$actual = $this->transformer->transform($this->beforeTransform);
// Json::encodeFile("{$this->dir}/animeListItemAfterTransform.json", $actual);
$this->assertEquals($expected, $actual);
}
public function dataUntransform()
{
return [[
'input' => [
'id' => 14047981,
'watching_status' => 'current',
'user_rating' => 8,
'episodes_watched' => 38,
'rewatched' => 0,
'notes' => 'Very formulaic.',
'edit' => true
],
'expected' => [
'id' => 14047981,
'data' => [
'status' => 'current',
'rating' => 4,
'reconsuming' => false,
'reconsumeCount' => 0,
'notes' => 'Very formulaic.',
'progress' => 38,
'private' => false
]
]
], [
'input' => [
'id' => 14047981,
'watching_status' => 'current',
'user_rating' => 8,
'episodes_watched' => 38,
'rewatched' => 0,
'notes' => 'Very formulaic.',
'edit' => 'true',
'private' => 'On',
'rewatching' => 'On'
],
'expected' => [
'id' => 14047981,
'data' => [
'status' => 'current',
'rating' => 4,
'reconsuming' => true,
'reconsumeCount' => 0,
'notes' => 'Very formulaic.',
'progress' => 38,
'private' => true,
]
]
]];
}
/**
* @dataProvider dataUntransform
*/
public function testUntransform($input, $expected)
{
$actual = $this->transformer->untransform($input);
$this->assertEquals($expected, $actual);
}
}

View File

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
namespace Aviat\AnimeClient\Tests\API\Kitsu\Transformer;
use AnimeClient_TestCase;
use Aviat\AnimeClient\API\Kitsu\Transformer\AnimeTransformer;
use Aviat\Ion\Friend;
use Aviat\Ion\Json;
class AnimeTransformerTest extends AnimeClient_TestCase {
public function setUp()
{
parent::setUp();
$this->dir = AnimeClient_TestCase::TEST_DATA_DIR . '/Kitsu';
$this->beforeTransform = Json::decodeFile("{$this->dir}/animeBeforeTransform.json");
$this->afterTransform = Json::decodeFile("{$this->dir}/animeAfterTransform.json");
$this->transformer = new AnimeTransformer();
}
public function testTransform()
{
$expected = $this->afterTransform;
$actual = $this->transformer->transform($this->beforeTransform);
// Json::encodeFile("{$this->dir}/animeAfterTransform.json", $actual);
$this->assertEquals($expected, $actual);
}
}

70
tests/API/XMLTest.php Normal file
View File

@ -0,0 +1,70 @@
<?php declare(strict_types=1);
namespace Aviat\AnimeClient\Tests\API;
use Aviat\AnimeClient\API\XML;
use PHPUnit\Framework\TestCase;
class XMLTest extends TestCase {
public function setUp()
{
$this->xml = file_get_contents(__DIR__ . '/../test_data/XML/xmlTestFile.xml');
$this->expectedXml = file_get_contents(__DIR__ . '/../test_data/XML/minifiedXmlTestFile.xml');
$this->array = [
'entry' => [
'foo' => [
'bar' => [
'baz' => 42
]
],
'episode' => '11',
'status' => 'watching',
'score' => '7',
'storage_type' => '1',
'storage_value' => '2.5',
'times_rewatched' => '1',
'rewatch_value' => '3',
'date_start' => '01152015',
'date_finish' => '10232016',
'priority' => '2',
'enable_discussion' => '0',
'enable_rewatching' => '1',
'comments' => 'Should you say something?',
'tags' => 'test tag, 2nd tag'
]
];
$this->object = new XML();
}
public function testToArray()
{
$this->assertEquals($this->array, XML::toArray($this->xml));
}
public function testParse()
{
$this->object->setXML($this->xml);
$this->assertEquals($this->array, $this->object->parse());
}
public function testToXML()
{
$this->assertEquals($this->expectedXml, XML::toXML($this->array));
}
public function testCreateXML()
{
$this->object->setData($this->array);
$this->assertEquals($this->expectedXml, $this->object->createXML());
}
public function testToString()
{
$this->object->setData($this->array);
$this->assertEquals($this->expectedXml, $this->object->__toString());
$this->assertEquals($this->expectedXml, (string) $this->object);
}
}

View File

@ -7,8 +7,11 @@ use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack; use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use Zend\Diactoros\Response as HttpResponse; use PHPUnit\Framework\TestCase;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\{
Response as HttpResponse,
ServerRequestFactory
};
define('ROOT_DIR', __DIR__ . '/../'); define('ROOT_DIR', __DIR__ . '/../');
define('TEST_DATA_DIR', __DIR__ . '/test_data'); define('TEST_DATA_DIR', __DIR__ . '/test_data');
@ -17,7 +20,7 @@ define('TEST_VIEW_DIR', __DIR__ . '/test_views');
/** /**
* Base class for TestCases * Base class for TestCases
*/ */
class AnimeClient_TestCase extends PHPUnit_Framework_TestCase { class AnimeClient_TestCase extends TestCase {
// Test directory constants // Test directory constants
const ROOT_DIR = ROOT_DIR; const ROOT_DIR = ROOT_DIR;
const SRC_DIR = AnimeClient::SRC_DIR; const SRC_DIR = AnimeClient::SRC_DIR;

View File

@ -1,10 +1,12 @@
<?php <?php declare(strict_types=1);
use Aura\Router\RouterFactory; use Aura\Router\RouterFactory;
use Aura\Web\WebFactory; use Aura\Web\WebFactory;
use Aviat\AnimeClient\Controller; use Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Controller\Anime as AnimeController; use Aviat\AnimeClient\Controller\{
use Aviat\AnimeClient\Controller\Collection as CollectionController; Anime as AnimeController,
use Aviat\AnimeClient\Controller\Manga as MangaController; Collection as CollectionController,
Manga as MangaController
};
class ControllerTest extends AnimeClient_TestCase { class ControllerTest extends AnimeClient_TestCase {

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,385 @@
{
"anime": {
"11474": {
"slug": "hibike-euphonium-2",
"synopsis": "Second season of Hibike! Euphonium.",
"coverImageTopOffset": 120,
"titles": {
"en": "Sound! Euphonium 2",
"en_jp": "Hibike! Euphonium 2",
"ja_jp": "\u97ff\u3051\uff01\u30e6\u30fc\u30d5\u30a9\u30cb\u30a2\u30e0 \uff12"
},
"canonicalTitle": "Hibike! Euphonium 2",
"abbreviatedTitles": null,
"averageRating": 4.1684326428476,
"ratingFrequencies": {
"0.5": "1",
"1.0": "1",
"1.5": "2",
"2.0": "8",
"2.5": "13",
"3.0": "42",
"3.5": "90",
"4.0": "193",
"4.5": "180",
"5.0": "193",
"nil": "1972"
},
"startDate": "2016-10-06",
"endDate": null,
"posterImage": {
"tiny": "https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/tiny.jpg?1470781430",
"small": "https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/small.jpg?1470781430",
"medium": "https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/medium.jpg?1470781430",
"large": "https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/large.jpg?1470781430",
"original": "https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/original.jpg?1470781430"
},
"coverImage": {
"small": "https:\/\/media.kitsu.io\/anime\/cover_images\/11474\/small.jpg?1476203965",
"large": "https:\/\/media.kitsu.io\/anime\/cover_images\/11474\/large.jpg?1476203965",
"original": "https:\/\/media.kitsu.io\/anime\/cover_images\/11474\/original.jpg?1476203965"
},
"episodeCount": 13,
"episodeLength": 25,
"subtype": "TV",
"youtubeVideoId": "d2Di5swwzxg",
"ageRating": "PG",
"ageRatingGuide": "",
"showType": "TV",
"nsfw": false,
"relationships": {
"genres": ["24", "35", "4"],
"mappings": ["3155"]
}
},
"10802": {
"slug": "nisekoimonogatari",
"synopsis": "Trailer for a fake anime created by Shaft as an April Fool's Day joke.",
"coverImageTopOffset": 80,
"titles": {
"en": "",
"en_jp": "Nisekoimonogatari",
"ja_jp": ""
},
"canonicalTitle": "Nisekoimonogatari",
"abbreviatedTitles": null,
"averageRating": 3.4857993435287,
"ratingFrequencies": {
"0.5": "22",
"1.0": "10",
"1.5": "16",
"2.0": "32",
"2.5": "74",
"3.0": "97",
"3.5": "118",
"4.0": "72",
"4.5": "34",
"5.0": "136",
"nil": "597",
"0.89": "-1",
"3.63": "-1",
"4.11": "-1",
"0.068": "-1",
"0.205": "-1",
"0.274": "-2",
"0.479": "-1",
"0.548": "-1",
"1.096": "-2",
"1.164": "-1",
"1.438": "-1",
"1.918": "-1",
"2.055": "-1",
"3.973": "-1",
"4.178": "-3",
"4.247": "-1",
"4.726": "-1",
"4.932": "-3",
"1.0958904109589": "3",
"0.89041095890411": "2",
"1.02739726027397": "1",
"1.16438356164384": "2",
"1.43835616438356": "2",
"1.57534246575342": "1",
"1.91780821917808": "1",
"2.05479452054794": "2",
"2.12328767123288": "1",
"2.73972602739726": "1",
"2.80821917808219": "2",
"2.94520547945205": "1",
"3.15068493150685": "1",
"3.35616438356164": "2",
"3.63013698630137": "2",
"3.97260273972603": "1",
"4.10958904109589": "2",
"4.17808219178082": "3",
"4.24657534246575": "1",
"4.38356164383562": "2",
"4.65753424657534": "1",
"4.72602739726027": "2",
"4.86301369863014": "1",
"4.93150684931507": "10",
"0.205479452054795": "1",
"0.273972602739726": "2",
"0.479452054794521": "2",
"0.547945205479452": "2",
"0.753424657534246": "1",
"0.0684931506849315": "1"
},
"startDate": "2015-04-01",
"endDate": null,
"posterImage": {
"tiny": "https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/tiny.jpg?1427974534",
"small": "https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/small.jpg?1427974534",
"medium": "https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/medium.jpg?1427974534",
"large": "https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/large.jpg?1427974534",
"original": "https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/original.jpg?1427974534"
},
"coverImage": {
"small": "https:\/\/media.kitsu.io\/anime\/cover_images\/10802\/small.jpg?1427928458",
"large": "https:\/\/media.kitsu.io\/anime\/cover_images\/10802\/large.jpg?1427928458",
"original": "https:\/\/media.kitsu.io\/anime\/cover_images\/10802\/original.jpg?1427928458"
},
"episodeCount": 1,
"episodeLength": 1,
"subtype": "ONA",
"youtubeVideoId": "",
"ageRating": "PG",
"ageRatingGuide": "Teens 13 or older",
"showType": "ONA",
"nsfw": false,
"relationships": {
"genres": ["3"],
"mappings": ["1755"]
}
},
"11887": {
"slug": "brave-witches",
"synopsis": "In September 1944, allied forces led by the 501st Joint Fighter Wing \"Strike Witches\" successfully eliminate the Neuroi threat from the skies of the Republic of Gallia, thus ensuring the security of western Europe. Taking advantage of this victory, allied forces begin a full-fledged push toward central and eastern Europe. From a base in Petersburg in the Empire of Orussia, the 502nd Joint Fighter Wing \"Brave Witches,\" upon whom mankind has placed its hopes, flies with courage in the cold skies of eastern Europe.\n\n(Source: MAL News)",
"coverImageTopOffset": 380,
"titles": {
"en": "",
"en_jp": "Brave Witches",
"ja_jp": "\u30d6\u30ec\u30a4\u30d6\u30a6\u30a3\u30c3\u30c1\u30fc\u30ba"
},
"canonicalTitle": "Brave Witches",
"abbreviatedTitles": null,
"averageRating": 3.5846888163849,
"ratingFrequencies": {
"0.5": "1",
"1.0": "4",
"1.5": "8",
"2.0": "12",
"2.5": "17",
"3.0": "33",
"3.5": "41",
"4.0": "32",
"4.5": "9",
"5.0": "19",
"nil": "620"
},
"startDate": "2016-10-06",
"endDate": null,
"posterImage": {
"tiny": "https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/tiny.jpg?1476481854",
"small": "https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/small.jpg?1476481854",
"medium": "https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/medium.jpg?1476481854",
"large": "https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/large.jpg?1476481854",
"original": "https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/original.png?1476481854"
},
"coverImage": {
"small": "https:\/\/media.kitsu.io\/anime\/cover_images\/11887\/small.jpg?1479834725",
"large": "https:\/\/media.kitsu.io\/anime\/cover_images\/11887\/large.jpg?1479834725",
"original": "https:\/\/media.kitsu.io\/anime\/cover_images\/11887\/original.jpg?1479834725"
},
"episodeCount": 12,
"episodeLength": 24,
"subtype": "TV",
"youtubeVideoId": "VLUqd-jEBuE",
"ageRating": "R",
"ageRatingGuide": "Mild Nudity",
"showType": "TV",
"nsfw": false,
"relationships": {
"genres": ["5", "8", "28", "1", "25"],
"mappings": ["2593"]
}
},
"12024": {
"slug": "www-working",
"synopsis": "Daisuke Higashida is a serious first-year student at Higashizaka High School. He lives a peaceful everyday life even though he is not satisfied with the family who doesn't laugh at all and makes him tired. However, his father's company goes bankrupt one day, and he can no longer afford allowances, cellphone bills, and commuter tickets. When his father orders him to take up a part-time job, Daisuke decides to work at a nearby family restaurant in order to avoid traveling 15 kilometers to school by bicycle.",
"coverImageTopOffset": 165,
"titles": {
"en": "WWW.WAGNARIA!!",
"en_jp": "WWW.Working!!",
"ja_jp": ""
},
"canonicalTitle": "WWW.Working!!",
"abbreviatedTitles": null,
"averageRating": 3.8238374224378,
"ratingFrequencies": {
"1.0": "2",
"1.5": "7",
"2.0": "19",
"2.5": "28",
"3.0": "68",
"3.5": "114",
"4.0": "144",
"4.5": "78",
"5.0": "74",
"nil": "1182"
},
"startDate": "2016-10-01",
"endDate": "2016-12-24",
"posterImage": {
"tiny": "https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/tiny.jpg?1473990267",
"small": "https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/small.jpg?1473990267",
"medium": "https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/medium.jpg?1473990267",
"large": "https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/large.jpg?1473990267",
"original": "https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/original.jpg?1473990267"
},
"coverImage": {
"small": "https:\/\/media.kitsu.io\/anime\/cover_images\/12024\/small.jpg?1479834612",
"large": "https:\/\/media.kitsu.io\/anime\/cover_images\/12024\/large.jpg?1479834612",
"original": "https:\/\/media.kitsu.io\/anime\/cover_images\/12024\/original.png?1479834612"
},
"episodeCount": 13,
"episodeLength": 23,
"subtype": "TV",
"youtubeVideoId": "",
"ageRating": "PG",
"ageRatingGuide": "Teens 13 or older",
"showType": "TV",
"nsfw": false,
"relationships": {
"genres": ["3", "16"],
"mappings": ["2538"]
}
},
"12465": {
"slug": "bishoujo-yuugi-unit-crane-game-girls-galaxy",
"synopsis": "Second season of Bishoujo Yuugi Unit Crane Game Girls.",
"coverImageTopOffset": 0,
"titles": {
"en": "Crane Game Girls Galaxy",
"en_jp": "Bishoujo Yuugi Unit Crane Game Girls Galaxy",
"ja_jp": "\u7f8e\u5c11\u5973\u904a\u622f\u30e6\u30cb\u30c3\u30c8 \u30af\u30ec\u30fc\u30f3\u30b2\u30fc\u30eb\u30ae\u30e3\u30e9\u30af\u30b7\u30fc"
},
"canonicalTitle": "Bishoujo Yuugi Unit Crane Game Girls Galaxy",
"abbreviatedTitles": null,
"averageRating": null,
"ratingFrequencies": {
"0.5": "2",
"1.0": "2",
"1.5": "0",
"2.0": "4",
"2.5": "6",
"3.0": "2",
"3.5": "4",
"4.0": "1",
"4.5": "2",
"nil": "66"
},
"startDate": "2016-10-05",
"endDate": null,
"posterImage": {
"tiny": "https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/tiny.jpg?1473601756",
"small": "https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/small.jpg?1473601756",
"medium": "https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/medium.jpg?1473601756",
"large": "https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/large.jpg?1473601756",
"original": "https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/original.png?1473601756"
},
"coverImage": null,
"episodeCount": null,
"episodeLength": 13,
"subtype": "TV",
"youtubeVideoId": "",
"ageRating": "PG",
"ageRatingGuide": "Children",
"showType": "TV",
"nsfw": false,
"relationships": {
"genres": ["3"],
"mappings": ["9871"]
}
}
},
"genres": {
"24": {
"name": "School",
"slug": "school",
"description": null
},
"35": {
"name": "Music",
"slug": "music",
"description": null
},
"4": {
"name": "Drama",
"slug": "drama",
"description": ""
},
"3": {
"name": "Comedy",
"slug": "comedy",
"description": null
},
"5": {
"name": "Sci-Fi",
"slug": "sci-fi",
"description": null
},
"8": {
"name": "Magic",
"slug": "magic",
"description": null
},
"28": {
"name": "Military",
"slug": "military",
"description": null
},
"1": {
"name": "Action",
"slug": "action",
"description": ""
},
"25": {
"name": "Ecchi",
"slug": "ecchi",
"description": ""
},
"16": {
"name": "Slice of Life",
"slug": "slice-of-life",
"description": ""
}
},
"mappings": {
"3155": {
"externalSite": "myanimelist\/anime",
"externalId": "31988",
"relationships": []
},
"1755": {
"externalSite": "myanimelist\/anime",
"externalId": "30514",
"relationships": []
},
"2593": {
"externalSite": "myanimelist\/anime",
"externalId": "32866",
"relationships": []
},
"2538": {
"externalSite": "myanimelist\/anime",
"externalId": "33094",
"relationships": []
},
"9871": {
"externalSite": "myanimelist\/anime",
"externalId": "33541",
"relationships": []
}
}
}

View File

@ -0,0 +1,51 @@
{
"title": "Attack on Titan",
"titles": ["Attack on Titan", "Shingeki no Kyojin", "\u9032\u6483\u306e\u5de8\u4eba"],
"status": "Finished Airing",
"cover_image": "https:\/\/media.kitsu.io\/anime\/poster_images\/7442\/small.jpg?1418580054",
"show_type": "TV",
"episode_count": 25,
"episode_length": 24,
"synopsis": "Several hundred years ago, humans were nearly exterminated by titans. Titans are typically several stories tall, seem to have no intelligence, devour human beings and, worst of all, seem to do it for the pleasure rather than as a food source. A small percentage of humanity survived by enclosing themselves in a city protected by extremely high walls, even taller than the biggest of titans. Flash forward to the present and the city has not seen a titan in over 100 years. Teenage boy Eren and his foster sister Mikasa witness something horrific as the city walls are destroyed by a colossal titan that appears out of thin air. As the smaller titans flood the city, the two kids watch in horror as their mother is eaten alive. Eren vows that he will murder every single titan and take revenge for all of mankind.\n\n(Source: ANN)",
"age_rating": "R",
"age_rating_guide": "Violence, Profanity",
"url": "https:\/\/kitsu.io\/anime\/attack-on-titan",
"genres": ["Action", "Drama", "Fantasy", "Super Power"],
"streaming_links": [{
"meta": {
"name": "Crunchyroll",
"link": true,
"logo": "<svg width=\"50\" height=\"50\" viewBox=\"0 0 50 50\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"><g fill=\"#F78B24\" fill-rule=\"evenodd\"><path d=\"M22.549 49.145c-.815-.077-2.958-.456-3.753-.663-6.873-1.79-12.693-6.59-15.773-13.009C1.335 31.954.631 28.807.633 24.788c.003-4.025.718-7.235 2.38-10.686 1.243-2.584 2.674-4.609 4.706-6.66 3.8-3.834 8.614-6.208 14.067-6.936 1.783-.239 5.556-.161 7.221.148 3.463.642 6.571 1.904 9.357 3.797 5.788 3.934 9.542 9.951 10.52 16.861.21 1.48.332 4.559.19 4.816-.077.14-.117-.007-.167-.615-.25-3.015-1.528-6.66-3.292-9.388C40.253 7.836 30.249 4.32 20.987 7.467c-7.15 2.43-12.522 8.596-13.997 16.06-.73 3.692-.51 7.31.658 10.882a21.426 21.426 0 0 0 13.247 13.518c1.475.515 3.369.944 4.618 1.047 1.496.122 1.119.239-.727.224-1.006-.008-2.013-.032-2.237-.053z\"><\/path><path d=\"M27.685 46.1c-7.731-.575-14.137-6.455-15.474-14.204-.243-1.41-.29-4.047-.095-5.345 1.16-7.706 6.97-13.552 14.552-14.639 1.537-.22 4.275-.143 5.746.162 1.28.266 2.7.737 3.814 1.266l.865.411-.814.392c-2.936 1.414-4.748 4.723-4.323 7.892.426 3.173 2.578 5.664 5.667 6.56 1.112.322 2.812.322 3.925 0 1.438-.417 2.566-1.1 3.593-2.173.346-.362.652-.621.68-.576.027.046.106.545.176 1.11.171 1.395.07 4.047-.204 5.371-.876 4.218-3.08 7.758-6.463 10.374-3.2 2.476-7.434 3.711-11.645 3.399z\"><\/path><\/g><\/svg>"
},
"link": "http:\/\/www.crunchyroll.com\/attack-on-titan",
"subs": ["en"],
"dubs": ["ja"]
}, {
"meta": {
"name": "Hulu",
"link": true,
"logo": "<svg width=\"50\" height=\"50\" viewBox=\"0 0 34 50\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"><path d=\"M22.222 13.889h-11.11V0H0v50h11.111V27.778c0-1.39 1.111-2.778 2.778-2.778h5.555c1.39 0 2.778 1.111 2.778 2.778V50h11.111V25c0-6.111-5-11.111-11.11-11.111z\" fill=\"#8BC34A\" fill-rule=\"evenodd\"><\/path><\/svg>"
},
"link": "http:\/\/www.hulu.com\/attack-on-titan",
"subs": ["en"],
"dubs": ["ja"]
}, {
"meta": {
"name": "Funimation",
"link": true,
"logo": "<svg width=\"50\" height=\"50\" viewBox=\"0 0 50 50\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"><path d=\"M24.066.017a24.922 24.922 0 0 1 13.302 3.286 25.098 25.098 0 0 1 7.833 7.058 24.862 24.862 0 0 1 4.207 9.575c.82 4.001.641 8.201-.518 12.117a24.946 24.946 0 0 1-4.868 9.009 24.98 24.98 0 0 1-7.704 6.118 24.727 24.727 0 0 1-10.552 2.718A24.82 24.82 0 0 1 13.833 47.3c-5.815-2.872-10.408-8.107-12.49-14.25-2.162-6.257-1.698-13.375 1.303-19.28C5.483 8.07 10.594 3.55 16.602 1.435A24.94 24.94 0 0 1 24.066.017zm-8.415 33.31c.464 2.284 1.939 4.358 3.99 5.48 2.174 1.217 4.765 1.444 7.202 1.181 2.002-.217 3.986-.992 5.455-2.397 1.173-1.151 2.017-2.648 2.33-4.267-1.189-.027-2.378 0-3.566-.03-.568.082-1.137-.048-1.705.014-1.232.012-2.465.003-3.697-.01-.655.066-1.309-.035-1.963.013-1.166-.053-2.334.043-3.5-.025-1.515.08-3.03-.035-4.546.042z\" fill=\"#411299\" fill-rule=\"evenodd\"><\/path><\/svg>"
},
"link": "http:\/\/www.funimation.com\/shows\/attack-on-titan\/videos\/episodes",
"subs": ["en"],
"dubs": ["ja"]
}, {
"meta": {
"name": "Netflix",
"link": false,
"logo": "<svg width=\"50\" height=\"50\" viewBox=\"0 0 26 50\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"><path d=\"M.057.258C2.518.253 4.982.263 7.446.253c2.858 7.76 5.621 15.556 8.456 23.324.523 1.441 1.003 2.897 1.59 4.312.078-9.209.01-18.42.034-27.631h7.763v46.36c-2.812.372-5.637.627-8.457.957-1.203-3.451-2.396-6.902-3.613-10.348-1.796-5.145-3.557-10.302-5.402-15.428.129 8.954.015 17.912.057 26.871-2.603.39-5.227.637-7.815 1.119C.052 33.279.06 16.768.057.258z\" fill=\"#E21221\" fill-rule=\"evenodd\"><\/path><\/svg>"
},
"link": "t",
"subs": ["en"],
"dubs": ["ja"]
}]
}

View File

@ -0,0 +1,291 @@
{
"slug": "attack-on-titan",
"synopsis": "Several hundred years ago, humans were nearly exterminated by titans. Titans are typically several stories tall, seem to have no intelligence, devour human beings and, worst of all, seem to do it for the pleasure rather than as a food source. A small percentage of humanity survived by enclosing themselves in a city protected by extremely high walls, even taller than the biggest of titans. Flash forward to the present and the city has not seen a titan in over 100 years. Teenage boy Eren and his foster sister Mikasa witness something horrific as the city walls are destroyed by a colossal titan that appears out of thin air. As the smaller titans flood the city, the two kids watch in horror as their mother is eaten alive. Eren vows that he will murder every single titan and take revenge for all of mankind.\n\n(Source: ANN)",
"coverImageTopOffset": 263,
"titles": {
"en": "Attack on Titan",
"en_jp": "Shingeki no Kyojin",
"ja_jp": "\u9032\u6483\u306e\u5de8\u4eba"
},
"canonicalTitle": "Attack on Titan",
"abbreviatedTitles": null,
"averageRating": 4.2678183033371,
"ratingFrequencies": {
"0.0": "3",
"0.5": "126",
"1.0": "292",
"1.5": "172",
"2.0": "394",
"2.5": "817",
"3.0": "2423",
"3.5": "3210",
"4.0": "5871",
"4.5": "6159",
"5.0": "13117",
"nil": "18571",
"0.479": "-1",
"4.658": "-3",
"4.726": "-1",
"4.932": "-1",
"2.05479452054794": "1",
"2.53424657534247": "1",
"4.10958904109589": "1",
"4.65753424657534": "3",
"4.72602739726027": "3",
"4.86301369863014": "1",
"4.93150684931507": "2",
"0.273972602739726": "1",
"0.410958904109589": "2",
"0.479452054794521": "1",
"0.684931506849315": "1"
},
"startDate": "2013-04-07",
"endDate": "2013-09-28",
"posterImage": {
"tiny": "https:\/\/media.kitsu.io\/anime\/poster_images\/7442\/tiny.jpg?1418580054",
"small": "https:\/\/media.kitsu.io\/anime\/poster_images\/7442\/small.jpg?1418580054",
"medium": "https:\/\/media.kitsu.io\/anime\/poster_images\/7442\/medium.jpg?1418580054",
"large": "https:\/\/media.kitsu.io\/anime\/poster_images\/7442\/large.jpg?1418580054",
"original": "https:\/\/media.kitsu.io\/anime\/poster_images\/7442\/original.jpg?1418580054"
},
"coverImage": {
"small": "https:\/\/media.kitsu.io\/anime\/cover_images\/7442\/small.jpg?1471880659",
"large": "https:\/\/media.kitsu.io\/anime\/cover_images\/7442\/large.jpg?1471880659",
"original": "https:\/\/media.kitsu.io\/anime\/cover_images\/7442\/original.png?1471880659"
},
"episodeCount": 25,
"episodeLength": 24,
"subtype": "TV",
"youtubeVideoId": "n4Nj6Y_SNYI",
"ageRating": "R",
"ageRatingGuide": "Violence, Profanity",
"showType": "TV",
"nsfw": false,
"included": [
{
"id": "23",
"type": "genres",
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/genres\/23"
},
"attributes": {
"name": "Super Power",
"slug": "super-power",
"description": null
}
},
{
"id": "11",
"type": "genres",
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/genres\/11"
},
"attributes": {
"name": "Fantasy",
"slug": "fantasy",
"description": ""
}
},
{
"id": "4",
"type": "genres",
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/genres\/4"
},
"attributes": {
"name": "Drama",
"slug": "drama",
"description": ""
}
},
{
"id": "1",
"type": "genres",
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/genres\/1"
},
"attributes": {
"name": "Action",
"slug": "action",
"description": ""
}
},
{
"id": "5686",
"type": "mappings",
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/mappings\/5686"
},
"attributes": {
"externalSite": "myanimelist\/anime",
"externalId": "16498"
},
"relationships": {
"media": {
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/mappings\/5686\/relationships\/media",
"related": "https:\/\/kitsu.io\/api\/edge\/mappings\/5686\/media"
}
}
}
},
{
"id": "14153",
"type": "mappings",
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/mappings\/14153"
},
"attributes": {
"externalSite": "thetvdb\/series",
"externalId": "267440"
},
"relationships": {
"media": {
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/mappings\/14153\/relationships\/media",
"related": "https:\/\/kitsu.io\/api\/edge\/mappings\/14153\/media"
}
}
}
},
{
"id": "15073",
"type": "mappings",
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/mappings\/15073"
},
"attributes": {
"externalSite": "thetvdb\/season",
"externalId": "514060"
},
"relationships": {
"media": {
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/mappings\/15073\/relationships\/media",
"related": "https:\/\/kitsu.io\/api\/edge\/mappings\/15073\/media"
}
}
}
},
{
"id": "103",
"type": "streamingLinks",
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/103"
},
"attributes": {
"url": "http:\/\/www.crunchyroll.com\/attack-on-titan",
"subs": [
"en"
],
"dubs": [
"ja"
]
},
"relationships": {
"streamer": {
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/103\/relationships\/streamer",
"related": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/103\/streamer"
}
},
"media": {
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/103\/relationships\/media",
"related": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/103\/media"
}
}
}
},
{
"id": "102",
"type": "streamingLinks",
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/102"
},
"attributes": {
"url": "http:\/\/www.hulu.com\/attack-on-titan",
"subs": [
"en"
],
"dubs": [
"ja"
]
},
"relationships": {
"streamer": {
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/102\/relationships\/streamer",
"related": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/102\/streamer"
}
},
"media": {
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/102\/relationships\/media",
"related": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/102\/media"
}
}
}
},
{
"id": "101",
"type": "streamingLinks",
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/101"
},
"attributes": {
"url": "http:\/\/www.funimation.com\/shows\/attack-on-titan\/videos\/episodes",
"subs": [
"en"
],
"dubs": [
"ja"
]
},
"relationships": {
"streamer": {
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/101\/relationships\/streamer",
"related": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/101\/streamer"
}
},
"media": {
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/101\/relationships\/media",
"related": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/101\/media"
}
}
}
},
{
"id": "100",
"type": "streamingLinks",
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/100"
},
"attributes": {
"url": "t",
"subs": [
"en"
],
"dubs": [
"ja"
]
},
"relationships": {
"streamer": {
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/100\/relationships\/streamer",
"related": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/100\/streamer"
}
},
"media": {
"links": {
"self": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/100\/relationships\/media",
"related": "https:\/\/kitsu.io\/api\/edge\/streaming-links\/100\/media"
}
}
}
}
]
}

View File

@ -0,0 +1 @@
{"id":"15839442","mal_id":"33206","episodes":{"watched":0,"total":"-","length":null},"airing":{"status":"Currently Airing","started":"2017-01-12","ended":null},"anime":{"age_rating":null,"titles":["Kobayashi-san Chi no Maid Dragon","Miss Kobayashi's Dragon Maid","\u5c0f\u6797\u3055\u3093\u3061\u306e\u30e1\u30a4\u30c9\u30e9\u30b4\u30f3"],"slug":"kobayashi-san-chi-no-maid-dragon","type":"TV","image":"https:\/\/media.kitsu.io\/anime\/poster_images\/12243\/small.jpg?1481144116","genres":["Comedy","Fantasy","Slice of Life"],"streaming_links":[]},"watching_status":"current","notes":null,"rewatching":false,"rewatched":0,"user_rating":"-","private":false}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<entry><foo><bar><baz>42</baz></bar></foo><episode>11</episode><status>watching</status><score>7</score><storage_type>1</storage_type><storage_value>2.5</storage_value><times_rewatched>1</times_rewatched><rewatch_value>3</rewatch_value><date_start>01152015</date_start><date_finish>10232016</date_finish><priority>2</priority><enable_discussion>0</enable_discussion><enable_rewatching>1</enable_rewatching><comments>Should you say something?</comments><tags>test tag, 2nd tag</tags></entry>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<entry>
<foo>
<bar>
<baz>42</baz>
</bar>
</foo>
<episode>11</episode>
<status>watching</status>
<score>7</score>
<storage_type>1</storage_type>
<storage_value>2.5</storage_value>
<times_rewatched>1</times_rewatched>
<rewatch_value>3</rewatch_value>
<date_start>01152015</date_start>
<date_finish>10232016</date_finish>
<priority>2</priority>
<enable_discussion>0</enable_discussion>
<enable_rewatching>1</enable_rewatching>
<comments>Should you say something?</comments>
<tags>test tag, 2nd tag</tags>
</entry>