Merge branch 'develop'

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

View File

@ -1,5 +1,8 @@
# Changelog
## Version 4
* Updated to use Kitsu API after discontinuation of Hummingbird
## Version 3
* Converted user configuration to toml files
* 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
[![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)
[![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
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2016 Timothy J. Warren
* @copyright 2015 - 2017 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient
@ -19,9 +19,15 @@ namespace Aviat\AnimeClient;
use Aura\Html\HelperLocatorFactory;
use Aura\Router\RouterContainer;
use Aura\Session\SessionFactory;
use Aviat\AnimeClient\API\Kitsu\Auth as KitsuAuth;
use Aviat\AnimeClient\API\Kitsu\ListItem as KitsuListItem;
use Aviat\AnimeClient\API\Kitsu\KitsuModel;
use Aviat\AnimeClient\API\Kitsu\{
Auth as KitsuAuth,
ListItem as KitsuListItem,
KitsuModel
};
use Aviat\AnimeClient\API\MAL\{
ListItem as MALListItem,
Model as MALModel
};
use Aviat\AnimeClient\Model;
use Aviat\Banker\Pool;
use Aviat\Ion\Config;
@ -111,6 +117,15 @@ return function(array $config_array = []) {
$listItem->setContainer($container);
$model = new KitsuModel($listItem);
$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;
});
$container->set('api-model', function($container) {

View File

@ -2,6 +2,10 @@
# 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"
[connection]

View File

@ -3,7 +3,7 @@
################################################################################
# Username for anime and manga lists
hummingbird_username = "timw4mail"
kitsu_username = "timw4mail"
# Whose list is it?
whose_list = "Tim"
@ -14,6 +14,9 @@ show_anime_collection = true
# do you wish to show the manga collection?
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 = "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">
<?php foreach($items as $item): ?>
<?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()): ?>
<button title="Increment episode count" class="plus_one" hidden>+1 Episode</button>
<?php endif ?>
<?= $helper->img($item['anime']['image']); ?>
<img src="<?= $item['anime']['image'] ?>" alt="" />
<div class="name">
<a href="<?= $url->generate('anime.details', ['id' => $item['anime']['slug']]); ?>">
<?= array_shift($item['anime']['titles']) ?>
@ -32,6 +32,7 @@
</span>
</div>
<?php endif ?>
<?php if ($item['private'] || $item['rewatching']): ?>
<div class="row">
<?php foreach(['private', 'rewatching'] as $attr): ?>
@ -41,11 +42,29 @@
<?php endforeach ?>
</div>
<?php endif ?>
<?php if ($item['rewatched'] > 0): ?>
<div class="row">
<div>Rewatched <?= $item['rewatched'] ?> time(s)</div>
</div>
<?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="user_rating">Rating: <?= $item['user_rating'] ?> / 10</div>
<div class="completion">Episodes:

View File

@ -1,10 +1,10 @@
<main class="details">
<section class="flex flex-no-wrap">
<div>
<img class="cover" src="<?= $data['cover_image'] ?>" alt="<?= $data['title'] ?> cover image" />
<img class="cover" src="<?= $data['cover_image'] ?>" alt="" />
<br />
<br />
<table>
<table class="media_details">
<tr>
<td class="align_right">Airing Status</td>
<td><?= $data['status'] ?></td>
@ -15,16 +15,18 @@
</tr>
<tr>
<td>Episode Count</td>
<td><?= $data['episode_count'] ?></td>
<td><?= $data['episode_count'] ?? '-' ?></td>
</tr>
<tr>
<td>Episode Length</td>
<td><?= $data['episode_length'] ?> minutes</td>
</tr>
<?php if ( ! empty($data['age_rating'])): ?>
<tr>
<td>Age Rating</td>
<td><abbr title="<?= $data['age_rating_guide'] ?>"><?= $data['age_rating'] ?></abbr></td>
</tr>
<?php endif ?>
<tr>
<td>Genres</td>
<td>
@ -40,6 +42,39 @@
<?php endforeach ?>
<br />
<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>
</section>
</main>

View File

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

View File

@ -19,7 +19,7 @@
<th>Type</th>
<th>Progress</th>
<th>Rated</th>
<th>Attributes</th>
<th colspan="2">Attributes</th>
<th>Notes</th>
<th>Genres</th>
</tr>
@ -61,6 +61,17 @@
<?php endforeach ?>
</ul>
</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>
<p><?= $escape->html($item['notes']) ?></p>
</td>

View File

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

View File

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

View File

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

View File

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

@ -1,25 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
colors="true"
stopOnFailure="false"
bootstrap="tests/bootstrap.php"
beStrictAboutTestsThatDoNotTestAnything="true"
colors="true"
stopOnFailure="false"
bootstrap="tests/bootstrap.php"
>
<filter>
<whitelist>
<directory suffix=".php">src</directory>
</whitelist>
</filter>
<testsuites>
<testsuite name="AnimeClient">
<directory>tests</directory>
</testsuite>
</testsuites>
<php>
<server name="HTTP_USER_AGENT" value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0" />
<server name="HTTP_HOST" value="localhost" />
<server name="SERVER_NAME" value="localhost" />
<server name="REQUEST_URI" value="/" />
<server name="REQUEST_METHOD" value="GET" />
</php>
<filter>
<whitelist>
<directory suffix=".php">src</directory>
</whitelist>
</filter>
<testsuites>
<testsuite name="AnimeClient">
<directory>tests</directory>
</testsuite>
</testsuites>
<php>
<server name="HTTP_USER_AGENT" value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0" />
<server name="HTTP_HOST" value="localhost" />
<server name="SERVER_NAME" value="localhost" />
<server name="REQUEST_URI" value="/" />
<server name="REQUEST_METHOD" value="GET" />
</php>
</phpunit>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@
*
* @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2016 Timothy J. Warren
* @copyright 2015 - 2017 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient
@ -16,6 +16,8 @@
namespace Aviat\AnimeClient\API;
use Aviat\Ion\Json;
/**
* Class encapsulating Json API data structure for a request or response
*/
@ -40,12 +42,19 @@ class JsonAPI {
*/
protected $data = [];
/**
* Data array parsed out from a request
*
* @var array
*/
protected $parsedData = [];
/**
* Related objects included with the request
*
* @var array
*/
protected $included = [];
public $included = [];
/**
* Pagination links
@ -54,13 +63,142 @@ class JsonAPI {
*/
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
*
* @param array $data
*/
public function parse(array $data)
{
$this->included = static::organizeIncludes($data['included']);
}
/**
* Return data array after input is parsed
* to inline includes inside of relationship objects
*
* @return array
*/
public function getParsedData(): array
{
}
/**
* Take inlined included data and inline it into the main object's relationships
*
* @param array $mainObject
* @param array $included
* @return array
*/
public static function inlineIncludedIntoMainObject(array $mainObject, array $included): array
{
$output = clone $mainObject;
}
/**
* 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
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2016 Timothy J. Warren
* @copyright 2015 - 2017 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient
@ -24,7 +24,7 @@ use Aviat\AnimeClient\API\Kitsu\Enum\{
use DateTimeImmutable;
/**
* Constants and mappings for the Kitsu API
* Data massaging helpers for the Kitsu API
*/
class Kitsu {
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()
{
return [
@ -84,6 +89,78 @@ class Kitsu {
return AnimeAiringStatus::NOT_YET_AIRED;
}
}
/**
* 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
@ -110,66 +187,6 @@ class Kitsu {
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
*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@
*
* @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2016 Timothy J. Warren
* @copyright 2015 - 2017 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @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

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

View File

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

View File

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

View File

@ -304,7 +304,8 @@ class Controller {
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()
{
$this->cache->purge();
$this->cache->clear();
$this->outputHTML('blank', [
'title' => 'Cache cleared'
], NULL, 200);

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ namespace Aviat\AnimeClient;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Friend;
use GuzzleHttp\Exception\ServerException;
/**
* Basic routing/ dispatch
@ -128,9 +129,23 @@ class Dispatcher extends RoutingBase {
$actionMethod = $error_route['action_method'];
$params = $error_route['params'];
}
// Actually instantiate the controller
$this->call($controllerName, $actionMethod, $params);
// Try to catch API errors in a presentable fashion
try
{
// Actually instantiate the controller
$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()
]);
}
}
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,6 @@
namespace Aviat\AnimeClient;
use abeautifulsite\SimpleImage;
use Aviat\Ion\ConfigInterface;
use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
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\HandlerStack;
use GuzzleHttp\Psr7\Response;
use Zend\Diactoros\Response as HttpResponse;
use Zend\Diactoros\ServerRequestFactory;
use PHPUnit\Framework\TestCase;
use Zend\Diactoros\{
Response as HttpResponse,
ServerRequestFactory
};
define('ROOT_DIR', __DIR__ . '/../');
define('TEST_DATA_DIR', __DIR__ . '/test_data');
@ -17,7 +20,7 @@ define('TEST_VIEW_DIR', __DIR__ . '/test_views');
/**
* Base class for TestCases
*/
class AnimeClient_TestCase extends PHPUnit_Framework_TestCase {
class AnimeClient_TestCase extends TestCase {
// Test directory constants
const ROOT_DIR = ROOT_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\Web\WebFactory;
use Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Controller\Anime as AnimeController;
use Aviat\AnimeClient\Controller\Collection as CollectionController;
use Aviat\AnimeClient\Controller\Manga as MangaController;
use Aviat\AnimeClient\Controller\{
Anime as AnimeController,
Collection as CollectionController,
Manga as MangaController
};
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>