Merge branch 'develop' into 'master'

Merge develop into master

See merge request !18
This commit is contained in:
Timothy Warren 2017-04-24 09:28:40 -04:00
commit e632843bab
69 changed files with 2060 additions and 3904 deletions

6
.gitignore vendored
View File

@ -13,7 +13,7 @@ composer.lock
*.sqlite
*.db
*.sqlite3
docs/*
apidocs/**
tests/test_data/sessions/*
cache.properties
build/**
@ -26,3 +26,7 @@ phinx.yml
.idea/
Caddyfile
build/humbuglog.txt
public/images/anime/**
public/images/avatars/**
public/images/manga/**
public/images/characters/**

View File

@ -45,6 +45,10 @@ Update your anime/manga list on Kitsu.io and MyAnimeList.net
3. Configure settings in `app/config/config.toml` to your liking
4. Create the following directories if they don't exist, and make sure they are world writable
* public/js/cache
* public/images/avatars
* public/images/anime
* public/images/characters
* public/images/manga
5. Make sure the `console` script is executable
### Using MAL API

View File

@ -18,46 +18,6 @@
return [
/*
|--------------------------------------------------------------------------
| CSS Folder
|--------------------------------------------------------------------------
|
| The folder where css files exist, in relation to the document root
|
*/
'css_root' => 'css/',
/*
|--------------------------------------------------------------------------
| Path from
|--------------------------------------------------------------------------
|
| Path fragment to rewrite in css files
|
*/
'path_from' => '',
/*
|--------------------------------------------------------------------------
| Path to
|--------------------------------------------------------------------------
|
| The path fragment replacement for the css files
|
*/
'path_to' => '',
/*
|--------------------------------------------------------------------------
| CSS Groups file
|--------------------------------------------------------------------------
|
| The file where the css groups are configured
|
*/
'css_groups_file' => __DIR__ . '/minify_css_groups.php',
/*
|--------------------------------------------------------------------------
| JS Folder
@ -70,13 +30,40 @@ return [
/*
|--------------------------------------------------------------------------
| JS Groups file
| JS Groups
|--------------------------------------------------------------------------
|
| The file where the javascript groups are configured
| Config array for javascript files to concatenate and minify
|
*/
'js_groups_file' => __DIR__ . '/minify_js_groups.php',
'groups' => [
'base' => [
'base/classList.js',
'base/AnimeClient.js',
],
'event' => [
'base/events.js',
],
'table' => [
'base/sort_tables.js',
],
'table_edit' => [
'base/sort_tables.js',
'anime_edit.js',
'manga_edit.js',
],
'edit' => [
'anime_edit.js',
'manga_edit.js',
],
'anime_collection' => [
'lib/mustache.js',
'anime_collection.js',
],
'manga_collection' => [
'lib/mustache.js',
'manga_collection.js',
],
]
];
// End of minify_config.php

View File

@ -1,40 +0,0 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2017 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient
*/
// --------------------------------------------------------------------------
/**
* This is the config array for css files to concatenate and minify
*/
return [
/*-----
Css
-----*/
/*
For each group create an array like so
'my_group' => array(
'path/to/css/file1.css',
'path/to/css/file2.css'
),
*/
'base' => [
'base.css'
]
];
// End of css_groups.php

View File

@ -1,53 +0,0 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2017 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient
*/
// --------------------------------------------------------------------------
/**
* This is the config array for javascript files to concatenate and minify
*/
return [
'base' => [
'base/classList.js',
'base/AnimeClient.js',
],
'event' => [
'base/events.js',
],
'table' => [
'base/sort_tables.js',
],
'table_edit' => [
'base/sort_tables.js',
'anime_edit.js',
'manga_edit.js',
],
'edit' => [
'anime_edit.js',
'manga_edit.js',
],
'anime_collection' => [
'lib/mustache.js',
'anime_collection.js',
],
'manga_collection' => [
'lib/mustache.js',
'manga_collection.js',
],
];
// End of js_groups.php

View File

@ -147,6 +147,16 @@ return [
// ---------------------------------------------------------------------
// Default / Shared routes
// ---------------------------------------------------------------------
'image_proxy' => [
'path' => '/public/images/{type}/{file}',
'action' => 'images',
'controller' => DEFAULT_CONTROLLER,
'verb' => 'get',
'tokens' => [
'type' => '[a-z0-9\-]+',
'file' => '[a-z0-9\-]+\.[a-z]{3}'
]
],
'cache_purge' => [
'path' => '/cache_purge',
'action' => 'clearCache',

View File

@ -15,7 +15,7 @@
<?php if ($auth->isAuthenticated()): ?>
<button title="Increment episode count" class="plus_one" hidden>+1 Episode</button>
<?php endif ?>
<img src="<?= $item['anime']['image'] ?>" alt="" />
<img src="<?= $urlGenerator->assetUrl("images/anime/{$item['anime']['id']}.jpg") ?>" alt="" />
<div class="name">
<a href="<?= $url->generate('anime.details', ['id' => $item['anime']['slug']]); ?>">
<?= array_shift($item['anime']['titles']) ?>

View File

@ -1,7 +1,7 @@
<main class="details fixed">
<section class="flex flex-no-wrap">
<div>
<img class="cover" width="402" height="284" src="<?= $data['cover_image'] ?>" alt="" />
<img class="cover" width="402" height="284" src="<?= $urlGenerator->assetUrl("images/anime/{$data['id']}.jpg") ?>" alt="" />
<br />
<br />
<table class="media_details">
@ -79,8 +79,8 @@
<?php if (count($characters) > 0): ?>
<h2>Characters</h2>
<section class="media-wrap">
<?php foreach($characters as $char): ?>
<section class="align_left media-wrap">
<?php foreach($characters as $id => $char): ?>
<?php if ( ! empty($char['image']['original'])): ?>
<article class="character">
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
@ -88,7 +88,7 @@
<?= $helper->a($link, $char['name']); ?>
</div>
<a href="<?= $link ?>">
<?= $helper->img($char['image']['original'], [
<?= $helper->img($urlGenerator->assetUrl("images/characters/{$id}.jpg"), [
'width' => '225'
]) ?>
</a>

View File

@ -13,7 +13,7 @@
</th>
<th>
<article class="media">
<?= $helper->img($item['anime']['image']); ?>
<?= $helper->img($urlGenerator->assetUrl('images/anime', "{$item['anime']['id']}.jpg")) ?>
</article>
</th>
</tr>

View File

@ -1,12 +1,126 @@
<main class="details fixed">
<?php use Aviat\AnimeClient\API\Kitsu; ?>
<main class="details">
<section class="flex flex-no-wrap">
<div>
<img class="cover" width="284" src="<?= $data['image']['original'] ?>" alt="" />
<img class="cover" width="284" src="<?= $urlGenerator->assetUrl("images/characters/{$data[0]['id']}.jpg") ?>" alt="" />
</div>
<div>
<h2><?= $data['name'] ?></h2>
<h2><?= $data[0]['attributes']['name'] ?></h2>
<p><?= $data['description'] ?></p>
<p class="description"><?= $data[0]['attributes']['description'] ?></p>
</div>
</section>
<?php if (array_key_exists('anime', $data['included']) || array_key_exists('manga', $data['included'])): ?>
<h3>Media</h3>
<section class="flex flex-no-wrap">
<?php if (array_key_exists('anime', $data['included'])): ?>
<div>
<h4>Anime</h4>
<section class="align_left media-wrap">
<?php foreach($data['included']['anime'] as $id => $anime): ?>
<article class="media">
<?php
$link = $url->generate('anime.details', ['id' => $anime['attributes']['slug']]);
$titles = Kitsu::filterTitles($anime['attributes']);
?>
<a href="<?= $link ?>">
<img src="<?= $urlGenerator->assetUrl("images/anime/{$id}.jpg") ?>" width="220" alt="" />
</a>
<div class="name">
<a href="<?= $link ?>">
<?= array_shift($titles) ?>
<?php foreach ($titles as $title): ?>
<br /><small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
</article>
<?php endforeach ?>
</section>
</div>
<?php endif ?>
<?php if (array_key_exists('manga', $data['included'])): ?>
<div>
<h4>Manga</h4>
<section class="align_left media-wrap">
<?php foreach($data['included']['manga'] as $id => $manga): ?>
<article class="media">
<?php
$link = $url->generate('manga.details', ['id' => $manga['attributes']['slug']]);
$titles = Kitsu::filterTitles($manga['attributes']);
?>
<a href="<?= $link ?>">
<img src="<?= $urlGenerator->assetUrl("images/manga/{$id}.jpg") ?>" width="220" alt="" />
</a>
<div class="name">
<a href="<?= $link ?>">
<?= array_shift($titles) ?>
<?php foreach ($titles as $title): ?>
<br /><small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
</article>
<?php endforeach ?>
</section>
</div>
<?php endif ?>
</section>
<?php endif ?>
<section>
<?php if ($castCount > 0): ?>
<h3>Castings</h3>
<?php foreach($castings as $role => $entries): ?>
<h4><?= $role ?></h4>
<?php foreach($entries as $language => $casting): ?>
<h5><?= $language ?></h5>
<table class="min-table">
<tr>
<th>Cast Member</th>
<th>Series</th>
</tr>
<?php foreach($casting as $c):?>
<tr>
<td style="width:229px">
<article class="character">
<img src="<?= $c['person']['image'] ?>" alt="" />
<div class="name">
<?= $c['person']['name'] ?>
</div>
</article>
</td>
<td>
<section class="align_left media-wrap">
<?php foreach($c['series'] as $series): ?>
<article class="media">
<?php
$link = $url->generate('anime.details', ['id' => $series['attributes']['slug']]);
$titles = Kitsu::filterTitles($series['attributes']);
?>
<a href="<?= $link ?>">
<img src="<?= $series['attributes']['posterImage']['small'] ?>" width="220" alt="" />
</a>
<div class="name">
<a href="<?= $link ?>">
<?= array_shift($titles) ?>
<?php foreach ($titles as $title): ?>
<br /><small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
</article>
<?php endforeach ?>
</section>
</td>
</tr>
<?php endforeach; ?>
</table>
<?php endforeach ?>
<?php endforeach ?>
<?php endif ?>
</section>
</main>

View File

@ -11,7 +11,7 @@
<section class="media-wrap">
<?php foreach($items as $item): ?>
<article class="media" id="a-<?= $item['hummingbird_id'] ?>">
<img src="https://media.kitsu.io/anime/poster_images/<?= $item['hummingbird_id'] ?>/small.jpg"
<img src="<?= $urlGenerator->assetUrl("images/anime/{$item['hummingbird_id']}.jpg") ?>"
alt="<?= $item['title'] ?> cover image" />
<div class="name">
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">

View File

@ -6,7 +6,7 @@
<meta http-equiv="cache-control" content="no-store" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self'" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0" />
<link rel="stylesheet" href="<?= $urlGenerator->assetUrl('css.php/g/base/debug') ?>" />
<link rel="stylesheet" href="<?= $urlGenerator->assetUrl('css/app.min.css') ?>" />
<link rel="icon" href="<?= $urlGenerator->assetUrl('images/icons/favicon.ico') ?>" />
<link rel="apple-touch-icon" sizes="57x57" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-57x57.png') ?>">
<link rel="apple-touch-icon" sizes="60x60" href="<?= $urlGenerator->assetUrl('images/icons/apple-icon-60x60.png') ?>">
@ -21,17 +21,19 @@
<link rel="icon" type="image/png" sizes="32x32" href="<?= $urlGenerator->assetUrl('images/icons/favicon-32x32.png') ?>">
<link rel="icon" type="image/png" sizes="96x96" href="<?= $urlGenerator->assetUrl('images/icons/favicon-96x96.png') ?>">
<link rel="icon" type="image/png" sizes="16x16" href="<?= $urlGenerator->assetUrl('images/icons/favicon-16x16.png') ?>">
<link rel="manifest" href="/manifest.json">
<script defer="defer" src="<?= $urlGenerator->assetUrl('js.php/g/base') ?>"></script>
</head>
<body class="<?= $escape->attr($url_type) ?> list">
<header>
<?php include 'main-menu.php' ?>
<?php if(isset($message) && is_array($message)):
<?php
include 'main-menu.php';
if(isset($message) && is_array($message))
{
foreach($message as $m)
{
extract($m);
include 'message.php';
include 'message.php';
}
endif ?>
}
?>
</header>

View File

@ -1,37 +1,58 @@
<?php declare(strict_types=1); namespace Aviat\AnimeClient; ?>
<?php declare(strict_types=1);
namespace Aviat\AnimeClient;
$whose = $config->get('whose_list') . "'s ";
$lastSegment = $urlGenerator->lastSegment();
$extraSegment = $lastSegment === 'list' ? '/list' : '';
?>
<h1 class="flex flex-align-end flex-wrap">
<span class="flex-no-wrap grow-1">
<?php if(strpos($route_path, 'collection') === FALSE): ?>
<a href="<?= $escape->attr($urlGenerator->defaultUrl($url_type)) ?>">
<?= $config->get('whose_list') ?>'s <?= ucfirst($url_type) ?> List
</a>
<?= $helper->a(
$urlGenerator->defaultUrl($url_type),
$whose . ucfirst($url_type) . ' List'
) ?>
<?php if($config->get("show_{$url_type}_collection")): ?>
[<a href="<?= $url->generate('collection.view') ?>"><?= ucfirst($url_type) ?> Collection</a>]
[<?= $helper->a(
$url->generate('collection.view') . $extraSegment,
ucfirst($url_type) . ' Collection'
) ?>]
<?php endif ?>
[<a href="<?= $urlGenerator->defaultUrl($other_type) ?>"><?= ucfirst($other_type) ?> List</a>]
[<?= $helper->a(
$urlGenerator->defaultUrl($other_type) . $extraSegment,
ucfirst($other_type) . ' List'
) ?>]
<?php else: ?>
<a href="<?= $url->generate('collection.view') ?>">
<?= $config->get('whose_list') ?>'s <?= ucfirst($url_type) ?> Collection
</a>
[<a href="<?= $urlGenerator->defaultUrl('anime') ?>">Anime List</a>]
[<a href="<?= $urlGenerator->defaultUrl('manga') ?>">Manga List</a>]
<?= $whose . ucfirst($url_type) . ' Collection' ?>
[<?= $helper->a($urlGenerator->defaultUrl('anime') . $extraSegment, 'Anime List') ?>]
[<?= $helper->a($urlGenerator->defaultUrl('manga') . $extraSegment, 'Manga List') ?>]
<?php endif ?>
</span>
<span class="flex-no-wrap small-font">
[<?= $helper->a($url->generate('user_info'), 'About '. $config->get('whose_list')) ?>]
</span>
<span class="flex-no-wrap small-font">[<?= $helper->a(
$url->generate('user_info'),
'About '. $config->get('whose_list')
) ?>]</span>
<?php if ($auth->isAuthenticated()): ?>
<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>
<button type="button" class="js-clear-cache user-btn">Clear API Cache</button>
</span>
<span class="flex-no-wrap">&nbsp;</span>
<?php endif ?>
<span class="flex-no-wrap small-font">
<?php if ($auth->isAuthenticated()): ?>
<a class="bracketed" href="<?= $url->generate('logout') ?>">Logout</a>
<?= $helper->a(
$url->generate('logout'),
'Logout',
['class' => 'bracketed']
) ?>
<?php else: ?>
[<a href="<?= $url->generate('login'); ?>"><?= $config->get('whose_list') ?>'s Login</a>]
[<?= $helper->a($url->generate('login'), "{$whose} Login") ?>]
<?php endif ?>
</span>
</h1>
@ -40,8 +61,8 @@
<?= $helper->menu($menu_name) ?>
<br />
<ul>
<li class="<?= Util::isNotSelected('list', $urlGenerator->lastSegment()) ?>"><a href="<?= $urlGenerator->url($route_path) ?>">Cover View</a></li>
<li class="<?= Util::isSelected('list', $urlGenerator->lastSegment()) ?>"><a href="<?= $urlGenerator->url("{$route_path}/list") ?>">List View</a></li>
<li class="<?= Util::isNotSelected('list', $lastSegment) ?>"><a href="<?= $urlGenerator->url($route_path) ?>">Cover View</a></li>
<li class="<?= Util::isSelected('list', $lastSegment) ?>"><a href="<?= $urlGenerator->url("{$route_path}/list") ?>">List View</a></li>
</ul>
<?php endif ?>
</nav>

View File

@ -17,7 +17,7 @@
<?php /* <button class="plus_one_volume">+1 Volume</button> */ ?>
</div>
<?php endif ?>
<img src="<?= $escape->attr($item['manga']['image']) ?>" />
<img src="<?= $urlGenerator->assetUrl('images/manga', "{$item['manga']['id']}.jpg") ?>" />
<div class="name">
<a href="<?= $url->generate('manga.details', ['id' => $item['manga']['slug']]) ?>">
<?= $escape->html(array_shift($item['manga']['titles'])) ?>

View File

@ -1,7 +1,7 @@
<main class="details fixed">
<section class="flex flex-no-wrap">
<div>
<img class="cover" src="<?= $data['cover_image'] ?>" alt="<?= $data['title'] ?> cover image" />
<img class="cover" src="<?= $urlGenerator->assetUrl('images/manga', "{$data['id']}.jpg") ?>" alt="<?= $data['title'] ?> cover image" />
<br />
<br />
<table>
@ -39,7 +39,7 @@
<?php if (count($characters) > 0): ?>
<h2>Characters</h2>
<section class="media-wrap">
<?php foreach($characters as $char): ?>
<?php foreach($characters as $id => $char): ?>
<?php if ( ! empty($char['image']['original'])): ?>
<article class="character">
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
@ -47,7 +47,7 @@
<?= $helper->a($link, $char['name']); ?>
</div>
<a href="<?= $link ?>">
<?= $helper->img($char['image']['original'], [
<?= $helper->img($urlGenerator->assetUrl('images/characters', "{$id}.jpg"), [
'width' => '225'
]) ?>
</a>

View File

@ -15,7 +15,7 @@
</th>
<th>
<article class="media">
<?= $helper->img($item['manga']['image']); ?>
<?= $helper->img($urlGenerator->assetUrl('images/manga', "{$item['manga']['id']}.jpg")); ?>
</article>
</th>
</tr>

View File

@ -9,7 +9,12 @@
<?= $attributes['name'] ?>
</a>
</h2>
<img src="<?= $attributes['avatar']['original'] ?>" alt="" />
<?php
$file = basename(parse_url($attributes['avatar']['original'], \PHP_URL_PATH));
$parts = explode('.', $file);
$ext = end($parts);
?>
<img src="<?= $urlGenerator->assetUrl('images/avatars', "{$data['id']}.{$ext}") ?>" alt="" />
</center>
<br />
<br />
@ -65,13 +70,13 @@
<?php if ( ! empty($favorites['characters'])): ?>
<h4>Favorite Characters</h4>
<section class="media-wrap">
<?php foreach($favorites['characters'] as $char): ?>
<?php foreach($favorites['characters'] as $id => $char): ?>
<?php if ( ! empty($char['image']['original'])): ?>
<article class="small_character">
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
<div class="name"><?= $helper->a($link, $char['name']); ?></div>
<a href="<?= $link ?>">
<?= $helper->img($char['image']['original']) ?>
<?= $helper->img($urlGenerator->assetUrl('images/characters', "{$char['id']}.jpg")) ?>
</a>
</article>
<?php endif ?>
@ -88,7 +93,7 @@
$titles = Kitsu::filterTitles($anime);
?>
<a href="<?= $link ?>">
<img src="<?= $anime['posterImage']['small'] ?>" width="220" alt="" />
<img src="<?= $urlGenerator->assetUrl('images/anime', "{$anime['id']}.jpg") ?>" width="220" alt="" />
</a>
<div class="name">
<a href="<?= $link ?>">
@ -112,7 +117,7 @@
$titles = Kitsu::filterTitles($manga);
?>
<a href="<?= $link ?>">
<img src="<?= $manga['posterImage']['small'] ?>" width="220" alt="" />
<img src="<?= $urlGenerator->assetUrl('images/manga', "{$manga['id']}.jpg") ?>" width="220" alt="" />
</a>
<div class="name">
<a href="<?= $link ?>">

View File

@ -21,7 +21,7 @@
"aura/router": "^3.0",
"aura/session": "^2.0",
"aviat/banker": "^1.0.0",
"aviat/ion": "^2.0.0",
"aviat/ion": "^2.1.0",
"monolog/monolog": "^1.0",
"psr/http-message": "~1.0",
"psr/log": "~1.0",
@ -37,12 +37,13 @@
"phploc/phploc": "^3.0",
"phpmd/phpmd": "^2.4",
"phpunit/phpunit": "^6.0",
"robmorgan/phinx": "~0.6.4",
"robmorgan/phinx": "^0.8.0",
"consolidation/robo": "~1.0",
"henrikbjorn/lurker": "^1.1.0",
"symfony/var-dumper": "^3.2",
"squizlabs/php_codesniffer": "^3.0.0@beta",
"phpstan/phpstan": "^0.6.4"
"phpstan/phpstan": "^0.6.4",
"spatie/phpunit-snapshot-assertions": "^0.4.1"
},
"scripts": {
"build": "vendor/bin/robo build",

View File

@ -18,9 +18,9 @@ unset($APP_DIR);
unset($SRC_DIR);
unset($CONF_DIR);
// ---------------------------------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// Start console script
// ---------------------------------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------
$console = new \ConsoleKit\Console([
'cache-prime' => Command\CachePrime::class,
'cache-clear' => Command\CacheClear::class,

View File

@ -1,12 +0,0 @@
{
"source": {
"directories": [
"src"
]
},
"timeout": 10,
"logs": {
"text": "build\/humbuglog.txt",
"json": "build\/humbug.json"
}
}

View File

@ -56,7 +56,7 @@
</collector>
<!-- Configuration of generation process -->
<generator output="docs">
<generator>
<!-- @output - (Base-)Directory to store output data in -->
<!-- A generation process consists of one or more build tasks and of (optional) enrich sources -->
@ -117,10 +117,10 @@
<!-- An engine and thus build node can have additional configuration child nodes, please check the documentation for the engine to find out more -->
<!-- default engine "html" -->
<build engine="html" output="html" />
<!-- <template dir="${phpDox.home}/templates/html" /> -
<build engine="html" output="apidocs">
<!-- <template dir="${phpDox.home}/templates/html" /> -->
<file extension="html" />
</build> -->
</build>
</generator>
</project>

29
public/css.js Normal file
View File

@ -0,0 +1,29 @@
/**
* Script for optimizing css
*/
const fs = require('fs');
const postcss = require('postcss');
const atImport = require('postcss-import');
const cssNext = require('postcss-cssnext');
const cssNano = require('cssnano');
const css = fs.readFileSync('css/base.css', 'utf8');
postcss()
.use(atImport())
.use(cssNext())
.use(cssNano({
autoprefixer: false,
colormin: false,
minifyFontValues: false,
options: {
sourcemap: false
}
}))
.process(css, {
from: 'css/base.css',
to: 'css/app.min.css'
}).then(result => {
fs.writeFileSync('css/app.min.css', result.css);
fs.writeFileSync('css/app.min.css.map', result.map);
});

View File

@ -1,180 +0,0 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2017 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\EasyMin;
require_once('./min.php');
/**
* Simple CSS Minifier
*/
class CSSMin extends BaseMin {
protected $cssRoot;
protected $pathFrom;
protected $pathTo;
protected $group;
protected $lastModified;
protected $requestedTime;
public function __construct(array $config, array $groups)
{
$group = $_GET['g'];
$this->cssRoot = $config['css_root'];
$this->pathFrom = $config['path_from'];
$this->pathTo = $config['path_to'];
$this->group = $groups[$group];
$this->lastModified = $this->getLastModified();
$this->send();
}
/**
* Send the CSS
*
* @return void
*/
protected function send()
{
if($this->lastModified >= $this->getIfModified() && $this->isNotDebug())
{
throw new FileNotChangedException();
}
$css = ( ! array_key_exists('debug', $_GET))
? $this->compress($this->getCss())
: $this->getCss();
$this->output($css);
}
/**
* Function for compressing the CSS as tightly as possible
*
* @param string $buffer
* @return string
*/
protected function compress($buffer)
{
//Remove CSS comments
$buffer = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $buffer);
//Remove tabs, spaces, newlines, etc.
$buffer = preg_replace('`\s+`', ' ', $buffer);
$replace = [
' )' => ')',
') ' => ')',
' }' => '}',
'} ' => '}',
' {' => '{',
'{ ' => '{',
', ' => ',',
': ' => ':',
'; ' => ';',
];
//Eradicate every last space!
$buffer = trim(strtr($buffer, $replace));
$buffer = str_replace('{ ', '{', $buffer);
$buffer = str_replace('} ', '}', $buffer);
return $buffer;
}
/**
* Get the most recent file modification date
*
* @return int
*/
protected function getLastModified()
{
$modified = [];
// Get all the css files, and concatenate them together
if(isset($this->group))
{
foreach($this->group as $file)
{
$newFile = realpath("{$this->cssRoot}{$file}");
$modified[] = filemtime($newFile);
}
}
//Add this page for last modified check
$modified[] = filemtime(__FILE__);
//Get the latest modified date
rsort($modified);
return array_shift($modified);
}
/**
* Get the css to display
*
* @return string
*/
protected function getCss()
{
$css = '';
foreach($this->group as $file)
{
$newFile = realpath("{$this->cssRoot}{$file}");
$css .= file_get_contents($newFile);
}
// Correct paths that have changed due to concatenation
// based on rules in the config file
$css = str_replace($this->pathFrom, $this->pathTo, $css);
return $css;
}
/**
* Output the CSS
*
* @return void
*/
protected function output($css)
{
$this->sendFinalOutput($css, 'text/css', $this->lastModified);
}
}
// --------------------------------------------------------------------------
// ! Start Minifying
// --------------------------------------------------------------------------
//Get config files
$config = require('../app/appConf/minify_config.php');
$groups = require($config['css_groups_file']);
if ( ! array_key_exists($_GET['g'], $groups))
{
throw new InvalidArgumentException('You must specify a css group that exists');
}
try
{
new CSSMin($config, $groups);
}
catch (FileNotChangedException $e)
{
BaseMin::send304();
}
//End of css.php

1
public/css/app.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
undefined

File diff suppressed because it is too large Load Diff

View File

@ -1,643 +0,0 @@
@import "./marx.myth.css";
:root {
--link-shadow: 1px 1px 1px #000;
--shadow: 1px 2px 1px rgba(0, 0, 0, 0.85);
--title-overlay: rgba(0, 0, 0, 0.45);
--title-overlay-fallback: #000;
--text-color: #ffffff;
--normal-padding: 0.25em 0.125em;
--link-hover-color: #7d12db;
--edit-link-hover-color: #db7d12;
--edit-link-color: #12db18;
--radius: 5px;
}
template, [hidden="hidden"], .media[hidden] {display:none}
body {margin: 0.5em;}
button {
background:rgba(255,255,255,0.65);
margin: 0;
}
table {
min-width:85%;
margin: 0 auto;
}
td {
padding:1em;
padding:1rem;
}
thead td, thead th {
padding:0.5em;
padding:0.5rem;
}
input[type=number] {
width: 4em;
}
tbody > tr:nth-child(odd) {
background: #ddd;
}
a:hover, a:active {
color: var(--link-hover-color)
}
/* -----------------------------------------------------------------------------
Utility classes
------------------------------------------------------------------------------*/
.bracketed {
color: var(--edit-link-color);
}
.bracketed, h1 a {
text-shadow: var(--link-shadow);
}
.bracketed:before {content: '[\00a0'}
.bracketed:after {content: '\00a0]'}
.bracketed:hover, .bracketed:active {
color: var(--edit-link-hover-color)
}
.grow-1 {flex-grow: 1}
.flex-wrap {flex-wrap: wrap}
.flex-no-wrap {flex-wrap: nowrap}
.flex-align-end {align-items: flex-end}
.flex-align-space-around {align-content: space-around}
.flex-justify-space-around {justify-content: space-around}
.flex-self-center {align-self:center}
.flex {display: flex}
.small-font {
font-size:1.6rem;
}
.justify {text-align:justify}
.align_center {text-align:center}
.align_left {text-align:left}
.align_right {text-align:right}
.valign_top {vertical-align:top}
.no_border {border:none}
.media-wrap {
text-align:center;
margin:0 auto;
}
.danger {
background-color: #ff4136;
border-color: #924949;
color:#fff;
}
.danger:hover, .danger:active {
background-color: #924949;
border-color: #ff4136;
color:#fff;
}
.user-btn {
border-color: var(--edit-link-color);
color: var(--edit-link-color);
text-shadow: var(--link-shadow);
padding:0 0.5em;
padding:0 0.5rem;
}
.user-btn:hover, .user-btn:active {
border-color: var(--edit-link-hover-color);
background-color: var(--edit-link-hover-color);
}
.full_width {
width: 100%;
}
/* -----------------------------------------------------------------------------
CSS loading icon
------------------------------------------------------------------------------*/
.cssload-loader {
position: relative;
left: calc(50% - 31px);
width: 62px;
height: 62px;
border-radius: 50%;
perspective: 780px;
}
.cssload-inner {
position: absolute;
width: 100%;
height: 100%;
box-sizing: border-box;
border-radius: 50%;
}
.cssload-inner.cssload-one {
left: 0%;
top: 0%;
animation: cssload-rotate-one 1.15s linear infinite;
border-bottom: 3px solid rgb(0,0,0);
}
.cssload-inner.cssload-two {
right: 0%;
top: 0%;
animation: cssload-rotate-two 1.15s linear infinite;
border-right: 3px solid rgb(0,0,0);
}
.cssload-inner.cssload-three {
right: 0%;
bottom: 0%;
animation: cssload-rotate-three 1.15s linear infinite;
border-top: 3px solid rgb(0,0,0);
}
@keyframes cssload-rotate-one {
0% {
transform: rotateX(35deg) rotateY(-45deg) rotateZ(0deg);
}
100% {
transform: rotateX(35deg) rotateY(-45deg) rotateZ(360deg);
}
}
@keyframes cssload-rotate-two {
0% {
transform: rotateX(50deg) rotateY(10deg) rotateZ(0deg);
}
100% {
transform: rotateX(50deg) rotateY(10deg) rotateZ(360deg);
}
}
@keyframes cssload-rotate-three {
0% {
transform: rotateX(35deg) rotateY(55deg) rotateZ(0deg);
}
100% {
transform: rotateX(35deg) rotateY(55deg) rotateZ(360deg);
}
}
/* -----------------------------------------------------------------------------
Table sorting and form styles
------------------------------------------------------------------------------*/
.sorting,
.sorting_asc,
.sorting_desc {
vertical-align:text-bottom;
}
.sorting::before {
content: " ↕\00a0";
}
.sorting_asc::before {
content: " ↑\00a0";
}
.sorting_desc::before {
content: " ↓\00a0";
}
.form { width:100%; }
.form thead th, .form thead tr {
background: inherit;
border:0;
}
.form tr > td:nth-child(odd) {
text-align:right;
min-width:25px;
max-width:30%;
}
.form tr > td:nth-child(even) {
text-align:left;
width:70%;
}
.invisible tbody > tr:nth-child(odd) {
background: inherit;
}
.invisible tr, .invisible td, .invisible th {
border:0;
}
/* -----------------------------------------------------------------------------
Message boxes
------------------------------------------------------------------------------*/
.message{
position:relative;
margin:0.5em auto;
padding:0.5em;
width:95%;
}
.message .close{
width:1em;
height:1em;
position:absolute;
right:0.5em;
top:0.5em;
text-align:center;
vertical-align:middle;
line-height:1em;
}
.message:hover .close:after {
content: '☒';
}
.message:hover {
cursor:pointer;
}
.message .icon{
left:0.5em;
top:0.5em;
margin-right:1em;
}
.message.error{
border:1px solid #924949;
background: #f3e6e6;
}
.message.error .icon::after {
content: '✘';
}
.message.success{
border:1px solid #1f8454;
background: #70dda9;
}
.message.success .icon::after {
content: '✔'
}
.message.info{
border:1px solid #bfbe3a;
background: #FFFFCC;
}
.message.info .icon::after {
content: '⚠';
}
/* -----------------------------------------------------------------------------
Base list styles
------------------------------------------------------------------------------*/
.media, .character, .small_character {
position:relative;
vertical-align:top;
display:inline-block;
text-align:center;
width:220px;
height:311px;
margin: var(--normal-padding);
}
.media > img,
.character > img,
.small_character > img {
width: 100%;
}
.media .edit_buttons > button {
margin:0.5em auto;
}
.name,
.media_metadata > div,
.medium_metadata > div,
.row {
text-shadow: var(--shadow);
background: var(--title-overlay-fallback);
background: var(--title-overlay);
color: var(--text-color);
padding: var(--normal-padding);
text-align:right;
}
.media_type, .age_rating {
text-align:left;
}
.media > .media_metadata {
position:absolute;
bottom:0;
right:0;
}
.media > .medium_metadata {
position:absolute;
bottom: 0;
left:0;
}
.media > .name {
position:absolute;
top: 0;
}
.small_character:hover > .name,
.character:hover > .name,
.media:hover > .name,
.media:hover > .media_metadata > div,
.media:hover > .medium_metadata > div,
.media:hover > .table .row
{
transition: .25s ease;
background:rgba(0,0,0,0.75);
}
.media:hover > button[hidden],
.media:hover > .edit_buttons[hidden]
{
transition: .25s ease;
display:block;
}
.small_character > .name a,
.small_character > .name a small,
.character > .name a,
.character > .name a small,
.media > .name a,
.media > .name a small
{
background:none;
color:#fff;
text-shadow: var(--shadow);
}
/* -----------------------------------------------------------------------------
Anime-list-specific styles
------------------------------------------------------------------------------*/
.anime .name, .manga .name {
text-align:center;
width:100%;
padding:0.5em 0.25em;
}
.anime .media_type,
.anime .airing_status,
.anime .user_rating,
.anime .completion,
.anime .age_rating,
.anime .edit,
.anime .delete {
background: none;
text-align:center;
}
.anime .table, .manga .table {
position:absolute;
bottom:0;
left:0;
width:100%;
}
.anime .row, .manga .row {
width:100%;
background: var(--title-overlay-fallback);
background: var(--title-overlay);
display: flex;
align-content: space-around;
justify-content: space-around;
text-align:center;
padding:0 inherit;
}
.anime .row > span, .manga .row > span {
text-align:left;
}
.anime .row > div, .manga .row > div {
font-size:0.8em;
display:flex-item;
align-self:center;
text-align:center;
vertical-align:middle;
}
.anime .media > button.plus_one {
position:absolute;
top: 138px;
top: calc(50% - 21.5px);
left: 44px;
left: calc(50% - 66.5px);
}
/* -----------------------------------------------------------------------------
Manga-list-specific styles
------------------------------------------------------------------------------*/
.manga .row {
padding:1px;
}
.manga .media {
border:1px solid #ddd;
height:310px;
margin:0.25em;
}
.manga .media > .edit_buttons {
position:absolute;
top: 86px;
top: calc(50% - 58.5px);
left: 43.5px;
left: calc(50% - 66.5px);
}
/* -----------------------------------------------------------------------------
Search page styles
------------------------------------------------------------------------------*/
.media.search > .name {
background-color:#555;
background-color: rgba(000,000,000,0.35);
background-size: cover;
background-size: contain;
background-repeat: no-repeat;
}
.big-check {
display:none;
}
.big-check:checked + label {
transition: .25s ease;
background:rgba(0,0,0,0.75);
}
.big-check:checked + label:after {
content: '✓';
font-size: 15em;
font-size: 15rem;
text-align:center;
color: greenyellow;
position:absolute;
top:147px;
left:0;
height:100%;
width:100%;
}
#series_list article.media {
position:relative;
}
#series_list .name, #series_list .name label {
position:absolute;
display:block;
top:0;
left:0;
height:100%;
width:100%;
vertical-align:middle;
line-height: 1.25em;
}
#series_list .name small {
color: #fff;
}
/* ----------------------------------------------------------------------------
Details page styles
-----------------------------------------------------------------------------*/
.details {
margin: 1.5rem auto 0 auto;
padding:1rem;
font-size:inherit;
}
.details.fixed {
max-width:93rem;
}
.details .cover {
display: block;
width: 284px;
/* height: 402px; */
}
.details h2 {
margin-top: 0;
}
.details .flex > div {
margin: 1rem;
}
.details .media_details {
max-width:300px;
}
.details .media_details td {
padding:0 1.5rem;
}
.details p {
text-align:justify;
}
.details .media_details td:nth-child(odd) {
width:1%;
white-space:nowrap;
text-align:right;
}
.details .media_details td:nth-child(even) {
text-align:left;
}
.character,
.small_character {
background: rgba(0,0,0,0.5);
width: 225px;
height: 350px;
vertical-align: middle;
white-space: nowrap;
}
.small_character a {
display:inline-block;
width: 100%;
height: 100%;
}
.small_character .name,
.character .name {
position: absolute;
bottom: 0;
left: 0;
z-index: 10;
}
.small_character img,
.character img {
position: relative;
top: 50%;
transform: translateY(-50%);
z-index: 5;
width: 100%;
}
/* ----------------------------------------------------------------------------
User page styles
-----------------------------------------------------------------------------*/
.small_character {
width: 160px;
height: 250px;
}
.user-page .media-wrap {
text-align: left;
}
.media a {
display: inline-block;
width: 100%;
height: 100%;
}
/* ----------------------------------------------------------------------------
Viewport-based styles
-----------------------------------------------------------------------------*/
@media screen and (max-width: 40em) {
nav a {
line-height:4em;
line-height:4rem;
}
.media {
margin:2px 0;
}
main {
padding:0 0,5em 0.5em;
padding:0 0.5rem 0.5rem;
}
}
/* ----------------------------------------------------------------------------
Images / Logos
-----------------------------------------------------------------------------*/
.streaming-logo {
width: 50px;
height: 50px;
vertical-align:middle;
}
.cover_streaming_link .streaming-logo {
width: 20px;
height: 20px;
}

View File

@ -1,6 +1,7 @@
:root
{
--default-font-list:'Open Sans', 'Nimbus Sans L', 'Helvetica Neue', Helvetica, 'Lucida Grande', sans-serif;
:root {
--default-font-list: system-ui,sans-serif;
--monospace-font-list:'Anonymous Pro','Fira Code',Menlo,Monaco,Consolas,'Courier New',monospace;
--serif-font-list:Georgia,Times,'Times New Roman',serif;
-ms-text-size-adjust:100%;
-webkit-text-size-adjust:100%;
box-sizing:border-box;
@ -9,48 +10,41 @@
line-height:1.4;
overflow-y:scroll;
text-size-adjust:100%;
scroll-behavior: smooth;
scroll-behavior:smooth;
}
audio:not([controls])
{
audio:not([controls]) {
display:none;
}
details
{
details {
display:block;
}
input[type=search]
{
input[type=search] {
-webkit-appearance:textfield;
}
input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration
{
input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration {
-webkit-appearance:none;
}
main
{
main {
display:block;
margin:0 auto;
padding:0 1.6em 1.6em;
padding:0 1.6rem 1.6rem;
}
summary
{
summary {
display:block;
}
pre
{
pre {
background:#efefef;
color:#444;
display:block;
font-family:Menlo, Monaco, Consolas, 'Courier New', monospace;
font-family:var(--monospace-font-list);
font-size:1.4em;
font-size:1.4rem;
margin:1.6em 0;
@ -62,29 +56,24 @@ pre
word-wrap:break-word;
}
progress
{
progress {
display:inline-block;
}
small
{
small {
color:#777;
font-size:75%;
}
big
{
big {
font-size:125%;
}
template
{
template {
display:none;
}
textarea
{
textarea {
border:.1rem solid #ccc;
border-radius:0;
display:block;
@ -95,55 +84,47 @@ textarea
vertical-align:middle;
}
[hidden]
{
[hidden] {
display:none;
}
[unselectable]
{
[unselectable] {
-moz-user-select:none;
-ms-user-select:none;
-webkit-user-select:none;
user-select:none;
}
*,::before,::after
{
*,::before,::after {
border-style:solid;
border-width:0;
box-sizing:inherit;
}
*
{
* {
font-size:inherit;
line-height:inherit;
margin:0;
padding:0;
}
::before,::after
{
::before,::after {
text-decoration:inherit;
vertical-align:inherit;
}
a
{
a {
-webkit-transition:.25s ease;
color:#1271db;
text-decoration:none;
transition:.25s ease;
}
audio,canvas,iframe,img,svg,video
{
audio,canvas,iframe,img,svg,video {
vertical-align:middle;
}
button,input,select,textarea
{
button,input,select,textarea {
border:.1rem solid #ccc;
color:inherit;
font-family:inherit;
@ -152,37 +133,31 @@ button,input,select,textarea
min-height:1.4em;
}
code,kbd,pre,samp
{
font-family:Menlo, Monaco, Consolas, 'Courier New', monospace, monospace;
code,kbd,pre,samp {
font-family:var(--monospace-font-list);
}
table
{
table {
border-collapse:collapse;
border-spacing:0;
margin-bottom:1.6rem;
}
::-moz-selection
{
::-moz-selection {
background-color:#b3d4fc;
text-shadow:none;
}
::selection
{
::selection {
background-color:#b3d4fc;
text-shadow:none;
}
button::-moz-focus-inner
{
button::-moz-focus-inner {
border:0;
}
body
{
body {
color:#444;
font-family:var(--default-font-list);
font-size:1.6rem;
@ -191,20 +166,17 @@ body
padding:0;
}
p
{
p {
margin:0 0 1.6rem;
}
h1,h2,h3,h4,h5,h6
{
font-family:Lato, var(--default-font-list);
h1,h2,h3,h4,h5,h6 {
font-family:var(--default-font-list);
margin:2em 0 1.6em;
margin:2rem 0 1.6rem;
}
h1
{
h1 {
border-bottom:.1rem solid rgba(0,0,0,0.2);
font-size:3.6em;
font-size:3.6rem;
@ -212,16 +184,14 @@ h1
font-weight:500;
}
h2
{
h2 {
font-size:3em;
font-size:3rem;
font-style:normal;
font-weight:500;
}
h3
{
h3 {
font-size:2.4em;
font-size:2.4rem;
font-style:normal;
@ -229,8 +199,7 @@ h3
margin:1.6rem 0 .4rem;
}
h4
{
h4 {
font-size:1.8em;
font-size:1.8rem;
font-style:normal;
@ -238,8 +207,7 @@ h4
margin:1.6rem 0 .4rem;
}
h5
{
h5 {
font-size:1.6em;
font-size:1.6rem;
font-style:normal;
@ -247,8 +215,7 @@ h5
margin:1.6rem 0 .4rem;
}
h6
{
h6 {
color:#777;
font-size:1.4em;
font-size:1.4rem;
@ -257,66 +224,56 @@ h6
margin:1.6rem 0 .4rem;
}
code
{
code {
background:#efefef;
color:#444;
font-family:Menlo, Monaco, Consolas, 'Courier New', monospace;
font-family:var(--monospace-font-list);
font-size:1.4rem;
word-break:break-all;
word-wrap:break-word;
}
a:hover,a:focus
{
a:hover,a:focus {
text-decoration:none;
}
dl
{
dl {
margin-bottom:1.6rem;
}
dd
{
dd {
margin-left:4rem;
}
ul,ol
{
ul,ol {
margin-bottom:.8rem;
padding-left:2rem;
}
blockquote
{
blockquote {
border-left:.2rem solid #1271db;
font-family:Georgia, Times, 'Times New Roman', serif;
font-family:var(--serif-font-list);
font-style:italic;
margin:1.6rem 0;
padding-left:1.6rem;
}
figcaption
{
font-family:Georgia, Times, 'Times New Roman', serif;
figcaption {
font-family:var(--serif-font-list);
}
html
{
html {
font-size:62.5%;
}
main,header,footer,article,section,aside,details,summary
{
main,header,footer,article,section,aside,details,summary {
display:block;
height:auto;
margin:0 auto;
width:100%;
}
footer
{
footer {
border-top:.1rem solid rgba(0,0,0,0.2);
clear:both;
display:inline-block;
@ -326,23 +283,20 @@ footer
text-align:center;
}
hr
{
hr {
border-top:.1rem solid rgba(0,0,0,0.2);
display:block;
margin-bottom:1.6rem;
width:100%;
}
img
{
img {
height:auto;
/* max-width:100%; */
/* max-width:100%; */
vertical-align:baseline;
}
input[type=text],input[type=password],input[type=email],input[type=url],input[type=date],input[type=month],input[type=time],input[type=datetime],input[type=datetime-local],input[type=week],input[type=number],input[type=search],input[type=tel],input[type=color],select
{
input[type=text],input[type=password],input[type=email],input[type=url],input[type=date],input[type=month],input[type=time],input[type=datetime],input[type=datetime-local],input[type=week],input[type=number],input[type=search],input[type=tel],input[type=color],select {
border:.1rem solid #ccc;
border-radius:0;
display:inline-block;
@ -350,8 +304,7 @@ input[type=text],input[type=password],input[type=email],input[type=url],input[ty
vertical-align:middle;
}
input:not([type])
{
input:not([type]) {
-webkit-appearance:none;
background-clip:padding-box;
background-color:#fff;
@ -363,88 +316,73 @@ input:not([type])
text-align:left;
}
input[type=color]
{
input[type=color] {
padding:.8rem 1.6rem;
}
input[type=text]:focus,input[type=password]:focus,input[type=email]:focus,input[type=url]:focus,input[type=date]:focus,input[type=month]:focus,input[type=time]:focus,input[type=datetime]:focus,input[type=datetime-local]:focus,input[type=week]:focus,input[type=number]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=color]:focus,select:focus,textarea:focus
{
input[type=text]:focus,input[type=password]:focus,input[type=email]:focus,input[type=url]:focus,input[type=date]:focus,input[type=month]:focus,input[type=time]:focus,input[type=datetime]:focus,input[type=datetime-local]:focus,input[type=week]:focus,input[type=number]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=color]:focus,select:focus,textarea:focus {
border-color:#b3d4fc;
}
input:not([type]):focus
{
input:not([type]):focus {
border-color:#b3d4fc;
}
input[type=radio],input[type=checkbox]
{
input[type=radio],input[type=checkbox] {
vertical-align:middle;
}
input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus
{
input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus {
outline:.1rem solid thin #444;
}
input[type=text][disabled],input[type=password][disabled],input[type=email][disabled],input[type=url][disabled],input[type=date][disabled],input[type=month][disabled],input[type=time][disabled],input[type=datetime][disabled],input[type=datetime-local][disabled],input[type=week][disabled],input[type=number][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=color][disabled],select[disabled],textarea[disabled]
{
input[type=text][disabled],input[type=password][disabled],input[type=email][disabled],input[type=url][disabled],input[type=date][disabled],input[type=month][disabled],input[type=time][disabled],input[type=datetime][disabled],input[type=datetime-local][disabled],input[type=week][disabled],input[type=number][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=color][disabled],select[disabled],textarea[disabled] {
background-color:#efefef;
color:#777;
cursor:not-allowed;
}
input:not([type])[disabled]
{
input:not([type])[disabled] {
background-color:#efefef;
color:#777;
cursor:not-allowed;
}
input[readonly],select[readonly],textarea[readonly]
{
input[readonly],select[readonly],textarea[readonly] {
background-color:#efefef;
border-color:#ccc;
color:#777;
}
input:focus:invalid,textarea:focus:invalid,select:focus:invalid
{
input:focus:invalid,textarea:focus:invalid,select:focus:invalid {
border-color:#e9322d;
color:#b94a48;
}
input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus,input[type=checkbox]:focus:invalid:focus
{
input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus,input[type=checkbox]:focus:invalid:focus {
outline-color:#ff4136;
}
select
{
select {
background-color:#fff;
border:.1rem solid #ccc;
}
select[multiple]
{
select[multiple] {
height:auto;
}
label
{
label {
line-height:2;
}
fieldset
{
fieldset {
border:0;
margin:0;
padding:.8rem 0;
}
legend
{
legend {
border-bottom:.1rem solid #ccc;
color:#444;
display:block;
@ -453,8 +391,7 @@ legend
width:100%;
}
input[type=submit],button
{
input[type=submit],button {
-moz-user-select:none;
-ms-user-select:none;
-webkit-transition:.25s ease;
@ -476,62 +413,52 @@ input[type=submit],button
vertical-align:baseline;
}
input[type=submit] a,button a
{
input[type=submit] a,button a {
color:#444;
}
input[type=submit]::-moz-focus-inner,button::-moz-focus-inner
{
input[type=submit]::-moz-focus-inner,button::-moz-focus-inner {
padding:0;
}
input[type=submit]:hover,button:hover
{
input[type=submit]:hover,button:hover {
background:#444;
border-color:#444;
color:#fff;
}
input[type=submit]:hover a,button:hover a
{
input[type=submit]:hover a,button:hover a {
color:#fff;
}
input[type=submit]:active,button:active
{
input[type=submit]:active,button:active {
background:#6a6a6a;
border-color:#6a6a6a;
color:#fff;
}
input[type=submit]:active a,button:active a
{
input[type=submit]:active a,button:active a {
color:#fff;
}
input[type=submit]:disabled,button:disabled
{
input[type=submit]:disabled,button:disabled {
box-shadow:none;
cursor:not-allowed;
opacity:.40;
opacity:.4;
}
nav ul
{
nav ul {
list-style:none;
margin:0;
padding:0;
text-align:center;
}
nav ul li
{
nav ul li {
display:inline;
}
nav a
{
nav a {
-webkit-transition:.25s ease;
border-bottom:.2rem solid transparent;
color:#444;
@ -540,48 +467,40 @@ nav a
transition:.25s ease;
}
nav a:hover,nav li.selected a
{
nav a:hover,nav li.selected a {
border-color:rgba(0,0,0,0.2);
}
nav a:active
{
nav a:active {
border-color:rgba(0,0,0,0.56);
}
caption
{
caption {
padding:.8rem 0;
}
thead th
{
thead th {
background:#efefef;
color:#444;
}
tr
{
tr {
background:#fff;
margin-bottom:.8rem;
}
th,td
{
th,td {
border:.1rem solid #ccc;
padding:.8rem 1.6rem;
text-align:center;
vertical-align:inherit;
}
tfoot tr
{
tfoot tr {
background:none;
}
tfoot td
{
tfoot td {
color:#efefef;
font-size:.8rem;
font-style:italic;
@ -589,28 +508,24 @@ tfoot td
}
@media screen {
[hidden~=screen]
{
[hidden~=screen] {
display:inherit;
}
[hidden~=screen]:not(:active):not(:focus):not(:target)
{
[hidden~=screen]:not(:active):not(:focus):not(:target) {
clip:rect(0000)!important;
position:absolute!important;
}
}
@media screen and max-width 40rem {
article,section,aside
{
article,section,aside {
clear:both;
display:block;
max-width:100%;
}
img
{
img {
margin-right:1.6rem;
}
}

3
public/cssfilter.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = function filter(filename) {
return ! String(filename).includes('min');
}

View File

View File

@ -22,29 +22,48 @@ use Aviat\Ion\Json;
// Include guzzle
require_once('../vendor/autoload.php');
require_once('./min.php');
//Creative rewriting of /g/groupname to ?g=groupname
$pi = $_SERVER['PATH_INFO'];
$pia = explode('/', $pi);
$piaLen = count($pia);
$i = 1;
while($i < $piaLen)
{
$j = $i+1;
$j = (isset($pia[$j])) ? $j : $i;
$_GET[$pia[$i]] = $pia[$j];
$i = $j + 1;
};
class FileNotChangedException extends \Exception {}
/**
* Simple Javascript minfier, using google closure compiler
*/
class JSMin extends BaseMin {
class JSMin {
protected $jsRoot;
protected $jsGroup;
protected $jsGroupsFile;
protected $configFile;
protected $cacheFile;
protected $lastModified;
protected $requestedTime;
protected $cacheModified;
public function __construct(array $config, array $groups)
public function __construct(array $config, string $configFile)
{
$group = $_GET['g'];
$groups = $config['groups'];
$this->jsRoot = $config['js_root'];
$this->jsGroup = $groups[$group];
$this->jsGroupsFile = $config['js_groups_file'];
$this->configFile = $configFile;
$this->cacheFile = "{$this->jsRoot}cache/{$group}";
$this->lastModified = $this->getLastModified();
@ -178,7 +197,7 @@ class JSMin extends BaseMin {
//Add this page too, as well as the groups file
$modified[] = filemtime(__FILE__);
$modified[] = filemtime($this->jsGroupsFile);
$modified[] = filemtime($this->configFile);
rsort($modified);
$lastModified = $modified[0];
@ -227,14 +246,97 @@ class JSMin extends BaseMin {
{
$this->sendFinalOutput($js, 'application/javascript', $this->lastModified);
}
/**
* Get value of the if-modified-since header
*
* @return int - timestamp to compare for cache control
*/
protected function getIfModified()
{
return (array_key_exists('HTTP_IF_MODIFIED_SINCE', $_SERVER))
? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])
: time();
}
/**
* Get value of etag to compare to hash of output
*
* @return string - the etag to compare
*/
protected function getIfNoneMatch()
{
return (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER))
? $_SERVER['HTTP_IF_NONE_MATCH']
: '';
}
/**
* Determine whether or not to send debug version
*
* @return boolean
*/
protected function isNotDebug()
{
return ! $this->isDebugCall();
}
/**
* Determine whether or not to send debug version
*
* @return boolean
*/
protected function isDebugCall()
{
return array_key_exists('debug', $_GET);
}
/**
* Send actual output to browser
*
* @param string $content - the body of the response
* @param string $mimeType - the content type
* @param int $lastModified - the last modified date
* @return void
*/
protected function sendFinalOutput($content, $mimeType, $lastModified)
{
//This GZIPs the CSS for transmission to the user
//making file size smaller and transfer rate quicker
ob_start("ob_gzhandler");
$expires = $lastModified + 691200;
$lastModifiedDate = gmdate('D, d M Y H:i:s', $lastModified);
$expiresDate = gmdate('D, d M Y H:i:s', $expires);
header("Content-Type: {$mimeType}; charset=utf8");
header("Cache-control: public, max-age=691200, must-revalidate");
header("Last-Modified: {$lastModifiedDate} GMT");
header("Expires: {$expiresDate} GMT");
echo $content;
ob_end_flush();
}
/**
* Send a 304 Not Modified header
*
* @return void
*/
public static function send304()
{
header("status: 304 Not Modified", true, 304);
}
}
// --------------------------------------------------------------------------
// ! Start Minifying
// --------------------------------------------------------------------------
$config = require_once('../app/appConf/minify_config.php');
$groups = require_once($config['js_groups_file']);
$configFile = realpath(__DIR__ . '/../app/appConf/minify_config.php');
$config = require_once($configFile);
$groups = $config['groups'];
$cacheDir = "{$config['js_root']}cache";
if ( ! is_dir($cacheDir))
@ -249,11 +351,11 @@ if ( ! array_key_exists($_GET['g'], $groups))
try
{
new JSMin($config, $groups);
new JSMin($config, $configFile);
}
catch (FileNotChangedException $e)
{
BaseMin::send304();
JSMin::send304();
}
//end of js.php

View File

@ -8,7 +8,7 @@
// Action to increment episode count
_.on('body.anime.list', 'click', '.plus_one', (e) => {
let parentSel = _.closestParent(e.target, 'article');
let watchedCount = parseInt(_.$('.completed_number', parentSel)[0].textContent, 10);
let watchedCount = parseInt(_.$('.completed_number', parentSel)[0].textContent, 10) || 0;
let totalCount = parseInt(_.$('.total_number', parentSel)[0].textContent, 10);
let title = _.$('.name a', parentSel)[0].textContent;

View File

@ -9,7 +9,7 @@
let thisSel = e.target;
let parentSel = _.closestParent(e.target, 'article');
let type = thisSel.classList.contains('plus_one_chapter') ? 'chapter' : 'volume';
let completed = parseInt(_.$(`.${type}s_read`, parentSel)[0].textContent, 10);
let completed = parseInt(_.$(`.${type}s_read`, parentSel)[0].textContent, 10) || 0;
let total = parseInt(_.$(`.${type}_count`, parentSel)[0].textContent, 10);
let mangaName = _.$('.name', parentSel)[0].textContent;

View File

@ -1,121 +0,0 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2017 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\EasyMin;
//Creative rewriting of /g/groupname to ?g=groupname
$pi = $_SERVER['PATH_INFO'];
$pia = explode('/', $pi);
$piaLen = count($pia);
$i = 1;
while($i < $piaLen)
{
$j = $i+1;
$j = (isset($pia[$j])) ? $j : $i;
$_GET[$pia[$i]] = $pia[$j];
$i = $j + 1;
};
class FileNotChangedException extends \Exception {}
class BaseMin {
/**
* Get value of the if-modified-since header
*
* @return int - timestamp to compare for cache control
*/
protected function getIfModified()
{
return (array_key_exists('HTTP_IF_MODIFIED_SINCE', $_SERVER))
? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])
: time();
}
/**
* Get value of etag to compare to hash of output
*
* @return string - the etag to compare
*/
protected function getIfNoneMatch()
{
return (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER))
? $_SERVER['HTTP_IF_NONE_MATCH']
: '';
}
/**
* Determine whether or not to send debug version
*
* @return boolean
*/
protected function isNotDebug()
{
return ! $this->isDebugCall();
}
/**
* Determine whether or not to send debug version
*
* @return boolean
*/
protected function isDebugCall()
{
return array_key_exists('debug', $_GET);
}
/**
* Send actual output to browser
*
* @param string $content - the body of the response
* @param string $mimeType - the content type
* @param int $lastModified - the last modified date
* @return void
*/
protected function sendFinalOutput($content, $mimeType, $lastModified)
{
//This GZIPs the CSS for transmission to the user
//making file size smaller and transfer rate quicker
ob_start("ob_gzhandler");
$expires = $lastModified + 691200;
$lastModifiedDate = gmdate('D, d M Y H:i:s', $lastModified);
$expiresDate = gmdate('D, d M Y H:i:s', $expires);
header("Content-Type: {$mimeType}; charset=utf8");
header("Cache-control: public, max-age=691200, must-revalidate");
header("Last-Modified: {$lastModifiedDate} GMT");
header("Expires: {$expiresDate} GMT");
echo $content;
ob_end_flush();
}
/**
* Send a 304 Not Modified header
*
* @return void
*/
public static function send304()
{
header("status: 304 Not Modified", true, 304);
}
}

View File

@ -1,14 +1,13 @@
{
"scripts": {
"build": "postcss -u postcss-import --autoprefixer.browsers \"> 5%\" -u postcss-cssnext -o css/base.css css/base.myth.css",
"watch": "postcss -u postcss-import --autoprefixer.browsers \"> 5%\" -u postcss-cssnext -w -o css/base.css css/base.myth.css"
"build": "node ./css.js",
"watch": "watch 'npm run build' --filter=./cssfilter.js"
},
"devDependencies": {
"autoprefixer": "^6.6.1",
"npm-run-all": "^4.0.0",
"cssnano": "^3.10.0",
"postcss-cachify": "^1.3.1",
"postcss-cli": "^2.6.0",
"postcss-cssnext": "^2.9.0",
"postcss-import": "^9.0.0"
"postcss-import": "^9.0.0",
"watch": "^1.0.2"
}
}

View File

@ -1,8 +1,9 @@
{{#data}}
<article class="media search">
<div class="name" style="background-image:url({{attributes.posterImage.small}})">
<div class="name">
<input type="radio" class="big-check" id="{{attributes.slug}}" name="id" value="{{id}}" />
<label for="{{attributes.slug}}">
<img src="/public/images/anime/{{id}}.jpg" alt="" width="220" />
<span class="name">
{{attributes.canonicalTitle}}
<br />

View File

@ -1,8 +1,9 @@
{{#data}}
<article class="media search">
<div class="name" style="background-image:url({{attributes.posterImage.small}})">
<div class="name">
<input type="radio" class="big-check" id="{{attributes.slug}}" name="id" value="{{id}}" />
<label for="{{attributes.slug}}">
<img src="/public/images/manga/{{id}}.jpg" alt="" width="220" />
<span class="name">
{{attributes.canonicalTitle}}
<br />

File diff suppressed because it is too large Load Diff

View File

@ -54,7 +54,9 @@ class JsonAPI {
];
// Reorganize included data
$included = static::organizeIncluded($data['included']);
$included = (array_key_exists('included', $data))
? static::organizeIncluded($data['included'])
: [];
// Inline organized data
foreach($data['data'] as $i => &$item)
@ -125,23 +127,13 @@ class JsonAPI {
$typeKey = $props['data'][$j]['type'];
$relationship =& $item['relationships'][$relType];
unset($relationship['data'][$j]);
if (empty($relationship['data']))
{
unset($relationship['data']);
}
if ($relType === $typeKey)
{
$relationship[$idKey] = $included[$typeKey][$idKey];
continue;
}
$relationship[$typeKey][$idKey] = array_merge(
$included[$typeKey][$idKey],
$relationship[$typeKey][$idKey] ?? []
);
$relationship[$typeKey][$idKey][$j] = $included[$typeKey][$idKey];
}
}
}
@ -149,6 +141,8 @@ class JsonAPI {
}
}
$data['data']['included'] = $included;
return $data['data'];
}
@ -202,11 +196,11 @@ class JsonAPI {
{
foreach($items as $id => $item)
{
if (array_key_exists('relationships', $item))
if (array_key_exists('relationships', $item) && is_array($item['relationships']))
{
foreach($item['relationships'] as $relType => $props)
{
if (array_key_exists('data', $props))
if (array_key_exists('data', $props) && is_array($props['data']) && array_key_exists('id', $props['data']))
{
if (array_key_exists($props['data']['id'], $organized[$props['data']['type']]))
{

View File

@ -219,11 +219,11 @@ class Kitsu {
foreach($existingTitles as $existing)
{
$isSubset = stripos($existing, $title) !== FALSE;
$isSubset = mb_substr_count($existing, $title) > 0;
$diff = levenshtein($existing, $title);
$onlydifferentCase = (mb_strtolower($existing) === mb_strtolower($title));
if ($diff < 3 OR $isSubset OR $onlydifferentCase)
if ($diff <= 3 OR $isSubset OR $onlydifferentCase OR mb_strlen($title) > 55)
{
return FALSE;
}

View File

@ -22,7 +22,7 @@ use function Amp\wait;
use Amp\Artax\{Client, Request};
use Aviat\AnimeClient\AnimeClient;
use Aviat\AnimeClient\API\Kitsu as K;
use Aviat\AnimeClient\API\{FailedResponseException, Kitsu as K};
use Aviat\Ion\Json;
trait KitsuTrait {
@ -142,8 +142,10 @@ trait KitsuTrait {
{
if ($logger)
{
$logger->warning('Non 200 response for api call', (array)$response->getBody());
$logger->warning('Non 200 response for api call', (array)$response);
}
throw new FailedResponseException('Failed to get the proper response from the API');
}
return Json::decode($response->getBody(), TRUE);

View File

@ -47,7 +47,7 @@ class Model {
use ContainerAware;
use KitsuTrait;
const FULL_TRANSFORMED_LIST_CACHE_KEY = 'kitsu-full-organized-anime-list';
const LIST_PAGE_SIZE = 100;
/**
* Class to map anime list items
@ -160,13 +160,16 @@ class Model {
*/
public function getCharacter(string $slug): array
{
// @todo catch non-existent characters and show 404
$data = $this->getRequest('/characters', [
'query' => [
'filter' => [
'name' => $slug
'slug' => $slug,
],
// 'include' => 'primaryMedia,castings'
'fields' => [
'anime' => 'canonicalTitle,titles,slug,posterImage',
'manga' => 'canonicalTitle,titles,slug,posterImage'
],
'include' => 'castings.person,castings.media'
]
]);
@ -235,9 +238,9 @@ class Model {
*
* @param string $malId
* @param string $type "anime" or "manga"
* @return string
* @return string|NULL
*/
public function getKitsuIdFromMALId(string $malId, string $type="anime"): string
public function getKitsuIdFromMALId(string $malId, string $type="anime")
{
$options = [
'query' => [
@ -254,6 +257,11 @@ class Model {
$raw = $this->getRequest('mappings', $options);
if ( ! array_key_exists('included', $raw))
{
return NULL;
}
return $raw['included'][0]['id'];
}
@ -277,7 +285,7 @@ class Model {
}
$transformed = $this->animeTransformer->transform($baseData);
$transformed['included'] = $baseData['included'];
$transformed['included'] = JsonAPI::organizeIncluded($baseData['included']);
return $transformed;
}
@ -374,7 +382,7 @@ class Model {
{
$status = $options['filter']['status'] ?? '';
$count = $this->getAnimeListCount($status);
$size = 100;
$size = static::LIST_PAGE_SIZE;
$pages = ceil($count / $size);
$requester = new ParallelAPIRequest();
@ -460,7 +468,7 @@ class Model {
* @param array $options
* @return Request
*/
public function getPagedAnimeList(int $limit = 100, int $offset = 0, array $options = [
public function getPagedAnimeList(int $limit, int $offset = 0, array $options = [
'include' => 'anime.mappings'
]): Request
{
@ -618,7 +626,7 @@ class Model {
{
$status = $options['filter']['status'] ?? '';
$count = $this->getMangaListCount($status);
$size = 100;
$size = static::LIST_PAGE_SIZE;
$pages = ceil($count / $size);
$requester = new ParallelAPIRequest();
@ -668,7 +676,7 @@ class Model {
* @param array $options
* @return Request
*/
public function getPagedMangaList(int $limit = 100, int $offset = 0, array $options = [
public function getPagedMangaList(int $limit, int $offset = 0, array $options = [
'include' => 'manga.mappings'
]): Request
{
@ -821,6 +829,7 @@ class Model {
}
$baseData = $data['data']['attributes'];
$baseData['id'] = $data['id'];
$baseData['included'] = $data['included'];
return $baseData;
}
@ -856,6 +865,7 @@ class Model {
}
$baseData = $data['data'][0]['attributes'];
$baseData['id'] = $data['data'][0]['id'];
$baseData['included'] = $data['included'];
return $baseData;
}

View File

@ -40,7 +40,9 @@ class AnimeListTransformer extends AbstractTransformer {
$genres = array_column($anime['relationships']['genres'], 'name') ?? [];
sort($genres);
$rating = (int) 2 * $item['attributes']['rating'];
$rating = (int) $item['attributes']['rating'] !== 0
? (int) 2 * $item['attributes']['rating']
: '-';
$total_episodes = array_key_exists('episodeCount', $anime) && (int) $anime['episodeCount'] !== 0
? (int) $anime['episodeCount']
@ -68,7 +70,7 @@ class AnimeListTransformer extends AbstractTransformer {
'id' => $item['id'],
'mal_id' => $MALid,
'episodes' => [
'watched' => (int) $item['attributes']['progress'] !== '0'
'watched' => (int) $item['attributes']['progress'] !== 0
? (int) $item['attributes']['progress']
: '-',
'total' => $total_episodes,
@ -80,6 +82,7 @@ class AnimeListTransformer extends AbstractTransformer {
'ended' => $anime['endDate']
],
'anime' => [
'id' => $animeId,
'age_rating' => $anime['ageRating'],
'title' => $anime['canonicalTitle'],
'titles' => Kitsu::filterTitles($anime),
@ -93,7 +96,7 @@ class AnimeListTransformer extends AbstractTransformer {
'notes' => $item['attributes']['notes'],
'rewatching' => (bool) $item['attributes']['reconsuming'],
'rewatched' => (int) $item['attributes']['reconsumeCount'],
'user_rating' => ($rating === 0) ? '-' : (int) $rating,
'user_rating' => $rating,
'private' => (bool) $item['attributes']['private'] ?? FALSE,
];
}
@ -118,12 +121,16 @@ class AnimeListTransformer extends AbstractTransformer {
'reconsuming' => $rewatching,
'reconsumeCount' => $item['rewatched'],
'notes' => $item['notes'],
'progress' => $item['episodes_watched'],
'private' => $privacy
]
];
if (is_numeric($item['user_rating']))
if (is_numeric($item['episodes_watched']) && $item['episodes_watched'] > 0)
{
$untransformed['data']['progress'] = (int) $item['episodes_watched'];
}
if (is_numeric($item['user_rating']) && $item['user_rating'] > 0)
{
$untransformed['data']['rating'] = $item['user_rating'] / 2;
}

View File

@ -40,6 +40,7 @@ class AnimeTransformer extends AbstractTransformer {
$titles = Kitsu::filterTitles($item);
return [
'id' => $item['id'],
'slug' => $item['slug'],
'title' => $titles[0],
'titles' => $titles,

View File

@ -42,18 +42,22 @@ class MangaListTransformer extends AbstractTransformer {
$genres = array_column($manga['relationships']['genres'], 'name') ?? [];
sort($genres);
$rating = (is_numeric($item['attributes']['rating']))
? intval(2 * $item['attributes']['rating'])
$rating = (int) $item['attributes']['rating'] !== 0
? (int) 2 * $item['attributes']['rating']
: '-';
$totalChapters = ($manga['chapterCount'] > 0)
$totalChapters = ((int) $manga['chapterCount'] !== 0)
? $manga['chapterCount']
: '-';
$totalVolumes = ($manga['volumeCount'] > 0)
$totalVolumes = ((int) $manga['volumeCount'] !== 0)
? $manga['volumeCount']
: '-';
$readChapters = ((int) $item['attributes']['progress'] !== 0)
? $item['attributes']['progress']
: '-';
$MALid = NULL;
if (array_key_exists('mappings', $manga['relationships']))
@ -72,7 +76,7 @@ class MangaListTransformer extends AbstractTransformer {
'id' => $item['id'],
'mal_id' => $MALid,
'chapters' => [
'read' => $item['attributes']['progress'],
'read' => $readChapters,
'total' => $totalChapters
],
'volumes' => [
@ -80,6 +84,7 @@ class MangaListTransformer extends AbstractTransformer {
'total' => $totalVolumes
],
'manga' => [
'id' => $mangaId,
'titles' => Kitsu::filterTitles($manga),
'alternate_title' => NULL,
'slug' => $manga['slug'],
@ -113,14 +118,18 @@ class MangaListTransformer extends AbstractTransformer {
'mal_id' => $item['mal_id'],
'data' => [
'status' => $item['status'],
'progress' => (int)$item['chapters_read'],
'reconsuming' => $rereading,
'reconsumeCount' => (int)$item['reread_count'],
'notes' => $item['notes'],
],
];
if (is_numeric($item['new_rating']))
if (is_numeric($item['chapters_read']) && $item['chapters_read'] > 0)
{
$map['data']['progress'] = (int)$item['chapters_read'];
}
if (is_numeric($item['new_rating']) && $item['new_rating'] > 0)
{
$map['data']['rating'] = $item['new_rating'] / 2;
}

View File

@ -45,6 +45,7 @@ class MangaTransformer extends AbstractTransformer {
sort($genres);
return [
'id' => $item['id'],
'title' => $item['canonicalTitle'],
'en_title' => $item['titles']['en'],
'jp_title' => $item['titles']['en_jp'],

View File

@ -44,9 +44,7 @@ class AnimeListTransformer extends AbstractTransformer {
{
$map = [
'id' => $item['mal_id'],
'data' => [
'episode' => $item['data']['progress']
]
'data' => []
];
$data =& $item['data'];
@ -55,6 +53,10 @@ class AnimeListTransformer extends AbstractTransformer {
{
switch($key)
{
case 'progress':
$map['data']['episode'] = $value;
break;
case 'notes':
$map['data']['comments'] = $value;
break;

View File

@ -44,9 +44,7 @@ class MangaListTransformer extends AbstractTransformer {
{
$map = [
'id' => $item['mal_id'],
'data' => [
'chapter' => $item['data']['progress']
]
'data' => []
];
$data =& $item['data'];
@ -55,6 +53,10 @@ class MangaListTransformer extends AbstractTransformer {
{
switch($key)
{
case 'progress':
$map['data']['chapter'] = $value;
break;
case 'notes':
$map['data']['comments'] = $value;
break;

View File

@ -63,61 +63,48 @@ class SyncKitsuWithMal extends BaseCommand {
$this->kitsuModel = $this->container->get('kitsu-model');
$this->malModel = $this->container->get('mal-model');
$this->syncAnime();
$this->syncManga();
$this->sync('anime');
$this->sync('manga');
}
public function syncAnime()
public function sync(string $type)
{
$malCount = count($this->malModel->getAnimeList());
$kitsuCount = $this->kitsuModel->getAnimeListCount();
$uType = ucfirst($type);
$malCount = count($this->malModel->{"get{$uType}List"}());
$kitsuCount = $this->kitsuModel->{"get{$uType}ListCount"}();
$this->echoBox("Number of MAL anime list items: {$malCount}");
$this->echoBox("Number of Kitsu anime list items: {$kitsuCount}");
$this->echoBox("Number of MAL {$type} list items: {$malCount}");
$this->echoBox("Number of Kitsu {$type} list items: {$kitsuCount}");
$data = $this->diffAnimeLists();
$this->echoBox("Number of anime items that need to be added to MAL: " . count($data['addToMAL']));
$data = $this->diffLists($type);
if ( ! empty($data['addToMAL']))
{
$this->echoBox("Adding missing anime list items to MAL");
$this->createMALListItems($data['addToMAL'], 'anime');
$count = count($data['addToMAL']);
$this->echoBox("Adding {$count} missing {$type} list items to MAL");
$this->createMALListItems($data['addToMAL'], $type);
}
$this->echoBox('Number of anime items that need to be added to Kitsu: ' . count($data['addToKitsu']));
if ( ! empty($data['addToKitsu']))
{
$this->echoBox("Adding missing anime list items to Kitsu");
$this->createKitsuListItems($data['addToKitsu'], 'anime');
}
}
public function syncManga()
{
$malCount = count($this->malModel->getMangaList());
$kitsuCount = $this->kitsuModel->getMangaListCount();
$this->echoBox("Number of MAL manga list items: {$malCount}");
$this->echoBox("Number of Kitsu manga list items: {$kitsuCount}");
$data = $this->diffMangaLists();
$this->echoBox("Number of manga items that need to be added to MAL: " . count($data['addToMAL']));
if ( ! empty($data['addToMAL']))
{
$this->echoBox("Adding missing manga list items to MAL");
$this->createMALListItems($data['addToMAL'], 'manga');
$count = count($data['addToKitsu']);
$this->echoBox("Adding {$count} missing {$type} list items to Kitsu");
$this->createKitsuListItems($data['addToKitsu'], $type);
}
$this->echoBox('Number of manga items that need to be added to Kitsu: ' . count($data['addToKitsu']));
if ( ! empty($data['addToKitsu']))
if ( ! empty($data['updateMAL']))
{
$this->echoBox("Adding missing manga list items to Kitsu");
$this->createKitsuListItems($data['addToKitsu'], 'manga');
$count = count($data['updateMAL']);
$this->echoBox("Updating {$count} outdated MAL {$type} list items");
$this->updateMALListItems($data['updateMAL'], $type);
}
if ( ! empty($data['updateKitsu']))
{
print_r($data['updateKitsu']);
$count = count($data['updateKitsu']);
$this->echoBox("Updating {$count} outdated Kitsu {$type} list items");
$this->updateKitsuListItems($data['updateKitsu'], $type);
}
}
@ -136,6 +123,19 @@ class SyncKitsuWithMal extends BaseCommand {
return $output;
}
public function formatMALList(string $type): array
{
if ($type === 'anime')
{
return $this->formatMALAnimeList();
}
if ($type === 'manga')
{
return $this->formatMALMangaList();
}
}
public function formatMALAnimeList()
{
$orig = $this->malModel->getAnimeList();
@ -149,10 +149,6 @@ class SyncKitsuWithMal extends BaseCommand {
'status' => AnimeWatchingStatus::MAL_TO_KITSU[$item['my_status']],
'progress' => $item['my_watched_episodes'],
'reconsuming' => (bool) $item['my_rewatching'],
'reconsumeCount' => array_key_exists('times_rewatched', $item)
? $item['times_rewatched']
: 0,
// 'notes' => ,
'rating' => $item['my_score'] / 2,
'updatedAt' => (new \DateTime())
->setTimestamp((int)$item['my_last_updated'])
@ -179,10 +175,6 @@ class SyncKitsuWithMal extends BaseCommand {
'progress' => $item['my_read_chapters'],
'volumes' => $item['my_read_volumes'],
'reconsuming' => (bool) $item['my_rereadingg'],
/* 'reconsumeCount' => array_key_exists('times_rewatched', $item)
? $item['times_rewatched']
: 0, */
// 'notes' => ,
'rating' => $item['my_score'] / 2,
'updatedAt' => (new \DateTime())
->setTimestamp((int)$item['my_last_updated'])
@ -194,18 +186,18 @@ class SyncKitsuWithMal extends BaseCommand {
return $output;
}
public function filterKitsuAnimeList()
public function formatKitsuList(string $type = 'anime'): array
{
$data = $this->kitsuModel->getFullAnimeList();
$data = $this->kitsuModel->{'getFull' . ucfirst($type) . 'List'}();
$includes = JsonAPI::organizeIncludes($data['included']);
$includes['mappings'] = $this->filterMappings($includes['mappings']);
$includes['mappings'] = $this->filterMappings($includes['mappings'], $type);
$output = [];
foreach($data['data'] as $listItem)
{
$animeId = $listItem['relationships']['anime']['data']['id'];
$potentialMappings = $includes['anime'][$animeId]['relationships']['mappings'];
$id = $listItem['relationships'][$type]['data']['id'];
$potentialMappings = $includes[$type][$id]['relationships']['mappings'];
$malId = NULL;
foreach ($potentialMappings as $mappingId)
@ -232,94 +224,14 @@ class SyncKitsuWithMal extends BaseCommand {
return $output;
}
public function filterKitsuMangaList()
{
$data = $this->kitsuModel->getFullMangaList();
$includes = JsonAPI::organizeIncludes($data['included']);
$includes['mappings'] = $this->filterMappings($includes['mappings'], 'manga');
$output = [];
foreach($data['data'] as $listItem)
{
$mangaId = $listItem['relationships']['manga']['data']['id'];
$potentialMappings = $includes['manga'][$mangaId]['relationships']['mappings'];
$malId = NULL;
foreach ($potentialMappings as $mappingId)
{
if (array_key_exists($mappingId, $includes['mappings']))
{
$malId = $includes['mappings'][$mappingId]['externalId'];
}
}
// Skip to the next item if there isn't a MAL ID
if (is_null($malId))
{
continue;
}
$output[$listItem['id']] = [
'id' => $listItem['id'],
'malId' => $malId,
'data' => $listItem['attributes'],
];
}
return $output;
}
public function diffMangaLists()
{
$kitsuList = $this->filterKitsuMangaList();
$malList = $this->formatMALMangaList();
$itemsToAddToMAL = [];
$itemsToAddToKitsu = [];
$malIds = array_column($malList, 'id');
$kitsuMalIds = array_column($kitsuList, 'malId');
$missingMalIds = array_diff($malIds, $kitsuMalIds);
foreach($missingMalIds as $mid)
{
$itemsToAddToKitsu[] = array_merge($malList[$mid]['data'], [
'id' => $this->kitsuModel->getKitsuIdFromMALId($mid, 'manga'),
'type' => 'manga'
]);
}
foreach($kitsuList as $kitsuItem)
{
if (in_array($kitsuItem['malId'], $malIds))
{
// Eventually, compare the list entries, and determine which
// needs to be updated
continue;
}
// Looks like this item only exists on Kitsu
$itemsToAddToMAL[] = [
'mal_id' => $kitsuItem['malId'],
'data' => $kitsuItem['data']
];
}
return [
'addToMAL' => $itemsToAddToMAL,
'addToKitsu' => $itemsToAddToKitsu
];
}
public function diffAnimeLists()
public function diffLists(string $type = 'anime'): array
{
// Get libraryEntries with media.mappings from Kitsu
// Organize mappings, and ignore entries without mappings
$kitsuList = $this->filterKitsuAnimeList();
$kitsuList = $this->formatKitsuList($type);
// Get MAL list data
$malList = $this->formatMALAnimeList();
$malList = $this->formatMALList($type);
$itemsToAddToMAL = [];
$itemsToAddToKitsu = [];
@ -334,8 +246,8 @@ class SyncKitsuWithMal extends BaseCommand {
{
// print_r($malList[$mid]);
$itemsToAddToKitsu[] = array_merge($malList[$mid]['data'], [
'id' => $this->kitsuModel->getKitsuIdFromMALId($mid),
'type' => 'anime'
'id' => $this->kitsuModel->getKitsuIdFromMALId($mid, $type),
'type' => $type
]);
}
@ -343,8 +255,23 @@ class SyncKitsuWithMal extends BaseCommand {
{
if (in_array($kitsuItem['malId'], $malIds))
{
// Eventually, compare the list entries, and determine which
// needs to be updated
$item = $this->compareListItems($kitsuItem, $malList[$kitsuItem['malId']]);
if (is_null($item))
{
continue;
}
if (in_array('kitsu', $item['updateType']))
{
$kitsuUpdateItems[] = $item['data'];
}
if (in_array('mal', $item['updateType']))
{
$malUpdateItems[] = $item['data'];
}
continue;
}
@ -356,15 +283,6 @@ class SyncKitsuWithMal extends BaseCommand {
}
// Compare each list entry
// If a list item exists only on MAL, create it on Kitsu with the existing data from MAL
// If a list item exists only on Kitsu, create it on MAL with the existing data from Kitsu
// If an item already exists on both APIS:
// Compare last updated dates, and use the later one
// Otherwise, use rewatch count, then episode progress as critera for selecting the more up
// to date entry
// Based on the 'newer' entry, update the other api list item
return [
'addToMAL' => $itemsToAddToMAL,
'updateMAL' => $malUpdateItems,
@ -373,6 +291,174 @@ class SyncKitsuWithMal extends BaseCommand {
];
}
public function compareListItems(array $kitsuItem, array $malItem)
{
$compareKeys = ['status', 'progress', 'rating', 'reconsuming'];
$diff = [];
$dateDiff = (new \DateTime($kitsuItem['data']['updatedAt'])) <=> (new \DateTime($malItem['data']['updatedAt']));
foreach($compareKeys as $key)
{
$diff[$key] = $kitsuItem['data'][$key] <=> $malItem['data'][$key];
}
// No difference? Bail out early
$diffValues = array_values($diff);
$diffValues = array_unique($diffValues);
if (count($diffValues) === 1 && $diffValues[0] === 0)
{
return;
}
$update = [
'id' => $kitsuItem['id'],
'mal_id' => $kitsuItem['malId'],
'data' => []
];
$return = [
'updateType' => []
];
$sameStatus = $diff['status'] === 0;
$sameProgress = $diff['progress'] === 0;
$sameRating = $diff['rating'] === 0;
// If status is the same, and progress count is different, use greater progress
if ($sameStatus && ( ! $sameProgress))
{
if ($diff['progress'] === 1)
{
$update['data']['progress'] = $kitsuItem['data']['progress'];
$return['updateType'][] = 'mal';
}
else if($diff['progress'] === -1)
{
$update['data']['progress'] = $malItem['data']['progress'];
$return['updateType'][] = 'kitsu';
}
}
// If status and progress are different, it's a bit more complicated...
// But, at least for now, assume newer record is correct
if ( ! ($sameStatus || $sameProgress))
{
if ($dateDiff === 1)
{
$update['data']['status'] = $kitsuItem['data']['status'];
if ((int)$kitsuItem['data']['progress'] !== 0)
{
$update['data']['progress'] = $kitsuItem['data']['progress'];
}
$return['updateType'][] = 'mal';
}
else if($dateDiff === -1)
{
$update['data']['status'] = $malItem['data']['status'];
if ((int)$malItem['data']['progress'] !== 0)
{
$update['data']['progress'] = $kitsuItem['data']['progress'];
}
$return['updateType'][] = 'kitsu';
}
}
// If rating is different, use the rating from the item most recently updated
if ( ! $sameRating)
{
if ($dateDiff === 1)
{
$update['data']['rating'] = $kitsuItem['data']['rating'];
$return['updateType'][] = 'mal';
}
else if ($dateDiff === -1)
{
$update['data']['rating'] = $malItem['data']['rating'];
$return['updateType'][] = 'kitsu';
}
}
// If status is different, use the status of the more recently updated item
if ( ! $sameStatus)
{
if ($dateDiff === 1)
{
$update['data']['status'] = $kitsuItem['data']['status'];
$return['updateType'][] = 'mal';
}
else if ($dateDiff === -1)
{
$update['data']['status'] = $malItem['data']['status'];
$return['updateType'][] = 'kitsu';
}
}
$return['meta'] = [
'kitsu' => $kitsuItem['data'],
'mal' => $malItem['data'],
'dateDiff' => $dateDiff,
'diff' => $diff,
];
$return['data'] = $update;
$return['updateType'] = array_unique($return['updateType']);
return $return;
}
public function updateKitsuListItems($itemsToUpdate, $type = 'anime')
{
$requester = new ParallelAPIRequest();
foreach($itemsToUpdate as $item)
{
$requester->addRequest($this->kitsuModel->updateListItem($item));
}
$responses = $requester->makeRequests();
foreach($responses as $key => $response)
{
$id = $itemsToUpdate[$key]['id'];
if ($response->getStatus() === 200)
{
$this->echoBox("Successfully updated Kitsu {$type} list item with id: {$id}");
}
else
{
echo $response->getBody();
$this->echoBox("Failed to update Kitsu {$type} list item with id: {$id}");
}
}
}
public function updateMALListItems($itemsToUpdate, $type = 'anime')
{
$transformer = new ALT();
$requester = new ParallelAPIRequest();
foreach($itemsToUpdate as $item)
{
$requester->addRequest($this->malModel->updateListItem($item, $type));
}
$responses = $requester->makeRequests();
foreach($responses as $key => $response)
{
$id = $itemsToUpdate[$key]['mal_id'];
if ($response->getBody() === 'Updated')
{
$this->echoBox("Successfully updated MAL {$type} list item with id: {$id}");
}
else
{
$this->echoBox("Failed to update MAL {$type} list item with id: {$id}");
}
}
}
public function createKitsuListItems($itemsToAdd, $type = 'anime')
{
$requester = new ParallelAPIRequest();

View File

@ -239,6 +239,13 @@ class Controller {
*/
protected function renderFullPage($view, string $template, array $data)
{
$csp = [
"default-src 'self'",
"object-src 'none'",
"child-src 'none'",
];
$view->addHeader('Content-Security-Policy', implode('; ', $csp));
$view->appendOutput($this->loadPartial($view, 'header', $data));
if (array_key_exists('message', $data) && is_array($data['message']))

View File

@ -280,11 +280,11 @@ class Anime extends BaseController {
);
}
foreach($data['included'] as $included)
if (array_key_exists('characters', $data['included']))
{
if ($included['type'] === 'characters')
foreach($data['included']['characters'] as $id => $character)
{
$characters[$included['id']] = $included['attributes'];
$characters[$id] = $character['attributes'];
}
}

View File

@ -17,19 +17,23 @@
namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\API\JsonAPI;
use Aviat\Ion\ArrayWrapper;
/**
* Controller for character description pages
*/
class Character extends BaseController {
use ArrayWrapper;
public function index(string $slug)
{
$model = $this->container->get('kitsu-model');
$data = $model->getCharacter($slug);
$rawData = $model->getCharacter($slug);
if (( ! array_key_exists('data', $data)) || empty($data['data']))
if (( ! array_key_exists('data', $rawData)) || empty($rawData['data']))
{
return $this->notFound(
$this->formatTitle(
@ -40,12 +44,124 @@ class Character extends BaseController {
);
}
$this->outputHTML('character', [
$data = JsonAPI::organizeData($rawData);
$viewData = [
'title' => $this->formatTitle(
'Characters',
$data['data'][0]['attributes']['name']
$data[0]['attributes']['name']
),
'data' => $data['data'][0]['attributes']
]);
'data' => $data,
'castCount' => 0,
'castings' => []
];
if (array_key_exists('included', $data) && array_key_exists('castings', $data['included']))
{
$viewData['castings'] = $this->organizeCast($data['included']['castings']);
$viewData['castCount'] = $this->getCastCount($viewData['castings']);
}
$this->outputHTML('character', $viewData);
}
/**
* Organize VA => anime relationships
*
* @param array $cast
* @return array
*/
private function dedupeCast(array $cast): array
{
$output = [];
$people = [];
$i = 0;
foreach ($cast as &$role)
{
if (empty($role['attributes']['role']))
{
continue;
}
$person = current($role['relationships']['person']['people'])['attributes'];
if ( ! array_key_exists($person['name'], $people))
{
$people[$person['name']] = $i;
$role['relationships']['media']['anime'] = [current($role['relationships']['media']['anime'])];
$output[$i] = $role;
$i++;
continue;
}
else if(array_key_exists($person['name'], $people))
{
if (array_key_exists('anime', $role['relationships']['media']))
{
$key = $people[$person['name']];
$output[$key]['relationships']['media']['anime'][] = current($role['relationships']['media']['anime']);
}
continue;
}
}
return $output;
}
private function getCastCount(array $cast): int
{
$count = 0;
foreach($cast as $role)
{
if (
array_key_exists('attributes', $role) &&
array_key_exists('role', $role['attributes']) &&
( ! is_null($role['attributes']['role']))
) {
$count++;
}
}
return $count;
}
private function organizeCast(array $cast): array
{
$cast = $this->dedupeCast($cast);
$output = [];
foreach($cast as $id => $role)
{
if (empty($role['attributes']['role']))
{
continue;
}
$language = $role['attributes']['language'];
$roleName = $role['attributes']['role'];
$isVA = $role['attributes']['voiceActor'];
if ($isVA)
{
$person = current($role['relationships']['person']['people'])['attributes'];
$name = $person['name'];
$item = [
'person' => $person,
'series' => $role['relationships']['media']['anime']
];
$output[$roleName][$language][] = $item;
}
else
{
$output[$roleName][] = $role['relationships']['person']['people'];
}
}
return $output;
}
}

View File

@ -16,10 +16,16 @@
namespace Aviat\AnimeClient\Controller;
use function Amp\wait;
use Amp\Artax\Client;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\API\JsonAPI;
use Aviat\Ion\View\HtmlView;
/**
* Controller for handling routes that don't fit elsewhere
*/
class Index extends BaseController {
/**
@ -105,7 +111,7 @@ class Index extends BaseController {
$data = $model->getUserData($username);
$orgData = JsonAPI::organizeData($data);
$this->outputHTML('me', [
'title' => 'About' . $this->config->get('whose_list'),
'title' => 'About ' . $this->config->get('whose_list'),
'data' => $orgData[0],
'attributes' => $orgData[0]['attributes'],
'relationships' => $orgData[0]['relationships'],
@ -113,11 +119,55 @@ class Index extends BaseController {
]);
}
/**
* Get image covers from kitsu
*
* @return void
*/
public function images($type, $file)
{
$kitsuUrl = 'https://media.kitsu.io/';
list($id, $ext) = explode('.', basename($file));
switch ($type)
{
case 'anime':
$kitsuUrl .= "anime/poster_images/{$id}/small.{$ext}";
break;
case 'avatars':
$kitsuUrl .= "users/avatars/{$id}/original.{$ext}";
break;
case 'manga':
$kitsuUrl .= "manga/poster_images/{$id}/small.{$ext}";
break;
case 'characters':
$kitsuUrl .= "characters/images/{$id}/original.{$ext}";
break;
default:
$this->notFound();
return;
}
$promise = (new Client)->request($kitsuUrl);
$response = wait($promise);
$data = (string) $response->getBody();
$baseSavePath = $this->config->get('img_cache_path');
file_put_contents("{$baseSavePath}/{$type}/{$id}.{$ext}", $data);
header('Content-type: ' . $response->getHeader('content-type')[0]);
echo (string) $response->getBody();
}
private function organizeFavorites(array $rawfavorites): array
{
// return $rawfavorites;
$output = [];
unset($rawfavorites['data']);
foreach($rawfavorites as $item)
{
$rank = $item['attributes']['favRank'];
@ -126,7 +176,7 @@ class Index extends BaseController {
$output[$key] = $output[$key] ?? [];
foreach ($fav as $id => $data)
{
$output[$key][$rank] = $data['attributes'];
$output[$key][$rank] = array_merge(['id' => $id], $data['attributes']);
}
}

View File

@ -26,6 +26,7 @@ use const Aviat\AnimeClient\{
use function Aviat\Ion\_dir;
use Aviat\AnimeClient\API\FailedResponseException;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Friend;
@ -256,13 +257,24 @@ class Dispatcher extends RoutingBase {
{
$logger = $this->container->getLogger('default');
$controller = new $controllerName($this->container);
try
{
$controller = new $controllerName($this->container);
// Run the appropriate controller method
$logger->debug('Dispatcher - controller arguments');
$logger->debug(print_r($params, TRUE));
// Run the appropriate controller method
$logger->debug('Dispatcher - controller arguments', $params);
call_user_func_array([$controller, $method], $params);
}
catch (FailedResponseException $e)
{
$controllerName = DEFAULT_CONTROLLER;
$controller = new $controllerName($this->container);
$controller->errorPage(500,
'API request timed out',
'Failed to retrieve data from API (╯°□°)╯︵ ┻━┻');
}
call_user_func_array([$controller, $method], $params);
}
/**

View File

@ -98,7 +98,7 @@ class MenuGenerator extends UrlGenerator {
foreach ($menuConfig as $title => $path)
{
$has = $this->string($this->path())->contains($path);
$selected = ($has && strlen($this->path()) >= strlen($path));
$selected = ($has && mb_strlen($this->path()) >= mb_strlen($path));
$link = $this->helper->a($this->url($path), $title);

View File

@ -22,7 +22,6 @@ use Aviat\Ion\Friend;
use Aviat\Ion\Json;
class AnimeListTransformerTest extends AnimeClientTestCase {
protected $dir;
protected $beforeTransform;
protected $afterTransform;
@ -34,19 +33,14 @@ class AnimeListTransformerTest extends AnimeClientTestCase {
$this->dir = AnimeClientTestCase::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);
$this->assertMatchesSnapshot($actual);
}
public function dataUntransform()
@ -60,19 +54,6 @@ class AnimeListTransformerTest extends AnimeClientTestCase {
'rewatched' => 0,
'notes' => 'Very formulaic.',
'edit' => true
],
'expected' => [
'id' => 14047981,
'mal_id' => null,
'data' => [
'status' => 'current',
'rating' => 4,
'reconsuming' => false,
'reconsumeCount' => 0,
'notes' => 'Very formulaic.',
'progress' => 38,
'private' => false
]
]
], [
'input' => [
@ -86,19 +67,19 @@ class AnimeListTransformerTest extends AnimeClientTestCase {
'edit' => 'true',
'private' => 'On',
'rewatching' => 'On'
],
'expected' => [
'id' => 14047981,
'mal_id' => '12345',
'data' => [
'status' => 'current',
'rating' => 4,
'reconsuming' => true,
'reconsumeCount' => 0,
'notes' => 'Very formulaic.',
'progress' => 38,
'private' => true,
]
]
], [
'input' => [
'id' => 14047983,
'mal_id' => '12347',
'watching_status' => 'current',
'user_rating' => 0,
'episodes_watched' => 12,
'rewatched' => 0,
'notes' => '',
'edit' => 'true',
'private' => 'On',
'rewatching' => 'On'
]
]];
}
@ -106,9 +87,9 @@ class AnimeListTransformerTest extends AnimeClientTestCase {
/**
* @dataProvider dataUntransform
*/
public function testUntransform($input, $expected)
public function testUntransform($input)
{
$actual = $this->transformer->untransform($input);
$this->assertEquals($expected, $actual);
$this->assertMatchesSnapshot($actual);
}
}

View File

@ -34,7 +34,6 @@ class AnimeTransformerTest extends AnimeClientTestCase {
$this->dir = AnimeClientTestCase::TEST_DATA_DIR . '/Kitsu';
$this->beforeTransform = Json::decodeFile("{$this->dir}/animeBeforeTransform.json");
$this->afterTransform = Json::decodeFile("{$this->dir}/animeAfterTransform.json");
$this->transformer = new AnimeTransformer();
}
@ -43,8 +42,7 @@ class AnimeTransformerTest extends AnimeClientTestCase {
{
$expected = $this->afterTransform;
$actual = $this->transformer->transform($this->beforeTransform);
// Json::encodeFile("{$this->dir}/animeAfterTransform.json", $actual);
$this->assertEquals($expected, $actual);
$this->assertMatchesSnapshot($actual);
}
}

View File

@ -47,19 +47,15 @@ class MangaListTransformerTest extends AnimeClientTestCase {
}
$this->beforeTransform = $rawBefore['data'];
$this->afterTransform = Json::decodeFile("{$this->dir}/mangaListAfterTransform.json");
// $this->afterTransform = Json::decodeFile("{$this->dir}/mangaListAfterTransform.json");
$this->transformer = new MangaListTransformer();
}
public function testTransform()
{
$expected = $this->afterTransform;
$actual = $this->transformer->transformCollection($this->beforeTransform);
// Json::encodeFile("{$this->dir}/mangaListAfterTransform.json", $actual);
$this->assertEquals($expected, $actual);
$this->assertMatchesSnapshot($actual);
}
public function testUntransform()
@ -69,6 +65,7 @@ class MangaListTransformerTest extends AnimeClientTestCase {
'mal_id' => '26769',
'chapters_read' => 67,
'manga' => [
'id' => '12345',
'titles' => ["Bokura wa Minna Kawaisou"],
'alternate_title' => NULL,
'slug' => "bokura-wa-minna-kawaisou",

View File

@ -36,8 +36,8 @@ class MangaTransformerTest extends AnimeClientTestCase {
$data = Json::decodeFile("{$this->dir}/mangaBeforeTransform.json");
$baseData = $data['data'][0]['attributes'];
$baseData['included'] = $data['included'];
$baseData['id'] = $data['data'][0]['id'];
$this->beforeTransform = $baseData;
$this->afterTransform = Json::decodeFile("{$this->dir}/mangaAfterTransform.json");
$this->transformer = new MangaTransformer();
}
@ -45,9 +45,6 @@ class MangaTransformerTest extends AnimeClientTestCase {
public function testTransform()
{
$actual = $this->transformer->transform($this->beforeTransform);
$expected = $this->afterTransform;
//Json::encodeFile("{$this->dir}/mangaAfterTransform.json", $actual);
$this->assertEquals($expected, $actual);
$this->assertMatchesSnapshot($actual);
}
}

View File

@ -0,0 +1,46 @@
<?php return array (
'id' => '15839442',
'mal_id' => '33206',
'episodes' =>
array (
'watched' => '-',
'total' => '-',
'length' => NULL,
),
'airing' =>
array (
'status' => 'Currently Airing',
'started' => '2017-01-12',
'ended' => NULL,
),
'anime' =>
array (
'id' => '12243',
'age_rating' => NULL,
'title' => 'Kobayashi-san Chi no Maid Dragon',
'titles' =>
array (
0 => 'Kobayashi-san Chi no Maid Dragon',
1 => 'Miss Kobayashi\'s Dragon Maid',
2 => '小林さんちのメイドラゴン',
),
'slug' => 'kobayashi-san-chi-no-maid-dragon',
'type' => 'TV',
'image' => 'https://media.kitsu.io/anime/poster_images/12243/small.jpg?1481144116',
'genres' =>
array (
0 => 'Comedy',
1 => 'Fantasy',
2 => 'Slice of Life',
),
'streaming_links' =>
array (
),
),
'watching_status' => 'current',
'notes' => NULL,
'rewatching' => false,
'rewatched' => 0,
'user_rating' => '-',
'private' => false,
);

View File

@ -0,0 +1,14 @@
<?php return array (
'id' => 14047981,
'mal_id' => NULL,
'data' =>
array (
'status' => 'current',
'reconsuming' => false,
'reconsumeCount' => 0,
'notes' => 'Very formulaic.',
'progress' => 38,
'private' => false,
'rating' => 4,
),
);

View File

@ -0,0 +1,14 @@
<?php return array (
'id' => 14047981,
'mal_id' => '12345',
'data' =>
array (
'status' => 'current',
'reconsuming' => true,
'reconsumeCount' => 0,
'notes' => 'Very formulaic.',
'progress' => 38,
'private' => true,
'rating' => 4,
),
);

View File

@ -0,0 +1,13 @@
<?php return array (
'id' => 14047983,
'mal_id' => '12347',
'data' =>
array (
'status' => 'current',
'reconsuming' => true,
'reconsumeCount' => 0,
'notes' => '',
'progress' => 12,
'private' => true,
),
);

View File

@ -0,0 +1,104 @@
<?php return array (
'id' => 32344,
'slug' => 'attack-on-titan',
'title' => 'Attack on Titan',
'titles' =>
array (
0 => 'Attack on Titan',
1 => 'Shingeki no Kyojin',
2 => '進撃の巨人',
),
'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.
(Source: ANN)',
'age_rating' => 'R',
'age_rating_guide' => 'Violence, Profanity',
'url' => 'https://kitsu.io/anime/attack-on-titan',
'genres' =>
array (
0 => 'Action',
1 => 'Drama',
2 => 'Fantasy',
3 => 'Super Power',
),
'streaming_links' =>
array (
0 =>
array (
'meta' =>
array (
'name' => 'Crunchyroll',
'link' => true,
'image' => 'streaming-logos/crunchyroll.svg',
),
'link' => 'http://www.crunchyroll.com/attack-on-titan',
'subs' =>
array (
0 => 'en',
),
'dubs' =>
array (
0 => 'ja',
),
),
1 =>
array (
'meta' =>
array (
'name' => 'Hulu',
'link' => true,
'image' => 'streaming-logos/hulu.svg',
),
'link' => 'http://www.hulu.com/attack-on-titan',
'subs' =>
array (
0 => 'en',
),
'dubs' =>
array (
0 => 'ja',
),
),
2 =>
array (
'meta' =>
array (
'name' => 'Funimation',
'link' => true,
'image' => 'streaming-logos/funimation.svg',
),
'link' => 'http://www.funimation.com/shows/attack-on-titan/videos/episodes',
'subs' =>
array (
0 => 'en',
),
'dubs' =>
array (
0 => 'ja',
),
),
3 =>
array (
'meta' =>
array (
'name' => 'Netflix',
'link' => false,
'image' => 'streaming-logos/netflix.svg',
),
'link' => 't',
'subs' =>
array (
0 => 'en',
),
'dubs' =>
array (
0 => 'ja',
),
),
),
);

View File

@ -0,0 +1,206 @@
<?php return array (
0 =>
array (
'id' => '15084773',
'mal_id' => '26769',
'chapters' =>
array (
'read' => 67,
'total' => '-',
),
'volumes' =>
array (
'read' => '-',
'total' => '-',
),
'manga' =>
array (
'id' => '20286',
'titles' =>
array (
0 => 'Bokura wa Minna Kawaisou',
),
'alternate_title' => NULL,
'slug' => 'bokura-wa-minna-kawaisou',
'url' => 'https://kitsu.io/manga/bokura-wa-minna-kawaisou',
'type' => 'manga',
'image' => 'https://media.kitsu.io/manga/poster_images/20286/small.jpg?1434293999',
'genres' =>
array (
0 => 'Comedy',
1 => 'Romance',
2 => 'School',
3 => 'Slice of Life',
4 => 'Thriller',
),
),
'reading_status' => 'current',
'notes' => '',
'rereading' => false,
'reread' => 0,
'user_rating' => 9.0,
),
1 =>
array (
'id' => '15085607',
'mal_id' => '16',
'chapters' =>
array (
'read' => 17,
'total' => 120,
),
'volumes' =>
array (
'read' => '-',
'total' => 14,
),
'manga' =>
array (
'id' => '47',
'titles' =>
array (
0 => 'Love Hina',
),
'alternate_title' => NULL,
'slug' => 'love-hina',
'url' => 'https://kitsu.io/manga/love-hina',
'type' => 'manga',
'image' => 'https://media.kitsu.io/manga/poster_images/47/small.jpg?1434249493',
'genres' =>
array (
0 => 'Comedy',
1 => 'Ecchi',
2 => 'Harem',
3 => 'Romance',
4 => 'Sports',
),
),
'reading_status' => 'current',
'notes' => '',
'rereading' => false,
'reread' => 0,
'user_rating' => 7.0,
),
2 =>
array (
'id' => '15084529',
'mal_id' => '35003',
'chapters' =>
array (
'read' => 16,
'total' => '-',
),
'volumes' =>
array (
'read' => '-',
'total' => '-',
),
'manga' =>
array (
'id' => '11777',
'titles' =>
array (
0 => 'Yamada-kun to 7-nin no Majo',
1 => 'Yamada-kun and the Seven Witches',
),
'alternate_title' => NULL,
'slug' => 'yamada-kun-to-7-nin-no-majo',
'url' => 'https://kitsu.io/manga/yamada-kun-to-7-nin-no-majo',
'type' => 'manga',
'image' => 'https://media.kitsu.io/manga/poster_images/11777/small.jpg?1438784325',
'genres' =>
array (
0 => 'Comedy',
1 => 'Ecchi',
2 => 'Gender Bender',
3 => 'Romance',
4 => 'School',
5 => 'Sports',
6 => 'Supernatural',
),
),
'reading_status' => 'current',
'notes' => '',
'rereading' => false,
'reread' => 0,
'user_rating' => 9.0,
),
3 =>
array (
'id' => '15312827',
'mal_id' => '78523',
'chapters' =>
array (
'read' => 68,
'total' => '-',
),
'volumes' =>
array (
'read' => '-',
'total' => '-',
),
'manga' =>
array (
'id' => '27175',
'titles' =>
array (
0 => 'ReLIFE',
),
'alternate_title' => NULL,
'slug' => 'relife',
'url' => 'https://kitsu.io/manga/relife',
'type' => 'manga',
'image' => 'https://media.kitsu.io/manga/poster_images/27175/small.jpg?1464379411',
'genres' =>
array (
0 => 'Romance',
1 => 'School',
2 => 'Slice of Life',
),
),
'reading_status' => 'current',
'notes' => '',
'rereading' => false,
'reread' => 0,
'user_rating' => '-',
),
4 =>
array (
'id' => '15084769',
'mal_id' => '60815',
'chapters' =>
array (
'read' => 43,
'total' => '-',
),
'volumes' =>
array (
'read' => '-',
'total' => '-',
),
'manga' =>
array (
'id' => '25491',
'titles' =>
array (
0 => 'Joshikausei',
),
'alternate_title' => NULL,
'slug' => 'joshikausei',
'url' => 'https://kitsu.io/manga/joshikausei',
'type' => 'manga',
'image' => 'https://media.kitsu.io/manga/poster_images/25491/small.jpg?1434305043',
'genres' =>
array (
0 => 'Comedy',
1 => 'School',
2 => 'Slice of Life',
),
),
'reading_status' => 'current',
'notes' => '',
'rereading' => false,
'reread' => 0,
'user_rating' => 8.0,
),
);

View File

@ -0,0 +1,21 @@
<?php return array (
'id' => '20286',
'title' => 'Bokura wa Minna Kawaisou',
'en_title' => NULL,
'jp_title' => 'Bokura wa Minna Kawaisou',
'cover_image' => 'https://media.kitsu.io/manga/poster_images/20286/small.jpg?1434293999',
'manga_type' => 'manga',
'chapter_count' => '-',
'volume_count' => '-',
'synopsis' => 'Usa, a high-school student aspiring to begin a bachelor lifestyle, moves into a new apartment only to discover that he not only shares a room with a perverted roommate that has an obsession for underaged girls, but also that another girl, Ritsu, a love-at-first-sight, is living in the same building as well!
(Source: Kirei Cake)',
'url' => 'https://kitsu.io/manga/bokura-wa-minna-kawaisou',
'genres' =>
array (
0 => 'Comedy',
1 => 'Romance',
2 => 'School',
3 => 'Slice of Life',
4 => 'Thriller',
),
);

View File

@ -23,6 +23,7 @@ use function Aviat\Ion\_dir;
use Aura\Web\WebFactory;
use Aviat\Ion\Json;
use PHPUnit\Framework\TestCase;
use Spatie\Snapshots\MatchesSnapshots;
use Zend\Diactoros\{
Response as HttpResponse,
ServerRequestFactory
@ -36,6 +37,9 @@ define('TEST_VIEW_DIR', __DIR__ . '/test_views');
* Base class for TestCases
*/
class AnimeClientTestCase extends TestCase {
use MatchesSnapshots;
// Test directory constants
const ROOT_DIR = ROOT_DIR;
const SRC_DIR = SRC_DIR;

View File

@ -1,4 +1,5 @@
{
"id": 32344,
"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,