diff --git a/.gitignore b/.gitignore index 9a13d5e9..46eb891b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,118 @@ + +# Created by https://www.gitignore.io/api/macos,jetbrains+all + +### JetBrains+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +# End of https://www.gitignore.io/api/macos,jetbrains+all + .codelite .phing_targets .sonar/ @@ -23,10 +138,11 @@ build/** app/config/*.toml !app/config/*.toml.example phinx.yml -.idea/ Caddyfile build/humbuglog.txt public/images/anime/** public/images/avatars/** public/images/manga/** -public/images/characters/** \ No newline at end of file +public/images/characters/** +public/images/people/** +public/mal_mappings.json \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 6a3d138b..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,21 +0,0 @@ -test:7.1: - stage: test - before_script: - - sh build/docker_install.sh > /dev/null - - apk add --no-cache php7-phpdbg - - curl -sS https://getcomposer.org/installer | php - - php composer.phar install --ignore-platform-reqs - image: php:7.1-alpine - script: - - phpdbg -qrr -- ./vendor/bin/phpunit --coverage-text --colors=never - -test:7.2: - stage: test - before_script: - - sh build/docker_install.sh > /dev/null - - apk add --no-cache php7-phpdbg - - curl -sS https://getcomposer.org/installer | php - - php composer.phar install --ignore-platform-reqs - image: php:7.2-alpine - script: - - phpdbg -qrr -- ./vendor/bin/phpunit --coverage-text --colors=never diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d1fda30..3583d3e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,20 @@ # Changelog +## Version 4.1 +* Removed MAL integration, added Anilist Integration +* Now uses WebP cache images when the browser supports it +* Replaces JS minifier with pre-minified scripts (Removes the need for one caching folder, too) +* Updated console command to sync Kitsu and Anilist data (Kitsu can sync MAL, and MAL's API broke, so MAL sync was removed) +* Added page to update settings without having to edit config files +* Defaulted to secure (HTTPS) urls +* Updated Character pages to show voice actors +* Added People pages, showing which works they contributed to, and in what role + ## Version 4 * Updated to use Kitsu API after discontinuation of Hummingbird * Added streaming links to list entries from the Kitsu API * Added simple integration with MyAnimeList, so an update can cross-post to both Kitsu and MyAnimeList (anime and manga) * Added console command to sync Kitsu and MyAnimeList data - * Added character pages ## Version 3 diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 00000000..ac979353 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,37 @@ +pipeline { + agent none + stages { + stage('PHP 7.1') { + agent { + docker { + image 'php:7.1-alpine' + args '-u root --privileged' + } + } + steps { + sh 'chmod +x ./build/docker_install.sh' + sh 'sh build/docker_install.sh' + sh 'apk add --no-cache php7-phpdbg' + sh 'curl -sS https://getcomposer.org/installer | php' + sh 'php composer.phar install --ignore-platform-reqs' + sh 'phpdbg -qrr -- ./vendor/bin/phpunit --coverage-text --colors=never' + } + } + stage('PHP 7.2') { + agent { + docker { + image 'php:7.2-alpine' + args '-u root --privileged' + } + } + steps { + sh 'chmod +x ./build/docker_install.sh' + sh 'sh build/docker_install.sh' + sh 'apk add --no-cache php7-phpdbg' + sh 'curl -sS https://getcomposer.org/installer | php' + sh 'php composer.phar install --ignore-platform-reqs' + sh 'phpdbg -qrr -- ./vendor/bin/phpunit --coverage-text --colors=never' + } + } + } + } \ No newline at end of file diff --git a/README.md b/README.md index 337f94f5..a70a6cd8 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Update your anime/manga list on Kitsu.io and MyAnimeList.net [![Build Status](https://travis-ci.org/timw4mail/HummingBirdAnimeClient.svg?branch=master)](https://travis-ci.org/timw4mail/HummingBirdAnimeClient) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/timw4mail/HummingBirdAnimeClient/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/timw4mail/HummingBirdAnimeClient/?branch=master) +[![Build Status](https://jenkins.timshomepage.net/buildStatus/icon?job=aviat/HummingBirdAnimeClient/develop)](https://jenkins.timshomepage.net/job/aviat/HummingBirdAnimeClient/develop) [[Hosted Example](https://list.timshomepage.net)] @@ -33,22 +33,25 @@ Update your anime/manga list on Kitsu.io and MyAnimeList.net * PHP 7.1+ * PDO SQLite or PDO PostgreSQL (For collection tab) -* GD +* GD extension for caching images + +### Highly Recommended * Redis or Memcached for caching ### Installation 1. Install via git, then install dependencies via composer: `composer install` -2. Duplicate `app/config/*.toml.example` files as `app/config/*.toml` +2. Duplicate `app/config/config.toml.example` file as `app/config/config.toml` 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 + * app/config * app/logs - * public/js/cache * public/images/avatars * public/images/anime * public/images/characters * public/images/manga 5. Make sure the `console` script is executable +6. Additional settings are on the settings page once you log in. ### Server Setup diff --git a/app/appConf/base_config.php b/app/appConf/base_config.php index c58684aa..18a96c66 100644 --- a/app/appConf/base_config.php +++ b/app/appConf/base_config.php @@ -28,6 +28,24 @@ $tomlConfig = loadToml(__DIR__); return array_merge($tomlConfig, [ 'asset_dir' => "{$ROOT_DIR}/public", + 'base_config_dir' => __DIR__, + 'config_dir' => "{$APP_DIR}/config", + + // No config defaults + 'kitsu_username' => 'timw4mail', + 'whose_list' => 'Someone', + 'cache' => [ + 'connection' => [], + 'driver' => 'null', + ], + 'secure_urls' => TRUE, + + // Routing defaults + 'asset_path' => '/public', + 'default_list' => 'anime', //anime|manga + 'default_anime_list_path' => 'watching', // watching|plan_to_watch|on_hold|dropped|completed|all + 'default_manga_list_path' => 'reading', // reading|plan_to_read|on_hold|dropped|completed|all + 'default_view_type' => 'cover_view', // cover_view|list_view // Template file path 'view_path' => "{$APP_DIR}/views", diff --git a/app/appConf/minify_config.php b/app/appConf/minify_config.php deleted file mode 100644 index d06dd41c..00000000 --- a/app/appConf/minify_config.php +++ /dev/null @@ -1,69 +0,0 @@ - - * @copyright 2015 - 2017 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://github.com/timw4mail/HummingBirdAnimeClient - */ - -// -------------------------------------------------------------------------- - -return [ - - /* - |-------------------------------------------------------------------------- - | JS Folder - |-------------------------------------------------------------------------- - | - | The folder where javascript files exist, in relation to the document root - | - */ - 'js_root' => 'js/', - - /* - |-------------------------------------------------------------------------- - | JS Groups - |-------------------------------------------------------------------------- - | - | Config array for javascript files to concatenate and minify - | - */ - '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' => [ - 'anime_search_results.js', - 'anime_collection.js', - ], - 'manga_collection' => [ - 'manga_search_results.js', - 'manga_collection.js', - ], - ] -]; -// End of minify_config.php \ No newline at end of file diff --git a/app/appConf/route_config.toml b/app/appConf/route_config.toml deleted file mode 100644 index 74191a89..00000000 --- a/app/appConf/route_config.toml +++ /dev/null @@ -1,19 +0,0 @@ -################################################################################ -# Route config -# -# Default views and paths -################################################################################ - -# Path to public directory, where images/css/javascript are located, -# appended to the url -asset_path = "/public" - -# Which list should be the default? -default_list = "anime" # anime or manga - -# Default pages for anime/manga -default_anime_list_path = "watching" # watching|plan_to_watch|on_hold|dropped|completed|all -default_manga_list_path = "reading" # reading|plan_to_read|on_hold|dropped|completed|all - -# Default view type (cover_view/list_view) -default_view_type = "cover_view" \ No newline at end of file diff --git a/app/appConf/routes.php b/app/appConf/routes.php index f2157a8a..feadb761 100644 --- a/app/appConf/routes.php +++ b/app/appConf/routes.php @@ -15,6 +15,9 @@ */ use const Aviat\AnimeClient\{ + ALPHA_SLUG_PATTERN, + NUM_PATTERN, + SLUG_PATTERN, DEFAULT_CONTROLLER_METHOD, DEFAULT_CONTROLLER }; @@ -24,14 +27,13 @@ use const Aviat\AnimeClient\{ // // Maps paths to controllers and methods // ------------------------------------------------------------------------- -return [ +$routes = [ // --------------------------------------------------------------------- // Anime List Routes // --------------------------------------------------------------------- 'anime.add.get' => [ 'path' => '/anime/add', 'action' => 'addForm', - 'verb' => 'get', ], 'anime.add.post' => [ 'path' => '/anime/add', @@ -42,7 +44,7 @@ return [ 'path' => '/anime/details/{id}', 'action' => 'details', 'tokens' => [ - 'id' => '[a-z0-9\-]+', + 'id' => SLUG_PATTERN, ], ], 'anime.delete' => [ @@ -60,7 +62,6 @@ return [ 'manga.add.get' => [ 'path' => '/manga/add', 'action' => 'addForm', - 'verb' => 'get', ], 'manga.add.post' => [ 'path' => '/manga/add', @@ -76,7 +77,7 @@ return [ 'path' => '/manga/details/{id}', 'action' => 'details', 'tokens' => [ - 'id' => '[a-z0-9\-]+', + 'id' => SLUG_PATTERN, ], ], // --------------------------------------------------------------------- @@ -89,13 +90,12 @@ return [ 'anime.collection.add.get' => [ 'path' => '/anime-collection/add', 'action' => 'form', - 'params' => [], ], 'anime.collection.edit.get' => [ 'path' => '/anime-collection/edit/{id}', 'action' => 'form', 'tokens' => [ - 'id' => '[0-9]+', + 'id' => NUM_PATTERN, ], ], 'anime.collection.add.post' => [ @@ -110,10 +110,8 @@ return [ ], 'anime.collection.view' => [ 'path' => '/anime-collection/view{/view}', - 'action' => 'index', - 'params' => [], 'tokens' => [ - 'view' => '[a-z_]+', + 'view' => ALPHA_SLUG_PATTERN, ], ], 'anime.collection.delete' => [ @@ -131,13 +129,12 @@ return [ 'manga.collection.add.get' => [ 'path' => '/manga-collection/add', 'action' => 'form', - 'params' => [], ], 'manga.collection.edit.get' => [ 'path' => '/manga-collection/edit/{id}', 'action' => 'form', 'tokens' => [ - 'id' => '[0-9]+', + 'id' => NUM_PATTERN, ], ], 'manga.collection.add.post' => [ @@ -152,10 +149,8 @@ return [ ], 'manga.collection.view' => [ 'path' => '/manga-collection/view{/view}', - 'action' => 'index', - 'params' => [], 'tokens' => [ - 'view' => '[a-z_]+', + 'view' => ALPHA_SLUG_PATTERN, ], ], 'manga.collection.delete' => [ @@ -168,17 +163,27 @@ return [ // --------------------------------------------------------------------- 'character' => [ 'path' => '/character/{slug}', - 'action' => 'index', - 'params' => [], 'tokens' => [ - 'slug' => '[a-z0-9\-]+' + 'slug' => SLUG_PATTERN ] ], - 'user_info' => [ + 'person' => [ + 'path' => '/people/{id}', + 'tokens' => [ + 'id' => SLUG_PATTERN + ] + ], + 'default_user_info' => [ 'path' => '/me', 'action' => 'me', - 'controller' => 'me', - 'verb' => 'get', + 'controller' => 'user', + ], + 'user_info' => [ + 'path' => '/user/{username}', + 'controller' => 'user', + 'tokens' => [ + 'username' => '.*?' + ] ], // --------------------------------------------------------------------- // Default / Shared routes @@ -186,52 +191,61 @@ return [ 'anilist-redirect' => [ 'path' => '/anilist-redirect', 'action' => 'anilistRedirect', - 'controller' => DEFAULT_CONTROLLER, + 'controller' => 'settings', ], - 'anilist-oauth' => [ + 'anilist-callback' => [ 'path' => '/anilist-oauth', 'action' => 'anilistCallback', - 'controller' => DEFAULT_CONTROLLER, + 'controller' => 'settings', ], 'image_proxy' => [ 'path' => '/public/images/{type}/{file}', - 'action' => 'images', - 'controller' => DEFAULT_CONTROLLER, - 'verb' => 'get', + 'action' => 'cache', + 'controller' => 'images', 'tokens' => [ - 'type' => '[a-z0-9\-]+', - 'file' => '[a-z0-9\-]+\.[a-z]{3}' + 'type' => SLUG_PATTERN, + 'file' => '[a-z0-9\-]+\.[a-z]{3,4}' ] ], 'cache_purge' => [ 'path' => '/cache_purge', 'action' => 'clearCache', - 'controller' => DEFAULT_CONTROLLER, - 'verb' => 'get', + ], + 'settings' => [ + 'path' => '/settings', + ], + 'settings-post' => [ + 'path' => '/settings/update', + 'action' => 'update', + 'verb' => 'post', ], 'login' => [ 'path' => '/login', 'action' => 'login', - 'controller' => DEFAULT_CONTROLLER, - 'verb' => 'get', ], 'login.post' => [ 'path' => '/login', 'action' => 'loginAction', - 'controller' => DEFAULT_CONTROLLER, 'verb' => 'post', ], 'logout' => [ 'path' => '/logout', 'action' => 'logout', - 'controller' => DEFAULT_CONTROLLER, + ], + 'increment' => [ + 'path' => '/{controller}/increment', + 'action' => 'increment', + 'verb' => 'post', + 'tokens' => [ + 'controller' => ALPHA_SLUG_PATTERN, + ], ], 'update' => [ 'path' => '/{controller}/update', 'action' => 'update', 'verb' => 'post', 'tokens' => [ - 'controller' => '[a-z_]+', + 'controller' => ALPHA_SLUG_PATTERN, ], ], 'update.post' => [ @@ -239,28 +253,46 @@ return [ 'action' => 'formUpdate', 'verb' => 'post', 'tokens' => [ - 'controller' => '[a-z_]+', + 'controller' => ALPHA_SLUG_PATTERN, ], ], 'edit' => [ 'path' => '/{controller}/edit/{id}/{status}', 'action' => 'edit', 'tokens' => [ - 'id' => '[0-9a-z_]+', + 'id' => SLUG_PATTERN, 'status' => '([a-zA-Z\-_]|%20)+', ], ], 'list' => [ 'path' => '/{controller}/{type}{/view}', - 'action' => DEFAULT_CONTROLLER_METHOD, 'tokens' => [ - 'type' => '[a-z_]+', - 'view' => '[a-z_]+', + 'type' => ALPHA_SLUG_PATTERN, + 'view' => ALPHA_SLUG_PATTERN, ], ], 'index_redirect' => [ 'path' => '/', - 'controller' => DEFAULT_CONTROLLER, 'action' => 'redirectToDefaultRoute', ], -]; \ No newline at end of file +]; + +$defaultMap = [ + 'action' => DEFAULT_CONTROLLER_METHOD, + 'controller' => DEFAULT_CONTROLLER, + 'params' => [], + 'verb' => 'get', +]; + +foreach ($routes as &$route) +{ + foreach($defaultMap as $key => $val) + { + if ( ! array_key_exists($key, $route)) + { + $route[$key] = $val; + } + } +} + +return $routes; diff --git a/app/bootstrap.php b/app/bootstrap.php index e216fbb3..fbcb9411 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -22,9 +22,7 @@ use Aura\Session\SessionFactory; use Aviat\AnimeClient\API\{ Anilist, Kitsu, - MAL, - Kitsu\KitsuRequestBuilder, - MAL\MALRequestBuilder + Kitsu\KitsuRequestBuilder }; use Aviat\AnimeClient\Model; use Aviat\Banker\Pool; @@ -37,7 +35,7 @@ use Zend\Diactoros\{Response, ServerRequestFactory}; // ----------------------------------------------------------------------------- // Setup DI container // ----------------------------------------------------------------------------- -return function (array $configArray = []) { +return function ($configArray = []) { $container = new Container(); // ------------------------------------------------------------------------- @@ -50,12 +48,9 @@ return function (array $configArray = []) { $anilistRequestLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/anilist_request.log', Logger::NOTICE)); $kitsuRequestLogger = new Logger('kitsu-request'); $kitsuRequestLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/kitsu_request.log', Logger::NOTICE)); - $malRequestLogger = new Logger('mal-request'); - $malRequestLogger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/mal_request.log', Logger::NOTICE)); $container->setLogger($appLogger); $container->setLogger($anilistRequestLogger, 'anilist-request'); $container->setLogger($kitsuRequestLogger, 'kitsu-request'); - $container->setLogger($malRequestLogger, 'mal-request'); // ------------------------------------------------------------------------- // Injected Objects @@ -86,6 +81,16 @@ return function (array $configArray = []) { $menuHelper->setContainer($container); return $menuHelper; }); + $htmlHelper->set('field', function() use ($container) { + $formHelper = new Helper\Form(); + $formHelper->setContainer($container); + return $formHelper; + }); + $htmlHelper->set('picture', function() use ($container) { + $pictureHelper = new Helper\Picture(); + $pictureHelper->setContainer($container); + return $pictureHelper; + }); return $htmlHelper; }); @@ -131,17 +136,18 @@ return function (array $configArray = []) { $model->setCache($cache); return $model; }); - $container->set('mal-model', function($container) { - $requestBuilder = new MALRequestBuilder(); - $requestBuilder->setLogger($container->getLogger('mal-request')); + $container->set('anilist-model', function($container) { + $requestBuilder = new Anilist\AnilistRequestBuilder(); + $requestBuilder->setLogger($container->getLogger('anilist-request')); - $listItem = new MAL\ListItem(); + $listItem = new Anilist\ListItem(); $listItem->setContainer($container); $listItem->setRequestBuilder($requestBuilder); - $model = new MAL\Model($listItem); + $model = new Anilist\Model($listItem); $model->setContainer($container); $model->setRequestBuilder($requestBuilder); + return $model; }); @@ -160,6 +166,11 @@ return function (array $configArray = []) { $container->set('manga-collection-model', function($container) { return new Model\MangaCollection($container); }); + $container->set('settings-model', function($container) { + $model = new Model\Settings($container->get('config')); + $model->setContainer($container); + return $model; + }); // Miscellaneous Classes $container->set('auth', function($container) { diff --git a/app/config/cache.toml.example b/app/config/cache.toml.example index 50e19777..7efacce5 100644 --- a/app/config/cache.toml.example +++ b/app/config/cache.toml.example @@ -2,7 +2,7 @@ # Cache Setup # ################################################################################ -# See https://git.timshomepage.net/timw4mail/banker for more information +# See https://git.timshomepage.net/aviat/banker for more information # Available drivers are apcu, memcache, memcached, redis or null # Null cache driver means no caching diff --git a/app/config/config.toml.example b/app/config/config.toml.example index 28f954b7..b185050b 100644 --- a/app/config/config.toml.example +++ b/app/config/config.toml.example @@ -3,13 +3,34 @@ ################################################################################ # Username for anime and manga lists -kitsu_username = "timw4mail" +kitsu_username = "johnsmith" # Whose list is it? -whose_list = "Tim" +whose_list = "Someone" # do you wish to show the anime collection? show_anime_collection = true -# path to public directory on the server -asset_dir = "/../../public" \ No newline at end of file +# do you wish to show the manga collection? +show_manga_collection = false + +################################################################################ +# Default views and paths +################################################################################ + +# Which list should be the default? +default_list = "anime" # anime or manga + +# Default pages for anime/manga +default_anime_list_path = "watching" # watching|plan_to_watch|on_hold|dropped|completed|all +default_manga_list_path = "reading" # reading|plan_to_read|on_hold|dropped|completed|all + +################################################################################ +# Not on Settings Page +# +# These settings are not available to change on the settings page +################################################################################ + +# Use HTTPs for URLs +# It is not recommended to change this setting +secure_urls = true diff --git a/app/config/database.toml.example b/app/config/database.toml.example index d80fcf05..d71eb9fb 100644 --- a/app/config/database.toml.example +++ b/app/config/database.toml.example @@ -2,7 +2,6 @@ # Database Configuration # ################################################################################ -[collection] type = "sqlite" host = "" user = "" diff --git a/app/config/route_config.toml.example b/app/config/route_config.toml.example deleted file mode 100644 index 74191a89..00000000 --- a/app/config/route_config.toml.example +++ /dev/null @@ -1,19 +0,0 @@ -################################################################################ -# Route config -# -# Default views and paths -################################################################################ - -# Path to public directory, where images/css/javascript are located, -# appended to the url -asset_path = "/public" - -# Which list should be the default? -default_list = "anime" # anime or manga - -# Default pages for anime/manga -default_anime_list_path = "watching" # watching|plan_to_watch|on_hold|dropped|completed|all -default_manga_list_path = "reading" # reading|plan_to_read|on_hold|dropped|completed|all - -# Default view type (cover_view/list_view) -default_view_type = "cover_view" \ No newline at end of file diff --git a/app/views/anime/add.php b/app/views/anime/add.php index 8e586e37..c471026b 100644 --- a/app/views/anime/add.php +++ b/app/views/anime/add.php @@ -9,7 +9,7 @@
-
+

@@ -36,5 +36,4 @@ - \ No newline at end of file diff --git a/app/views/anime/cover-item.php b/app/views/anime/cover-item.php new file mode 100644 index 00000000..92b2e559 --- /dev/null +++ b/app/views/anime/cover-item.php @@ -0,0 +1,91 @@ +
+ isAuthenticated()): ?> + + + picture("images/anime/{$item['anime']['id']}.webp") ?> + + +
+ +
+ + + + + +
+ + + 0): ?> +
+
Rewatched time(s)
+
+ + + 0): ?> +
+ + + +
+ + + isAuthenticated()): ?> +
+ + Edit + +
+ + +
+
Rating: / 10
+
Episodes: + / + +
+
+
+
html($item['anime']['show_type']) ?>
+
html($item['airing']['status']) ?>
+
html($item['anime']['age_rating']) ?>
+
+
+
\ No newline at end of file diff --git a/app/views/anime/cover.php b/app/views/anime/cover.php index 7f6d5e66..60e08632 100644 --- a/app/views/anime/cover.php +++ b/app/views/anime/cover.php @@ -17,80 +17,7 @@
isAuthenticated()) continue; ?> -
- isAuthenticated()): ?> - - - " alt="" /> - -
- -
- - - - - -
- - - 0): ?> -
-
Rewatched time(s)
-
- - - 0): ?> -
- - - -
- - - isAuthenticated()): ?> -
- - Edit - -
- - -
-
Rating: / 10
-
Episodes: - / - -
-
-
-
html($item['anime']['show_type']) ?>
-
html($item['airing']['status']) ?>
-
html($item['anime']['age_rating']) ?>
-
-
-
+
@@ -98,6 +25,3 @@ -isAuthenticated()): ?> - - \ No newline at end of file diff --git a/app/views/anime/details.php b/app/views/anime/details.php index 7e6d4fc6..3460dd2b 100644 --- a/app/views/anime/details.php +++ b/app/views/anime/details.php @@ -1,12 +1,14 @@ +
-
-
- " alt="" /> +
+
-
-

- -

- + +
+

+ +

+
-

+

0): ?> -
-

Streaming on:

- - +
+

Streaming on:

+ + - + - - - - - - - - - - - + + + + + + + + picture("images/{$link['meta']['image']}", 'svg', [ + 'class' => 'streaming-logo', + 'width' => 50, + 'height' => 50, + 'alt' => "{$link['meta']['name']} logo", + ]); ?> +    + + + picture("images/{$link['meta']['image']}", 'svg', [ + 'class' => 'streaming-logo', + 'width' => 50, + 'height' => 50, + 'alt' => "{$link['meta']['name']} logo", + ]); ?> +    + + + + + + + + -
+

Trailer

- + +
-
+
0): ?> -
-

Characters

-
- $char): ?> - - - - +
+

Characters

+ +
+ + $list): ?> + /> + +
+ $char): ?> + + + + +
+ + +
+
+ + + 0): ?> + +
+

Staff

+ +
+ + $people): ?> +
+ /> + +
+ $person): ?> + + +
+
+ + +
\ No newline at end of file diff --git a/app/views/anime/edit.php b/app/views/anime/edit.php index d7831d86..b75a2423 100644 --- a/app/views/anime/edit.php +++ b/app/views/anime/edit.php @@ -17,7 +17,7 @@
- img($urlGenerator->assetUrl('images/anime', "{$item['anime']['id']}.jpg")) ?> + picture("images/anime/{$item['anime']['id']}.webp") ?>
@@ -79,7 +79,9 @@   - + + + @@ -87,11 +89,9 @@ -
-
-
- Danger Zone -
+ +
+ Danger Zone @@ -100,14 +100,15 @@ - -
+
+ - \ No newline at end of file diff --git a/app/views/anime/list.php b/app/views/anime/list.php index 489bcc59..429f6814 100644 --- a/app/views/anime/list.php +++ b/app/views/anime/list.php @@ -14,7 +14,7 @@ isAuthenticated()): ?> -   +   Title Airing Status @@ -72,17 +72,27 @@ - + picture("images/{$link['meta']['image']}", 'svg', [ + 'class' => 'streaming-logo', + 'width' => 50, + 'height' => 50, + 'alt' => "{$link['meta']['name']} logo", + ]); ?> - + picture("images/{$link['meta']['image']}", 'svg', [ + 'class' => 'streaming-logo', + 'width' => 50, + 'height' => 50, + 'alt' => "{$link['meta']['name']} logo", + ]); ?>

html($item['notes']) ?>

- + genres) ?> genres) ?> @@ -94,5 +104,4 @@ -isAuthenticated()) ? 'table_edit' : 'table' ?> - \ No newline at end of file + \ No newline at end of file diff --git a/app/views/character.php b/app/views/character.php deleted file mode 100644 index e28b1795..00000000 --- a/app/views/character.php +++ /dev/null @@ -1,128 +0,0 @@ - -
-
-
- " alt="" /> -
-
-

- -

-
-
- - -

Media

-
- -
-

Anime

-
- $anime): ?> - - -
-
- -
-
- -
-

Manga

-
- - $manga): ?> - - - -
-
- -
- - -
- 0): ?> -

Castings

- $entries): ?> -

- $casting): ?> -
- - - - - - - - - - - -
Cast MemberSeries
-
- -
- -
-
-
-
- -
- generate('anime.details', ['id' => $series['attributes']['slug']]); - $titles = Kitsu::filterTitles($series['attributes']); - ?> - - - - -
- -
-
- - - -
-
\ No newline at end of file diff --git a/app/views/character/details.php b/app/views/character/details.php new file mode 100644 index 00000000..cb3e8c78 --- /dev/null +++ b/app/views/character/details.php @@ -0,0 +1,221 @@ + +
+
+
+ picture("images/characters/{$data[0]['id']}-original.webp") ?> + +

Nicknames / Other names

+ +

+ + +
+
+

+ +

+ + +
+ +

+
+
+ + +

Media

+
+ + + + +
+ $anime): ?> + + +
+ + + + + + +
+ $manga): ?> + + +
+ +
+ + +
+ 0): ?> +

Castings

+ + + +

Voice Actors

+ +
+ + + $casting): ?> + type="radio" id="character-va" + name="character-vas" + /> + +
+ + + + + + $c): ?> + + + + + +
Cast MemberSeries
+ + +
+ + + +
+
+
+ + +
+ + + + $entries): ?> +

+ $casting): ?> +
+ + + + + + $c): ?> + + + + + +
Cast MemberSeries
+ + +
+ + + +
+
+ + + +
+
\ No newline at end of file diff --git a/app/views/collection/add.php b/app/views/collection/add.php index dc768568..59d93fce 100644 --- a/app/views/collection/add.php +++ b/app/views/collection/add.php @@ -9,7 +9,7 @@
-
+

@@ -39,5 +39,4 @@ - \ No newline at end of file diff --git a/app/views/collection/cover-item.php b/app/views/collection/cover-item.php index 0244dd83..5e740899 100644 --- a/app/views/collection/cover-item.php +++ b/app/views/collection/cover-item.php @@ -1,6 +1,5 @@
- " - alt=" cover image"/> + picture("images/anime/{$item['hummingbird_id']}.webp") ?>
\ No newline at end of file diff --git a/app/views/collection/cover.php b/app/views/collection/cover.php index 7441f79b..97eb1fc2 100644 --- a/app/views/collection/cover.php +++ b/app/views/collection/cover.php @@ -9,9 +9,8 @@ $items): ?> type="radio" id="collection-tab-" name="collection-tabs" /> - -
-

+ +
diff --git a/app/views/collection/edit.php b/app/views/collection/edit.php index b7ef28d1..e59947cb 100644 --- a/app/views/collection/edit.php +++ b/app/views/collection/edit.php @@ -3,27 +3,29 @@

Edit Anime Collection Item

- - - - - - - - + + + + + + + + + - - + + \ No newline at end of file diff --git a/app/views/collection/list.php b/app/views/collection/list.php index 049a63be..19f74239 100644 --- a/app/views/collection/list.php +++ b/app/views/collection/list.php @@ -10,10 +10,9 @@ $items): ?> type="radio" id="collection-tab-" name="collection-tabs"/> - -
-

-
+ +
+
isAuthenticated()): ?> @@ -24,6 +23,7 @@ + @@ -39,4 +39,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/views/footer.php b/app/views/footer.php index e1a73767..bd549b88 100644 --- a/app/views/footer.php +++ b/app/views/footer.php @@ -10,6 +10,12 @@ - +isAuthenticated()): ?> + + + + + + \ No newline at end of file diff --git a/app/views/header.php b/app/views/header.php index 4a7de884..5f43077f 100644 --- a/app/views/header.php +++ b/app/views/header.php @@ -21,9 +21,10 @@ - + +
get('whose_list') . "'s "; $lastSegment = $urlGenerator->lastSegment(); $extraSegment = $lastSegment === 'list' ? '/list' : ''; +$hasAnime = stripos($_SERVER['REQUEST_URI'], 'anime') !== FALSE; +$hasManga = stripos($_SERVER['REQUEST_URI'], 'manga') !== FALSE; ?>
Episode Length Show Type Age RatingGenres Notes
- \ No newline at end of file diff --git a/app/views/manga/cover.php b/app/views/manga/cover.php index d239b118..463906a9 100644 --- a/app/views/manga/cover.php +++ b/app/views/manga/cover.php @@ -18,18 +18,18 @@
isAuthenticated()): ?> -
0): ?> -

Characters

-
- $char): ?> - - - - -
+

Characters

+
+ + $list): ?> + /> + +
+ $char): ?> + + + + +
+ + +
+ + + 0): ?> +

Staff

+ +
+ + $people): ?> +
+ /> + +
+ $person): ?> + + +
+
+ + +
\ No newline at end of file diff --git a/app/views/manga/list.php b/app/views/manga/list.php index 523a8734..20877d0a 100644 --- a/app/views/manga/list.php +++ b/app/views/manga/list.php @@ -37,7 +37,7 @@ ]) ?>">Edit - + @@ -61,7 +61,7 @@ - + @@ -72,4 +72,4 @@ - \ No newline at end of file + diff --git a/app/views/person/character-mapping.php b/app/views/person/character-mapping.php new file mode 100644 index 00000000..e4daadbb --- /dev/null +++ b/app/views/person/character-mapping.php @@ -0,0 +1,67 @@ + +

Voice Acting Roles

+
+ + $characterList): ?> + type="radio" name="character-type-tabs" id="character-type-" /> + +
+ + + + + + $character): ?> + + + + + +
CharacterSeries
+ + +
+ $series): ?> + + +
+
+
+ + +
diff --git a/app/views/person/details.php b/app/views/person/details.php new file mode 100644 index 00000000..d6717ed3 --- /dev/null +++ b/app/views/person/details.php @@ -0,0 +1,67 @@ + +
+
+
+ picture("images/people/{$data['id']}-original.webp", 'jpg', ['class' => 'cover' ]) ?> +
+
+

+
+
+ + +
+

Castings

+
+ + $entries): ?> +
+ /> + + $casting): ?> + + +

+ +
+ $series): ?> + + +
+ +
+ + +
+
+ + + +
+ +
+ +
diff --git a/app/views/settings/_form.php b/app/views/settings/_form.php new file mode 100644 index 00000000..eccaf1ec --- /dev/null +++ b/app/views/settings/_form.php @@ -0,0 +1,24 @@ + + + $field): ?> + + +
+

+ +
+ +
+
+
+ field($fieldname, $field); ?> +
+ + field($fieldname, $field); ?> + + diff --git a/app/views/settings/settings.php b/app/views/settings/settings.php new file mode 100644 index 00000000..b51319aa --- /dev/null +++ b/app/views/settings/settings.php @@ -0,0 +1,66 @@ +isAuthenticated()) +{ + echo '

Not Authorized

'; + return; +} + +$sectionMapping = [ + 'anilist' => 'Anilist API Integration', + 'config' => 'General Settings', + 'cache' => 'Caching', + 'database' => 'Collection Database Settings', +]; + +$hiddenFields = []; +$nestedPrefix = 'config'; +?> + +
+
+ +
+ + + $fields): ?> + type="radio" id="settings-tab" + name="settings-tabs" + /> + +
+ + +
+ checkAuth(); ?> + +

Not Authorized.

+ a( + $url->generate('anilist-redirect'), + 'Link Anilist Account' + ) ?> + + get(['anilist', 'access_token_expires']); ?> +

+ Linked to Anilist. Your access token will expire around +

+ a( + $url->generate('anilist-redirect'), + 'Update Access Token' + ) ?> + + +
+ + +
+
+ + __toString() ?> + + +
+
+ + + + diff --git a/app/views/setup-check.php b/app/views/setup-check.php new file mode 100644 index 00000000..c22e5b0e --- /dev/null +++ b/app/views/setup-check.php @@ -0,0 +1,27 @@ +get('config')); +?> + + + + \ No newline at end of file diff --git a/app/views/me.php b/app/views/user/details.php similarity index 53% rename from app/views/me.php rename to app/views/user/details.php index 024f7f68..8fa17d66 100644 --- a/app/views/me.php +++ b/app/views/user/details.php @@ -1,27 +1,31 @@ - +
+

+ a( + "https://kitsu.io/users/{$attributes['slug']}", + $attributes['name'], [ + 'title' => 'View profile on Kitsu' + ]) + ?> +

+ +

html($attributes['about']) ?>

+
-
+
-
-
-
About:
-
html($attributes['about']) ?>
-
+ +
+

Favorites

+ -
+
\ No newline at end of file diff --git a/build/docker_install.sh b/build/docker_install.sh index 7ddee016..73163966 100644 --- a/build/docker_install.sh +++ b/build/docker_install.sh @@ -6,7 +6,7 @@ set -xe # Install git (the php image doesn't have it) which is required by composer -echo -e 'http://dl-cdn.alpinelinux.org/alpine/edge/main\nhttp://dl-cdn.alpinelinux.org/alpine/edge/community\nhttp://dl-cdn.alpinelinux.org/alpine/edge/testing' > /etc/apk/repositories -apk add --no-cache \ +# echo -e 'http://dl-cdn.alpinelinux.org/alpine/edge/main\nhttp://dl-cdn.alpinelinux.org/alpine/edge/community\nhttp://dl-cdn.alpinelinux.org/alpine/edge/testing' > /etc/apk/repositories +apk --update add --no-cache \ curl \ git diff --git a/build/header_comment.txt b/build/header_comment.txt index 5ca5be22..3f8b1752 100644 --- a/build/header_comment.txt +++ b/build/header_comment.txt @@ -1,15 +1,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/composer.json b/composer.json index 48f5b5ec..894d8ae6 100644 --- a/composer.json +++ b/composer.json @@ -23,13 +23,17 @@ "aura/router": "^3.0", "aura/session": "^2.0", "aviat/banker": "^1.0.0", - "aviat/ion": "^2.3.0", + "aviat/ion": "^2.4.1", + "ext-iconv": "*", + "ext-json": "*", + "ext-gd":"*", + "ext-pdo": "*", "maximebf/consolekit": "^1.0", "monolog/monolog": "^1.0", "psr/http-message": "~1.0", "psr/log": "~1.0", "yosymfony/toml": "^1.0", - "zendframework/zend-diactoros": "^1.3" + "zendframework/zend-diactoros": "^2.0.0" }, "require-dev": { "consolidation/robo": "~1.0", @@ -40,7 +44,7 @@ "phpstan/phpstan": "^0.9.1", "phpunit/phpunit": "^6.0", "roave/security-advisories": "dev-master", - "robmorgan/phinx": "^0.9.1", + "robmorgan/phinx": "^0.10.6", "sebastian/phpcpd": "^3.0", "spatie/phpunit-snapshot-assertions": "^1.2.0", "squizlabs/php_codesniffer": "^3.2.2", diff --git a/console b/console index 11b12688..1631e100 100755 --- a/console +++ b/console @@ -17,7 +17,13 @@ try (new Console([ 'cache:clear' => Command\CacheClear::class, 'cache:refresh' => Command\CachePrime::class, + 'clear:cache' => Command\CacheClear::class, + 'clear:thumbnails' => Command\ClearThumbnails::class, + 'refresh:cache' => Command\CachePrime::class, + 'refresh:thumbnails' => Command\UpdateThumbnails::class, + 'regenerate-thumbnails' => Command\UpdateThumbnails::class, 'lists:sync' => Command\SyncLists::class, + 'mal_id:check' => Command\MALIDCheck::class, ]))->run(); } catch (\Exception $e) diff --git a/index.php b/index.php index a23462f7..e249fd48 100644 --- a/index.php +++ b/index.php @@ -2,22 +2,26 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ namespace Aviat\AnimeClient; +use Aviat\AnimeClient\Types\Config as ConfigType; + use function Aviat\Ion\_dir; +setlocale(LC_CTYPE, 'en_US'); + // Work around the silly timezone error $timezone = ini_get('date.timezone'); if ($timezone === '' || $timezone === FALSE) @@ -43,16 +47,23 @@ $CONF_DIR = _dir($APP_DIR, 'config'); // ----------------------------------------------------------------------------- // Dependency Injection setup // ----------------------------------------------------------------------------- -$base_config = require $APPCONF_DIR . '/base_config.php'; +$baseConfig = require $APPCONF_DIR . '/base_config.php'; $di = require $APP_DIR . '/bootstrap.php'; $config = loadToml($CONF_DIR); -$config_array = array_merge($base_config, $config); -$container = $di($config_array); +$overrideFile = $CONF_DIR . '/admin-override.toml'; +$overrideConfig = file_exists($overrideFile) + ? loadTomlFile($overrideFile) + : []; + +$configArray = array_replace_recursive($baseConfig, $config, $overrideConfig); + +$checkedConfig = (new ConfigType($configArray))->toArray(); +$container = $di($checkedConfig); // Unset 'constants' -unset($APP_DIR, $APPCONF_DIR); +unset($APP_DIR, $CONF_DIR, $APPCONF_DIR); // ----------------------------------------------------------------------------- // Dispatch to the current route diff --git a/public/css/all.css b/public/css/all.css new file mode 100644 index 00000000..ff810e2e --- /dev/null +++ b/public/css/all.css @@ -0,0 +1,4 @@ +@import "./marx.css"; +@import "./general.css"; +@import "./components.css"; +@import "./responsive.css"; diff --git a/public/css/app.min.css b/public/css/app.min.css index c9cbdcaf..e59c4fc1 100644 --- a/public/css/app.min.css +++ b/public/css/app.min.css @@ -1 +1 @@ -:root{-moz-text-size-adjust:100%;-ms-text-size-adjust:100%;-webkit-box-sizing:border-box;-webkit-text-size-adjust:100%;box-sizing:border-box;cursor:default;font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,sans-serif;line-height:1.4;overflow-y:scroll;scroll-behavior:smooth;text-size-adjust:100%}audio:not([controls]){display:none}details{display:block}input[type=search]{-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}main{margin:0 auto;padding:0 1.6rem 1.6rem}main,pre,summary{display:block}pre{background:#efefef;color:#444;font-family:Anonymous Pro,Fira Code,Menlo,Monaco,Consolas,Courier New,monospace;font-size:1.4em;font-size:14px;font-size:1.4rem;margin:1.6rem 0;overflow:auto;padding:1.6rem;word-break:break-all;word-wrap:break-word}progress{display:inline-block}small{color:#777;font-size:75%}big{font-size:125%}template{display:none}textarea{border:.1rem solid #ccc;border-radius:0;display:block;margin-bottom:.8rem;overflow:auto;padding:.8rem;resize:vertical;vertical-align:middle}[hidden]{display:none}[unselectable]{-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}*,:after,:before{-webkit-box-sizing:inherit;border-style:solid;border-width:0;box-sizing:inherit}*{font-size:inherit;line-height:inherit;margin:0;padding:0}:after,:before{text-decoration:inherit;vertical-align:inherit}a{-webkit-transition:.25s ease;color:#1271db;text-decoration:none;transition:.25s ease}audio,canvas,iframe,img,svg,video{vertical-align:middle}button,input,select,textarea{border:.1rem solid #ccc;color:inherit;font-family:inherit;font-style:inherit;font-weight:inherit;min-height:1.4em}code,kbd,pre,samp{font-family:Anonymous Pro,Fira Code,Menlo,Monaco,Consolas,Courier New,monospace}table{border-collapse:collapse;border-spacing:0;margin-bottom:1.6rem}::-moz-selection{background-color:#b3d4fc;text-shadow:none}::selection{background-color:#b3d4fc;text-shadow:none}button::-moz-focus-inner{border:0}body{color:#444;font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,sans-serif;font-size:16px;font-size:1.6rem;font-style:normal;font-weight:400;padding:0}p{margin:0 0 1.6rem}h1,h2,h3,h4,h5,h6{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,sans-serif;margin:2rem 0 1.6rem}h1{border-bottom:.1rem solid rgba(0,0,0,.2);font-size:3.6em;font-size:36px;font-size:3.6rem}h1,h2{font-style:normal;font-weight:500}h2{font-size:3em;font-size:30px;font-size:3rem}h3{font-size:2.4em;font-size:24px;font-size:2.4rem;font-style:normal;font-weight:500;margin:1.6rem 0 .4rem}h4{font-size:1.8em;font-size:18px;font-size:1.8rem}h4,h5{font-style:normal;font-weight:600;margin:1.6rem 0 .4rem}h5{font-size:1.6em;font-size:16px;font-size:1.6rem}h6{color:#777;font-size:1.4em;font-style:normal;font-weight:600;margin:1.6rem 0 .4rem}code,h6{font-size:14px;font-size:1.4rem}code{background:#efefef;color:#444;font-family:Anonymous Pro,Fira Code,Menlo,Monaco,Consolas,Courier New,monospace;word-break:break-all;word-wrap:break-word}a:focus,a:hover{text-decoration:none}dl{margin-bottom:1.6rem}dd{margin-left:4rem}ol,ul{margin-bottom:.8rem;padding-left:2rem}blockquote{border-left:.2rem solid #1271db;font-style:italic;margin:1.6rem 0;padding-left:1.6rem}blockquote,figcaption{font-family:Georgia,Times,Times New Roman,serif}html{font-size:62.5%}article,aside,details,footer,header,main,section,summary{display:block;height:auto;margin:0 auto;width:100%}footer{clear:both;display:inline-block;float:left;max-width:100%;padding:1rem 0;text-align:center}footer,hr{border-top:.1rem solid rgba(0,0,0,.2)}hr{display:block;margin-bottom:1.6rem;width:100%}img{height:auto;vertical-align:baseline}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select{border:.1rem solid #ccc;border-radius:0;display:inline-block;padding:.8rem;vertical-align:middle}input:not([type]){-webkit-appearance:none;background-clip:padding-box;background-color:#fff;border:.1rem solid #ccc;border-radius:0;color:#444;display:inline-block;padding:.8rem;text-align:left}input[type=color]{padding:.8rem 1.6rem}input:not([type]):focus,input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus,select:focus,textarea:focus{border-color:#b3d4fc}input[type=checkbox],input[type=radio]{vertical-align:middle}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:1px thin solid #444;outline:.1rem thin solid #444}input:not([type])[disabled],input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled],select[disabled],textarea[disabled]{background-color:#efefef;color:#777;cursor:not-allowed}input[readonly],select[readonly],textarea[readonly]{background-color:#efefef;border-color:#ccc;color:#777}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{border-color:#e9322d;color:#b94a48}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#ff4136}select{background-color:#fff;border:.1rem solid #ccc}select[multiple]{height:auto}label{line-height:2}fieldset{border:0;margin:0;padding:.8rem 0}legend{border-bottom:.1rem solid #ccc;color:#444;display:block;margin-bottom:.8rem;padding:.8rem 0;width:100%}button,input[type=submit]{-moz-user-select:none;-ms-user-select:none;-webkit-transition:.25s ease;-webkit-user-drag:none;-webkit-user-select:none;border:.2rem solid #444;border-radius:0;color:#444;cursor:pointer;display:inline-block;margin-bottom:.8rem;margin-right:.4rem;padding:.8rem 1.6rem;text-align:center;text-decoration:none;text-transform:uppercase;transition:.25s ease;user-select:none;vertical-align:baseline}button a,input[type=submit] a{color:#444}button::-moz-focus-inner,input[type=submit]::-moz-focus-inner{padding:0}button:hover,input[type=submit]:hover{background:#444;border-color:#444;color:#fff}button:hover a,input[type=submit]:hover a{color:#fff}button:active,input[type=submit]:active{background:#6a6a6a;border-color:#6a6a6a;color:#fff}button:active a,input[type=submit]:active a{color:#fff}button:disabled,input[type=submit]:disabled{-webkit-box-shadow:none;box-shadow:none;cursor:not-allowed;opacity:.4}nav ul{list-style:none;margin:0;padding:0;text-align:center}nav ul li{display:inline}nav a{-webkit-transition:.25s ease;border-bottom:.2rem solid transparent;color:#444;padding:.8rem 1.6rem;text-decoration:none;transition:.25s ease}nav a:hover,nav li.selected a{border-color:rgba(0,0,0,.2)}nav a:active{border-color:rgba(0,0,0,.56)}caption{padding:.8rem 0}thead th{background:#efefef;color:#444}tr{background:#fff;margin-bottom:.8rem}td,th{border:.1rem solid #ccc;padding:.8rem 1.6rem;text-align:center;vertical-align:inherit}tfoot tr{background:none}tfoot td{color:#efefef;font-size:8px;font-size:.8rem;font-style:italic;padding:1.6rem .4rem}@media screen{[hidden~=screen]{display:inherit}[hidden~=screen]:not(:active):not(:focus):not(:target){clip:rect(0)!important;position:absolute!important}}@media screen and max-width 40rem{article,aside,section{clear:both;display:block;max-width:100%}img{margin-right:1.6rem}}.media[hidden],[hidden=hidden],template{display:none}body{margin:.5em}button{background:hsla(0,0%,100%,.65);margin:0}table{margin:0 auto}td{padding:1rem}thead td,thead th{padding:.5rem}input[type=number]{width:4em}tbody>tr:nth-child(odd){background:#ddd}a:active,a:hover{color:#7d12db}.bracketed{color:#12db18}#main-nav a,.bracketed{text-shadow:1px 1px 1px #000}.bracketed:before{content:"[\00a0"}.bracketed:after{content:"\00a0]"}.bracketed:active,.bracketed:hover{color:#db7d12}.grow-1{-ms-flex-positive:1;-webkit-box-flex:1;flex-grow:1}.flex-wrap{-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-no-wrap{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.flex-align-end{-ms-flex-align:end;-webkit-box-align:end;align-items:flex-end}.flex-align-space-around{-ms-flex-line-pack:distribute;align-content:space-around}.flex-justify-space-around{-ms-flex-pack:distribute;justify-content:space-around}.flex-self-center{-ms-flex-item-align:center;align-self:center}.flex{display:-webkit-box;display:-ms-flexbox;display:flex}.small-font{font-size:16px;font-size:1.6rem}.justify{text-align:justify}.align_center{text-align:center!important}.align_left{text-align:left!important}.align_right{text-align:right!important}.valign_top{vertical-align:top}.no_border{border:none}.media-wrap{margin:0 auto;position:relative;text-align:center}.danger{background-color:#ff4136;border-color:#924949;color:#fff}.danger:active,.danger:hover{background-color:#924949;border-color:#ff4136;color:#fff}.user-btn{border-color:#12db18;color:#12db18;padding:0 .5rem;text-shadow:1px 1px 1px #000}.user-btn:active,.user-btn:hover{background-color:#db7d12;border-color:#db7d12}.full_width{width:100%}#main-nav{border-bottom:.1rem solid rgba(0,0,0,.2);font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,sans-serif;font-size:3.6em;font-size:36px;font-size:3.6rem;font-style:normal;font-weight:500;margin:2rem 0 1.6rem}.cssload-loader{-webkit-perspective:780px;border-radius:50%;height:62px;left:calc(50% - 31px);perspective:780px;position:relative;width:62px}.cssload-inner{-webkit-box-sizing:border-box;border-radius:50%;box-sizing:border-box;height:100%;position:absolute;width:100%}.cssload-inner.cssload-one{-webkit-animation:cssload-rotate-one 1.15s linear infinite;animation:cssload-rotate-one 1.15s linear infinite;border-bottom:3px solid #000;left:0;top:0}.cssload-inner.cssload-two{-webkit-animation:cssload-rotate-two 1.15s linear infinite;animation:cssload-rotate-two 1.15s linear infinite;border-right:3px solid #000;right:0;top:0}.cssload-inner.cssload-three{-webkit-animation:cssload-rotate-three 1.15s linear infinite;animation:cssload-rotate-three 1.15s linear infinite;border-top:3px solid #000;bottom:0;right:0}@-webkit-keyframes cssload-rotate-one{0%{-webkit-transform:rotateX(35deg) rotateY(-45deg) rotate(0deg);transform:rotateX(35deg) rotateY(-45deg) rotate(0deg)}to{-webkit-transform:rotateX(35deg) rotateY(-45deg) rotate(1turn);transform:rotateX(35deg) rotateY(-45deg) rotate(1turn)}}@keyframes cssload-rotate-one{0%{-webkit-transform:rotateX(35deg) rotateY(-45deg) rotate(0deg);transform:rotateX(35deg) rotateY(-45deg) rotate(0deg)}to{-webkit-transform:rotateX(35deg) rotateY(-45deg) rotate(1turn);transform:rotateX(35deg) rotateY(-45deg) rotate(1turn)}}@-webkit-keyframes cssload-rotate-two{0%{-webkit-transform:rotateX(50deg) rotateY(10deg) rotate(0deg);transform:rotateX(50deg) rotateY(10deg) rotate(0deg)}to{-webkit-transform:rotateX(50deg) rotateY(10deg) rotate(1turn);transform:rotateX(50deg) rotateY(10deg) rotate(1turn)}}@keyframes cssload-rotate-two{0%{-webkit-transform:rotateX(50deg) rotateY(10deg) rotate(0deg);transform:rotateX(50deg) rotateY(10deg) rotate(0deg)}to{-webkit-transform:rotateX(50deg) rotateY(10deg) rotate(1turn);transform:rotateX(50deg) rotateY(10deg) rotate(1turn)}}@-webkit-keyframes cssload-rotate-three{0%{-webkit-transform:rotateX(35deg) rotateY(55deg) rotate(0deg);transform:rotateX(35deg) rotateY(55deg) rotate(0deg)}to{-webkit-transform:rotateX(35deg) rotateY(55deg) rotate(1turn);transform:rotateX(35deg) rotateY(55deg) rotate(1turn)}}@keyframes cssload-rotate-three{0%{-webkit-transform:rotateX(35deg) rotateY(55deg) rotate(0deg);transform:rotateX(35deg) rotateY(55deg) rotate(0deg)}to{-webkit-transform:rotateX(35deg) rotateY(55deg) rotate(1turn);transform:rotateX(35deg) rotateY(55deg) rotate(1turn)}}.sorting,.sorting_asc,.sorting_desc{vertical-align:text-bottom}.sorting:before{content:" ↕\00a0"}.sorting_asc:before{content:" ↑\00a0"}.sorting_desc:before{content:" ↓\00a0"}.form thead th,.form thead tr{background:inherit;border:0}.form tr>td:nth-child(odd){max-width:30%;min-width:25px;text-align:right}.form tr>td:nth-child(2n){text-align:left}.invisible tbody>tr:nth-child(odd){background:inherit}.invisible td,.invisible th,.invisible tr{border:0}.message{margin:.5em auto;padding:.5em;position:relative;width:95%}.message .close{height:1em;line-height:1em;position:absolute;right:.5em;text-align:center;top:.5em;vertical-align:middle;width:1em}.message:hover .close:after{content:"☒"}.message:hover{cursor:pointer}.message .icon{left:.5em;margin-right:1em;top:.5em}.message.error{background:#f3e6e6;border:1px solid #924949}.message.error .icon:after{content:"✘"}.message.success{background:#70dda9;border:1px solid #1f8454}.message.success .icon:after{content:"✔"}.message.info{background:#ffc;border:1px solid #bfbe3a}.message.info .icon:after{content:"⚠"}.character,.media,.small_character{display:inline-block;height:311px;margin:.25em .125em;position:relative;text-align:center;vertical-align:top;width:220px;z-index:0}.character>img,.media>img,.small_character>img{width:100%}.media .edit_buttons>button{margin:.5em auto}.media_metadata>div,.medium_metadata>div,.name,.row{color:#fff;padding:.25em .125em;text-align:right;text-shadow:2px 2px 2px #000;z-index:2}.age_rating,.media_type{text-align:left}.media>.media_metadata{bottom:0;position:absolute;right:0}.media>.medium_metadata{bottom:0;left:0;position:absolute}.media>.name{position:absolute;top:0}.media>.name a{-webkit-transition:none;transition:none}.media .name a:before{content:"";display:block;height:311px;left:0;position:absolute;top:0;width:220px;z-index:-1}.media-list .media:hover .name a:before{background:rgba(0,0,0,.75)}.media>.name span.canonical{font-weight:700}.media>.name small{font-weight:400}.media:hover .name{background:rgba(0,0,0,.75)}.media-list .media>.name a:hover,.media-list .media>.name a:hover small{color:#1271db}.media:hover>.edit_buttons[hidden],.media:hover>button[hidden]{-webkit-transition:.25s ease;display:block;transition:.25s ease}.media:hover{-webkit-transition:.25s ease;transition:.25s ease}.character>.name a,.character>.name a small,.media>.name a,.media>.name a small,.small_character>.name a,.small_character>.name a small{background:none;color:#fff;text-shadow:2px 2px 2px #000}.anime .name,.manga .name{background:#000;background:rgba(0,0,0,.45);padding:.5em .25em;text-align:center;width:100%}.anime .age_rating,.anime .airing_status,.anime .completion,.anime .delete,.anime .edit,.anime .media_type,.anime .user_rating{background:none;text-align:center}.anime .table,.manga .table{bottom:0;left:0;position:absolute;width:100%}.anime .row,.manga .row{-ms-flex-line-pack:distribute;-ms-flex-pack:distribute;align-content:space-around;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:space-around;padding:0 inherit;text-align:center;width:100%}.anime .row>span,.manga .row>span{text-align:left;z-index:2}.anime .row>div,.manga .row>div{-ms-flex-item-align:center;align-self:center;display:flex-item;font-size:.8em;text-align:center;vertical-align:middle;z-index:2}.anime .media>button.plus_one{border-color:hsla(0,0%,100%,.65);left:44px;left:calc(50% - 66.5px);position:absolute;top:138px;top:calc(50% - 21.5px);z-index:50}.anime .media>button.plus_one:hover{background:#888;color:hsla(0,0%,100%,.65)}.anime .media>button.plus_one:active{background:#444}.manga .row{padding:1px}.manga .media{height:310px;margin:.25em}.manga .media>.edit_buttons{left:43.5px;left:calc(50% - 66.5px);position:absolute;top:86px;top:calc(50% - 22.4px);z-index:40}.manga .media>.edit_buttons button{border-color:hsla(0,0%,100%,.65)}.manga .media>.edit_buttons:hover button{background:#888;color:hsla(0,0%,100%,.65)}.manga .media>.edit_buttons button:active{background:#444}.media.search>.name{background-color:#555;background-color:rgba(0,0,0,.35);background-repeat:no-repeat;background-size:cover;background-size:contain}.big-check{display:none}.big-check:checked+label{-webkit-transition:.25s ease;background:rgba(0,0,0,.75);transition:.25s ease}.big-check:checked+label:after{color:#adff2f;content:"✓";font-size:15em;font-size:150px;font-size:15rem;height:100%;left:0;position:absolute;text-align:center;top:147px;width:100%;z-index:5}#series_list article.media{position:relative}#series_list .name,#series_list .name label{display:block;height:100%;left:0;line-height:1.25em;position:absolute;top:0;vertical-align:middle;width:100%}#series_list .name small{color:#fff}.details{font-size:inherit;margin:1.5rem auto 0;padding:1rem}.description{max-width:800px;max-width:80rem}.fixed{max-width:930px;max-width:93rem}.details .cover{display:block;width:284px}.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){text-align:right;white-space:nowrap;width:1%}.details .media_details td:nth-child(2n){text-align:left}.character,.small_character{height:350px;vertical-align:middle;white-space:nowrap;width:225px}.character:hover .name,.small_character:hover .name{background:rgba(0,0,0,.8)}.small_character a{display:inline-block;height:100%;width:100%}.character .name,.small_character .name{bottom:0;left:0;position:absolute;z-index:10}.character img,.small_character img{-webkit-transform:translateY(-50%);position:relative;top:50%;transform:translateY(-50%);width:100%;z-index:5}.min-table{margin-left:0;min-width:0}.small_character{height:250px;width:160px}.user-page .media-wrap{text-align:left}.media a{display:inline-block;height:100%;width:100%}@media screen and (max-width:40em){nav a{line-height:4em;line-height:4rem}.media{margin:2px 0}main{padding:0 .5rem .5rem}}.streaming-logo{height:50px;vertical-align:middle;width:50px}.cover_streaming_link{display:none}.media:hover .cover_streaming_link{display:block}.cover_streaming_link .streaming-logo{-webkit-filter:drop-shadow(0 -1px 4px #fff);filter:drop-shadow(0 -1px 4px #fff);height:20px;width:20px}#loading-shadow{background:rgba(0,0,0,.8);z-index:500}#loading-shadow,#loading-shadow .loading-wrapper{height:100%;left:0;position:fixed;top:0;width:100%}#loading-shadow .loading-wrapper{-ms-flex-align:center;-ms-flex-pack:center;-webkit-box-align:center;-webkit-box-pack:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:center;z-index:501}#loading-shadow .loading-content{color:#fff;position:relative}.loading-content .cssload-inner.cssload-one,.loading-content .cssload-inner.cssload-three,.loading-content .cssload-inner.cssload-two{border-color:#fff}.tabs{-ms-flex-wrap:wrap;-webkit-box-shadow:0 48px 80px -32px rgba(0,0,0,.3);background:#efefef;box-shadow:0 48px 80px -32px rgba(0,0,0,.3);display:-webkit-box;display:-ms-flexbox;display:flex;flex-wrap:wrap;margin-top:1.5em}.tabs label{-webkit-transition:background .1s,color .1s;background:#e5e5e5;border:1px solid #e5e5e5;color:#7f7f7f;cursor:pointer;font-size:18px;font-weight:700;padding:20px 30px;transition:background .1s,color .1s;width:100%}.tabs label:hover{background:#d8d8d8}.tabs label:active{background:#ccc}.tabs [type=radio]:focus+label{-webkit-box-shadow:inset 0 0 0 3px #2aa1c0;box-shadow:inset 0 0 0 3px #2aa1c0;z-index:1}.tabs [type=radio]{opacity:0;position:absolute}.tabs [type=radio]:checked+label{background:#fff;border-bottom:1px solid #fff;color:#000}.tabs [type=radio]:checked+label+.content{background:#fff;border:1px solid #e5e5e5;border-top:0;display:block;padding:20px 30px 30px;width:100%}.tabs .content{display:none}@media (min-width:600px){.tabs label{width:auto}.tabs .content{-ms-flex-order:99;-webkit-box-ordinal-group:100;order:99}} \ No newline at end of file +:root{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-box-sizing:border-box;box-sizing:border-box;cursor:default;font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,sans-serif;line-height:1.4;overflow-y:scroll;-moz-text-size-adjust:100%;text-size-adjust:100%;scroll-behavior:smooth}audio:not([controls]){display:none}details{display:block}input[type=search]{-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}main{margin:0 auto;padding:0 1.6rem 1.6rem}main,pre,summary{display:block}pre{background:#efefef;color:#444;font-family:Anonymous Pro,Fira Code,Menlo,Monaco,Consolas,Courier New,monospace;font-size:1.4em;font-size:14px;font-size:1.4rem;margin:1.6rem 0;overflow:auto;padding:1.6rem;word-break:break-all;word-wrap:break-word}progress{display:inline-block}small{color:#777;font-size:75%}big{font-size:125%}template{display:none}textarea{border:.1rem solid #ccc;border-radius:0;display:block;margin-bottom:.8rem;overflow:auto;padding:.8rem;resize:vertical;vertical-align:middle}[hidden]{display:none}[unselectable]{-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}*,:after,:before{border-style:solid;border-width:0;-webkit-box-sizing:inherit;box-sizing:inherit}*{font-size:inherit;line-height:inherit;margin:0;padding:0}:after,:before{text-decoration:inherit;vertical-align:inherit}a{-webkit-transition:.25s ease;color:#1271db;text-decoration:none;transition:.25s ease}audio,canvas,iframe,img,svg,video{vertical-align:middle}button,input,select,textarea{border:.1rem solid #ccc;color:inherit;font-family:inherit;font-style:inherit;font-weight:inherit;min-height:1.4em}code,kbd,pre,samp{font-family:Anonymous Pro,Fira Code,Menlo,Monaco,Consolas,Courier New,monospace}table{border-collapse:collapse;border-spacing:0;margin-bottom:1.6rem}::-moz-selection{background-color:#b3d4fc;text-shadow:none}::selection{background-color:#b3d4fc;text-shadow:none}button::-moz-focus-inner{border:0}body{color:#444;font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,sans-serif;font-size:16px;font-size:1.6rem;font-style:normal;font-weight:400;padding:0}p{margin:0 0 1.6rem}h1,h2,h3,h4,h5,h6{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,sans-serif;margin:2rem 0 1.6rem}h1{border-bottom:.1rem solid rgba(0,0,0,.2);font-size:3.6em;font-size:36px;font-size:3.6rem}h1,h2{font-style:normal;font-weight:500}h2{font-size:3em;font-size:30px;font-size:3rem}h3{font-size:2.4em;font-size:24px;font-size:2.4rem;font-style:normal;font-weight:500;margin:1.6rem 0 .4rem}h4{font-size:1.8em;font-size:18px;font-size:1.8rem}h4,h5{font-style:normal;font-weight:600;margin:1.6rem 0 .4rem}h5{font-size:1.6em;font-size:16px;font-size:1.6rem}h6{color:#777;font-size:1.4em;font-style:normal;font-weight:600;margin:1.6rem 0 .4rem}code,h6{font-size:14px;font-size:1.4rem}code{background:#efefef;color:#444;font-family:Anonymous Pro,Fira Code,Menlo,Monaco,Consolas,Courier New,monospace;word-break:break-all;word-wrap:break-word}a:focus,a:hover{text-decoration:none}dl{margin-bottom:1.6rem}dd{margin-left:4rem}ol,ul{margin-bottom:.8rem;padding-left:2rem}blockquote{border-left:.2rem solid #1271db;font-style:italic;margin:1.6rem 0;padding-left:1.6rem}blockquote,figcaption{font-family:Georgia,Times,Times New Roman,serif}html{font-size:62.5%}article,aside,details,footer,header,main,section,summary{display:block;height:auto;margin:0 auto;width:100%}footer{clear:both;display:inline-block;float:left;max-width:100%;padding:1rem 0;text-align:center}footer,hr{border-top:.1rem solid rgba(0,0,0,.2)}hr{display:block;margin-bottom:1.6rem;width:100%}img{height:auto;vertical-align:baseline}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select{border:.1rem solid #ccc;border-radius:0;display:inline-block;padding:.8rem;vertical-align:middle}input:not([type]){-webkit-appearance:none;background-clip:padding-box;background-color:#fff;border:.1rem solid #ccc;border-radius:0;color:#444;display:inline-block;padding:.8rem;text-align:left}input[type=color]{padding:.8rem 1.6rem}input:not([type]):focus,input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus,select:focus,textarea:focus{border-color:#b3d4fc}input[type=checkbox],input[type=radio]{vertical-align:middle}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:1px thin solid #444;outline:.1rem thin solid #444}input:not([type])[disabled],input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled],select[disabled],textarea[disabled]{background-color:#efefef;color:#777;cursor:not-allowed}input[readonly],select[readonly],textarea[readonly]{background-color:#efefef;border-color:#ccc;color:#777}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{border-color:#e9322d;color:#b94a48}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#ff4136}select{background-color:#fff;border:.1rem solid #ccc}select[multiple]{height:auto}label{line-height:2}fieldset{border:0;margin:0;padding:.8rem 0}legend{border-bottom:.1rem solid #ccc;color:#444;display:block;margin-bottom:.8rem;padding:.8rem 0;width:100%}button,input[type=submit]{-moz-user-select:none;-ms-user-select:none;-webkit-transition:.25s ease;-webkit-user-drag:none;-webkit-user-select:none;border:.2rem solid #444;border-radius:0;color:#444;cursor:pointer;display:inline-block;margin-bottom:.8rem;margin-right:.4rem;padding:.8rem 1.6rem;text-align:center;text-decoration:none;text-transform:uppercase;transition:.25s ease;user-select:none;vertical-align:baseline}button a,input[type=submit] a{color:#444}button::-moz-focus-inner,input[type=submit]::-moz-focus-inner{padding:0}button:hover,input[type=submit]:hover{background:#444;border-color:#444;color:#fff}button:hover a,input[type=submit]:hover a{color:#fff}button:active,input[type=submit]:active{background:#6a6a6a;border-color:#6a6a6a;color:#fff}button:active a,input[type=submit]:active a{color:#fff}button:disabled,input[type=submit]:disabled{-webkit-box-shadow:none;box-shadow:none;cursor:not-allowed;opacity:.4}nav ul{list-style:none;margin:0;padding:0;text-align:center}nav ul li{display:inline}nav a{-webkit-transition:.25s ease;border-bottom:.2rem solid transparent;color:#444;padding:.8rem 1.6rem;text-decoration:none;transition:.25s ease}nav a:hover,nav li.selected a{border-color:rgba(0,0,0,.2)}nav a:active{border-color:rgba(0,0,0,.56)}caption{padding:.8rem 0}thead th{background:#efefef;color:#444}tr{background:#fff;margin-bottom:.8rem}td,th{border:.1rem solid #ccc;padding:.8rem 1.6rem;text-align:center;vertical-align:inherit}tfoot tr{background:none}tfoot td{color:#efefef;font-size:8px;font-size:.8rem;font-style:italic;padding:1.6rem .4rem}@media screen{[hidden~=screen]{display:inherit}[hidden~=screen]:not(:active):not(:focus):not(:target){clip:rect(0)!important;position:absolute!important}}@media screen and max-width 40rem{article,aside,section{clear:both;display:block;max-width:100%}img{margin-right:1.6rem}}.media[hidden],[hidden=hidden],template{display:none}body{margin:.5em}button{background:hsla(0,0%,100%,.65);margin:0}table{-webkit-box-shadow:0 48px 80px -32px rgba(0,0,0,.3);box-shadow:0 48px 80px -32px rgba(0,0,0,.3);margin:0 auto}td{padding:1rem}thead td,thead th{padding:.5rem}input[type=number]{width:4em}tbody>tr:nth-child(odd){background:#ddd}a:active,a:hover{color:#7d12db}iframe{display:block;margin:0 auto}.bracketed{color:#12db18}#main-nav a,.bracketed{text-shadow:1px 1px 1px #000}.bracketed:before{content:"[\00a0"}.bracketed:after{content:"\00a0]"}.bracketed:active,.bracketed:hover{color:#db7d12}.grow-1{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.flex-wrap{-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-no-wrap{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.flex-align-start{-ms-flex-line-pack:start;align-content:flex-start}.flex-align-end{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}.flex-align-space-around{-ms-flex-line-pack:distribute;align-content:space-around}.flex-justify-start{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.flex-justify-space-around{-ms-flex-pack:distribute;justify-content:space-around}.flex-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.flex-self-center{-ms-flex-item-align:center;align-self:center}.flex-space-evenly{-webkit-box-pack:space-evenly;-ms-flex-pack:space-evenly;justify-content:space-evenly}.flex{display:inline-block;display:-webkit-box;display:-ms-flexbox;display:flex}.small-font{font-size:16px;font-size:1.6rem}.justify{text-align:justify}.align-center{text-align:center!important}.align-left{text-align:left!important}.align-right{text-align:right!important}.valign-top{vertical-align:top}.no-border{border:none}.media-wrap{text-align:center;margin:0 auto;position:relative}.media-wrap-flex{display:inline-block;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-line-pack:space-evenly;align-content:space-evenly;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;position:relative}td .media-wrap-flex{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.danger{background-color:#ff4136;border-color:#924949;color:#fff}.danger:active,.danger:hover{background-color:#924949;border-color:#ff4136;color:#fff}.user-btn{border-color:#12db18;color:#12db18;text-shadow:1px 1px 1px #000;padding:0 .5rem}.user-btn:active,.user-btn:hover{border-color:#db7d12;background-color:#db7d12}.full-width{width:100%}.full-height{max-height:none}.toph{margin-top:0}#main-nav{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,sans-serif;margin:2rem 0 1.6rem;border-bottom:.1rem solid rgba(0,0,0,.2);font-size:3.6em;font-size:36px;font-size:3.6rem;font-style:normal;font-weight:500}.sorting,.sorting-asc,.sorting-desc{vertical-align:text-bottom}.sorting:before{content:" ↕\00a0"}.sorting-asc:before{content:" ↑\00a0"}.sorting-desc:before{content:" ↓\00a0"}.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(2n){text-align:left}.invisible tbody>tr:nth-child(odd){background:inherit}.borderless,.borderless td,.borderless th,.borderless tr,.invisible td,.invisible th,.invisible tr{-webkit-box-shadow:none;box-shadow:none;border:0}.message,.static-message{position:relative;margin:.5em auto;padding:.5em;width:95%}.message .close{width:1em;height:1em;position:absolute;right:.5em;top:.5em;text-align:center;vertical-align:middle;line-height:1em}.message:hover .close:after{content:"☒"}.message:hover{cursor:pointer}.message .icon{left:.5em;top:.5em;margin-right:1em}.message.error,.static-message.error{border:1px solid #924949;background:#f3e6e6}.message.error .icon:after{content:"✘"}.message.success,.static-message.success{border:1px solid #1f8454;background:#70dda9}.message.success .icon:after{content:"✔"}.message.info,.static-message.info{border:1px solid #bfbe3a;background:#ffc}.message.info .icon:after{content:"⚠"}.character,.media,.small-character{position:relative;vertical-align:top;display:inline-block;text-align:center;width:220px;height:311px;margin:.25em .125em;z-index:0;background:rgba(0,0,0,.15)}.details picture.cover,picture.cover{display:inline;display:initial;width:100%}.character>img,.media>img,.small-character>img{width:100%}.media .edit-buttons>button{margin:.5em auto}.media-metadata>div,.medium-metadata>div,.name,.row{text-shadow:2px 2px 2px #000;color:#fff;padding:.25em .125em;text-align:right;z-index:2}.age-rating,.media-type{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}.media>.name a{display:inline-block;-webkit-transition:none;transition:none}.media .name a:before{content:"";display:block;height:311px;left:0;position:absolute;top:0;width:220px;z-index:-1}.media-list .media:hover .name a:before{background:rgba(0,0,0,.75)}.media>.name span.canonical{font-weight:700}.media>.name small{font-weight:400}.media:hover .name{background:rgba(0,0,0,.75)}.media-list .media>.name a:hover,.media-list .media>.name a:hover small{color:#1271db}.media:hover>.edit-buttons[hidden],.media:hover>button[hidden]{-webkit-transition:.25s ease;transition:.25s ease;display:block}.media:hover{-webkit-transition:.25s ease;transition:.25s ease}.character>.name a,.character>.name a small,.media>.name a,.media>.name a small,.small-character>.name a,.small-character>.name a small{background:none;color:#fff;text-shadow:2px 2px 2px #000}.anime .name,.manga .name{background:#000;background:rgba(0,0,0,.45);text-align:center;width:100%;padding:.5em .25em}.anime .age-rating,.anime .airing-status,.anime .completion,.anime .delete,.anime .edit,.anime .media-type,.anime .user-rating{background:none;text-align:center}.anime .table,.manga .table{position:absolute;bottom:0;left:0;width:100%}.anime .row,.manga .row{width:100%;display:inline-block;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-line-pack:distribute;align-content:space-around;-ms-flex-pack:distribute;justify-content:space-around;text-align:center;padding:0 inherit}.anime .row>span,.manga .row>span{text-align:left;z-index:2}.anime .row>div,.manga .row>div{font-size:.8em;display:inline-block;display:flex-item;-ms-flex-item-align:center;align-self:center;text-align:center;vertical-align:middle;z-index:2}.anime .media>button.plus-one{border-color:hsla(0,0%,100%,.65);position:absolute;top:138px;top:calc(50% - 21.5px);left:44px;left:calc(50% - 66.5px);z-index:50}.anime .media>button.plus-one:hover{color:hsla(0,0%,100%,.65);background:#888}.anime .media>button.plus-one:active{background:#444}.manga .row{padding:1px}.manga .media{height:310px;margin:.25em}.manga .media>.edit-buttons{position:absolute;top:86px;top:calc(50% - 22.4px);left:43.5px;left:calc(50% - 66.5px);z-index:40}.manga .media>.edit-buttons button{border-color:hsla(0,0%,100%,.65)}.manga .media>.edit-buttons:hover button{color:hsla(0,0%,100%,.65);background:#888}.manga .media>.edit-buttons button:active{background:#444}.media.search>.name{background-color:#555;background-color:rgba(0,0,0,.35);background-size:cover;background-size:contain;background-repeat:no-repeat}.media.search>.row{z-index:6}.big-check,.mal-check{display:none}.big-check:checked+label{-webkit-transition:.25s ease;transition:.25s ease;background:rgba(0,0,0,.75)}.big-check:checked+label:after{content:"✓";font-size:15em;font-size:150px;font-size:15rem;text-align:center;color:#adff2f;position:absolute;top:147px;left:0;height:100%;width:100%;z-index:5}#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{margin:1.5rem auto 0;padding:1rem;font-size:inherit}.description{max-width:800px;max-width:80rem}.fixed{max-width:80%;margin:0 auto}.fixed .text{max-width:40em}.details .cover{display:block}.details .flex>*{margin:1rem}.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(2n){text-align:left}.details a h1,.details a h2{margin-top:0}.character,.person,.small-character{width:225px;height:350px;vertical-align:middle;white-space:nowrap;position:relative}.person{width:225px;height:338px}.small-person{width:200px;height:300px}.character a{height:350px}.character:hover .name,.small-character:hover .name{background:rgba(0,0,0,.8)}.small-character a{display:inline-block;width:100%;height:100%}.character .name,.small-character .name{position:absolute;bottom:0;left:0;z-index:10}.character img,.character picture,.person img,.person picture,.small-character img,.small-character picture{position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);z-index:5;max-height:350px;max-width:225px}.person img,.person picture{max-height:338px}.small-person img,.small-person picture{max-height:300px;max-width:200px}.min-table{min-width:0;margin-left:0}.max-table{min-width:100%;margin:0}aside.info{max-width:33%}.fixed aside.info{max-width:390px}aside.info img,aside.info picture{display:block;margin:0 auto}aside.info+article{max-width:66%}.small-character{width:160px;height:250px}.small-character img,.small-character picture{max-height:250px;max-width:160px}.user-page .media-wrap{text-align:left}.media a{display:inline-block;width:100%;height:100%}.streaming-logo{width:50px;height:50px;vertical-align:middle}.cover-streaming-link{display:none}.media:hover .cover-streaming-link{display:block}.cover-streaming-link .streaming-logo{width:20px;height:20px;-webkit-filter:drop-shadow(0 -1px 4px #fff);filter:drop-shadow(0 -1px 4px #fff)}.settings.form .content article{margin:1em;display:inline-block;width:auto}.responsive-iframe{margin-top:1em;overflow:hidden;padding-bottom:56.25%;position:relative;height:0}.responsive-iframe iframe{left:0;top:0;height:100%;width:100%;position:absolute}.cssload-loader{position:relative;left:calc(50% - 31px);width:62px;height:62px;border-radius:50%;-webkit-perspective:780px;perspective:780px}.cssload-inner{position:absolute;width:100%;height:100%;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:50%}.cssload-inner.cssload-one{left:0;top:0;-webkit-animation:cssload-rotate-one 1.15s linear infinite;animation:cssload-rotate-one 1.15s linear infinite;border-bottom:3px solid #000}.cssload-inner.cssload-two{right:0;top:0;-webkit-animation:cssload-rotate-two 1.15s linear infinite;animation:cssload-rotate-two 1.15s linear infinite;border-right:3px solid #000}.cssload-inner.cssload-three{right:0;bottom:0;-webkit-animation:cssload-rotate-three 1.15s linear infinite;animation:cssload-rotate-three 1.15s linear infinite;border-top:3px solid #000}@-webkit-keyframes cssload-rotate-one{0%{-webkit-transform:rotateX(35deg) rotateY(-45deg) rotate(0deg);transform:rotateX(35deg) rotateY(-45deg) rotate(0deg)}to{-webkit-transform:rotateX(35deg) rotateY(-45deg) rotate(1turn);transform:rotateX(35deg) rotateY(-45deg) rotate(1turn)}}@keyframes cssload-rotate-one{0%{-webkit-transform:rotateX(35deg) rotateY(-45deg) rotate(0deg);transform:rotateX(35deg) rotateY(-45deg) rotate(0deg)}to{-webkit-transform:rotateX(35deg) rotateY(-45deg) rotate(1turn);transform:rotateX(35deg) rotateY(-45deg) rotate(1turn)}}@-webkit-keyframes cssload-rotate-two{0%{-webkit-transform:rotateX(50deg) rotateY(10deg) rotate(0deg);transform:rotateX(50deg) rotateY(10deg) rotate(0deg)}to{-webkit-transform:rotateX(50deg) rotateY(10deg) rotate(1turn);transform:rotateX(50deg) rotateY(10deg) rotate(1turn)}}@keyframes cssload-rotate-two{0%{-webkit-transform:rotateX(50deg) rotateY(10deg) rotate(0deg);transform:rotateX(50deg) rotateY(10deg) rotate(0deg)}to{-webkit-transform:rotateX(50deg) rotateY(10deg) rotate(1turn);transform:rotateX(50deg) rotateY(10deg) rotate(1turn)}}@-webkit-keyframes cssload-rotate-three{0%{-webkit-transform:rotateX(35deg) rotateY(55deg) rotate(0deg);transform:rotateX(35deg) rotateY(55deg) rotate(0deg)}to{-webkit-transform:rotateX(35deg) rotateY(55deg) rotate(1turn);transform:rotateX(35deg) rotateY(55deg) rotate(1turn)}}@keyframes cssload-rotate-three{0%{-webkit-transform:rotateX(35deg) rotateY(55deg) rotate(0deg);transform:rotateX(35deg) rotateY(55deg) rotate(0deg)}to{-webkit-transform:rotateX(35deg) rotateY(55deg) rotate(1turn);transform:rotateX(35deg) rotateY(55deg) rotate(1turn)}}#loading-shadow{background:rgba(0,0,0,.8);z-index:500}#loading-shadow,#loading-shadow .loading-wrapper{position:fixed;top:0;left:0;width:100%;height:100%}#loading-shadow .loading-wrapper{z-index:501;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}#loading-shadow .loading-content{position:relative;color:#fff}.loading-content .cssload-inner.cssload-one,.loading-content .cssload-inner.cssload-three,.loading-content .cssload-inner.cssload-two{border-color:#fff}.tabs{display:inline-block;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;background:#efefef;-webkit-box-shadow:0 48px 80px -32px rgba(0,0,0,.3);box-shadow:0 48px 80px -32px rgba(0,0,0,.3);margin-top:1.5em}.tabs>label{border:1px solid #e5e5e5;width:100%;padding:20px 30px;background:#e5e5e5;cursor:pointer;font-weight:700;font-size:18px;color:#7f7f7f;-webkit-transition:background .1s,color .1s;transition:background .1s,color .1s}.tabs>label:hover{background:#d8d8d8}.tabs>label:active{background:#ccc}.tabs>[type=radio]:focus+label{-webkit-box-shadow:inset 0 0 0 3px #2aa1c0;box-shadow:inset 0 0 0 3px #2aa1c0;z-index:1}.tabs>[type=radio]{position:absolute;opacity:0}.tabs>[type=radio]:checked+label{border-bottom:1px solid #fff;background:#fff;color:#000}.tabs>[type=radio]:checked+label+.content{display:block}.tabs .content,.tabs>[type=radio]:checked+label+.content{border:1px solid #e5e5e5;border-top:0;padding:15px;background:#fff;width:100%;margin:0 auto;overflow:auto}.tabs .content{display:none;max-height:950px}.tabs .content.full-height{max-height:none}@media (min-width:800px){.tabs>label{width:auto}.tabs .content{-webkit-box-ordinal-group:100;-ms-flex-order:99;order:99}}.vertical-tabs{border:1px solid #e5e5e5;-webkit-box-shadow:0 48px 80px -32px rgba(0,0,0,.3);box-shadow:0 48px 80px -32px rgba(0,0,0,.3);margin:0 auto;position:relative;width:100%}.vertical-tabs input[type=radio]{position:absolute;opacity:0}.vertical-tabs .tab{display:inline-block;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:nowrap;flex-wrap:nowrap}.vertical-tabs .tab,.vertical-tabs .tab label{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.vertical-tabs .tab label{background:#e5e5e5;border:1px solid #e5e5e5;color:#7f7f7f;cursor:pointer;font-size:18px;font-weight:700;padding:0 20px;width:28%}.vertical-tabs .tab label:hover{background:#d8d8d8}.vertical-tabs .tab label:active{background:#ccc}.vertical-tabs .tab .content{display:none;border:1px solid #e5e5e5;border-left:0;border-right:0;max-height:950px;overflow:auto}.vertical-tabs .tab .content.full-height{max-height:none}.vertical-tabs [type=radio]:checked+label{border:0;background:#fff;color:#000;width:38%}.vertical-tabs [type=radio]:focus+label{-webkit-box-shadow:inset 0 0 0 3px #2aa1c0;box-shadow:inset 0 0 0 3px #2aa1c0;z-index:1}.vertical-tabs [type=radio]:checked~.content{display:block}@media screen and (max-width:1100px){.flex{-ms-flex-wrap:wrap;flex-wrap:wrap}.fixed aside.info,.fixed aside.info+article,aside.info,aside.info+article{max-width:none;width:100%}}@media screen and (max-width:800px){*{max-width:none}table{-webkit-box-shadow:none;box-shadow:none}.details .flex>*,body{margin:0}table,table.align-center,table .align-right,table td,table th{border:0;display:block;margin:0 auto;text-align:left;width:100%}table tbody{width:100%}table td{display:inline-block}table.media-details td{display:block;text-align:left!important}table thead{display:none}.details .media-details td:nth-child(odd){font-weight:700;width:100%}table.streaming-links tr td:not(:first-child){display:none}}@media screen and (max-width:40em){nav a{line-height:4em;line-height:4rem}img,picture{width:100%}main{padding:0 .5rem .5rem}.media{margin:2px 0}.details{padding:.5rem}.tabs>[type=radio]:checked+label{background:#fff}.vertical-tabs .tab{-ms-flex-wrap:wrap;flex-wrap:wrap}.tabs .content,.tabs>[type=radio]:checked+label+.content,.vertical-tabs .tab .content{display:block;border:0;max-height:none}.tabs>[type=radio]:checked+label,.tabs>label,.tabs>label:active,.tabs>label:hover,.vertical-tabs .tab label,.vertical-tabs .tab label:active,.vertical-tabs .tab label:hover,.vertical-tabs [type=radio]:checked+label,.vertical-tabs [type=radio]:focus+label{background:#fff;border:0;width:100%;cursor:default;color:#000}} \ No newline at end of file diff --git a/public/css/components.css b/public/css/components.css new file mode 100644 index 00000000..e50deaaf --- /dev/null +++ b/public/css/components.css @@ -0,0 +1,264 @@ +/* ----------------------------------------------------------------------------- + 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); + } +} + +/* ---------------------------------------------------------------------------- + Loading overlay +-----------------------------------------------------------------------------*/ +#loading-shadow { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + z-index: 500; +} + +#loading-shadow .loading-wrapper { + position: fixed; + z-index: 501; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +#loading-shadow .loading-content { + position: relative; + color: #fff +} + +.loading-content .cssload-inner.cssload-one, +.loading-content .cssload-inner.cssload-two, +.loading-content .cssload-inner.cssload-three { + border-color: #fff +} + +/* ---------------------------------------------------------------------------- +CSS Tabs +-----------------------------------------------------------------------------*/ +.tabs { + display: inline-block; + display: flex; + flex-wrap: wrap; + background: #efefef; + box-shadow: 0 48px 80px -32px rgba(0, 0, 0, 0.3); + margin-top: 1.5em; +} + +.tabs > label { + border: 1px solid #e5e5e5; + width: 100%; + padding: 20px 30px; + background: #e5e5e5; + cursor: pointer; + font-weight: bold; + font-size: 18px; + color: #7f7f7f; + transition: background 0.1s, color 0.1s; + /* margin-left: 4em; */ +} + +.tabs > label:hover { + background: #d8d8d8; +} + +.tabs > label:active { + background: #ccc; +} + +.tabs > [type=radio]:focus + label { + box-shadow: inset 0px 0px 0px 3px #2aa1c0; + z-index: 1; +} + +.tabs > [type=radio] { + position: absolute; + opacity: 0; +} + +.tabs > [type=radio]:checked + label { + border-bottom: 1px solid #fff; + background: #fff; + color: #000; +} + +.tabs > [type=radio]:checked + label + .content { + border: 1px solid #e5e5e5; + border-top: 0; + display: block; + padding: 15px; + background: #fff; + width: 100%; + margin: 0 auto; + overflow: auto; + /* text-align: center; */ +} + +.tabs .content { + display: none; + max-height: 950px; + border: 1px solid #e5e5e5; + border-top: 0; + padding: 15px; + background: #fff; + width: 100%; + margin: 0 auto; + overflow: auto; +} + +.tabs .content.full-height { + max-height: none; +} + +@media (min-width: 800px) { + .tabs > label { + width: auto; + } + + .tabs .content { + order: 99; + } +} + +/* --------------------------------------------------------------------------- + Vertical Tabs + ----------------------------------------------------------------------------*/ + +.vertical-tabs { + border: 1px solid #e5e5e5; + box-shadow: 0 48px 80px -32px rgba(0, 0, 0, 0.3); + margin: 0 auto; + position: relative; + width: 100%; +} + +.vertical-tabs input[type="radio"] { + position: absolute; + opacity: 0; +} + +.vertical-tabs .tab { + align-items: center; + display: inline-block; + display: flex; + flex-wrap: nowrap; +} + +.vertical-tabs .tab label { + align-items: center; + background: #e5e5e5; + border: 1px solid #e5e5e5; + color: #7f7f7f; + cursor: pointer; + font-size: 18px; + font-weight: bold; + padding: 0 20px; + width: 28%; +} + +.vertical-tabs .tab label:hover { + background: #d8d8d8; +} + +.vertical-tabs .tab label:active { + background: #ccc; +} + +.vertical-tabs .tab .content { + display: none; + border: 1px solid #e5e5e5; + border-left: 0; + border-right: 0; + max-height: 950px; + overflow: auto; +} + +.vertical-tabs .tab .content.full-height { + max-height: none; +} + +.vertical-tabs [type=radio]:checked + label { + border: 0; + background: #fff; + color: #000; + width: 38%; +} + +.vertical-tabs [type=radio]:focus + label { + box-shadow: inset 0px 0px 0px 3px #2aa1c0; + z-index: 1; +} + +.vertical-tabs [type=radio]:checked ~ .content { + display: block; +} + diff --git a/public/css/base.css b/public/css/general.css similarity index 70% rename from public/css/base.css rename to public/css/general.css index 0a37b731..a8e5cbb1 100644 --- a/public/css/base.css +++ b/public/css/general.css @@ -1,5 +1,3 @@ -@import "./marx.css"; - :root { --blue-link: rgb(18, 113, 219); --link-shadow: 1px 1px 1px #000; @@ -30,6 +28,7 @@ button { table { /* min-width: 85%; */ + box-shadow: 0 48px 80px -32px rgba(0, 0, 0, 0.3); margin: 0 auto; } @@ -55,6 +54,11 @@ a:hover, a:active { color: var(--link-hover-color) } +iframe { + display: block; + margin: 0 auto; +} + /* ----------------------------------------------------------------------------- Utility classes ------------------------------------------------------------------------------*/ @@ -91,6 +95,10 @@ a:hover, a:active { flex-wrap: nowrap } +.flex-align-start { + align-content: flex-start; +} + .flex-align-end { align-items: flex-end } @@ -99,15 +107,28 @@ a:hover, a:active { align-content: space-around } +.flex-justify-start { + justify-content: flex-start; +} + .flex-justify-space-around { justify-content: space-around } +.flex-center { + justify-content: center; +} + .flex-self-center { align-self: center } +.flex-space-evenly { + justify-content: space-evenly; +} + .flex { + display: inline-block; display: flex } @@ -119,23 +140,23 @@ a:hover, a:active { text-align: justify } -.align_center { +.align-center { text-align: center !important } -.align_left { +.align-left { text-align: left !important } -.align_right { +.align-right { text-align: right !important } -.valign_top { +.valign-top { vertical-align: top } -.no_border { +.no-border { border: none } @@ -145,6 +166,19 @@ a:hover, a:active { position: relative; } +.media-wrap-flex { + display: inline-block; + display: flex; + flex-wrap: wrap; + align-content: space-evenly; + justify-content: space-between; + position: relative; +} + +td .media-wrap-flex { + justify-content: center; +} + .danger { background-color: #ff4136; border-color: #924949; @@ -170,10 +204,18 @@ a:hover, a:active { background-color: var(--edit-link-hover-color); } -.full_width { +.full-width { width: 100%; } +.full-height { + max-height: none; +} + +.toph { + margin-top: 0; +} + /* ----------------------------------------------------------------------------- Main Nav ------------------------------------------------------------------------------*/ @@ -188,81 +230,12 @@ a:hover, a:active { font-weight: 500; } -/* ----------------------------------------------------------------------------- - 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 { +.sorting-asc, +.sorting-desc { vertical-align: text-bottom; } @@ -270,11 +243,11 @@ a:hover, a:active { content: " ↕\00a0"; } -.sorting_asc::before { +.sorting-asc::before { content: " ↑\00a0"; } -.sorting_desc::before { +.sorting-desc::before { content: " ↓\00a0"; } @@ -302,7 +275,14 @@ a:hover, a:active { background: inherit; } -.invisible tr, .invisible td, .invisible th { +.borderless, +.borderless tr, +.borderless td, +.borderless th, +.invisible tr, +.invisible td, +.invisible th { + box-shadow: none; border: 0; } @@ -310,7 +290,7 @@ a:hover, a:active { Message boxes ------------------------------------------------------------------------------*/ -.message { +.message, .static-message { position: relative; margin: 0.5em auto; padding: 0.5em; @@ -342,7 +322,7 @@ a:hover, a:active { margin-right: 1em; } -.message.error { +.message.error, .static-message.error { border: 1px solid #924949; background: #f3e6e6; } @@ -351,7 +331,7 @@ a:hover, a:active { content: '✘'; } -.message.success { +.message.success, .static-message.success { border: 1px solid #1f8454; background: #70dda9; } @@ -360,7 +340,7 @@ a:hover, a:active { content: '✔' } -.message.info { +.message.info, .static-message.info { border: 1px solid #bfbe3a; background: #FFFFCC; } @@ -373,7 +353,7 @@ a:hover, a:active { Base list styles ------------------------------------------------------------------------------*/ -.media, .character, .small_character { +.media, .character, .small-character { position: relative; vertical-align: top; display: inline-block; @@ -382,21 +362,28 @@ a:hover, a:active { height: 311px; margin: var(--normal-padding); z-index: 0; + background: rgba(0, 0, 0, 0.15); +} + +.details picture.cover, +picture.cover { + display: initial; + width: 100%; } .media > img, .character > img, -.small_character > img { +.small-character > img { width: 100%; } -.media .edit_buttons > button { +.media .edit-buttons > button { margin: 0.5em auto; } .name, -.media_metadata > div, -.medium_metadata > div, +.media-metadata > div, +.medium-metadata > div, .row { text-shadow: var(--shadow); color: var(--text-color); @@ -405,17 +392,17 @@ a:hover, a:active { z-index: 2; } -.media_type, .age_rating { +.media-type, .age-rating { text-align: left; } -.media > .media_metadata { +.media > .media-metadata { position: absolute; bottom: 0; right: 0; } -.media > .medium_metadata { +.media > .medium-metadata { position: absolute; bottom: 0; left: 0; @@ -427,6 +414,7 @@ a:hover, a:active { } .media > .name a { + display: inline-block; transition: none; } @@ -466,7 +454,7 @@ a:hover, a:active { } .media:hover > button[hidden], -.media:hover > .edit_buttons[hidden] { +.media:hover > .edit-buttons[hidden] { transition: .25s ease; display: block; @@ -476,8 +464,8 @@ a:hover, a:active { transition: .25s ease; } -.small_character > .name a, -.small_character > .name a small, +.small-character > .name a, +.small-character > .name a small, .character > .name a, .character > .name a small, .media > .name a, @@ -498,11 +486,11 @@ a:hover, a:active { padding: 0.5em 0.25em; } -.anime .media_type, -.anime .airing_status, -.anime .user_rating, +.anime .media-type, +.anime .airing-status, +.anime .user-rating, .anime .completion, -.anime .age_rating, +.anime .age-rating, .anime .edit, .anime .delete { background: none; @@ -518,6 +506,7 @@ a:hover, a:active { .anime .row, .manga .row { width: 100%; + display: inline-block; display: flex; align-content: space-around; justify-content: space-around; @@ -532,6 +521,7 @@ a:hover, a:active { .anime .row > div, .manga .row > div { font-size: 0.8em; + display: inline-block; display: flex-item; align-self: center; text-align: center; @@ -539,7 +529,7 @@ a:hover, a:active { z-index: 2; } -.anime .media > button.plus_one { +.anime .media > button.plus-one { border-color: hsla(0, 0%, 100%, .65); position: absolute; top: 138px; @@ -549,12 +539,12 @@ a:hover, a:active { z-index: 50; } -.anime .media > button.plus_one:hover { +.anime .media > button.plus-one:hover { color: hsla(0, 0%, 100%, .65); background: #888; } -.anime .media > button.plus_one:active { +.anime .media > button.plus-one:active { background: #444; } @@ -571,7 +561,7 @@ a:hover, a:active { margin: 0.25em; } -.manga .media > .edit_buttons { +.manga .media > .edit-buttons { position: absolute; top: 86px; /* top: calc(50% - 58.5px); */ @@ -581,16 +571,16 @@ a:hover, a:active { z-index: 40; } -.manga .media > .edit_buttons button { +.manga .media > .edit-buttons button { border-color: hsla(0, 0%, 100%, .65); } -.manga .media > .edit_buttons:hover button { +.manga .media > .edit-buttons:hover button { color: hsla(0, 0%, 100%, .65); background: #888; } -.manga .media > .edit_buttons button:active { +.manga .media > .edit-buttons button:active { background: #444; } @@ -605,7 +595,11 @@ a:hover, a:active { background-repeat: no-repeat; } -.big-check { +.media.search > .row { + z-index: 6; +} + +.big-check, .mal-check { display: none; } @@ -628,11 +622,11 @@ a:hover, a:active { z-index: 5; } -#series_list article.media { +#series-list article.media { position: relative; } -#series_list .name, #series_list .name label { +#series-list .name, #series-list .name label { position: absolute; display: block; top: 0; @@ -643,7 +637,7 @@ a:hover, a:active { line-height: 1.25em; } -#series_list .name small { +#series-list .name small { color: #fff; } @@ -661,28 +655,24 @@ a:hover, a:active { } .fixed { - max-width: 93rem; + /* max-width: 100rem; */ + max-width: 80%; + margin: 0 auto; +} + +.fixed .text { + max-width: 40em; } .details .cover { display: block; - width: 284px; - /* height: 402px; */ } -.details h2 { - margin-top: 0; -} - -.details .flex > div { +.details .flex > * { margin: 1rem; } -.details .media_details { - max-width: 300px; -} - -.details .media_details td { +.details .media-details td { padding: 0 1.5rem; } @@ -690,37 +680,58 @@ a:hover, a:active { text-align: justify; } -.details .media_details td:nth-child(odd) { +.details .media-details td:nth-child(odd) { width: 1%; white-space: nowrap; text-align: right; } -.details .media_details td:nth-child(even) { +.details .media-details td:nth-child(even) { text-align: left; } +.details a h1, +.details a h2 { + margin-top: 0; +} + .character, -.small_character { +.small-character, +.person { /* background: rgba(0,0,0,0.5); */ width: 225px; height: 350px; vertical-align: middle; white-space: nowrap; + position: relative; +} + +.person { + width: 225px; + height: 338px; +} + +.small-person { + width: 200px; + height: 300px; +} + +.character a { + height: 350px; } .character:hover .name, -.small_character:hover .name { +.small-character:hover .name { background: rgba(0, 0, 0, 0.8); } -.small_character a { +.small-character a { display: inline-block; width: 100%; height: 100%; } -.small_character .name, +.small-character .name, .character .name { position: absolute; bottom: 0; @@ -728,13 +739,30 @@ a:hover, a:active { z-index: 10; } -.small_character img, -.character img { - position: relative; +.small-character img, +.character img, +.small-character picture, +.character picture, +.person img, +.person picture { + position: absolute; top: 50%; - transform: translateY(-50%); + left: 50%; + transform: translate(-50%, -50%); z-index: 5; - width: 100%; + max-height: 350px; + max-width: 225px; +} + +.person img, +.person picture { + max-height: 338px; +} + +.small-person img, +.small-person picture { + max-height: 300px; + max-width: 200px; } .min-table { @@ -742,14 +770,47 @@ a:hover, a:active { margin-left: 0; } +.max-table { + min-width: 100%; + margin: 0; +} + +aside.info { + /* max-width: 390px; */ + max-width: 33%; +} + +.fixed aside.info { + max-width: 390px; +} + +/* .fixed aside.info + article { + max-width: inherit; +} */ + +aside.info picture, aside.info img { + display: block; + margin: 0 auto; +} + +aside.info + article { + max-width: 66%; +} + /* ---------------------------------------------------------------------------- User page styles -----------------------------------------------------------------------------*/ -.small_character { +.small-character { width: 160px; height: 250px; } +.small-character img, +.small-character picture { + max-height: 250px; + max-width: 160px; +} + .user-page .media-wrap { text-align: left; } @@ -760,26 +821,6 @@ a:hover, a:active { 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 -----------------------------------------------------------------------------*/ @@ -789,15 +830,15 @@ a:hover, a:active { vertical-align: middle; } -.cover_streaming_link { - display:none; +.cover-streaming-link { + display: none; } -.media:hover .cover_streaming_link { +.media:hover .cover-streaming-link { display: block; } -.cover_streaming_link .streaming-logo { +.cover-streaming-link .streaming-logo { width: 20px; height: 20px; -webkit-filter: drop-shadow(0 -1px 4px #fff); @@ -805,109 +846,30 @@ a:hover, a:active { } /* ---------------------------------------------------------------------------- - Loading overlay +Settings Form -----------------------------------------------------------------------------*/ -#loading-shadow { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.8); - z-index: 500; -} - -#loading-shadow .loading-wrapper { - position: fixed; - z-index: 501; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; -} - -#loading-shadow .loading-content { - position: relative; - color: #fff -} - -.loading-content .cssload-inner.cssload-one, -.loading-content .cssload-inner.cssload-two, -.loading-content .cssload-inner.cssload-three { - border-color: #fff +.settings.form .content article { + margin: 1em; + display: inline-block; + width: auto; } /* ---------------------------------------------------------------------------- -CSS Tabs +iFrame container -----------------------------------------------------------------------------*/ -.tabs { - display: flex; - flex-wrap: wrap; - background: #efefef; - box-shadow: 0 48px 80px -32px rgba(0, 0, 0, 0.3); - margin-top: 1.5em; + +.responsive-iframe { + margin-top: 1em; + overflow: hidden; + padding-bottom: 56.25%; + position: relative; + height: 0; } -.tabs label { - border: 1px solid #e5e5e5; +.responsive-iframe iframe { + left: 0; + top: 0; + height: 100%; width: 100%; - padding: 20px 30px; - background: #e5e5e5; - cursor: pointer; - font-weight: bold; - font-size: 18px; - color: #7f7f7f; - transition: background 0.1s, color 0.1s; - /* margin-left: 4em; */ -} - -.tabs label:hover { - background: #d8d8d8; -} - -.tabs label:active { - background: #ccc; -} - -.tabs [type=radio]:focus + label { - box-shadow: inset 0px 0px 0px 3px #2aa1c0; - z-index: 1; -} - -.tabs [type=radio] { position: absolute; - opacity: 0; -} - -.tabs [type=radio]:checked + label { - border-bottom: 1px solid #fff; - background: #fff; - color: #000; -} - -.tabs [type=radio]:checked + label + .content { - border: 1px solid #e5e5e5; - border-top: 0; - display: block; - padding: 20px 30px 30px; - background: #fff; - width: 100%; -} - -.tabs .content { - display: none; -} - -@media (min-width: 600px) { - .tabs label { - width: auto; - } - - .tabs .content { - order: 99; - } -} - +} \ No newline at end of file diff --git a/public/css/responsive.css b/public/css/responsive.css new file mode 100644 index 00000000..860aec21 --- /dev/null +++ b/public/css/responsive.css @@ -0,0 +1,133 @@ +/* ---------------------------------------------------------------------------- + Viewport-based styles +-----------------------------------------------------------------------------*/ +@media screen and (max-width: 1100px) { + .flex { + flex-wrap: wrap; + } + + aside.info, + aside.info + article, + .fixed aside.info, + .fixed aside.info + article { + max-width: none; + width: 100%; + } + + /* aside.info { + order: 1; + } */ +} + +@media screen and (max-width: 800px) { + * { + max-width: none; + } + + table { + box-shadow: none; + } + + body, + .details .flex > * { + margin: 0; + } + + table, + table th, + table td, + table .align-right, + table.align-center { + border: 0; + display: block; + margin: 0 auto; + text-align: left; + width: 100%; + } + + table tbody { + width: 100%; + } + + table td { + display: inline-block; + } + + table.media-details td { + display: block; + text-align: left !important; + } + + table thead { + display: none; + } + + .details .media-details td:nth-child(2n+1) { + font-weight: bold; + width: 100%; + } + + table.streaming-links tr td:not(:first-child) { + display:none; + } +} + +@media screen and (max-width: 40em) { + nav a { + line-height: 4em; + line-height: 4rem; + } + + img, + picture { + width: 100%; + } + + main { + padding: 0 0, 5em 0.5em; + padding: 0 0.5rem 0.5rem; + } + + .media { + margin: 2px 0; + } + + .details { + padding: 0.5em; + padding: 0.5rem; + } + + /* Expand tabs */ + .tabs > [type="radio"]:checked + label { + background: #fff; + } + + /* Expand vertical tabs */ + .vertical-tabs .tab { + flex-wrap: wrap; + } + + .tabs .content, + .tabs > [type="radio"]:checked + label + .content, + .vertical-tabs .tab .content { + display: block; + border: 0; + max-height: none; + } + + .tabs > label, + .tabs > label:active, + .tabs > label:hover, + .tabs > [type="radio"]:checked + label, + .vertical-tabs .tab label, + .vertical-tabs .tab label:active, + .vertical-tabs .tab label:hover, + .vertical-tabs [type=radio]:focus + label, + .vertical-tabs [type=radio]:checked + label { + background: #fff; + border: 0; + width: 100%; + cursor: default; + color: #000; + } +} \ No newline at end of file diff --git a/public/js/cache/.gitkeep b/public/images/people/.gitkeep old mode 100755 new mode 100644 similarity index 100% rename from public/js/cache/.gitkeep rename to public/images/people/.gitkeep diff --git a/public/images/placeholder.png b/public/images/placeholder.png new file mode 100644 index 00000000..e878e933 Binary files /dev/null and b/public/images/placeholder.png differ diff --git a/public/images/placeholder.webp b/public/images/placeholder.webp new file mode 100644 index 00000000..cbbab7c5 Binary files /dev/null and b/public/images/placeholder.webp differ diff --git a/public/images/streaming-logos/amazon.svg b/public/images/streaming-logos/amazon.svg index 03e1093d..5e409515 100644 --- a/public/images/streaming-logos/amazon.svg +++ b/public/images/streaming-logos/amazon.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/public/images/streaming-logos/crunchyroll.svg b/public/images/streaming-logos/crunchyroll.svg index 57091058..069f18e2 100644 --- a/public/images/streaming-logos/crunchyroll.svg +++ b/public/images/streaming-logos/crunchyroll.svg @@ -1,12 +1 @@ - - - - - - - - - - - - + \ No newline at end of file diff --git a/public/images/streaming-logos/daisuki.svg b/public/images/streaming-logos/daisuki.svg index f63b5d56..a2b4dda7 100644 --- a/public/images/streaming-logos/daisuki.svg +++ b/public/images/streaming-logos/daisuki.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/public/images/streaming-logos/funimation.svg b/public/images/streaming-logos/funimation.svg index 66d15b9e..816c3357 100644 --- a/public/images/streaming-logos/funimation.svg +++ b/public/images/streaming-logos/funimation.svg @@ -1,9 +1 @@ - - - - - - - - - + \ No newline at end of file diff --git a/public/images/streaming-logos/hidive.svg b/public/images/streaming-logos/hidive.svg index 4f58126d..1c895a7e 100644 --- a/public/images/streaming-logos/hidive.svg +++ b/public/images/streaming-logos/hidive.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/public/images/streaming-logos/hulu.svg b/public/images/streaming-logos/hulu.svg index 1b539308..479b78eb 100644 --- a/public/images/streaming-logos/hulu.svg +++ b/public/images/streaming-logos/hulu.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/public/images/streaming-logos/netflix.svg b/public/images/streaming-logos/netflix.svg index c546053b..bfc2488b 100644 --- a/public/images/streaming-logos/netflix.svg +++ b/public/images/streaming-logos/netflix.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/public/images/streaming-logos/tubitv.svg b/public/images/streaming-logos/tubitv.svg index 6f6fbaec..cab3d37e 100644 --- a/public/images/streaming-logos/tubitv.svg +++ b/public/images/streaming-logos/tubitv.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/public/images/streaming-logos/viewster.svg b/public/images/streaming-logos/viewster.svg index 57840bc2..32ec3ac1 100644 --- a/public/images/streaming-logos/viewster.svg +++ b/public/images/streaming-logos/viewster.svg @@ -1,8 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/public/js.php b/public/js.php deleted file mode 100644 index 8370c65d..00000000 --- a/public/js.php +++ /dev/null @@ -1,371 +0,0 @@ - - * @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; - -use function Amp\Promise\wait; -use Amp\Artax\Request; -use Aviat\AnimeClient\API\HummingbirdClient; -use Aviat\Ion\{Json, JsonException}; - -// Include Amp and Artax -require_once '../vendor/autoload.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 { - - protected $jsRoot; - protected $jsGroup; - protected $configFile; - protected $cacheFile; - - protected $lastModified; - protected $requestedTime; - protected $cacheModified; - - public function __construct(array $config, string $configFile) - { - $group = $_GET['g']; - $groups = $config['groups']; - - $this->jsRoot = $config['js_root']; - $this->jsGroup = $groups[$group]; - $this->configFile = $configFile; - $this->cacheFile = "{$this->jsRoot}cache/{$group}"; - $this->lastModified = $this->getLastModified(); - - $this->cacheModified = (is_file($this->cacheFile)) - ? filemtime($this->cacheFile) - : 0; - - // Output some JS! - $this->send(); - } - - protected function send() - { - // Override caching if debug key is set - if($this->isDebugCall()) - { - return $this->output($this->getFiles()); - } - - // If the browser's cached version is up to date, - // don't resend the file - if($this->lastModified == $this->getIfModified() && $this->isNotDebug()) - { - throw new FileNotChangedException(); - } - - if($this->cacheModified < $this->lastModified) - { - $js = $this->minify($this->getFiles()); - - //Make sure cache file gets created/updated - if (file_put_contents($this->cacheFile, $js) === FALSE) - { - echo 'Cache file was not created. Make sure you have the correct folder permissions.'; - return; - } - - return $this->output($js); - } - else - { - return $this->output(file_get_contents($this->cacheFile)); - } - } - - /** - * Makes a call to google closure compiler service - * - * @param array $options - Form parameters - * @throws \TypeError - * @return object - */ - protected function closureCall(array $options) - { - $formFields = http_build_query($options); - - $request = (new Request('https://closure-compiler.appspot.com/compile')) - ->withMethod('POST') - ->withHeaders([ - 'Accept' => 'application/json', - 'Accept-Encoding' => 'gzip', - 'Content-type' => 'application/x-www-form-urlencoded' - ]) - ->withBody($formFields); - - $response = wait((new HummingbirdClient)->request($request, [ - HummingbirdClient::OP_AUTO_ENCODING => false - ])); - - return $response; - } - - /** - * Do a call to the closure compiler to check for compilation errors - * - * @param array $options - * @return void - */ - protected function checkMinifyErrors($options) - { - try - { - $errorRes = $this->closureCall($options); - $errorJson = wait($errorRes->getBody()); - $errorObj = Json::decode($errorJson) ?: (object)[]; - - - // Show error if exists - if ( ! empty($errorObj->errors) || ! empty($errorObj->serverErrors)) - { - $errorJson = Json::encode($errorObj, JSON_PRETTY_PRINT); - header('Content-type: application/javascript'); - echo "console.error(${errorJson});"; - die(); - } - } - catch (JsonException $e) - { - print_r($e); - die(); - } - } - - /** - * Get Files - * - * Concatenates the javascript files for the current - * group as a string - * - * @return string - */ - protected function getFiles() - { - $js = ''; - - foreach($this->jsGroup as $file) - { - $newFile = realpath("{$this->jsRoot}{$file}"); - $js .= file_get_contents($newFile) . "\n\n"; - } - - return $js; - } - - /** - * Get the most recent modified date - * - * @return int - */ - protected function getLastModified() - { - $modified = []; - - foreach($this->jsGroup as $file) - { - $newFile = realpath("{$this->jsRoot}{$file}"); - $modified[] = filemtime($newFile); - } - - //Add this page too, as well as the groups file - $modified[] = filemtime(__FILE__); - $modified[] = filemtime($this->configFile); - - rsort($modified); - $lastModified = $modified[0]; - - return $lastModified; - } - - /** - * Minifies javascript using google's closure compiler - * - * @param string $js - * @return string - */ - protected function minify($js) - { - $options = [ - 'output_info' => 'errors', - 'output_format' => 'json', - 'compilation_level' => 'SIMPLE_OPTIMIZATIONS', - //'compilation_level' => 'ADVANCED_OPTIMIZATIONS', - 'js_code' => $js, - 'language' => 'ECMASCRIPT6_STRICT', - 'language_out' => 'ECMASCRIPT5_STRICT' - ]; - - // Check for errors - $this->checkMinifyErrors($options); - - // Now actually retrieve the compiled code - $options['output_info'] = 'compiled_code'; - $res = $this->closureCall($options); - $json = wait($res->getBody()); - $obj = Json::decode($json); - - //return $obj; - return $obj['compiledCode']; - } - - /** - * Output the minified javascript - * - * @param string $js - * @return void - */ - protected function output($js) - { - $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=utf-8"); - header('Cache-control: public, max-age=691200, must-revalidate'); - header("Expires: {$expiresDate} GMT"); - header("Last-Modified: {$lastModifiedDate} GMT"); - header('X-Content-Type-Options: no-sniff'); - - 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 -// -------------------------------------------------------------------------- - -$configFile = realpath(__DIR__ . '/../app/appConf/minify_config.php'); -$config = require_once($configFile); -$groups = $config['groups']; -$cacheDir = "{$config['js_root']}cache"; - -if ( ! is_dir($cacheDir)) -{ - mkdir($cacheDir); -} - -if ( ! array_key_exists($_GET['g'], $groups)) -{ - throw new InvalidArgumentException('You must specify a js group that exists'); -} - -try -{ - new JSMin($config, $configFile); -} -catch (FileNotChangedException $e) -{ - JSMin::send304(); -} - -//end of js.php \ No newline at end of file diff --git a/public/js/anime_collection.js b/public/js/anime_collection.js deleted file mode 100644 index f3c115d5..00000000 --- a/public/js/anime_collection.js +++ /dev/null @@ -1,30 +0,0 @@ -((_) => { - - 'use strict'; - - const search = (query) => { - // Show the loader - _.$('.cssload-loader')[0].removeAttribute('hidden'); - - // Do the api search - _.get(_.url('/anime-collection/search'), {query}, (searchResults, status) => { - searchResults = JSON.parse(searchResults); - - // Hide the loader - _.$('.cssload-loader')[0].setAttribute('hidden', 'hidden'); - - // Show the results - _.$('#series_list')[0].innerHTML = render_anime_search_results(searchResults.data); - }); - }; - - _.on('#search', 'keyup', _.throttle(250, function() { - const query = encodeURIComponent(this.value); - if (query === '') { - return; - } - - search(query); - })); - -})(AnimeClient); \ No newline at end of file diff --git a/public/js/anime_edit.js b/public/js/anime_edit.js deleted file mode 100644 index 623e368e..00000000 --- a/public/js/anime_edit.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Javascript for editing anime, if logged in - */ -((_) => { - - 'use strict'; - - // 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) || 0; - let totalCount = parseInt(_.$('.total_number', parentSel)[0].textContent, 10); - let title = _.$('.name a', parentSel)[0].textContent; - - // Setup the update data - let data = { - id: parentSel.dataset.kitsuId, - mal_id: parentSel.dataset.malId, - data: { - progress: watchedCount + 1 - } - }; - - // If the episode count is 0, and incremented, - // change status to currently watching - if (isNaN(watchedCount) || watchedCount === 0) { - data.data.status = 'current'; - } - - // If you increment at the last episode, mark as completed - if (( ! isNaN(watchedCount)) && (watchedCount + 1) === totalCount) { - data.data.status = 'completed'; - } - - _.show(_.$('#loading-shadow')[0]); - - // okay, lets actually make some changes! - _.ajax(_.url('/anime/update'), { - data, - dataType: 'json', - type: 'POST', - success: (res) => { - const resData = JSON.parse(res); - - if (resData.errors) { - _.hide(_.$('#loading-shadow')[ 0 ]); - _.showMessage('error', `Failed to update ${title}. `); - _.scrollToTop(); - return; - } - - if (resData.data.attributes.status === 'completed') { - _.hide(parentSel); - } - - _.hide(_.$('#loading-shadow')[0]); - - _.showMessage('success', `Successfully updated ${title}`); - _.$('.completed_number', parentSel)[0].textContent = ++watchedCount; - _.scrollToTop(); - }, - error: (xhr, errorType, error) => { - _.hide(_.$('#loading-shadow')[0]); - _.showMessage('error', `Failed to update ${title}. `); - _.scrollToTop(); - } - }); - }); - -})(AnimeClient); \ No newline at end of file diff --git a/public/js/anime_search_results.js b/public/js/anime_search_results.js deleted file mode 100644 index 49c8d1e5..00000000 --- a/public/js/anime_search_results.js +++ /dev/null @@ -1,27 +0,0 @@ -function render_anime_search_results (data) { - const results = []; - - data.forEach(x => { - const item = x.attributes; - const titles = item.titles.reduce((prev, current) => { - return prev + `${current}
`; - }, []); - - results.push(` - - `); - }); - - return results.join(''); -} \ No newline at end of file diff --git a/public/js/base/AnimeClient.js b/public/js/base/AnimeClient.js deleted file mode 100644 index 7614063b..00000000 --- a/public/js/base/AnimeClient.js +++ /dev/null @@ -1,321 +0,0 @@ -var AnimeClient = (function(w) { - - 'use strict'; - - // ------------------------------------------------------------------------- - // ! Base - // ------------------------------------------------------------------------- - - function matches(elm, selector) { - let matches = (elm.document || elm.ownerDocument).querySelectorAll(selector), - i = matches.length; - while (--i >= 0 && matches.item(i) !== elm); - return i > -1; - } - - const _ = { - /** - * Placeholder function - */ - noop: () => {}, - /** - * DOM selector - * - * @param {string} selector - The dom selector string - * @param {object} context - * @return {array} - array of dom elements - */ - $(selector, context) { - if (typeof selector !== 'string') { - return selector; - } - - context = (context != null && context.nodeType === 1) - ? context - : document; - - let elements = []; - if (selector.match(/^#([\w]+$)/)) { - elements.push(document.getElementById(selector.split('#')[1])); - } else { - elements = [].slice.apply(context.querySelectorAll(selector)); - } - - return elements; - }, - /** - * Scroll to the top of the Page - * - * @return {void} - */ - scrollToTop() { - w.scroll(0,0); - }, - /** - * Hide the selected element - * - * @param {string|Element} sel - the selector of the element to hide - * @return {void} - */ - hide(sel) { - sel.setAttribute('hidden', 'hidden'); - }, - /** - * UnHide the selected element - * - * @param {string|Element} sel - the selector of the element to hide - * @return {void} - */ - show(sel) { - sel.removeAttribute('hidden'); - }, - /** - * Display a message box - * - * @param {String} type - message type: info, error, success - * @param {String} message - the message itself - * @return {void} - */ - showMessage(type, message) { - let template = - `
- - ${message} - -
`; - - let sel = AnimeClient.$('.message'); - if (sel[0] !== undefined) { - sel[0].remove(); - } - - _.$('header')[0].insertAdjacentHTML('beforeend', template); - }, - /** - * Finds the closest parent element matching the passed selector - * - * @param {DOMElement} current - the current DOMElement - * @param {string} parentSelector - selector for the parent element - * @return {DOMElement|null} - the parent element - */ - closestParent(current, parentSelector) { - if (Element.prototype.closest !== undefined) { - return current.closest(parentSelector); - } - - while (current !== document.documentElement) { - if (matches(current, parentSelector)) { - return current; - } - - current = current.parentElement; - } - - return null; - }, - /** - * Generate a full url from a relative path - * - * @param {String} path - url path - * @return {String} - full url - */ - url(path) { - let uri = `//${document.location.host}`; - uri += (path.charAt(0) === '/') ? path : `/${path}`; - - return uri; - }, - /** - * Throttle execution of a function - * - * @see https://remysharp.com/2010/07/21/throttling-function-calls - * @see https://jsfiddle.net/jonathansampson/m7G64/ - * @param {Number} interval - the minimum throttle time in ms - * @param {Function} fn - the function to throttle - * @param {Object} scope - the 'this' object for the function - * @return {void} - */ - throttle(interval, fn, scope) { - var wait = false; - return function () { - var context = scope || this; - var args = arguments; - - if ( ! wait) { - fn.apply(context, args); - wait = true; - setTimeout(function() { - wait = false; - }, interval); - } - }; - }, - }; - - // ------------------------------------------------------------------------- - // ! Events - // ------------------------------------------------------------------------- - - function addEvent(sel, event, listener) { - // Recurse! - if (! event.match(/^([\w\-]+)$/)) { - event.split(' ').forEach((evt) => { - addEvent(sel, evt, listener); - }); - } - - sel.addEventListener(event, listener, false); - } - - function delegateEvent(sel, target, event, listener) { - // Attach the listener to the parent - addEvent(sel, event, (e) => { - // Get live version of the target selector - _.$(target, sel).forEach((element) => { - if(e.target == element) { - listener.call(element, e); - e.stopPropagation(); - } - }); - }); - } - - /** - * Add an event listener - * - * @param {string|element} sel - the parent selector to bind to - * @param {string} event - event name(s) to bind - * @param {string|element} [target] - the element to directly bind the event to - * @param {function} listener - event listener callback - * @return {void} - */ - _.on = function (sel, event, target, listener) { - if (arguments.length === 3) { - listener = target; - _.$(sel).forEach((el) => { - addEvent(el, event, listener); - }); - } else { - _.$(sel).forEach((el) => { - delegateEvent(el, target, event, listener); - }); - } - }; - - // ------------------------------------------------------------------------- - // ! Ajax - // ------------------------------------------------------------------------- - - /** - * Url encoding for non-get requests - * - * @param data - * @returns {string} - * @private - */ - function ajaxSerialize(data) { - let pairs = []; - - Object.keys(data).forEach((name) => { - let value = data[name].toString(); - - name = encodeURIComponent(name); - value = encodeURIComponent(value); - - pairs.push(`${name}=${value}`); - }); - - return pairs.join('&'); - } - - /** - * Make an ajax request - * - * Config:{ - * data: // data to send with the request - * type: // http verb of the request, defaults to GET - * success: // success callback - * error: // error callback - * } - * - * @param {string} url - the url to request - * @param {Object} config - the configuration object - * @return {void} - */ - _.ajax = function(url, config) { - // Set some sane defaults - config = config || {}; - config.data = config.data || {}; - config.type = config.type || 'GET'; - config.dataType = config.dataType || ''; - config.success = config.success || _.noop; - config.mimeType = config.mimeType || 'application/x-www-form-urlencoded'; - config.error = config.error || _.noop; - - let request = new XMLHttpRequest(); - let method = String(config.type).toUpperCase(); - - if (method === 'GET') { - url += (url.match(/\?/)) - ? ajaxSerialize(config.data) - : `?${ajaxSerialize(config.data)}`; - } - - request.open(method, url); - - request.onreadystatechange = () => { - if (request.readyState === 4) { - let responseText = ''; - - if (request.responseType === 'json') { - responseText = JSON.parse(request.responseText); - } else { - responseText = request.responseText; - } - - if (request.status > 299) { - config.error.call(null, request.status, responseText, request.response); - } else { - config.success.call(null, responseText, request.status); - } - } - }; - - if (config.dataType === 'json') { - config.data = JSON.stringify(config.data); - config.mimeType = 'application/json'; - } else { - config.data = ajaxSerialize(config.data); - } - - request.setRequestHeader('Content-Type', config.mimeType); - - switch (method) { - case 'GET': - request.send(null); - break; - - default: - request.send(config.data); - break; - } - }; - - _.get = function(url, data, callback) { - if (arguments.length === 2) { - callback = data; - data = {}; - } - - return _.ajax(url, { - data, - success: callback - }); - }; - - // ------------------------------------------------------------------------- - // Export - // ------------------------------------------------------------------------- - - return _; -})(window); \ No newline at end of file diff --git a/public/js/base/events.js b/public/js/base/events.js deleted file mode 100644 index b7462294..00000000 --- a/public/js/base/events.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Event handlers - */ -((ac) => { - - 'use strict'; - - // Close event for messages - ac.on('header', 'click', '.message', function () { - ac.hide(this); - }); - - // Confirm deleting of list or library items - ac.on('form.js-delete', 'submit', (event) => { - const proceed = confirm('Are you ABSOLUTELY SURE you want to delete this item?'); - - if (proceed === false) { - event.preventDefault(); - event.stopPropagation(); - } - }); - - // Clear the api cache - ac.on('.js-clear-cache', 'click', () => { - ac.get('/cache_purge', () => { - ac.showMessage('success', 'Successfully purged api cache'); - }); - }); - -})(AnimeClient); \ No newline at end of file diff --git a/public/js/manga_collection.js b/public/js/manga_collection.js deleted file mode 100644 index 2c969145..00000000 --- a/public/js/manga_collection.js +++ /dev/null @@ -1,23 +0,0 @@ -((_) => { - - 'use strict'; - - const search = (query) => { - _.$('.cssload-loader')[0].removeAttribute('hidden'); - _.get(_.url('/manga/search'), {query}, (searchResults, status) => { - searchResults = JSON.parse(searchResults); - _.$('.cssload-loader')[0].setAttribute('hidden', 'hidden'); - _.$('#series_list')[0].innerHTML = render_manga_search_results(searchResults.data); - }); - }; - - _.on('#search', 'keyup', _.throttle(250, function(e) { - let query = encodeURIComponent(this.value); - if (query === '') { - return; - } - - search(query); - })); - -})(AnimeClient); \ No newline at end of file diff --git a/public/js/manga_edit.js b/public/js/manga_edit.js deleted file mode 100644 index 9fa62d9d..00000000 --- a/public/js/manga_edit.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Javascript for editing manga, if logged in - */ -((_) => { - - 'use strict'; - - _.on('.manga.list', 'click', '.edit_buttons button', (e) => { - 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) || 0; - let total = parseInt(_.$(`.${type}_count`, parentSel)[0].textContent, 10); - let mangaName = _.$('.name', parentSel)[0].textContent; - - if (isNaN(completed)) { - completed = 0; - } - - // Setup the update data - let data = { - id: parentSel.dataset.kitsuId, - mal_id: parentSel.dataset.malId, - data: { - progress: completed - } - }; - - // If the episode count is 0, and incremented, - // change status to currently reading - if (isNaN(completed) || completed === 0) { - data.data.status = 'current'; - } - - // If you increment at the last chapter, mark as completed - if (( ! isNaN(completed)) && (completed + 1) === total) { - data.data.status = 'completed'; - } - - // Update the total count - data.data.progress = ++completed; - - _.show(_.$('#loading-shadow')[0]); - - _.ajax(_.url('/manga/update'), { - data, - dataType: 'json', - type: 'POST', - mimeType: 'application/json', - success: () => { - if (data.data.status === 'completed') { - _.hide(parentSel); - } - - _.hide(_.$('#loading-shadow')[0]); - - _.$(`.${type}s_read`, parentSel)[0].textContent = completed; - _.showMessage('success', `Sucessfully updated ${mangaName}`); - _.scrollToTop(); - }, - error: () => { - _.hide(_.$('#loading-shadow')[0]); - _.showMessage('error', `Failed to update ${mangaName}`); - _.scrollToTop(); - } - }); - }); - -})(AnimeClient); \ No newline at end of file diff --git a/public/js/manga_search_results.js b/public/js/manga_search_results.js deleted file mode 100644 index 6e394938..00000000 --- a/public/js/manga_search_results.js +++ /dev/null @@ -1,27 +0,0 @@ -function render_manga_search_results (data) { - const results = []; - - data.forEach(x => { - const item = x.attributes; - const titles = item.titles.reduce((prev, current) => { - return prev + `${current}
`; - }, []); - - results.push(` - - `); - }); - - return results.join(''); -} diff --git a/public/js/scripts-authed.min.js b/public/js/scripts-authed.min.js new file mode 100644 index 00000000..c907e87f --- /dev/null +++ b/public/js/scripts-authed.min.js @@ -0,0 +1,23 @@ +(function(){var matches=function(elm,selector){var matches=(elm.document||elm.ownerDocument).querySelectorAll(selector),i=matches.length;while(--i>=0&&matches.item(i)!==elm);return i>-1};var AnimeClient={noop:function(){},$:function(selector,context){context=context===undefined?null:context;if(typeof selector!=="string")return selector;context=context!==null&&context.nodeType===1?context:document;var elements=[];if(selector.match(/^#([\w]+$)/))elements.push(document.getElementById(selector.split("#")[1])); +else elements=[].slice.apply(context.querySelectorAll(selector));return elements},hasElement:function(selector){return AnimeClient.$(selector).length>0},scrollToTop:function(){window.scroll(0,0)},hide:function(sel){sel.setAttribute("hidden","hidden")},show:function(sel){sel.removeAttribute("hidden")},showMessage:function(type,message){var template="
\n\t\t\t\t\n\t\t\t\t"+message+"\n\t\t\t\t\n\t\t\t
";var sel=AnimeClient.$(".message"); +if(sel[0]!==undefined)sel[0].remove();AnimeClient.$("header")[0].insertAdjacentHTML("beforeend",template)},closestParent:function(current,parentSelector){if(Element.prototype.closest!==undefined)return current.closest(parentSelector);while(current!==document.documentElement){if(matches(current,parentSelector))return current;current=current.parentElement}return null},url:function(path){var uri="//"+document.location.host;uri+=path.charAt(0)==="/"?path:"/"+path;return uri},throttle:function(interval, +fn,scope){var wait=false;return function(args){var $jscomp$restParams=[];for(var $jscomp$restIndex=0;$jscomp$restIndex299)config.error.call(null,request.status,responseText,request.response);else config.success.call(null,responseText,request.status)}};if(config.dataType==="json"){config.data= +JSON.stringify(config.data);config.mimeType="application/json"}else config.data=ajaxSerialize(config.data);request.setRequestHeader("Content-Type",config.mimeType);switch(method){case "GET":request.send(null);break;default:request.send(config.data);break}};AnimeClient.get=function(url,data,callback){callback=callback===undefined?null:callback;if(callback===null){callback=data;data={}}return AnimeClient.ajax(url,{data:data,success:callback})};AnimeClient.on("header","click",".message",function(e){AnimeClient.hide(e.target)}); +AnimeClient.on("form.js-delete","submit",function(event){var proceed=confirm("Are you ABSOLUTELY SURE you want to delete this item?");if(proceed===false){event.preventDefault();event.stopPropagation()}});AnimeClient.on(".js-clear-cache","click",function(){AnimeClient.get("/cache_purge",function(){AnimeClient.showMessage("success","Successfully purged api cache")})});AnimeClient.on(".vertical-tabs input","change",function(event){var el=event.currentTarget.parentElement;var rect=el.getBoundingClientRect(); +var top=rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})});AnimeClient.on("main","change",".big-check",function(e){var id=e.target.id;document.getElementById("mal_"+id).checked=true});function renderAnimeSearchResults(data){var results=[];data.forEach(function(x){var item=x.attributes;var titles=item.titles.reduce(function(prev,current){return prev+(current+"
")},[]);results.push('\n\t\t\t
\n\t\t')});return results.join("")}function renderMangaSearchResults(data){var results=[];data.forEach(function(x){var item=x.attributes; +var titles=item.titles.reduce(function(prev,current){return prev+(current+"
")},[]);results.push('\n\t\t\t\n\t\t')}); +return results.join("")}var search=function(query){AnimeClient.$(".cssload-loader")[0].removeAttribute("hidden");AnimeClient.get(AnimeClient.url("/anime-collection/search"),{query:query},function(searchResults,status){searchResults=JSON.parse(searchResults);AnimeClient.$(".cssload-loader")[0].setAttribute("hidden","hidden");AnimeClient.$("#series-list")[0].innerHTML=renderAnimeSearchResults(searchResults.data)})};if(AnimeClient.hasElement(".anime #search"))AnimeClient.on("#search","keyup",AnimeClient.throttle(250, +function(e){var query=encodeURIComponent(e.target.value);if(query==="")return;search(query)}));AnimeClient.on("body.anime.list","click",".plus-one",function(e){var parentSel=AnimeClient.closestParent(e.target,"article");var watchedCount=parseInt(AnimeClient.$(".completed_number",parentSel)[0].textContent,10)||0;var totalCount=parseInt(AnimeClient.$(".total_number",parentSel)[0].textContent,10);var title=AnimeClient.$(".name a",parentSel)[0].textContent;var data={id:parentSel.dataset.kitsuId,mal_id:parentSel.dataset.malId, +data:{progress:watchedCount+1}};if(isNaN(watchedCount)||watchedCount===0)data.data.status="current";if(!isNaN(watchedCount)&&watchedCount+1===totalCount)data.data.status="completed";AnimeClient.show(AnimeClient.$("#loading-shadow")[0]);AnimeClient.ajax(AnimeClient.url("/anime/increment"),{data:data,dataType:"json",type:"POST",success:function(res){var resData=JSON.parse(res);if(resData.errors){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+ +title+". ");AnimeClient.scrollToTop();return}if(resData.data.attributes.status==="completed")AnimeClient.hide(parentSel);AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("success","Successfully updated "+title);AnimeClient.$(".completed_number",parentSel)[0].textContent=++watchedCount;AnimeClient.scrollToTop()},error:function(){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+title+". ");AnimeClient.scrollToTop()}})}); +var search$1=function(query){AnimeClient.$(".cssload-loader")[0].removeAttribute("hidden");AnimeClient.get(AnimeClient.url("/manga/search"),{query:query},function(searchResults,status){searchResults=JSON.parse(searchResults);AnimeClient.$(".cssload-loader")[0].setAttribute("hidden","hidden");AnimeClient.$("#series-list")[0].innerHTML=renderMangaSearchResults(searchResults.data)})};if(AnimeClient.hasElement(".manga #search"))AnimeClient.on("#search","keyup",AnimeClient.throttle(250,function(e){var query= +encodeURIComponent(e.target.value);if(query==="")return;search$1(query)}));AnimeClient.on(".manga.list","click",".edit-buttons button",function(e){var thisSel=e.target;var parentSel=AnimeClient.closestParent(e.target,"article");var type=thisSel.classList.contains("plus-one-chapter")?"chapter":"volume";var completed=parseInt(AnimeClient.$("."+type+"s_read",parentSel)[0].textContent,10)||0;var total=parseInt(AnimeClient.$("."+type+"_count",parentSel)[0].textContent,10);var mangaName=AnimeClient.$(".name", +parentSel)[0].textContent;if(isNaN(completed))completed=0;var data={id:parentSel.dataset.kitsuId,mal_id:parentSel.dataset.malId,data:{progress:completed}};if(isNaN(completed)||completed===0)data.data.status="current";if(!isNaN(completed)&&completed+1===total)data.data.status="completed";data.data.progress=++completed;AnimeClient.show(AnimeClient.$("#loading-shadow")[0]);AnimeClient.ajax(AnimeClient.url("/manga/increment"),{data:data,dataType:"json",type:"POST",mimeType:"application/json",success:function(){if(data.data.status=== +"completed")AnimeClient.hide(parentSel);AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.$("."+type+"s_read",parentSel)[0].textContent=completed;AnimeClient.showMessage("success","Successfully updated "+mangaName);AnimeClient.scrollToTop()},error:function(){AnimeClient.hide(AnimeClient.$("#loading-shadow")[0]);AnimeClient.showMessage("error","Failed to update "+mangaName);AnimeClient.scrollToTop()}})})})(); +//# sourceMappingURL=scripts-authed.min.js.map diff --git a/public/js/scripts-authed.min.js.map b/public/js/scripts-authed.min.js.map new file mode 100644 index 00000000..14e651b4 --- /dev/null +++ b/public/js/scripts-authed.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"scripts-authed.min.js.map","sources":["src/base/AnimeClient.js","src/base/events.js","src/template-helpers.js","src/anime.js","src/manga.js"],"sourcesContent":["// -------------------------------------------------------------------------\n// ! Base\n// -------------------------------------------------------------------------\n\nconst matches = (elm, selector) => {\n\tlet matches = (elm.document || elm.ownerDocument).querySelectorAll(selector),\n\t\ti = matches.length;\n\twhile (--i >= 0 && matches.item(i) !== elm) {};\n\treturn i > -1;\n}\n\nexport const AnimeClient = {\n\t/**\n\t * Placeholder function\n\t */\n\tnoop: () => {},\n\t/**\n\t * DOM selector\n\t *\n\t * @param {string} selector - The dom selector string\n\t * @param {object} [context]\n\t * @return {[HTMLElement]} - array of dom elements\n\t */\n\t$(selector, context = null) {\n\t\tif (typeof selector !== 'string') {\n\t\t\treturn selector;\n\t\t}\n\n\t\tcontext = (context !== null && context.nodeType === 1)\n\t\t\t? context\n\t\t\t: document;\n\n\t\tlet elements = [];\n\t\tif (selector.match(/^#([\\w]+$)/)) {\n\t\t\telements.push(document.getElementById(selector.split('#')[1]));\n\t\t} else {\n\t\t\telements = [].slice.apply(context.querySelectorAll(selector));\n\t\t}\n\n\t\treturn elements;\n\t},\n\t/**\n\t * Does the selector exist on the current page?\n\t *\n\t * @param {string} selector\n\t * @returns {boolean}\n\t */\n\thasElement (selector) {\n\t\treturn AnimeClient.$(selector).length > 0;\n\t},\n\t/**\n\t * Scroll to the top of the Page\n\t *\n\t * @return {void}\n\t */\n\tscrollToTop () {\n\t\twindow.scroll(0,0);\n\t},\n\t/**\n\t * Hide the selected element\n\t *\n\t * @param {string|Element} sel - the selector of the element to hide\n\t * @return {void}\n\t */\n\thide (sel) {\n\t\tsel.setAttribute('hidden', 'hidden');\n\t},\n\t/**\n\t * UnHide the selected element\n\t *\n\t * @param {string|Element} sel - the selector of the element to hide\n\t * @return {void}\n\t */\n\tshow (sel) {\n\t\tsel.removeAttribute('hidden');\n\t},\n\t/**\n\t * Display a message box\n\t *\n\t * @param {string} type - message type: info, error, success\n\t * @param {string} message - the message itself\n\t * @return {void}\n\t */\n\tshowMessage (type, message) {\n\t\tlet template =\n\t\t\t`
\n\t\t\t\t\n\t\t\t\t${message}\n\t\t\t\t\n\t\t\t
`;\n\n\t\tlet sel = AnimeClient.$('.message');\n\t\tif (sel[0] !== undefined) {\n\t\t\tsel[0].remove();\n\t\t}\n\n\t\tAnimeClient.$('header')[0].insertAdjacentHTML('beforeend', template);\n\t},\n\t/**\n\t * Finds the closest parent element matching the passed selector\n\t *\n\t * @param {HTMLElement} current - the current HTMLElement\n\t * @param {string} parentSelector - selector for the parent element\n\t * @return {HTMLElement|null} - the parent element\n\t */\n\tclosestParent (current, parentSelector) {\n\t\tif (Element.prototype.closest !== undefined) {\n\t\t\treturn current.closest(parentSelector);\n\t\t}\n\n\t\twhile (current !== document.documentElement) {\n\t\t\tif (matches(current, parentSelector)) {\n\t\t\t\treturn current;\n\t\t\t}\n\n\t\t\tcurrent = current.parentElement;\n\t\t}\n\n\t\treturn null;\n\t},\n\t/**\n\t * Generate a full url from a relative path\n\t *\n\t * @param {string} path - url path\n\t * @return {string} - full url\n\t */\n\turl (path) {\n\t\tlet uri = `//${document.location.host}`;\n\t\turi += (path.charAt(0) === '/') ? path : `/${path}`;\n\n\t\treturn uri;\n\t},\n\t/**\n\t * Throttle execution of a function\n\t *\n\t * @see https://remysharp.com/2010/07/21/throttling-function-calls\n\t * @see https://jsfiddle.net/jonathansampson/m7G64/\n\t * @param {Number} interval - the minimum throttle time in ms\n\t * @param {Function} fn - the function to throttle\n\t * @param {Object} [scope] - the 'this' object for the function\n\t * @return {Function}\n\t */\n\tthrottle (interval, fn, scope) {\n\t\tlet wait = false;\n\t\treturn function (...args) {\n\t\t\tconst context = scope || this;\n\n\t\t\tif ( ! wait) {\n\t\t\t\tfn.apply(context, args);\n\t\t\t\twait = true;\n\t\t\t\tsetTimeout(function() {\n\t\t\t\t\twait = false;\n\t\t\t\t}, interval);\n\t\t\t}\n\t\t};\n\t},\n};\n\n// -------------------------------------------------------------------------\n// ! Events\n// -------------------------------------------------------------------------\n\nfunction addEvent(sel, event, listener) {\n\t// Recurse!\n\tif (! event.match(/^([\\w\\-]+)$/)) {\n\t\tevent.split(' ').forEach((evt) => {\n\t\t\taddEvent(sel, evt, listener);\n\t\t});\n\t}\n\n\tsel.addEventListener(event, listener, false);\n}\n\nfunction delegateEvent(sel, target, event, listener) {\n\t// Attach the listener to the parent\n\taddEvent(sel, event, (e) => {\n\t\t// Get live version of the target selector\n\t\tAnimeClient.$(target, sel).forEach((element) => {\n\t\t\tif(e.target == element) {\n\t\t\t\tlistener.call(element, e);\n\t\t\t\te.stopPropagation();\n\t\t\t}\n\t\t});\n\t});\n}\n\n/**\n * Add an event listener\n *\n * @param {string|HTMLElement} sel - the parent selector to bind to\n * @param {string} event - event name(s) to bind\n * @param {string|HTMLElement|function} target - the element to directly bind the event to\n * @param {function} [listener] - event listener callback\n * @return {void}\n */\nAnimeClient.on = (sel, event, target, listener) => {\n\tif (listener === undefined) {\n\t\tlistener = target;\n\t\tAnimeClient.$(sel).forEach((el) => {\n\t\t\taddEvent(el, event, listener);\n\t\t});\n\t} else {\n\t\tAnimeClient.$(sel).forEach((el) => {\n\t\t\tdelegateEvent(el, target, event, listener);\n\t\t});\n\t}\n};\n\n// -------------------------------------------------------------------------\n// ! Ajax\n// -------------------------------------------------------------------------\n\n/**\n * Url encoding for non-get requests\n *\n * @param data\n * @returns {string}\n * @private\n */\nfunction ajaxSerialize(data) {\n\tlet pairs = [];\n\n\tObject.keys(data).forEach((name) => {\n\t\tlet value = data[name].toString();\n\n\t\tname = encodeURIComponent(name);\n\t\tvalue = encodeURIComponent(value);\n\n\t\tpairs.push(`${name}=${value}`);\n\t});\n\n\treturn pairs.join('&');\n}\n\n/**\n * Make an ajax request\n *\n * Config:{\n * \tdata: // data to send with the request\n * \ttype: // http verb of the request, defaults to GET\n * \tsuccess: // success callback\n * \terror: // error callback\n * }\n *\n * @param {string} url - the url to request\n * @param {Object} config - the configuration object\n * @return {void}\n */\nAnimeClient.ajax = (url, config) => {\n\t// Set some sane defaults\n\tconst defaultConfig = {\n\t\tdata: {},\n\t\ttype: 'GET',\n\t\tdataType: '',\n\t\tsuccess: AnimeClient.noop,\n\t\tmimeType: 'application/x-www-form-urlencoded',\n\t\terror: AnimeClient.noop\n\t}\n\n\tconfig = {\n\t\t...defaultConfig,\n\t\t...config,\n\t}\n\n\tlet request = new XMLHttpRequest();\n\tlet method = String(config.type).toUpperCase();\n\n\tif (method === 'GET') {\n\t\turl += (url.match(/\\?/))\n\t\t\t? ajaxSerialize(config.data)\n\t\t\t: `?${ajaxSerialize(config.data)}`;\n\t}\n\n\trequest.open(method, url);\n\n\trequest.onreadystatechange = () => {\n\t\tif (request.readyState === 4) {\n\t\t\tlet responseText = '';\n\n\t\t\tif (request.responseType === 'json') {\n\t\t\t\tresponseText = JSON.parse(request.responseText);\n\t\t\t} else {\n\t\t\t\tresponseText = request.responseText;\n\t\t\t}\n\n\t\t\tif (request.status > 299) {\n\t\t\t\tconfig.error.call(null, request.status, responseText, request.response);\n\t\t\t} else {\n\t\t\t\tconfig.success.call(null, responseText, request.status);\n\t\t\t}\n\t\t}\n\t};\n\n\tif (config.dataType === 'json') {\n\t\tconfig.data = JSON.stringify(config.data);\n\t\tconfig.mimeType = 'application/json';\n\t} else {\n\t\tconfig.data = ajaxSerialize(config.data);\n\t}\n\n\trequest.setRequestHeader('Content-Type', config.mimeType);\n\n\tswitch (method) {\n\t\tcase 'GET':\n\t\t\trequest.send(null);\n\t\tbreak;\n\n\t\tdefault:\n\t\t\trequest.send(config.data);\n\t\tbreak;\n\t}\n};\n\n/**\n * Do a get request\n *\n * @param {string} url\n * @param {object|function} data\n * @param {function} [callback]\n */\nAnimeClient.get = (url, data, callback = null) => {\n\tif (callback === null) {\n\t\tcallback = data;\n\t\tdata = {};\n\t}\n\n\treturn AnimeClient.ajax(url, {\n\t\tdata,\n\t\tsuccess: callback\n\t});\n};\n\n// -------------------------------------------------------------------------\n// Export\n// -------------------------------------------------------------------------\n\nexport default AnimeClient;","import _ from './AnimeClient.js';\n/**\n * Event handlers\n */\n// Close event for messages\n_.on('header', 'click', '.message', (e) => {\n\t_.hide(e.target);\n});\n\n// Confirm deleting of list or library items\n_.on('form.js-delete', 'submit', (event) => {\n\tconst proceed = confirm('Are you ABSOLUTELY SURE you want to delete this item?');\n\n\tif (proceed === false) {\n\t\tevent.preventDefault();\n\t\tevent.stopPropagation();\n\t}\n});\n\n// Clear the api cache\n_.on('.js-clear-cache', 'click', () => {\n\t_.get('/cache_purge', () => {\n\t\t_.showMessage('success', 'Successfully purged api cache');\n\t});\n});\n\n// Alleviate some page jumping\n _.on('.vertical-tabs input', 'change', (event) => {\n\tconst el = event.currentTarget.parentElement;\n\tconst rect = el.getBoundingClientRect();\n\n\tconst top = rect.top + window.pageYOffset;\n\n\twindow.scrollTo({\n\t\ttop,\n\t\tbehavior: 'smooth',\n\t});\n});\n","import _ from './base/AnimeClient.js';\n\n// Click on hidden MAL checkbox so\n// that MAL id is passed\n_.on('main', 'change', '.big-check', (e) => {\n\tconst id = e.target.id;\n\tdocument.getElementById(`mal_${id}`).checked = true;\n});\n\nexport function renderAnimeSearchResults (data) {\n\tconst results = [];\n\n\tdata.forEach(x => {\n\t\tconst item = x.attributes;\n\t\tconst titles = item.titles.reduce((prev, current) => {\n\t\t\treturn prev + `${current}
`;\n\t\t}, []);\n\n\t\tresults.push(`\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\tInfo Page\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t
\n\t\t`);\n\t});\n\n\treturn results.join('');\n}\n\nexport function renderMangaSearchResults (data) {\n\tconst results = [];\n\n\tdata.forEach(x => {\n\t\tconst item = x.attributes;\n\t\tconst titles = item.titles.reduce((prev, current) => {\n\t\t\treturn prev + `${current}
`;\n\t\t}, []);\n\n\t\tresults.push(`\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\tInfo Page\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t
\n\t\t`);\n\t});\n\n\treturn results.join('');\n}","import _ from './base/AnimeClient.js'\nimport { renderAnimeSearchResults } from './template-helpers.js'\n\nconst search = (query) => {\n\t// Show the loader\n\t_.$('.cssload-loader')[ 0 ].removeAttribute('hidden');\n\n\t// Do the api search\n\t_.get(_.url('/anime-collection/search'), { query }, (searchResults, status) => {\n\t\tsearchResults = JSON.parse(searchResults);\n\n\t\t// Hide the loader\n\t\t_.$('.cssload-loader')[ 0 ].setAttribute('hidden', 'hidden');\n\n\t\t// Show the results\n\t\t_.$('#series-list')[ 0 ].innerHTML = renderAnimeSearchResults(searchResults.data);\n\t});\n};\n\nif (_.hasElement('.anime #search')) {\n\t_.on('#search', 'keyup', _.throttle(250, (e) => {\n\t\tconst query = encodeURIComponent(e.target.value);\n\t\tif (query === '') {\n\t\t\treturn;\n\t\t}\n\n\t\tsearch(query);\n\t}));\n}\n\n// Action to increment episode count\n_.on('body.anime.list', 'click', '.plus-one', (e) => {\n\tlet parentSel = _.closestParent(e.target, 'article');\n\tlet watchedCount = parseInt(_.$('.completed_number', parentSel)[ 0 ].textContent, 10) || 0;\n\tlet totalCount = parseInt(_.$('.total_number', parentSel)[ 0 ].textContent, 10);\n\tlet title = _.$('.name a', parentSel)[ 0 ].textContent;\n\n\t// Setup the update data\n\tlet data = {\n\t\tid: parentSel.dataset.kitsuId,\n\t\tmal_id: parentSel.dataset.malId,\n\t\tdata: {\n\t\t\tprogress: watchedCount + 1\n\t\t}\n\t};\n\n\t// If the episode count is 0, and incremented,\n\t// change status to currently watching\n\tif (isNaN(watchedCount) || watchedCount === 0) {\n\t\tdata.data.status = 'current';\n\t}\n\n\t// If you increment at the last episode, mark as completed\n\tif ((!isNaN(watchedCount)) && (watchedCount + 1) === totalCount) {\n\t\tdata.data.status = 'completed';\n\t}\n\n\t_.show(_.$('#loading-shadow')[ 0 ]);\n\n\t// okay, lets actually make some changes!\n\t_.ajax(_.url('/anime/increment'), {\n\t\tdata,\n\t\tdataType: 'json',\n\t\ttype: 'POST',\n\t\tsuccess: (res) => {\n\t\t\tconst resData = JSON.parse(res);\n\n\t\t\tif (resData.errors) {\n\t\t\t\t_.hide(_.$('#loading-shadow')[ 0 ]);\n\t\t\t\t_.showMessage('error', `Failed to update ${title}. `);\n\t\t\t\t_.scrollToTop();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (resData.data.attributes.status === 'completed') {\n\t\t\t\t_.hide(parentSel);\n\t\t\t}\n\n\t\t\t_.hide(_.$('#loading-shadow')[ 0 ]);\n\n\t\t\t_.showMessage('success', `Successfully updated ${title}`);\n\t\t\t_.$('.completed_number', parentSel)[ 0 ].textContent = ++watchedCount;\n\t\t\t_.scrollToTop();\n\t\t},\n\t\terror: () => {\n\t\t\t_.hide(_.$('#loading-shadow')[ 0 ]);\n\t\t\t_.showMessage('error', `Failed to update ${title}. `);\n\t\t\t_.scrollToTop();\n\t\t}\n\t});\n});","import _ from './base/AnimeClient.js'\nimport { renderMangaSearchResults } from './template-helpers.js'\n\nconst search = (query) => {\n\t_.$('.cssload-loader')[ 0 ].removeAttribute('hidden');\n\t_.get(_.url('/manga/search'), { query }, (searchResults, status) => {\n\t\tsearchResults = JSON.parse(searchResults);\n\t\t_.$('.cssload-loader')[ 0 ].setAttribute('hidden', 'hidden');\n\t\t_.$('#series-list')[ 0 ].innerHTML = renderMangaSearchResults(searchResults.data);\n\t});\n};\n\nif (_.hasElement('.manga #search')) {\n\t_.on('#search', 'keyup', _.throttle(250, (e) => {\n\t\tlet query = encodeURIComponent(e.target.value);\n\t\tif (query === '') {\n\t\t\treturn;\n\t\t}\n\n\t\tsearch(query);\n\t}));\n}\n\n/**\n * Javascript for editing manga, if logged in\n */\n_.on('.manga.list', 'click', '.edit-buttons button', (e) => {\n\tlet thisSel = e.target;\n\tlet parentSel = _.closestParent(e.target, 'article');\n\tlet type = thisSel.classList.contains('plus-one-chapter') ? 'chapter' : 'volume';\n\tlet completed = parseInt(_.$(`.${type}s_read`, parentSel)[ 0 ].textContent, 10) || 0;\n\tlet total = parseInt(_.$(`.${type}_count`, parentSel)[ 0 ].textContent, 10);\n\tlet mangaName = _.$('.name', parentSel)[ 0 ].textContent;\n\n\tif (isNaN(completed)) {\n\t\tcompleted = 0;\n\t}\n\n\t// Setup the update data\n\tlet data = {\n\t\tid: parentSel.dataset.kitsuId,\n\t\tmal_id: parentSel.dataset.malId,\n\t\tdata: {\n\t\t\tprogress: completed\n\t\t}\n\t};\n\n\t// If the episode count is 0, and incremented,\n\t// change status to currently reading\n\tif (isNaN(completed) || completed === 0) {\n\t\tdata.data.status = 'current';\n\t}\n\n\t// If you increment at the last chapter, mark as completed\n\tif ((!isNaN(completed)) && (completed + 1) === total) {\n\t\tdata.data.status = 'completed';\n\t}\n\n\t// Update the total count\n\tdata.data.progress = ++completed;\n\n\t_.show(_.$('#loading-shadow')[ 0 ]);\n\n\t_.ajax(_.url('/manga/increment'), {\n\t\tdata,\n\t\tdataType: 'json',\n\t\ttype: 'POST',\n\t\tmimeType: 'application/json',\n\t\tsuccess: () => {\n\t\t\tif (data.data.status === 'completed') {\n\t\t\t\t_.hide(parentSel);\n\t\t\t}\n\n\t\t\t_.hide(_.$('#loading-shadow')[ 0 ]);\n\n\t\t\t_.$(`.${type}s_read`, parentSel)[ 0 ].textContent = completed;\n\t\t\t_.showMessage('success', `Successfully updated ${mangaName}`);\n\t\t\t_.scrollToTop();\n\t\t},\n\t\terror: () => {\n\t\t\t_.hide(_.$('#loading-shadow')[ 0 ]);\n\t\t\t_.showMessage('error', `Failed to update ${mangaName}`);\n\t\t\t_.scrollToTop();\n\t\t}\n\t});\n});"],"names":["matches","elm","selector","querySelectorAll","document","ownerDocument","i","length","item","AnimeClient","noop","$","context","nodeType","elements","match","push","getElementById","split","slice","apply","hasElement","scrollToTop","window","scroll","hide","sel","setAttribute","show","removeAttribute","showMessage","type","message","template","undefined","remove","insertAdjacentHTML","closestParent","current","parentSelector","Element","prototype","closest","documentElement","parentElement","url","path","uri","location","host","charAt","throttle","interval","fn","scope","wait","args","setTimeout","addEvent","event","listener","forEach","evt","addEventListener","delegateEvent","target","e","element","call","stopPropagation","on","AnimeClient.on","el","ajaxSerialize","data","pairs","Object","keys","name","value","toString","encodeURIComponent","join","ajax","AnimeClient.ajax","config","defaultConfig","dataType","success","mimeType","error","request","XMLHttpRequest","method","String","toUpperCase","open","onreadystatechange","request.onreadystatechange","readyState","responseText","responseType","JSON","parse","status","response","stringify","setRequestHeader","send","get","AnimeClient.get","callback","_","proceed","confirm","preventDefault","currentTarget","rect","getBoundingClientRect","top","pageYOffset","scrollTo","behavior","id","checked","renderAnimeSearchResults","results","x","attributes","titles","reduce","prev","slug","mal_id","canonicalTitle","renderMangaSearchResults","search","query","searchResults","parentSel","watchedCount","parseInt","totalCount","title","dataset","kitsuId","malId","progress","isNaN","res","resData","errors","search$1","thisSel","classList","contains","completed","total","mangaName"],"mappings":"YAIA,IAAMA,QAAUA,QAAA,CAACC,GAAD,CAAMC,QAAN,CAAmB,CAClC,IAAIF,QAAUG,CAACF,GAAAG,SAADD,EAAiBF,GAAAI,cAAjBF,kBAAA,CAAqDD,QAArD,CAAd,CACCI,EAAIN,OAAAO,OACL,OAAO,EAAED,CAAT,EAAc,CAAd,EAAmBN,OAAAQ,KAAA,CAAaF,CAAb,CAAnB,GAAuCL,GAAvC,EACA,MAAOK,EAAP,CAAY,EAJsB,CAO5B,KAAMG,YAAc,CAI1BC,KAAMA,QAAA,EAAM,EAJc,CAY1B,EAAAC,QAAC,CAACT,QAAD,CAAWU,OAAX,CAA2B,CAAhBA,OAAA,CAAAA,OAAA,GAAA,SAAA,CAAU,IAAV,CAAAA,OACX,IAAI,MAAOV,SAAX,GAAwB,QAAxB,CACC,MAAOA,SAGRU,QAAA,CAAWA,OAAD,GAAa,IAAb,EAAqBA,OAAAC,SAArB,GAA0C,CAA1C,CACPD,OADO,CAEPR,QAEH,KAAIU,SAAW,EACf,IAAIZ,QAAAa,MAAA,CAAe,YAAf,CAAJ,CACCD,QAAAE,KAAA,CAAcZ,QAAAa,eAAA,CAAwBf,QAAAgB,MAAA,CAAe,GAAf,CAAA,CAAoB,CAApB,CAAxB,CAAd,CADD;IAGCJ,SAAA,CAAW,EAAAK,MAAAC,MAAA,CAAeR,OAAAT,iBAAA,CAAyBD,QAAzB,CAAf,CAGZ,OAAOY,SAhBoB,CAZF,CAoC1B,WAAAO,QAAW,CAACnB,QAAD,CAAW,CACrB,MAAOO,YAAAE,EAAA,CAAcT,QAAd,CAAAK,OAAP,CAAwC,CADnB,CApCI,CA4C1B,YAAAe,QAAY,EAAG,CACdC,MAAAC,OAAA,CAAc,CAAd,CAAgB,CAAhB,CADc,CA5CW,CAqD1B,KAAAC,QAAK,CAACC,GAAD,CAAM,CACVA,GAAAC,aAAA,CAAiB,QAAjB,CAA2B,QAA3B,CADU,CArDe,CA8D1B,KAAAC,QAAK,CAACF,GAAD,CAAM,CACVA,GAAAG,gBAAA,CAAoB,QAApB,CADU,CA9De,CAwE1B,YAAAC,QAAY,CAACC,IAAD,CAAOC,OAAP,CAAgB,CAC3B,IAAIC,SACH,sBADGA,CACoBF,IADpBE,CACH,kDADGA,CAGAD,OAHAC,CACH,qDAMD,KAAIP,IAAMjB,WAAAE,EAAA,CAAc,UAAd,CACV;GAAIe,GAAA,CAAI,CAAJ,CAAJ,GAAeQ,SAAf,CACCR,GAAA,CAAI,CAAJ,CAAAS,OAAA,EAGD1B,YAAAE,EAAA,CAAc,QAAd,CAAA,CAAwB,CAAxB,CAAAyB,mBAAA,CAA8C,WAA9C,CAA2DH,QAA3D,CAb2B,CAxEF,CA8F1B,cAAAI,QAAc,CAACC,OAAD,CAAUC,cAAV,CAA0B,CACvC,GAAIC,OAAAC,UAAAC,QAAJ,GAAkCR,SAAlC,CACC,MAAOI,QAAAI,QAAA,CAAgBH,cAAhB,CAGR,OAAOD,OAAP,GAAmBlC,QAAAuC,gBAAnB,CAA6C,CAC5C,GAAI3C,OAAA,CAAQsC,OAAR,CAAiBC,cAAjB,CAAJ,CACC,MAAOD,QAGRA,QAAA,CAAUA,OAAAM,cALkC,CAQ7C,MAAO,KAbgC,CA9Fd,CAmH1B,IAAAC,QAAI,CAACC,IAAD,CAAO,CACV,IAAIC,IAAM,IAANA,CAAW3C,QAAA4C,SAAAC,KACfF,IAAA,EAAQD,IAAAI,OAAA,CAAY,CAAZ,CAAD,GAAoB,GAApB,CAA2BJ,IAA3B,CAAkC,GAAlC,CAAsCA,IAE7C,OAAOC,IAJG,CAnHe,CAmI1B,SAAAI,QAAS,CAACC,QAAD;AAAWC,EAAX,CAAeC,KAAf,CAAsB,CAC9B,IAAIC,KAAO,KACX,OAAO,UAAU,KAAS,CAAT,IAAS,mBAAT,EAAA,KAAA,IAAA,kBAAA,CAAA,CAAA,iBAAA,CAAA,SAAA,OAAA,CAAA,EAAA,iBAAA,CAAS,kBAAT,CAAA,iBAAA,CAAA,CAAA,CAAA,CAAA,SAAA,CAAA,iBAAA,CAAS,EAAA,IAAA,OAAA,kBACzB,KAAM3C,QAAU0C,KAAV1C,EAAmB,IAEzB,IAAK,CAAE2C,IAAP,CAAa,CACZF,EAAAjC,MAAA,CAASR,OAAT,CAAkB4C,MAAlB,CACAD,KAAA,CAAO,IACPE,WAAA,CAAW,UAAW,CACrBF,IAAA,CAAO,KADc,CAAtB,CAEGH,QAFH,CAHY,CAHY,CAAA,CAFI,CAnIL,CAuJ3BM,SAASA,SAAQ,CAAChC,GAAD,CAAMiC,KAAN,CAAaC,QAAb,CAAuB,CAEvC,GAAI,CAAED,KAAA5C,MAAA,CAAY,aAAZ,CAAN,CACC4C,KAAAzC,MAAA,CAAY,GAAZ,CAAA2C,QAAA,CAAyB,QAAA,CAACC,GAAD,CAAS,CACjCJ,QAAA,CAAShC,GAAT,CAAcoC,GAAd,CAAmBF,QAAnB,CADiC,CAAlC,CAKDlC;GAAAqC,iBAAA,CAAqBJ,KAArB,CAA4BC,QAA5B,CAAsC,KAAtC,CARuC,CAWxCI,QAASA,cAAa,CAACtC,GAAD,CAAMuC,MAAN,CAAcN,KAAd,CAAqBC,QAArB,CAA+B,CAEpDF,QAAA,CAAShC,GAAT,CAAciC,KAAd,CAAqB,QAAA,CAACO,CAAD,CAAO,CAE3BzD,WAAAE,EAAA,CAAcsD,MAAd,CAAsBvC,GAAtB,CAAAmC,QAAA,CAAmC,QAAA,CAACM,OAAD,CAAa,CAC/C,GAAGD,CAAAD,OAAH,EAAeE,OAAf,CAAwB,CACvBP,QAAAQ,KAAA,CAAcD,OAAd,CAAuBD,CAAvB,CACAA,EAAAG,gBAAA,EAFuB,CADuB,CAAhD,CAF2B,CAA5B,CAFoD,CAsBrD5D,WAAA6D,GAAA,CAAiBC,QAAA,CAAC7C,GAAD,CAAMiC,KAAN,CAAaM,MAAb,CAAqBL,QAArB,CAAkC,CAClD,GAAIA,QAAJ,GAAiB1B,SAAjB,CAA4B,CAC3B0B,QAAA,CAAWK,MACXxD,YAAAE,EAAA,CAAce,GAAd,CAAAmC,QAAA,CAA2B,QAAA,CAACW,EAAD,CAAQ,CAClCd,QAAA,CAASc,EAAT,CAAab,KAAb,CAAoBC,QAApB,CADkC,CAAnC,CAF2B,CAA5B,IAMCnD,YAAAE,EAAA,CAAce,GAAd,CAAAmC,QAAA,CAA2B,QAAA,CAACW,EAAD,CAAQ,CAClCR,aAAA,CAAcQ,EAAd,CAAkBP,MAAlB,CAA0BN,KAA1B,CAAiCC,QAAjC,CADkC,CAAnC,CAPiD,CAwBnDa,SAASA,cAAa,CAACC,IAAD,CAAO,CAC5B,IAAIC;AAAQ,EAEZC,OAAAC,KAAA,CAAYH,IAAZ,CAAAb,QAAA,CAA0B,QAAA,CAACiB,IAAD,CAAU,CACnC,IAAIC,MAAQL,IAAA,CAAKI,IAAL,CAAAE,SAAA,EAEZF,KAAA,CAAOG,kBAAA,CAAmBH,IAAnB,CACPC,MAAA,CAAQE,kBAAA,CAAmBF,KAAnB,CAERJ,MAAA3D,KAAA,CAAc8D,IAAd,CAAW,GAAX,CAAsBC,KAAtB,CANmC,CAApC,CASA,OAAOJ,MAAAO,KAAA,CAAW,GAAX,CAZqB,CA6B7BzE,WAAA0E,KAAA,CAAmBC,QAAA,CAACvC,GAAD,CAAMwC,MAAN,CAAiB,CAEnC,IAAMC,cAAgB,CACrBZ,KAAM,EADe,CAErB3C,KAAM,KAFe,CAGrBwD,SAAU,EAHW,CAIrBC,QAAS/E,WAAAC,KAJY,CAKrB+E,SAAU,mCALW,CAMrBC,MAAOjF,WAAAC,KANc,CAStB2E,OAAA,CAAS,MAAA,OAAA,CAAA,EAAA,CACLC,aADK,CAELD,MAFK,CAKT,KAAIM,QAAU,IAAIC,cAClB,KAAIC,OAASC,MAAA,CAAOT,MAAAtD,KAAP,CAAAgE,YAAA,EAEb,IAAIF,MAAJ;AAAe,KAAf,CACChD,GAAA,EAAQA,GAAA9B,MAAA,CAAU,IAAV,CAAD,CACJ0D,aAAA,CAAcY,MAAAX,KAAd,CADI,CAEJ,GAFI,CAEAD,aAAA,CAAcY,MAAAX,KAAd,CAGRiB,QAAAK,KAAA,CAAaH,MAAb,CAAqBhD,GAArB,CAEA8C,QAAAM,mBAAA,CAA6BC,QAAA,EAAM,CAClC,GAAIP,OAAAQ,WAAJ,GAA2B,CAA3B,CAA8B,CAC7B,IAAIC,aAAe,EAEnB,IAAIT,OAAAU,aAAJ,GAA6B,MAA7B,CACCD,YAAA,CAAeE,IAAAC,MAAA,CAAWZ,OAAAS,aAAX,CADhB,KAGCA,aAAA,CAAeT,OAAAS,aAGhB,IAAIT,OAAAa,OAAJ,CAAqB,GAArB,CACCnB,MAAAK,MAAAtB,KAAA,CAAkB,IAAlB,CAAwBuB,OAAAa,OAAxB,CAAwCJ,YAAxC,CAAsDT,OAAAc,SAAtD,CADD,KAGCpB,OAAAG,QAAApB,KAAA,CAAoB,IAApB,CAA0BgC,YAA1B,CAAwCT,OAAAa,OAAxC,CAZ4B,CADI,CAkBnC,IAAInB,MAAAE,SAAJ,GAAwB,MAAxB,CAAgC,CAC/BF,MAAAX,KAAA;AAAc4B,IAAAI,UAAA,CAAerB,MAAAX,KAAf,CACdW,OAAAI,SAAA,CAAkB,kBAFa,CAAhC,IAICJ,OAAAX,KAAA,CAAcD,aAAA,CAAcY,MAAAX,KAAd,CAGfiB,QAAAgB,iBAAA,CAAyB,cAAzB,CAAyCtB,MAAAI,SAAzC,CAEA,QAAQI,MAAR,EACC,KAAK,KAAL,CACCF,OAAAiB,KAAA,CAAa,IAAb,CACD,MAEA,SACCjB,OAAAiB,KAAA,CAAavB,MAAAX,KAAb,CACD,MAPD,CAtDmC,CAwEpCjE,YAAAoG,IAAA,CAAkBC,QAAA,CAACjE,GAAD,CAAM6B,IAAN,CAAYqC,QAAZ,CAAgC,CAApBA,QAAA,CAAAA,QAAA,GAAA,SAAA,CAAW,IAAX,CAAAA,QAC7B,IAAIA,QAAJ,GAAiB,IAAjB,CAAuB,CACtBA,QAAA,CAAWrC,IACXA,KAAA,CAAO,EAFe,CAKvB,MAAOjE,YAAA0E,KAAA,CAAiBtC,GAAjB,CAAsB,CAC5B6B,KAAAA,IAD4B,CAE5Bc,QAASuB,QAFmB,CAAtB,CAN0C,iBC3T7C,SAAU,QAAS,WAAY,QAAA,CAAC7C,CAAD,CAAO,CAC1C8C,WAAAA,KAAAA,CAAO9C,CAAAD,OAAP+C,CAD0C;eAKtC,iBAAkB,SAAU,QAAA,CAACrD,KAAD,CAAW,CAC3C,IAAMsD,QAAUC,OAAA,CAAQ,uDAAR,CAEhB,IAAID,OAAJ,GAAgB,KAAhB,CAAuB,CACtBtD,KAAAwD,eAAA,EACAxD,MAAAU,gBAAA,EAFsB,CAHoB,kBAUvC,kBAAmB,QAAS,QAAA,EAAM,CACtC2C,WAAAA,IAAAA,CAAM,cAANA,CAAsB,QAAA,EAAM,CAC3BA,WAAAA,YAAAA,CAAc,SAAdA,CAAyB,+BAAzBA,CAD2B,CAA5BA,CADsC,EAOtCA,YAAAA,GAAAA,CAAK,sBAALA,CAA6B,QAA7BA,CAAuC,QAAA,CAACrD,KAAD,CAAW,CAClD,IAAMa,GAAKb,KAAAyD,cAAAxE,cACX,KAAMyE,KAAO7C,EAAA8C,sBAAA,EAEb;IAAMC,IAAMF,IAAAE,IAANA,CAAiBhG,MAAAiG,YAEvBjG,OAAAkG,SAAA,CAAgB,CACfF,IAAAA,GADe,CAEfG,SAAU,QAFK,CAAhB,CANkD,CAAlDV,iBCvBI,OAAQ,SAAU,aAAc,QAAA,CAAC9C,CAAD,CAAO,CAC3C,IAAMyD,GAAKzD,CAAAD,OAAA0D,GACXvH,SAAAa,eAAA,CAAwB,MAAxB,CAA+B0G,EAA/B,CAAAC,QAAA,CAA+C,IAFJ,EAKrCC,SAASA,0BAA0BnD,KAAM,CAC/C,IAAMoD,QAAU,EAEhBpD,KAAAb,QAAA,CAAa,QAAA,CAAAkE,CAAA,CAAK,CACjB,IAAMvH,KAAOuH,CAAAC,WACb,KAAMC,OAASzH,IAAAyH,OAAAC,OAAA,CAAmB,QAAA,CAACC,IAAD,CAAO7F,OAAP,CAAmB,CACpD,MAAO6F,KAAP,EAAiB7F,OAAjB,CAAc,QAAd,CADoD,CAAtC,CAEZ,EAFY,CAIfwF,QAAA9G,KAAA,CAAa,8HAAb;AAGmDR,IAAA4H,KAHnD,CAAa,yBAAb,CAGsFL,CAAAM,OAHtF,CAAa,4DAAb,CAI+C7H,IAAA4H,KAJ/C,CAAa,qBAAb,CAI8EL,CAAAJ,GAJ9E,CAAa,8BAAb,CAKiBnH,IAAA4H,KALjB,CAAa,4FAAb,CAO4CL,CAAAJ,GAP5C,CAAa,kFAAb,CAQ4CI,CAAAJ,GAR5C,CAAa,2EAAb,CASsCI,CAAAJ,GATtC,CAAa,oHAAb;AAaOnH,IAAA8H,eAbP,CAAa,+BAAb,CAccL,MAdd,CAAa,wNAAb,CAqBiDzH,IAAA4H,KArBjD,CAAa,gGAAb,CANiB,CAAlB,CAmCA,OAAON,QAAA5C,KAAA,CAAa,EAAb,CAtCwC,CAyCzCqD,QAASA,0BAA0B7D,KAAM,CAC/C,IAAMoD,QAAU,EAEhBpD,KAAAb,QAAA,CAAa,QAAA,CAAAkE,CAAA,CAAK,CACjB,IAAMvH,KAAOuH,CAAAC,WACb;IAAMC,OAASzH,IAAAyH,OAAAC,OAAA,CAAmB,QAAA,CAACC,IAAD,CAAO7F,OAAP,CAAmB,CACpD,MAAO6F,KAAP,EAAiB7F,OAAjB,CAAc,QAAd,CADoD,CAAtC,CAEZ,EAFY,CAIfwF,QAAA9G,KAAA,CAAa,4GAAb,CAGiCR,IAAA4H,KAHjC,CAAa,yBAAb,CAGoEL,CAAAM,OAHpE,CAAa,4DAAb,CAI+C7H,IAAA4H,KAJ/C,CAAa,qBAAb,CAI8EL,CAAAJ,GAJ9E,CAAa,8BAAb,CAKiBnH,IAAA4H,KALjB,CAAa,4FAAb,CAO4CL,CAAAJ,GAP5C,CAAa,kFAAb;AAQ4CI,CAAAJ,GAR5C,CAAa,2EAAb,CASsCI,CAAAJ,GATtC,CAAa,sGAAb,CAYOnH,IAAA8H,eAZP,CAAa,+BAAb,CAacL,MAbd,CAAa,wNAAb,CAoBiDzH,IAAA4H,KApBjD,CAAa,gGAAb,CANiB,CAAlB,CAkCA;MAAON,QAAA5C,KAAA,CAAa,EAAb,CArCwC,CC/ChD,IAAMsD,OAASA,QAAA,CAACC,KAAD,CAAW,CAEzBzB,WAAAA,EAAAA,CAAI,iBAAJA,CAAAA,CAAwB,CAAxBA,CAAAA,gBAAAA,CAA4C,QAA5CA,CAGAA,YAAAA,IAAAA,CAAMA,WAAAA,IAAAA,CAAM,0BAANA,CAANA,CAAyC,CAAEyB,MAAAA,KAAF,CAAzCzB,CAAoD,QAAA,CAAC0B,aAAD,CAAgBlC,MAAhB,CAA2B,CAC9EkC,aAAA,CAAgBpC,IAAAC,MAAA,CAAWmC,aAAX,CAGhB1B,YAAAA,EAAAA,CAAI,iBAAJA,CAAAA,CAAwB,CAAxBA,CAAAA,aAAAA,CAAyC,QAAzCA,CAAmD,QAAnDA,CAGAA,YAAAA,EAAAA,CAAI,cAAJA,CAAAA,CAAqB,CAArBA,CAAAA,UAAAA,CAAqCa,wBAAA,CAAyBa,aAAAhE,KAAzB,CAPyC,CAA/EsC,CALyB,CAgB1B,IAAIA,WAAAA,WAAAA,CAAa,gBAAbA,CAAJ,CACCA,WAAAA,GAAAA,CAAK,SAALA,CAAgB,OAAhBA,CAAyBA,WAAAA,SAAAA,CAAW,GAAXA;AAAgB,QAAA,CAAC9C,CAAD,CAAO,CAC/C,IAAMuE,MAAQxD,kBAAA,CAAmBf,CAAAD,OAAAc,MAAnB,CACd,IAAI0D,KAAJ,GAAc,EAAd,CACC,MAGDD,OAAA,CAAOC,KAAP,CAN+C,CAAvBzB,CAAzBA,iBAWI,kBAAmB,QAAS,YAAa,QAAA,CAAC9C,CAAD,CAAO,CACpD,IAAIyE,UAAY3B,WAAAA,cAAAA,CAAgB9C,CAAAD,OAAhB+C,CAA0B,SAA1BA,CAChB,KAAI4B,aAAeC,QAAA,CAAS7B,WAAAA,EAAAA,CAAI,mBAAJA,CAAyB2B,SAAzB3B,CAAAA,CAAqC,CAArCA,CAAAA,YAAT,CAA+D,EAA/D,CAAf4B,EAAqF,CACzF,KAAIE,WAAaD,QAAA,CAAS7B,WAAAA,EAAAA,CAAI,eAAJA,CAAqB2B,SAArB3B,CAAAA,CAAiC,CAAjCA,CAAAA,YAAT,CAA2D,EAA3D,CACjB,KAAI+B,MAAQ/B,WAAAA,EAAAA,CAAI,SAAJA,CAAe2B,SAAf3B,CAAAA,CAA2B,CAA3BA,CAAAA,YAGZ,KAAItC,KAAO,CACViD,GAAIgB,SAAAK,QAAAC,QADM,CAEVZ,OAAQM,SAAAK,QAAAE,MAFE;AAGVxE,KAAM,CACLyE,SAAUP,YAAVO,CAAyB,CADpB,CAHI,CAUX,IAAIC,KAAA,CAAMR,YAAN,CAAJ,EAA2BA,YAA3B,GAA4C,CAA5C,CACClE,IAAAA,KAAA8B,OAAA,CAAmB,SAIpB,IAAK,CAAC4C,KAAA,CAAMR,YAAN,CAAN,EAA+BA,YAA/B,CAA8C,CAA9C,GAAqDE,UAArD,CACCpE,IAAAA,KAAA8B,OAAA,CAAmB,WAGpBQ,YAAAA,KAAAA,CAAOA,WAAAA,EAAAA,CAAI,iBAAJA,CAAAA,CAAwB,CAAxBA,CAAPA,CAGAA,YAAAA,KAAAA,CAAOA,WAAAA,IAAAA,CAAM,kBAANA,CAAPA,CAAkC,CACjCtC,KAAAA,IADiC,CAEjCa,SAAU,MAFuB,CAGjCxD,KAAM,MAH2B,CAIjCyD,QAASA,QAAA,CAAC6D,GAAD,CAAS,CACjB,IAAMC,QAAUhD,IAAAC,MAAA,CAAW8C,GAAX,CAEhB,IAAIC,OAAAC,OAAJ,CAAoB,CACnBvC,WAAAA,KAAAA,CAAOA,WAAAA,EAAAA,CAAI,iBAAJA,CAAAA,CAAwB,CAAxBA,CAAPA,CACAA,YAAAA,YAAAA,CAAc,OAAdA,CAAuB,mBAAvBA;AAA2C+B,KAA3C/B,CAAuB,IAAvBA,CACAA,YAAAA,YAAAA,EACA,OAJmB,CAOpB,GAAIsC,OAAA5E,KAAAsD,WAAAxB,OAAJ,GAAuC,WAAvC,CACCQ,WAAAA,KAAAA,CAAO2B,SAAP3B,CAGDA,YAAAA,KAAAA,CAAOA,WAAAA,EAAAA,CAAI,iBAAJA,CAAAA,CAAwB,CAAxBA,CAAPA,CAEAA,YAAAA,YAAAA,CAAc,SAAdA,CAAyB,uBAAzBA,CAAiD+B,KAAjD/B,CACAA,YAAAA,EAAAA,CAAI,mBAAJA,CAAyB2B,SAAzB3B,CAAAA,CAAqC,CAArCA,CAAAA,YAAAA,CAAuD,EAAE4B,YACzD5B,YAAAA,YAAAA,EAlBiB,CAJe,CAwBjCtB,MAAOA,QAAA,EAAM,CACZsB,WAAAA,KAAAA,CAAOA,WAAAA,EAAAA,CAAI,iBAAJA,CAAAA,CAAwB,CAAxBA,CAAPA,CACAA,YAAAA,YAAAA,CAAc,OAAdA,CAAuB,mBAAvBA,CAA2C+B,KAA3C/B,CAAuB,IAAvBA,CACAA,YAAAA,YAAAA,EAHY,CAxBoB,CAAlCA,CA7BoD,EC5BrD;IAAMwB,SAASgB,QAAA,CAACf,KAAD,CAAW,CACzBzB,WAAAA,EAAAA,CAAI,iBAAJA,CAAAA,CAAwB,CAAxBA,CAAAA,gBAAAA,CAA4C,QAA5CA,CACAA,YAAAA,IAAAA,CAAMA,WAAAA,IAAAA,CAAM,eAANA,CAANA,CAA8B,CAAEyB,MAAAA,KAAF,CAA9BzB,CAAyC,QAAA,CAAC0B,aAAD,CAAgBlC,MAAhB,CAA2B,CACnEkC,aAAA,CAAgBpC,IAAAC,MAAA,CAAWmC,aAAX,CAChB1B,YAAAA,EAAAA,CAAI,iBAAJA,CAAAA,CAAwB,CAAxBA,CAAAA,aAAAA,CAAyC,QAAzCA,CAAmD,QAAnDA,CACAA,YAAAA,EAAAA,CAAI,cAAJA,CAAAA,CAAqB,CAArBA,CAAAA,UAAAA,CAAqCuB,wBAAA,CAAyBG,aAAAhE,KAAzB,CAH8B,CAApEsC,CAFyB,CAS1B,IAAIA,WAAAA,WAAAA,CAAa,gBAAbA,CAAJ,CACCA,WAAAA,GAAAA,CAAK,SAALA,CAAgB,OAAhBA,CAAyBA,WAAAA,SAAAA,CAAW,GAAXA,CAAgB,QAAA,CAAC9C,CAAD,CAAO,CAC/C,IAAIuE;AAAQxD,kBAAA,CAAmBf,CAAAD,OAAAc,MAAnB,CACZ,IAAI0D,KAAJ,GAAc,EAAd,CACC,MAGDD,SAAAA,CAAOC,KAAPD,CAN+C,CAAvBxB,CAAzBA,iBAaI,cAAe,QAAS,uBAAwB,QAAA,CAAC9C,CAAD,CAAO,CAC3D,IAAIuF,QAAUvF,CAAAD,OACd,KAAI0E,UAAY3B,WAAAA,cAAAA,CAAgB9C,CAAAD,OAAhB+C,CAA0B,SAA1BA,CAChB,KAAIjF,KAAO0H,OAAAC,UAAAC,SAAA,CAA2B,kBAA3B,CAAA,CAAiD,SAAjD,CAA6D,QACxE,KAAIC,UAAYf,QAAA,CAAS7B,WAAAA,EAAAA,CAAI,GAAJA,CAAQjF,IAARiF,CAAI,QAAJA,CAAsB2B,SAAtB3B,CAAAA,CAAkC,CAAlCA,CAAAA,YAAT,CAA4D,EAA5D,CAAZ4C,EAA+E,CACnF,KAAIC,MAAQhB,QAAA,CAAS7B,WAAAA,EAAAA,CAAI,GAAJA,CAAQjF,IAARiF,CAAI,QAAJA,CAAsB2B,SAAtB3B,CAAAA,CAAkC,CAAlCA,CAAAA,YAAT,CAA4D,EAA5D,CACZ,KAAI8C,UAAY9C,WAAAA,EAAAA,CAAI,OAAJA;AAAa2B,SAAb3B,CAAAA,CAAyB,CAAzBA,CAAAA,YAEhB,IAAIoC,KAAA,CAAMQ,SAAN,CAAJ,CACCA,SAAA,CAAY,CAIb,KAAIlF,KAAO,CACViD,GAAIgB,SAAAK,QAAAC,QADM,CAEVZ,OAAQM,SAAAK,QAAAE,MAFE,CAGVxE,KAAM,CACLyE,SAAUS,SADL,CAHI,CAUX,IAAIR,KAAA,CAAMQ,SAAN,CAAJ,EAAwBA,SAAxB,GAAsC,CAAtC,CACClF,IAAAA,KAAA8B,OAAA,CAAmB,SAIpB,IAAK,CAAC4C,KAAA,CAAMQ,SAAN,CAAN,EAA4BA,SAA5B,CAAwC,CAAxC,GAA+CC,KAA/C,CACCnF,IAAAA,KAAA8B,OAAA,CAAmB,WAIpB9B,KAAAA,KAAAyE,SAAA,CAAqB,EAAES,SAEvB5C,YAAAA,KAAAA,CAAOA,WAAAA,EAAAA,CAAI,iBAAJA,CAAAA,CAAwB,CAAxBA,CAAPA,CAEAA,YAAAA,KAAAA,CAAOA,WAAAA,IAAAA,CAAM,kBAANA,CAAPA,CAAkC,CACjCtC,KAAAA,IADiC,CAEjCa,SAAU,MAFuB,CAGjCxD,KAAM,MAH2B,CAIjC0D,SAAU,kBAJuB,CAKjCD,QAASA,QAAA,EAAM,CACd,GAAId,IAAAA,KAAA8B,OAAJ;AAAyB,WAAzB,CACCQ,WAAAA,KAAAA,CAAO2B,SAAP3B,CAGDA,YAAAA,KAAAA,CAAOA,WAAAA,EAAAA,CAAI,iBAAJA,CAAAA,CAAwB,CAAxBA,CAAPA,CAEAA,YAAAA,EAAAA,CAAI,GAAJA,CAAQjF,IAARiF,CAAI,QAAJA,CAAsB2B,SAAtB3B,CAAAA,CAAkC,CAAlCA,CAAAA,YAAAA,CAAoD4C,SACpD5C,YAAAA,YAAAA,CAAc,SAAdA,CAAyB,uBAAzBA,CAAiD8C,SAAjD9C,CACAA,YAAAA,YAAAA,EATc,CALkB,CAgBjCtB,MAAOA,QAAA,EAAM,CACZsB,WAAAA,KAAAA,CAAOA,WAAAA,EAAAA,CAAI,iBAAJA,CAAAA,CAAwB,CAAxBA,CAAPA,CACAA,YAAAA,YAAAA,CAAc,OAAdA,CAAuB,mBAAvBA,CAA2C8C,SAA3C9C,CACAA,YAAAA,YAAAA,EAHY,CAhBoB,CAAlCA,CArC2D;"} \ No newline at end of file diff --git a/public/js/scripts.min.js b/public/js/scripts.min.js new file mode 100644 index 00000000..0f7b3dc7 --- /dev/null +++ b/public/js/scripts.min.js @@ -0,0 +1,11 @@ +(function(){var matches=function(elm,selector){var matches=(elm.document||elm.ownerDocument).querySelectorAll(selector),i=matches.length;while(--i>=0&&matches.item(i)!==elm);return i>-1};var AnimeClient={noop:function(){},$:function(selector,context){context=context===undefined?null:context;if(typeof selector!=="string")return selector;context=context!==null&&context.nodeType===1?context:document;var elements=[];if(selector.match(/^#([\w]+$)/))elements.push(document.getElementById(selector.split("#")[1])); +else elements=[].slice.apply(context.querySelectorAll(selector));return elements},hasElement:function(selector){return AnimeClient.$(selector).length>0},scrollToTop:function(){window.scroll(0,0)},hide:function(sel){sel.setAttribute("hidden","hidden")},show:function(sel){sel.removeAttribute("hidden")},showMessage:function(type,message){var template="
\n\t\t\t\t\n\t\t\t\t"+message+"\n\t\t\t\t\n\t\t\t
";var sel=AnimeClient.$(".message"); +if(sel[0]!==undefined)sel[0].remove();AnimeClient.$("header")[0].insertAdjacentHTML("beforeend",template)},closestParent:function(current,parentSelector){if(Element.prototype.closest!==undefined)return current.closest(parentSelector);while(current!==document.documentElement){if(matches(current,parentSelector))return current;current=current.parentElement}return null},url:function(path){var uri="//"+document.location.host;uri+=path.charAt(0)==="/"?path:"/"+path;return uri},throttle:function(interval, +fn,scope){var wait=false;return function(args){var $jscomp$restParams=[];for(var $jscomp$restIndex=0;$jscomp$restIndex299)config.error.call(null,request.status,responseText,request.response);else config.success.call(null,responseText,request.status)}};if(config.dataType==="json"){config.data= +JSON.stringify(config.data);config.mimeType="application/json"}else config.data=ajaxSerialize(config.data);request.setRequestHeader("Content-Type",config.mimeType);switch(method){case "GET":request.send(null);break;default:request.send(config.data);break}};AnimeClient.get=function(url,data,callback){callback=callback===undefined?null:callback;if(callback===null){callback=data;data={}}return AnimeClient.ajax(url,{data:data,success:callback})};AnimeClient.on("header","click",".message",function(e){AnimeClient.hide(e.target)}); +AnimeClient.on("form.js-delete","submit",function(event){var proceed=confirm("Are you ABSOLUTELY SURE you want to delete this item?");if(proceed===false){event.preventDefault();event.stopPropagation()}});AnimeClient.on(".js-clear-cache","click",function(){AnimeClient.get("/cache_purge",function(){AnimeClient.showMessage("success","Successfully purged api cache")})});AnimeClient.on(".vertical-tabs input","change",function(event){var el=event.currentTarget.parentElement;var rect=el.getBoundingClientRect(); +var top=rect.top+window.pageYOffset;window.scrollTo({top:top,behavior:"smooth"})})})(); +//# sourceMappingURL=scripts.min.js.map diff --git a/public/js/scripts.min.js.map b/public/js/scripts.min.js.map new file mode 100644 index 00000000..14186577 --- /dev/null +++ b/public/js/scripts.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"scripts.min.js.map","sources":["src/base/AnimeClient.js","src/base/events.js"],"sourcesContent":["// -------------------------------------------------------------------------\n// ! Base\n// -------------------------------------------------------------------------\n\nconst matches = (elm, selector) => {\n\tlet matches = (elm.document || elm.ownerDocument).querySelectorAll(selector),\n\t\ti = matches.length;\n\twhile (--i >= 0 && matches.item(i) !== elm) {};\n\treturn i > -1;\n}\n\nexport const AnimeClient = {\n\t/**\n\t * Placeholder function\n\t */\n\tnoop: () => {},\n\t/**\n\t * DOM selector\n\t *\n\t * @param {string} selector - The dom selector string\n\t * @param {object} [context]\n\t * @return {[HTMLElement]} - array of dom elements\n\t */\n\t$(selector, context = null) {\n\t\tif (typeof selector !== 'string') {\n\t\t\treturn selector;\n\t\t}\n\n\t\tcontext = (context !== null && context.nodeType === 1)\n\t\t\t? context\n\t\t\t: document;\n\n\t\tlet elements = [];\n\t\tif (selector.match(/^#([\\w]+$)/)) {\n\t\t\telements.push(document.getElementById(selector.split('#')[1]));\n\t\t} else {\n\t\t\telements = [].slice.apply(context.querySelectorAll(selector));\n\t\t}\n\n\t\treturn elements;\n\t},\n\t/**\n\t * Does the selector exist on the current page?\n\t *\n\t * @param {string} selector\n\t * @returns {boolean}\n\t */\n\thasElement (selector) {\n\t\treturn AnimeClient.$(selector).length > 0;\n\t},\n\t/**\n\t * Scroll to the top of the Page\n\t *\n\t * @return {void}\n\t */\n\tscrollToTop () {\n\t\twindow.scroll(0,0);\n\t},\n\t/**\n\t * Hide the selected element\n\t *\n\t * @param {string|Element} sel - the selector of the element to hide\n\t * @return {void}\n\t */\n\thide (sel) {\n\t\tsel.setAttribute('hidden', 'hidden');\n\t},\n\t/**\n\t * UnHide the selected element\n\t *\n\t * @param {string|Element} sel - the selector of the element to hide\n\t * @return {void}\n\t */\n\tshow (sel) {\n\t\tsel.removeAttribute('hidden');\n\t},\n\t/**\n\t * Display a message box\n\t *\n\t * @param {string} type - message type: info, error, success\n\t * @param {string} message - the message itself\n\t * @return {void}\n\t */\n\tshowMessage (type, message) {\n\t\tlet template =\n\t\t\t`
\n\t\t\t\t\n\t\t\t\t${message}\n\t\t\t\t\n\t\t\t
`;\n\n\t\tlet sel = AnimeClient.$('.message');\n\t\tif (sel[0] !== undefined) {\n\t\t\tsel[0].remove();\n\t\t}\n\n\t\tAnimeClient.$('header')[0].insertAdjacentHTML('beforeend', template);\n\t},\n\t/**\n\t * Finds the closest parent element matching the passed selector\n\t *\n\t * @param {HTMLElement} current - the current HTMLElement\n\t * @param {string} parentSelector - selector for the parent element\n\t * @return {HTMLElement|null} - the parent element\n\t */\n\tclosestParent (current, parentSelector) {\n\t\tif (Element.prototype.closest !== undefined) {\n\t\t\treturn current.closest(parentSelector);\n\t\t}\n\n\t\twhile (current !== document.documentElement) {\n\t\t\tif (matches(current, parentSelector)) {\n\t\t\t\treturn current;\n\t\t\t}\n\n\t\t\tcurrent = current.parentElement;\n\t\t}\n\n\t\treturn null;\n\t},\n\t/**\n\t * Generate a full url from a relative path\n\t *\n\t * @param {string} path - url path\n\t * @return {string} - full url\n\t */\n\turl (path) {\n\t\tlet uri = `//${document.location.host}`;\n\t\turi += (path.charAt(0) === '/') ? path : `/${path}`;\n\n\t\treturn uri;\n\t},\n\t/**\n\t * Throttle execution of a function\n\t *\n\t * @see https://remysharp.com/2010/07/21/throttling-function-calls\n\t * @see https://jsfiddle.net/jonathansampson/m7G64/\n\t * @param {Number} interval - the minimum throttle time in ms\n\t * @param {Function} fn - the function to throttle\n\t * @param {Object} [scope] - the 'this' object for the function\n\t * @return {Function}\n\t */\n\tthrottle (interval, fn, scope) {\n\t\tlet wait = false;\n\t\treturn function (...args) {\n\t\t\tconst context = scope || this;\n\n\t\t\tif ( ! wait) {\n\t\t\t\tfn.apply(context, args);\n\t\t\t\twait = true;\n\t\t\t\tsetTimeout(function() {\n\t\t\t\t\twait = false;\n\t\t\t\t}, interval);\n\t\t\t}\n\t\t};\n\t},\n};\n\n// -------------------------------------------------------------------------\n// ! Events\n// -------------------------------------------------------------------------\n\nfunction addEvent(sel, event, listener) {\n\t// Recurse!\n\tif (! event.match(/^([\\w\\-]+)$/)) {\n\t\tevent.split(' ').forEach((evt) => {\n\t\t\taddEvent(sel, evt, listener);\n\t\t});\n\t}\n\n\tsel.addEventListener(event, listener, false);\n}\n\nfunction delegateEvent(sel, target, event, listener) {\n\t// Attach the listener to the parent\n\taddEvent(sel, event, (e) => {\n\t\t// Get live version of the target selector\n\t\tAnimeClient.$(target, sel).forEach((element) => {\n\t\t\tif(e.target == element) {\n\t\t\t\tlistener.call(element, e);\n\t\t\t\te.stopPropagation();\n\t\t\t}\n\t\t});\n\t});\n}\n\n/**\n * Add an event listener\n *\n * @param {string|HTMLElement} sel - the parent selector to bind to\n * @param {string} event - event name(s) to bind\n * @param {string|HTMLElement|function} target - the element to directly bind the event to\n * @param {function} [listener] - event listener callback\n * @return {void}\n */\nAnimeClient.on = (sel, event, target, listener) => {\n\tif (listener === undefined) {\n\t\tlistener = target;\n\t\tAnimeClient.$(sel).forEach((el) => {\n\t\t\taddEvent(el, event, listener);\n\t\t});\n\t} else {\n\t\tAnimeClient.$(sel).forEach((el) => {\n\t\t\tdelegateEvent(el, target, event, listener);\n\t\t});\n\t}\n};\n\n// -------------------------------------------------------------------------\n// ! Ajax\n// -------------------------------------------------------------------------\n\n/**\n * Url encoding for non-get requests\n *\n * @param data\n * @returns {string}\n * @private\n */\nfunction ajaxSerialize(data) {\n\tlet pairs = [];\n\n\tObject.keys(data).forEach((name) => {\n\t\tlet value = data[name].toString();\n\n\t\tname = encodeURIComponent(name);\n\t\tvalue = encodeURIComponent(value);\n\n\t\tpairs.push(`${name}=${value}`);\n\t});\n\n\treturn pairs.join('&');\n}\n\n/**\n * Make an ajax request\n *\n * Config:{\n * \tdata: // data to send with the request\n * \ttype: // http verb of the request, defaults to GET\n * \tsuccess: // success callback\n * \terror: // error callback\n * }\n *\n * @param {string} url - the url to request\n * @param {Object} config - the configuration object\n * @return {void}\n */\nAnimeClient.ajax = (url, config) => {\n\t// Set some sane defaults\n\tconst defaultConfig = {\n\t\tdata: {},\n\t\ttype: 'GET',\n\t\tdataType: '',\n\t\tsuccess: AnimeClient.noop,\n\t\tmimeType: 'application/x-www-form-urlencoded',\n\t\terror: AnimeClient.noop\n\t}\n\n\tconfig = {\n\t\t...defaultConfig,\n\t\t...config,\n\t}\n\n\tlet request = new XMLHttpRequest();\n\tlet method = String(config.type).toUpperCase();\n\n\tif (method === 'GET') {\n\t\turl += (url.match(/\\?/))\n\t\t\t? ajaxSerialize(config.data)\n\t\t\t: `?${ajaxSerialize(config.data)}`;\n\t}\n\n\trequest.open(method, url);\n\n\trequest.onreadystatechange = () => {\n\t\tif (request.readyState === 4) {\n\t\t\tlet responseText = '';\n\n\t\t\tif (request.responseType === 'json') {\n\t\t\t\tresponseText = JSON.parse(request.responseText);\n\t\t\t} else {\n\t\t\t\tresponseText = request.responseText;\n\t\t\t}\n\n\t\t\tif (request.status > 299) {\n\t\t\t\tconfig.error.call(null, request.status, responseText, request.response);\n\t\t\t} else {\n\t\t\t\tconfig.success.call(null, responseText, request.status);\n\t\t\t}\n\t\t}\n\t};\n\n\tif (config.dataType === 'json') {\n\t\tconfig.data = JSON.stringify(config.data);\n\t\tconfig.mimeType = 'application/json';\n\t} else {\n\t\tconfig.data = ajaxSerialize(config.data);\n\t}\n\n\trequest.setRequestHeader('Content-Type', config.mimeType);\n\n\tswitch (method) {\n\t\tcase 'GET':\n\t\t\trequest.send(null);\n\t\tbreak;\n\n\t\tdefault:\n\t\t\trequest.send(config.data);\n\t\tbreak;\n\t}\n};\n\n/**\n * Do a get request\n *\n * @param {string} url\n * @param {object|function} data\n * @param {function} [callback]\n */\nAnimeClient.get = (url, data, callback = null) => {\n\tif (callback === null) {\n\t\tcallback = data;\n\t\tdata = {};\n\t}\n\n\treturn AnimeClient.ajax(url, {\n\t\tdata,\n\t\tsuccess: callback\n\t});\n};\n\n// -------------------------------------------------------------------------\n// Export\n// -------------------------------------------------------------------------\n\nexport default AnimeClient;","import _ from './AnimeClient.js';\n/**\n * Event handlers\n */\n// Close event for messages\n_.on('header', 'click', '.message', (e) => {\n\t_.hide(e.target);\n});\n\n// Confirm deleting of list or library items\n_.on('form.js-delete', 'submit', (event) => {\n\tconst proceed = confirm('Are you ABSOLUTELY SURE you want to delete this item?');\n\n\tif (proceed === false) {\n\t\tevent.preventDefault();\n\t\tevent.stopPropagation();\n\t}\n});\n\n// Clear the api cache\n_.on('.js-clear-cache', 'click', () => {\n\t_.get('/cache_purge', () => {\n\t\t_.showMessage('success', 'Successfully purged api cache');\n\t});\n});\n\n// Alleviate some page jumping\n _.on('.vertical-tabs input', 'change', (event) => {\n\tconst el = event.currentTarget.parentElement;\n\tconst rect = el.getBoundingClientRect();\n\n\tconst top = rect.top + window.pageYOffset;\n\n\twindow.scrollTo({\n\t\ttop,\n\t\tbehavior: 'smooth',\n\t});\n});\n"],"names":["matches","elm","selector","querySelectorAll","document","ownerDocument","i","length","item","AnimeClient","noop","$","context","nodeType","elements","match","push","getElementById","split","slice","apply","hasElement","scrollToTop","window","scroll","hide","sel","setAttribute","show","removeAttribute","showMessage","type","message","template","undefined","remove","insertAdjacentHTML","closestParent","current","parentSelector","Element","prototype","closest","documentElement","parentElement","url","path","uri","location","host","charAt","throttle","interval","fn","scope","wait","args","setTimeout","addEvent","event","listener","forEach","evt","addEventListener","delegateEvent","target","e","element","call","stopPropagation","on","AnimeClient.on","el","ajaxSerialize","data","pairs","Object","keys","name","value","toString","encodeURIComponent","join","ajax","AnimeClient.ajax","config","defaultConfig","dataType","success","mimeType","error","request","XMLHttpRequest","method","String","toUpperCase","open","onreadystatechange","request.onreadystatechange","readyState","responseText","responseType","JSON","parse","status","response","stringify","setRequestHeader","send","get","AnimeClient.get","callback","_","proceed","confirm","preventDefault","currentTarget","rect","getBoundingClientRect","top","pageYOffset","scrollTo","behavior"],"mappings":"YAIA,IAAMA,QAAUA,QAAA,CAACC,GAAD,CAAMC,QAAN,CAAmB,CAClC,IAAIF,QAAUG,CAACF,GAAAG,SAADD,EAAiBF,GAAAI,cAAjBF,kBAAA,CAAqDD,QAArD,CAAd,CACCI,EAAIN,OAAAO,OACL,OAAO,EAAED,CAAT,EAAc,CAAd,EAAmBN,OAAAQ,KAAA,CAAaF,CAAb,CAAnB,GAAuCL,GAAvC,EACA,MAAOK,EAAP,CAAY,EAJsB,CAO5B,KAAMG,YAAc,CAI1BC,KAAMA,QAAA,EAAM,EAJc,CAY1B,EAAAC,QAAC,CAACT,QAAD,CAAWU,OAAX,CAA2B,CAAhBA,OAAA,CAAAA,OAAA,GAAA,SAAA,CAAU,IAAV,CAAAA,OACX,IAAI,MAAOV,SAAX,GAAwB,QAAxB,CACC,MAAOA,SAGRU,QAAA,CAAWA,OAAD,GAAa,IAAb,EAAqBA,OAAAC,SAArB,GAA0C,CAA1C,CACPD,OADO,CAEPR,QAEH,KAAIU,SAAW,EACf,IAAIZ,QAAAa,MAAA,CAAe,YAAf,CAAJ,CACCD,QAAAE,KAAA,CAAcZ,QAAAa,eAAA,CAAwBf,QAAAgB,MAAA,CAAe,GAAf,CAAA,CAAoB,CAApB,CAAxB,CAAd,CADD;IAGCJ,SAAA,CAAW,EAAAK,MAAAC,MAAA,CAAeR,OAAAT,iBAAA,CAAyBD,QAAzB,CAAf,CAGZ,OAAOY,SAhBoB,CAZF,CAoC1B,WAAAO,QAAW,CAACnB,QAAD,CAAW,CACrB,MAAOO,YAAAE,EAAA,CAAcT,QAAd,CAAAK,OAAP,CAAwC,CADnB,CApCI,CA4C1B,YAAAe,QAAY,EAAG,CACdC,MAAAC,OAAA,CAAc,CAAd,CAAgB,CAAhB,CADc,CA5CW,CAqD1B,KAAAC,QAAK,CAACC,GAAD,CAAM,CACVA,GAAAC,aAAA,CAAiB,QAAjB,CAA2B,QAA3B,CADU,CArDe,CA8D1B,KAAAC,QAAK,CAACF,GAAD,CAAM,CACVA,GAAAG,gBAAA,CAAoB,QAApB,CADU,CA9De,CAwE1B,YAAAC,QAAY,CAACC,IAAD,CAAOC,OAAP,CAAgB,CAC3B,IAAIC,SACH,sBADGA,CACoBF,IADpBE,CACH,kDADGA,CAGAD,OAHAC,CACH,qDAMD,KAAIP,IAAMjB,WAAAE,EAAA,CAAc,UAAd,CACV;GAAIe,GAAA,CAAI,CAAJ,CAAJ,GAAeQ,SAAf,CACCR,GAAA,CAAI,CAAJ,CAAAS,OAAA,EAGD1B,YAAAE,EAAA,CAAc,QAAd,CAAA,CAAwB,CAAxB,CAAAyB,mBAAA,CAA8C,WAA9C,CAA2DH,QAA3D,CAb2B,CAxEF,CA8F1B,cAAAI,QAAc,CAACC,OAAD,CAAUC,cAAV,CAA0B,CACvC,GAAIC,OAAAC,UAAAC,QAAJ,GAAkCR,SAAlC,CACC,MAAOI,QAAAI,QAAA,CAAgBH,cAAhB,CAGR,OAAOD,OAAP,GAAmBlC,QAAAuC,gBAAnB,CAA6C,CAC5C,GAAI3C,OAAA,CAAQsC,OAAR,CAAiBC,cAAjB,CAAJ,CACC,MAAOD,QAGRA,QAAA,CAAUA,OAAAM,cALkC,CAQ7C,MAAO,KAbgC,CA9Fd,CAmH1B,IAAAC,QAAI,CAACC,IAAD,CAAO,CACV,IAAIC,IAAM,IAANA,CAAW3C,QAAA4C,SAAAC,KACfF,IAAA,EAAQD,IAAAI,OAAA,CAAY,CAAZ,CAAD,GAAoB,GAApB,CAA2BJ,IAA3B,CAAkC,GAAlC,CAAsCA,IAE7C,OAAOC,IAJG,CAnHe,CAmI1B,SAAAI,QAAS,CAACC,QAAD;AAAWC,EAAX,CAAeC,KAAf,CAAsB,CAC9B,IAAIC,KAAO,KACX,OAAO,UAAU,KAAS,CAAT,IAAS,mBAAT,EAAA,KAAA,IAAA,kBAAA,CAAA,CAAA,iBAAA,CAAA,SAAA,OAAA,CAAA,EAAA,iBAAA,CAAS,kBAAT,CAAA,iBAAA,CAAA,CAAA,CAAA,CAAA,SAAA,CAAA,iBAAA,CAAS,EAAA,IAAA,OAAA,kBACzB,KAAM3C,QAAU0C,KAAV1C,EAAmB,IAEzB,IAAK,CAAE2C,IAAP,CAAa,CACZF,EAAAjC,MAAA,CAASR,OAAT,CAAkB4C,MAAlB,CACAD,KAAA,CAAO,IACPE,WAAA,CAAW,UAAW,CACrBF,IAAA,CAAO,KADc,CAAtB,CAEGH,QAFH,CAHY,CAHY,CAAA,CAFI,CAnIL,CAuJ3BM,SAASA,SAAQ,CAAChC,GAAD,CAAMiC,KAAN,CAAaC,QAAb,CAAuB,CAEvC,GAAI,CAAED,KAAA5C,MAAA,CAAY,aAAZ,CAAN,CACC4C,KAAAzC,MAAA,CAAY,GAAZ,CAAA2C,QAAA,CAAyB,QAAA,CAACC,GAAD,CAAS,CACjCJ,QAAA,CAAShC,GAAT,CAAcoC,GAAd,CAAmBF,QAAnB,CADiC,CAAlC,CAKDlC;GAAAqC,iBAAA,CAAqBJ,KAArB,CAA4BC,QAA5B,CAAsC,KAAtC,CARuC,CAWxCI,QAASA,cAAa,CAACtC,GAAD,CAAMuC,MAAN,CAAcN,KAAd,CAAqBC,QAArB,CAA+B,CAEpDF,QAAA,CAAShC,GAAT,CAAciC,KAAd,CAAqB,QAAA,CAACO,CAAD,CAAO,CAE3BzD,WAAAE,EAAA,CAAcsD,MAAd,CAAsBvC,GAAtB,CAAAmC,QAAA,CAAmC,QAAA,CAACM,OAAD,CAAa,CAC/C,GAAGD,CAAAD,OAAH,EAAeE,OAAf,CAAwB,CACvBP,QAAAQ,KAAA,CAAcD,OAAd,CAAuBD,CAAvB,CACAA,EAAAG,gBAAA,EAFuB,CADuB,CAAhD,CAF2B,CAA5B,CAFoD,CAsBrD5D,WAAA6D,GAAA,CAAiBC,QAAA,CAAC7C,GAAD,CAAMiC,KAAN,CAAaM,MAAb,CAAqBL,QAArB,CAAkC,CAClD,GAAIA,QAAJ,GAAiB1B,SAAjB,CAA4B,CAC3B0B,QAAA,CAAWK,MACXxD,YAAAE,EAAA,CAAce,GAAd,CAAAmC,QAAA,CAA2B,QAAA,CAACW,EAAD,CAAQ,CAClCd,QAAA,CAASc,EAAT,CAAab,KAAb,CAAoBC,QAApB,CADkC,CAAnC,CAF2B,CAA5B,IAMCnD,YAAAE,EAAA,CAAce,GAAd,CAAAmC,QAAA,CAA2B,QAAA,CAACW,EAAD,CAAQ,CAClCR,aAAA,CAAcQ,EAAd,CAAkBP,MAAlB,CAA0BN,KAA1B,CAAiCC,QAAjC,CADkC,CAAnC,CAPiD,CAwBnDa,SAASA,cAAa,CAACC,IAAD,CAAO,CAC5B,IAAIC;AAAQ,EAEZC,OAAAC,KAAA,CAAYH,IAAZ,CAAAb,QAAA,CAA0B,QAAA,CAACiB,IAAD,CAAU,CACnC,IAAIC,MAAQL,IAAA,CAAKI,IAAL,CAAAE,SAAA,EAEZF,KAAA,CAAOG,kBAAA,CAAmBH,IAAnB,CACPC,MAAA,CAAQE,kBAAA,CAAmBF,KAAnB,CAERJ,MAAA3D,KAAA,CAAc8D,IAAd,CAAW,GAAX,CAAsBC,KAAtB,CANmC,CAApC,CASA,OAAOJ,MAAAO,KAAA,CAAW,GAAX,CAZqB,CA6B7BzE,WAAA0E,KAAA,CAAmBC,QAAA,CAACvC,GAAD,CAAMwC,MAAN,CAAiB,CAEnC,IAAMC,cAAgB,CACrBZ,KAAM,EADe,CAErB3C,KAAM,KAFe,CAGrBwD,SAAU,EAHW,CAIrBC,QAAS/E,WAAAC,KAJY,CAKrB+E,SAAU,mCALW,CAMrBC,MAAOjF,WAAAC,KANc,CAStB2E,OAAA,CAAS,MAAA,OAAA,CAAA,EAAA,CACLC,aADK,CAELD,MAFK,CAKT,KAAIM,QAAU,IAAIC,cAClB,KAAIC,OAASC,MAAA,CAAOT,MAAAtD,KAAP,CAAAgE,YAAA,EAEb,IAAIF,MAAJ;AAAe,KAAf,CACChD,GAAA,EAAQA,GAAA9B,MAAA,CAAU,IAAV,CAAD,CACJ0D,aAAA,CAAcY,MAAAX,KAAd,CADI,CAEJ,GAFI,CAEAD,aAAA,CAAcY,MAAAX,KAAd,CAGRiB,QAAAK,KAAA,CAAaH,MAAb,CAAqBhD,GAArB,CAEA8C,QAAAM,mBAAA,CAA6BC,QAAA,EAAM,CAClC,GAAIP,OAAAQ,WAAJ,GAA2B,CAA3B,CAA8B,CAC7B,IAAIC,aAAe,EAEnB,IAAIT,OAAAU,aAAJ,GAA6B,MAA7B,CACCD,YAAA,CAAeE,IAAAC,MAAA,CAAWZ,OAAAS,aAAX,CADhB,KAGCA,aAAA,CAAeT,OAAAS,aAGhB,IAAIT,OAAAa,OAAJ,CAAqB,GAArB,CACCnB,MAAAK,MAAAtB,KAAA,CAAkB,IAAlB,CAAwBuB,OAAAa,OAAxB,CAAwCJ,YAAxC,CAAsDT,OAAAc,SAAtD,CADD,KAGCpB,OAAAG,QAAApB,KAAA,CAAoB,IAApB,CAA0BgC,YAA1B,CAAwCT,OAAAa,OAAxC,CAZ4B,CADI,CAkBnC,IAAInB,MAAAE,SAAJ,GAAwB,MAAxB,CAAgC,CAC/BF,MAAAX,KAAA;AAAc4B,IAAAI,UAAA,CAAerB,MAAAX,KAAf,CACdW,OAAAI,SAAA,CAAkB,kBAFa,CAAhC,IAICJ,OAAAX,KAAA,CAAcD,aAAA,CAAcY,MAAAX,KAAd,CAGfiB,QAAAgB,iBAAA,CAAyB,cAAzB,CAAyCtB,MAAAI,SAAzC,CAEA,QAAQI,MAAR,EACC,KAAK,KAAL,CACCF,OAAAiB,KAAA,CAAa,IAAb,CACD,MAEA,SACCjB,OAAAiB,KAAA,CAAavB,MAAAX,KAAb,CACD,MAPD,CAtDmC,CAwEpCjE,YAAAoG,IAAA,CAAkBC,QAAA,CAACjE,GAAD,CAAM6B,IAAN,CAAYqC,QAAZ,CAAgC,CAApBA,QAAA,CAAAA,QAAA,GAAA,SAAA,CAAW,IAAX,CAAAA,QAC7B,IAAIA,QAAJ,GAAiB,IAAjB,CAAuB,CACtBA,QAAA,CAAWrC,IACXA,KAAA,CAAO,EAFe,CAKvB,MAAOjE,YAAA0E,KAAA,CAAiBtC,GAAjB,CAAsB,CAC5B6B,KAAAA,IAD4B,CAE5Bc,QAASuB,QAFmB,CAAtB,CAN0C,iBC3T7C,SAAU,QAAS,WAAY,QAAA,CAAC7C,CAAD,CAAO,CAC1C8C,WAAAA,KAAAA,CAAO9C,CAAAD,OAAP+C,CAD0C;eAKtC,iBAAkB,SAAU,QAAA,CAACrD,KAAD,CAAW,CAC3C,IAAMsD,QAAUC,OAAA,CAAQ,uDAAR,CAEhB,IAAID,OAAJ,GAAgB,KAAhB,CAAuB,CACtBtD,KAAAwD,eAAA,EACAxD,MAAAU,gBAAA,EAFsB,CAHoB,kBAUvC,kBAAmB,QAAS,QAAA,EAAM,CACtC2C,WAAAA,IAAAA,CAAM,cAANA,CAAsB,QAAA,EAAM,CAC3BA,WAAAA,YAAAA,CAAc,SAAdA,CAAyB,+BAAzBA,CAD2B,CAA5BA,CADsC,EAOtCA,YAAAA,GAAAA,CAAK,sBAALA,CAA6B,QAA7BA,CAAuC,QAAA,CAACrD,KAAD,CAAW,CAClD,IAAMa,GAAKb,KAAAyD,cAAAxE,cACX,KAAMyE,KAAO7C,EAAA8C,sBAAA,EAEb;IAAMC,IAAMF,IAAAE,IAANA,CAAiBhG,MAAAiG,YAEvBjG,OAAAkG,SAAA,CAAgB,CACfF,IAAAA,GADe,CAEfG,SAAU,QAFK,CAAhB,CANkD,CAAlDV;"} \ No newline at end of file diff --git a/public/js/src/anime.js b/public/js/src/anime.js new file mode 100644 index 00000000..ee792199 --- /dev/null +++ b/public/js/src/anime.js @@ -0,0 +1,91 @@ +import _ from './base/AnimeClient.js' +import { renderAnimeSearchResults } from './template-helpers.js' + +const search = (query) => { + // Show the loader + _.$('.cssload-loader')[ 0 ].removeAttribute('hidden'); + + // Do the api search + _.get(_.url('/anime-collection/search'), { query }, (searchResults, status) => { + searchResults = JSON.parse(searchResults); + + // Hide the loader + _.$('.cssload-loader')[ 0 ].setAttribute('hidden', 'hidden'); + + // Show the results + _.$('#series-list')[ 0 ].innerHTML = renderAnimeSearchResults(searchResults.data); + }); +}; + +if (_.hasElement('.anime #search')) { + _.on('#search', 'keyup', _.throttle(250, (e) => { + const query = encodeURIComponent(e.target.value); + if (query === '') { + return; + } + + search(query); + })); +} + +// 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) || 0; + let totalCount = parseInt(_.$('.total_number', parentSel)[ 0 ].textContent, 10); + let title = _.$('.name a', parentSel)[ 0 ].textContent; + + // Setup the update data + let data = { + id: parentSel.dataset.kitsuId, + mal_id: parentSel.dataset.malId, + data: { + progress: watchedCount + 1 + } + }; + + // If the episode count is 0, and incremented, + // change status to currently watching + if (isNaN(watchedCount) || watchedCount === 0) { + data.data.status = 'current'; + } + + // If you increment at the last episode, mark as completed + if ((!isNaN(watchedCount)) && (watchedCount + 1) === totalCount) { + data.data.status = 'completed'; + } + + _.show(_.$('#loading-shadow')[ 0 ]); + + // okay, lets actually make some changes! + _.ajax(_.url('/anime/increment'), { + data, + dataType: 'json', + type: 'POST', + success: (res) => { + const resData = JSON.parse(res); + + if (resData.errors) { + _.hide(_.$('#loading-shadow')[ 0 ]); + _.showMessage('error', `Failed to update ${title}. `); + _.scrollToTop(); + return; + } + + if (resData.data.attributes.status === 'completed') { + _.hide(parentSel); + } + + _.hide(_.$('#loading-shadow')[ 0 ]); + + _.showMessage('success', `Successfully updated ${title}`); + _.$('.completed_number', parentSel)[ 0 ].textContent = ++watchedCount; + _.scrollToTop(); + }, + error: () => { + _.hide(_.$('#loading-shadow')[ 0 ]); + _.showMessage('error', `Failed to update ${title}. `); + _.scrollToTop(); + } + }); +}); \ No newline at end of file diff --git a/public/js/src/base/AnimeClient.js b/public/js/src/base/AnimeClient.js new file mode 100644 index 00000000..9d9fd3bc --- /dev/null +++ b/public/js/src/base/AnimeClient.js @@ -0,0 +1,337 @@ +// ------------------------------------------------------------------------- +// ! Base +// ------------------------------------------------------------------------- + +const matches = (elm, selector) => { + let matches = (elm.document || elm.ownerDocument).querySelectorAll(selector), + i = matches.length; + while (--i >= 0 && matches.item(i) !== elm) {}; + return i > -1; +} + +export const AnimeClient = { + /** + * Placeholder function + */ + noop: () => {}, + /** + * DOM selector + * + * @param {string} selector - The dom selector string + * @param {object} [context] + * @return {[HTMLElement]} - array of dom elements + */ + $(selector, context = null) { + if (typeof selector !== 'string') { + return selector; + } + + context = (context !== null && context.nodeType === 1) + ? context + : document; + + let elements = []; + if (selector.match(/^#([\w]+$)/)) { + elements.push(document.getElementById(selector.split('#')[1])); + } else { + elements = [].slice.apply(context.querySelectorAll(selector)); + } + + return elements; + }, + /** + * Does the selector exist on the current page? + * + * @param {string} selector + * @returns {boolean} + */ + hasElement (selector) { + return AnimeClient.$(selector).length > 0; + }, + /** + * Scroll to the top of the Page + * + * @return {void} + */ + scrollToTop () { + window.scroll(0,0); + }, + /** + * Hide the selected element + * + * @param {string|Element} sel - the selector of the element to hide + * @return {void} + */ + hide (sel) { + sel.setAttribute('hidden', 'hidden'); + }, + /** + * UnHide the selected element + * + * @param {string|Element} sel - the selector of the element to hide + * @return {void} + */ + show (sel) { + sel.removeAttribute('hidden'); + }, + /** + * Display a message box + * + * @param {string} type - message type: info, error, success + * @param {string} message - the message itself + * @return {void} + */ + showMessage (type, message) { + let template = + `
+ + ${message} + +
`; + + let sel = AnimeClient.$('.message'); + if (sel[0] !== undefined) { + sel[0].remove(); + } + + AnimeClient.$('header')[0].insertAdjacentHTML('beforeend', template); + }, + /** + * Finds the closest parent element matching the passed selector + * + * @param {HTMLElement} current - the current HTMLElement + * @param {string} parentSelector - selector for the parent element + * @return {HTMLElement|null} - the parent element + */ + closestParent (current, parentSelector) { + if (Element.prototype.closest !== undefined) { + return current.closest(parentSelector); + } + + while (current !== document.documentElement) { + if (matches(current, parentSelector)) { + return current; + } + + current = current.parentElement; + } + + return null; + }, + /** + * Generate a full url from a relative path + * + * @param {string} path - url path + * @return {string} - full url + */ + url (path) { + let uri = `//${document.location.host}`; + uri += (path.charAt(0) === '/') ? path : `/${path}`; + + return uri; + }, + /** + * Throttle execution of a function + * + * @see https://remysharp.com/2010/07/21/throttling-function-calls + * @see https://jsfiddle.net/jonathansampson/m7G64/ + * @param {Number} interval - the minimum throttle time in ms + * @param {Function} fn - the function to throttle + * @param {Object} [scope] - the 'this' object for the function + * @return {Function} + */ + throttle (interval, fn, scope) { + let wait = false; + return function (...args) { + const context = scope || this; + + if ( ! wait) { + fn.apply(context, args); + wait = true; + setTimeout(function() { + wait = false; + }, interval); + } + }; + }, +}; + +// ------------------------------------------------------------------------- +// ! Events +// ------------------------------------------------------------------------- + +function addEvent(sel, event, listener) { + // Recurse! + if (! event.match(/^([\w\-]+)$/)) { + event.split(' ').forEach((evt) => { + addEvent(sel, evt, listener); + }); + } + + sel.addEventListener(event, listener, false); +} + +function delegateEvent(sel, target, event, listener) { + // Attach the listener to the parent + addEvent(sel, event, (e) => { + // Get live version of the target selector + AnimeClient.$(target, sel).forEach((element) => { + if(e.target == element) { + listener.call(element, e); + e.stopPropagation(); + } + }); + }); +} + +/** + * Add an event listener + * + * @param {string|HTMLElement} sel - the parent selector to bind to + * @param {string} event - event name(s) to bind + * @param {string|HTMLElement|function} target - the element to directly bind the event to + * @param {function} [listener] - event listener callback + * @return {void} + */ +AnimeClient.on = (sel, event, target, listener) => { + if (listener === undefined) { + listener = target; + AnimeClient.$(sel).forEach((el) => { + addEvent(el, event, listener); + }); + } else { + AnimeClient.$(sel).forEach((el) => { + delegateEvent(el, target, event, listener); + }); + } +}; + +// ------------------------------------------------------------------------- +// ! Ajax +// ------------------------------------------------------------------------- + +/** + * Url encoding for non-get requests + * + * @param data + * @returns {string} + * @private + */ +function ajaxSerialize(data) { + let pairs = []; + + Object.keys(data).forEach((name) => { + let value = data[name].toString(); + + name = encodeURIComponent(name); + value = encodeURIComponent(value); + + pairs.push(`${name}=${value}`); + }); + + return pairs.join('&'); +} + +/** + * Make an ajax request + * + * Config:{ + * data: // data to send with the request + * type: // http verb of the request, defaults to GET + * success: // success callback + * error: // error callback + * } + * + * @param {string} url - the url to request + * @param {Object} config - the configuration object + * @return {void} + */ +AnimeClient.ajax = (url, config) => { + // Set some sane defaults + const defaultConfig = { + data: {}, + type: 'GET', + dataType: '', + success: AnimeClient.noop, + mimeType: 'application/x-www-form-urlencoded', + error: AnimeClient.noop + } + + config = { + ...defaultConfig, + ...config, + } + + let request = new XMLHttpRequest(); + let method = String(config.type).toUpperCase(); + + if (method === 'GET') { + url += (url.match(/\?/)) + ? ajaxSerialize(config.data) + : `?${ajaxSerialize(config.data)}`; + } + + request.open(method, url); + + request.onreadystatechange = () => { + if (request.readyState === 4) { + let responseText = ''; + + if (request.responseType === 'json') { + responseText = JSON.parse(request.responseText); + } else { + responseText = request.responseText; + } + + if (request.status > 299) { + config.error.call(null, request.status, responseText, request.response); + } else { + config.success.call(null, responseText, request.status); + } + } + }; + + if (config.dataType === 'json') { + config.data = JSON.stringify(config.data); + config.mimeType = 'application/json'; + } else { + config.data = ajaxSerialize(config.data); + } + + request.setRequestHeader('Content-Type', config.mimeType); + + switch (method) { + case 'GET': + request.send(null); + break; + + default: + request.send(config.data); + break; + } +}; + +/** + * Do a get request + * + * @param {string} url + * @param {object|function} data + * @param {function} [callback] + */ +AnimeClient.get = (url, data, callback = null) => { + if (callback === null) { + callback = data; + data = {}; + } + + return AnimeClient.ajax(url, { + data, + success: callback + }); +}; + +// ------------------------------------------------------------------------- +// Export +// ------------------------------------------------------------------------- + +export default AnimeClient; \ No newline at end of file diff --git a/public/js/base/classList.js b/public/js/src/base/classList.js similarity index 100% rename from public/js/base/classList.js rename to public/js/src/base/classList.js diff --git a/public/js/src/base/events.js b/public/js/src/base/events.js new file mode 100644 index 00000000..cb36a9ed --- /dev/null +++ b/public/js/src/base/events.js @@ -0,0 +1,38 @@ +import _ from './AnimeClient.js'; +/** + * Event handlers + */ +// Close event for messages +_.on('header', 'click', '.message', (e) => { + _.hide(e.target); +}); + +// Confirm deleting of list or library items +_.on('form.js-delete', 'submit', (event) => { + const proceed = confirm('Are you ABSOLUTELY SURE you want to delete this item?'); + + if (proceed === false) { + event.preventDefault(); + event.stopPropagation(); + } +}); + +// Clear the api cache +_.on('.js-clear-cache', 'click', () => { + _.get('/cache_purge', () => { + _.showMessage('success', 'Successfully purged api cache'); + }); +}); + +// Alleviate some page jumping + _.on('.vertical-tabs input', 'change', (event) => { + const el = event.currentTarget.parentElement; + const rect = el.getBoundingClientRect(); + + const top = rect.top + window.pageYOffset; + + window.scrollTo({ + top, + behavior: 'smooth', + }); +}); diff --git a/public/js/base/sort_tables.js b/public/js/src/base/sort_tables.js similarity index 83% rename from public/js/base/sort_tables.js rename to public/js/src/base/sort_tables.js index 84ff8ca6..78620d42 100644 --- a/public/js/base/sort_tables.js +++ b/public/js/src/base/sort_tables.js @@ -1,11 +1,8 @@ -'use strict'; const LightTableSorter = (() => { let th = null; let cellIndex = null; let order = ''; - const text = (row) => { - return row.cells.item(cellIndex).textContent.toLowerCase(); - }; + const text = (row) => row.cells.item(cellIndex).textContent.toLowerCase(); const sort = (a, b) => { let textA = text(a); let textB = text(b); @@ -23,12 +20,12 @@ const LightTableSorter = (() => { return 0; }; const toggle = () => { - const c = order !== 'sorting_asc' ? 'sorting_asc' : 'sorting_desc'; + const c = order !== 'sorting-asc' ? 'sorting-asc' : 'sorting-desc'; th.className = (th.className.replace(order, '') + ' ' + c).trim(); return order = c; }; const reset = () => { - th.classList.remove('sorting_asc', 'sorting_desc'); + th.classList.remove('sorting-asc', 'sorting-desc'); th.classList.add('sorting'); return order = ''; }; @@ -43,7 +40,7 @@ const LightTableSorter = (() => { let rows = Array.from(tbody.rows); if (rows) { rows.sort(sort); - if (order === 'sorting_asc') { + if (order === 'sorting-asc') { rows.reverse(); } toggle(); diff --git a/public/js/src/index-authed.js b/public/js/src/index-authed.js new file mode 100644 index 00000000..5d852c20 --- /dev/null +++ b/public/js/src/index-authed.js @@ -0,0 +1,4 @@ +import './index.js'; + +import './anime.js'; +import './manga.js'; diff --git a/public/js/src/index.js b/public/js/src/index.js new file mode 100644 index 00000000..a03b2fdc --- /dev/null +++ b/public/js/src/index.js @@ -0,0 +1,10 @@ +import './base/events.js'; + +/* if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js').then(reg => { + console.log('Service worker registered', reg.scope); + }).catch(error => { + console.error('Failed to register service worker', error); + }); +} */ + diff --git a/public/js/src/manga.js b/public/js/src/manga.js new file mode 100644 index 00000000..607b3aeb --- /dev/null +++ b/public/js/src/manga.js @@ -0,0 +1,86 @@ +import _ from './base/AnimeClient.js' +import { renderMangaSearchResults } from './template-helpers.js' + +const search = (query) => { + _.$('.cssload-loader')[ 0 ].removeAttribute('hidden'); + _.get(_.url('/manga/search'), { query }, (searchResults, status) => { + searchResults = JSON.parse(searchResults); + _.$('.cssload-loader')[ 0 ].setAttribute('hidden', 'hidden'); + _.$('#series-list')[ 0 ].innerHTML = renderMangaSearchResults(searchResults.data); + }); +}; + +if (_.hasElement('.manga #search')) { + _.on('#search', 'keyup', _.throttle(250, (e) => { + let query = encodeURIComponent(e.target.value); + if (query === '') { + return; + } + + search(query); + })); +} + +/** + * Javascript for editing manga, if logged in + */ +_.on('.manga.list', 'click', '.edit-buttons button', (e) => { + 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) || 0; + let total = parseInt(_.$(`.${type}_count`, parentSel)[ 0 ].textContent, 10); + let mangaName = _.$('.name', parentSel)[ 0 ].textContent; + + if (isNaN(completed)) { + completed = 0; + } + + // Setup the update data + let data = { + id: parentSel.dataset.kitsuId, + mal_id: parentSel.dataset.malId, + data: { + progress: completed + } + }; + + // If the episode count is 0, and incremented, + // change status to currently reading + if (isNaN(completed) || completed === 0) { + data.data.status = 'current'; + } + + // If you increment at the last chapter, mark as completed + if ((!isNaN(completed)) && (completed + 1) === total) { + data.data.status = 'completed'; + } + + // Update the total count + data.data.progress = ++completed; + + _.show(_.$('#loading-shadow')[ 0 ]); + + _.ajax(_.url('/manga/increment'), { + data, + dataType: 'json', + type: 'POST', + mimeType: 'application/json', + success: () => { + if (data.data.status === 'completed') { + _.hide(parentSel); + } + + _.hide(_.$('#loading-shadow')[ 0 ]); + + _.$(`.${type}s_read`, parentSel)[ 0 ].textContent = completed; + _.showMessage('success', `Successfully updated ${mangaName}`); + _.scrollToTop(); + }, + error: () => { + _.hide(_.$('#loading-shadow')[ 0 ]); + _.showMessage('error', `Failed to update ${mangaName}`); + _.scrollToTop(); + } + }); +}); \ No newline at end of file diff --git a/public/js/src/template-helpers.js b/public/js/src/template-helpers.js new file mode 100644 index 00000000..4de89e8b --- /dev/null +++ b/public/js/src/template-helpers.js @@ -0,0 +1,89 @@ +import _ from './base/AnimeClient.js'; + +// Click on hidden MAL checkbox so +// that MAL id is passed +_.on('main', 'change', '.big-check', (e) => { + const id = e.target.id; + document.getElementById(`mal_${id}`).checked = true; +}); + +export function renderAnimeSearchResults (data) { + const results = []; + + data.forEach(x => { + const item = x.attributes; + const titles = item.titles.reduce((prev, current) => { + return prev + `${current}
`; + }, []); + + results.push(` + + `); + }); + + return results.join(''); +} + +export function renderMangaSearchResults (data) { + const results = []; + + data.forEach(x => { + const item = x.attributes; + const titles = item.titles.reduce((prev, current) => { + return prev + `${current}
`; + }, []); + + results.push(` + + `); + }); + + return results.join(''); +} \ No newline at end of file diff --git a/public/js/tables.min.js b/public/js/tables.min.js new file mode 100644 index 00000000..c075b7b8 --- /dev/null +++ b/public/js/tables.min.js @@ -0,0 +1,4 @@ +(function(){var LightTableSorter=function(){var th=null;var cellIndex=null;var order="";var text=function(row){return row.cells.item(cellIndex).textContent.toLowerCase()};var sort=function(a,b){var textA=text(a);var textB=text(b);var n=parseInt(textA,10);if(n){textA=n;textB=parseInt(textB,10)}if(textA>textB)return 1;if(textA {\n\tlet th = null;\n\tlet cellIndex = null;\n\tlet order = '';\n\tconst text = (row) => row.cells.item(cellIndex).textContent.toLowerCase();\n\tconst sort = (a, b) => {\n\t\tlet textA = text(a);\n\t\tlet textB = text(b);\n\t\tconst n = parseInt(textA, 10);\n\t\tif (n) {\n\t\t\ttextA = n;\n\t\t\ttextB = parseInt(textB, 10);\n\t\t}\n\t\tif (textA > textB) {\n\t\t\treturn 1;\n\t\t}\n\t\tif (textA < textB) {\n\t\t\treturn -1;\n\t\t}\n\t\treturn 0;\n\t};\n\tconst toggle = () => {\n\t\tconst c = order !== 'sorting-asc' ? 'sorting-asc' : 'sorting-desc';\n\t\tth.className = (th.className.replace(order, '') + ' ' + c).trim();\n\t\treturn order = c;\n\t};\n\tconst reset = () => {\n\t\tth.classList.remove('sorting-asc', 'sorting-desc');\n\t\tth.classList.add('sorting');\n\t\treturn order = '';\n\t};\n\tconst onClickEvent = (e) => {\n\t\tif (th && (cellIndex !== e.target.cellIndex)) {\n\t\t\treset();\n\t\t}\n\t\tth = e.target;\n\t\tif (th.nodeName.toLowerCase() === 'th') {\n\t\t\tcellIndex = th.cellIndex;\n\t\t\tconst tbody = th.offsetParent.getElementsByTagName('tbody')[0];\n\t\t\tlet rows = Array.from(tbody.rows);\n\t\t\tif (rows) {\n\t\t\t\trows.sort(sort);\n\t\t\t\tif (order === 'sorting-asc') {\n\t\t\t\t\trows.reverse();\n\t\t\t\t}\n\t\t\t\ttoggle();\n\t\t\t\ttbody.innerHtml = '';\n\n\t\t\t\trows.forEach(row => {\n\t\t\t\t\ttbody.appendChild(row);\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t};\n\treturn {\n\t\tinit: () => {\n\t\t\tlet ths = document.getElementsByTagName('th');\n\t\t\tlet results = [];\n\t\t\tfor (let i = 0, len = ths.length; i < len; i++) {\n\t\t\t\tlet th = ths[i];\n\t\t\t\tth.classList.add('sorting');\n\t\t\t\tresults.push(th.onclick = onClickEvent);\n\t\t\t}\n\t\t\treturn results;\n\t\t}\n\t};\n})();\n\nLightTableSorter.init();"],"names":["LightTableSorter","th","cellIndex","order","text","row","cells","item","textContent","toLowerCase","sort","a","b","textA","textB","n","parseInt","toggle","c","className","trim","replace","reset","classList","remove","add","onClickEvent","e","target","nodeName","tbody","offsetParent","getElementsByTagName","rows","Array","from","reverse","innerHtml","forEach","appendChild","init","ths","document","results","i","len","length","push","onclick"],"mappings":"YAAA,IAAMA,iBAAoB,QAAA,EAAM,CAC/B,IAAIC,GAAK,IACT,KAAIC,UAAY,IAChB,KAAIC,MAAQ,EACZ,KAAMC,KAAOA,QAAA,CAACC,GAAD,CAAS,CAAA,MAAAA,IAAAC,MAAAC,KAAA,CAAeL,SAAf,CAAAM,YAAAC,YAAA,EAAA,CACtB,KAAMC,KAAOA,QAAA,CAACC,CAAD,CAAIC,CAAJ,CAAU,CACtB,IAAIC,MAAQT,IAAA,CAAKO,CAAL,CACZ,KAAIG,MAAQV,IAAA,CAAKQ,CAAL,CACZ,KAAMG,EAAIC,QAAA,CAASH,KAAT,CAAgB,EAAhB,CACV,IAAIE,CAAJ,CAAO,CACNF,KAAA,CAAQE,CACRD,MAAA,CAAQE,QAAA,CAASF,KAAT,CAAgB,EAAhB,CAFF,CAIP,GAAID,KAAJ,CAAYC,KAAZ,CACC,MAAO,EAER,IAAID,KAAJ,CAAYC,KAAZ,CACC,MAAQ,EAET,OAAO,EAde,CAgBvB,KAAMG,OAASA,QAAA,EAAM,CACpB,IAAMC,EAAIf,KAAA,GAAU,aAAV,CAA0B,aAA1B,CAA0C,cACpDF,GAAAkB,UAAA,CAAeC,CAACnB,EAAAkB,UAAAE,QAAA,CAAqBlB,KAArB,CAA4B,EAA5B,CAADiB,CAAmC,GAAnCA,CAAyCF,CAAzCE,MAAA,EACf,OAAOjB,MAAP;AAAee,CAHK,CAKrB,KAAMI,MAAQA,QAAA,EAAM,CACnBrB,EAAAsB,UAAAC,OAAA,CAAoB,aAApB,CAAmC,cAAnC,CACAvB,GAAAsB,UAAAE,IAAA,CAAiB,SAAjB,CACA,OAAOtB,MAAP,CAAe,EAHI,CAKpB,KAAMuB,aAAeA,QAAA,CAACC,CAAD,CAAO,CAC3B,GAAI1B,EAAJ,EAAWC,SAAX,GAAyByB,CAAAC,OAAA1B,UAAzB,CACCoB,KAAA,EAEDrB,GAAA,CAAK0B,CAAAC,OACL,IAAI3B,EAAA4B,SAAApB,YAAA,EAAJ,GAAkC,IAAlC,CAAwC,CACvCP,SAAA,CAAYD,EAAAC,UACZ,KAAM4B,MAAQ7B,EAAA8B,aAAAC,qBAAA,CAAqC,OAArC,CAAA,CAA8C,CAA9C,CACd,KAAIC,KAAOC,KAAAC,KAAA,CAAWL,KAAAG,KAAX,CACX,IAAIA,IAAJ,CAAU,CACTA,IAAAvB,KAAA,CAAUA,IAAV,CACA,IAAIP,KAAJ,GAAc,aAAd,CACC8B,IAAAG,QAAA,EAEDnB,OAAA,EACAa,MAAAO,UAAA,CAAkB,EAElBJ,KAAAK,QAAA,CAAa,QAAA,CAAAjC,GAAA,CAAO,CACnByB,KAAAS,YAAA,CAAkBlC,GAAlB,CADmB,CAApB,CARS,CAJ6B,CALb,CAuB5B;MAAO,CACNmC,KAAMA,QAAA,EAAM,CACX,IAAIC,IAAMC,QAAAV,qBAAA,CAA8B,IAA9B,CACV,KAAIW,QAAU,EACd,KAAK,IAAIC,EAAI,CAAR,CAAWC,IAAMJ,GAAAK,OAAtB,CAAkCF,CAAlC,CAAsCC,GAAtC,CAA2CD,CAAA,EAA3C,CAAgD,CAC/C,IAAI3C,KAAKwC,GAAA,CAAIG,CAAJ,CACT3C,KAAAsB,UAAAE,IAAA,CAAiB,SAAjB,CACAkB,QAAAI,KAAA,CAAa9C,IAAA+C,QAAb,CAA0BtB,YAA1B,CAH+C,CAKhD,MAAOiB,QARI,CADN,CAtDwB,CAAP,EAoEzB3C,iBAAAwC,KAAA;"} \ No newline at end of file diff --git a/public/package.json b/public/package.json index f4da21bc..3a12562f 100644 --- a/public/package.json +++ b/public/package.json @@ -1,13 +1,22 @@ { + "license": "MIT", "scripts": { - "build": "node ./css.js", - "watch": "watch 'npm run build' --filter=./cssfilter.js" + "build": "npm run build:css && npm run build:js", + "build:css": "node ./tools/css.js", + "build:js": "rollup -c ./tools/build-js.js", + "watch:css": "watch 'npm run build:css' --filter=./tools/cssfilter.js", + "watch:js": "watch 'npm run build:js' ./js/src", + "watch": "concurrently \"npm:watch:css\" \"npm:watch:js\" --kill-others" }, "devDependencies": { + "@ampproject/rollup-plugin-closure-compiler": "^0.8.3", + "concurrently": "^4.0.1", "cssnano": "^4.0.5", "postcss-cachify": "^1.3.1", "postcss-cssnext": "^3.0.0", "postcss-import": "^12.0.0", + "rollup": "^0.66.6", + "rollup-plugin-closure-compiler-js": "^1.0.6", "watch": "^1.0.2" } } diff --git a/public/test/index.html b/public/test/index.html index 70842fd0..93cdb56c 100644 --- a/public/test/index.html +++ b/public/test/index.html @@ -20,7 +20,7 @@
    - + - + diff --git a/public/tools/build-js.js b/public/tools/build-js.js new file mode 100644 index 00000000..e6dcad97 --- /dev/null +++ b/public/tools/build-js.js @@ -0,0 +1,44 @@ +import compiler from '@ampproject/rollup-plugin-closure-compiler'; + +const plugins = [ + compiler({ + assumeFunctionWrapper: true, + compilationLevel: 'WHITESPACE_ONLY', //'ADVANCED', + createSourceMap: true, + env: 'BROWSER', + languageIn: 'ECMASCRIPT_2018', + languageOut: 'ES3' + }) +]; + +const defaultOutput = { + format: 'iife', + sourcemap: true, +} + +export default [{ + input: './js/src/index.js', + output: { + ...defaultOutput, + file: './js/scripts.min.js', + sourcemapFile: './js/scripts.min.js.map', + }, + plugins, +}, { + input: './js/src/index-authed.js', + output: { + ...defaultOutput, + file: './js/scripts-authed.min.js', + sourcemapFile: './js/scripts-authed.min.js.map', + }, + plugins, +}, { + input: './js/src/base/sort_tables.js', + output: { + ...defaultOutput, + file: './js/tables.min.js', + sourcemapFile: './js/tables.min.js.map', + }, + plugins, +}]; + diff --git a/public/css.js b/public/tools/css.js similarity index 87% rename from public/css.js rename to public/tools/css.js index df99fd96..507890d6 100644 --- a/public/css.js +++ b/public/tools/css.js @@ -7,7 +7,7 @@ const atImport = require('postcss-import'); const cssNext = require('postcss-cssnext'); const cssNano = require('cssnano'); -const css = fs.readFileSync('css/base.css', 'utf-8'); +const css = fs.readFileSync('css/all.css', 'utf-8'); postcss() .use(atImport()) @@ -21,7 +21,7 @@ postcss() } })) .process(css, { - from: 'css/base.css', + from: 'css/all.css', to: 'css/app.min.css' }).then(result => { fs.writeFileSync('css/app.min.css', result.css); diff --git a/public/cssfilter.js b/public/tools/cssfilter.js similarity index 100% rename from public/cssfilter.js rename to public/tools/cssfilter.js diff --git a/public/yarn.lock b/public/yarn.lock index 426f93b4..1d7d1c22 100644 --- a/public/yarn.lock +++ b/public/yarn.lock @@ -2,33 +2,81 @@ # yarn lockfile v1 +"@ampproject/rollup-plugin-closure-compiler@^0.8.3": + version "0.8.3" + resolved "https://registry.yarnpkg.com/@ampproject/rollup-plugin-closure-compiler/-/rollup-plugin-closure-compiler-0.8.3.tgz#2d1fa0f62345a50926c44d9b5ad8775cc12c6297" + integrity sha512-8pTsWQ098MJUKY3iMfIMaThhv9SNhqC/8LRDmH7Ldvc5ATgzw6aMhYdt4vlZNN1hqIGQiMpHAB2I6J7ijGDNQg== + dependencies: + acorn "6.0.2" + acorn-dynamic-import "4.0.0" + acorn-walk "6.1.0" + google-closure-compiler "20181008.0.0" + magic-string "0.25.1" + temp-write "3.4.0" + +"@types/estree@0.0.39": + version "0.0.39" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== + +"@types/node@*": + version "10.12.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.2.tgz#d77f9faa027cadad9c912cd47f4f8b07b0fb0864" + integrity sha512-53ElVDSnZeFUUFIYzI8WLQ25IhWzb6vbddNp8UHlXQyU0ET2RhV5zg0NfubzU7iNMh5bBXb0htCzfvrSVNgzaQ== + +acorn-dynamic-import@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" + integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw== + +acorn-walk@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.0.tgz#c957f4a1460da46af4a0388ce28b4c99355b0cbc" + integrity sha512-ugTb7Lq7u4GfWSqqpwE0bGyoBZNMTok/zDBXxfEG0QM50jNlGhIWjRC1pPN7bvV1anhF+bs+/gNcRw+o55Evbg== + +acorn@6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.2.tgz#6a459041c320ab17592c6317abbfdf4bbaa98ca4" + integrity sha512-GXmKIvbrN3TV7aVqAzVFaMW8F8wzVX7voEBRO3bDA64+EX37YSayggRJP5Xig6HYHBkWKpFg9W5gg6orklubhg== + alphanum-sort@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" + integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: sprintf-js "~1.0.2" autoprefixer@^7.1.1: version "7.2.6" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.2.6.tgz#256672f86f7c735da849c4f07d008abb056067dc" + integrity sha512-Iq8TRIB+/9eQ8rbGhcP7ct5cYb/3qjNYAR2SnzLCEcwF6rvVOax8+9+fccgXk4bEhQGjOZd5TLhsksmAdsbGqQ== dependencies: browserslist "^2.11.3" caniuse-lite "^1.0.30000805" @@ -40,6 +88,7 @@ autoprefixer@^7.1.1: babel-runtime@^6.23.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= dependencies: core-js "^2.4.0" regenerator-runtime "^0.11.0" @@ -47,22 +96,27 @@ babel-runtime@^6.23.0: balanced-match@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.1.0.tgz#b504bd05869b39259dd0c5efc35d843176dccc4a" + integrity sha1-tQS9BYabOSWd0MXvw12EMXbczEo= balanced-match@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + integrity sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg= balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= boolbase@^1.0.0, boolbase@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" @@ -70,21 +124,34 @@ brace-expansion@^1.1.7: browserslist@^2.0.0, browserslist@^2.11.3: version "2.11.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.11.3.tgz#fe36167aed1bbcde4827ebfe71347a2cc70b99b2" + integrity sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA== dependencies: caniuse-lite "^1.0.30000792" electron-to-chromium "^1.3.30" browserslist@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.0.1.tgz#61c05ce2a5843c7d96166408bc23d58b5416e818" + version "4.3.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.3.4.tgz#4477b737db6a1b07077275b24791e680d4300425" + integrity sha512-u5iz+ijIMUlmV8blX82VGFrB9ecnUg5qEt55CMZ/YJEhha+d8qpBfOFuutJ6F/VKRXjZoD33b6uvarpPxcl3RA== dependencies: - caniuse-lite "^1.0.30000865" - electron-to-chromium "^1.3.52" - node-releases "^1.0.0-alpha.10" + caniuse-lite "^1.0.30000899" + electron-to-chromium "^1.3.82" + node-releases "^1.0.1" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= + +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= caniuse-api@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-2.0.0.tgz#b1ddb5a5966b16f48dc4998444d4bbc6c7d9d834" + integrity sha1-sd21pZZrFvSNxJmERNS7xsfZ2DQ= dependencies: browserslist "^2.0.0" caniuse-lite "^1.0.0" @@ -94,19 +161,22 @@ caniuse-api@^2.0.0: caniuse-api@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== dependencies: browserslist "^4.0.0" caniuse-lite "^1.0.0" lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000805, caniuse-lite@^1.0.30000865: - version "1.0.30000874" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000874.tgz#a641b1f1c420d58d9b132920ef6ba87bbdcd2223" +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000805, caniuse-lite@^1.0.30000899: + version "1.0.30000903" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000903.tgz#86d46227759279b3db345ddbe778335dbba9e858" + integrity sha512-T1XVJEpGCoaq7MDw7/6hCdYUukmSaS+1l/OQJkLtw7Cr2+/+d67tNGKEbyiqf7Ck8x6EhNFUxjYFXXka0N/w5g== -chalk@^1.1.3: +chalk@^1.0.0, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= dependencies: ansi-styles "^2.2.1" escape-string-regexp "^1.0.2" @@ -117,44 +187,90 @@ chalk@^1.1.3: chalk@^2.0.1, chalk@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" + integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ== dependencies: ansi-styles "^3.2.1" escape-string-regexp "^1.0.5" supports-color "^5.3.0" +cliui@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" + integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + wrap-ansi "^2.0.0" + +clone-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" + integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= + +clone-stats@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" + integrity sha1-s3gt/4u1R04Yuba/D9/ngvh3doA= + clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= + +clone@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= + +cloneable-readable@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.1.2.tgz#d591dee4a8f8bc15da43ce97dceeba13d43e2a65" + integrity sha512-Bq6+4t+lbM8vhTs/Bef5c5AdEMtapp/iFb6+s4/Hh9MVTt8OLKH7ZOOZSCT+Ys7hsHvqv0GuMPJ1lnQJVHvxpg== + dependencies: + inherits "^2.0.1" + process-nextick-args "^2.0.0" + readable-stream "^2.3.5" coa@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.1.tgz#f3f8b0b15073e35d70263fb1042cb2c023db38af" + integrity sha512-5wfTTO8E2/ja4jFSxePXlG5nRu5bBtL/r1HCIpJW/lzT6yDtKl0u0Z4o/Vpz32IpKmBn7HerheEZQgA9N2DarQ== dependencies: q "^1.1.2" +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + color-convert@^1.3.0, color-convert@^1.8.2, color-convert@^1.9.0, color-convert@^1.9.1: - version "1.9.2" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.2.tgz#49881b8fba67df12a96bdf3f56c0aab9e7913147" + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== dependencies: - color-name "1.1.1" + color-name "1.1.3" -color-name@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.1.tgz#4b1415304cf50028ea81643643bd82ea05803689" - -color-name@^1.0.0: +color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== color-string@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" + integrity sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE= dependencies: color-name "^1.0.0" color-string@^1.4.0, color-string@^1.5.2: version "1.5.3" resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" + integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== dependencies: color-name "^1.0.0" simple-swizzle "^0.2.2" @@ -162,6 +278,7 @@ color-string@^1.4.0, color-string@^1.5.2: color@^0.11.0: version "0.11.4" resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" + integrity sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q= dependencies: clone "^1.0.2" color-convert "^1.3.0" @@ -170,6 +287,7 @@ color@^0.11.0: color@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/color/-/color-1.0.3.tgz#e48e832d85f14ef694fb468811c2d5cfe729b55d" + integrity sha1-5I6DLYXxTvaU+0aIEcLVz+cptV0= dependencies: color-convert "^1.8.2" color-string "^1.4.0" @@ -177,13 +295,15 @@ color@^1.0.3: color@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color/-/color-2.0.1.tgz#e4ed78a3c4603d0891eba5430b04b86314f4c839" + integrity sha512-ubUCVVKfT7r2w2D3qtHakj8mbmKms+tThR8gI8zEYCbUBl8/voqFGt3kgBqGwXAopgXybnkuOq+qMYCRrp4cXw== dependencies: color-convert "^1.9.1" color-string "^1.5.2" color@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a" + version "3.1.0" + resolved "https://registry.yarnpkg.com/color/-/color-3.1.0.tgz#d8e9fb096732875774c84bf922815df0308d0ffc" + integrity sha512-CwyopLkuRYO5ei2EpzpIh6LqJMt6Mt+jZhO5VI5f/wJLZriXQE32/SSqzmrh+QB+AZT81Cj8yv+7zwToW8ahZg== dependencies: color-convert "^1.9.1" color-string "^1.5.2" @@ -191,14 +311,32 @@ color@^3.0.0: colors@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" + integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM= concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +concurrently@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-4.0.1.tgz#f6310fbadf2f476dd95df952edb5c0ab789f672c" + integrity sha512-D8UI+mlI/bfvrA57SeKOht6sEpb01dKk+8Yee4fbnkk1Ue8r3S+JXoEdFZIpzQlXJGtnxo47Wvvg/kG4ba3U6Q== + dependencies: + chalk "^2.4.1" + date-fns "^1.23.0" + lodash "^4.17.10" + read-pkg "^4.0.1" + rxjs "6.2.2" + spawn-command "^0.0.2-1" + supports-color "^4.5.0" + tree-kill "^1.1.0" + yargs "^12.0.1" connect-cachify-static@^1.3.0: version "1.6.0" resolved "https://registry.yarnpkg.com/connect-cachify-static/-/connect-cachify-static-1.6.0.tgz#f97eac98fa0ac6e6fe793fc32565f9ca028e9b38" + integrity sha512-rmBn6Xy7erXXuUtWWoxztyyuLVCp/FxF07g7MWOIWhSJJb5mv08jInLUlbFE1b7LMLEs9ff0x8j0KFOBHY8eMw== dependencies: debug "~2" find "~0" @@ -209,18 +347,37 @@ connect-cachify-static@^1.3.0: core-js@^2.4.0: version "2.5.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" + integrity sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw== + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= cosmiconfig@^5.0.0: - version "5.0.5" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.0.5.tgz#a809e3c2306891ce17ab70359dc8bdf661fe2cd0" + version "5.0.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.0.6.tgz#dca6cf680a0bd03589aff684700858c81abeeb39" + integrity sha512-6DWfizHriCrFWURP1/qyhsiFvYdlJzbCzmtFWh744+KyWsJo5+kPzUZZaMRSSItoYc0pxFX7gEO7ZC1/gN/7AQ== dependencies: is-directory "^0.3.1" js-yaml "^3.9.0" parse-json "^4.0.0" +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + css-color-function@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.3.tgz#8ed24c2c0205073339fafa004bc8c141fccb282e" + integrity sha1-jtJMLAIFBzM5+voAS8jBQfzLKC4= dependencies: balanced-match "0.1.0" color "^0.11.0" @@ -230,158 +387,197 @@ css-color-function@~1.3.3: css-color-names@0.0.4, css-color-names@^0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" + integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA= -css-declaration-sorter@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-3.0.1.tgz#d0e3056b0fd88dc1ea9dceff435adbe9c702a7f8" +css-declaration-sorter@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22" + integrity sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA== dependencies: - postcss "^6.0.0" + postcss "^7.0.1" timsort "^0.3.0" css-select-base-adapter@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.0.tgz#0102b3d14630df86c3eb9fa9f5456270106cf990" + version "0.1.1" + resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" + integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== -css-select@~1.3.0-rc0: - version "1.3.0-rc0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.3.0-rc0.tgz#6f93196aaae737666ea1036a8cb14a8fcb7a9231" +css-select@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.0.2.tgz#ab4386cec9e1f668855564b17c3733b43b2a5ede" + integrity sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ== dependencies: boolbase "^1.0.0" - css-what "2.1" - domutils "1.5.1" - nth-check "^1.0.1" + css-what "^2.1.2" + domutils "^1.7.0" + nth-check "^1.0.2" -css-tree@1.0.0-alpha.29: - version "1.0.0-alpha.29" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.29.tgz#3fa9d4ef3142cbd1c301e7664c1f352bd82f5a39" +css-tree@1.0.0-alpha.28: + version "1.0.0-alpha.28" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.28.tgz#8e8968190d886c9477bc8d61e96f61af3f7ffa7f" + integrity sha512-joNNW1gCp3qFFzj4St6zk+Wh/NBv0vM5YbEreZk0SD4S23S+1xBKb6cLDg2uj4P4k/GUMlIm6cKIDqIG+vdt0w== dependencies: mdn-data "~1.1.0" source-map "^0.5.3" -css-tree@1.0.0-alpha25: - version "1.0.0-alpha25" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha25.tgz#1bbfabfbf6eeef4f01d9108ff2edd0be2fe35597" +css-tree@1.0.0-alpha.29: + version "1.0.0-alpha.29" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.29.tgz#3fa9d4ef3142cbd1c301e7664c1f352bd82f5a39" + integrity sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg== dependencies: - mdn-data "^1.0.0" + mdn-data "~1.1.0" source-map "^0.5.3" css-unit-converter@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996" + integrity sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY= css-url-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/css-url-regex/-/css-url-regex-1.1.0.tgz#83834230cc9f74c457de59eebd1543feeb83b7ec" + integrity sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w= -css-what@2.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd" +css-what@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.2.tgz#c0876d9d0480927d7d4920dcd72af3595649554d" + integrity sha512-wan8dMWQ0GUeF7DGEPVjhHemVW/vy6xUYmFzRY8RYqgA0JtXC9rJmbScBjqSu6dg9q0lwPQy6ZAmJVr3PPTvqQ== -cssnano-preset-default@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.0.tgz#c334287b4f7d49fb2d170a92f9214655788e3b6b" +cssesc@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703" + integrity sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg== + +cssnano-preset-default@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.5.tgz#d1756c0259d98ad311e601ba76e95c60f6771ac1" + integrity sha512-f1uhya0ZAjPYtDD58QkBB0R+uYdzHPei7cDxJyQQIHt5acdhyGXaSXl2nDLzWHLwGFbZcHxQtkJS8mmNwnxTvw== dependencies: - css-declaration-sorter "^3.0.0" - cssnano-util-raw-cache "^4.0.0" - postcss "^6.0.0" - postcss-calc "^6.0.0" - postcss-colormin "^4.0.0" - postcss-convert-values "^4.0.0" - postcss-discard-comments "^4.0.0" - postcss-discard-duplicates "^4.0.0" - postcss-discard-empty "^4.0.0" - postcss-discard-overridden "^4.0.0" - postcss-merge-longhand "^4.0.0" - postcss-merge-rules "^4.0.0" - postcss-minify-font-values "^4.0.0" - postcss-minify-gradients "^4.0.0" - postcss-minify-params "^4.0.0" - postcss-minify-selectors "^4.0.0" - postcss-normalize-charset "^4.0.0" - postcss-normalize-display-values "^4.0.0" - postcss-normalize-positions "^4.0.0" - postcss-normalize-repeat-style "^4.0.0" - postcss-normalize-string "^4.0.0" - postcss-normalize-timing-functions "^4.0.0" - postcss-normalize-unicode "^4.0.0" - postcss-normalize-url "^4.0.0" - postcss-normalize-whitespace "^4.0.0" - postcss-ordered-values "^4.0.0" - postcss-reduce-initial "^4.0.0" - postcss-reduce-transforms "^4.0.0" - postcss-svgo "^4.0.0" - postcss-unique-selectors "^4.0.0" + css-declaration-sorter "^4.0.1" + cssnano-util-raw-cache "^4.0.1" + postcss "^7.0.0" + postcss-calc "^7.0.0" + postcss-colormin "^4.0.2" + postcss-convert-values "^4.0.1" + postcss-discard-comments "^4.0.1" + postcss-discard-duplicates "^4.0.2" + postcss-discard-empty "^4.0.1" + postcss-discard-overridden "^4.0.1" + postcss-merge-longhand "^4.0.9" + postcss-merge-rules "^4.0.2" + postcss-minify-font-values "^4.0.2" + postcss-minify-gradients "^4.0.1" + postcss-minify-params "^4.0.1" + postcss-minify-selectors "^4.0.1" + postcss-normalize-charset "^4.0.1" + postcss-normalize-display-values "^4.0.1" + postcss-normalize-positions "^4.0.1" + postcss-normalize-repeat-style "^4.0.1" + postcss-normalize-string "^4.0.1" + postcss-normalize-timing-functions "^4.0.1" + postcss-normalize-unicode "^4.0.1" + postcss-normalize-url "^4.0.1" + postcss-normalize-whitespace "^4.0.1" + postcss-ordered-values "^4.1.1" + postcss-reduce-initial "^4.0.2" + postcss-reduce-transforms "^4.0.1" + postcss-svgo "^4.0.1" + postcss-unique-selectors "^4.0.1" cssnano-util-get-arguments@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f" + integrity sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8= cssnano-util-get-match@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d" + integrity sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0= -cssnano-util-raw-cache@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.0.tgz#be0a2856e25f185f5f7a2bcc0624e28b7f179a9f" +cssnano-util-raw-cache@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282" + integrity sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA== dependencies: - postcss "^6.0.0" + postcss "^7.0.0" cssnano-util-same-parent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.0.tgz#d2a3de1039aa98bc4ec25001fa050330c2a16dac" + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3" + integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q== cssnano@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.0.5.tgz#8789b5fdbe7be05d8a0f7e45c4c789ebe712f5aa" + version "4.1.7" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.7.tgz#0bf112294bec103ab5f68d3f805732c8325a0b1b" + integrity sha512-AiXL90l+MDuQmRNyypG2P7ux7K4XklxYzNNUd5HXZCNcH8/N9bHPcpN97v8tXgRVeFL/Ed8iP8mVmAAu0ZpT7A== dependencies: cosmiconfig "^5.0.0" - cssnano-preset-default "^4.0.0" + cssnano-preset-default "^4.0.5" is-resolvable "^1.0.0" - postcss "^6.0.0" + postcss "^7.0.0" csso@^3.5.0: version "3.5.1" resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.1.tgz#7b9eb8be61628973c1b261e169d2f024008e758b" + integrity sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg== dependencies: css-tree "1.0.0-alpha.29" +date-fns@^1.23.0: + version "1.29.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" + integrity sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw== + debug@^2.1.2, debug@~2: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" debug@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== dependencies: - ms "2.0.0" + ms "^2.1.1" + +decamelize@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-2.0.0.tgz#656d7bbc8094c4c788ea53c5840908c9c7d063c7" + integrity sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg== + dependencies: + xregexp "4.0.0" define-properties@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== dependencies: - foreach "^2.0.5" - object-keys "^1.0.8" + object-keys "^1.0.12" dom-serializer@0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" + integrity sha1-BzxpdUbOB4DOI75KKOKT5AvDDII= dependencies: domelementtype "~1.1.1" entities "~1.1.1" domelementtype@1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" + version "1.2.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.2.1.tgz#578558ef23befac043a1abb0db07635509393479" + integrity sha512-SQVCLFS2E7G5CRCMdn6K9bIhRj1bS6QBWZfF0TUPh4V/BbqrQ619IdSS3/izn0FZ+9l+uODzaZjb08fjOfablA== domelementtype@~1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" + integrity sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs= -domutils@1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" +domutils@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== dependencies: dom-serializer "0" domelementtype "1" @@ -389,26 +585,31 @@ domutils@1.5.1: dot-prop@^4.1.1: version "4.2.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" + integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ== dependencies: is-obj "^1.0.0" -electron-to-chromium@^1.3.30, electron-to-chromium@^1.3.52: - version "1.3.55" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.55.tgz#f150e10b20b77d9d41afcca312efe0c3b1a7fdce" +electron-to-chromium@^1.3.30, electron-to-chromium@^1.3.82: + version "1.3.83" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.83.tgz#74584eb0972bb6777811c5d68d988c722f5e6666" + integrity sha512-DqJoDarxq50dcHsOOlMLNoy+qQitlMNbYb6wwbE0oUw2veHdRkpNrhmngiUYKMErdJ8SJ48rpJsZTQgy5SoEAA== entities@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== dependencies: is-arrayish "^0.2.1" es-abstract@^1.5.1, es-abstract@^1.6.1: version "1.12.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" + integrity sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA== dependencies: es-to-primitive "^1.1.1" function-bind "^1.1.1" @@ -417,104 +618,216 @@ es-abstract@^1.5.1, es-abstract@^1.6.1: is-regex "^1.0.4" es-to-primitive@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d" + version "1.2.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" + integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== dependencies: - is-callable "^1.1.1" + is-callable "^1.1.4" is-date-object "^1.0.1" - is-symbol "^1.0.1" + is-symbol "^1.0.2" escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== exec-sh@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36" + integrity sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw== dependencies: merge "^1.2.0" +execa@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" + integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw== + dependencies: + cross-spawn "^6.0.0" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + find@~0: version "0.2.9" resolved "https://registry.yarnpkg.com/find/-/find-0.2.9.tgz#4b73f1ff9e56ad91b76e716407fe5ffe6554bb8c" + integrity sha1-S3Px/55WrZG3bnFkB/5f/mVUu4w= dependencies: traverse-chain "~0.1.0" flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" - -foreach@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" + integrity sha1-2uRqnXj74lKSJYzB54CkHZXAN4I= function-bind@^1.1.0, function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +get-caller-file@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" + integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= + +google-closure-compiler-js@>20170000: + version "20181008.0.0" + resolved "https://registry.yarnpkg.com/google-closure-compiler-js/-/google-closure-compiler-js-20181008.0.0.tgz#aa252fe9bfff47a4d1790c8af1a2adc755d7a3a7" + integrity sha512-vE3v9FZf7l/RjG2rsQ2X2Ho0nkqAcfldiuBrKsPLomYQn1z9uFgWgD+kQP2TXigactA10cX9ZNddKMO81tO45Q== + +google-closure-compiler-linux@^20181008.0.0: + version "20181008.0.0" + resolved "https://registry.yarnpkg.com/google-closure-compiler-linux/-/google-closure-compiler-linux-20181008.0.0.tgz#d351110b5eaa0d05ec0aadcd29a3b64e812e8e15" + integrity sha512-k8njGfH2uzWJiRPPvUxM7MJB28gPrf4kI2bbuiF0gJk/1arXcWCPGjLD6pzCU0UylMy52MUXLgsIpRorqf2brw== + +google-closure-compiler-osx@^20181008.0.0: + version "20181008.0.0" + resolved "https://registry.yarnpkg.com/google-closure-compiler-osx/-/google-closure-compiler-osx-20181008.0.0.tgz#7f7a0a19afdad4fb1f40b1a44d9f08b50762e67f" + integrity sha512-xzf/yH/4MXdb6GbP84iHnpcVCOPBbH0gMVOs0JhR/KbrQh+DlJU+Y8Z/DQzTkw9HgD650R2/WZmBknURyg9OTw== + +google-closure-compiler@20181008.0.0: + version "20181008.0.0" + resolved "https://registry.yarnpkg.com/google-closure-compiler/-/google-closure-compiler-20181008.0.0.tgz#dcf02ab3a679822a0d1ded519ec8bec99d6887ba" + integrity sha512-XmJIasXHyy4kirthlsuDev2LZcXjYXWfOHwHdCLUQnfJH8T2sxWDNjFLQycaCIXwQLOyw2Kem38VgxrYfG0hzg== + dependencies: + chalk "^1.0.0" + minimist "^1.2.0" + vinyl "^2.0.1" + vinyl-sourcemaps-apply "^0.2.0" + optionalDependencies: + google-closure-compiler-linux "^20181008.0.0" + google-closure-compiler-osx "^20181008.0.0" + +graceful-fs@^4.1.2: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg= has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= dependencies: ansi-regex "^2.0.0" has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + integrity sha1-6CB68cx7MNRGzHC3NLXovhj4jVE= has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" + integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= has@^1.0.0, has@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== dependencies: function-bind "^1.1.1" hex-color-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" + integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== + +hosted-git-info@^2.1.4: + version "2.7.1" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" + integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w== hsl-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" + integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4= hsla-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" + integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= html-comment-regex@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" + version "1.1.2" + resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" + integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== indexes-of@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= + +inherits@^2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +invert-kv@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" + integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== is-absolute-url@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" + integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY= is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= is-arrayish@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== -is-callable@^1.1.1, is-callable@^1.1.3: +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + integrity sha1-VAVy0096wxGfj3bDDLwbHgN6/74= + dependencies: + builtin-modules "^1.0.0" + +is-callable@^1.1.3, is-callable@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" + integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== is-color-stop@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" + integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U= dependencies: css-color-names "^0.0.4" hex-color-regex "^1.1.0" @@ -526,53 +839,85 @@ is-color-stop@^1.0.0: is-date-object@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" + integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= is-directory@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= is-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= is-regex@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" + integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= dependencies: has "^1.0.1" is-resolvable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" + integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= is-svg@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" + integrity sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ== dependencies: html-comment-regex "^1.1.0" -is-symbol@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572" +is-symbol@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" + integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== + dependencies: + has-symbols "^1.0.0" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= isnumeric@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/isnumeric/-/isnumeric-0.2.0.tgz#a2347ba360de19e33d0ffd590fddf7755cbf2e64" + integrity sha1-ojR7o2DeGeM9D/1ZD933dVy/LmQ= js-base64@^2.1.9: - version "2.4.8" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.8.tgz#57a9b130888f956834aa40c5b165ba59c758f033" + version "2.4.9" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.9.tgz#748911fb04f48a60c4771b375cac45a80df11c03" + integrity sha512-xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ== -js-yaml@^3.9.0: +js-yaml@^3.12.0, js-yaml@^3.9.0: version "3.12.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -js-yaml@~3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" + integrity sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -580,18 +925,37 @@ js-yaml@~3.10.0: json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +lcid@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" + integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== + dependencies: + invert-kv "^2.0.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" lodash._reinterpolate@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= lodash.template@^4.2.4: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" + integrity sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A= dependencies: lodash._reinterpolate "~3.0.0" lodash.templatesettings "^4.0.0" @@ -599,80 +963,169 @@ lodash.template@^4.2.4: lodash.templatesettings@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316" + integrity sha1-K01OlbpEDZFf8IvImeRVNmZxMxY= dependencies: lodash._reinterpolate "~3.0.0" lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= + +lodash@^4.17.10: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + +magic-string@0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.1.tgz#b1c248b399cd7485da0fe7385c2fc7011843266e" + integrity sha512-sCuTz6pYom8Rlt4ISPFn6wuFodbKMIHUMv4Qko9P17dpxb7s52KJTmRuZZqHdGmLCK9AOcDare039nRIcfdkEg== + dependencies: + sourcemap-codec "^1.4.1" + +make-dir@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== + dependencies: + pify "^3.0.0" + +map-age-cleaner@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.2.tgz#098fb15538fd3dbe461f12745b0ca8568d4e3f74" + integrity sha512-UN1dNocxQq44IhJyMI4TU8phc2m9BddacHRPRjKGLYaF0jqd3xLz0jS0skpAU9WgYyoR4gHtUpzytNBS385FWQ== + dependencies: + p-defer "^1.0.0" math-expression-evaluator@^1.2.14: version "1.2.17" resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" + integrity sha1-3oGf282E3M2PrlnGrreWFbnSZqw= -mdn-data@^1.0.0, mdn-data@~1.1.0: +mdn-data@~1.1.0: version "1.1.4" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01" + integrity sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA== + +mem@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-4.0.0.tgz#6437690d9471678f6cc83659c00cbafcd6b0cdaf" + integrity sha512-WQxG/5xYc3tMbYLXoXPm81ET2WDULiU5FxbuIoNbJqLOOI8zehXFdZuiUEgfdrU2mVB1pxBZUGlYORSrpuJreA== + dependencies: + map-age-cleaner "^0.1.1" + mimic-fn "^1.0.0" + p-is-promise "^1.1.0" merge@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" + integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== + +mimic-fn@^1.0.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== minimatch@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== dependencies: brace-expansion "^1.1.7" minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= dependencies: minimist "0.0.8" ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -node-releases@^1.0.0-alpha.10: - version "1.0.0-alpha.10" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.0.0-alpha.10.tgz#61c8d5f9b5b2e05d84eba941d05b6f5202f68a2a" +ms@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-releases@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.0.2.tgz#27c296d9fca3b659c64f7d43ea47a31ad2a90e4b" + integrity sha512-zP8Asfg13lG9KDAW85rylSxXBYvaSdtjMIYKHUk8c1fM8drmFwRqbSYKYD+UlNVPUvrceSvgLUKHMOWR5jPWQg== dependencies: semver "^5.3.0" +normalize-package-data@^2.3.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + integrity sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw== + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + normalize-range@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= normalize-url@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.2.0.tgz#98d0948afc82829f374320f405fe9ca55a5f8567" + version "3.3.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" + integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== -nth-check@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.1.tgz#9929acdf628fc2c41098deab82ac580cf149aae4" +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +nth-check@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== dependencies: boolbase "~1.0.0" num2fraction@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= -object-keys@^1.0.8: +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +object-keys@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" + integrity sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag== object.getownpropertydescriptors@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" + integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY= dependencies: define-properties "^1.1.2" es-abstract "^1.5.1" @@ -680,6 +1133,7 @@ object.getownpropertydescriptors@^2.0.3: object.values@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.0.4.tgz#e524da09b4f66ff05df457546ec72ac99f13069a" + integrity sha1-5STaCbT2b/Bd9FdUbscqyZ8TBpo= dependencies: define-properties "^1.1.2" es-abstract "^1.6.1" @@ -689,14 +1143,60 @@ object.values@^1.0.4: on-headers@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" + integrity sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c= onecolor@^3.0.4: - version "3.0.5" - resolved "https://registry.yarnpkg.com/onecolor/-/onecolor-3.0.5.tgz#36eff32201379efdf1180fb445e51a8e2425f9f6" + version "3.1.0" + resolved "https://registry.yarnpkg.com/onecolor/-/onecolor-3.1.0.tgz#b72522270a49569ac20d244b3cd40fe157fda4d2" + integrity sha512-YZSypViXzu3ul5LMu/m6XjJ9ol8qAy9S2VjHl5E6UlhUH1KGKWabyEJifn0Jjpw23bYDzC2ucKMPGiH5kfwSGQ== + +os-locale@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.0.1.tgz#3b014fbf01d87f60a1e5348d80fe870dc82c4620" + integrity sha512-7g5e7dmXPtzcP4bgsZ8ixDVqA7oWYuEz4lOSujeWyliPai4gfVDiFIcwBg3aGCPnmSGfzOKTK3ccPn0CKv3DBw== + dependencies: + execa "^0.10.0" + lcid "^2.0.0" + mem "^4.0.0" + +p-defer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-is-promise@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" + integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4= + +p-limit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.0.0.tgz#e624ed54ee8c460a778b3c9f3670496ff8a57aec" + integrity sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A== + dependencies: + p-try "^2.0.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-try@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" + integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== parse-json@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= dependencies: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" @@ -704,18 +1204,37 @@ parse-json@^4.0.0: parseurl@~1: version "1.3.2" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" + integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M= + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= path-parse@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= pixrem@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/pixrem/-/pixrem-4.0.1.tgz#2da4a1de6ec4423c5fc3794e930b81d4490ec686" + integrity sha1-LaSh3m7EQjxfw3lOkwuB1EkOxoY= dependencies: browserslist "^2.0.0" postcss "^6.0.0" @@ -724,6 +1243,7 @@ pixrem@^4.0.0: pleeease-filters@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/pleeease-filters/-/pleeease-filters-4.0.0.tgz#6632b2fb05648d2758d865384fbced79e1ccaec7" + integrity sha1-ZjKy+wVkjSdY2GU4T7zteeHMrsc= dependencies: onecolor "^3.0.4" postcss "^6.0.1" @@ -731,6 +1251,7 @@ pleeease-filters@^4.0.0: postcss-apply@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/postcss-apply/-/postcss-apply-0.8.0.tgz#14e544bbb5cb6f1c1e048857965d79ae066b1343" + integrity sha1-FOVEu7XLbxweBIhXll15rgZrE0M= dependencies: babel-runtime "^6.23.0" balanced-match "^0.4.2" @@ -739,6 +1260,7 @@ postcss-apply@^0.8.0: postcss-attribute-case-insensitive@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-2.0.0.tgz#94dc422c8f90997f16bd33a3654bbbec084963b4" + integrity sha1-lNxCLI+QmX8WvTOjZUu77AhJY7Q= dependencies: postcss "^6.0.0" postcss-selector-parser "^2.2.3" @@ -746,23 +1268,36 @@ postcss-attribute-case-insensitive@^2.0.0: postcss-cachify@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/postcss-cachify/-/postcss-cachify-1.3.2.tgz#2c34282244ee50ba217cc2959305f6198453f139" + integrity sha1-LDQoIkTuULohfMKVkwX2GYRT8Tk= dependencies: connect-cachify-static "^1.3.0" debug "^2.1.2" postcss "^5.0.0" postcss-calc@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-6.0.1.tgz#3d24171bbf6e7629d422a436ebfe6dd9511f4330" + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-6.0.2.tgz#4d9a43e27dbbf27d095fecb021ac6896e2318337" + integrity sha512-fiznXjEN5T42Qm7qqMCVJXS3roaj9r4xsSi+meaBVe7CJBl8t/QLOXu02Z2E6oWAMWIvCuF6JrvzFekmVEbOKA== dependencies: css-unit-converter "^1.1.1" - postcss "^6.0.0" + postcss "^7.0.2" postcss-selector-parser "^2.2.2" reduce-css-calc "^2.0.0" +postcss-calc@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.1.tgz#36d77bab023b0ecbb9789d84dcb23c4941145436" + integrity sha512-oXqx0m6tb4N3JGdmeMSc/i91KppbYsFZKdH0xMOqK8V1rJlzrKlTdokz8ozUXLVejydRN6u2IddxpcijRj2FqQ== + dependencies: + css-unit-converter "^1.1.1" + postcss "^7.0.5" + postcss-selector-parser "^5.0.0-rc.4" + postcss-value-parser "^3.3.1" + postcss-color-function@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-4.0.1.tgz#402b3f2cebc3f6947e618fb6be3654fbecef6444" + integrity sha1-QCs/LOvD9pR+YY+2vjZU++zvZEQ= dependencies: css-color-function "~1.3.3" postcss "^6.0.1" @@ -772,6 +1307,7 @@ postcss-color-function@^4.0.0: postcss-color-gray@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/postcss-color-gray/-/postcss-color-gray-4.1.0.tgz#e5581ed57eaa826fb652ca11b1e2b7b136a9f9df" + integrity sha512-L4iLKQLdqChz6ZOgGb6dRxkBNw78JFYcJmBz1orHpZoeLtuhDDGegRtX9gSyfoCIM7rWZ3VNOyiqqvk83BEN+w== dependencies: color "^2.0.1" postcss "^6.0.14" @@ -781,6 +1317,7 @@ postcss-color-gray@^4.0.0: postcss-color-hex-alpha@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-3.0.0.tgz#1e53e6c8acb237955e8fd08b7ecdb1b8b8309f95" + integrity sha1-HlPmyKyyN5Vej9CLfs2xuLgwn5U= dependencies: color "^1.0.3" postcss "^6.0.1" @@ -789,6 +1326,7 @@ postcss-color-hex-alpha@^3.0.0: postcss-color-hsl@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postcss-color-hsl/-/postcss-color-hsl-2.0.0.tgz#12703666fa310430e3f30a454dac1386317d5844" + integrity sha1-EnA2ZvoxBDDj8wpFTawThjF9WEQ= dependencies: postcss "^6.0.1" postcss-value-parser "^3.3.0" @@ -797,6 +1335,7 @@ postcss-color-hsl@^2.0.0: postcss-color-hwb@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-color-hwb/-/postcss-color-hwb-3.0.0.tgz#3402b19ef4d8497540c1fb5072be9863ca95571e" + integrity sha1-NAKxnvTYSXVAwftQcr6YY8qVVx4= dependencies: color "^1.0.3" postcss "^6.0.1" @@ -806,6 +1345,7 @@ postcss-color-hwb@^3.0.0: postcss-color-rebeccapurple@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-3.1.0.tgz#ce1269ecc2d0d8bf92aab44bd884e633124c33ec" + integrity sha512-212hJUk9uSsbwO5ECqVjmh/iLsmiVL1xy9ce9TVf+X3cK/ZlUIlaMdoxje/YpsL9cmUH3I7io+/G2LyWx5rg1g== dependencies: postcss "^6.0.22" postcss-values-parser "^1.5.0" @@ -813,6 +1353,7 @@ postcss-color-rebeccapurple@^3.0.0: postcss-color-rgb@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postcss-color-rgb/-/postcss-color-rgb-2.0.0.tgz#14539c8a7131494b482e0dd1cc265ff6514b5263" + integrity sha1-FFOcinExSUtILg3RzCZf9lFLUmM= dependencies: postcss "^6.0.1" postcss-value-parser "^3.3.0" @@ -820,31 +1361,35 @@ postcss-color-rgb@^2.0.0: postcss-color-rgba-fallback@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-color-rgba-fallback/-/postcss-color-rgba-fallback-3.0.0.tgz#37d5c9353a07a09270912a82606bb42a0d702c04" + integrity sha1-N9XJNToHoJJwkSqCYGu0Kg1wLAQ= dependencies: postcss "^6.0.6" postcss-value-parser "^3.3.0" rgb-hex "^2.1.0" -postcss-colormin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.1.tgz#6f1c18a0155bc69613f2ff13843e2e4ae8ff0bbe" +postcss-colormin@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.2.tgz#93cd1fa11280008696887db1a528048b18e7ed99" + integrity sha512-1QJc2coIehnVFsz0otges8kQLsryi4lo19WD+U5xCWvXd0uw/Z+KKYnbiNDCnO9GP+PvErPHCG0jNvWTngk9Rw== dependencies: browserslist "^4.0.0" color "^3.0.0" has "^1.0.0" - postcss "^6.0.0" + postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-convert-values@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.0.tgz#77d77d9aed1dc4e6956e651cc349d53305876f62" +postcss-convert-values@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f" + integrity sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ== dependencies: - postcss "^6.0.0" + postcss "^7.0.0" postcss-value-parser "^3.0.0" postcss-cssnext@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/postcss-cssnext/-/postcss-cssnext-3.1.0.tgz#927dc29341a938254cde38ea60a923b9dfedead9" + integrity sha512-awPDhI4OKetcHCr560iVCoDuP6e/vn0r6EAqdWPpAavJMvkBSZ6kDpSN4b3mB3Ti57hQMunHHM8Wvx9PeuYXtA== dependencies: autoprefixer "^7.1.1" caniuse-api "^2.0.0" @@ -881,12 +1426,14 @@ postcss-cssnext@^3.0.0: postcss-custom-media@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-6.0.0.tgz#be532784110ecb295044fb5395a18006eb21a737" + integrity sha1-vlMnhBEOyylQRPtTlaGABushpzc= dependencies: postcss "^6.0.1" postcss-custom-properties@^6.1.0: version "6.3.1" resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-6.3.1.tgz#5c52abde313d7ec9368c4abf67d27a656cba8b39" + integrity sha512-zoiwn4sCiUFbr4KcgcNZLFkR6gVQom647L+z1p/KBVHZ1OYwT87apnS42atJtx6XlX2yI7N5fjXbFixShQO2QQ== dependencies: balanced-match "^1.0.0" postcss "^6.0.18" @@ -894,56 +1441,65 @@ postcss-custom-properties@^6.1.0: postcss-custom-selectors@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-4.0.1.tgz#781382f94c52e727ef5ca4776ea2adf49a611382" + integrity sha1-eBOC+UxS5yfvXKR3bqKt9JphE4I= dependencies: postcss "^6.0.1" postcss-selector-matches "^3.0.0" -postcss-discard-comments@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.0.tgz#9684a299e76b3e93263ef8fd2adbf1a1c08fd88d" +postcss-discard-comments@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.1.tgz#30697735b0c476852a7a11050eb84387a67ef55d" + integrity sha512-Ay+rZu1Sz6g8IdzRjUgG2NafSNpp2MSMOQUb+9kkzzzP+kh07fP0yNbhtFejURnyVXSX3FYy2nVNW1QTnNjgBQ== dependencies: - postcss "^6.0.0" + postcss "^7.0.0" -postcss-discard-duplicates@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.0.tgz#42f3c267f85fa909e042c35767ecfd65cb2bd72c" +postcss-discard-duplicates@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb" + integrity sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ== dependencies: - postcss "^6.0.0" + postcss "^7.0.0" -postcss-discard-empty@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.0.tgz#55e18a59c74128e38c7d2804bcfa4056611fb97f" +postcss-discard-empty@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765" + integrity sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w== dependencies: - postcss "^6.0.0" + postcss "^7.0.0" -postcss-discard-overridden@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.0.tgz#4a0bf85978784cf1f81ed2c1c1fd9d964a1da1fa" +postcss-discard-overridden@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57" + integrity sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg== dependencies: - postcss "^6.0.0" + postcss "^7.0.0" postcss-font-family-system-ui@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-font-family-system-ui/-/postcss-font-family-system-ui-3.0.0.tgz#675fe7a9e029669f05f8dba2e44c2225ede80623" + integrity sha512-58G/hTxMSSKlIRpcPUjlyo6hV2MEzvcVO2m4L/T7Bb2fJTG4DYYfQjQeRvuimKQh1V1sOzCIz99g+H2aFNtlQw== dependencies: postcss "^6.0" postcss-font-variant@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-3.0.0.tgz#08ccc88f6050ba82ed8ef2cc76c0c6a6b41f183e" + integrity sha1-CMzIj2BQuoLtjvLMdsDGprQfGD4= dependencies: postcss "^6.0.1" postcss-image-set-polyfill@^0.3.5: version "0.3.5" resolved "https://registry.yarnpkg.com/postcss-image-set-polyfill/-/postcss-image-set-polyfill-0.3.5.tgz#0f193413700cf1f82bd39066ef016d65a4a18181" + integrity sha1-Dxk0E3AM8fgr05Bm7wFtZaShgYE= dependencies: postcss "^6.0.1" postcss-media-query-parser "^0.2.3" postcss-import@^12.0.0: - version "12.0.0" - resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-12.0.0.tgz#149f96a4ef0b27525c419784be8517ebd17e92c5" + version "12.0.1" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-12.0.1.tgz#cf8c7ab0b5ccab5649024536e565f841928b7153" + integrity sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw== dependencies: postcss "^7.0.1" postcss-value-parser "^3.2.3" @@ -953,6 +1509,7 @@ postcss-import@^12.0.0: postcss-initial@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-2.0.0.tgz#72715f7336e0bb79351d99ee65c4a253a8441ba4" + integrity sha1-cnFfczbgu3k1HZnuZcSiU6hEG6Q= dependencies: lodash.template "^4.2.4" postcss "^6.0.1" @@ -960,160 +1517,183 @@ postcss-initial@^2.0.0: postcss-media-minmax@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-3.0.0.tgz#675256037a43ef40bc4f0760bfd06d4dc69d48d2" + integrity sha1-Z1JWA3pD70C8Twdgv9BtTcadSNI= dependencies: postcss "^6.0.1" postcss-media-query-parser@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244" + integrity sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ= -postcss-merge-longhand@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.4.tgz#bffc7c6ffa146591c993a0bb8373d65f9a06d4d0" +postcss-merge-longhand@^4.0.9: + version "4.0.9" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.9.tgz#c2428b994833ffb2a072f290ca642e75ceabcd6f" + integrity sha512-UVMXrXF5K/kIwUbK/crPFCytpWbNX2Q3dZSc8+nQUgfOHrCT4+MHncpdxVphUlQeZxlLXUJbDyXc5NBhTnS2tA== dependencies: css-color-names "0.0.4" - postcss "^6.0.0" + postcss "^7.0.0" postcss-value-parser "^3.0.0" stylehacks "^4.0.0" -postcss-merge-rules@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.1.tgz#430fd59b3f2ed2e8afcd0b31278eda39854abb10" +postcss-merge-rules@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.2.tgz#2be44401bf19856f27f32b8b12c0df5af1b88e74" + integrity sha512-UiuXwCCJtQy9tAIxsnurfF0mrNHKc4NnNx6NxqmzNNjXpQwLSukUxELHTRF0Rg1pAmcoKLih8PwvZbiordchag== dependencies: browserslist "^4.0.0" caniuse-api "^3.0.0" cssnano-util-same-parent "^4.0.0" - postcss "^6.0.0" + postcss "^7.0.0" postcss-selector-parser "^3.0.0" vendors "^1.0.0" postcss-message-helpers@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e" + integrity sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4= -postcss-minify-font-values@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.0.tgz#4cc33d283d6a81759036e757ef981d92cbd85bed" +postcss-minify-font-values@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6" + integrity sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg== dependencies: - postcss "^6.0.0" + postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-minify-gradients@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.0.tgz#3fc3916439d27a9bb8066db7cdad801650eb090e" +postcss-minify-gradients@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.1.tgz#6da95c6e92a809f956bb76bf0c04494953e1a7dd" + integrity sha512-pySEW3E6Ly5mHm18rekbWiAjVi/Wj8KKt2vwSfVFAWdW6wOIekgqxKxLU7vJfb107o3FDNPkaYFCxGAJBFyogA== dependencies: cssnano-util-get-arguments "^4.0.0" is-color-stop "^1.0.0" - postcss "^6.0.0" + postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-minify-params@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.0.tgz#05e9166ee48c05af651989ce84d39c1b4d790674" +postcss-minify-params@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.1.tgz#5b2e2d0264dd645ef5d68f8fec0d4c38c1cf93d2" + integrity sha512-h4W0FEMEzBLxpxIVelRtMheskOKKp52ND6rJv+nBS33G1twu2tCyurYj/YtgU76+UDCvWeNs0hs8HFAWE2OUFg== dependencies: alphanum-sort "^1.0.0" + browserslist "^4.0.0" cssnano-util-get-arguments "^4.0.0" - postcss "^6.0.0" + postcss "^7.0.0" postcss-value-parser "^3.0.0" uniqs "^2.0.0" -postcss-minify-selectors@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.0.tgz#b1e9f6c463416d3fcdcb26e7b785d95f61578aad" +postcss-minify-selectors@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.1.tgz#a891c197977cc37abf60b3ea06b84248b1c1e9cd" + integrity sha512-8+plQkomve3G+CodLCgbhAKrb5lekAnLYuL1d7Nz+/7RANpBEVdgBkPNwljfSKvZ9xkkZTZITd04KP+zeJTJqg== dependencies: alphanum-sort "^1.0.0" has "^1.0.0" - postcss "^6.0.0" + postcss "^7.0.0" postcss-selector-parser "^3.0.0" postcss-nesting@^4.0.1: version "4.2.1" resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-4.2.1.tgz#0483bce338b3f0828ced90ff530b29b98b00300d" + integrity sha512-IkyWXICwagCnlaviRexi7qOdwPw3+xVVjgFfGsxmztvRVaNxAlrypOIKqDE5mxY+BVxnId1rnUKBRQoNE2VDaA== dependencies: postcss "^6.0.11" -postcss-normalize-charset@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.0.tgz#24527292702d5e8129eafa3d1de49ed51a6ab730" +postcss-normalize-charset@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4" + integrity sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g== dependencies: - postcss "^6.0.0" + postcss "^7.0.0" -postcss-normalize-display-values@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.0.tgz#950e0c7be3445770a160fffd6b6644c3c0cd8f89" +postcss-normalize-display-values@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.1.tgz#d9a83d47c716e8a980f22f632c8b0458cfb48a4c" + integrity sha512-R5mC4vaDdvsrku96yXP7zak+O3Mm9Y8IslUobk7IMP+u/g+lXvcN4jngmHY5zeJnrQvE13dfAg5ViU05ZFDwdg== dependencies: cssnano-util-get-match "^4.0.0" - postcss "^6.0.0" + postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-normalize-positions@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.0.tgz#ee9343ab981b822c63ab72615ecccd08564445a3" +postcss-normalize-positions@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.1.tgz#ee2d4b67818c961964c6be09d179894b94fd6ba1" + integrity sha512-GNoOaLRBM0gvH+ZRb2vKCIujzz4aclli64MBwDuYGU2EY53LwiP7MxOZGE46UGtotrSnmarPPZ69l2S/uxdaWA== dependencies: cssnano-util-get-arguments "^4.0.0" has "^1.0.0" - postcss "^6.0.0" + postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-normalize-repeat-style@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.0.tgz#b711c592cf16faf9ff575e42fa100b6799083eff" +postcss-normalize-repeat-style@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.1.tgz#5293f234b94d7669a9f805495d35b82a581c50e5" + integrity sha512-fFHPGIjBUyUiswY2rd9rsFcC0t3oRta4wxE1h3lpwfQZwFeFjXFSiDtdJ7APCmHQOnUZnqYBADNRPKPwFAONgA== dependencies: cssnano-util-get-arguments "^4.0.0" cssnano-util-get-match "^4.0.0" - postcss "^6.0.0" + postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-normalize-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.0.tgz#718cb6d30a6fac6ac6a830e32c06c07dbc66fe5d" +postcss-normalize-string@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.1.tgz#23c5030c2cc24175f66c914fa5199e2e3c10fef3" + integrity sha512-IJoexFTkAvAq5UZVxWXAGE0yLoNN/012v7TQh5nDo6imZJl2Fwgbhy3J2qnIoaDBrtUP0H7JrXlX1jjn2YcvCQ== dependencies: has "^1.0.0" - postcss "^6.0.0" + postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-normalize-timing-functions@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.0.tgz#0351f29886aa981d43d91b2c2bd1aea6d0af6d23" +postcss-normalize-timing-functions@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.1.tgz#8be83e0b9cb3ff2d1abddee032a49108f05f95d7" + integrity sha512-1nOtk7ze36+63ONWD8RCaRDYsnzorrj+Q6fxkQV+mlY5+471Qx9kspqv0O/qQNMeApg8KNrRf496zHwJ3tBZ7w== dependencies: cssnano-util-get-match "^4.0.0" - postcss "^6.0.0" + postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-normalize-unicode@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.0.tgz#5acd5d47baea5d17674b2ccc4ae5166fa88cdf97" +postcss-normalize-unicode@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb" + integrity sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg== dependencies: - postcss "^6.0.0" + browserslist "^4.0.0" + postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-normalize-url@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.0.tgz#b7a9c8ad26cf26694c146eb2d68bd0cf49956f0d" +postcss-normalize-url@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1" + integrity sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA== dependencies: is-absolute-url "^2.0.0" normalize-url "^3.0.0" - postcss "^6.0.0" + postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-normalize-whitespace@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.0.tgz#1da7e76b10ae63c11827fa04fc3bb4a1efe99cc0" +postcss-normalize-whitespace@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.1.tgz#d14cb639b61238418ac8bc8d3b7bdd65fc86575e" + integrity sha512-U8MBODMB2L+nStzOk6VvWWjZgi5kQNShCyjRhMT3s+W9Jw93yIjOnrEkKYD3Ul7ChWbEcjDWmXq0qOL9MIAnAw== dependencies: - postcss "^6.0.0" + postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-ordered-values@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.0.0.tgz#58b40c74f72e022eb34152c12e4b0f9354482fc2" +postcss-ordered-values@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.1.tgz#2e3b432ef3e489b18333aeca1f1295eb89be9fc2" + integrity sha512-PeJiLgJWPzkVF8JuKSBcylaU+hDJ/TX3zqAMIjlghgn1JBi6QwQaDZoDIlqWRcCAI8SxKrt3FCPSRmOgKRB97Q== dependencies: cssnano-util-get-arguments "^4.0.0" - postcss "^6.0.0" + postcss "^7.0.0" postcss-value-parser "^3.0.0" postcss-pseudo-class-any-link@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-4.0.0.tgz#9152a0613d3450720513e8892854bae42d0ee68e" + integrity sha1-kVKgYT00UHIFE+iJKFS65C0O5o4= dependencies: postcss "^6.0.1" postcss-selector-parser "^2.2.3" @@ -1121,36 +1701,41 @@ postcss-pseudo-class-any-link@^4.0.0: postcss-pseudoelements@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/postcss-pseudoelements/-/postcss-pseudoelements-5.0.0.tgz#eef194e8d524645ca520a949e95e518e812402cb" + integrity sha1-7vGU6NUkZFylIKlJ6V5RjoEkAss= dependencies: postcss "^6.0.0" -postcss-reduce-initial@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.1.tgz#f2d58f50cea2b0c5dc1278d6ea5ed0ff5829c293" +postcss-reduce-initial@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.2.tgz#bac8e325d67510ee01fa460676dc8ea9e3b40f15" + integrity sha512-epUiC39NonKUKG+P3eAOKKZtm5OtAtQJL7Ye0CBN1f+UQTHzqotudp+hki7zxXm7tT0ZAKDMBj1uihpPjP25ug== dependencies: browserslist "^4.0.0" caniuse-api "^3.0.0" has "^1.0.0" - postcss "^6.0.0" + postcss "^7.0.0" -postcss-reduce-transforms@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.0.tgz#f645fc7440c35274f40de8104e14ad7163edf188" +postcss-reduce-transforms@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.1.tgz#8600d5553bdd3ad640f43bff81eb52f8760d4561" + integrity sha512-sZVr3QlGs0pjh6JAIe6DzWvBaqYw05V1t3d9Tp+VnFRT5j+rsqoWsysh/iSD7YNsULjq9IAylCznIwVd5oU/zA== dependencies: cssnano-util-get-match "^4.0.0" has "^1.0.0" - postcss "^6.0.0" + postcss "^7.0.0" postcss-value-parser "^3.0.0" postcss-replace-overflow-wrap@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-2.0.0.tgz#794db6faa54f8db100854392a93af45768b4e25b" + integrity sha1-eU22+qVPjbEAhUOSqTr0V2i04ls= dependencies: postcss "^6.0.1" postcss-selector-matches@^3.0.0, postcss-selector-matches@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-3.0.1.tgz#e5634011e13950881861bbdd58c2d0111ffc96ab" + integrity sha1-5WNAEeE5UIgYYbvdWMLQER/8lqs= dependencies: balanced-match "^0.4.2" postcss "^6.0.1" @@ -1158,6 +1743,7 @@ postcss-selector-matches@^3.0.0, postcss-selector-matches@^3.0.1: postcss-selector-not@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-3.0.1.tgz#2e4db2f0965336c01e7cec7db6c60dff767335d9" + integrity sha1-Lk2y8JZTNsAefOx9tsYN/3ZzNdk= dependencies: balanced-match "^0.4.2" postcss "^6.0.1" @@ -1165,6 +1751,7 @@ postcss-selector-not@^3.0.1: postcss-selector-parser@^2.2.2, postcss-selector-parser@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90" + integrity sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A= dependencies: flatten "^1.0.2" indexes-of "^1.0.1" @@ -1173,35 +1760,49 @@ postcss-selector-parser@^2.2.2, postcss-selector-parser@^2.2.3: postcss-selector-parser@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865" + integrity sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU= dependencies: dot-prop "^4.1.1" indexes-of "^1.0.1" uniq "^1.0.1" -postcss-svgo@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.0.tgz#c0bbad02520fc636c9d78b0e8403e2e515c32285" +postcss-selector-parser@^5.0.0-rc.4: + version "5.0.0-rc.4" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0-rc.4.tgz#ca5e77238bf152966378c13e91ad6d611568ea87" + integrity sha512-0XvfYuShrKlTk1ooUrVzMCFQRcypsdEIsGqh5IxC5rdtBi4/M/tDAJeSONwC2MTqEFsmPZYAV7Dd4X8rgAfV0A== + dependencies: + cssesc "^2.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-svgo@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.1.tgz#5628cdb38f015de6b588ce6d0bf0724b492b581d" + integrity sha512-YD5uIk5NDRySy0hcI+ZJHwqemv2WiqqzDgtvgMzO8EGSkK5aONyX8HMVFRFJSdO8wUWTuisUFn/d7yRRbBr5Qw== dependencies: is-svg "^3.0.0" - postcss "^6.0.0" + postcss "^7.0.0" postcss-value-parser "^3.0.0" svgo "^1.0.0" -postcss-unique-selectors@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.0.tgz#04c1e9764c75874261303402c41f0e9769fc5501" +postcss-unique-selectors@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac" + integrity sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg== dependencies: alphanum-sort "^1.0.0" - postcss "^6.0.0" + postcss "^7.0.0" uniqs "^2.0.0" -postcss-value-parser@^3.0.0, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" +postcss-value-parser@^3.0.0, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0, postcss-value-parser@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== postcss-values-parser@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-1.5.0.tgz#5d9fa63e2bcb0179ce48f3235303765eb89f3047" + integrity sha512-3M3p+2gMp0AH3da530TlX8kiO1nxdTnc3C6vr8dMxRLIlh8UYkz0/wcwptSXjhtx2Fr0TySI7a+BHDQ8NL7LaQ== dependencies: flatten "^1.0.2" indexes-of "^1.0.1" @@ -1210,6 +1811,7 @@ postcss-values-parser@^1.5.0: postcss@^5.0.0: version "5.2.18" resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" + integrity sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg== dependencies: chalk "^1.1.3" js-base64 "^2.1.9" @@ -1219,40 +1821,73 @@ postcss@^5.0.0: postcss@^6.0, postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.11, postcss@^6.0.14, postcss@^6.0.17, postcss@^6.0.18, postcss@^6.0.22, postcss@^6.0.5, postcss@^6.0.6: version "6.0.23" resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" + integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== dependencies: chalk "^2.4.1" source-map "^0.6.1" supports-color "^5.4.0" -postcss@^7.0.1: - version "7.0.2" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.2.tgz#7b5a109de356804e27f95a960bef0e4d5bc9bb18" +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.2, postcss@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.5.tgz#70e6443e36a6d520b0fd4e7593fcca3635ee9f55" + integrity sha512-HBNpviAUFCKvEh7NZhw1e8MBPivRszIiUnhrJ+sBFVSYSqubrzwX3KG51mYgcRHX8j/cAgZJedONZcm5jTBdgQ== dependencies: chalk "^2.4.1" source-map "^0.6.1" - supports-color "^5.4.0" + supports-color "^5.5.0" + +process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= read-cache@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha1-5mTvMRYRZsl1HNvo28+GtftY93Q= dependencies: pify "^2.3.0" +read-pkg@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-4.0.1.tgz#963625378f3e1c4d48c85872b5a6ec7d5d093237" + integrity sha1-ljYlN48+HE1IyFhytabsfV0JMjc= + dependencies: + normalize-package-data "^2.3.2" + parse-json "^4.0.0" + pify "^3.0.0" + +readable-stream@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + reduce-css-calc@^1.2.7: version "1.3.0" resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" + integrity sha1-dHyRTgSWFKTJz7umKYca0dKSdxY= dependencies: balanced-match "^0.4.2" math-expression-evaluator "^1.2.14" reduce-function-call "^1.0.1" reduce-css-calc@^2.0.0: - version "2.1.4" - resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.4.tgz#c20e9cda8445ad73d4ff4bea960c6f8353791708" + version "2.1.5" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.5.tgz#f283712f0c9708ef952d328f4b16112d57b03714" + integrity sha512-AybiBU03FKbjYzyvJvwkJZY6NLN+80Ufc2EqEs+41yQH+8wqBEslD6eGiS0oIeq5TNLA5PrhBeYHXWdn8gtW7A== dependencies: css-unit-converter "^1.1.1" postcss-value-parser "^3.3.0" @@ -1260,107 +1895,275 @@ reduce-css-calc@^2.0.0: reduce-function-call@^1.0.1, reduce-function-call@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.2.tgz#5a200bf92e0e37751752fe45b0ab330fd4b6be99" + integrity sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk= dependencies: balanced-match "^0.4.2" regenerator-runtime@^0.11.0: version "0.11.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +replace-ext@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" + integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs= + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= resolve@^1.1.7: version "1.8.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" + integrity sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA== dependencies: path-parse "^1.0.5" rgb-hex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/rgb-hex/-/rgb-hex-2.1.0.tgz#c773c5fe2268a25578d92539a82a7a5ce53beda6" + integrity sha1-x3PF/iJoolV42SU5qCp6XOU77aY= rgb-regex@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" + integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE= rgb@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/rgb/-/rgb-0.1.0.tgz#be27b291e8feffeac1bd99729721bfa40fc037b5" + integrity sha1-vieykej+/+rBvZlylyG/pA/AN7U= rgba-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" + integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= + +rollup-plugin-closure-compiler-js@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/rollup-plugin-closure-compiler-js/-/rollup-plugin-closure-compiler-js-1.0.6.tgz#58e3e31297ad1a532d9114108bc06f2756d72c3d" + integrity sha1-WOPjEpetGlMtkRQQi8BvJ1bXLD0= + dependencies: + google-closure-compiler-js ">20170000" + +rollup@^0.66.6: + version "0.66.6" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.66.6.tgz#ce7d6185beb7acea644ce220c25e71ae03275482" + integrity sha512-J7/SWanrcb83vfIHqa8+aVVGzy457GcjA6GVZEnD0x2u4OnOd0Q1pCrEoNe8yLwM6z6LZP02zBT2uW0yh5TqOw== + dependencies: + "@types/estree" "0.0.39" + "@types/node" "*" + +rxjs@6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.2.2.tgz#eb75fa3c186ff5289907d06483a77884586e1cf9" + integrity sha512-0MI8+mkKAXZUF9vMrEoPnaoHkfzBPP4IGwUYRJhIRJF6/w3uByO1e91bEHn8zd43RdkTMKiooYKmwz7RH6zfOQ== + dependencies: + tslib "^1.9.0" + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -semver@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" + integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= dependencies: is-arrayish "^0.3.1" -source-map@^0.5.3, source-map@^0.5.6: +source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= source-map@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sourcemap-codec@^1.4.1: + version "1.4.3" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.3.tgz#0ba615b73ec35112f63c2f2d9e7c3f87282b0e33" + integrity sha512-vFrY/x/NdsD7Yc8mpTJXuao9S8lq08Z/kOITHz6b7YbfI9xL8Spe5EvSQUHOI7SbpY8bRPr0U3kKSsPuqEGSfA== + +spawn-command@^0.0.2-1: + version "0.0.2-1" + resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" + integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A= + +spdx-correct@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.2.tgz#19bb409e91b47b1ad54159243f7312a858db3c2e" + integrity sha512-q9hedtzyXHr5S0A1vEPoK/7l8NpfkFYTq6iCY+Pno2ZbdZR6WexZFtqeVGkGxW3TEJMN914Z55EnAGMmenlIQQ== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" + integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.2.tgz#a59efc09784c2a5bada13cfeaf5c75dd214044d2" + integrity sha512-qky9CVt0lVIECkEsYbNILVnPvycuEBkXoMFLRWsREkomQLevYhtRKC+R91a5TOAQ3bCMjikRwhyaRqj1VYatYg== sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= stable@~0.1.6: version "0.1.8" resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== -strip-ansi@^3.0.0: +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= dependencies: ansi-regex "^2.0.0" -stylehacks@^4.0.0: +strip-ansi@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.0.tgz#64b323951c4a24e5fc7b2ec06c137bf32d155e8a" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +stylehacks@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.1.tgz#3186595d047ab0df813d213e51c8b94e0b9010f2" + integrity sha512-TK5zEPeD9NyC1uPIdjikzsgWxdQQN/ry1X3d1iOz1UkYDCmcr928gWD1KHgyC27F50UnE0xCTrBOO1l6KR8M4w== dependencies: browserslist "^4.0.0" - postcss "^6.0.0" + postcss "^7.0.0" postcss-selector-parser "^3.0.0" supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= supports-color@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= dependencies: has-flag "^1.0.0" -supports-color@^5.3.0, supports-color@^5.4.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" +supports-color@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" + integrity sha1-vnoN5ITexcXN34s9WRJQRJEvY1s= + dependencies: + has-flag "^2.0.0" + +supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== dependencies: has-flag "^3.0.0" svgo@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.0.5.tgz#7040364c062a0538abacff4401cea6a26a7a389a" + version "1.1.1" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.1.1.tgz#12384b03335bcecd85cfa5f4e3375fed671cb985" + integrity sha512-GBkJbnTuFpM4jFbiERHDWhZc/S/kpHToqmZag3aEBjPYK44JAN2QBjvrGIxLOoCyMZjuFQIfTO2eJd8uwLY/9g== dependencies: coa "~2.0.1" colors "~1.1.2" - css-select "~1.3.0-rc0" + css-select "^2.0.0" css-select-base-adapter "~0.1.0" - css-tree "1.0.0-alpha25" + css-tree "1.0.0-alpha.28" css-url-regex "^1.1.0" csso "^3.5.0" - js-yaml "~3.10.0" + js-yaml "^3.12.0" mkdirp "~0.5.1" object.values "^1.0.4" sax "~1.2.4" @@ -1368,25 +2171,57 @@ svgo@^1.0.0: unquote "~1.1.1" util.promisify "~1.0.0" +temp-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" + integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0= + +temp-write@3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/temp-write/-/temp-write-3.4.0.tgz#8cff630fb7e9da05f047c74ce4ce4d685457d492" + integrity sha1-jP9jD7fp2gXwR8dM5M5NaFRX1JI= + dependencies: + graceful-fs "^4.1.2" + is-stream "^1.1.0" + make-dir "^1.0.0" + pify "^3.0.0" + temp-dir "^1.0.0" + uuid "^3.0.1" + timsort@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" + integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= traverse-chain@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/traverse-chain/-/traverse-chain-0.1.0.tgz#61dbc2d53b69ff6091a12a168fd7d433107e40f1" + integrity sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE= + +tree-kill@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.0.tgz#5846786237b4239014f05db156b643212d4c6f36" + integrity sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg== + +tslib@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" + integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== uniq@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= uniqs@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" + integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= units-css@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/units-css/-/units-css-0.4.0.tgz#d6228653a51983d7c16ff28f8b9dc3b1ffed3a07" + integrity sha1-1iKGU6UZg9fBb/KPi53Dsf/tOgc= dependencies: isnumeric "^0.2.0" viewport-dimensions "^0.2.0" @@ -1394,25 +2229,122 @@ units-css@^0.4.0: unquote@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" + integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ= + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= util.promisify@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" + integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== dependencies: define-properties "^1.1.2" object.getownpropertydescriptors "^2.0.3" +uuid@^3.0.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + vendors@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.2.tgz#7fcb5eef9f5623b156bcea89ec37d63676f21801" + integrity sha512-w/hry/368nO21AN9QljsaIhb9ZiZtZARoVH5f3CsFbawdLdayCgKRPup7CggujvySMxx0I91NOyxdVENohprLQ== viewport-dimensions@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/viewport-dimensions/-/viewport-dimensions-0.2.0.tgz#de740747db5387fd1725f5175e91bac76afdf36c" + integrity sha1-3nQHR9tTh/0XJfUXXpG6x2r982w= + +vinyl-sourcemaps-apply@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" + integrity sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU= + dependencies: + source-map "^0.5.1" + +vinyl@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86" + integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg== + dependencies: + clone "^2.1.1" + clone-buffer "^1.0.0" + clone-stats "^1.0.0" + cloneable-readable "^1.0.0" + remove-trailing-separator "^1.0.1" + replace-ext "^1.0.0" watch@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/watch/-/watch-1.0.2.tgz#340a717bde765726fa0aa07d721e0147a551df0c" + integrity sha1-NApxe952Vyb6CqB9ch4BR6VR3ww= dependencies: exec-sh "^0.2.0" minimist "^1.2.0" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +xregexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020" + integrity sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg== + +"y18n@^3.2.1 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +yargs-parser@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" + integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== + dependencies: + camelcase "^4.1.0" + +yargs@^12.0.1: + version "12.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.2.tgz#fe58234369392af33ecbef53819171eff0f5aadc" + integrity sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ== + dependencies: + cliui "^4.0.0" + decamelize "^2.0.0" + find-up "^3.0.0" + get-caller-file "^1.0.1" + os-locale "^3.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1 || ^4.0.0" + yargs-parser "^10.1.0" diff --git a/src/API/APIRequestBuilder.php b/src/API/APIRequestBuilder.php index 3fb2a7b5..e0027e7b 100644 --- a/src/API/APIRequestBuilder.php +++ b/src/API/APIRequestBuilder.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -66,6 +66,18 @@ class APIRequestBuilder { */ protected $request; + /** + * Do a basic minimal GET request + * + * @param string $uri + * @return Request + */ + public static function simpleRequest(string $uri): Request + { + return (new Request($uri)) + ->withHeader('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:64.0) Gecko/20100101 Firefox/64.0 '); + } + /** * Set an authorization header * diff --git a/src/API/Anilist.php b/src/API/Anilist.php index ec5bb837..8bc3640a 100644 --- a/src/API/Anilist.php +++ b/src/API/Anilist.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -29,10 +29,11 @@ use Aviat\AnimeClient\API\Enum\{ * Constants and mappings for the Anilist API */ final class Anilist { - const AUTH_URL = 'https://anilist.co/api/v2/oauth/authorize'; - const BASE_URL = 'https://graphql.anilist.co'; + public const AUTH_URL = 'https://anilist.co/api/v2/oauth/authorize'; + public const TOKEN_URL = 'https://anilist.co/api/v2/oauth/token'; + public const BASE_URL = 'https://graphql.anilist.co'; - const KITSU_ANILIST_WATCHING_STATUS_MAP = [ + public const KITSU_ANILIST_WATCHING_STATUS_MAP = [ KAWS::WATCHING => AnimeWatchingStatus::WATCHING, KAWS::COMPLETED => AnimeWatchingStatus::COMPLETED, KAWS::ON_HOLD => AnimeWatchingStatus::ON_HOLD, @@ -40,12 +41,28 @@ final class Anilist { KAWS::PLAN_TO_WATCH => AnimeWatchingStatus::PLAN_TO_WATCH, ]; - const ANILIST_KITSU_WATCHING_STATUS_MAP = [ - 'CURRENT' => KAWS::WATCHING, - 'COMPLETED' => KAWS::COMPLETED, - 'PAUSED' => KAWS::ON_HOLD, - 'DROPPED' => KAWS::DROPPED, - 'PLANNING' => KAWS::PLAN_TO_WATCH, + public const ANILIST_KITSU_WATCHING_STATUS_MAP = [ + AnimeWatchingStatus::WATCHING => KAWS::WATCHING, + AnimeWatchingStatus::COMPLETED => KAWS::COMPLETED, + AnimeWatchingStatus::ON_HOLD => KAWS::ON_HOLD, + AnimeWatchingStatus::DROPPED => KAWS::DROPPED, + AnimeWatchingStatus::PLAN_TO_WATCH => KAWS::PLAN_TO_WATCH, + ]; + + public const KITSU_ANILIST_READING_STATUS_MAP = [ + KMRS::READING => MangaReadingStatus::READING, + KMRS::COMPLETED => MangaReadingStatus::COMPLETED, + KMRS::ON_HOLD => MangaReadingStatus::ON_HOLD, + KMRS::DROPPED => MangaReadingStatus::DROPPED, + KMRS::PLAN_TO_READ => MangaReadingStatus::PLAN_TO_READ, + ]; + + public const ANILIST_KITSU_READING_STATUS_MAP = [ + MangaReadingStatus::READING => KMRS::READING, + MangaReadingStatus::COMPLETED => KMRS::COMPLETED, + MangaReadingStatus::ON_HOLD => KMRS::ON_HOLD, + MangaReadingStatus::DROPPED => KMRS::DROPPED, + MangaReadingStatus::PLAN_TO_READ => KMRS::PLAN_TO_READ, ]; public static function getIdToWatchingStatusMap() @@ -67,7 +84,8 @@ final class Anilist { 'COMPLETED' => MangaReadingStatus::COMPLETED, 'PAUSED' => MangaReadingStatus::ON_HOLD, 'DROPPED' => MangaReadingStatus::DROPPED, - 'PLANNING' => MangaReadingStatus::PLAN_TO_READ + 'PLANNING' => MangaReadingStatus::PLAN_TO_READ, + 'REPEATING' => MangaReadingStatus::READING, ]; } } \ No newline at end of file diff --git a/src/API/Anilist/AnilistRequestBuilder.php b/src/API/Anilist/AnilistRequestBuilder.php index 493cc0b2..d80eaad0 100644 --- a/src/API/Anilist/AnilistRequestBuilder.php +++ b/src/API/Anilist/AnilistRequestBuilder.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -26,8 +26,8 @@ final class AnilistRequestBuilder extends APIRequestBuilder { * The base url for api requests * @var string $base_url */ - protected $baseUrl = 'https://kitsu.io/api/edge/'; - + protected $baseUrl = 'https://graphql.anilist.co'; + /** * Valid HTTP request methods * @var array @@ -41,9 +41,7 @@ final class AnilistRequestBuilder extends APIRequestBuilder { */ protected $defaultHeaders = [ 'User-Agent' => USER_AGENT, - 'Accept' => 'application/vnd.api+json', - 'Content-Type' => 'application/vnd.api+json', - 'CLIENT_ID' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd', - 'CLIENT_SECRET' => '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151', + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', ]; } \ No newline at end of file diff --git a/src/API/Anilist/AnilistTrait.php b/src/API/Anilist/AnilistTrait.php index c4ae953e..483905ee 100644 --- a/src/API/Anilist/AnilistTrait.php +++ b/src/API/Anilist/AnilistTrait.php @@ -2,31 +2,39 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ -namespace Aviat\AnimeClient\API\MAL; +namespace Aviat\AnimeClient\API\Anilist; + +use const Aviat\AnimeClient\USER_AGENT; use function Amp\Promise\wait; +use Amp\Artax\Request; +use Amp\Artax\Response; + use Aviat\AnimeClient\API\{ Anilist, HummingbirdClient }; +use Aviat\Ion\Json; +use Aviat\Ion\Di\ContainerAware; trait AnilistTrait { + use ContainerAware; /** - * The request builder for the MAL API + * The request builder for the Anilist API * @var AnilistRequestBuilder */ protected $requestBuilder; @@ -46,13 +54,13 @@ trait AnilistTrait { 'Accept' => 'application/json', 'Accept-Encoding' => 'gzip', 'Content-type' => 'application/json', - 'User-Agent' => "Tim's Anime Client/4.0" + 'User-Agent' => USER_AGENT, ]; /** * Set the request builder object * - * @param MALRequestBuilder $requestBuilder + * @param AnilistRequestBuilder $requestBuilder * @return self */ public function setRequestBuilder($requestBuilder): self @@ -63,19 +71,29 @@ trait AnilistTrait { /** * Create a request object - * - * @param string $type + * @param string $url * @param array $options - * @return \Amp\Artax\Response + * @return Request */ - public function setUpRequest(string $type, string $url, array $options = []) + public function setUpRequest(string $url, array $options = []): Request { - $config = $this->container->get('config'); + $config = $this->getContainer()->get('config'); + $anilistConfig = $config->get('anilist'); - $request = $this->requestBuilder - ->newRequest($type, $url) - ->setBasicAuth($config->get(['mal','username']), $config->get(['mal','password'])); + $request = $this->requestBuilder->newRequest('POST', $url); + + // You can only authenticate the request if you + // actually have an access_token saved + if ($config->has(['anilist', 'access_token'])) + { + $request = $request->setAuth('bearer', $anilistConfig['access_token']); + } + + if (array_key_exists('form_params', $options)) + { + $request = $request->setFormFields($options['form_params']); + } if (array_key_exists('query', $options)) { @@ -84,32 +102,128 @@ trait AnilistTrait { if (array_key_exists('body', $options)) { - $request = $request->setBody($options['body']); + $request = $request->setJsonBody($options['body']); + } + + if (array_key_exists('headers', $options)) + { + $request = $request->setHeaders($options['headers']); } return $request->getFullRequest(); } + /** + * Run a GraphQL API query + * + * @param string $name + * @param array $variables + * @return array + */ + public function runQuery(string $name, array $variables = []): array + { + $file = realpath(__DIR__ . "/GraphQL/Queries/{$name}.graphql"); + if ( ! file_exists($file)) + { + throw new \LogicException('GraphQL query file does not exist.'); + } + + // $query = str_replace(["\t", "\n"], ' ', file_get_contents($file)); + $query = file_get_contents($file); + $body = [ + 'query' => $query + ]; + + if ( ! empty($variables)) + { + $body['variables'] = []; + foreach($variables as $key => $val) + { + $body['variables'][$key] = $val; + } + } + + return $this->postRequest([ + 'body' => $body + ]); + } + + public function mutateRequest (string $name, array $variables = []): Request + { + $file = realpath(__DIR__ . "/GraphQL/Mutations/{$name}.graphql"); + if (!file_exists($file)) + { + throw new \LogicException('GraphQL mutation file does not exist.'); + } + + // $query = str_replace(["\t", "\n"], ' ', file_get_contents($file)); + $query = file_get_contents($file); + + $body = [ + 'query' => $query + ]; + + if (!empty($variables)) { + $body['variables'] = []; + foreach ($variables as $key => $val) + { + $body['variables'][$key] = $val; + } + } + + return $this->setUpRequest(Anilist::BASE_URL, [ + 'body' => $body, + ]); + } + + public function mutate (string $name, array $variables = []): array + { + $request = $this->mutateRequest($name, $variables); + $response = $this->getResponseFromRequest($request); + + return Json::decode(wait($response->getBody())); + } + /** * Make a request * - * @param string $type * @param string $url * @param array $options - * @return \Amp\Artax\Response + * @return Response */ - private function getResponse(string $type, string $url, array $options = []) + private function getResponse(string $url, array $options = []): Response { $logger = NULL; if ($this->getContainer()) { - $logger = $this->container->getLogger('mal-request'); + $logger = $this->container->getLogger('anilist-request'); } - $request = $this->setUpRequest($type, $url, $options); + $request = $this->setUpRequest($url, $options); $response = wait((new HummingbirdClient)->request($request)); - $logger->debug('MAL api response', [ + $logger->debug('Anilist response', [ + 'status' => $response->getStatus(), + 'reason' => $response->getReason(), + 'body' => $response->getBody(), + 'headers' => $response->getHeaders(), + 'requestHeaders' => $request->getHeaders(), + ]); + + return $response; + } + + private function getResponseFromRequest(Request $request): Response + { + $logger = NULL; + if ($this->getContainer()) + { + $logger = $this->container->getLogger('anilist-request'); + } + + $response = wait((new HummingbirdClient)->request($request)); + + $logger->debug('Anilist response', [ 'status' => $response->getStatus(), 'reason' => $response->getReason(), 'body' => $response->getBody(), @@ -121,59 +235,39 @@ trait AnilistTrait { } /** - * Make a request + * Remove some boilerplate for post requests * - * @param string $type - * @param string $url * @param array $options * @return array */ - private function request(string $type, string $url, array $options = []): array + protected function postRequest(array $options = []): array { - $logger = NULL; - if ($this->getContainer()) - { - $logger = $this->container->getLogger('anilist-request'); - } - - $response = $this->getResponse($type, $url, $options); - - if ((int) $response->getStatus() > 299 OR (int) $response->getStatus() < 200) - { - if ($logger) - { - $logger->warning('Non 200 response for api call', (array)$response->getBody()); - } - } - - return XML::toArray(wait($response->getBody())); - } - - /** - * Remove some boilerplate for post requests - * - * @param mixed ...$args - * @return array - */ - protected function postRequest(...$args): array - { - $logger = NULL; - if ($this->getContainer()) - { - $logger = $this->container->getLogger('anilist-request'); - } - - $response = $this->getResponse('POST', ...$args); + $response = $this->getResponse(Anilist::BASE_URL, $options); $validResponseCodes = [200, 201]; - if ( ! \in_array((int) $response->getStatus(), $validResponseCodes, TRUE)) + $logger = NULL; + if ($this->getContainer()) + { + $logger = $this->container->getLogger('anilist-request'); + $logger->debug('Anilist response', [ + 'status' => $response->getStatus(), + 'reason' => $response->getReason(), + 'body' => $response->getBody(), + 'headers' => $response->getHeaders(), + //'requestHeaders' => $request->getHeaders(), + ]); + } + + if ( ! \in_array($response->getStatus(), $validResponseCodes, TRUE)) { if ($logger) { - $logger->warning('Non 201 response for POST api call', (array)$response->getBody()); + $logger->warning('Non 200 response for POST api call', (array)$response->getBody()); } } - return XML::toArray($response->getBody()); + // dump(wait($response->getBody())); + + return Json::decode(wait($response->getBody())); } } \ No newline at end of file diff --git a/src/API/Anilist/GraphQL/Mutations/CreateFullMediaListEntry.graphql b/src/API/Anilist/GraphQL/Mutations/CreateFullMediaListEntry.graphql new file mode 100644 index 00000000..623cb085 --- /dev/null +++ b/src/API/Anilist/GraphQL/Mutations/CreateFullMediaListEntry.graphql @@ -0,0 +1,27 @@ +mutation ( + $id: Int, + $notes: String, + $private: Boolean, + $progress: Int, + $repeat: Int, + $status: MediaListStatus, + $score: Int, +) { + SaveMediaListEntry ( + mediaId: $id, + notes: $notes, + private: $private, + progress: $progress, + repeat: $repeat, + scoreRaw: $score, + status: $status + ) { + mediaId + notes + private + progress + repeat + score(format: POINT_10) + status + } +} \ No newline at end of file diff --git a/src/API/Anilist/GraphQL/Mutations/CreateMediaListEntry.graphql b/src/API/Anilist/GraphQL/Mutations/CreateMediaListEntry.graphql new file mode 100644 index 00000000..b2385201 --- /dev/null +++ b/src/API/Anilist/GraphQL/Mutations/CreateMediaListEntry.graphql @@ -0,0 +1,12 @@ +mutation ( + $id: Int, + $status: MediaListStatus, +) { + SaveMediaListEntry ( + mediaId: $id, + status: $status + ) { + mediaId + status + } +} \ No newline at end of file diff --git a/src/API/Anilist/GraphQL/Mutations/DeleteMediaListEntry.graphql b/src/API/Anilist/GraphQL/Mutations/DeleteMediaListEntry.graphql new file mode 100644 index 00000000..6cc570f4 --- /dev/null +++ b/src/API/Anilist/GraphQL/Mutations/DeleteMediaListEntry.graphql @@ -0,0 +1,9 @@ +mutation ( + $id: Int +) { + DeleteMediaListEntry ( + id: $id + ) { + deleted + } +} \ No newline at end of file diff --git a/src/API/Anilist/GraphQL/Mutations/IncrementMediaListEntry.graphql b/src/API/Anilist/GraphQL/Mutations/IncrementMediaListEntry.graphql new file mode 100644 index 00000000..1347050a --- /dev/null +++ b/src/API/Anilist/GraphQL/Mutations/IncrementMediaListEntry.graphql @@ -0,0 +1,12 @@ +mutation ( + $id: Int, + $progress: Int, +) { + SaveMediaListEntry ( + id: $id, + progress: $progress, + ) { + id + progress + } +} diff --git a/src/API/Anilist/GraphQL/Mutations/UpdateMediaListEntry.graphql b/src/API/Anilist/GraphQL/Mutations/UpdateMediaListEntry.graphql new file mode 100644 index 00000000..61fb8221 --- /dev/null +++ b/src/API/Anilist/GraphQL/Mutations/UpdateMediaListEntry.graphql @@ -0,0 +1,27 @@ +mutation ( +$id: Int, +$status: MediaListStatus, +$score: Int, +$progress: Int, +$repeat: Int, +$private: Boolean, +$notes: String +) { + SaveMediaListEntry ( + id: $id, + status: $status, + scoreRaw: $score, + progress: $progress, + repeat: $repeat, + private: $private, + notes: $notes + ) { + id + status + score + progress + repeat + private + notes + } +} diff --git a/src/API/Anilist/GraphQL/Queries/AnimeDetails.graphql b/src/API/Anilist/GraphQL/Queries/AnimeDetails.graphql new file mode 100644 index 00000000..db4ba20b --- /dev/null +++ b/src/API/Anilist/GraphQL/Queries/AnimeDetails.graphql @@ -0,0 +1,159 @@ +query ($id: Int) { + Media(type: ANIME, idMal:$id) { + id + idMal + isAdult + season + title { + romaji + english + native + userPreferred + } + description(asHtml: true) + duration + format + status + chapters + volumes + genres + synonyms + countryOfOrigin + source + startDate { + year + month + day + } + endDate { + year + month + day + } + trailer { + id + site + } + coverImage { + large + medium + } + bannerImage + tags { + id + name + description + category + isGeneralSpoiler + isMediaSpoiler + isAdult + } + characters { + edges { + role + voiceActors { + id + name { + first + last + native + } + language + image { + large + medium + } + description(asHtml: true) + siteUrl + } + node { + id + name { + first + last + native + } + image { + large + medium + } + description + siteUrl + } + } + pageInfo { + total + perPage + currentPage + lastPage + hasNextPage + } + } + staff { + edges { + role + node { + id + name { + first + last + native + } + language + image { + large + medium + } + description(asHtml: true) + siteUrl + } + } + pageInfo { + total + perPage + currentPage + lastPage + hasNextPage + } + } + studios { + edges { + isMain + node { + name + siteUrl + } + } + pageInfo { + total + perPage + currentPage + lastPage + hasNextPage + } + } + externalLinks { + id + url + site + } + mediaListEntry { + id + userId + status + score + progress + progressVolumes + repeat + private + notes + } + streamingEpisodes { + title + thumbnail + url + site + } + siteUrl + } +} \ No newline at end of file diff --git a/src/API/Anilist/GraphQL/Queries/CheckLogin.graphql b/src/API/Anilist/GraphQL/Queries/CheckLogin.graphql new file mode 100644 index 00000000..f4838534 --- /dev/null +++ b/src/API/Anilist/GraphQL/Queries/CheckLogin.graphql @@ -0,0 +1,6 @@ +query { + Viewer { + id + name + } +} \ No newline at end of file diff --git a/src/API/Anilist/GraphQL/Queries/ListItemIdByMalId.graphql b/src/API/Anilist/GraphQL/Queries/ListItemIdByMalId.graphql new file mode 100644 index 00000000..704f006d --- /dev/null +++ b/src/API/Anilist/GraphQL/Queries/ListItemIdByMalId.graphql @@ -0,0 +1,9 @@ +query ($id: Int, $type: MediaType) { + Media (idMal: $id, type: $type) { + mediaListEntry { + id + userId + mediaId + } + } +} \ No newline at end of file diff --git a/src/API/Anilist/GraphQL/Queries/ListItemIdByMediaId.graphql b/src/API/Anilist/GraphQL/Queries/ListItemIdByMediaId.graphql new file mode 100644 index 00000000..dec155da --- /dev/null +++ b/src/API/Anilist/GraphQL/Queries/ListItemIdByMediaId.graphql @@ -0,0 +1,7 @@ +query ($id: Int, $userName: String) { + MediaList (mediaId: $id, userName: $userName) { + id + userId + mediaId + } +} \ No newline at end of file diff --git a/src/API/Anilist/GraphQL/Queries/MangaDetails.graphql b/src/API/Anilist/GraphQL/Queries/MangaDetails.graphql new file mode 100644 index 00000000..c032070e --- /dev/null +++ b/src/API/Anilist/GraphQL/Queries/MangaDetails.graphql @@ -0,0 +1,101 @@ +query ($id: Int){ + Media(type: MANGA, id: $id) { + id + idMal + isAdult + season + title { + romaji + english + native + userPreferred + } + description(asHtml:true) + duration + format + status + chapters + volumes + genres + synonyms + countryOfOrigin + source + startDate { + year + month + day + } + endDate { + year + month + day + } + trailer { + id + site + } + coverImage { + large + medium + } + bannerImage + tags { + id + name + description + category + isGeneralSpoiler + isMediaSpoiler + isAdult + } + characters { + edges { + id + } + nodes { + id + name { + first + last + native + } + image { + large + medium + } + description + siteUrl + } + pageInfo { + total + perPage + currentPage + lastPage + hasNextPage + } + } + externalLinks { + id + url + site + } + mediaListEntry { + id + userId + status + score + progress + progressVolumes + repeat + private + notes + } + streamingEpisodes { + title + thumbnail + url + site + } + siteUrl + } +} \ No newline at end of file diff --git a/src/API/Anilist/GraphQL/Queries/MangaIdByMalId.graphql b/src/API/Anilist/GraphQL/Queries/MangaIdByMalId.graphql new file mode 100644 index 00000000..86899951 --- /dev/null +++ b/src/API/Anilist/GraphQL/Queries/MangaIdByMalId.graphql @@ -0,0 +1,5 @@ +query ($id: Int) { + Media (type: ANIME, malId: $id) { + id + } +} \ No newline at end of file diff --git a/src/API/Anilist/GraphQL/Queries/MediaIdByMalId.graphql b/src/API/Anilist/GraphQL/Queries/MediaIdByMalId.graphql new file mode 100644 index 00000000..24282245 --- /dev/null +++ b/src/API/Anilist/GraphQL/Queries/MediaIdByMalId.graphql @@ -0,0 +1,5 @@ +query ($id: Int, $type: MediaType) { + Media (type: $type, idMal: $id) { + id + } +} \ No newline at end of file diff --git a/src/API/Anilist/GraphQL/Queries/MediaListItem.graphql b/src/API/Anilist/GraphQL/Queries/MediaListItem.graphql new file mode 100644 index 00000000..f2fd5c05 --- /dev/null +++ b/src/API/Anilist/GraphQL/Queries/MediaListItem.graphql @@ -0,0 +1,14 @@ +query ($id: Int) { + MediaList (id: $id) { + id + userId + mediaId + status + score(format: POINT_10) + progress + progressVolumes + repeat + private + notes + } +} \ No newline at end of file diff --git a/src/API/Anilist/GraphQL/Queries/SyncUserList.graphql b/src/API/Anilist/GraphQL/Queries/SyncUserList.graphql new file mode 100644 index 00000000..a5032529 --- /dev/null +++ b/src/API/Anilist/GraphQL/Queries/SyncUserList.graphql @@ -0,0 +1,28 @@ +query ($name: String, $type: MediaType) { + MediaListCollection(userName: $name, type: $type) { + lists { + entries { + id + mediaId + score + progress + progressVolumes + repeat + private + notes + status + updatedAt + media { + id + idMal + title { + romaji + english + native + userPreferred + } + } + } + } + } +} \ No newline at end of file diff --git a/src/API/Anilist/GraphQL/Queries/UserAnimeList.graphql b/src/API/Anilist/GraphQL/Queries/UserAnimeList.graphql new file mode 100644 index 00000000..ea25ddca --- /dev/null +++ b/src/API/Anilist/GraphQL/Queries/UserAnimeList.graphql @@ -0,0 +1,56 @@ +query ($name: String) { + MediaListCollection(userName: $name, type: ANIME) { + lists { + entries { + id + mediaId + score + progress + repeat + private + notes + status + media { + id + idMal + title { + romaji + english + native + userPreferred + } + type + format + status + episodes + season + genres + synonyms + countryOfOrigin + source + trailer { + id + } + coverImage { + large + medium + } + bannerImage + tags { + id + } + externalLinks { + id + } + mediaListEntry { + id + } + } + user { + id + } + } + } + } +} + diff --git a/src/API/Anilist/GraphQL/Queries/UserMangaList.graphql b/src/API/Anilist/GraphQL/Queries/UserMangaList.graphql new file mode 100644 index 00000000..df0f039d --- /dev/null +++ b/src/API/Anilist/GraphQL/Queries/UserMangaList.graphql @@ -0,0 +1,56 @@ +query ($name: String) { + MediaListCollection(userName: $name, type: MANGA) { + lists { + entries { + id + mediaId + score + progress + progressVolumes + repeat + private + notes + status + media { + id + idMal + title { + romaji + english + native + userPreferred + } + type + format + status + chapters + volumes + genres + synonyms + countryOfOrigin + source + trailer { + id + } + coverImage { + large + medium + } + bannerImage + tags { + id + } + externalLinks { + id + } + mediaListEntry { + id + } + } + user { + id + } + } + } + } +} diff --git a/src/API/Anilist/ListItem.php b/src/API/Anilist/ListItem.php index 7aa5a4cb..ca8d6283 100644 --- a/src/API/Anilist/ListItem.php +++ b/src/API/Anilist/ListItem.php @@ -2,57 +2,53 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ namespace Aviat\AnimeClient\API\Anilist; -use Amp\Artax\{FormBody, Request}; -use Aviat\AnimeClient\API\{ - XML -}; -use Aviat\AnimeClient\Types\AbstractType; -use Aviat\Ion\Di\ContainerAware; +use Amp\Artax\Request; + +use Aviat\AnimeClient\API\ListItemInterface; +use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\Anilist as AnilistStatus; +use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus; +use Aviat\AnimeClient\Types\FormItemData; /** * CRUD operations for MAL list items */ -final class ListItem { - use ContainerAware; +final class ListItem implements ListItemInterface{ use AnilistTrait; /** - * Create a list item + * Create a minimal list item * * @param array $data - * @param string $type * @return Request */ - public function create(array $data, string $type = 'anime'): Request + public function create(array $data): Request { - $id = $data['id']; - $createData = [ - 'id' => $id, - 'data' => XML::toXML([ - 'entry' => $data['data'] - ]) - ]; + return $this->mutateRequest('CreateMediaListEntry', $data); + } - $config = $this->container->get('config'); - - return $this->requestBuilder->newRequest('POST', "{$type}list/add/{$id}.xml") - ->setFormFields($createData) - ->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password'])) - ->getFullRequest(); + /** + * Create a fleshed-out list item + * + * @param array $data + * @return Request + */ + public function createFull(array $data): Request + { + return $this->mutateRequest('CreateFullMediaListEntry', $data); } /** @@ -64,46 +60,62 @@ final class ListItem { */ public function delete(string $id, string $type = 'anime'): Request { - $config = $this->container->get('config'); - - return $this->requestBuilder->newRequest('DELETE', "{$type}list/delete/{$id}.xml") - ->setFormFields([ - 'id' => $id - ]) - ->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password'])) - ->getFullRequest(); - - // return $response->getBody() === 'Deleted' + return $this->mutateRequest('DeleteMediaListEntry', ['id' => $id]); } + /** + * Get the data for a list item + * + * @param string $id + * @return array + */ public function get(string $id): array { - return []; + return $this->runQuery('MediaListItem', ['id' => $id]); + } + + /** + * Increase the progress on the medium by 1 + * + * @param string $id + * @param FormItemData $data + * @return Request + */ + public function increment(string $id, FormItemData $data): Request + { + return $this->mutateRequest('IncrementMediaListEntry', [ + 'id' => $id, + 'progress' => $data['progress'], + ]); } /** * Update a list item * * @param string $id - * @param AbstractType $data - * @param string $type + * @param FormItemData $data * @return Request */ - public function update(string $id, AbstractType $data, string $type = 'anime'): Request + public function update(string $id, FormItemData $data): Request { - $config = $this->container->get('config'); + $array = $data->toArray(); - $xml = XML::toXML(['entry' => $data]); - $body = new FormBody(); - $body->addField('id', $id); - $body->addField('data', $xml); + $notes = $data['notes'] ?? ''; + $progress = array_key_exists('progress', $array) ? $data['progress'] : 0; + $private = array_key_exists('private', $array) ? (bool)$data['private'] : false; + $rating = array_key_exists('ratingTwenty', $array) ? $data['ratingTwenty'] : NULL; + $status = ($data['reconsuming'] === true) ? AnilistStatus::REPEATING : AnimeWatchingStatus::KITSU_TO_ANILIST[$data['status']]; - return $this->requestBuilder->newRequest('POST', "{$type}list/update/{$id}.xml") - ->setFormFields([ - 'id' => $id, - 'data' => $xml - ]) - ->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password'])) - ->getFullRequest(); + $updateData = [ + 'id' => (int)$id, + 'status' => $status, + 'score' => $rating * 5, + 'progress' => $progress, + 'repeat' => (int)$data['reconsumeCount'], + 'private' => $private, + 'notes' => $notes, + ]; + + return $this->mutateRequest('UpdateMediaListEntry', $updateData); } } \ No newline at end of file diff --git a/src/API/Anilist/Model.php b/src/API/Anilist/Model.php index cb8b0582..820f8611 100644 --- a/src/API/Anilist/Model.php +++ b/src/API/Anilist/Model.php @@ -2,22 +2,282 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ namespace Aviat\AnimeClient\API\Anilist; +use function Amp\Promise\wait; + +use InvalidArgumentException; + +use Amp\Artax\Request; +use Aviat\AnimeClient\API\Anilist; +use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus}; +use Aviat\AnimeClient\Types\FormItem; +use Aviat\Ion\Json; + /** * Anilist API Model */ -final class Model { +final class Model +{ + use AnilistTrait; + /** + * @var ListItem + */ + private $listItem; + + /** + * Constructor + * + * @param ListItem $listItem + */ + public function __construct(ListItem $listItem) + { + $this->listItem = $listItem; + } + + // ------------------------------------------------------------------------- + // ! Generic API calls + // ------------------------------------------------------------------------- + + /** + * Attempt to get an auth token + * + * @param string $code - The request token + * @param string $redirectUri - The oauth callback url + * @return array + */ + public function authenticate(string $code, string $redirectUri): array + { + $config = $this->getContainer()->get('config'); + $request = $this->requestBuilder + ->newRequest('POST', Anilist::TOKEN_URL) + ->setJsonBody([ + 'grant_type' => 'authorization_code', + 'client_id' => $config->get(['anilist', 'client_id']), + 'client_secret' => $config->get(['anilist', 'client_secret']), + 'redirect_uri' => $redirectUri, + 'code' => $code, + ]) + ->getFullRequest(); + + $response = $this->getResponseFromRequest($request); + + return Json::decode(wait($response->getBody())); + } + + /** + * Check auth status with simple API call + * + * @return array + */ + public function checkAuth(): array + { + return $this->runQuery('CheckLogin'); + } + + /** + * Get user list data for syncing with Kitsu + * + * @param string $type + * @return array + * @throws \Aviat\Ion\Di\Exception\ContainerException + * @throws \Aviat\Ion\Di\Exception\NotFoundException + */ + public function getSyncList(string $type = 'anime'): array + { + $config = $this->container->get('config'); + $anilistUser = $config->get(['anilist', 'username']); + + if ( ! is_string($anilistUser)) + { + throw new InvalidArgumentException('Anilist username is not defined in config'); + } + + return $this->runQuery('SyncUserList', [ + 'name' => $anilistUser, + 'type' => $type, + ]); + } + + /** + * Create a list item + * + * @param array $data + * @param string $type + * @return Request + */ + public function createListItem(array $data, string $type = 'anime'): Request + { + $createData = []; + + $mediaId = $this->getMediaIdFromMalId($data['mal_id'], mb_strtoupper($type)); + + if (empty($mediaId)) + { + throw new InvalidArgumentException('Media id missing'); + } + + if ($type === 'ANIME') + { + $createData = [ + 'id' => $mediaId, + 'status' => AnimeWatchingStatus::KITSU_TO_ANILIST[$data['status']], + ]; + } + elseif ($type === 'MANGA') + { + $createData = [ + 'id' => $mediaId, + 'status' => MangaReadingStatus::KITSU_TO_ANILIST[$data['status']], + ]; + } + + return $this->listItem->create($createData, $type); + } + + /** + * Create a list item with all the relevant data + * + * @param array $data + * @param string $type + * @return Request + */ + public function createFullListItem(array $data, string $type = 'anime'): Request + { + $createData = $data['data']; + $mediaId = $this->getMediaIdFromMalId($data['mal_id']); + + $createData['id'] = $mediaId; + + return $this->listItem->createFull($createData); + } + + /** + * Get the data for a specific list item, generally for editing + * + * @param string $malId - The unique identifier of that list item + * @return mixed + */ + public function getListItem(string $malId, string $type): array + { + $id = $this->getListIdFromMalId($malId, $type); + + $data = $this->listItem->get($id)['data']; + + return ($data !== null) + ? $data['MediaList'] + : []; + } + + /** + * Increase the watch count for the current list item + * + * @param FormItem $data + * @return Request + */ + public function incrementListItem(FormItem $data, string $type): Request + { + $id = $this->getListIdFromMalId($data['mal_id'], $type); + + return $this->listItem->increment($id, $data['data']); + } + + /** + * Modify a list item + * + * @param FormItem $data + * @param int [$id] + * @return Request + */ + public function updateListItem(FormItem $data, string $type): Request + { + $id = $this->getListIdFromMalId($data['mal_id'], mb_strtoupper($type)); + + return $this->listItem->update($id, $data['data']); + } + + /** + * Remove a list item + * + * @param string $malId - The id of the list item to remove + * @return Request + */ + public function deleteListItem(string $malId, string $type): Request + { + $item_id = $this->getListIdFromMalId($malId, $type); + + return $this->listItem->delete($item_id); + } + + /** + * Get the id of the specific list entry from the malId + * + * @param string $malId + * @return string + */ + public function getListIdFromMalId(string $malId, string $type): ?string + { + $mediaId = $this->getMediaIdFromMalId($malId, $type); + return $this->getListIdFromMediaId($mediaId); + } + + /** + * Get the Anilist media id from its MAL id + * this way is more accurate than getting the list item id + * directly from the MAL id + */ + private function getListIdFromMediaId(string $mediaId): string + { + $config = $this->container->get('config'); + $anilistUser = $config->get(['anilist', 'username']); + + $info = $this->runQuery('ListItemIdByMediaId', [ + 'id' => $mediaId, + 'userName' => $anilistUser, + ]); + + /* dump([ + 'media_id' => $mediaId, + 'userName' => $anilistUser, + 'response' => $info, + ]); + die(); */ + + return (string)$info['data']['MediaList']['id']; + } + + /** + * Get the Anilist media id from the malId + * + * @param string $malId + * @param string $type + * @return string + */ + private function getMediaIdFromMalId(string $malId, string $type = 'ANIME'): ?string + { + $info = $this->runQuery('MediaIdByMalId', [ + 'id' => $malId, + 'type' => mb_strtoupper($type), + ]); + + /* dump([ + 'mal_id' => $malId, + 'response' => $info, + ]); + die(); */ + + return (string)$info['data']['Media']['id']; + } } \ No newline at end of file diff --git a/src/API/Anilist/Transformer/AnimeListTransformer.php b/src/API/Anilist/Transformer/AnimeListTransformer.php new file mode 100644 index 00000000..500133d8 --- /dev/null +++ b/src/API/Anilist/Transformer/AnimeListTransformer.php @@ -0,0 +1,71 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\API\Anilist\Transformer; + +use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\Anilist as AnilistStatus; +use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus; +use Aviat\AnimeClient\Types\{AnimeListItem, FormItem}; + +use Aviat\Ion\Transformer\AbstractTransformer; + +use DateTime; + +class AnimeListTransformer extends AbstractTransformer { + + public function transform($item): AnimeListItem + { + return new AnimeListItem([]); + } + + /** + * Transform Anilist list item to Kitsu form update format + * + * @param array $item + * @return FormItem + */ + public function untransform(array $item): FormItem + { + return new FormItem([ + 'id' => $item['id'], + 'mal_id' => $item['media']['idMal'], + 'data' => [ + 'notes' => $item['notes'] ?? '', + 'private' => $item['private'], + 'progress' => $item['progress'], + 'rating' => $item['score'], + 'reconsumeCount' => $item['repeat'], + 'reconsuming' => $item['status'] === AnilistStatus::REPEATING, + 'status' => AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']], + 'updatedAt' => (new DateTime()) + ->setTimestamp($item['updatedAt']) + ->format(DateTime::W3C) + ], + ]); + } + + /** + * Transform a set of structures + * + * @param array|object $collection + * @return array + */ + public function untransformCollection($collection): array + { + $list = (array)$collection; + return array_map([$this, 'untransform'], $list); + } +} \ No newline at end of file diff --git a/src/API/Anilist/Transformer/MangaListTransformer.php b/src/API/Anilist/Transformer/MangaListTransformer.php new file mode 100644 index 00000000..030c8c89 --- /dev/null +++ b/src/API/Anilist/Transformer/MangaListTransformer.php @@ -0,0 +1,71 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\API\Anilist\Transformer; + +use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Anilist as AnilistStatus; +use Aviat\AnimeClient\API\Mapping\MangaReadingStatus; +use Aviat\AnimeClient\Types\FormItem; + +use Aviat\Ion\Transformer\AbstractTransformer; + +use DateTime; + +class MangaListTransformer extends AbstractTransformer { + + public function transform($item) + { + + } + + /** + * Transform Anilist list item to Kitsu form update format + * + * @param array $item + * @return FormItem + */ + public function untransform(array $item): FormItem + { + return new FormItem([ + 'id' => $item['id'], + 'mal_id' => $item['media']['idMal'], + 'data' => [ + 'notes' => $item['notes'] ?? '', + 'private' => $item['private'], + 'progress' => $item['progress'], + 'rating' => $item['score'], + 'reconsumeCount' => $item['repeat'], + 'reconsuming' => $item['status'] === AnilistStatus::REPEATING, + 'status' => MangaReadingStatus::ANILIST_TO_KITSU[$item['status']], + 'updatedAt' => (new DateTime()) + ->setTimestamp($item['updatedAt']) + ->format(DateTime::W3C), + ] + ]); + } + + /** + * Transform a set of structures + * + * @param array|object $collection + * @return array + */ + public function untransformCollection($collection): array + { + $list = (array)$collection; + return array_map([$this, 'untransform'], $list); + } +} \ No newline at end of file diff --git a/src/API/CacheTrait.php b/src/API/CacheTrait.php index de3ff71a..92f349f0 100644 --- a/src/API/CacheTrait.php +++ b/src/API/CacheTrait.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/API/Enum/AnimeWatchingStatus/Anilist.php b/src/API/Enum/AnimeWatchingStatus/Anilist.php index 6a8ee915..68a544c8 100644 --- a/src/API/Enum/AnimeWatchingStatus/Anilist.php +++ b/src/API/Enum/AnimeWatchingStatus/Anilist.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/API/Enum/AnimeWatchingStatus/Kitsu.php b/src/API/Enum/AnimeWatchingStatus/Kitsu.php index 8bff6591..c4d77e9d 100644 --- a/src/API/Enum/AnimeWatchingStatus/Kitsu.php +++ b/src/API/Enum/AnimeWatchingStatus/Kitsu.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/API/Enum/AnimeWatchingStatus/MAL.php b/src/API/Enum/AnimeWatchingStatus/MAL.php deleted file mode 100644 index 8d26bb72..00000000 --- a/src/API/Enum/AnimeWatchingStatus/MAL.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @copyright 2015 - 2018 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient\API\Enum\AnimeWatchingStatus; - -use Aviat\Ion\Enum; - -/** - * Possible values for watching status for the current anime - */ -final class MAL extends Enum { - const WATCHING = 1; - const COMPLETED = 2; - const ON_HOLD = 3; - const DROPPED = 4; - const PLAN_TO_WATCH = 6; -} \ No newline at end of file diff --git a/src/API/Enum/AnimeWatchingStatus/Route.php b/src/API/Enum/AnimeWatchingStatus/Route.php index 05998b54..e0f397d7 100644 --- a/src/API/Enum/AnimeWatchingStatus/Route.php +++ b/src/API/Enum/AnimeWatchingStatus/Route.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/API/Enum/AnimeWatchingStatus/Title.php b/src/API/Enum/AnimeWatchingStatus/Title.php index 5a93d437..425e6f70 100644 --- a/src/API/Enum/AnimeWatchingStatus/Title.php +++ b/src/API/Enum/AnimeWatchingStatus/Title.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/API/Enum/MangaReadingStatus/Anilist.php b/src/API/Enum/MangaReadingStatus/Anilist.php index c2466045..a31ddcbb 100644 --- a/src/API/Enum/MangaReadingStatus/Anilist.php +++ b/src/API/Enum/MangaReadingStatus/Anilist.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -22,10 +22,10 @@ use Aviat\Ion\Enum; * Possible values for watching status for the current anime */ final class Anilist extends Enum { - const WATCHING = 'CURRENT'; + const READING = 'CURRENT'; const COMPLETED = 'COMPLETED'; const ON_HOLD = 'PAUSED'; const DROPPED = 'DROPPED'; - const PLAN_TO_WATCH = 'PLANNING'; + const PLAN_TO_READ = 'PLANNING'; const REPEATING = 'REPEATING'; } \ No newline at end of file diff --git a/src/API/Enum/MangaReadingStatus/Kitsu.php b/src/API/Enum/MangaReadingStatus/Kitsu.php index f7c157dc..f4344322 100644 --- a/src/API/Enum/MangaReadingStatus/Kitsu.php +++ b/src/API/Enum/MangaReadingStatus/Kitsu.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/API/Enum/MangaReadingStatus/MAL.php b/src/API/Enum/MangaReadingStatus/MAL.php deleted file mode 100644 index 469de135..00000000 --- a/src/API/Enum/MangaReadingStatus/MAL.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @copyright 2015 - 2018 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient\API\Enum\MangaReadingStatus; - -use Aviat\Ion\Enum; - -/** - * Possible values for watching status for the current anime - */ -final class MAL extends Enum { - const READING = 'reading'; - const COMPLETED = 'completed'; - const ON_HOLD = 'onhold'; - const DROPPED = 'dropped'; - const PLAN_TO_READ = 'plantoread'; -} \ No newline at end of file diff --git a/src/API/Enum/MangaReadingStatus/Route.php b/src/API/Enum/MangaReadingStatus/Route.php index ecd54366..67ccfbb5 100644 --- a/src/API/Enum/MangaReadingStatus/Route.php +++ b/src/API/Enum/MangaReadingStatus/Route.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/API/Enum/MangaReadingStatus/Title.php b/src/API/Enum/MangaReadingStatus/Title.php index 8188658f..26413667 100644 --- a/src/API/Enum/MangaReadingStatus/Title.php +++ b/src/API/Enum/MangaReadingStatus/Title.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/API/FailedResponseException.php b/src/API/FailedResponseException.php index b64ed202..1813d683 100644 --- a/src/API/FailedResponseException.php +++ b/src/API/FailedResponseException.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/API/HummingbirdClient.php b/src/API/HummingbirdClient.php index 6f72e536..44ddc5c1 100644 --- a/src/API/HummingbirdClient.php +++ b/src/API/HummingbirdClient.php @@ -1,16 +1,17 @@ - * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -30,7 +31,20 @@ use Amp\{ TimeoutCancellationToken }; use Amp\Artax\{ - ConnectionInfo, Client, DnsException, HttpException, HttpSocketPool, MetaInfo, ParseException, RequestBody, Response, Request, SocketException, TimeoutException, TlsInfo, TooManyRedirectsException + ConnectionInfo, + Client, + DnsException, + HttpException, + HttpSocketPool, + MetaInfo, + ParseException, + RequestBody, + Response, + Request, + SocketException, + TimeoutException, + TlsInfo, + TooManyRedirectsException }; use Amp\Artax\Cookie\{ Cookie, @@ -39,7 +53,10 @@ use Amp\Artax\Cookie\{ NullCookieJar }; use Amp\Artax\Internal\{ - CombinedCancellationToken, Parser, PublicSuffixList, RequestCycle + CombinedCancellationToken, + Parser, + PublicSuffixList, + RequestCycle }; use Amp\ByteStream\{ InputStream, IteratorStream, Message, ZlibInputStream @@ -51,39 +68,36 @@ use Amp\Socket\{ use Amp\Uri\{ InvalidUriException, Uri }; +use const Aviat\AnimeClient\USER_AGENT; use function Amp\{ asyncCall, call }; /** - * Standard client implementation. - * - * Use the `Client` interface for your type declarations so people can use composition to add layers like caching. - * - * @see Client + * Re-implementation of Artax's default client */ -final class HummingbirdClient implements Client { - const DEFAULT_USER_AGENT = 'Hummingbird Anime Client/5.0'; +class HummingbirdClient implements Client { + const DEFAULT_USER_AGENT = USER_AGENT; private $cookieJar; private $socketPool; private $tlsContext; private $hasZlib; private $options = [ - self::OP_AUTO_ENCODING => true, - self::OP_TRANSFER_TIMEOUT => 60000, + self::OP_AUTO_ENCODING => TRUE, + self::OP_TRANSFER_TIMEOUT => 100000, self::OP_MAX_REDIRECTS => 5, - self::OP_AUTO_REFERER => true, - self::OP_DISCARD_BODY => false, + self::OP_AUTO_REFERER => TRUE, + self::OP_DISCARD_BODY => FALSE, self::OP_DEFAULT_HEADERS => [], self::OP_MAX_HEADER_BYTES => Parser::DEFAULT_MAX_HEADER_BYTES, self::OP_MAX_BODY_BYTES => Parser::DEFAULT_MAX_BODY_BYTES, ]; public function __construct( - CookieJar $cookieJar = null, - HttpSocketPool $socketPool = null, - ClientTlsContext $tlsContext = null + CookieJar $cookieJar = NULL, + HttpSocketPool $socketPool = NULL, + ClientTlsContext $tlsContext = NULL ) { $this->cookieJar = $cookieJar ?? new NullCookieJar; @@ -93,12 +107,13 @@ final class HummingbirdClient implements Client { } /** @inheritdoc */ - public function request($uriOrRequest, array $options = [], CancellationToken $cancellation = null): Promise + public function request($uriOrRequest, array $options = [], CancellationToken $cancellation = NULL): Promise { return call(function () use ($uriOrRequest, $options, $cancellation) { $cancellation = $cancellation ?? new NullCancellationToken; - foreach ($options as $option => $value) { + foreach ($options as $option => $value) + { $this->validateOption($option, $value); } @@ -106,27 +121,32 @@ final class HummingbirdClient implements Client { list($request, $uri) = $this->generateRequestFromUri($uriOrRequest); $options = $options ? array_merge($this->options, $options) : $this->options; - foreach ($this->options[self::OP_DEFAULT_HEADERS] as $name => $header) { - if (!$request->hasHeader($name)) { + foreach ($this->options[self::OP_DEFAULT_HEADERS] as $name => $header) + { + if ( ! $request->hasHeader($name)) + { $request = $request->withHeaders([$name => $header]); } } /** @var array $headers */ $headers = yield $request->getBody()->getHeaders(); - foreach ($headers as $name => $header) { - if (!$request->hasHeader($name)) { + foreach ($headers as $name => $header) + { + if ( ! $request->hasHeader($name)) + { $request = $request->withHeaders([$name => $header]); } } $originalUri = $uri; - $previousResponse = null; + $previousResponse = NULL; $maxRedirects = $options[self::OP_MAX_REDIRECTS]; $requestNr = 1; - do { + do + { /** @var Request $request */ $request = yield from $this->normalizeRequestBodyHeaders($request); $request = $this->normalizeRequestHeaders($request, $uri, $options); @@ -138,10 +158,11 @@ final class HummingbirdClient implements Client { $response = yield $this->doRequest($request, $uri, $options, $previousResponse, $cancellation); // Explicit $maxRedirects !== 0 check to not consume redirect bodies if redirect following is disabled - if ($maxRedirects !== 0 && $redirectUri = $this->getRedirectUri($response)) { + if ($maxRedirects !== 0 && $redirectUri = $this->getRedirectUri($response)) + { // Discard response body of redirect responses $body = $response->getBody(); - while (null !== yield $body->read()) ; + while (NULL !== yield $body->read()) ; /** * If this is a 302/303 we need to follow the location with a GET if the original request wasn't @@ -153,19 +174,22 @@ final class HummingbirdClient implements Client { */ $method = $request->getMethod(); $status = $response->getStatus(); - $isSameHost = $redirectUri->getAuthority(false) === $originalUri->getAuthority(false); + $isSameHost = $redirectUri->getAuthority(FALSE) === $originalUri->getAuthority(FALSE); - if ($isSameHost) { + if ($isSameHost) + { $request = $request->withUri($redirectUri); - if ($status >= 300 && $status <= 303 && $method !== 'GET') { + if ($status >= 300 && $status <= 303 && $method !== 'GET') + { $request = $request->withMethod('GET'); $request = $request->withoutHeader('Transfer-Encoding'); $request = $request->withoutHeader('Content-Length'); $request = $request->withoutHeader('Content-Type'); - $request = $request->withBody(null); + $request = $request->withBody(NULL); } - } else { + } else + { // We ALWAYS follow with a GET and without any set headers or body for redirects to other hosts. $optionsWithoutHeaders = $options; unset($optionsWithoutHeaders[self::OP_DEFAULT_HEADERS]); @@ -174,19 +198,22 @@ final class HummingbirdClient implements Client { $request = $this->normalizeRequestHeaders($request, $redirectUri, $optionsWithoutHeaders); } - if ($options[self::OP_AUTO_REFERER]) { + if ($options[self::OP_AUTO_REFERER]) + { $request = $this->assignRedirectRefererHeader($request, $originalUri, $redirectUri); } $previousResponse = $response; $originalUri = $redirectUri; $uri = $redirectUri; - } else { + } else + { break; } } while (++$requestNr <= $maxRedirects + 1); - if ($maxRedirects !== 0 && $redirectUri = $this->getRedirectUri($response)) { + if ($maxRedirects !== 0 && $redirectUri = $this->getRedirectUri($response)) + { throw new TooManyRedirectsException($response); } @@ -196,37 +223,43 @@ final class HummingbirdClient implements Client { private function validateOption(string $option, $value) { - switch ($option) { + switch ($option) + { case self::OP_AUTO_ENCODING: - if (!\is_bool($value)) { + if ( ! \is_bool($value)) + { throw new \TypeError("Invalid value for OP_AUTO_ENCODING, bool expected"); } break; case self::OP_TRANSFER_TIMEOUT: - if (!\is_int($value) || $value < 0) { + if ( ! \is_int($value) || $value < 0) + { throw new \Error("Invalid value for OP_TRANSFER_TIMEOUT, int >= 0 expected"); } break; case self::OP_MAX_REDIRECTS: - if (!\is_int($value) || $value < 0) { + if ( ! \is_int($value) || $value < 0) + { throw new \Error("Invalid value for OP_MAX_REDIRECTS, int >= 0 expected"); } break; case self::OP_AUTO_REFERER: - if (!\is_bool($value)) { + if ( ! \is_bool($value)) + { throw new \TypeError("Invalid value for OP_AUTO_REFERER, bool expected"); } break; case self::OP_DISCARD_BODY: - if (!\is_bool($value)) { + if ( ! \is_bool($value)) + { throw new \TypeError("Invalid value for OP_DISCARD_BODY, bool expected"); } @@ -239,14 +272,16 @@ final class HummingbirdClient implements Client { break; case self::OP_MAX_HEADER_BYTES: - if (!\is_int($value) || $value < 0) { + if ( ! \is_int($value) || $value < 0) + { throw new \Error("Invalid value for OP_MAX_HEADER_BYTES, int >= 0 expected"); } break; case self::OP_MAX_BODY_BYTES: - if (!\is_int($value) || $value < 0) { + if ( ! \is_int($value) || $value < 0) + { throw new \Error("Invalid value for OP_MAX_BODY_BYTES, int >= 0 expected"); } @@ -261,13 +296,16 @@ final class HummingbirdClient implements Client { private function generateRequestFromUri($uriOrRequest) { - if (is_string($uriOrRequest)) { + if (is_string($uriOrRequest)) + { $uri = $this->buildUriFromString($uriOrRequest); $request = new Request($uri); - } elseif ($uriOrRequest instanceof Request) { + } elseif ($uriOrRequest instanceof Request) + { $uri = $this->buildUriFromString($uriOrRequest->getUri()); $request = $uriOrRequest; - } else { + } else + { throw new HttpException( 'Request must be a valid HTTP URI or Amp\Artax\Request instance' ); @@ -278,27 +316,32 @@ final class HummingbirdClient implements Client { private function buildUriFromString($str): Uri { - try { + try + { $uri = new Uri($str); $scheme = $uri->getScheme(); - if (($scheme === "http" || $scheme === "https") && $uri->getHost()) { + if (($scheme === "http" || $scheme === "https") && $uri->getHost()) + { return $uri; } throw new HttpException("Request must specify a valid HTTP URI"); - } catch (InvalidUriException $e) { + } catch (InvalidUriException $e) + { throw new HttpException("Request must specify a valid HTTP URI", 0, $e); } } private function normalizeRequestBodyHeaders(Request $request): \Generator { - if ($request->hasHeader("Transfer-Encoding")) { + if ($request->hasHeader("Transfer-Encoding")) + { return $request->withoutHeader("Content-Length"); } - if ($request->hasHeader("Content-Length")) { + if ($request->hasHeader("Content-Length")) + { return $request; } @@ -306,14 +349,18 @@ final class HummingbirdClient implements Client { $body = $request->getBody(); $bodyLength = yield $body->getBodyLength(); - if ($bodyLength === 0) { + if ($bodyLength === 0) + { $request = $request->withHeader('Content-Length', '0'); $request = $request->withoutHeader('Transfer-Encoding'); - } else { - if ($bodyLength > 0) { - $request = $request->withHeader("Content-Length", $bodyLength); + } else + { + if ($bodyLength > 0) + { + $request = $request->withHeader("Content-Length", (string)$bodyLength); $request = $request->withoutHeader("Transfer-Encoding"); - } else { + } else + { $request = $request->withHeader("Transfer-Encoding", "chunked"); } } @@ -336,11 +383,13 @@ final class HummingbirdClient implements Client { { $autoEncoding = $options[self::OP_AUTO_ENCODING]; - if (!$autoEncoding) { + if ( ! $autoEncoding) + { return $request; } - if ($this->hasZlib) { + if ($this->hasZlib) + { return $request->withHeader('Accept-Encoding', 'gzip, deflate, identity'); } @@ -349,7 +398,8 @@ final class HummingbirdClient implements Client { private function normalizeRequestHostHeader(Request $request, Uri $uri): Request { - if ($request->hasHeader('Host')) { + if ($request->hasHeader('Host')) + { return $request; } @@ -372,9 +422,11 @@ final class HummingbirdClient implements Client { // Though servers are supposed to be able to handle standard port names on the end of the // Host header some fail to do this correctly. As a result, we strip the port from the end // if it's a standard 80 or 443 - if (strpos($host, ':80') === strlen($host) - 3) { + if (strpos($host, ':80') === strlen($host) - 3) + { return substr($host, 0, -3); - } elseif (strpos($host, ':443') === strlen($host) - 4) { + } elseif (strpos($host, ':443') === strlen($host) - 4) + { return substr($host, 0, -4); } @@ -383,7 +435,8 @@ final class HummingbirdClient implements Client { private function normalizeRequestUserAgent(Request $request): Request { - if ($request->hasHeader('User-Agent')) { + if ($request->hasHeader('User-Agent')) + { return $request; } @@ -392,7 +445,8 @@ final class HummingbirdClient implements Client { private function normalizeRequestAcceptHeader(Request $request): Request { - if ($request->hasHeader('Accept')) { + if ($request->hasHeader('Accept')) + { return $request; } @@ -406,7 +460,8 @@ final class HummingbirdClient implements Client { $domain = $uri->getHost(); $path = $uri->getPath(); - if (!$applicableCookies = $this->cookieJar->get($domain, $path)) { + if ( ! $applicableCookies = $this->cookieJar->get($domain, $path)) + { // No cookies matched our request; we're finished. return $request->withoutHeader("Cookie"); } @@ -415,13 +470,16 @@ final class HummingbirdClient implements Client { $cookiePairs = []; /** @var Cookie $cookie */ - foreach ($applicableCookies as $cookie) { - if (!$cookie->isSecure() || $isRequestSecure) { + foreach ($applicableCookies as $cookie) + { + if ( ! $cookie->isSecure() || $isRequestSecure) + { $cookiePairs[] = $cookie->getName() . "=" . $cookie->getValue(); } } - if ($cookiePairs) { + if ($cookiePairs) + { return $request->withHeader("Cookie", \implode("; ", $cookiePairs)); } @@ -432,13 +490,14 @@ final class HummingbirdClient implements Client { { $method = $request->getMethod(); - if ($method !== 'TRACE') { + if ($method !== 'TRACE') + { return $request; } // https://tools.ietf.org/html/rfc7231#section-4.3.8 /** @var Request $request */ - $request = $request->withBody(null); + $request = $request->withBody(NULL); // Remove all body and sensitive headers $request = $request->withHeaders([ @@ -452,7 +511,7 @@ final class HummingbirdClient implements Client { return $request; } - private function doRequest(Request $request, Uri $uri, array $options, Response $previousResponse = null, CancellationToken $cancellation): Promise + private function doRequest(Request $request, Uri $uri, array $options, Response $previousResponse = NULL, CancellationToken $cancellation): Promise { $deferred = new Deferred; @@ -468,20 +527,25 @@ final class HummingbirdClient implements Client { $protocolVersions = $request->getProtocolVersions(); - if (\in_array("1.1", $protocolVersions, true)) { + if (\in_array("1.1", $protocolVersions, TRUE)) + { $requestCycle->protocolVersion = "1.1"; - } elseif (\in_array("1.0", $protocolVersions, true)) { + } elseif (\in_array("1.0", $protocolVersions, TRUE)) + { $requestCycle->protocolVersion = "1.0"; - } else { + } else + { return new Failure(new HttpException( "None of the requested protocol versions are supported: " . \implode(", ", $protocolVersions) )); } asyncCall(function () use ($requestCycle) { - try { + try + { yield from $this->doWrite($requestCycle); - } catch (\Throwable $e) { + } catch (\Throwable $e) + { $this->fail($requestCycle, $e); } }); @@ -494,7 +558,8 @@ final class HummingbirdClient implements Client { $timeout = $requestCycle->options[self::OP_TRANSFER_TIMEOUT]; $timeoutToken = new NullCancellationToken; - if ($timeout > 0) { + if ($timeout > 0) + { $transferTimeoutWatcher = Loop::delay($timeout, function () use ($requestCycle, $timeout) { $this->fail($requestCycle, new TimeoutException( sprintf('Allowed transfer timeout exceeded: %d ms', $timeout) @@ -512,15 +577,19 @@ final class HummingbirdClient implements Client { $socketCheckoutUri = $requestCycle->uri->getScheme() . "://{$authority}"; $connectTimeoutToken = new CombinedCancellationToken($requestCycle->cancellation, $timeoutToken); - try { + try + { /** @var ClientSocket $socket */ $socket = yield $this->socketPool->checkout($socketCheckoutUri, $connectTimeoutToken); $requestCycle->socket = $socket; - } catch (ResolutionException $dnsException) { + } catch (ResolutionException $dnsException) + { throw new DnsException(\sprintf("Resolving the specified domain failed: '%s'", $requestCycle->uri->getHost()), 0, $dnsException); - } catch (ConnectException $e) { + } catch (ConnectException $e) + { throw new SocketException(\sprintf("Connection to '%s' failed", $authority), 0, $e); - } catch (CancelledException $e) { + } catch (CancelledException $e) + { // In case of a user cancellation request, throw the expected exception $requestCycle->cancellation->throwIfRequested(); @@ -532,8 +601,10 @@ final class HummingbirdClient implements Client { $this->fail($requestCycle, $error); }); - try { - if ($requestCycle->uri->getScheme() === 'https') { + try + { + if ($requestCycle->uri->getScheme() === 'https') + { $tlsContext = $this->tlsContext ->withPeerName($requestCycle->uri->getHost()) ->withPeerCapturing(); @@ -551,21 +622,25 @@ final class HummingbirdClient implements Client { $chunking = $requestCycle->request->getHeader("transfer-encoding") === "chunked"; $remainingBytes = $requestCycle->request->getHeader("content-length"); - if ($chunking && $requestCycle->protocolVersion === "1.0") { + if ($chunking && $requestCycle->protocolVersion === "1.0") + { throw new HttpException("Can't send chunked bodies over HTTP/1.0"); } // We always buffer the last chunk to make sure we don't write $contentLength bytes if the body is too long. $buffer = ""; - while (null !== $chunk = yield $body->read()) { + while (NULL !== $chunk = yield $body->read()) + { $requestCycle->cancellation->throwIfRequested(); - if ($chunk === "") { + if ($chunk === "") + { continue; } - if ($chunking) { + if ($chunking) + { $chunk = \dechex(\strlen($chunk)) . "\r\n" . $chunk . "\r\n"; }/* elseif ($remainingBytes !== null) { $remainingBytes -= \strlen($chunk); @@ -582,14 +657,16 @@ final class HummingbirdClient implements Client { // Flush last buffered chunk. yield $socket->write($buffer); - if ($chunking) { + if ($chunking) + { yield $socket->write("0\r\n\r\n"); }/* elseif ($remainingBytes !== null && $remainingBytes > 0) { throw new HttpException("Body contained fewer bytes than specified in Content-Length, aborting request"); }*/ yield from $this->doRead($requestCycle, $socket, $connectionInfo); - } finally { + } finally + { $requestCycle->cancellation->unsubscribe($cancellation); } } @@ -597,43 +674,48 @@ final class HummingbirdClient implements Client { private function fail(RequestCycle $requestCycle, \Throwable $error) { $toFails = []; - $socket = null; + $socket = NULL; - if ($requestCycle->deferred) { + if ($requestCycle->deferred) + { $toFails[] = $requestCycle->deferred; - $requestCycle->deferred = null; + $requestCycle->deferred = NULL; } - if ($requestCycle->body) { + if ($requestCycle->body) + { $toFails[] = $requestCycle->body; - $requestCycle->body = null; + $requestCycle->body = NULL; } - if ($requestCycle->bodyDeferred) { + if ($requestCycle->bodyDeferred) + { $toFails[] = $requestCycle->bodyDeferred; - $requestCycle->bodyDeferred = null; + $requestCycle->bodyDeferred = NULL; } - if ($requestCycle->socket) { + if ($requestCycle->socket) + { $this->socketPool->clear($requestCycle->socket); $socket = $requestCycle->socket; - $requestCycle->socket = null; + $requestCycle->socket = NULL; $socket->close(); } - foreach ($toFails as $toFail) { + foreach ($toFails as $toFail) + { $toFail->fail($error); } } private function collectConnectionInfo(ClientSocket $socket): ConnectionInfo { - $crypto = \stream_get_meta_data($socket->getResource())["crypto"] ?? null; + $crypto = \stream_get_meta_data($socket->getResource())["crypto"] ?? NULL; return new ConnectionInfo( $socket->getLocalAddress(), $socket->getRemoteAddress(), - $crypto ? TlsInfo::fromMetaData($crypto, \stream_context_get_options($socket->getResource())["ssl"]) : null + $crypto ? TlsInfo::fromMetaData($crypto, \stream_context_get_options($socket->getResource())["ssl"]) : NULL ); } @@ -654,33 +736,26 @@ final class HummingbirdClient implements Client { $requestUri = $uri->getPath() ?: '/'; - if ($query = $uri->getQuery()) { + if ($query = $uri->getQuery()) + { $requestUri .= '?' . $query; } $head = $request->getMethod() . ' ' . $requestUri . ' HTTP/' . $protocolVersion . "\r\n"; - $headers = $request->getHeaders(true); - /*$newHeaders = []; + $headers = $request->getHeaders(TRUE); - foreach($headers as $key => $val) + foreach ($headers as $field => $values) { - if ($key !== 'Content-Length') + if (\strcspn($field, "\r\n") !== \strlen($field)) { - $newHeaders[$key] = $val; - } - }*/ - - // Curse you Kitsu, for this stupid work-around because the login API endpoint doesn't allow for a Content-Length header! - //unset($headers['Content-Length']); - - foreach ($headers as $field => $values) { - if (\strcspn($field, "\r\n") !== \strlen($field)) { throw new HttpException("Blocked header injection attempt for header '{$field}'"); } - foreach ($values as $value) { - if (\strcspn($value, "\r\n") !== \strlen($value)) { + foreach ($values as $value) + { + if (\strcspn($value, "\r\n") !== \strlen($value)) + { throw new HttpException("Blocked header injection attempt for header '{$field}' with value '{$value}'"); } @@ -695,10 +770,11 @@ final class HummingbirdClient implements Client { private function doRead(RequestCycle $requestCycle, ClientSocket $socket, ConnectionInfo $connectionInfo): \Generator { - try { + try + { $backpressure = new Success; $bodyCallback = $requestCycle->options[self::OP_DISCARD_BODY] - ? null + ? NULL : static function ($data) use ($requestCycle, &$backpressure) { $backpressure = $requestCycle->body->emit($data); }; @@ -711,12 +787,14 @@ final class HummingbirdClient implements Client { Parser::OP_MAX_BODY_BYTES => $requestCycle->options[self::OP_MAX_BODY_BYTES], ]); - while (null !== $chunk = yield $socket->read()) { + while (NULL !== $chunk = yield $socket->read()) + { $requestCycle->cancellation->throwIfRequested(); $parseResult = $parser->parse($chunk); - if (!$parseResult) { + if ( ! $parseResult) + { continue; } @@ -724,54 +802,65 @@ final class HummingbirdClient implements Client { $response = $this->finalizeResponse($requestCycle, $parseResult, $connectionInfo); $shouldCloseSocketAfterResponse = $this->shouldCloseSocketAfterResponse($response); - $ignoreIncompleteBodyCheck = false; + $ignoreIncompleteBodyCheck = FALSE; $responseHeaders = $response->getHeaders(); - if ($requestCycle->deferred) { + if ($requestCycle->deferred) + { $deferred = $requestCycle->deferred; - $requestCycle->deferred = null; + $requestCycle->deferred = NULL; $deferred->resolve($response); - $response = null; // clear references - $deferred = null; // there's also a reference in the deferred - } else { + $response = NULL; // clear references + $deferred = NULL; // there's also a reference in the deferred + } else + { return; } // Required, otherwise responses without body hang - if ($parseResult["headersOnly"]) { + if ($parseResult["headersOnly"]) + { // Directly parse again in case we already have the full body but aborted parsing // to resolve promise with headers. - $chunk = null; + $chunk = NULL; - do { - try { + do + { + try + { $parseResult = $parser->parse($chunk); - } catch (ParseException $e) { + } catch (ParseException $e) + { $this->fail($requestCycle, $e); throw $e; } - if ($parseResult) { + if ($parseResult) + { break; } - if (!$backpressure instanceof Success) { + if ( ! $backpressure instanceof Success) + { yield $this->withCancellation($backpressure, $requestCycle->cancellation); } - if ($requestCycle->bodyTooLarge) { + if ($requestCycle->bodyTooLarge) + { throw new HttpException("Response body exceeded the specified size limit"); } - } while (null !== $chunk = yield $socket->read()); + } while (NULL !== $chunk = yield $socket->read()); $parserState = $parser->getState(); - if ($parserState !== Parser::AWAITING_HEADERS) { + if ($parserState !== Parser::AWAITING_HEADERS) + { // Ignore check if neither content-length nor chunked encoding are given. $ignoreIncompleteBodyCheck = $parserState === Parser::BODY_IDENTITY_EOF && - !isset($responseHeaders["content-length"]) && + ! isset($responseHeaders["content-length"]) && strcasecmp('identity', $responseHeaders['transfer-encoding'][0] ?? ""); - if (!$ignoreIncompleteBodyCheck) { + if ( ! $ignoreIncompleteBodyCheck) + { throw new SocketException(sprintf( 'Socket disconnected prior to response completion (Parser state: %s)', $parserState @@ -780,35 +869,39 @@ final class HummingbirdClient implements Client { } } - if ($shouldCloseSocketAfterResponse || $ignoreIncompleteBodyCheck) { + if ($shouldCloseSocketAfterResponse || $ignoreIncompleteBodyCheck) + { $this->socketPool->clear($socket); $socket->close(); - } else { + } else + { $this->socketPool->checkin($socket); } - $requestCycle->socket = null; + $requestCycle->socket = NULL; // Complete body AFTER socket checkin, so the socket can be reused for a potential redirect $body = $requestCycle->body; - $requestCycle->body = null; + $requestCycle->body = NULL; $bodyDeferred = $requestCycle->bodyDeferred; - $requestCycle->bodyDeferred = null; + $requestCycle->bodyDeferred = NULL; $body->complete(); $bodyDeferred->resolve(); return; } - } catch (\Throwable $e) { + } catch (\Throwable $e) + { $this->fail($requestCycle, $e); return; } - if ($socket->getResource() !== null) { - $requestCycle->socket = null; + if ($socket->getResource() !== NULL) + { + $requestCycle->socket = NULL; $this->socketPool->clear($socket); $socket->close(); } @@ -816,16 +909,19 @@ final class HummingbirdClient implements Client { // Required, because if the write fails, the read() call immediately resolves. yield new Delayed(0); - if ($requestCycle->deferred === null) { + if ($requestCycle->deferred === NULL) + { return; } $parserState = $parser->getState(); - if ($parserState === Parser::AWAITING_HEADERS && $requestCycle->retryCount < 1) { + if ($parserState === Parser::AWAITING_HEADERS && $requestCycle->retryCount < 1) + { $requestCycle->retryCount++; yield from $this->doWrite($requestCycle); - } else { + } else + { $this->fail($requestCycle, new SocketException(sprintf( 'Socket disconnected prior to response completion (Parser state: %s)', $parserState @@ -837,20 +933,20 @@ final class HummingbirdClient implements Client { { $body = new IteratorStream($requestCycle->body->iterate()); - if ($encoding = $this->determineCompressionEncoding($parserResult["headers"])) { + if ($encoding = $this->determineCompressionEncoding($parserResult["headers"])) + { $body = new ZlibInputStream($body, $encoding); } // Wrap the input stream so we can discard the body in case it's destructed but hasn't been consumed. // This allows reusing the connection for further requests. It's important to have __destruct in InputStream and // not in Message, because an InputStream might be pulled out of Message and used separately. - $body = new class($body, $requestCycle, $this->socketPool) implements InputStream - { + $body = new class($body, $requestCycle, $this->socketPool) implements InputStream { private $body; private $bodySize = 0; private $requestCycle; private $socketPool; - private $successfulEnd = false; + private $successfulEnd = FALSE; public function __construct(InputStream $body, RequestCycle $requestCycle, HttpSocketPool $socketPool) { @@ -863,14 +959,17 @@ final class HummingbirdClient implements Client { { $promise = $this->body->read(); $promise->onResolve(function ($error, $value) { - if ($value !== null) { + if ($value !== NULL) + { $this->bodySize += \strlen($value); $maxBytes = $this->requestCycle->options[Client::OP_MAX_BODY_BYTES]; - if ($maxBytes !== 0 && $this->bodySize >= $maxBytes) { - $this->requestCycle->bodyTooLarge = true; + if ($maxBytes !== 0 && $this->bodySize >= $maxBytes) + { + $this->requestCycle->bodyTooLarge = TRUE; } - } elseif ($error === null) { - $this->successfulEnd = true; + } elseif ($error === NULL) + { + $this->successfulEnd = TRUE; } }); @@ -879,17 +978,17 @@ final class HummingbirdClient implements Client { public function __destruct() { - if (!$this->successfulEnd && $this->requestCycle->socket) { + if ( ! $this->successfulEnd && $this->requestCycle->socket) + { $this->socketPool->clear($this->requestCycle->socket); $socket = $this->requestCycle->socket; - $this->requestCycle->socket = null; + $this->requestCycle->socket = NULL; $socket->close(); } } }; - $response = new class($parserResult["protocol"], $parserResult["status"], $parserResult["reason"], $parserResult["headers"], $body, $requestCycle->request, $requestCycle->previousResponse, new MetaInfo($connectionInfo)) implements Response - { + $response = new class($parserResult["protocol"], $parserResult["status"], $parserResult["reason"], $parserResult["headers"], $body, $requestCycle->request, $requestCycle->previousResponse, new MetaInfo($connectionInfo)) implements Response { private $protocolVersion; private $status; private $reason; @@ -906,7 +1005,7 @@ final class HummingbirdClient implements Client { array $headers, InputStream $body, Request $request, - Response $previousResponse = null, + Response $previousResponse = NULL, MetaInfo $metaInfo ) { @@ -942,7 +1041,8 @@ final class HummingbirdClient implements Client { public function getOriginalRequest(): Request { - if (empty($this->previousResponse)) { + if (empty($this->previousResponse)) + { return $this->request; } @@ -961,7 +1061,7 @@ final class HummingbirdClient implements Client { public function getHeader(string $field) { - return $this->headers[\strtolower($field)][0] ?? null; + return $this->headers[\strtolower($field)][0] ?? NULL; } public function getHeaderArray(string $field): array @@ -985,11 +1085,13 @@ final class HummingbirdClient implements Client { } }; - if ($response->hasHeader('Set-Cookie')) { + if ($response->hasHeader('Set-Cookie')) + { $requestDomain = $requestCycle->uri->getHost(); $cookies = $response->getHeaderArray('Set-Cookie'); - foreach ($cookies as $rawCookieStr) { + foreach ($cookies as $rawCookieStr) + { $this->storeResponseCookie($requestDomain, $rawCookieStr); } } @@ -999,21 +1101,25 @@ final class HummingbirdClient implements Client { private function determineCompressionEncoding(array $responseHeaders): int { - if (!$this->hasZlib) { + if ( ! $this->hasZlib) + { return 0; } - if (!isset($responseHeaders["content-encoding"])) { + if ( ! isset($responseHeaders["content-encoding"])) + { return 0; } $contentEncodingHeader = \trim(\current($responseHeaders["content-encoding"])); - if (strcasecmp($contentEncodingHeader, 'gzip') === 0) { + if (strcasecmp($contentEncodingHeader, 'gzip') === 0) + { return \ZLIB_ENCODING_GZIP; } - if (strcasecmp($contentEncodingHeader, 'deflate') === 0) { + if (strcasecmp($contentEncodingHeader, 'deflate') === 0) + { return \ZLIB_ENCODING_DEFLATE; } @@ -1022,26 +1128,32 @@ final class HummingbirdClient implements Client { private function storeResponseCookie(string $requestDomain, string $rawCookieStr) { - try { + try + { $cookie = Cookie::fromString($rawCookieStr); - if (!$cookie->getDomain()) { + if ( ! $cookie->getDomain()) + { $cookie = $cookie->withDomain($requestDomain); - } else { + } else + { // https://tools.ietf.org/html/rfc6265#section-4.1.2.3 $cookieDomain = $cookie->getDomain(); // If a domain is set, left dots are ignored and it's always a wildcard $cookieDomain = \ltrim($cookieDomain, "."); - if ($cookieDomain !== $requestDomain) { + if ($cookieDomain !== $requestDomain) + { // ignore cookies on domains that are public suffixes - if (PublicSuffixList::isPublicSuffix($cookieDomain)) { + if (PublicSuffixList::isPublicSuffix($cookieDomain)) + { return; } // cookie origin would not be included when sending the cookie - if (\substr($requestDomain, 0, -\strlen($cookieDomain) - 1) . "." . $cookieDomain !== $requestDomain) { + if (\substr($requestDomain, 0, -\strlen($cookieDomain) - 1) . "." . $cookieDomain !== $requestDomain) + { return; } } @@ -1051,7 +1163,8 @@ final class HummingbirdClient implements Client { } $this->cookieJar->store($cookie); - } catch (CookieFormatException $e) { + } catch (CookieFormatException $e) + { // Ignore malformed Set-Cookie headers } } @@ -1063,15 +1176,18 @@ final class HummingbirdClient implements Client { $requestConnHeader = $request->getHeader('Connection'); $responseConnHeader = $response->getHeader('Connection'); - if ($requestConnHeader && !strcasecmp($requestConnHeader, 'close')) { - return true; - } elseif ($responseConnHeader && !strcasecmp($responseConnHeader, 'close')) { - return true; - } elseif ($response->getProtocolVersion() === '1.0' && !$responseConnHeader) { - return true; + if ($requestConnHeader && ! strcasecmp($requestConnHeader, 'close')) + { + return TRUE; + } elseif ($responseConnHeader && ! strcasecmp($responseConnHeader, 'close')) + { + return TRUE; + } elseif ($response->getProtocolVersion() === '1.0' && ! $responseConnHeader) + { + return TRUE; } - return false; + return FALSE; } private function withCancellation(Promise $promise, CancellationToken $cancellationToken): Promise @@ -1080,21 +1196,25 @@ final class HummingbirdClient implements Client { $newPromise = $deferred->promise(); $promise->onResolve(function ($error, $value) use (&$deferred) { - if ($deferred) { - if ($error) { + if ($deferred) + { + if ($error) + { $deferred->fail($error); - $deferred = null; - } else { + $deferred = NULL; + } else + { $deferred->resolve($value); - $deferred = null; + $deferred = NULL; } } }); $cancellationSubscription = $cancellationToken->subscribe(function ($e) use (&$deferred) { - if ($deferred) { + if ($deferred) + { $deferred->fail($e); - $deferred = null; + $deferred = NULL; } }); @@ -1107,8 +1227,9 @@ final class HummingbirdClient implements Client { private function getRedirectUri(Response $response) { - if (!$response->hasHeader('Location')) { - return null; + if ( ! $response->hasHeader('Location')) + { + return NULL; } $request = $response->getRequest(); @@ -1116,17 +1237,20 @@ final class HummingbirdClient implements Client { $status = $response->getStatus(); $method = $request->getMethod(); - if ($status < 300 || $status > 399 || $method === 'HEAD') { - return null; + if ($status < 300 || $status > 399 || $method === 'HEAD') + { + return NULL; } $requestUri = new Uri($request->getUri()); $redirectLocation = $response->getHeader('Location'); - try { + try + { return $requestUri->resolve($redirectLocation); - } catch (InvalidUriException $e) { - return null; + } catch (InvalidUriException $e) + { + return NULL; } } @@ -1147,7 +1271,8 @@ final class HummingbirdClient implements Client { $refererIsEncrypted = (\stripos($refererUri, 'https') === 0); $destinationIsEncrypted = (\stripos($newUri, 'https') === 0); - if (!$refererIsEncrypted || $destinationIsEncrypted) { + if ( ! $refererIsEncrypted || $destinationIsEncrypted) + { return $request->withHeader('Referer', $refererUri); } @@ -1163,7 +1288,8 @@ final class HummingbirdClient implements Client { */ public function setOptions(array $options) { - foreach ($options as $option => $value) { + foreach ($options as $option => $value) + { $this->setOption($option, $value); } } diff --git a/src/API/JsonAPI.php b/src/API/JsonAPI.php index f9ef15b3..f2dc8d60 100644 --- a/src/API/JsonAPI.php +++ b/src/API/JsonAPI.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -61,6 +61,11 @@ final class JsonAPI { // Inline organized data foreach($data['data'] as $i => &$item) { + if ( ! is_array($item)) + { + continue; + } + if (array_key_exists('relationships', $item)) { foreach($item['relationships'] as $relType => $props) @@ -96,27 +101,28 @@ final class JsonAPI { continue; } + // Single data item - else if (array_key_exists('id', $props['data'])) + if (array_key_exists('id', $props['data'])) { $idKey = $props['data']['id']; - $typeKey = $props['data']['type']; + $dataType = $props['data']['type']; $relationship =& $item['relationships'][$relType]; unset($relationship['data']); - if (in_array($relType, $singular)) + if (\in_array($relType, $singular, TRUE)) { - $relationship = $included[$typeKey][$idKey]; + $relationship = $included[$dataType][$idKey]; continue; } - if ($relType === $typeKey) + if ($relType === $dataType) { - $relationship[$idKey] = $included[$typeKey][$idKey]; + $relationship[$idKey] = $included[$dataType][$idKey]; continue; } - $relationship[$typeKey][$idKey] = $included[$typeKey][$idKey]; + $relationship[$dataType][$idKey] = $included[$dataType][$idKey]; } // Multiple data items else @@ -124,17 +130,19 @@ final class JsonAPI { foreach($props['data'] as $j => $datum) { $idKey = $props['data'][$j]['id']; - $typeKey = $props['data'][$j]['type']; + $dataType = $props['data'][$j]['type']; $relationship =& $item['relationships'][$relType]; - if ($relType === $typeKey) + if ($relType === $dataType) { - $relationship[$idKey] = $included[$typeKey][$idKey]; + $relationship[$idKey] = $included[$dataType][$idKey]; continue; } - $relationship[$typeKey][$idKey][$j] = $included[$typeKey][$idKey]; + $relationship[$dataType][$idKey][$j] = $included[$dataType][$idKey]; } + + unset($item['relationships'][$relType]['data']); } } } @@ -196,29 +204,33 @@ final class JsonAPI { { foreach($items as $id => $item) { - if (array_key_exists('relationships', $item) && is_array($item['relationships'])) + if (array_key_exists('relationships', $item) && \is_array($item['relationships'])) { foreach($item['relationships'] as $relType => $props) { - if (array_key_exists('data', $props) && is_array($props['data']) && array_key_exists('id', $props['data'])) + 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']])) + $idKey = $props['data']['id']; + $dataType = $props['data']['type']; + + $relationship =& $organized[$type][$id]['relationships'][$relType]; + unset($relationship['links']); + unset($relationship['data']); + + if ($relType === $dataType) { - $idKey = $props['data']['id']; - $typeKey = $props['data']['type']; + $relationship[$idKey] = $included[$dataType][$idKey]; + continue; + } + if ( ! array_key_exists($dataType, $organized)) + { + $organized[$dataType] = []; + } - $relationship =& $organized[$type][$id]['relationships'][$relType]; - unset($relationship['links']); - unset($relationship['data']); - - if ($relType === $typeKey) - { - $relationship[$idKey] = $included[$typeKey][$idKey]; - continue; - } - - $relationship[$typeKey][$idKey] = $organized[$typeKey][$idKey]; + if (array_key_exists($idKey, $organized[$dataType])) + { + $relationship[$dataType][$idKey] = $organized[$dataType][$idKey]; } } } @@ -250,6 +262,14 @@ final class JsonAPI { foreach($item['relationships'] as $type => $ids) { $inlined[$key][$itemId]['relationships'][$type] = []; + + if ( ! array_key_exists($type, $included)) continue; + + if (array_key_exists('data', $ids )) + { + $ids = array_column($ids['data'], 'id'); + } + foreach($ids as $id) { $inlined[$key][$itemId]['relationships'][$type][$id] = $included[$type][$id]; @@ -272,13 +292,23 @@ final class JsonAPI { public static function organizeIncludes(array $includes): array { $organized = []; + $types = array_unique(array_column($includes, 'type')); + sort($types); + + foreach ($types as $type) + { + $organized[$type] = []; + } foreach ($includes as $item) { $type = $item['type']; $id = $item['id']; - $organized[$type] = $organized[$type] ?? []; - $organized[$type][$id] = $item['attributes']; + + if (array_key_exists('attributes', $item)) + { + $organized[$type][$id] = $item['attributes']; + } if (array_key_exists('relationships', $item)) { @@ -300,17 +330,17 @@ final class JsonAPI { */ public static function organizeRelationships(array $relationships): array { - $organized = []; + $organized = $relationships; foreach($relationships as $key => $data) { + $organized[$key] = $organized[$key] ?? []; + if ( ! array_key_exists('data', $data)) { continue; } - $organized[$key] = $organized[$key] ?? []; - foreach ($data['data'] as $item) { if (\is_array($item) && array_key_exists('id', $item)) diff --git a/src/API/Kitsu.php b/src/API/Kitsu.php index 82cbf246..83a9737b 100644 --- a/src/API/Kitsu.php +++ b/src/API/Kitsu.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -45,17 +45,17 @@ final class Kitsu { $isDoneAiring = $now > $endAirDate; $isCurrentlyAiring = ($now > $startAirDate) && ! $isDoneAiring; - switch (TRUE) + if ($isCurrentlyAiring) { - case $isCurrentlyAiring: - return AnimeAiringStatus::AIRING; - - case $isDoneAiring: - return AnimeAiringStatus::FINISHED_AIRING; - - default: - return AnimeAiringStatus::NOT_YET_AIRED; + return AnimeAiringStatus::AIRING; } + + if ($isDoneAiring) + { + return AnimeAiringStatus::FINISHED_AIRING; + } + + return AnimeAiringStatus::NOT_YET_AIRED; } /** @@ -66,73 +66,63 @@ final class Kitsu { */ protected static function getServiceMetaData(string $hostname = NULL): array { - switch($hostname) + $hostname = str_replace('www.', '', $hostname); + + $serviceMap = [ + 'amazon.com' => [ + 'name' => 'Amazon Prime', + 'link' => TRUE, + 'image' => 'streaming-logos/amazon.svg', + ], + 'crunchyroll.com' => [ + 'name' => 'Crunchyroll', + 'link' => TRUE, + 'image' => 'streaming-logos/crunchyroll.svg', + ], + 'daisuki.net' => [ + 'name' => 'Daisuki', + 'link' => TRUE, + 'image' => 'streaming-logos/daisuki.svg' + ], + 'funimation.com' => [ + 'name' => 'Funimation', + 'link' => TRUE, + 'image' => 'streaming-logos/funimation.svg', + ], + 'hidive.com' => [ + 'name' => 'Hidive', + 'link' => TRUE, + 'image' => 'streaming-logos/hidive.svg', + ], + 'hulu.com' => [ + 'name' => 'Hulu', + 'link' => TRUE, + 'image' => 'streaming-logos/hulu.svg', + ], + 'tubitv.com' => [ + 'name' => 'TubiTV', + 'link' => TRUE, + 'image' => 'streaming-logos/tubitv.svg', + ], + 'viewster.com' => [ + 'name' => 'Viewster', + 'link' => TRUE, + 'image' => 'streaming-logos/viewster.svg' + ], + ]; + + if (array_key_exists($hostname, $serviceMap)) { - case 'www.amazon.com': - return [ - 'name' => 'Amazon Prime', - 'link' => TRUE, - 'image' => 'streaming-logos/amazon.svg', - ]; - - case 'www.crunchyroll.com': - return [ - 'name' => 'Crunchyroll', - 'link' => TRUE, - 'image' => 'streaming-logos/crunchyroll.svg', - ]; - - case 'www.daisuki.net': - return [ - 'name' => 'Daisuki', - 'link' => TRUE, - 'image' => 'streaming-logos/daisuki.svg' - ]; - - case 'www.funimation.com': - return [ - 'name' => 'Funimation', - 'link' => TRUE, - 'image' => 'streaming-logos/funimation.svg', - ]; - - case 'www.hidive.com': - return [ - 'name' => 'Hidive', - 'link' => TRUE, - 'image' => 'streaming-logos/hidive.svg', - ]; - - case 'www.hulu.com': - return [ - 'name' => 'Hulu', - 'link' => TRUE, - 'image' => 'streaming-logos/hulu.svg', - ]; - - case 'tubitv.com': - return [ - 'name' => 'TubiTV', - 'link' => TRUE, - 'image' => 'streaming-logos/tubitv.svg', - ]; - - case 'www.viewster.com': - return [ - 'name' => 'Viewster', - 'link' => TRUE, - 'image' => 'streaming-logos/viewster.svg' - ]; - - // Default to Netflix, because the API links are broken, - // and there's no other real identifier for Netflix - default: - return [ - 'name' => 'Netflix', - 'link' => FALSE, - 'image' => 'streaming-logos/netflix.svg', - ]; + return $serviceMap[$hostname]; } + + // Default to Netflix, because the API links are broken, + // and there's no other real identifier for Netflix + return [ + 'name' => 'Netflix', + 'link' => FALSE, + 'image' => 'streaming-logos/netflix.svg', + ]; } /** @@ -173,6 +163,10 @@ final class Kitsu { 'dubs' => $streamingLink['dubs'] ]; } + + usort($links, function ($a, $b) { + return $a['meta']['name'] <=> $b['meta']['name']; + }); return $links; } diff --git a/src/API/Kitsu/Auth.php b/src/API/Kitsu/Auth.php index e6386ff4..e5591e2e 100644 --- a/src/API/Kitsu/Auth.php +++ b/src/API/Kitsu/Auth.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -67,20 +67,12 @@ final class Auth { * @param string $password * @return boolean */ - public function authenticate($password) + public function authenticate(string $password): bool { $config = $this->container->get('config'); - $username = $config->get(['kitsu_username']); - - // try - { - $auth = $this->model->authenticate($username, $password); - } - /* catch (Exception $e) - { - return FALSE; - }*/ + $username = $config->get('kitsu_username'); + $auth = $this->model->authenticate($username, $password); if (FALSE !== $auth) { @@ -104,6 +96,7 @@ final class Auth { $this->segment->set('auth_token', $auth['access_token']); $this->segment->set('auth_token_expires', $expire_time); $this->segment->set('refresh_token', $auth['refresh_token']); + return TRUE; } @@ -117,16 +110,9 @@ final class Auth { * @param string $token * @return boolean */ - public function reAuthenticate(string $token) + public function reAuthenticate(string $token): bool { - try - { - $auth = $this->model->reAuthenticate($token); - } - catch (Exception $e) - { - return FALSE; - } + $auth = $this->model->reAuthenticate($token); if (FALSE !== $auth) { @@ -162,7 +148,7 @@ final class Auth { * * @return boolean */ - public function isAuthenticated() + public function isAuthenticated(): bool { return ($this->get_auth_token() !== FALSE); } @@ -172,7 +158,7 @@ final class Auth { * * @return void */ - public function logout() + public function logout(): void { $this->segment->clear(); } @@ -184,16 +170,22 @@ final class Auth { */ public function get_auth_token() { + $now = time(); + $token = $this->segment->get('auth_token', FALSE); - $refresh_token = $this->segment->get('refresh_token', FALSE); - $isExpired = time() > $this->segment->get('auth_token_expires', 0); + $refreshToken = $this->segment->get('refresh_token', FALSE); + $isExpired = time() > $this->segment->get('auth_token_expires', $now + 5000); // Attempt to re-authenticate with refresh token - if ($isExpired && $refresh_token) + /* if ($isExpired && $refreshToken) { - $reauthenticated = $this->reAuthenticate($refresh_token); - return $this->segment->get('auth_token', FALSE); - } + if ($this->reAuthenticate($refreshToken)) + { + return $this->segment->get('auth_token', FALSE); + } + + return FALSE; + } */ return $token; } diff --git a/src/API/Kitsu/Enum/AnimeAiringStatus.php b/src/API/Kitsu/Enum/AnimeAiringStatus.php index 02a0bbdf..6b79cb2b 100644 --- a/src/API/Kitsu/Enum/AnimeAiringStatus.php +++ b/src/API/Kitsu/Enum/AnimeAiringStatus.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/API/Kitsu/GraphQL/Mutations/.gitkeep b/src/API/Kitsu/GraphQL/Mutations/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/API/Kitsu/GraphQL/Queries/.gitkeep b/src/API/Kitsu/GraphQL/Queries/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/API/Kitsu/KitsuRequestBuilder.php b/src/API/Kitsu/KitsuRequestBuilder.php index 9fe30b91..bc926f6b 100644 --- a/src/API/Kitsu/KitsuRequestBuilder.php +++ b/src/API/Kitsu/KitsuRequestBuilder.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/API/Kitsu/KitsuTrait.php b/src/API/Kitsu/KitsuTrait.php index efe3a930..b9aaee17 100644 --- a/src/API/Kitsu/KitsuTrait.php +++ b/src/API/Kitsu/KitsuTrait.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -33,7 +33,7 @@ use Aviat\Ion\JsonException; trait KitsuTrait { /** - * The request builder for the MAL API + * The request builder for the Kitsu API * @var KitsuRequestBuilder */ protected $requestBuilder; diff --git a/src/API/Kitsu/ListItem.php b/src/API/Kitsu/ListItem.php index e91ca394..4b09124f 100644 --- a/src/API/Kitsu/ListItem.php +++ b/src/API/Kitsu/ListItem.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -37,7 +37,7 @@ final class ListItem implements ListItemInterface { use KitsuTrait; public function create(array $data): Request - { + { $body = [ 'data' => [ 'type' => 'libraryEntries', @@ -61,6 +61,11 @@ final class ListItem implements ListItemInterface { ] ] ]; + + if (array_key_exists('notes', $data)) + { + $body['data']['attributes']['notes'] = $data['notes']; + } $authHeader = $this->getAuthHeader(); @@ -98,7 +103,7 @@ final class ListItem implements ListItemInterface { $request = $this->requestBuilder->newRequest('GET', "library-entries/{$id}") ->setQuery([ - 'include' => 'media,media.genres,media.mappings' + 'include' => 'media,media.categories,media.mappings' ]); if ($authHeader !== FALSE) @@ -112,6 +117,11 @@ final class ListItem implements ListItemInterface { return Json::decode(wait($response->getBody())); } + public function increment(string $id, FormItemData $data): Request + { + return $this->update($id, $data); + } + public function update(string $id, FormItemData $data): Request { $authHeader = $this->getAuthHeader(); diff --git a/src/API/Kitsu/Model.php b/src/API/Kitsu/Model.php index 634c5462..93de3fe6 100644 --- a/src/API/Kitsu/Model.php +++ b/src/API/Kitsu/Model.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -37,11 +37,8 @@ use Aviat\AnimeClient\API\Kitsu\Transformer\{ MangaListTransformer }; use Aviat\AnimeClient\Types\{ - AbstractType, Anime, FormItem, - FormItemData, - AnimeListItem, MangaPage }; use Aviat\Ion\{Di\ContainerAware, Json}; @@ -124,11 +121,6 @@ final class Model { ]); $data = Json::decode(wait($response->getBody())); - if (array_key_exists('access_token', $data)) - { - return $data; - } - if (array_key_exists('error', $data)) { dump($data['error']); @@ -136,6 +128,11 @@ final class Model { die(); } + if (array_key_exists('access_token', $data)) + { + return $data; + } + return FALSE; } @@ -224,6 +221,41 @@ final class Model { return $data; } + /** + * Get information about a person + * + * @param string $id + * @return array + */ + public function getPerson(string $id): array + { + $cacheItem = $this->cache->getItem("kitsu-person-{$id}"); + + if ( ! $cacheItem->isHit()) + { + $data = $this->getRequest("people/{$id}", [ + 'query' => [ + 'filter' => [ + 'id' => $id, + ], + 'fields' => [ + 'characters' => 'canonicalName,slug,image', + 'characterVoices' => 'mediaCharacter', + 'anime' => 'canonicalTitle,titles,slug,posterImage', + 'manga' => 'canonicalTitle,titles,slug,posterImage', + 'mediaCharacters' => 'role,media,character', + 'mediaStaff' => 'role,media,person', + ], + 'include' => 'voices.mediaCharacter.media,voices.mediaCharacter.character,staff.media', + ], + ]); + $cacheItem->set($data); + $cacheItem->save(); + } + + return $cacheItem->get(); + } + /** * Get profile information for the configured user * @@ -239,10 +271,11 @@ final class Model { 'name' => $username, ], 'fields' => [ - // 'anime' => 'slug,name,canonicalTitle', - 'characters' => 'slug,name,image' + 'anime' => 'slug,canonicalTitle,posterImage', + 'manga' => 'slug,canonicalTitle,posterImage', + 'characters' => 'slug,canonicalName,image', ], - 'include' => 'waifu,pinnedPost,blocks,linkedAccounts,profileLinks,profileLinks.profileLinkSite,userRoles,favorites.item' + 'include' => 'waifu,favorites.item,stats' ] ]); @@ -261,21 +294,34 @@ final class Model { $options = [ 'query' => [ 'filter' => [ - 'text' => $query + 'text' => $query, ], 'page' => [ 'offset' => 0, 'limit' => 20 ], + 'include' => 'mappings' ] ]; $raw = $this->getRequest($type, $options); + $raw['included'] = JsonAPI::organizeIncluded($raw['included']); foreach ($raw['data'] as &$item) { $item['attributes']['titles'] = K::filterTitles($item['attributes']); array_shift($item['attributes']['titles']); + + // Map the mal_id if it exists for syncing with other APIs + foreach($item['relationships']['mappings']['data'] as $rel) + { + $mapping = $raw['included']['mappings'][$rel['id']]; + + if ($mapping['attributes']['externalSite'] === "myanimelist/{$type}") + { + $item['mal_id'] = $mapping['attributes']['externalId']; + } + } } return $raw; @@ -323,27 +369,25 @@ final class Model { * @param string $slug * @return Anime */ - public function getAnime(string $slug): Anime + public function getAnime(string $slug) { $baseData = $this->getRawMediaData('anime', $slug); if (empty($baseData)) { - return new Anime(); + return (new Anime([]))->toArray(); } - $transformed = $this->animeTransformer->transform($baseData); - $transformed['included'] = JsonAPI::organizeIncluded($baseData['included']); - return $transformed; + return $this->animeTransformer->transform($baseData); } /** * Get information about a particular anime * * @param string $animeId - * @return array + * @return Anime */ - public function getAnimeById(string $animeId): array + public function getAnimeById(string $animeId): Anime { $baseData = $this->getRawMediaDataById('anime', $animeId); return $this->animeTransformer->transform($baseData); @@ -366,9 +410,7 @@ final class Model { // Bail out on no data if (empty($data)) { - $cacheItem->set([]); - $cacheItem->save(); - return $cacheItem->get(); + return []; } $included = JsonAPI::organizeIncludes($data['included']); @@ -455,7 +497,7 @@ final class Model { } /** - * Get all the anine entries, that are organized for output to html + * Get all the anime entries, that are organized for output to html * * @return array */ @@ -550,7 +592,7 @@ final class Model { 'media_type' => 'Anime', 'status' => $status, ], - 'include' => 'media,media.genres,media.mappings,anime.streamingLinks', + 'include' => 'media,media.categories,media.mappings,anime.streamingLinks', 'sort' => '-updated_at' ]; @@ -576,9 +618,7 @@ final class Model { return new MangaPage([]); } - $transformed = $this->mangaTransformer->transform($baseData); - $transformed['included'] = $baseData['included']; - return $transformed; + return $this->mangaTransformer->transform($baseData); } /** @@ -610,7 +650,7 @@ final class Model { 'media_type' => 'Manga', 'status' => $status, ], - 'include' => 'media,media.genres,media.mappings', + 'include' => 'media,media.categories,media.mappings', 'page' => [ 'offset' => $offset, 'limit' => $limit @@ -628,9 +668,7 @@ final class Model { // Bail out on no data if (empty($data) || ( ! array_key_exists('included', $data))) { - $cacheItem->set([]); - $cacheItem->save(); - return $cacheItem->get(); + return []; } $included = JsonAPI::organizeIncludes($data['included']); @@ -818,23 +856,33 @@ final class Model { $baseData = $this->listItem->get($listId); $included = JsonAPI::organizeIncludes($baseData['included']); - - switch (TRUE) + if (array_key_exists('anime', $included)) { - case array_key_exists('anime', $included): // in_array('anime', array_keys($included)): - $included = JsonAPI::inlineIncludedRelationships($included, 'anime'); - $baseData['data']['included'] = $included; - return $this->animeListTransformer->transform($baseData['data']); - - case array_key_exists('manga', $included): // in_array('manga', array_keys($included)): - $included = JsonAPI::inlineIncludedRelationships($included, 'manga'); - $baseData['data']['included'] = $included; - $baseData['data']['manga'] = $baseData['included'][0]; - return $this->mangaListTransformer->transform($baseData['data']); - - default: - return $baseData['data']; + $included = JsonAPI::inlineIncludedRelationships($included, 'anime'); + $baseData['data']['included'] = $included; + return $this->animeListTransformer->transform($baseData['data']); } + + if (array_key_exists('manga', $included)) + { + $included = JsonAPI::inlineIncludedRelationships($included, 'manga'); + $baseData['data']['included'] = $included; + $baseData['data']['manga'] = $baseData['included'][0]; + return $this->mangaListTransformer->transform($baseData['data']); + } + + return $baseData['data']; + } + + /** + * Increase the progress count for a list item + * + * @param FormItem $data + * @return Request + */ + public function incrementListItem(FormItem $data): Request + { + return $this->listItem->increment($data['id'], $data['data']); } /** @@ -916,11 +964,15 @@ final class Model { 'slug' => $slug ], 'fields' => [ - 'characters' => 'slug,name,image' + 'categories' => 'slug,title', + 'characters' => 'slug,name,image', + 'mappings' => 'externalSite,externalId', + 'animeCharacters' => 'character,role', + 'mediaCharacters' => 'character,role', ], 'include' => ($type === 'anime') - ? 'categories,mappings,streamingLinks,animeCharacters.character' - : 'categories,mappings,mangaCharacters.character,castings.character', + ? 'staff,staff.person,categories,mappings,streamingLinks,animeCharacters.character,characters.character' + : 'staff,staff.person,categories,mappings,characters.character', ] ]; diff --git a/src/API/Kitsu/Transformer/AnimeListTransformer.php b/src/API/Kitsu/Transformer/AnimeListTransformer.php index ab73ac50..e7da03a5 100644 --- a/src/API/Kitsu/Transformer/AnimeListTransformer.php +++ b/src/API/Kitsu/Transformer/AnimeListTransformer.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -19,8 +19,7 @@ namespace Aviat\AnimeClient\API\Kitsu\Transformer; use Aviat\AnimeClient\API\Kitsu; use Aviat\AnimeClient\Types\{ Anime, - AnimeFormItem, - AnimeFormItemData, + FormItem, AnimeListItem }; use Aviat\Ion\Transformer\AbstractTransformer; @@ -43,11 +42,17 @@ final class AnimeListTransformer extends AbstractTransformer { $animeId = $item['relationships']['media']['data']['id']; $anime = $included['anime'][$animeId]; - $genres = array_column($anime['relationships']['genres'], 'name') ?? []; + $genres = []; + + foreach($anime['relationships']['categories'] as $genre) + { + $genres[] = $genre['title']; + } + sort($genres); - $rating = (int) $item['attributes']['rating'] !== 0 - ? 2 * $item['attributes']['rating'] + $rating = (int) $item['attributes']['ratingTwenty'] !== 0 + ? $item['attributes']['ratingTwenty'] / 2 : '-'; $total_episodes = array_key_exists('episodeCount', $anime) && (int) $anime['episodeCount'] !== 0 @@ -96,7 +101,7 @@ final class AnimeListTransformer extends AbstractTransformer { 'title' => $title, 'titles' => $titles, 'slug' => $anime['slug'], - 'show_type' => $this->string($anime['showType'])->upperCaseFirst()->__toString(), + 'show_type' => $this->string($anime['subtype'])->upperCaseFirst()->__toString(), 'cover_image' => $anime['posterImage']['small'], 'genres' => $genres, 'streaming_links' => $streamingLinks, @@ -115,15 +120,16 @@ final class AnimeListTransformer extends AbstractTransformer { * api response format * * @param array $item Transformed library item - * @return AnimeFormItem API library item + * @return FormItem API library item */ - public function untransform($item): AnimeFormItem + public function untransform($item): FormItem { $privacy = (array_key_exists('private', $item) && $item['private']); $rewatching = (array_key_exists('rewatching', $item) && $item['rewatching']); - $untransformed = new AnimeFormItem([ + $untransformed = new FormItem([ 'id' => $item['id'], + 'anilist_item_id' => $item['anilist_item_id'] ?? NULL, 'mal_id' => $item['mal_id'] ?? NULL, 'data' => [ 'status' => $item['watching_status'], @@ -141,7 +147,7 @@ final class AnimeListTransformer extends AbstractTransformer { if (is_numeric($item['user_rating']) && $item['user_rating'] > 0) { - $untransformed['data']['rating'] = $item['user_rating'] / 2; + $untransformed['data']['ratingTwenty'] = $item['user_rating'] * 2; } return $untransformed; diff --git a/src/API/Kitsu/Transformer/AnimeTransformer.php b/src/API/Kitsu/Transformer/AnimeTransformer.php index 40d494c6..f39dc52c 100644 --- a/src/API/Kitsu/Transformer/AnimeTransformer.php +++ b/src/API/Kitsu/Transformer/AnimeTransformer.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -40,7 +40,9 @@ final class AnimeTransformer extends AbstractTransformer { sort($item['genres']); $title = $item['canonicalTitle']; - $titles = array_diff($item['titles'], [$title]); + + $titles = Kitsu::filterTitles($item); + // $titles = array_unique(array_diff($item['titles'], [$title])); return new Anime([ 'age_rating' => $item['ageRating'], @@ -50,6 +52,7 @@ final class AnimeTransformer extends AbstractTransformer { 'episode_length' => $item['episodeLength'], 'genres' => $item['genres'], 'id' => $item['id'], + 'included' => $item['included'], 'show_type' => $this->string($item['showType'])->upperCaseFirst()->__toString(), 'slug' => $item['slug'], 'status' => Kitsu::getAiringStatus($item['startDate'], $item['endDate']), diff --git a/src/API/Kitsu/Transformer/MangaListTransformer.php b/src/API/Kitsu/Transformer/MangaListTransformer.php index b41bd805..28691ecb 100644 --- a/src/API/Kitsu/Transformer/MangaListTransformer.php +++ b/src/API/Kitsu/Transformer/MangaListTransformer.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -18,7 +18,7 @@ namespace Aviat\AnimeClient\API\Kitsu\Transformer; use Aviat\AnimeClient\API\Kitsu; use Aviat\AnimeClient\Types\{ - MangaFormItem, MangaFormItemData, + FormItem, FormItemData, MangaListItem, MangaListItemDetail }; use Aviat\Ion\StringWrapper; @@ -43,11 +43,17 @@ final class MangaListTransformer extends AbstractTransformer { $mangaId = $item['relationships']['media']['data']['id']; $manga = $included['manga'][$mangaId]; - $genres = array_column($manga['relationships']['genres'], 'name') ?? []; + $genres = []; + + foreach ($manga['relationships']['categories'] as $genre) + { + $genres[] = $genre['title']; + } + sort($genres); - $rating = (int) $item['attributes']['rating'] !== 0 - ? 2 * $item['attributes']['rating'] + $rating = (int) $item['attributes']['ratingTwenty'] !== 0 + ? $item['attributes']['ratingTwenty'] / 2 : '-'; $totalChapters = ((int) $manga['chapterCount'] !== 0) @@ -97,7 +103,7 @@ final class MangaListTransformer extends AbstractTransformer { 'slug' => $manga['slug'], 'title' => $title, 'titles' => $titles, - 'type' => $manga['mangaType'], + 'type' => $this->string($manga['subtype'])->upperCaseFirst()->__toString(), 'url' => 'https://kitsu.io/manga/' . $manga['slug'], ]), 'reading_status' => $item['attributes']['status'], @@ -114,16 +120,16 @@ final class MangaListTransformer extends AbstractTransformer { * Untransform data to update the api * * @param array $item - * @return MangaFormItem + * @return FormItem */ - public function untransform($item): MangaFormItem + public function untransform($item): FormItem { $rereading = array_key_exists('rereading', $item) && (bool)$item['rereading']; - $map = new MangaFormItem([ + $map = new FormItem([ 'id' => $item['id'], 'mal_id' => $item['mal_id'], - 'data' => new MangaFormItemData([ + 'data' => new FormItemData([ 'status' => $item['status'], 'reconsuming' => $rereading, 'reconsumeCount' => (int)$item['reread_count'], @@ -138,7 +144,7 @@ final class MangaListTransformer extends AbstractTransformer { if (is_numeric($item['new_rating']) && $item['new_rating'] > 0) { - $map['data']['rating'] = $item['new_rating'] / 2; + $map['data']['ratingTwenty'] = $item['new_rating'] * 2; } return $map; diff --git a/src/API/Kitsu/Transformer/MangaTransformer.php b/src/API/Kitsu/Transformer/MangaTransformer.php index afd468d5..e645226e 100644 --- a/src/API/Kitsu/Transformer/MangaTransformer.php +++ b/src/API/Kitsu/Transformer/MangaTransformer.php @@ -2,20 +2,21 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ namespace Aviat\AnimeClient\API\Kitsu\Transformer; +use Aviat\AnimeClient\API\JsonAPI; use Aviat\AnimeClient\Types\MangaPage; use Aviat\Ion\Transformer\AbstractTransformer; @@ -33,31 +34,35 @@ final class MangaTransformer extends AbstractTransformer { */ public function transform($item): MangaPage { - // \dump($item); $genres = []; - foreach($item['included'] as $included) + $item['included'] = JsonAPI::organizeIncluded($item['included']); + + if (array_key_exists('categories', $item['included'])) { - if ($included['type'] === 'categories') + foreach ($item['included']['categories'] as $cat) { - $genres[] = $included['attributes']['title']; + $genres[] = $cat['attributes']['title']; } + sort($genres); } - sort($genres); + $title = $item['canonicalTitle']; + $rawTitles = array_values($item['titles']); + $titles = array_unique(array_diff($rawTitles, [$title])); return new MangaPage([ - 'id' => $item['id'], - 'title' => $item['canonicalTitle'], - 'en_title' => $item['titles']['en'], - 'jp_title' => $item['titles']['en_jp'], - 'cover_image' => $item['posterImage']['small'], - 'manga_type' => $item['mangaType'], 'chapter_count' => $this->count($item['chapterCount']), - 'volume_count' => $this->count($item['volumeCount']), - 'synopsis' => $item['synopsis'], - 'url' => "https://kitsu.io/manga/{$item['slug']}", + 'cover_image' => $item['posterImage']['small'], 'genres' => $genres, + 'id' => $item['id'], + 'included' => $item['included'], + 'manga_type' => $item['mangaType'], + 'synopsis' => $item['synopsis'], + 'title' => $title, + 'titles' => $titles, + 'url' => "https://kitsu.io/manga/{$item['slug']}", + 'volume_count' => $this->count($item['volumeCount']), ]); } diff --git a/src/API/ListItemInterface.php b/src/API/ListItemInterface.php index 2fb6404a..310b273b 100644 --- a/src/API/ListItemInterface.php +++ b/src/API/ListItemInterface.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -40,6 +40,15 @@ interface ListItemInterface { */ public function get(string $id): array; + /** + * Increase progress on a list item + * + * @param string $id + * @param FormItemData $data + * @return Request + */ + public function increment(string $id, FormItemData $data): Request; + /** * Update a list item * diff --git a/src/API/MAL.php b/src/API/MAL.php deleted file mode 100644 index 332a721f..00000000 --- a/src/API/MAL.php +++ /dev/null @@ -1,82 +0,0 @@ - - * @copyright 2015 - 2018 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient\API; - -use Aviat\AnimeClient\API\Enum\{ - AnimeWatchingStatus\Kitsu as KAWS, - MangaReadingStatus\Kitsu as KMRS -}; -use Aviat\AnimeClient\API\Enum\{ - AnimeWatchingStatus\MAL as AnimeWatchingStatus, - MangaReadingStatus\MAL as MangaReadingStatus -}; - -/** - * Constants and mappings for the My Anime List API - */ -final class MAL { - const AUTH_URL = 'https://myanimelist.net/api/account/verify_credentials.xml'; - const BASE_URL = 'https://myanimelist.net/api/'; - - const KITSU_MAL_WATCHING_STATUS_MAP = [ - KAWS::WATCHING => AnimeWatchingStatus::WATCHING, - KAWS::COMPLETED => AnimeWatchingStatus::COMPLETED, - KAWS::ON_HOLD => AnimeWatchingStatus::ON_HOLD, - KAWS::DROPPED => AnimeWatchingStatus::DROPPED, - KAWS::PLAN_TO_WATCH => AnimeWatchingStatus::PLAN_TO_WATCH - ]; - - const MAL_KITSU_WATCHING_STATUS_MAP = [ - 1 => KAWS::WATCHING, - 2 => KAWS::COMPLETED, - 3 => KAWS::ON_HOLD, - 4 => KAWS::DROPPED, - 6 => KAWS::PLAN_TO_WATCH - ]; - - public static function getIdToWatchingStatusMap() - { - return [ - 1 => AnimeWatchingStatus::WATCHING, - 2 => AnimeWatchingStatus::COMPLETED, - 3 => AnimeWatchingStatus::ON_HOLD, - 4 => AnimeWatchingStatus::DROPPED, - 6 => AnimeWatchingStatus::PLAN_TO_WATCH, - 'watching' => AnimeWatchingStatus::WATCHING, - 'completed' => AnimeWatchingStatus::COMPLETED, - 'onhold' => AnimeWatchingStatus::ON_HOLD, - 'dropped' => AnimeWatchingStatus::DROPPED, - 'plantowatch' => AnimeWatchingStatus::PLAN_TO_WATCH - ]; - } - - public static function getIdToReadingStatusMap() - { - return [ - 1 => MangaReadingStatus::READING, - 2 => MangaReadingStatus::COMPLETED, - 3 => MangaReadingStatus::ON_HOLD, - 4 => MangaReadingStatus::DROPPED, - 6 => MangaReadingStatus::PLAN_TO_READ, - 'reading' => MangaReadingStatus::READING, - 'completed' => MangaReadingStatus::COMPLETED, - 'onhold' => MangaReadingStatus::ON_HOLD, - 'dropped' => MangaReadingStatus::DROPPED, - 'plantoread' => MangaReadingStatus::PLAN_TO_READ - ]; - } -} \ No newline at end of file diff --git a/src/API/MAL/ListItem.php b/src/API/MAL/ListItem.php deleted file mode 100644 index 09405394..00000000 --- a/src/API/MAL/ListItem.php +++ /dev/null @@ -1,109 +0,0 @@ - - * @copyright 2015 - 2018 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient\API\MAL; - -use Amp\Artax\{FormBody, Request}; -use Aviat\AnimeClient\API\{ - XML -}; -use Aviat\AnimeClient\Types\AbstractType; -use Aviat\Ion\Di\ContainerAware; - -/** - * CRUD operations for MAL list items - */ -final class ListItem { - use ContainerAware; - use MALTrait; - - /** - * Create a list item - * - * @param array $data - * @param string $type - * @return Request - */ - public function create(array $data, string $type = 'anime'): Request - { - $id = $data['id']; - $createData = [ - 'id' => $id, - 'data' => XML::toXML([ - 'entry' => $data['data'] - ]) - ]; - - $config = $this->container->get('config'); - - return $this->requestBuilder->newRequest('POST', "{$type}list/add/{$id}.xml") - ->setFormFields($createData) - ->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password'])) - ->getFullRequest(); - } - - /** - * Delete a list item - * - * @param string $id - * @param string $type - * @return Request - */ - public function delete(string $id, string $type = 'anime'): Request - { - $config = $this->container->get('config'); - - return $this->requestBuilder->newRequest('DELETE', "{$type}list/delete/{$id}.xml") - ->setFormFields([ - 'id' => $id - ]) - ->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password'])) - ->getFullRequest(); - - // return $response->getBody() === 'Deleted' - } - - public function get(string $id): array - { - return []; - } - - /** - * Update a list item - * - * @param string $id - * @param AbstractType $data - * @param string $type - * @return Request - */ - public function update(string $id, AbstractType $data, string $type = 'anime'): Request - { - $config = $this->container->get('config'); - - $xml = XML::toXML(['entry' => $data]); - $body = new FormBody(); - $body->addField('id', $id); - $body->addField('data', $xml); - - return $this->requestBuilder->newRequest('POST', "{$type}list/update/{$id}.xml") - ->setFormFields([ - 'id' => $id, - 'data' => $xml - ]) - ->setBasicAuth($config->get(['mal','username']), $config->get(['mal', 'password'])) - ->getFullRequest(); - } -} \ No newline at end of file diff --git a/src/API/MAL/MALRequestBuilder.php b/src/API/MAL/MALRequestBuilder.php deleted file mode 100644 index 9888a12e..00000000 --- a/src/API/MAL/MALRequestBuilder.php +++ /dev/null @@ -1,51 +0,0 @@ - - * @copyright 2015 - 2018 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient\API\MAL; - -use const Aviat\AnimeClient\USER_AGENT; - -use Aviat\AnimeClient\API\{ - APIRequestBuilder, - MAL as M -}; - -final class MALRequestBuilder extends APIRequestBuilder { - - /** - * The base url for api requests - * @var string $base_url - */ - protected $baseUrl = M::BASE_URL; - - /** - * HTTP headers to send with every request - * - * @var array - */ - protected $defaultHeaders = [ - 'Accept' => 'text/xml', - 'Accept-Encoding' => 'gzip', - 'Content-type' => 'application/x-www-form-urlencoded', - 'User-Agent' => USER_AGENT, - ]; - - /** - * Valid HTTP request methos - * @var array - */ - protected $validMethods = ['GET', 'POST', 'DELETE']; -} \ No newline at end of file diff --git a/src/API/MAL/MALTrait.php b/src/API/MAL/MALTrait.php deleted file mode 100644 index 336fa041..00000000 --- a/src/API/MAL/MALTrait.php +++ /dev/null @@ -1,191 +0,0 @@ - - * @copyright 2015 - 2018 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient\API\MAL; - -use function Amp\Promise\wait; - -use Aviat\AnimeClient\API\{ - HummingbirdClient, - MAL as M, - XML -}; - -trait MALTrait { - - /** - * The request builder for the MAL API - * @var MALRequestBuilder - */ - protected $requestBuilder; - - /** - * The base url for api requests - * @var string $base_url - */ - protected $baseUrl = M::BASE_URL; - - /** - * HTTP headers to send with every request - * - * @var array - */ - protected $defaultHeaders = [ - 'Accept' => 'text/xml', - 'Accept-Encoding' => 'gzip', - 'Content-type' => 'application/x-www-form-urlencoded', - 'User-Agent' => "Tim's Anime Client/4.0" - ]; - - /** - * Set the request builder object - * - * @param MALRequestBuilder $requestBuilder - * @return self - */ - public function setRequestBuilder($requestBuilder): self - { - $this->requestBuilder = $requestBuilder; - return $this; - } - - /** - * Create a request object - * - * @param string $type - * @param string $url - * @param array $options - * @return \Amp\Artax\Response - */ - public function setUpRequest(string $type, string $url, array $options = []) - { - $config = $this->container->get('config'); - - $request = $this->requestBuilder - ->newRequest($type, $url) - ->setBasicAuth($config->get(['mal','username']), $config->get(['mal','password'])); - - if (array_key_exists('query', $options)) - { - $request = $request->setQuery($options['query']); - } - - if (array_key_exists('body', $options)) - { - $request = $request->setBody($options['body']); - } - - return $request->getFullRequest(); - } - - /** - * Make a request - * - * @param string $type - * @param string $url - * @param array $options - * @return \Amp\Artax\Response - */ - private function getResponse(string $type, string $url, array $options = []) - { - $logger = NULL; - if ($this->getContainer()) - { - $logger = $this->container->getLogger('mal-request'); - } - - $request = $this->setUpRequest($type, $url, $options); - $response = wait((new HummingbirdClient)->request($request)); - - $logger->debug('MAL api response', [ - 'status' => $response->getStatus(), - 'reason' => $response->getReason(), - 'body' => $response->getBody(), - 'headers' => $response->getHeaders(), - 'requestHeaders' => $request->getHeaders(), - ]); - - return $response; - } - - /** - * Make a request - * - * @param string $type - * @param string $url - * @param array $options - * @return array - */ - private function request(string $type, string $url, array $options = []): array - { - $logger = NULL; - if ($this->getContainer()) - { - $logger = $this->container->getLogger('mal-request'); - } - - $response = $this->getResponse($type, $url, $options); - - if ((int) $response->getStatus() > 299 OR (int) $response->getStatus() < 200) - { - if ($logger) - { - $logger->warning('Non 200 response for api call', (array)$response->getBody()); - } - } - - return XML::toArray(wait($response->getBody())); - } - - /** - * Remove some boilerplate for get requests - * - * @param mixed ...$args - * @return array - */ - protected function getRequest(...$args): array - { - return $this->request('GET', ...$args); - } - - /** - * Remove some boilerplate for post requests - * - * @param mixed ...$args - * @return array - */ - protected function postRequest(...$args): array - { - $logger = NULL; - if ($this->getContainer()) - { - $logger = $this->container->getLogger('mal-request'); - } - - $response = $this->getResponse('POST', ...$args); - $validResponseCodes = [200, 201]; - - if ( ! \in_array((int) $response->getStatus(), $validResponseCodes, TRUE)) - { - if ($logger) - { - $logger->warning('Non 201 response for POST api call', (array)$response->getBody()); - } - } - - return XML::toArray($response->getBody()); - } -} \ No newline at end of file diff --git a/src/API/MAL/Model.php b/src/API/MAL/Model.php deleted file mode 100644 index 0f245d81..00000000 --- a/src/API/MAL/Model.php +++ /dev/null @@ -1,182 +0,0 @@ - - * @copyright 2015 - 2018 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient\API\MAL; - -use Amp\Artax\Request; -use Aviat\AnimeClient\API\MAL\{ - ListItem, - Transformer\AnimeListTransformer, - Transformer\MangaListTransformer -}; -use Aviat\AnimeClient\API\XML; -use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus}; -use Aviat\AnimeClient\Types\{Anime, FormItem}; -use Aviat\Ion\Di\ContainerAware; - -/** - * MyAnimeList API Model - */ -final class Model { - use ContainerAware; - use MALTrait; - - /** - * @var AnimeListTransformer - */ - protected $animeListTransformer; - - /** - * @var MangaListTransformer - */ - protected $mangaListTransformer; - - /** - * @var ListItem - */ - protected $listItem; - - /** - * MAL Model constructor. - * - * @param ListItem $listItem - */ - public function __construct(ListItem $listItem) - { - $this->animeListTransformer = new AnimeListTransformer(); - $this->mangaListTransformer = new MangaListTransformer(); - $this->listItem = $listItem; - } - - /** - * Create a list item on MAL - * - * @param array $data - * @param string $type "anime" or "manga" - * @return Request - */ - public function createFullListItem(array $data, string $type = 'anime'): Request - { - return $this->listItem->create($data, $type); - } - - /** - * Create a list item on MAL from a Kitsu list item - * - * @param array $data - * @param string $type "anime" or "manga" - * @return Request - */ - public function createListItem(array $data, string $type = 'anime'): Request - { - $createData = []; - - if ($type === 'anime') - { - $createData = [ - 'id' => $data['id'], - 'data' => [ - 'status' => AnimeWatchingStatus::KITSU_TO_MAL[$data['status']] - ] - ]; - } - elseif ($type === 'manga') - { - $createData = [ - 'id' => $data['id'], - 'data' => [ - 'status' => MangaReadingStatus::KITSU_TO_MAL[$data['status']] - ] - ]; - } - - return $this->listItem->create($createData, $type); - } - - /** - * Get list info - * - * @param string $type "anime" or "manga" - * @return array - */ - public function getList(string $type = "anime"): array - { - $config = $this->container->get('config'); - $userName = $config->get(['mal', 'username']); - $list = $this->getRequest('https://myanimelist.net/malappinfo.php', [ - 'headers' => [ - 'Accept' => 'text/xml' - ], - 'query' => [ - 'u' => $userName, - 'status' => 'all', - 'type' => $type - ] - ]); - - return array_key_exists($type, $list['myanimelist']) - ? $list['myanimelist'][$type] - : []; - } - - /** - * Retrieve a list item - * - * Does not apply to MAL - * - * @param string $listId - * @return array - */ - public function getListItem(string $listId): array - { - return []; - } - - /** - * Update a list item - * - * @param FormItem $data - * @param string $type "anime" or "manga" - * @return Request - */ - public function updateListItem(FormItem $data, string $type = 'anime'): Request - { - $updateData = []; - - if ($type === 'anime') - { - $updateData = $this->animeListTransformer->untransform($data); - } - else if ($type === 'manga') - { - $updateData = $this->mangaListTransformer->untransform($data); - } - - return $this->listItem->update($updateData['id'], $updateData['data'], $type); - } - - /** - * Delete a list item - * - * @param string $id - * @param string $type "anime" or "manga" - * @return Request - */ - public function deleteListItem(string $id, string $type = 'anime'): Request - { - return $this->listItem->delete($id, $type); - } -} \ No newline at end of file diff --git a/src/API/MAL/Transformer/AnimeListTransformer.php b/src/API/MAL/Transformer/AnimeListTransformer.php deleted file mode 100644 index af38f8e7..00000000 --- a/src/API/MAL/Transformer/AnimeListTransformer.php +++ /dev/null @@ -1,86 +0,0 @@ - - * @copyright 2015 - 2018 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient\API\MAL\Transformer; - -use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus; -use Aviat\AnimeClient\Types\{AnimeFormItem, AnimeFormItemData}; -use Aviat\Ion\Transformer\AbstractTransformer; - -/** - * Transformer for updating MAL List - */ -final class AnimeListTransformer extends AbstractTransformer { - /** - * Identity transformation - * - * @param array $item - * @return array - */ - public function transform($item) - { - return $item; - } - - /** - * Transform Kitsu episode data to MAL episode data - * - * @param array $item - * @return AnimeFormItem - */ - public function untransform(array $item): AnimeFormItem - { - $map = new AnimeFormItem([ - 'id' => $item['mal_id'], - 'data' => new AnimeFormItemData([]), - ]); - - foreach($item['data'] as $key => $value) - { - switch($key) - { - case 'progress': - $map['data']['episode'] = $value; - break; - - case 'notes': - $map['data']['comments'] = $value; - break; - - case 'rating': - $map['data']['score'] = $value * 2; - break; - - case 'reconsuming': - $map['data']['enable_rewatching'] = (bool) $value; - break; - - case 'reconsumeCount': - $map['data']['times_rewatched'] = $value; - break; - - case 'status': - $map['data']['status'] = AnimeWatchingStatus::KITSU_TO_MAL[$value]; - break; - - default: - break; - } - } - - return $map; - } -} \ No newline at end of file diff --git a/src/API/MAL/Transformer/MangaListTransformer.php b/src/API/MAL/Transformer/MangaListTransformer.php deleted file mode 100644 index b55f91fc..00000000 --- a/src/API/MAL/Transformer/MangaListTransformer.php +++ /dev/null @@ -1,87 +0,0 @@ - - * @copyright 2015 - 2018 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient\API\MAL\Transformer; - -use Aviat\AnimeClient\API\Mapping\MangaReadingStatus; -use Aviat\Ion\Transformer\AbstractTransformer; - -/** - * Transformer for updating MAL List - */ -final class MangaListTransformer extends AbstractTransformer { - /** - * Identity transformation - * - * @param array $item - * @return array - */ - public function transform($item) - { - return $item; - } - - /** - * Transform Kitsu data to MAL data - * - * @param array $item - * @return array - */ - public function untransform(array $item): array - { - $map = [ - 'id' => $item['mal_id'], - 'data' => [] - ]; - - $data =& $item['data']; - - foreach($item['data'] as $key => $value) - { - switch($key) - { - case 'progress': - $map['data']['chapter'] = $value; - break; - - case 'notes': - $map['data']['comments'] = $value; - break; - - case 'rating': - $map['data']['score'] = $value * 2; - break; - - case 'reconsuming': - $map['data']['enable_rereading'] = (bool) $value; - break; - - case 'reconsumeCount': - $map['data']['times_reread'] = $value; - break; - - case 'status': - $map['data']['status'] = MangaReadingStatus::KITSU_TO_MAL[$value]; - break; - - default: - break; - } - } - - return $map; - } -} \ No newline at end of file diff --git a/src/API/Mapping/AnimeWatchingStatus.php b/src/API/Mapping/AnimeWatchingStatus.php index 37fa3f88..d55c0c6a 100644 --- a/src/API/Mapping/AnimeWatchingStatus.php +++ b/src/API/Mapping/AnimeWatchingStatus.php @@ -2,21 +2,21 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ namespace Aviat\AnimeClient\API\Mapping; -use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\{Anilist, Kitsu, MAL, Route, Title}; +use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\{Anilist, Kitsu, Route, Title}; use Aviat\Ion\Enum; /** @@ -29,9 +29,9 @@ final class AnimeWatchingStatus extends Enum { Anilist::PLAN_TO_WATCH => Kitsu::PLAN_TO_WATCH, Anilist::COMPLETED => Kitsu::COMPLETED, Anilist::ON_HOLD => Kitsu::ON_HOLD, - Anilist::DROPPED => Kitsu::DROPPED + Anilist::DROPPED => Kitsu::DROPPED ]; - + const KITSU_TO_ANILIST = [ Kitsu::WATCHING => Anilist::WATCHING, Kitsu::PLAN_TO_WATCH => Anilist::PLAN_TO_WATCH, @@ -39,14 +39,6 @@ final class AnimeWatchingStatus extends Enum { Kitsu::ON_HOLD => Anilist::ON_HOLD, Kitsu::DROPPED => Anilist::DROPPED ]; - - const KITSU_TO_MAL = [ - Kitsu::WATCHING => MAL::WATCHING, - Kitsu::PLAN_TO_WATCH => MAL::PLAN_TO_WATCH, - Kitsu::COMPLETED => MAL::COMPLETED, - Kitsu::ON_HOLD => MAL::ON_HOLD, - Kitsu::DROPPED => MAL::DROPPED - ]; const KITSU_TO_TITLE = [ Kitsu::WATCHING => Title::WATCHING, @@ -56,14 +48,6 @@ final class AnimeWatchingStatus extends Enum { Kitsu::COMPLETED => Title::COMPLETED ]; - const MAL_TO_KITSU = [ - MAL::WATCHING => Kitsu::WATCHING, - MAL::PLAN_TO_WATCH => Kitsu::PLAN_TO_WATCH, - MAL::COMPLETED => Kitsu::COMPLETED, - MAL::ON_HOLD => Kitsu::ON_HOLD, - MAL::DROPPED => Kitsu::DROPPED - ]; - const ROUTE_TO_KITSU = [ Route::WATCHING => Kitsu::WATCHING, Route::PLAN_TO_WATCH => Kitsu::PLAN_TO_WATCH, diff --git a/src/API/Mapping/MangaReadingStatus.php b/src/API/Mapping/MangaReadingStatus.php index 1e182af6..197aa608 100644 --- a/src/API/Mapping/MangaReadingStatus.php +++ b/src/API/Mapping/MangaReadingStatus.php @@ -2,21 +2,21 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ namespace Aviat\AnimeClient\API\Mapping; -use Aviat\AnimeClient\API\Enum\MangaReadingStatus\{Anilist, Kitsu, MAL, Title, Route}; +use Aviat\AnimeClient\API\Enum\MangaReadingStatus\{Anilist, Kitsu, Title, Route}; use Aviat\Ion\Enum; /** @@ -29,9 +29,9 @@ final class MangaReadingStatus extends Enum { Anilist::PLAN_TO_READ => Kitsu::PLAN_TO_READ, Anilist::COMPLETED => Kitsu::COMPLETED, Anilist::ON_HOLD => Kitsu::ON_HOLD, - Anilist::DROPPED => Kitsu::DROPPED + Anilist::DROPPED => Kitsu::DROPPED ]; - + const KITSU_TO_ANILIST = [ Kitsu::READING => Anilist::READING, Kitsu::PLAN_TO_READ => Anilist::PLAN_TO_READ, @@ -39,28 +39,6 @@ final class MangaReadingStatus extends Enum { Kitsu::ON_HOLD => Anilist::ON_HOLD, Kitsu::DROPPED => Anilist::DROPPED ]; - - - const KITSU_TO_MAL = [ - Kitsu::READING => MAL::READING, - Kitsu::PLAN_TO_READ => MAL::PLAN_TO_READ, - Kitsu::COMPLETED => MAL::COMPLETED, - Kitsu::ON_HOLD => MAL::ON_HOLD, - Kitsu::DROPPED => MAL::DROPPED - ]; - - const MAL_TO_KITSU = [ - '1' => Kitsu::READING, - '2' => Kitsu::COMPLETED, - '3' => Kitsu::ON_HOLD, - '4' => Kitsu::DROPPED, - '6' => Kitsu::PLAN_TO_READ, - MAL::READING => Kitsu::READING, - MAL::COMPLETED => Kitsu::COMPLETED, - MAL::ON_HOLD => Kitsu::ON_HOLD, - MAL::DROPPED => Kitsu::DROPPED, - MAL::PLAN_TO_READ => Kitsu::PLAN_TO_READ, - ]; const KITSU_TO_TITLE = [ Kitsu::READING => Title::READING, diff --git a/src/API/ParallelAPIRequest.php b/src/API/ParallelAPIRequest.php index 877cf94a..e19778ce 100644 --- a/src/API/ParallelAPIRequest.php +++ b/src/API/ParallelAPIRequest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -63,9 +63,10 @@ final class ParallelAPIRequest { } /** - * Actually make the requests + * Make the requests, and return the body for each * * @return array + * @throws \Throwable */ public function makeRequests(): array { @@ -82,4 +83,25 @@ final class ParallelAPIRequest { return wait(all($promises)); } + + /** + * Make the requests and return the response objects + * + * @return array + * @throws \Throwable + */ + public function getResponses(): array + { + $client = new HummingbirdClient(); + $promises = []; + + foreach ($this->requests as $key => $url) + { + $promises[$key] = call(function () use ($client, $url) { + return yield $client->request($url); + }); + } + + return wait(all($promises)); + } } \ No newline at end of file diff --git a/src/API/XML.php b/src/API/XML.php index eeedf0c9..ed4126ba 100644 --- a/src/API/XML.php +++ b/src/API/XML.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/AnimeClient.php b/src/AnimeClient.php index 5383dabb..b4f15536 100644 --- a/src/AnimeClient.php +++ b/src/AnimeClient.php @@ -2,54 +2,290 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ namespace Aviat\AnimeClient; -use Yosymfony\Toml\Toml; +use Aviat\Ion\ConfigInterface; +use Yosymfony\Toml\{Toml, TomlBuilder}; -if ( ! \function_exists('Aviat\AnimeClient\loadToml')) +// ---------------------------------------------------------------------------- +//! TOML Functions +// ---------------------------------------------------------------------------- + +/** + * Load configuration options from .toml files + * + * @param string $path - Path to load config + * @return array + */ +function loadToml(string $path): array { - /** - * Load configuration options from .toml files - * - * @param string $path - Path to load config - * @return array - */ - function loadToml(string $path): array + $output = []; + $files = glob("{$path}/*.toml"); + + foreach ($files as $file) { - $output = []; - $files = glob("{$path}/*.toml"); - - foreach ($files as $file) + $key = str_replace('.toml', '', basename($file)); + if ($key === 'admin-override') { - $key = str_replace('.toml', '', basename($file)); - $toml = file_get_contents($file); - $config = Toml::parse($toml); - - if ($key === 'config') - { - foreach($config as $name => $value) - { - $output[$name] = $value; - } - - continue; - } - - $output[$key] = $config; + continue; } - return $output; + $config = Toml::parseFile($file); + + if ($key === 'config') + { + foreach($config as $name => $value) + { + $output[$name] = $value; + } + + continue; + } + + $output[$key] = $config; + } + + return $output; +} + +/** + * Load config from one specific TOML file + * + * @param string $filename + * @return array + */ +function loadTomlFile(string $filename): array +{ + return Toml::parseFile($filename); +} + +/** + * Recursively create a toml file from a data array + * + * @param TomlBuilder $builder + * @param iterable $data + * @param null $parentKey + */ +function _iterateToml(TomlBuilder $builder, iterable $data, $parentKey = NULL): void +{ + foreach ($data as $key => $value) + { + if ($value === NULL) + { + continue; + } + + + if (is_scalar($value) || isSequentialArray($value)) + { + // $builder->addTable(''); + $builder->addValue($key, $value); + continue; + } + + $newKey = ($parentKey !== NULL) + ? "{$parentKey}.{$key}" + : $key; + + if ( ! isSequentialArray($value)) + { + $builder->addTable($newKey); + } + + _iterateToml($builder, $value, $newKey); } } + +/** + * Serialize config data into a Toml file + * + * @param mixed $data + * @return string + */ +function arrayToToml(iterable $data): string +{ + $builder = new TomlBuilder(); + + _iterateToml($builder, $data); + + return $builder->getTomlString(); +} + +/** + * Serialize toml back to an array + * + * @param string $toml + * @return array + */ +function tomlToArray(string $toml): array +{ + return Toml::parse($toml); +} + +// ---------------------------------------------------------------------------- +//! Misc Functions +// ---------------------------------------------------------------------------- + +/** + * Is the array sequential, not associative? + * + * @param mixed $array + * @return bool + */ +function isSequentialArray($array): bool +{ + if ( ! is_array($array)) + { + return FALSE; + } + + $i = 0; + foreach ($array as $k => $v) + { + if ($k !== $i++) + { + return FALSE; + } + } + return TRUE; +} + +/** + * Check that folder permissions are correct for proper operation + * + * @param ConfigInterface $config + * @return array + */ +function checkFolderPermissions(ConfigInterface $config): array +{ + $errors = []; + $publicDir = $config->get('asset_dir'); + + $pathMap = [ + 'app/config' => realpath(__DIR__ . '/../app/config'), + 'app/logs' => realpath(__DIR__ . '/../app/logs'), + 'public/images/avatars' => "{$publicDir}/images/avatars", + 'public/images/anime' => "{$publicDir}/images/anime", + 'public/images/characters' => "{$publicDir}/images/characters", + 'public/images/manga' => "{$publicDir}/images/manga", + 'public/images/people' => "{$publicDir}/images/people", + ]; + + foreach ($pathMap as $pretty => $actual) + { + // Make sure the folder exists first + if ( ! is_dir($actual)) + { + $errors['missing'][] = $pretty; + continue; + } + + $writable = is_writable($actual) && is_executable($actual); + + if ( ! $writable) + { + $errors['writable'][] = $pretty; + } + } + + return $errors; +} + +/** + * Generate the path for the cached image from the original image + * + * @param string $kitsuUrl + * @param bool $webp + * @return string + */ +function getLocalImg ($kitsuUrl, $webp = TRUE): string +{ + if ( ! is_string($kitsuUrl)) + { + return 'images/placeholder.webp'; + } + + $parts = parse_url($kitsuUrl); + + if ($parts === FALSE) + { + return 'images/placeholder.webp'; + } + + $file = basename($parts['path']); + $fileParts = explode('.', $file); + $ext = array_pop($fileParts); + $ext = $webp ? 'webp' : $ext; + + $segments = explode('/', trim($parts['path'], '/')); + + $type = $segments[0] === 'users' ? $segments[1] : $segments[0]; + + $id = $segments[count($segments) - 2]; + + return implode('/', ['images', $type, "{$id}.{$ext}"]); +} + +/** + * Create a transparent placeholder image + * + * @param string $path + * @param int $width + * @param int $height + * @param string $text + */ +function createPlaceholderImage ($path, $width, $height, $text = 'Image Unavailable') +{ + $width = $width ?? 200; + $height = $height ?? 200; + + $img = imagecreatetruecolor($width, $height); + imagealphablending($img, TRUE); + + $path = rtrim($path, '/'); + + // Background is the first color by default + $fillColor = imagecolorallocatealpha($img, 255, 255, 255, 127); + imagefill($img, 0, 0, $fillColor); + + $textColor = imagecolorallocate($img, 64, 64, 64); + + imagealphablending($img, TRUE); + + // Generate placeholder text + $fontSize = 10; + $fontWidth = imagefontwidth($fontSize); + $fontHeight = imagefontheight($fontSize); + $length = strlen($text); + $textWidth = $length * $fontWidth; + $fxPos = (int) ceil((imagesx($img) - $textWidth) / 2); + $fyPos = (int) ceil((imagesy($img) - $fontHeight) / 2); + + // Add the image text + imagestring($img, $fontSize, $fxPos, $fyPos, $text, $textColor); + + // Save the images + imagesavealpha($img, TRUE); + imagepng($img, $path . '/placeholder.png', 9); + imagedestroy($img); + + $pngImage = imagecreatefrompng($path . '/placeholder.png'); + imagealphablending($pngImage, TRUE); + imagesavealpha($pngImage, TRUE); + + imagewebp($pngImage, $path . '/placeholder.webp'); + + imagedestroy($pngImage); +} \ No newline at end of file diff --git a/src/Command/BaseCommand.php b/src/Command/BaseCommand.php index ea06394d..dcdf2ef9 100644 --- a/src/Command/BaseCommand.php +++ b/src/Command/BaseCommand.php @@ -2,28 +2,32 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ namespace Aviat\AnimeClient\Command; use function Aviat\AnimeClient\loadToml; +use function Aviat\AnimeClient\loadTomlFile; +use Aura\Router\RouterContainer; use Aura\Session\SessionFactory; +use Aviat\AnimeClient\UrlGenerator; use Aviat\AnimeClient\Util; use Aviat\AnimeClient\API\CacheTrait; -use Aviat\AnimeClient\API\{Kitsu, MAL}; +use Aviat\AnimeClient\API\Anilist; +use Aviat\AnimeClient\API\Kitsu; use Aviat\AnimeClient\API\Kitsu\KitsuRequestBuilder; -use Aviat\AnimeClient\API\MAL\MALRequestBuilder; +use Aviat\AnimeClient\Model; use Aviat\Banker\Pool; use Aviat\Ion\Config; use Aviat\Ion\Di\{Container, ContainerAware}; @@ -31,6 +35,7 @@ use ConsoleKit\{Command, ConsoleException}; use ConsoleKit\Widgets\Box; use Monolog\Handler\RotatingFileHandler; use Monolog\Logger; +use Zend\Diactoros\{Response, ServerRequestFactory}; /** * Base class for console command setup @@ -70,12 +75,18 @@ class BaseCommand extends Command { $APP_DIR = realpath(__DIR__ . '/../../app'); $APPCONF_DIR = realpath("{$APP_DIR}/appConf/"); $CONF_DIR = realpath("{$APP_DIR}/config/"); - $base_config = require $APPCONF_DIR . '/base_config.php'; + $baseConfig = require $APPCONF_DIR . '/base_config.php'; $config = loadToml($CONF_DIR); - $config_array = array_merge($base_config, $config); - $di = function ($config_array) use ($APP_DIR) { + $overrideFile = $CONF_DIR . '/admin-override.toml'; + $overrideConfig = file_exists($overrideFile) + ? loadTomlFile($overrideFile) + : []; + + $configArray = array_replace_recursive($baseConfig, $config, $overrideConfig); + + $di = function ($configArray) use ($APP_DIR) { $container = new Container(); // ------------------------------------------------------------------------- @@ -86,15 +97,15 @@ class BaseCommand extends Command { $app_logger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/app-cli.log', Logger::NOTICE)); $kitsu_request_logger = new Logger('kitsu-request'); $kitsu_request_logger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/kitsu_request-cli.log', Logger::NOTICE)); - $mal_request_logger = new Logger('mal-request'); - $mal_request_logger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/mal_request-cli.log', Logger::NOTICE)); + $anilistRequestLogger = new Logger('anilist-request'); + $anilistRequestLogger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/anilist_request-cli.log', Logger::NOTICE)); $container->setLogger($app_logger); + $container->setLogger($anilistRequestLogger, 'anilist-request'); $container->setLogger($kitsu_request_logger, 'kitsu-request'); - $container->setLogger($mal_request_logger, 'mal-request'); // Create Config Object - $container->set('config', function() use ($config_array) { - return new Config($config_array); + $container->set('config', function() use ($configArray) { + return new Config($configArray); }); // Create Cache Object @@ -104,6 +115,25 @@ class BaseCommand extends Command { return new Pool($config, $logger); }); + // Create Aura Router Object + $container->set('aura-router', function() { + return new RouterContainer; + }); + + // Create Request/Response Objects + $container->set('request', function() { + return ServerRequestFactory::fromGlobals( + $_SERVER, + $_GET, + $_POST, + $_COOKIE, + $_FILES + ); + }); + $container->set('response', function() { + return new Response; + }); + // Create session Object $container->set('session', function() { return (new SessionFactory())->newInstance($_COOKIE); @@ -126,19 +156,33 @@ class BaseCommand extends Command { $model->setCache($cache); return $model; }); - $container->set('mal-model', function($container) { - $requestBuilder = new MALRequestBuilder(); - $requestBuilder->setLogger($container->getLogger('mal-request')); + $container->set('anilist-model', function ($container) { + $requestBuilder = new Anilist\AnilistRequestBuilder(); + $requestBuilder->setLogger($container->getLogger('anilist-request')); - $listItem = new MAL\ListItem(); + $listItem = new Anilist\ListItem(); $listItem->setContainer($container); $listItem->setRequestBuilder($requestBuilder); - $model = new MAL\Model($listItem); + $model = new Anilist\Model($listItem); $model->setContainer($container); $model->setRequestBuilder($requestBuilder); + return $model; }); + $container->set('settings-model', function($container) { + $model = new Model\Settings($container->get('config')); + $model->setContainer($container); + return $model; + }); + + $container->set('auth', function($container) { + return new Kitsu\Auth($container); + }); + + $container->set('url-generator', function($container) { + return new UrlGenerator($container); + }); $container->set('util', function($container) { return new Util($container); @@ -147,6 +191,6 @@ class BaseCommand extends Command { return $container; }; - return $di($config_array); + return $di($configArray); } } \ No newline at end of file diff --git a/src/Command/CacheClear.php b/src/Command/CacheClear.php index 61f98ba0..ac7b6538 100644 --- a/src/Command/CacheClear.php +++ b/src/Command/CacheClear.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/Command/CachePrime.php b/src/Command/CachePrime.php index 871f8f70..e265ae83 100644 --- a/src/Command/CachePrime.php +++ b/src/Command/CachePrime.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/Command/ClearThumbnails.php b/src/Command/ClearThumbnails.php new file mode 100644 index 00000000..b3345534 --- /dev/null +++ b/src/Command/ClearThumbnails.php @@ -0,0 +1,59 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Command; + +/** + * Clears out image cache directories + */ +class ClearThumbnails extends BaseCommand { + + public function execute(array $args, array $options = []): void + { + $this->clearThumbs(); + $this->echoBox('All cached images have been removed'); + } + + public function clearThumbs() + { + $imgDir = realpath(__DIR__ . '/../../public/images'); + + $paths = [ + 'avatars/*.gif', + 'avatars/*.jpg', + 'avatars/*.png', + 'avatars/*.webp', + 'anime/*.jpg', + 'anime/*.png', + 'anime/*.webp', + 'manga/*.jpg', + 'manga/*.png', + 'manga/*.webp', + 'characters/*.jpg', + 'characters/*.png', + 'characters/*.webp', + 'people/*.jpg', + 'people/*.png', + 'people/*.webp', + ]; + + foreach($paths as $path) + { + $cmd = "rm -rf {$imgDir}/{$path}"; + exec($cmd); + } + } +} \ No newline at end of file diff --git a/src/Command/MALIDCheck.php b/src/Command/MALIDCheck.php new file mode 100644 index 00000000..ac50b30a --- /dev/null +++ b/src/Command/MALIDCheck.php @@ -0,0 +1,241 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Command; + +use const Aviat\AnimeClient\SRC_DIR; + +use function Amp\Promise\wait; + +use Aviat\AnimeClient\API\{ + APIRequestBuilder, + JsonAPI, + ParallelAPIRequest +}; + +use Aviat\Ion\Json; + + +final class MALIDCheck extends BaseCommand { + + private $kitsuModel; + + /** + * Check MAL mapping validity + * + * @param array $args + * @param array $options + * @throws \Aviat\Ion\Di\Exception\ContainerException + * @throws \Aviat\Ion\Di\Exception\NotFoundException + * @throws \Throwable + */ + public function execute(array $args, array $options = []): void + { + $this->setContainer($this->setupContainer()); + $this->setCache($this->container->get('cache')); + $this->kitsuModel = $this->container->get('kitsu-model'); + + $kitsuAnimeIdList = $this->formatKitsuList('anime'); + $animeCount = count($kitsuAnimeIdList); + $this->echoBox("{$animeCount} mappings for Anime"); + $animeMappings = $this->checkMALIds($kitsuAnimeIdList, 'anime'); + $this->mappingStatus($animeMappings, $animeCount, 'anime'); + + $kitsuMangaIdList = $this->formatKitsuList('manga'); + $mangaCount = count($kitsuMangaIdList); + $this->echoBox("{$mangaCount} mappings for Manga"); + $mangaMappings = $this->checkMALIds($kitsuMangaIdList, 'manga'); + $this->mappingStatus($mangaMappings, $mangaCount, 'manga'); + + $publicDir = realpath(SRC_DIR . '/../public') . '/'; + file_put_contents($publicDir . 'mal_mappings.json', Json::encode([ + 'anime' => $animeMappings, + 'manga' => $mangaMappings, + ])); + + $this->echoBox('Mapping file saved to "' . $publicDir . 'mal_mappings.json' . '"'); + } + + /** + * Format a kitsu list for the sake of comparision + * + * @param string $type + * @return array + */ + private function formatKitsuList(string $type = 'anime'): array + { + $options = [ + 'include' => 'media,media.mappings', + ]; + $data = $this->kitsuModel->{'getFullRaw' . ucfirst($type) . 'List'}($options); + + if (empty($data)) + { + return []; + } + + $includes = JsonAPI::organizeIncludes($data['included']); + + // Only bother with mappings from MAL that are of the specified media type + $includes['mappings'] = array_filter($includes['mappings'], function ($mapping) use ($type) { + return $mapping['externalSite'] === "myanimelist/{$type}"; + }); + + $output = []; + + foreach ($data['data'] as $listItem) + { + $id = $listItem['relationships']['media']['data']['id']; + $mediaItem = $includes[$type][$id]; + + // Set titles + $listItem['titles'] = $mediaItem['titles']; + + $potentialMappings = $mediaItem['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 ($malId === NULL) + { + continue; + } + + // Group by malIds to simplify lookup of media details + // for checking validity of the malId mappings + $output[$malId] = $listItem; + } + + ksort($output); + + return $output; + } + + /** + * Check for valid Kitsu -> MAL mapping + * + * @param array $kitsuList + * @param string $type + * @return array + * @throws \Throwable + */ + private function checkMALIds(array $kitsuList, string $type): array + { + $goodMappings = []; + $badMappings = []; + $suspectMappings = []; + + $responses = $this->makeMALRequests(array_keys($kitsuList), $type); + + // If the page returns a 404, put it in the bad mappings list + // otherwise, do a search against the titles, to see if the mapping + // seems valid + foreach($responses as $id => $response) + { + $body = wait($response->getBody()); + $titles = $kitsuList[$id]['titles']; + + if ($response->getStatus() === 404) + { + $badMappings[$id] = $titles; + } + else + { + $titleMatches = FALSE; + + // Attempt to determine if the id matches + // By searching for a matching title + foreach($titles as $title) + { + if (empty($title)) + { + continue; + } + + if (mb_stripos($body, $title) !== FALSE) + { + $titleMatches = TRUE; + $goodMappings[$id] = $title; + + // Continue on outer loop + continue 2; + } + } + + if ( ! $titleMatches) + { + $suspectMappings[$id] = $titles; + } + else + { + $goodMappings[$id] = $titles; + } + } + } + + return [ + 'good' => $goodMappings, + 'bad' => $badMappings, + 'suspect' => $suspectMappings, + ]; + } + + private function makeMALRequests(array $ids, string $type): array + { + $baseUrl = "https://myanimelist.net/{$type}/"; + + $requestChunks = array_chunk($ids, 50, TRUE); + $responses = []; + + // Chunk parallel requests so that we don't hit rate + // limiting, and get spurious 404 HTML responses + foreach($requestChunks as $idChunk) + { + $requester = new ParallelAPIRequest(); + + foreach($idChunk as $id) + { + $request = APIRequestBuilder::simpleRequest($baseUrl . $id); + $requester->addRequest($request, (string)$id); + } + + foreach($requester->getResponses() as $id => $response) + { + $responses[$id] = $response; + } + } + + return $responses; + } + + private function mappingStatus(array $mapping, int $count, string $type): void + { + $good = count($mapping['good']); + $bad = count($mapping['bad']); + $suspect = count($mapping['suspect']); + + $uType = ucfirst($type); + + $this->echoBox("{$uType} mappings: {$good}/{$count} Good, {$suspect}/{$count} Suspect, {$bad}/{$count} Broken"); + } +} \ No newline at end of file diff --git a/src/Command/SyncLists.php b/src/Command/SyncLists.php index 4567a742..2b52b0ee 100644 --- a/src/Command/SyncLists.php +++ b/src/Command/SyncLists.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -19,21 +19,28 @@ namespace Aviat\AnimeClient\Command; use Aviat\AnimeClient\API\{ FailedResponseException, JsonAPI, - ParallelAPIRequest, - Mapping\AnimeWatchingStatus, - Mapping\MangaReadingStatus + ParallelAPIRequest }; -use Aviat\AnimeClient\API\MAL\Transformer\{ - AnimeListTransformer as ALT +use Aviat\AnimeClient\API\Anilist\Transformer\{ + AnimeListTransformer as AALT, + MangaListTransformer as AMLT }; +use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus}; +use Aviat\AnimeClient\Types\FormItem; use Aviat\Ion\Json; use DateTime; /** - * Clears the API Cache + * Syncs list data between Anilist and Kitsu */ final class SyncLists extends BaseCommand { + /** + * Model for making requests to Anilist API + * @var \Aviat\AnimeClient\API\Anilist\Model + */ + protected $anilistModel; + /** * Model for making requests to Kitsu API * @var \Aviat\AnimeClient\API\Kitsu\Model @@ -41,53 +48,47 @@ final class SyncLists extends BaseCommand { protected $kitsuModel; /** - * Model for making requests to MAL API - * @var \Aviat\AnimeClient\API\MAL\Model - */ - protected $malModel; - - /** - * Run the Kitsu <=> MAL sync script + * Run the Kitsu <=> Anilist sync script * * @param array $args * @param array $options - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException - * @return void + * @throws \Aviat\Ion\Di\Exception\ContainerException + * @throws \Aviat\Ion\Di\Exception\NotFoundException + * @throws \Throwable */ public function execute(array $args, array $options = []): void { $this->setContainer($this->setupContainer()); $this->setCache($this->container->get('cache')); + + $config = $this->container->get('config'); + $anilistEnabled = $config->get(['anilist', 'enabled']); + + if ( ! $anilistEnabled) + { + $this->echoBox('Anlist API is not enabled. Can not sync.'); + return; + } + + $this->anilistModel = $this->container->get('anilist-model'); $this->kitsuModel = $this->container->get('kitsu-model'); - $this->malModel = $this->container->get('mal-model'); $this->sync('anime'); $this->sync('manga'); + + $this->echoBox('Finished syncing lists'); } /** - * Attempt to synchronize external apis + * Attempt to synchronize external APIs * - * @param string $type anime|manga - * @return void + * @param string $type + * @throws \Throwable */ protected function sync(string $type): void { $uType = ucfirst($type); - // Do a little check to make sure you don't have immediate issues - // if you have 0 or 1 items in a list on MAL. - $malList = $this->malModel->getList($type); - $malCount = 0; - if ( ! empty($malList)) - { - $malCount = count(array_key_exists(0, $malList) - ? $malList - : [$malList] - ); - } - $kitsuCount = 0; try { @@ -99,23 +100,22 @@ final class SyncLists extends BaseCommand { } - $this->echoBox("Number of MAL {$type} list items: {$malCount}"); $this->echoBox("Number of Kitsu {$type} list items: {$kitsuCount}"); $data = $this->diffLists($type); - if ( ! empty($data['addToMAL'])) + if ( ! empty($data['addToAnilist'])) { - $count = count($data['addToMAL']); - $this->echoBox("Adding {$count} missing {$type} list items to MAL"); - $this->updateMALListItems($data['addToMAL'], 'create', $type); + $count = count($data['addToAnilist']); + $this->echoBox("Adding {$count} missing {$type} list items to Anilist"); + $this->updateAnilistListItems($data['addToAnilist'], 'create', $type); } - if ( ! empty($data['updateMAL'])) + if ( ! empty($data['updateAnilist'])) { - $count = count($data['updateMAL']); - $this->echoBox("Updating {$count} outdated MAL {$type} list items"); - $this->updateMALListItems($data['updateMAL'], 'update', $type); + $count = count($data['updateAnilist']); + $this->echoBox("Updating {$count} outdated Anilist {$type} list items"); + $this->updateAnilistListItems($data['updateAnilist'], 'update', $type); } if ( ! empty($data['addToKitsu'])) @@ -156,104 +156,80 @@ final class SyncLists extends BaseCommand { } /** - * Format a MAL list for comparison + * Format an Anilist list for comparison * * @param string $type * @return array */ - protected function formatMALList(string $type): array + protected function formatAnilistList(string $type): array { $type = ucfirst($type); - $method = "formatMAL{$type}List"; + $method = "formatAnilist{$type}List"; return $this->$method(); } /** - * Format a MAL anime list for comparison + * Format an Anilist anime list for comparison * * @return array + * @throws \Aviat\Ion\Di\Exception\ContainerException + * @throws \Aviat\Ion\Di\Exception\NotFoundException */ - protected function formatMALAnimeList(): array + protected function formatAnilistAnimeList(): array { - $orig = $this->malModel->getList('anime'); + $anilistList = $this->anilistModel->getSyncList('ANIME'); + $anilistTransformer = new AALT(); + + $transformedAnilist = []; + + foreach ($anilistList['data']['MediaListCollection']['lists'] as $list) + { + $newTransformed = $anilistTransformer->untransformCollection($list['entries']); + $transformedAnilist = array_merge($transformedAnilist, $newTransformed); + } + + // Key the array by the mal_id for easier reference in the next comparision step $output = []; - - // Bail early on empty list - if (empty($orig)) + foreach ($transformedAnilist as $item) { - return []; + $output[$item['mal_id']] = $item->toArray(); } - // Due to xml parsing differences, - // 1 item has no wrapping array. - // In this case, just re-create the - // wrapper array - if ( ! array_key_exists(0, $orig)) - { - $orig = [$orig]; - } - - foreach($orig as $item) - { - $output[$item['series_animedb_id']] = [ - 'id' => $item['series_animedb_id'], - 'data' => [ - 'status' => AnimeWatchingStatus::MAL_TO_KITSU[$item['my_status']], - 'progress' => $item['my_watched_episodes'], - 'reconsuming' => (bool) $item['my_rewatching'], - 'rating' => $item['my_score'] / 2, - 'updatedAt' => (new \DateTime()) - ->setTimestamp((int)$item['my_last_updated']) - ->format(\DateTime::W3C), - ] - ]; - } + $count = count($output); + $this->echoBox("Number of Anilist anime list items: {$count}"); return $output; } /** - * Format a MAL manga list for comparison + * Format an Anilist manga list for comparison * * @return array + * @throws \Aviat\Ion\Di\Exception\ContainerException + * @throws \Aviat\Ion\Di\Exception\NotFoundException */ - protected function formatMALMangaList(): array + protected function formatAnilistMangaList(): array { - $orig = $this->malModel->getList('manga'); + $anilistList = $this->anilistModel->getSyncList('MANGA'); + $anilistTransformer = new AMLT(); + + $transformedAnilist = []; + + foreach ($anilistList['data']['MediaListCollection']['lists'] as $list) + { + $newTransformed = $anilistTransformer->untransformCollection($list['entries']); + $transformedAnilist = array_merge($transformedAnilist, $newTransformed); + } + + // Key the array by the mal_id for easier reference in the next comparision step $output = []; - - // Bail early on empty list - if (empty($orig)) + foreach ($transformedAnilist as $item) { - return []; + $output[$item['mal_id']] = $item->toArray(); } - // Due to xml parsing differences, - // 1 item has no wrapping array. - // In this case, just re-create the - // wrapper array - if ( ! array_key_exists(0, $orig)) - { - $orig = [$orig]; - } - - foreach($orig as $item) - { - $output[$item['series_mangadb_id']] = [ - 'id' => $item['series_mangadb_id'], - 'data' => [ - 'my_status' => $item['my_status'], - 'status' => MangaReadingStatus::MAL_TO_KITSU[$item['my_status']], - 'progress' => $item['my_read_chapters'], - 'volumes' => $item['my_read_volumes'], - 'reconsuming' => (bool) $item['my_rereadingg'], - 'rating' => $item['my_score'] / 2, - 'updatedAt' => (new \DateTime()) - ->setTimestamp((int)$item['my_last_updated']) - ->format(\DateTime::W3C), - ] - ]; - } + $count = count($output); + $this->echoBox("Number of Anilist manga list items: {$count}"); return $output; } @@ -266,7 +242,8 @@ final class SyncLists extends BaseCommand { */ protected function formatKitsuList(string $type = 'anime'): array { - $data = $this->kitsuModel->{'getFull' . ucfirst($type) . 'List'}(); + $method = 'getFullRaw' . ucfirst($type) . 'List'; + $data = $this->kitsuModel->$method(); if (empty($data)) { @@ -293,7 +270,7 @@ final class SyncLists extends BaseCommand { } } - // Skip to the next item if there isn't a MAL ID + // Skip to the next item if there isn't a Anilist ID if ($malId === NULL) { continue; @@ -321,31 +298,50 @@ final class SyncLists extends BaseCommand { // Organize mappings, and ignore entries without mappings $kitsuList = $this->formatKitsuList($type); - // Get MAL list data - $malList = $this->formatMALList($type); + // Get Anilist list data + $anilistList = $this->formatAnilistList($type); - $itemsToAddToMAL = []; + $itemsToAddToAnilist = []; $itemsToAddToKitsu = []; - $malUpdateItems = []; + $anilistUpdateItems = []; $kitsuUpdateItems = []; - $malIds = array_column($malList, 'id'); - $kitsuMalIds = array_column($kitsuList, 'malId'); + $malBlackList = ($type === 'anime') + ? [ + 27821, // Fate/stay night: Unlimited Blade Works - Prologue + 29317, // Saekano: How to Raise a Boring Girlfriend Prologue + 30514, // Nisekoinogatari + ] : [ + 114638, // Cells at Work: Black + ]; + + $malIds = array_keys($anilistList); + $kitsuMalIds = array_map('intval', array_column($kitsuList, 'malId')); $missingMalIds = array_diff($malIds, $kitsuMalIds); + $missingMalIds = array_diff($missingMalIds, $malBlackList); foreach($missingMalIds as $mid) { - $itemsToAddToKitsu[] = array_merge($malList[$mid]['data'], [ - 'id' => $this->kitsuModel->getKitsuIdFromMALId($mid, $type), + $itemsToAddToKitsu[] = array_merge($anilistList[$mid]['data'], [ + 'id' => $this->kitsuModel->getKitsuIdFromMALId((string)$mid, $type), 'type' => $type ]); } foreach($kitsuList as $kitsuItem) { - if (\in_array($kitsuItem['malId'], $malIds, TRUE)) + $malId = $kitsuItem['malId']; + + if (\in_array((int)$malId, $malBlackList, TRUE)) { - $item = $this->compareListItems($kitsuItem, $malList[$kitsuItem['malId']]); + continue; + } + + if (array_key_exists($malId, $anilistList)) + { + $anilistItem = $anilistList[$malId]; + + $item = $this->compareListItems($kitsuItem, $anilistItem); if ($item === NULL) { @@ -357,25 +353,35 @@ final class SyncLists extends BaseCommand { $kitsuUpdateItems[] = $item['data']; } - if (\in_array('mal', $item['updateType'], TRUE)) + if (\in_array('anilist', $item['updateType'], TRUE)) { - $malUpdateItems[] = $item['data']; + $anilistUpdateItems[] = $item['data']; } continue; } - // Looks like this item only exists on Kitsu - $itemsToAddToMAL[] = [ - 'mal_id' => $kitsuItem['malId'], - 'data' => $kitsuItem['data'] - ]; + $statusMap = ($type === 'anime') ? AnimeWatchingStatus::class : MangaReadingStatus::class; + // Looks like this item only exists on Kitsu + $kItem = $kitsuItem['data']; + $newItemStatus = ($kItem['reconsuming'] === true) ? 'REPEATING' : $statusMap::KITSU_TO_ANILIST[$kItem['status']]; + $itemsToAddToAnilist[] = [ + 'mal_id' => $malId, + 'data' => [ + 'notes' => $kItem['notes'], + 'private' => $kItem['private'], + 'progress' => $kItem['progress'], + 'repeat' => $kItem['reconsumeCount'], + 'score' => $kItem['ratingTwenty'] * 5, // 100 point score on Anilist + 'status' => $newItemStatus, + ], + ]; } return [ - 'addToMAL' => $itemsToAddToMAL, - 'updateMAL' => $malUpdateItems, + 'addToAnilist' => $itemsToAddToAnilist, + 'updateAnilist' => $anilistUpdateItems, 'addToKitsu' => $itemsToAddToKitsu, 'updateKitsu' => $kitsuUpdateItems ]; @@ -385,18 +391,28 @@ final class SyncLists extends BaseCommand { * Compare two list items, and return the out of date one, if one exists * * @param array $kitsuItem - * @param array $malItem + * @param array $anilistItem * @return array|null */ - protected function compareListItems(array $kitsuItem, array $malItem): ?array + protected function compareListItems(array $kitsuItem, array $anilistItem): ?array { - $compareKeys = ['status', 'progress', 'rating', 'reconsuming']; + $compareKeys = [ + 'notes', + 'progress', + 'rating', + 'reconsumeCount', + 'reconsuming', + 'status', + ]; $diff = []; - $dateDiff = new DateTime($kitsuItem['data']['updatedAt']) <=> new DateTime($malItem['data']['updatedAt']); + $dateDiff = new DateTime($kitsuItem['data']['updatedAt']) <=> new DateTime((string)$anilistItem['data']['updatedAt']); + + // Correct differences in notation + $kitsuItem['data']['rating'] = $kitsuItem['data']['ratingTwenty'] / 2; foreach($compareKeys as $key) { - $diff[$key] = $kitsuItem['data'][$key] <=> $malItem['data'][$key]; + $diff[$key] = $kitsuItem['data'][$key] <=> $anilistItem['data'][$key]; } // No difference? Bail out early @@ -416,10 +432,18 @@ final class SyncLists extends BaseCommand { 'updateType' => [] ]; + $sameNotes = $diff['notes'] === 0; $sameStatus = $diff['status'] === 0; $sameProgress = $diff['progress'] === 0; $sameRating = $diff['rating'] === 0; + $sameRewatchCount = $diff['reconsumeCount'] === 0; + // If an item is completed, make sure the 'reconsuming' flag is false + if ($kitsuItem['data']['status'] === 'completed' && $kitsuItem['data']['reconsuming'] === TRUE) + { + $update['data']['reconsuming'] = FALSE; + $return['updateType'][] = 'kitsu'; + } // If status is the same, and progress count is different, use greater progress if ($sameStatus && ( ! $sameProgress)) @@ -427,11 +451,25 @@ final class SyncLists extends BaseCommand { if ($diff['progress'] === 1) { $update['data']['progress'] = $kitsuItem['data']['progress']; - $return['updateType'][] = 'mal'; + $return['updateType'][] = 'anilist'; } else if($diff['progress'] === -1) { - $update['data']['progress'] = $malItem['data']['progress']; + $update['data']['progress'] = $anilistItem['data']['progress']; + $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'][] = 'anilist'; + } else if ($dateDiff === -1) + { + $update['data']['status'] = $anilistItem['data']['status']; $return['updateType'][] = 'kitsu'; } } @@ -449,13 +487,13 @@ final class SyncLists extends BaseCommand { $update['data']['progress'] = $kitsuItem['data']['progress']; } - $return['updateType'][] = 'mal'; + $return['updateType'][] = 'anilist'; } else if($dateDiff === -1) { - $update['data']['status'] = $malItem['data']['status']; + $update['data']['status'] = $anilistItem['data']['status']; - if ((int)$malItem['data']['progress'] !== 0) + if ((int)$anilistItem['data']['progress'] !== 0) { $update['data']['progress'] = $kitsuItem['data']['progress']; } @@ -464,44 +502,78 @@ final class SyncLists extends BaseCommand { } } - // If rating is different, use the rating from the item most recently updated + // Use the first set rating, otherwise use the newer rating if ( ! $sameRating) { - if ($dateDiff === 1) + if ($kitsuItem['data']['rating'] !== 0 && $dateDiff === 1) { - $update['data']['rating'] = $kitsuItem['data']['rating']; - $return['updateType'][] = 'mal'; + $update['data']['ratingTwenty'] = $kitsuItem['data']['ratingTwenty']; + $return['updateType'][] = 'anilist'; } - else if ($dateDiff === -1) + else if($dateDiff === -1) { - $update['data']['rating'] = $malItem['data']['rating']; + $update['data']['ratingTwenty'] = $anilistItem['data']['rating'] * 2; $return['updateType'][] = 'kitsu'; } } - // If status is different, use the status of the more recently updated item - if ( ! $sameStatus) + // If notes are set, use kitsu, otherwise, set kitsu from anilist + if ( ! $sameNotes) { - if ($dateDiff === 1) + if ($kitsuItem['data']['notes'] !== '') { - $update['data']['status'] = $kitsuItem['data']['status']; - $return['updateType'][] = 'mal'; + $update['data']['notes'] = $kitsuItem['data']['notes']; + $return['updateType'][] = 'anilist'; } - else if ($dateDiff === -1) + else { - $update['data']['status'] = $malItem['data']['status']; + $update['data']['notes'] = $anilistItem['data']['notes']; + $return['updateType'][] = 'kitsu'; + } + } + + // Assume the larger reconsumeCount is correct + if ( ! $sameRewatchCount) + { + if ($diff['reconsumeCount'] === 1) + { + $update['data']['reconsumeCount'] = $kitsuItem['data']['reconsumeCount']; + $return['updateType'][] = 'anilist'; + } + else if ($diff['reconsumeCount'] === -1) + { + $update['data']['reconsumeCount'] = $anilistItem['data']['reconsumeCount']; $return['updateType'][] = 'kitsu'; } } $return['meta'] = [ 'kitsu' => $kitsuItem['data'], - 'mal' => $malItem['data'], + 'anilist' => $anilistItem['data'], 'dateDiff' => $dateDiff, 'diff' => $diff, ]; $return['data'] = $update; $return['updateType'] = array_unique($return['updateType']); + + // Fill in missing data values for update on Anlist + // so I don't have to create a really complex graphql query + // to handle each combination of fields + if ($return['updateType'][0] === 'anilist') + { + $prevData = [ + 'notes' => $kitsuItem['data']['notes'], + 'private' => $kitsuItem['data']['private'], + 'progress' => $kitsuItem['data']['progress'], + 'rating' => $kitsuItem['data']['ratingTwenty'] * 5, + 'reconsumeCount' => $kitsuItem['data']['reconsumeCount'], + 'reconsuming' => $kitsuItem['data']['reconsuming'], + 'status' => $kitsuItem['data']['status'], + ]; + + $return['data']['data'] = array_merge($prevData, $return['data']['data']); + } + return $return; } @@ -511,6 +583,7 @@ final class SyncLists extends BaseCommand { * @param array $itemsToUpdate * @param string $action * @param string $type + * @throws \Throwable */ protected function updateKitsuListItems(array $itemsToUpdate, string $action = 'update', string $type = 'anime'): void { @@ -519,7 +592,9 @@ final class SyncLists extends BaseCommand { { if ($action === 'update') { - $requester->addRequest($this->kitsuModel->updateListItem($item)); + $requester->addRequest( + $this->kitsuModel->updateListItem(new FormItem($item)) + ); } else if ($action === 'create') { @@ -549,27 +624,28 @@ final class SyncLists extends BaseCommand { } /** - * Create/Update list items on MAL + * Create/Update list items on Anilist * * @param array $itemsToUpdate * @param string $action * @param string $type + * @throws \Throwable */ - protected function updateMALListItems(array$itemsToUpdate, string $action = 'update', string $type = 'anime'): void + protected function updateAnilistListItems(array $itemsToUpdate, string $action = 'update', string $type = 'anime'): void { - $transformer = new ALT(); $requester = new ParallelAPIRequest(); foreach($itemsToUpdate as $item) { if ($action === 'update') { - $requester->addRequest($this->malModel->updateListItem($item, $type)); + $requester->addRequest( + $this->anilistModel->updateListItem(new FormItem($item), $type) + ); } else if ($action === 'create') { - $data = $transformer->untransform($item); - $requester->addRequest($this->malModel->createFullListItem($data, $type)); + $requester->addRequest($this->anilistModel->createFullListItem($item, $type)); } } @@ -578,19 +654,20 @@ final class SyncLists extends BaseCommand { foreach($responses as $key => $response) { $id = $itemsToUpdate[$key]['mal_id']; - $goodResponse = ( - ($action === 'update' && $response === 'Updated') || - ($action === 'create' && $response === 'Created') - ); - if ($goodResponse) + + $responseData = Json::decode($response); + + // $id = $itemsToUpdate[$key]['id']; + if ( ! array_key_exists('errors', $responseData)) { $verb = ($action === 'update') ? 'updated' : 'created'; - $this->echoBox("Successfully {$verb} MAL {$type} list item with id: {$id}"); + $this->echoBox("Successfully {$verb} Anilist {$type} list item with id: {$id}"); } else { + dump($responseData); $verb = ($action === 'update') ? 'update' : 'create'; - $this->echoBox("Failed to {$verb} MAL {$type} list item with id: {$id}"); + $this->echoBox("Failed to {$verb} Anilist {$type} list item with id: {$id}"); } } } diff --git a/src/Command/UpdateThumbnails.php b/src/Command/UpdateThumbnails.php new file mode 100644 index 00000000..7291eeb6 --- /dev/null +++ b/src/Command/UpdateThumbnails.php @@ -0,0 +1,83 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Command; + +use Aviat\AnimeClient\API\JsonAPI; +use Aviat\AnimeClient\Controller\Index; + +/** + * Clears out image cache directories, then re-creates the image cache + * for manga and anime + */ +final class UpdateThumbnails extends ClearThumbnails { + /** + * Model for making requests to Kitsu API + * @var \Aviat\AnimeClient\API\Kitsu\Model + */ + protected $kitsuModel; + + /** + * The default controller, which has the method to cache the images + */ + protected $controller; + + public function execute(array $args, array $options = []): void + { + $this->setContainer($this->setupContainer()); + $this->setCache($this->container->get('cache')); + + $this->controller = new Index($this->container); + $this->kitsuModel = $this->container->get('kitsu-model'); + + // Clear the existing thunbnails + parent::execute($args, $options); + + $ids = $this->getImageList(); + + // Resave the images + foreach($ids as $type => $typeIds) + { + foreach ($typeIds as $id) + { + $this->controller->images($type, "{$id}.jpg", FALSE); + } + + $this->echoBox("Finished regenerating {$type} thumbnails"); + } + + $this->echoBox('Finished regenerating all thumbnails'); + } + + public function getImageList() + { + $mangaList = $this->kitsuModel->getFullRawMangaList(); + $includes = JsonAPI::organizeIncludes($mangaList['included']); + $mangaIds = array_keys($includes['manga']); + + $animeList = $this->kitsuModel->getFullRawAnimeList(); + $includes = JsonAPI::organizeIncludes($animeList['included']); + $animeIds = array_keys($includes['anime']); + + // print_r($mangaIds); + // die(); + + return [ + 'anime' => $animeIds, + 'manga' => $mangaIds, + ]; + } +} \ No newline at end of file diff --git a/src/Controller.php b/src/Controller.php index f06f2e38..08c334a7 100644 --- a/src/Controller.php +++ b/src/Controller.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -86,11 +86,11 @@ class Controller { ]; /** - * Constructor + * Controller constructor. * - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException * @param ContainerInterface $container + * @throws \Aviat\Ion\Di\Exception\ContainerException + * @throws \Aviat\Ion\Di\Exception\NotFoundException */ public function __construct(ContainerInterface $container) { @@ -140,10 +140,9 @@ class Controller { /** * Set the current url in the session as the target of a future redirect * - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException - * @param string|null $url - * @return void + * @param string|NULL $url + * @throws \Aviat\Ion\Di\Exception\ContainerException + * @throws \Aviat\Ion\Di\Exception\NotFoundException */ public function setSessionRedirect(string $url = NULL): void { @@ -180,8 +179,8 @@ class Controller { * Redirect to the url previously set in the session * * @throws InvalidArgumentException - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException + * @throws \Aviat\Ion\Di\Exception\ContainerException + * @throws \Aviat\Ion\Di\Exception\NotFoundException * @return void */ public function sessionRedirect() @@ -205,8 +204,8 @@ class Controller { * @param string $template * @param array $data * @throws InvalidArgumentException - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException + * @throws \Aviat\Ion\Di\Exception\ContainerException + * @throws \Aviat\Ion\Di\Exception\NotFoundException * @return string */ protected function loadPartial($view, string $template, array $data = []) @@ -229,7 +228,7 @@ class Controller { throw new InvalidArgumentException("Invalid template : {$template}"); } - return $view->renderTemplate($templatePath, (array)$data); + return $view->renderTemplate($templatePath, $data); } /** @@ -239,8 +238,8 @@ class Controller { * @param string $template * @param array $data * @throws InvalidArgumentException - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException + * @throws \Aviat\Ion\Di\Exception\ContainerException + * @throws \Aviat\Ion\Di\Exception\NotFoundException * @return void */ protected function renderFullPage($view, string $template, array $data) @@ -269,8 +268,8 @@ class Controller { * @param string $title * @param string $message * @throws InvalidArgumentException - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException + * @throws \Aviat\Ion\Di\Exception\ContainerException + * @throws \Aviat\Ion\Di\Exception\NotFoundException * @return void */ public function notFound( @@ -292,8 +291,8 @@ class Controller { * @param string $message * @param string $long_message * @throws InvalidArgumentException - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException + * @throws \Aviat\Ion\Di\Exception\ContainerException + * @throws \Aviat\Ion\Di\Exception\NotFoundException * @return void */ public function errorPage(int $httpCode, string $title, string $message, string $long_message = ''): void @@ -313,7 +312,7 @@ class Controller { */ public function redirectToDefaultRoute(): void { - $defaultType = $this->config->get(['routes', 'route_config', 'default_list']) ?? 'anime'; + $defaultType = $this->config->get('default_list'); $this->redirect($this->urlGenerator->defaultUrl($defaultType), 303); } @@ -345,7 +344,7 @@ class Controller { /** * Helper for consistent page titles * - * @param string[] ...$parts Title segments + * @param string[] $parts Title segments * @return string */ public function formatTitle(string ...$parts) : string @@ -360,8 +359,8 @@ class Controller { * @param string $type * @param string $message * @throws InvalidArgumentException - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException + * @throws \Aviat\Ion\Di\Exception\ContainerException + * @throws \Aviat\Ion\Di\Exception\NotFoundException * @return string */ protected function showMessage($view, string $type, string $message): string @@ -380,8 +379,8 @@ class Controller { * @param HtmlView|null $view * @param int $code * @throws InvalidArgumentException - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException + * @throws \Aviat\Ion\Di\Exception\ContainerException + * @throws \Aviat\Ion\Di\Exception\NotFoundException * @return void */ protected function outputHTML(string $template, array $data = [], $view = NULL, int $code = 200) @@ -393,6 +392,7 @@ class Controller { $view->setStatusCode($code); $this->renderFullPage($view, $template, $data); + exit(); } /** @@ -407,8 +407,9 @@ class Controller { { (new JsonView($this->container)) ->setStatusCode($code) - ->setOutput($data) - ->send(); + ->setOutput($data); + // ->send(); + exit(); } /** diff --git a/src/Controller/Anime.php b/src/Controller/Anime.php index 4fec2e7f..86e6a9d5 100644 --- a/src/Controller/Anime.php +++ b/src/Controller/Anime.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -20,7 +20,7 @@ use Aviat\AnimeClient\Controller as BaseController; use Aviat\AnimeClient\API\Kitsu\Transformer\AnimeListTransformer; use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\Kitsu as KitsuWatchingStatus; use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus; -use Aviat\AnimeClient\Types\AnimeFormItem; +use Aviat\AnimeClient\Types\FormItem; use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Json; use Aviat\Ion\StringWrapper; @@ -127,9 +127,15 @@ final class Anime extends BaseController { public function add(): void { $data = $this->request->getParsedBody(); + + if (empty($data['mal_id'])) + { + unset($data['mal_id']); + } + if ( ! array_key_exists('id', $data)) { - $this->redirect("anime/add", 303); + $this->redirect('anime/add', 303); } $result = $this->model->createLibraryItem($data); @@ -150,14 +156,10 @@ final class Anime extends BaseController { /** * Form to edit details about a series * - * @param int $id + * @param string $id * @param string $status - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException - * @throws \InvalidArgumentException - * @return void */ - public function edit($id, $status = 'all'): void + public function edit(string $id, $status = 'all'): void { $item = $this->model->getLibraryItem($id); $this->setSessionRedirect(); @@ -202,7 +204,7 @@ final class Anime extends BaseController { // large form-based updates $transformer = new AnimeListTransformer(); $postData = $transformer->untransform($data); - $fullResult = $this->model->updateLibraryItem(new AnimeFormItem($postData)); + $fullResult = $this->model->updateLibraryItem(new FormItem($postData)); if ($fullResult['statusCode'] === 200) { @@ -218,11 +220,11 @@ final class Anime extends BaseController { } /** - * Update an anime item + * Increase the watched count for an anime item * * @return void */ - public function update(): void + public function increment(): void { if (stripos($this->request->getHeader('content-type')[0], 'application/json') !== FALSE) { @@ -233,7 +235,7 @@ final class Anime extends BaseController { $data = $this->request->getParsedBody(); } - $response = $this->model->updateLibraryItem(new AnimeFormItem($data)); + $response = $this->model->incrementLibraryItem(new FormItem($data)); $this->cache->clear(); $this->outputJSON($response['body'], $response['statusCode']); @@ -242,8 +244,6 @@ final class Anime extends BaseController { /** * Remove an anime from the list * - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException * @return void */ public function delete(): void @@ -275,10 +275,11 @@ final class Anime extends BaseController { */ public function details(string $animeId): void { - $show_data = $this->model->getAnime($animeId); + $data = $this->model->getAnime($animeId); $characters = []; + $staff = []; - if ($show_data->title === '') + if (empty($data)) { $this->notFound( $this->config->get('whose_list') . @@ -290,22 +291,77 @@ final class Anime extends BaseController { return; } - if (array_key_exists('characters', $show_data['included'])) + if (array_key_exists('animeCharacters', $data['included'])) { - foreach($show_data['included']['characters'] as $id => $character) + $animeCharacters = $data['included']['animeCharacters']; + + foreach ($animeCharacters as $rel) { - $characters[$id] = $character['attributes']; + $charId = $rel['relationships']['character']['data']['id']; + $role = $rel['role']; + + if (array_key_exists($charId, $data['included']['characters'])) + { + $characters[$role][$charId] = $data['included']['characters'][$charId]; + } } } + if (array_key_exists('mediaStaff', $data['included'])) + { + foreach ($data['included']['mediaStaff'] as $id => $staffing) + { + $personId = $staffing['relationships']['person']['data']['id']; + $personDetails = $data['included']['people'][$personId]; + + $role = $staffing['role']; + + if ( ! array_key_exists($role, $staff)) + { + $staff[$role] = []; + } + + $staff[$role][$personId] = [ + 'id' => $personId, + 'name' => $personDetails['name'] ?? '??', + 'image' => $personDetails['image'], + ]; + + usort($staff[$role], function ($a, $b) { + return $a['name'] <=> $b['name']; + }); + } + } + + if ( ! empty($characters['main'])) + { + uasort($characters['main'], function ($a, $b) { + return $a['name'] <=> $b['name']; + }); + } + + if ( ! empty($characters['supporting'])) + { + uasort($characters['supporting'], function ($a, $b) { + return $a['name'] <=> $b['name']; + }); + } + + ksort($characters); + ksort($staff); + + // dump($characters); + // dump($staff); + $this->outputHTML('anime/details', [ 'title' => $this->formatTitle( $this->config->get('whose_list') . "'s Anime List", 'Anime', - $show_data->title + $data->title ), 'characters' => $characters, - 'show_data' => $show_data, + 'show_data' => $data, + 'staff' => $staff, ]); } diff --git a/src/Controller/AnimeCollection.php b/src/Controller/AnimeCollection.php index 094f2120..99da14c2 100644 --- a/src/Controller/AnimeCollection.php +++ b/src/Controller/AnimeCollection.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/Controller/Character.php b/src/Controller/Character.php index d14b099a..cff88071 100644 --- a/src/Controller/Character.php +++ b/src/Controller/Character.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -23,7 +23,7 @@ use Aviat\Ion\ArrayWrapper; /** * Controller for character description pages */ -final class Character extends BaseController { +class Character extends BaseController { use ArrayWrapper; @@ -57,6 +57,31 @@ final class Character extends BaseController { $data = JsonAPI::organizeData($rawData); + $data['names'] = array_unique( + array_merge( + [ $data[0]['attributes']['canonicalName'] ], + $data[0]['attributes']['names'] + ) + ); + $data['name'] = array_shift($data['names']); + + if (array_key_exists('included', $data)) + { + if (array_key_exists('anime', $data['included'])) + { + uasort($data['included']['anime'], function ($a, $b) { + return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle']; + }); + } + + if (array_key_exists('manga', $data['included'])) + { + uasort($data['included']['manga'], function ($a, $b) { + return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle']; + }); + } + } + $viewData = [ 'title' => $this->formatTitle( 'Characters', @@ -67,13 +92,16 @@ final class Character extends BaseController { 'castings' => [] ]; - if (array_key_exists('included', $data) && array_key_exists('castings', $data['included'])) + if (array_key_exists('included', $data)) { - $viewData['castings'] = $this->organizeCast($data['included']['castings']); - $viewData['castCount'] = $this->getCastCount($viewData['castings']); + if (array_key_exists('castings', $data['included'])) + { + $viewData['castings'] = $this->organizeCast($data['included']['castings']); + $viewData['castCount'] = $this->getCastCount($viewData['castings']); + } } - $this->outputHTML('character', $viewData); + $this->outputHTML('character/details', $viewData); } /** @@ -121,25 +149,26 @@ final class Character extends BaseController { return $output; } - private function getCastCount(array $cast): int + protected function getCastCount(array $cast): int { $count = 0; foreach($cast as $role) { - if ( + $count++; + /* if ( array_key_exists('attributes', $role) && array_key_exists('role', $role['attributes']) && $role['attributes']['role'] !== NULL ) { $count++; - } + } */ } return $count; } - private function organizeCast(array $cast): array + protected function organizeCast(array $cast): array { $cast = $this->dedupeCast($cast); $output = []; @@ -157,8 +186,19 @@ final class Character extends BaseController { if ($isVA) { - $person = current($role['relationships']['person']['people'])['attributes']; - $name = $person['name']; + foreach($role['relationships']['person']['people'] as $pid => $peoples) + { + $p = $peoples; + } + + $person = $p['attributes']; + $person['id'] = $pid; + $person['image'] = $person['image']['original']; + + uasort($role['relationships']['media']['anime'], function ($a, $b) { + return $a['attributes']['canonicalTitle'] <=> $b['attributes']['canonicalTitle']; + }); + $item = [ 'person' => $person, 'series' => $role['relationships']['media']['anime'] @@ -168,7 +208,11 @@ final class Character extends BaseController { } else { - $output[$roleName][] = $role['relationships']['person']['people']; + foreach($role['relationships']['person']['people'] as $pid => $person) + { + $person['id'] = $pid; + $output[$roleName][$pid] = $person; + } } } diff --git a/src/Controller/Images.php b/src/Controller/Images.php new file mode 100644 index 00000000..cfc32bec --- /dev/null +++ b/src/Controller/Images.php @@ -0,0 +1,198 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Controller; + +use function Aviat\AnimeClient\createPlaceholderImage; +use function Amp\Promise\wait; + +use Aviat\AnimeClient\Controller as BaseController; +use Aviat\AnimeClient\API\{HummingbirdClient, JsonAPI}; +use Aviat\Ion\Di\ContainerInterface; +use Aviat\Ion\View\HtmlView; + +/** + * Controller for handling routes that don't fit elsewhere + */ +final class Images extends BaseController { + /** + * Get image covers from kitsu + * + * @param string $type The category of image + * @param string $file The filename to look for + * @param bool $display Whether to output the image to the server + * @throws \Aviat\Ion\Di\ContainerException + * @throws \Aviat\Ion\Di\NotFoundException + * @throws \InvalidArgumentException + * @throws \TypeError + * @throws \Error + * @throws \Throwable + * @return void + */ + public function cache(string $type, string $file, $display = TRUE): void + { + $currentUrl = $this->request->getUri()->__toString(); + + $kitsuUrl = 'https://media.kitsu.io/'; + $fileName = str_replace('-original', '', $file); + [$id, $ext] = explode('.', basename($fileName)); + + $baseSavePath = $this->config->get('img_cache_path'); + + // Kitsu doesn't serve webp, but for most use cases, + // jpg is a safe assumption + $tryJpg = ['anime','characters','manga','people']; + if ($ext === 'webp' && in_array($type, $tryJpg, TRUE)) + { + $ext = 'jpg'; + $currentUrl = str_replace('webp', 'jpg', $currentUrl); + } + + $typeMap = [ + 'anime' => [ + 'kitsuUrl' => "anime/poster_images/{$id}/medium.{$ext}", + 'width' => 220, + 'height' => 312, + ], + 'avatars' => [ + 'kitsuUrl' => "users/avatars/{$id}/original.{$ext}", + 'width' => null, + 'height' => null, + ], + 'characters' => [ + 'kitsuUrl' => "characters/images/{$id}/original.{$ext}", + 'width' => 225, + 'height' => 350, + ], + 'manga' => [ + 'kitsuUrl' => "manga/poster_images/{$id}/medium.{$ext}", + 'width' => 220, + 'height' => 312, + ], + 'people' => [ + 'kitsuUrl' => "people/images/{$id}/original.{$ext}", + 'width' => null, + 'height' => null, + ], + ]; + + $imageType = $typeMap[$type] ?? NULL; + + if (NULL === $imageType) + { + $this->getPlaceholder($baseSavePath, 200, 200); + return; + } + + $kitsuUrl .= $imageType['kitsuUrl']; + $width = $imageType['width']; + $height = $imageType['height']; + $filePrefix = "{$baseSavePath}/{$type}/{$id}"; + + $promise = (new HummingbirdClient)->request($kitsuUrl); + $response = wait($promise); + + if ($response->getStatus() !== 200) + { + // Try a few different file types before giving up + // webm => jpg => png => gif + $nextType = [ + 'jpg' => 'png', + 'png' => 'gif', + ]; + + if (array_key_exists($ext, $nextType)) + { + $newUrl = str_replace($ext, $nextType[$ext], $currentUrl); + $this->redirect($newUrl, 303); + return; + } + + if ($display) + { + $this->getPlaceholder("{$baseSavePath}/{$type}", $width, $height); + } + else + { + createPlaceholderImage("{$baseSavePath}/{$type}", $width, $height); + } + return; + } + + $data = wait($response->getBody()); + + + + [$origWidth] = getimagesizefromstring($data); + $gdImg = imagecreatefromstring($data); + $resizedImg = imagescale($gdImg, $width ?? $origWidth); + + if ($ext === 'gif') + { + file_put_contents("{$filePrefix}.gif", $data); + imagepalletetotruecolor($gdImg); + } + + // save the webp versions + imagewebp($gdImg, "{$filePrefix}-original.webp"); + imagewebp($resizedImg, "{$filePrefix}.webp"); + + // save the scaled jpeg file + imagejpeg($resizedImg, "{$filePrefix}.jpg"); + + // And the original + file_put_contents("{$filePrefix}-original.jpg", $data); + + imagedestroy($gdImg); + imagedestroy($resizedImg); + + if ($display) + { + $contentType = ($ext === 'webp') + ? "image/webp" + : $response->getHeader('content-type')[0]; + + $outputFile = (strpos($file, '-original') !== FALSE) + ? "{$filePrefix}-original.{$ext}" + : "{$filePrefix}.{$ext}"; + + header("Content-Type: {$contentType}"); + echo file_get_contents($outputFile); + } + } + + /** + * Get a placeholder for a missing image + * + * @param string $path + * @param int|null $width + * @param int|null $height + */ + private function getPlaceholder (string $path, ?int $width = 200, ?int $height = NULL): void + { + $height = $height ?? $width; + + $filename = $path . '/placeholder.png'; + + if ( ! file_exists($path . '/placeholder.png')) + { + createPlaceholderImage($path, $width, $height); + } + + header('Content-Type: image/png'); + echo file_get_contents($filename); + } +} \ No newline at end of file diff --git a/src/Controller/Index.php b/src/Controller/Index.php deleted file mode 100644 index ab7050a8..00000000 --- a/src/Controller/Index.php +++ /dev/null @@ -1,238 +0,0 @@ - - * @copyright 2015 - 2018 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient\Controller; - -use function Amp\Promise\wait; - -use Aviat\AnimeClient\Controller as BaseController; -use Aviat\AnimeClient\API\{HummingbirdClient, JsonAPI}; -use Aviat\Ion\View\HtmlView; - -/** - * Controller for handling routes that don't fit elsewhere - */ -final class Index extends BaseController { - - /** - * Purges the API cache - * - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException - * @throws \InvalidArgumentException - * @return void - */ - public function clearCache() - { - $this->cache->clear(); - $this->outputHTML('blank', [ - 'title' => 'Cache cleared' - ]); - } - - /** - * Show the login form - * - * @param string $status - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException - * @throws \InvalidArgumentException - * @return void - */ - public function login(string $status = '') - { - $message = ''; - - $view = new HtmlView($this->container); - - if ($status !== '') - { - $message = $this->showMessage($view, 'error', $status); - } - - // Set the redirect url - $this->setSessionRedirect(); - - $this->outputHTML('login', [ - 'title' => 'Api login', - 'message' => $message - ], $view); - } - - /** - * Redirect to Anilist to start Oauth flow - */ - public function anilistRedirect() - { - - } - - /** - * Oauth callback for Anilist API - */ - public function anilistCallback() - { - $this->outputHTML('blank', [ - 'title' => 'Oauth!' - ]); - } - - /** - * Attempt login authentication - * - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException - * @throws \Aura\Router\Exception\RouteNotFound - * @throws \InvalidArgumentException - * @return void - */ - public function loginAction() - { - $auth = $this->container->get('auth'); - $post = $this->request->getParsedBody(); - if ($auth->authenticate($post['password'])) - { - $this->sessionRedirect(); - return; - } - - $this->setFlashMessage('Invalid username or password.'); - $this->redirect($this->url->generate('login'), 303); - } - - /** - * Deauthorize the current user - * - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException - * @throws \InvalidArgumentException - * @return void - */ - public function logout() - { - $auth = $this->container->get('auth'); - $auth->logout(); - - $this->redirectToDefaultRoute(); - } - - /** - * Show the user profile page - * - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException - * @throws \InvalidArgumentException - * @return void - */ - public function me() - { - $username = $this->config->get(['kitsu_username']); - $model = $this->container->get('kitsu-model'); - $data = $model->getUserData($username); - $orgData = JsonAPI::organizeData($data)[0]; - $rels = $orgData['relationships'] ?? []; - $favorites = array_key_exists('favorites', $rels) ? $rels['favorites'] : []; - - - $this->outputHTML('me', [ - 'title' => 'About ' . $this->config->get('whose_list'), - 'data' => $orgData, - 'attributes' => $orgData['attributes'], - 'relationships' => $rels, - 'favorites' => $this->organizeFavorites($favorites), - ]); - } - - /** - * Get image covers from kitsu - * - * @param string $type The category of image - * @param string $file The filename to look for - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException - * @throws \InvalidArgumentException - * @throws \TypeError - * @throws \Error - * @throws \Throwable - * @return void - */ - public function images(string $type, string $file): void - { - $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 HummingbirdClient)->request($kitsuUrl); - $response = wait($promise); - $data = wait($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 $data; - } - - /** - * Reorganize favorites data to be more useful - * - * @param array $rawfavorites - * @return array - */ - private function organizeFavorites(array $rawfavorites): array - { - $output = []; - - unset($rawfavorites['data']); - - foreach($rawfavorites as $item) - { - $rank = $item['attributes']['favRank']; - foreach($item['relationships']['item'] as $key => $fav) - { - $output[$key] = $output[$key] ?? []; - foreach ($fav as $id => $data) - { - $output[$key][$rank] = array_merge(['id' => $id], $data['attributes']); - } - } - - ksort($output[$key]); - } - - return $output; - } -} \ No newline at end of file diff --git a/src/Controller/Manga.php b/src/Controller/Manga.php index 9f984308..9c9a7578 100644 --- a/src/Controller/Manga.php +++ b/src/Controller/Manga.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -20,7 +20,7 @@ use Aviat\AnimeClient\Controller; use Aviat\AnimeClient\API\Kitsu\Transformer\MangaListTransformer; use Aviat\AnimeClient\API\Mapping\MangaReadingStatus; use Aviat\AnimeClient\Model\Manga as MangaModel; -use Aviat\AnimeClient\Types\MangaFormItem; +use Aviat\AnimeClient\Types\FormItem; use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\{Json, StringWrapper}; @@ -130,6 +130,11 @@ final class Manga extends Controller { $this->redirect('manga/add', 303); } + if (empty($data['mal_id'])) + { + unset($data['mal_id']); + } + $result = $this->model->createLibraryItem($data); if ($result) @@ -182,8 +187,9 @@ final class Manga extends Controller { */ public function search(): void { - $query_data = $this->request->getQueryParams(); - $this->outputJSON($this->model->search($query_data['query'])); + $queryParams = $this->request->getQueryParams(); + $query = $queryParams['query']; + $this->outputJSON($this->model->search($query)); } /** @@ -201,7 +207,7 @@ final class Manga extends Controller { // large form-based updates $transformer = new MangaListTransformer(); $post_data = $transformer->untransform($data); - $full_result = $this->model->updateLibraryItem(new MangaFormItem($post_data)); + $full_result = $this->model->updateLibraryItem(new FormItem($post_data)); if ($full_result['statusCode'] === 200) { @@ -218,13 +224,9 @@ final class Manga extends Controller { } /** - * Update a manga item - * - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException - * @return void + * Increment the progress of a manga item */ - public function update(): void + public function increment(): void { if (stripos($this->request->getHeader('content-type')[0], 'application/json') !== FALSE) { @@ -235,7 +237,7 @@ final class Manga extends Controller { $data = $this->request->getParsedBody(); } - $response = $this->model->updateLibraryItem(new MangaFormItem($data)); + $response = $this->model->incrementLibraryItem(new FormItem($data)); $this->cache->clear(); $this->outputJSON($response['body'], $response['statusCode']); @@ -251,13 +253,11 @@ final class Manga extends Controller { public function delete(): void { $body = $this->request->getParsedBody(); - $id = $body['id']; - $malId = $body['mal_id']; - $response = $this->model->deleteLibraryItem($id, $malId); + $response = $this->model->deleteLibraryItem($body['id'], $body['mal_id']); if ($response) { - $this->setFlashMessage("Successfully deleted manga.", 'success'); + $this->setFlashMessage('Successfully deleted manga.', 'success'); $this->cache->clear(); } else @@ -280,6 +280,7 @@ final class Manga extends Controller { public function details($manga_id): void { $data = $this->model->getManga($manga_id); + $staff = []; $characters = []; if (empty($data)) @@ -293,14 +294,65 @@ final class Manga extends Controller { return; } - foreach($data['included'] as $included) + if (array_key_exists('mediaCharacters', $data['included'])) { - if ($included['type'] === 'characters') + $mediaCharacters = $data['included']['mediaCharacters']; + + foreach ($mediaCharacters as $rel) { - $characters[$included['id']] = $included['attributes']; + // dd($rel); + // $charId = $rel['relationships']['character']['data']['id']; + $role = $rel['attributes']['role']; + + foreach($rel['relationships']['character']['characters'] as $charId => $char) + { + if (array_key_exists($charId, $data['included']['characters'])) + { + $characters[$role][$charId] = $char['attributes']; + } + } } } + if (array_key_exists('mediaStaff', $data['included'])) + { + foreach ($data['included']['mediaStaff'] as $id => $staffing) + { + $role = $staffing['attributes']['role']; + + foreach($staffing['relationships']['person']['people'] as $personId => $personDetails) + { + if ( ! array_key_exists($role, $staff)) + { + $staff[$role] = []; + } + + $staff[$role][$personId] = [ + 'id' => $personId, + 'name' => $personDetails['attributes']['name'] ?? '??', + 'image' => $personDetails['attributes']['image'], + ]; + } + } + } + + if ( ! empty($characters['main'])) + { + uasort($characters['main'], function ($a, $b) { + return $a['name'] <=> $b['name']; + }); + } + + if ( ! empty($characters['supporting'])) + { + uasort($characters['supporting'], function ($a, $b) { + return $a['name'] <=> $b['name']; + }); + } + + ksort($characters); + ksort($staff); + $this->outputHTML('manga/details', [ 'title' => $this->formatTitle( $this->config->get('whose_list') . "'s Manga List", @@ -309,6 +361,7 @@ final class Manga extends Controller { ), 'characters' => $characters, 'data' => $data, + 'staff' => $staff, ]); } diff --git a/src/Controller/MangaCollection.php b/src/Controller/MangaCollection.php index a904d50e..65895dc4 100644 --- a/src/Controller/MangaCollection.php +++ b/src/Controller/MangaCollection.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/Controller/Misc.php b/src/Controller/Misc.php new file mode 100644 index 00000000..6d2b43fb --- /dev/null +++ b/src/Controller/Misc.php @@ -0,0 +1,98 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Controller; + +use Aviat\AnimeClient\Controller as BaseController; +use Aviat\Ion\Di\ContainerInterface; +use Aviat\Ion\View\HtmlView; + +/** + * Controller for handling routes that don't fit elsewhere + */ +final class Misc extends BaseController { + /** + * Purges the API cache + * + * @return void + */ + public function clearCache() + { + $this->cache->clear(); + $this->outputHTML('blank', [ + 'title' => 'Cache cleared' + ]); + } + + /** + * Show the login form + * + * @param string $status + * @return void + */ + public function login(string $status = '') + { + $message = ''; + + $view = new HtmlView($this->container); + + if ($status !== '') + { + $message = $this->showMessage($view, 'error', $status); + } + + // Set the redirect url + $this->setSessionRedirect(); + + $this->outputHTML('login', [ + 'title' => 'Api login', + 'message' => $message + ], $view); + } + + /** + * Attempt login authentication + * + * @return void + */ + public function loginAction() + { + $auth = $this->container->get('auth'); + $post = $this->request->getParsedBody(); + + if ($auth->authenticate($post['password'])) + { + $this->sessionRedirect(); + return; + } + + $this->setFlashMessage('Invalid username or password.'); + $this->redirect($this->url->generate('login'), 303); + } + + /** + * Deauthorize the current user + * + * @return void + */ + public function logout() + { + $auth = $this->container->get('auth'); + $auth->logout(); + + $this->redirectToDefaultRoute(); + } +} \ No newline at end of file diff --git a/src/Controller/People.php b/src/Controller/People.php new file mode 100644 index 00000000..4c38d017 --- /dev/null +++ b/src/Controller/People.php @@ -0,0 +1,161 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Controller; + +use Aviat\AnimeClient\Controller as BaseController; +use Aviat\AnimeClient\API\JsonAPI; + +/** + * Controller for People pages + */ +final class People extends BaseController { + /** + * Show information about a person + * + * @param string $id + * @return void + */ + public function index(string $id): void + { + $model = $this->container->get('kitsu-model'); + + $rawData = $model->getPerson($id); + + if (( ! array_key_exists('data', $rawData)) || empty($rawData['data'])) + { + $this->notFound( + $this->formatTitle( + 'People', + 'Person not found' + ), + 'Person Not Found' + ); + + return; + } + + $data = JsonAPI::organizeData($rawData); + $included = JsonAPI::organizeIncludes($rawData['included']); + + $orgData = $this->organizeData($included); + + $viewData = [ + 'included' => $included, + 'title' => $this->formatTitle( + 'People', + $data['attributes']['name'] + ), + 'data' => $data, + 'castCount' => 0, + 'castings' => [], + 'characters' => $orgData['characters'], + 'staff' => $orgData['staff'], + ]; + + $this->outputHTML('person/details', $viewData); + } + + protected function organizeData(array $data): array + { + $output = [ + 'characters' => [ + 'main' => [], + 'supporting' => [], + ], + 'staff' => [], + ]; + + if (array_key_exists('characterVoices', $data)) + { + foreach ($data['characterVoices'] as $cv) + { + $mcId = $cv['relationships']['mediaCharacter']['data']['id']; + + if ( ! array_key_exists($mcId, $data['mediaCharacters'])) + { + continue; + } + + $mc = $data['mediaCharacters'][$mcId]; + + $role = $mc['role']; + + $charId = $mc['relationships']['character']['data']['id']; + $mediaId = $mc['relationships']['media']['data']['id']; + + $existingMedia = array_key_exists($charId, $output['characters'][$role]) + ? $output['characters'][$role][$charId]['media'] + : []; + + $relatedMedia = [ + $mediaId => $data['anime'][$mediaId], + ]; + + $includedMedia = array_replace_recursive($existingMedia, $relatedMedia); + + uasort($includedMedia, function ($a, $b) { + return $a['canonicalTitle'] <=> $b['canonicalTitle']; + }); + + $character = $data['characters'][$charId]; + + $output['characters'][$role][$charId] = [ + 'character' => $character, + 'media' => $includedMedia, + ]; + } + } + + if (array_key_exists('mediaStaff', $data)) + { + foreach($data['mediaStaff'] as $rid => $role) + { + $roleName = $role['role']; + $mediaType = $role['relationships']['media']['data']['type']; + $mediaId = $role['relationships']['media']['data']['id']; + $media = $data[$mediaType][$mediaId]; + $output['staff'][$roleName][$mediaType][$mediaId] = $media; + } + } + + uasort($output['characters']['main'], function ($a, $b) { + return $a['character']['canonicalName'] <=> $b['character']['canonicalName']; + }); + uasort($output['characters']['supporting'], function ($a, $b) { + return $a['character']['canonicalName'] <=> $b['character']['canonicalName']; + }); + ksort($output['staff']); + foreach($output['staff'] as $role => &$media) + { + if (array_key_exists('anime', $media)) + { + uasort($media['anime'], function ($a, $b) { + return $a['canonicalTitle'] <=> $b['canonicalTitle']; + }); + } + + if (array_key_exists('manga', $media)) + { + uasort($media['manga'], function ($a, $b) { + return $a['canonicalTitle'] <=> $b['canonicalTitle']; + }); + } + } + + return $output; + } +} \ No newline at end of file diff --git a/src/Controller/Settings.php b/src/Controller/Settings.php new file mode 100644 index 00000000..6ae5ad8b --- /dev/null +++ b/src/Controller/Settings.php @@ -0,0 +1,149 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Controller; + +use Aviat\AnimeClient\Controller as BaseController; +use Aviat\Ion\Di\ContainerInterface; + +/** + * Controller for user settings + */ +final class Settings extends BaseController { + /** + * @var \Aviat\API\Anilist\Model + */ + private $anilistModel; + + /** + * @var \Aviat\AnimeClient\Model\Settings + */ + private $settingsModel; + + public function __construct(ContainerInterface $container) + { + parent::__construct($container); + + $this->anilistModel = $container->get('anilist-model'); + $this->settingsModel = $container->get('settings-model'); + } + + /** + * Show the user settings, if logged in + */ + public function index() + { + $auth = $this->container->get('auth'); + $form = $this->settingsModel->getSettingsForm(); + + $hasAnilistLogin = $this->config->has(['anilist', 'access_token']); + + $this->outputHTML('settings/settings', [ + 'anilistModel' => $this->anilistModel, + 'auth' => $auth, + 'form' => $form, + 'hasAnilistLogin' => $hasAnilistLogin, + 'config' => $this->config, + 'title' => $this->config->get('whose_list') . "'s Settings", + ]); + } + + /** + * Attempt to save the user's settings + * + * @throws \Aura\Router\Exception\RouteNotFound + */ + public function update() + { + $post = $this->request->getParsedBody(); + unset($post['settings-tabs']); + + // dump($post); + $saved = $this->settingsModel->saveSettingsFile($post); + + if ($saved) + { + $this->setFlashMessage('Saved config settings.', 'success'); + } else + { + $this->setFlashMessage('Failed to save config file.', 'error'); + } + + $this->redirect($this->url->generate('settings'), 303); + } + + /** + * Redirect to Anilist to start Oauth flow + */ + public function anilistRedirect() + { + $redirectUrl = 'https://anilist.co/api/v2/oauth/authorize?' . + http_build_query([ + 'client_id' => $this->config->get(['anilist', 'client_id']), + 'redirect_uri' => $this->urlGenerator->url('/anilist-oauth'), + 'response_type' => 'code', + ]); + + $this->redirect($redirectUrl, 303); + } + + /** + * Oauth callback for Anilist API + */ + public function anilistCallback() + { + $query = $this->request->getQueryParams(); + $authCode = $query['code']; + $uri = $this->urlGenerator->url('/anilist-oauth'); + + $authData = $this->anilistModel->authenticate($authCode, $uri); + $settings = $this->settingsModel->getSettings(); + + if (array_key_exists('error', $authData)) + { + $this->errorPage(400, 'Error Linking Account', $authData['hint']); + return; + } + + // Update the override config file + $anilistSettings = [ + 'access_token' => $authData['access_token'], + 'access_token_expires' => (time() - 10) + $authData['expires_in'], + 'refresh_token' => $authData['refresh_token'], + ]; + + $newSettings = $settings; + $newSettings['anilist'] = array_merge($settings['anilist'], $anilistSettings); + + foreach ($newSettings['config'] as $key => $value) + { + $newSettings[$key] = $value; + } + unset($newSettings['config']); + + $saved = $this->settingsModel->saveSettingsFile($newSettings); + + if ($saved) + { + $this->setFlashMessage('Linked Anilist Account', 'success'); + } else + { + $this->setFlashMessage('Error Linking Anilist Account', 'error'); + } + + $this->redirect($this->url->generate('settings'), 303); + } +} \ No newline at end of file diff --git a/src/Controller/User.php b/src/Controller/User.php new file mode 100644 index 00000000..2780542f --- /dev/null +++ b/src/Controller/User.php @@ -0,0 +1,158 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Controller; + +use Aviat\AnimeClient\Controller as BaseController; +use Aviat\AnimeClient\API\JsonAPI; +use Aviat\Ion\Di\ContainerInterface; + +/** + * Controller for handling routes that don't fit elsewhere + */ +final class User extends BaseController { + + private $kitsuModel; + + public function __construct(ContainerInterface $container) + { + parent::__construct($container); + + $this->kitsuModel = $container->get('kitsu-model'); + } + + /** + * Show the user profile page for the configured user + */ + public function me(): void + { + $this->about('me'); + } + + /** + * Show the user profile page + * + * @param string $username + * @return void + */ + public function about(string $username): void + { + $isMainUser = $username === 'me'; + + $username = $isMainUser + ? $this->config->get(['kitsu_username']) + : $username; + + $data = $this->kitsuModel->getUserData($username); + $orgData = JsonAPI::organizeData($data)[0]; + $rels = $orgData['relationships'] ?? []; + $favorites = array_key_exists('favorites', $rels) ? $rels['favorites'] : []; + + $stats = []; + foreach ($rels['stats'] as $sid => &$item) + { + $key = $item['attributes']['kind']; + $stats[$key] = $item['attributes']['statsData']; + unset($item); + } + + //dump($orgData); + // dump($stats); + + // $timeOnAnime = $this->formatAnimeTime($orgData['attributes']['lifeSpentOnAnime']); + $timeOnAnime = $this->formatAnimeTime($stats['anime-amount-consumed']['time']); + + + $whom = $isMainUser + ? $this->config->get('whose_list') + : $username; + + $this->outputHTML('user/details', [ + 'title' => 'About ' . $whom, + 'data' => $orgData, + 'attributes' => $orgData['attributes'], + 'relationships' => $rels, + 'favorites' => $this->organizeFavorites($favorites), + 'stats' => $stats, + 'timeOnAnime' => $timeOnAnime, + ]); + } + + /** + * Reorganize favorites data to be more useful + * + * @param array $rawFavorites + * @return array + */ + private function organizeFavorites(array $rawFavorites): array + { + $output = []; + + unset($rawFavorites['data']); + + foreach ($rawFavorites as $item) + { + $rank = $item['attributes']['favRank']; + foreach ($item['relationships']['item'] as $key => $fav) + { + $output[$key] = $output[$key] ?? []; + foreach ($fav as $id => $data) + { + $output[$key][$rank] = array_merge(['id' => $id], $data['attributes']); + } + } + + ksort($output[$key]); + } + + return $output; + } + + /** + * Format the time spent on anime in a more readable format + * + * @param int $minutes + * @return string + */ + private function formatAnimeTime(int $minutes): string + { + $minutesPerDay = 1440; + $minutesPerYear = $minutesPerDay * 365; + + // Minutes short of a year + $years = (int)floor($minutes / $minutesPerYear); + $minutes %= $minutesPerYear; + + // Minutes short of a day + $extraMinutes = $minutes % $minutesPerDay; + + $days = ($minutes - $extraMinutes) / $minutesPerDay; + + // Minutes short of an hour + $remMinutes = $extraMinutes % 60; + + $hours = ($extraMinutes - $remMinutes) / 60; + + $output = "{$days} days, {$hours} hours, and {$remMinutes} minutes."; + + if ($years > 0) + { + $output = "{$years} year(s),{$output}"; + } + + return $output; + } +} \ No newline at end of file diff --git a/src/Dispatcher.php b/src/Dispatcher.php index d40e89a0..4777d5af 100644 --- a/src/Dispatcher.php +++ b/src/Dispatcher.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -18,7 +18,7 @@ namespace Aviat\AnimeClient; use function Aviat\Ion\_dir; -use Aura\Router\Matcher; +use Aura\Router\{Matcher, Rule}; use Aviat\AnimeClient\API\FailedResponseException; use Aviat\Ion\Di\ContainerInterface; @@ -314,7 +314,7 @@ final class Dispatcher extends RoutingBase { $params = []; switch($failure->failedRule) { - case 'Aura\Router\Rule\Allows': + case Rule\Allows::class: $params = [ 'http_code' => 405, 'title' => '405 Method Not Allowed', @@ -322,7 +322,7 @@ final class Dispatcher extends RoutingBase { ]; break; - case 'Aura\Router\Rule\Accepts': + case Rule\Accepts::class: $params = [ 'http_code' => 406, 'title' => '406 Not Acceptable', @@ -363,9 +363,15 @@ final class Dispatcher extends RoutingBase { ? $controllerMap[$routeType] : DEFAULT_CONTROLLER; - if (array_key_exists($routeType, $controllerMap)) + // If there's an explicit controller, try to find + // the full namespaced class name + if (array_key_exists('controller', $route)) { - $controllerClass = $controllerMap[$routeType]; + $controllerKey = $route['controller']; + if (array_key_exists($controllerKey, $controllerMap)) + { + $controllerClass = $controllerMap[$controllerKey]; + } } // Prepend the controller to the route parameters diff --git a/src/FormGenerator.php b/src/FormGenerator.php new file mode 100644 index 00000000..05c953fa --- /dev/null +++ b/src/FormGenerator.php @@ -0,0 +1,110 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient; + +use Aviat\Ion\ +{ + ArrayWrapper, StringWrapper +}; +use Aviat\Ion\Di\ContainerInterface; + +/** + * Helper object to manage form generation, especially for config editing + */ +final class FormGenerator { + use ArrayWrapper; + use StringWrapper; + + /** + * Injection Container + * @var ContainerInterface $container + */ + protected $container; + + /** + * Html generation helper + * + * @var \Aura\Html\HelperLocator + */ + protected $helper; + + public function __construct(ContainerInterface $container) + { + $this->container = $container; + $this->helper = $container->get('html-helper'); + } + + /** + * Generate the html structure of the form + * + * @param string $name + * @param array $form + * @return string + */ + public function generate(string $name, array $form) + { + $type = $form['type']; + + if ($form['display'] === FALSE) + { + return $this->helper->input([ + 'type' => 'hidden', + 'name' => $name, + 'value' => $form['value'], + ]); + } + + $params = [ + 'name' => $name, + 'value' => $form['value'], + 'attribs' => [ + 'id' => $name, + ], + ]; + + switch($type) + { + case 'boolean': + $params['type'] = 'radio'; + $params['options'] = [ + '1' => 'Yes', + '0' => 'No', + ]; + unset($params['attribs']['id']); + break; + + case 'string': + $params['type'] = 'text'; + break; + + case 'select': + $params['type'] = 'select'; + $params['options'] = array_flip($form['options']); + break; + } + + foreach (['readonly', 'disabled'] as $key) + { + if ($form[$key] !== FALSE) + { + $params['attribs'][$key] = $form[$key]; + } + } + + return $this->helper->input($params); + } +} \ No newline at end of file diff --git a/src/Helper/Form.php b/src/Helper/Form.php new file mode 100644 index 00000000..244030ab --- /dev/null +++ b/src/Helper/Form.php @@ -0,0 +1,41 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Helper; + +use Aviat\AnimeClient\FormGenerator; +use Aviat\Ion\Di\ContainerAware; + +/** + * MenuGenerator helper wrapper + */ +final class Form { + + use ContainerAware; + + /** + * Create the html for the selected menu + * + * @param string $name + * @param array $form + * @return string + */ + public function __invoke(string $name, array $form) + { + return (new FormGenerator($this->container))->generate($name, $form); + } +} +// End of Menu.php \ No newline at end of file diff --git a/src/Helper/Menu.php b/src/Helper/Menu.php index cdbc37c4..faa32250 100644 --- a/src/Helper/Menu.php +++ b/src/Helper/Menu.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/Helper/Picture.php b/src/Helper/Picture.php new file mode 100644 index 00000000..9e8b2dcc --- /dev/null +++ b/src/Helper/Picture.php @@ -0,0 +1,122 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Helper; + +use Aviat\Ion\Di\ContainerAware; + +/** + * Simplify picture elements + */ +final class Picture { + + use ContainerAware; + + private const MIME_MAP = [ + 'apng' => 'image/vnd.mozilla.apng', + 'bmp' => 'image/bmp', + 'gif' => 'image/gif', + 'ico' => 'image/x-icon', + 'jpeg' => 'image/jpeg', + 'jpf' => 'image/jpx', + 'jpg' => 'image/jpeg', + 'jpx' => 'image/jpx', + 'png' => 'image/png', + 'svg' => 'image/svg+xml', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'webp' => 'image/webp', + ]; + + private const SIMPLE_IMAGE_TYPES = [ + 'gif', + 'jpeg', + 'jpg', + 'png', + ]; + + /** + * Create the html for an html picture element + * + * @param string $uri + * @param string $fallbackExt + * @param array $picAttrs + * @param array $imgAttrs + * @return string + */ + public function __invoke(string $uri, string $fallbackExt = 'jpg', array $picAttrs = [], array $imgAttrs = []): string + { + $urlGenerator = $this->container->get('url-generator'); + $helper = $this->container->get('html-helper'); + + // If it is a placeholder image, make the + // fallback a png, not a jpg + if (strpos($uri, 'placeholder') !== FALSE) + { + $fallbackExt = 'png'; + } + + if (strpos($uri, '//') === FALSE) + { + $uri = $urlGenerator->assetUrl($uri); + } + + $urlParts = explode('.', $uri); + $ext = array_pop($urlParts); + $path = implode('.', $urlParts); + + $mime = array_key_exists($ext, static::MIME_MAP) + ? static::MIME_MAP[$ext] + : 'image/jpeg'; + + $fallbackMime = array_key_exists($fallbackExt, static::MIME_MAP) + ? static::MIME_MAP[$fallbackExt] + : 'image/jpeg'; + + // For image types that are well-established, just return a + // simple element instead + if ( + $ext === $fallbackExt || + \in_array($ext, static::SIMPLE_IMAGE_TYPES, TRUE) + ) + { + $attrs = ( ! empty($imgAttrs)) + ? $imgAttrs + : $picAttrs; + + return $helper->img($uri, $attrs); + } + + $fallbackImg = "{$path}.{$fallbackExt}"; + + $pictureChildren = [ + $helper->void('source', [ + 'srcset' => $uri, + 'type' => $mime, + ]), + $helper->void('source', [ + 'srcset' => $fallbackImg, + 'type' => $fallbackMime + ]), + $helper->img($fallbackImg, array_merge(['alt' => ''], $imgAttrs)), + ]; + + $sources = implode('', $pictureChildren); + + return $helper->elementRaw('picture', $sources, $picAttrs); + } +} +// End of Picture.php \ No newline at end of file diff --git a/src/MenuGenerator.php b/src/MenuGenerator.php index eaf51066..169ac20c 100644 --- a/src/MenuGenerator.php +++ b/src/MenuGenerator.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -62,7 +62,7 @@ final class MenuGenerator extends UrlGenerator { * @param array $menus * @return array */ - protected function parseConfig(array $menus) + protected function parseConfig(array $menus) : array { $parsed = []; @@ -86,7 +86,7 @@ final class MenuGenerator extends UrlGenerator { * @throws ConfigException * @return string */ - public function generate($menu) + public function generate($menu) : string { $menus = $this->config->get('menus'); $parsedConfig = $this->parseConfig($menus); @@ -114,7 +114,7 @@ final class MenuGenerator extends UrlGenerator { } // Create the menu html - return $this->helper->ul(); + return (string) $this->helper->ul(); } } // End of MenuGenerator.php \ No newline at end of file diff --git a/src/Model/API.php b/src/Model/API.php index a71273bb..5662445b 100644 --- a/src/Model/API.php +++ b/src/Model/API.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -24,13 +24,6 @@ use Aviat\Ion\StringWrapper; class API { use StringWrapper; - /** - * Whether to use the MAL api - * - * @var boolean - */ - protected $useMALAPI; - /** * Sort the list entries by their title * diff --git a/src/Model/Anime.php b/src/Model/Anime.php index 34a5b366..42538dec 100644 --- a/src/Model/Anime.php +++ b/src/Model/Anime.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -20,7 +20,7 @@ use Aviat\AnimeClient\API\ParallelAPIRequest; use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus; use Aviat\AnimeClient\Types\{ Anime as AnimeType, - AnimeFormItem, + FormItem, AnimeListItem }; use Aviat\Ion\Di\ContainerInterface; @@ -30,6 +30,21 @@ use Aviat\Ion\Json; * Model for handling requests dealing with the anime list */ class Anime extends API { + + /** + * Is the Anilist API enabled? + * + * @var boolean + */ + protected $anilistEnabled; + + /** + * Model for making requests to Anilist API + * + * @var \Aviat\AnimeClient\API\Anilist\Model + */ + protected $anilistModel; + /** * Model for making requests to Kitsu API * @@ -37,13 +52,6 @@ class Anime extends API { */ protected $kitsuModel; - /** - * Model for making requests to MAL API - * - * @var \Aviat\AnimeClient\API\MAL\Model - */ - protected $malModel; - /** * Anime constructor. * @@ -51,11 +59,11 @@ class Anime extends API { */ public function __construct(ContainerInterface $container) { + $this->anilistModel = $container->get('anilist-model'); $this->kitsuModel = $container->get('kitsu-model'); - $this->malModel = $container->get('mal-model'); $config = $container->get('config'); - $this->useMALAPI = $config->get(['use_mal_api']) === TRUE; + $this->anilistEnabled = (bool) $config->get(['anilist', 'enabled']); } /** @@ -100,7 +108,7 @@ class Anime extends API { * @param string $slug * @return AnimeType */ - public function getAnime(string $slug): AnimeType + public function getAnime(string $slug) { return $this->kitsuModel->getAnime($slug); } @@ -109,9 +117,9 @@ class Anime extends API { * Get anime by its kitsu id * * @param string $animeId - * @return array + * @return AnimeType */ - public function getAnimeById(string $animeId): array + public function getAnimeById(string $animeId): AnimeType { return $this->kitsuModel->getAnimeById($animeId); } @@ -124,7 +132,7 @@ class Anime extends API { */ public function search(string $name): array { - return $this->kitsuModel->search('anime', $name); + return $this->kitsuModel->search('anime', urldecode($name)); } /** @@ -136,7 +144,15 @@ class Anime extends API { */ public function getLibraryItem(string $itemId): AnimeListItem { - return $this->kitsuModel->getListItem($itemId); + $item = $this->kitsuModel->getListItem($itemId); + $array = $item->toArray(); + + if (is_array($array['notes'])) + { + $array['notes'] = ''; + } + + return new AnimeListItem($array); } /** @@ -148,44 +164,67 @@ class Anime extends API { public function createLibraryItem(array $data): bool { $requester = new ParallelAPIRequest(); - - if ($this->useMALAPI) - { - $malData = $data; - $malId = $this->kitsuModel->getMalIdForAnime($malData['id']); - - if ($malId !== NULL) - { - $malData['id'] = $malId; - $requester->addRequest($this->malModel->createListItem($malData), 'mal'); - } - } - $requester->addRequest($this->kitsuModel->createListItem($data), 'kitsu'); + if (array_key_exists('mal_id', $data) && $this->anilistEnabled) + { + $requester->addRequest($this->anilistModel->createListItem($data, 'ANIME'), 'anilist'); + } + $results = $requester->makeRequests(); return count($results) > 0; } /** - * Update a list entry + * Increment progress for the specified anime * - * @param AnimeFormItem $data + * @param FormItem $data * @return array */ - public function updateLibraryItem(AnimeFormItem $data): array + public function incrementLibraryItem(FormItem $data): array { $requester = new ParallelAPIRequest(); + $requester->addRequest($this->kitsuModel->incrementListItem($data), 'kitsu'); - if ($this->useMALAPI) + $array = $data->toArray(); + + if (array_key_exists('mal_id', $array) && $this->anilistEnabled) { - $requester->addRequest($this->malModel->updateListItem($data), 'mal'); + $requester->addRequest($this->anilistModel->incrementListItem($data, 'ANIME'), 'anilist'); } + $results = $requester->makeRequests(); + + $body = Json::decode($results['kitsu']); + $statusCode = array_key_exists('error', $body) ? 400 : 200; + + return [ + 'body' => Json::decode($results['kitsu']), + 'statusCode' => $statusCode + ]; + } + + /** + * Update a list entry + * + * @param FormItem $data + * @return array + */ + public function updateLibraryItem(FormItem $data): array + { + $requester = new ParallelAPIRequest(); $requester->addRequest($this->kitsuModel->updateListItem($data), 'kitsu'); + $array = $data->toArray(); + + if (array_key_exists('mal_id', $array) && $this->anilistEnabled) + { + $requester->addRequest($this->anilistModel->updateListItem($data, 'ANIME'), 'anilist'); + } + $results = $requester->makeRequests(); + $body = Json::decode($results['kitsu']); $statusCode = array_key_exists('error', $body) ? 400: 200; @@ -205,14 +244,13 @@ class Anime extends API { public function deleteLibraryItem(string $id, string $malId = NULL): bool { $requester = new ParallelAPIRequest(); - - if ($this->useMALAPI && $malId !== NULL) - { - $requester->addRequest($this->malModel->deleteListItem($malId), 'MAL'); - } - $requester->addRequest($this->kitsuModel->deleteListItem($id), 'kitsu'); + if ($malId !== null && $this->anilistEnabled) + { + $requester->addRequest($this->anilistModel->deleteListItem($malId, 'ANIME'), 'anilist'); + } + $results = $requester->makeRequests(); return count($results) > 0; diff --git a/src/Model/AnimeCollection.php b/src/Model/AnimeCollection.php index ba578f0d..a140fbe7 100644 --- a/src/Model/AnimeCollection.php +++ b/src/Model/AnimeCollection.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -121,9 +121,25 @@ final class AnimeCollection extends Collection { ->join('media', 'media.id=a.media_id', 'inner') ->order_by('media') ->order_by('title') + ->group_by('a.hummingbird_id') ->get(); - return $query->fetchAll(PDO::FETCH_ASSOC); + // Add genres associated with each item + $rows = $query->fetchAll(PDO::FETCH_ASSOC); + $genres = $this->getGenresForList(); + + foreach($rows as &$row) + { + $id = $row['hummingbird_id']; + + $row['genres'] = array_key_exists($id, $genres) + ? $genres[$id] + : []; + + sort($row['genres']); + } + + return $rows; } /** @@ -210,6 +226,24 @@ final class AnimeCollection extends Collection { return $query->fetch(PDO::FETCH_ASSOC); } + private function getGenresForList(): array + { + $query = $this->db->select('hummingbird_id, genre') + ->from('genres g') + ->join('genre_anime_set_link gasl', 'gasl.genre_id=g.id') + ->get(); + + $rows = $query->fetchAll(PDO::FETCH_ASSOC); + $output = []; + + foreach($rows as $row) + { + $output[$row['hummingbird_id']][] = $row['genre']; + } + + return $output; + } + /** * Update genre information for selected anime * diff --git a/src/Model/Collection.php b/src/Model/Collection.php index 117227bf..3e51b688 100644 --- a/src/Model/Collection.php +++ b/src/Model/Collection.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -42,15 +42,15 @@ class Collection extends DB { try { - $this->db = \Query($this->dbConfig['collection']); + $this->db = \Query($this->dbConfig); } catch (PDOException $e) {} // Is database valid? If not, set a flag so the // app can be run without a valid database - if ($this->dbConfig['collection']['type'] === 'sqlite') + if ($this->dbConfig['type'] === 'sqlite') { - $dbFileName = $this->dbConfig['collection']['file']; + $dbFileName = $this->dbConfig['file']; if ($dbFileName !== ':memory:' && file_exists($dbFileName)) { diff --git a/src/Model/DB.php b/src/Model/DB.php index 03ee3ef4..50fd6bce 100644 --- a/src/Model/DB.php +++ b/src/Model/DB.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/Model/Manga.php b/src/Model/Manga.php index e902f8b6..d4b4b07e 100644 --- a/src/Model/Manga.php +++ b/src/Model/Manga.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -22,7 +22,7 @@ use Aviat\AnimeClient\API\{ ParallelAPIRequest }; use Aviat\AnimeClient\Types\{ - MangaFormItem, + FormItem, MangaListItem, MangaPage }; @@ -33,32 +33,37 @@ use Aviat\Ion\Json; * Model for handling requests dealing with the manga list */ class Manga extends API { + /** + * Is the Anilist API enabled? + * + * @var boolean + */ + protected $anilistEnabled; + + /** + * Model for making requests to the Anilist API + * @var \Aviat\AnimeClient\API\Anilist\Model + */ + protected $anilistModel; + /** * Model for making requests to Kitsu API * @var \Aviat\AnimeClient\API\Kitsu\Model */ protected $kitsuModel; - /** - * Model for making requests to MAL API - * @var \Aviat\AnimeClient\API\MAL\Model - */ - protected $malModel; - /** * Constructor * * @param ContainerInterface $container - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException */ public function __construct(ContainerInterface $container) { + $this->anilistModel = $container->get('anilist-model'); $this->kitsuModel = $container->get('kitsu-model'); - $this->malModel = $container->get('mal-model'); $config = $container->get('config'); - $this->useMALAPI = $config->get(['use_mal_api']) === TRUE; + $this->anilistEnabled = (bool)$config->get(['anilist', 'enabled']); } /** @@ -76,7 +81,7 @@ class Manga extends API { { $this->sortByName($section, 'manga'); } - + return $data; } @@ -129,21 +134,13 @@ class Manga extends API { public function createLibraryItem(array $data): bool { $requester = new ParallelAPIRequest(); - - if ($this->useMALAPI) - { - $malData = $data; - $malId = $this->kitsuModel->getMalIdForManga($malData['id']); - - if ($malId !== NULL) - { - $malData['id'] = $malId; - $requester->addRequest($this->malModel->createListItem($malData, 'manga'), 'mal'); - } - } - $requester->addRequest($this->kitsuModel->createListItem($data), 'kitsu'); + if (array_key_exists('mal_id', $data) && $this->anilistEnabled) + { + $requester->addRequest($this->anilistModel->createListItem($data, 'MANGA'), 'anilist'); + } + $results = $requester->makeRequests(); return count($results) > 0; @@ -152,20 +149,21 @@ class Manga extends API { /** * Update a list entry * - * @param MangaFormItem $data + * @param FormItem $data * @return array */ - public function updateLibraryItem(MangaFormItem $data): array + public function updateLibraryItem(FormItem $data): array { $requester = new ParallelAPIRequest(); - - if ($this->useMALAPI) - { - $requester->addRequest($this->malModel->updateListItem($data, 'manga'), 'mal'); - } - $requester->addRequest($this->kitsuModel->updateListItem($data), 'kitsu'); + $array = $data->toArray(); + + if (array_key_exists('mal_id', $array) && $this->anilistEnabled) + { + $requester->addRequest($this->anilistModel->updateListItem($data, 'MANGA'), 'anilist'); + } + $results = $requester->makeRequests(); $body = Json::decode($results['kitsu']); $statusCode = array_key_exists('error', $body) ? 400: 200; @@ -176,6 +174,34 @@ class Manga extends API { ]; } + /** + * Increase the progress of a list entry + * + * @param FormItem $data + * @return array + */ + public function incrementLibraryItem(FormItem $data): array + { + $requester = new ParallelAPIRequest(); + $requester->addRequest($this->kitsuModel->incrementListItem($data), 'kitsu'); + + $array = $data->toArray(); + + if (array_key_exists('mal_id', $array) && $this->anilistEnabled) + { + $requester->addRequest($this->anilistModel->incrementListItem($data, 'MANGA'), 'anilist'); + } + + $results = $requester->makeRequests(); + $body = Json::decode($results['kitsu']); + $statusCode = array_key_exists('error', $body) ? 400 : 200; + + return [ + 'body' => Json::decode($results['kitsu']), + 'statusCode' => $statusCode + ]; + } + /** * Delete a list entry * @@ -186,14 +212,13 @@ class Manga extends API { public function deleteLibraryItem(string $id, string $malId = NULL): bool { $requester = new ParallelAPIRequest(); - - if ($this->useMALAPI && $malId !== NULL) - { - $requester->addRequest($this->malModel->deleteListItem($malId, 'manga'), 'MAL'); - } - $requester->addRequest($this->kitsuModel->deleteListItem($id), 'kitsu'); + if ($malId !== null && $this->anilistEnabled) + { + $requester->addRequest($this->anilistModel->deleteListItem($malId, 'MANGA'), 'anilist'); + } + $results = $requester->makeRequests(); return count($results) > 0; diff --git a/src/Model/MangaCollection.php b/src/Model/MangaCollection.php index f3d41968..6c42f2bd 100644 --- a/src/Model/MangaCollection.php +++ b/src/Model/MangaCollection.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/Model/Settings.php b/src/Model/Settings.php new file mode 100644 index 00000000..15156e5a --- /dev/null +++ b/src/Model/Settings.php @@ -0,0 +1,218 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Model; + +use const Aviat\AnimeClient\SETTINGS_MAP; + +use function Aviat\AnimeClient\arrayToToml; +use function Aviat\Ion\_dir; + +use Aviat\AnimeClient\Types\{Config, UndefinedPropertyException}; + +use Aviat\Ion\ConfigInterface; +use Aviat\Ion\Di\ContainerAware; +use Aviat\Ion\StringWrapper; + +/** + * Model for handling settings control panel + */ +final class Settings { + use ContainerAware; + use StringWrapper; + + private $config; + + public function __construct(ConfigInterface $config) + { + $this->config = $config; + } + + public function getSettings() + { + $settings = [ + 'config' => [], + ]; + + foreach(SETTINGS_MAP as $file => $values) + { + if ($file === 'config') + { + $keys = array_keys($values); + foreach($keys as $key) + { + $settings['config'][$key] = $this->config->get($key); + } + } + else + { + $settings[$file] = $this->config->get($file); + } + } + + return $settings; + } + + public function getSettingsForm() + { + $output = []; + + $settings = $this->getSettings(); + + foreach($settings as $file => $values) + { + $values = $values ?? []; + + foreach(SETTINGS_MAP[$file] as $key => $value) + { + if ($value['type'] === 'subfield') + { + foreach($value['fields'] as $k => $field) + { + if (empty($values[$key][$k])) + { + unset($value['fields'][$k]); + continue; + } + + $value['fields'][$k]['disabled'] = FALSE; + $value['fields'][$k]['display'] = TRUE; + $value['fields'][$k]['readonly'] = FALSE; + $value['fields'][$k]['value'] = $values[$key][$k] ?? ''; + } + } + + if (array_key_exists($key, $values) && is_scalar($values[$key])) + { + $value['value'] = $values[$key]; + } + else + { + $value['value'] = $value['default'] ?? ''; + } + + foreach (['readonly', 'disabled'] as $flag) + { + if ( ! array_key_exists($flag, $value)) + { + $value[$flag] = FALSE; + } + } + + if ( ! array_key_exists('display', $value)) + { + $value['display'] = TRUE; + } + + $output[$file][$key] = $value; + } + } + + return $output; + } + + public function validateSettings(array $settings) + { + $config = (new Config($settings))->toArray(); + + $looseConfig = []; + $keyedConfig = []; + + // Convert 'boolean' values to true and false + // Also order keys so they can be saved properly + foreach ($config as $key => $val) + { + if (is_scalar($val)) + { + if ($val === '1') + { + $looseConfig[$key] = TRUE; + } + elseif ($val === '0') + { + $looseConfig[$key] = FALSE; + } + else + { + $looseConfig[$key] = $val; + } + } + elseif (is_array($val) && ! empty($val)) + { + foreach($val as $k => $v) + { + if ($v === '1') + { + $keyedConfig[$key][$k] = TRUE; + } + elseif($v === '0') + { + $keyedConfig[$key][$k] = FALSE; + } + else + { + $keyedConfig[$key][$k] = $v; + } + } + } + } + + ksort($looseConfig); + ksort($keyedConfig); + + $output = []; + + foreach($looseConfig as $k => $v) + { + $output[$k] = $v; + } + + foreach($keyedConfig as $k => $v) + { + $output[$k] = $v; + } + + return $output; + } + + public function saveSettingsFile(array $settings): bool + { + $configWrapped = (count(array_keys($settings)) === 1 && array_key_exists('config', $settings)); + if ($configWrapped) + { + $settings = $settings['config']; + } + + try + { + $settings = $this->validateSettings($settings); + } + catch (UndefinedPropertyException $e) + { + dump($e); + dump($settings); + die(); + return FALSE; + } + + $savePath = realpath(_dir(__DIR__, '..', '..', 'app', 'config')); + $saveFile = _dir($savePath, 'admin-override.toml'); + + $saved = file_put_contents($saveFile, arrayToToml($settings)); + + return $saved !== FALSE; + } +} \ No newline at end of file diff --git a/src/RoutingBase.php b/src/RoutingBase.php index 315c7ac9..14780445 100644 --- a/src/RoutingBase.php +++ b/src/RoutingBase.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -44,12 +44,6 @@ class RoutingBase { */ protected $routes; - /** - * Route configuration options - * @var array - */ - protected $routeConfig; - /** * Constructor * @@ -63,20 +57,19 @@ class RoutingBase { $this->container = $container; $this->config = $container->get('config'); $this->routes = $this->config->get('routes'); - $this->routeConfig = $this->config->get('route_config'); } /** * Retrieve the appropriate value for the routing key * - * @param string $key + * @param string|int|array $key * @return mixed */ public function __get($key) { - if (array_key_exists($key, $this->routeConfig)) + if ($this->config->has($key)) { - return $this->routeConfig[$key]; + return $this->config->get($key); } } diff --git a/src/Types/AbstractType.php b/src/Types/AbstractType.php index f4dd427d..ec00bca8 100644 --- a/src/Types/AbstractType.php +++ b/src/Types/AbstractType.php @@ -2,31 +2,30 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ namespace Aviat\AnimeClient\Types; use ArrayAccess; -use LogicException; abstract class AbstractType implements ArrayAccess { /** - * Populate values for unserializing data + * Populate values for un-serializing data * * @param $properties * @return mixed */ - public static function __set_state($properties) + public static function __set_state($properties): self { return new static($properties); } @@ -83,11 +82,11 @@ abstract class AbstractType implements ArrayAccess { return; } - if (!property_exists($this, $name)) + if ( ! property_exists($this, $name)) { $existing = json_encode($this); - throw new LogicException("Trying to set non-existent property: '$name'. Existing properties: $existing"); + throw new UndefinedPropertyException("Trying to set undefined property: '$name'. Existing properties: $existing"); } $this->$name = $value; @@ -106,7 +105,7 @@ abstract class AbstractType implements ArrayAccess { return $this->$name; } - throw new LogicException("Trying to get non-existent property: '$name'"); + throw new UndefinedPropertyException("Trying to get undefined property: '$name'"); } /** @@ -154,4 +153,31 @@ abstract class AbstractType implements ArrayAccess { unset($this->$offset); } } + + /** + * Recursively cast properties to an array + * + * @param null $parent + * @return mixed + */ + public function toArray($parent = null) + { + $object = $parent ?? $this; + + if (is_scalar($object) || empty($object)) + { + return $object; + } + + $output = []; + + foreach ($object as $key => $value) + { + $output[$key] = (is_scalar($value) || empty($value)) + ? $value + : $this->toArray((array) $value); + } + + return $output; + } } \ No newline at end of file diff --git a/src/Types/Anime.php b/src/Types/Anime.php index d3fd573a..2ca4a9ef 100644 --- a/src/Types/Anime.php +++ b/src/Types/Anime.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/Types/AnimeFormItem.php b/src/Types/AnimeFormItem.php deleted file mode 100644 index 69a6f501..00000000 --- a/src/Types/AnimeFormItem.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @copyright 2015 - 2018 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient\Types; - -/** - * Type representing an Anime object for display - */ -final class AnimeFormItem extends FormItem { - public function setData($value): void - { - $this->data = new AnimeFormItemData($value); - } -} diff --git a/src/Types/AnimeListItem.php b/src/Types/AnimeListItem.php index 0e80b04d..1b5fea74 100644 --- a/src/Types/AnimeListItem.php +++ b/src/Types/AnimeListItem.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -22,6 +22,7 @@ namespace Aviat\AnimeClient\Types; final class AnimeListItem extends AbstractType { public $id; public $mal_id; + public $anilist_item_id; public $episodes = [ 'length' => 0, 'total' => 0, @@ -33,10 +34,10 @@ final class AnimeListItem extends AbstractType { 'ended' => '', ]; public $anime; - public $watching_status; - public $notes; + public $notes = ''; + public $private; public $rewatching; public $rewatched; public $user_rating; - public $private; + public $watching_status; } diff --git a/src/Types/Config.php b/src/Types/Config.php new file mode 100644 index 00000000..ca031217 --- /dev/null +++ b/src/Types/Config.php @@ -0,0 +1,63 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Types; + +class Config extends AbstractType { + // Config files/namespaces + public $anilist; + public $cache; + public $database; + + // Settings in config.toml + public $asset_path; // Path to public folder for urls + public $default_anime_list_path; + public $default_list; + public $default_manga_list_path; + public $default_view_type; + public $kitsu_username; + public $secure_urls = TRUE; + public $show_anime_collection; + public $show_manga_collection; + public $whose_list; + + // Application config + public $menus; + public $routes; + + // Generated config values + public $asset_dir; // Path to public folder for local files + public $base_config_dir; + public $config_dir; + public $data_cache_path; + public $img_cache_path; + public $view_path; + + public function setAnilist ($data): void + { + $this->anilist = new Config\Anilist($data); + } + + public function setCache ($data): void + { + $this->cache = new Config\Cache($data); + } + + public function setDatabase ($data): void + { + $this->database = new Config\Database($data); + } +} \ No newline at end of file diff --git a/src/Types/Config/Anilist.php b/src/Types/Config/Anilist.php new file mode 100644 index 00000000..0636e478 --- /dev/null +++ b/src/Types/Config/Anilist.php @@ -0,0 +1,32 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Types\Config; + +use Aviat\AnimeClient\Types\AbstractType; + +class Anilist extends AbstractType { + public $enabled = FALSE; + + public $client_id; + public $client_secret; + + public $access_token; + public $access_token_expires; + public $refresh_token; + + public $username; +} \ No newline at end of file diff --git a/src/Types/Config/Cache.php b/src/Types/Config/Cache.php new file mode 100644 index 00000000..929b4531 --- /dev/null +++ b/src/Types/Config/Cache.php @@ -0,0 +1,32 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Types\Config; + +use Aviat\AnimeClient\Types\AbstractType; + +class Cache extends AbstractType { + public $driver; + public $connection = []; + public $options = []; + + /* public function setConnection($data): void + { + $this->connection = new class($data) extends AbstractType { + + }; + } */ +} \ No newline at end of file diff --git a/src/Types/AnimeFormItemData.php b/src/Types/Config/Database.php similarity index 53% rename from src/Types/AnimeFormItemData.php rename to src/Types/Config/Database.php index 3befff10..8c807caa 100644 --- a/src/Types/AnimeFormItemData.php +++ b/src/Types/Config/Database.php @@ -2,21 +2,28 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ -namespace Aviat\AnimeClient\Types; +namespace Aviat\AnimeClient\Types\Config; -/** - * Type representing an Anime object for display - */ -final class AnimeFormItemData extends FormItemData {} +use Aviat\AnimeClient\Types\AbstractType; + +class Database extends AbstractType { + public $type; + public $host; + public $user; + public $pass; + public $port; + public $database; + public $file; +} \ No newline at end of file diff --git a/src/Types/FormItem.php b/src/Types/FormItem.php index f5a3a5d8..7a309231 100644 --- a/src/Types/FormItem.php +++ b/src/Types/FormItem.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -19,11 +19,15 @@ namespace Aviat\AnimeClient\Types; /** * Type representing an Anime object for display */ -abstract class FormItem extends AbstractType { +class FormItem extends AbstractType { public $id; + public $anilist_item_id; public $mal_id; public $data; - abstract public function setData($value): void; + public function setData($value): void + { + $this->data = new FormItemData($value); + } } diff --git a/src/Types/FormItemData.php b/src/Types/FormItemData.php index 1539733e..7c1e635f 100644 --- a/src/Types/FormItemData.php +++ b/src/Types/FormItemData.php @@ -2,29 +2,31 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ namespace Aviat\AnimeClient\Types; /** - * Type representing an Anime object for display + * Type representing a Media object for editing/syncing */ -abstract class FormItemData extends AbstractType { +class FormItemData extends AbstractType { public $notes; public $private; public $progress; public $rating; + public $ratingTwenty; public $reconsumeCount; public $reconsuming; public $status; + public $updatedAt; } diff --git a/src/Types/MangaFormItem.php b/src/Types/MangaFormItem.php deleted file mode 100644 index 6b630c1a..00000000 --- a/src/Types/MangaFormItem.php +++ /dev/null @@ -1,28 +0,0 @@ - - * @copyright 2015 - 2018 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient\Types; - -/** - * Form data for updating a Manga List item - */ -final class MangaFormItem extends FormItem { - public function setData($value): void - { - $this->data = new MangaFormItemData($value); - } -} - diff --git a/src/Types/MangaListItem.php b/src/Types/MangaListItem.php index 2da37135..c4ce3943 100644 --- a/src/Types/MangaListItem.php +++ b/src/Types/MangaListItem.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/Types/MangaListItemDetail.php b/src/Types/MangaListItemDetail.php index 163901a6..6be6e8ab 100644 --- a/src/Types/MangaListItemDetail.php +++ b/src/Types/MangaListItemDetail.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/Types/MangaPage.php b/src/Types/MangaPage.php index 5734d1a5..3007280c 100644 --- a/src/Types/MangaPage.php +++ b/src/Types/MangaPage.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -22,14 +22,13 @@ namespace Aviat\AnimeClient\Types; final class MangaPage extends AbstractType { public $chapter_count; public $cover_image; - public $en_title; public $genres; public $id; public $included; - public $jp_title; public $manga_type; public $synopsis; public $title; + public $titles; public $url; public $volume_count; } diff --git a/src/Types/MangaFormItemData.php b/src/Types/UndefinedPropertyException.php similarity index 69% rename from src/Types/MangaFormItemData.php rename to src/Types/UndefinedPropertyException.php index 64c9632d..ff46700b 100644 --- a/src/Types/MangaFormItemData.php +++ b/src/Types/UndefinedPropertyException.php @@ -2,18 +2,20 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ namespace Aviat\AnimeClient\Types; -final class MangaFormItemData extends FormItemData {} +use LogicException; + +class UndefinedPropertyException extends LogicException {} \ No newline at end of file diff --git a/src/UrlGenerator.php b/src/UrlGenerator.php index df30409e..3a1b6193 100644 --- a/src/UrlGenerator.php +++ b/src/UrlGenerator.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -34,19 +34,20 @@ class UrlGenerator extends RoutingBase { * Constructor * * @param ContainerInterface $container - * @throws \Aviat\Ion\Di\ContainerException - * @throws \Aviat\Ion\Di\NotFoundException + * @throws \Aviat\Ion\Di\Exception\ContainerException + * @throws \Aviat\Ion\Di\Exception\NotFoundException */ public function __construct(ContainerInterface $container) { parent::__construct($container); + $this->host = $container->get('request')->getServerParams()['HTTP_HOST']; } /** * Get the base url for css/js/images * - * @param string[] ...$args + * @param string ...$args * @return string */ public function assetUrl(string ...$args): string @@ -88,7 +89,9 @@ class UrlGenerator extends RoutingBase { } $path = implode('/', $path_segments); - return "//{$this->host}/{$path}"; + $scheme = $this->config->get('secure_urls') !== FALSE ? 'https:' : 'http:'; + + return "{$scheme}//{$this->host}/{$path}"; } /** diff --git a/src/Util.php b/src/Util.php index 5c3fab63..02a80a3b 100644 --- a/src/Util.php +++ b/src/Util.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/src/constants.php b/src/constants.php index 69142d9a..e2fe0b9d 100644 --- a/src/constants.php +++ b/src/constants.php @@ -2,21 +2,21 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ namespace Aviat\AnimeClient; -const DEFAULT_CONTROLLER = Controller\Index::class; +const DEFAULT_CONTROLLER = Controller\Misc::class; const DEFAULT_CONTROLLER_METHOD = 'index'; const DEFAULT_CONTROLLER_NAMESPACE = Controller::class; const DEFAULT_LIST_CONTROLLER = Controller\Anime::class; @@ -24,4 +24,226 @@ const ERROR_MESSAGE_METHOD = 'errorPage'; const NOT_FOUND_METHOD = 'notFound'; const SESSION_SEGMENT = 'Aviat\AnimeClient\Auth'; const SRC_DIR = __DIR__; -const USER_AGENT = "Tim's Anime Client/4.0"; \ No newline at end of file +const USER_AGENT = "Tim's Anime Client/4.1"; + +// Regex patterns +const ALPHA_SLUG_PATTERN = '[a-z_]+'; +const NUM_PATTERN = '[0-9]+'; +const SLUG_PATTERN = '[a-z0-9\-]+'; + +// Why doesn't this already exist? +const MILLI_FROM_NANO = 1000 * 1000; + +/** + * Map config settings to form fields + */ +const SETTINGS_MAP = [ + 'anilist' => [ + 'enabled' => [ + 'type' => 'boolean', + 'title' => 'Enable Anilist Integration', + 'default' => FALSE, + 'description' => 'Enable syncing data between Kitsu and Anilist. Requires appropriate API keys to be set in config', + ], + 'client_id' => [ + 'type' => 'string', + 'title' => 'Anilist API Client ID', + 'default' => '', + 'description' => 'The client id for your Anilist API application', + ], + 'client_secret' => [ + 'type' => 'string', + 'title' => 'Anilist API Client Secret', + 'default' => '', + 'description' => 'The client secret for your Anilist API application', + ], + 'username' => [ + 'type' => 'string', + 'title' => 'Anilist Username', + 'default' => '', + 'readonly' => TRUE, + 'description' => 'Login username for Anilist account to integrate with', + ], + 'access_token' => [ + 'type' => 'hidden', + 'title' => 'API Access Token', + 'default' => '', + 'description' => 'The Access code for accessing the Anilist API', + 'readonly' => TRUE, + ], + 'access_token_expires' => [ + 'type' => 'string', + 'title' => 'Expiration timestamp of the access token', + 'default' => '0', + 'description' => 'The unix timestamp of when the access token expires.', + 'readonly' => TRUE, + ], + 'refresh_token' => [ + 'type' => 'string', + 'title' => 'API Refresh Token', + 'default' => '', + 'description' => 'Token to refresh the access token before it expires', + 'readonly' => TRUE, + ], + ], + + 'cache' => [ + 'driver' => [ + 'type' => 'select', + 'title' => 'Cache Type', + 'description' => 'The Cache backend', + 'options' => [ + 'APCu' => 'apcu', + 'Memcached' => 'memcached', + 'Redis' => 'redis', + 'No Cache' => 'null' + ], + ], + 'connection' => [ + 'type' => 'subfield', + 'title' => 'Connection', + 'fields' => [ + 'host' => [ + 'type' => 'string', + 'title' => 'Cache Host', + 'description' => 'Host of the cache backend to connect to', + ], + 'port' => [ + 'type' => 'string', + 'title' => 'Cache Port', + 'description' => 'Port of the cache backend to connect to', + 'default' => NULL, + ], + 'password' => [ + 'type' => 'string', + 'title' => 'Cache Password', + 'description' => 'Password to connect to cache backend', + 'default' => NULL, + ], + 'persistent' => [ + 'type' => 'boolean', + 'title' => 'Persistent Cache Connection', + 'description' => 'Whether to have a persistent connection to the cache', + 'default' => FALSE, + ], + 'database' => [ + 'type' => 'string', + 'title' => 'Cache Database', + 'default' => '1', + 'description' => 'Cache database number for Redis', + ], + ], + ], + /* 'options' => [ + 'type' => 'subfield', + 'title' => 'Options', + 'fields' => [], + ] */ + ], + 'config' => [ + 'kitsu_username' => [ + 'type' => 'string', + 'title' => 'Kitsu Username', + 'default' => '', + 'description' => 'Username of the account to pull list data from.', + ], + 'whose_list' => [ + 'type' => 'string', + 'title' => 'Whose List', + 'default' => 'Somebody', + 'description' => 'Name of the owner of the list data.', + ], + 'show_anime_collection' => [ + 'type' => 'boolean', + 'title' => 'Show Anime Collection', + 'default' => FALSE, + 'description' => 'Should the anime collection be shown?', + ], + 'show_manga_collection' => [ + 'type' => 'boolean', + 'title' => 'Show Manga Collection', + 'default' => FALSE, + 'description' => 'Should the manga collection be shown?', + ], + 'default_list' => [ + 'type' => 'select', + 'title' => 'Default List', + 'description' => 'Which list to show by default.', + 'options' => [ + 'Anime' => 'anime', + 'Manga' => 'manga', + ], + ], + 'default_anime_list_path' => [ //watching|plan_to_watch|on_hold|dropped|completed|all + 'type' => 'select', + 'title' => 'Default Anime List Section', + 'description' => 'Which part of the anime list to show by default.', + 'options' => [ + 'Watching' => 'watching', + 'Plan to Watch' => 'plan_to_watch', + 'On Hold' => 'on_hold', + 'Dropped' => 'dropped', + 'Completed' => 'completed', + 'All' => 'all', + ] + ], + 'default_manga_list_path' => [ //reading|plan_to_read|on_hold|dropped|completed|all + 'type' => 'select', + 'title' => 'Default Manga List Section', + 'description' => 'Which part of the manga list to show by default.', + 'options' => [ + 'Reading' => 'reading', + 'Plan to Read' => 'plan_to_read', + 'On Hold' => 'on_hold', + 'Dropped' => 'dropped', + 'Completed' => 'completed', + 'All' => 'all', + ] + ] + ], + 'database' => [ + 'type' => [ + 'type' => 'select', + 'title' => 'Database Type', + 'options' => [ + 'MySQL' => 'mysql', + 'PostgreSQL' => 'pgsql', + 'SQLite' => 'sqlite', + ], + 'default' => 'sqlite', + 'description' => 'Type of database to connect to', + ], + 'host' => [ + 'type' => 'string', + 'title' => 'Host', + 'description' => 'The host of the database server', + ], + 'user' => [ + 'type' => 'string', + 'title' => 'User', + 'description' => 'Database connection user', + ], + 'pass' => [ + 'type' => 'string', + 'title' => 'Password', + 'description' => 'Database connection password' + ], + 'port' => [ + 'type' => 'string', + 'title' => 'Port', + 'description' => 'Database connection port', + 'default' => NULL, + ], + 'database' => [ + 'type' => 'string', + 'title' => 'Database Name', + 'description' => 'Name of the database/schema to connect to', + ], + 'file' => [ + 'type' => 'string', + 'title' => 'Database File', + 'description' => 'Path to the database file, if required by the current database type.', + 'default' => 'anime_collection.sqlite', + ], + ], +]; \ No newline at end of file diff --git a/sw.js b/sw.js new file mode 100644 index 00000000..9113d9bc --- /dev/null +++ b/sw.js @@ -0,0 +1,81 @@ +const CACHE_NAME = 'hummingbird-anime-client'; + +async function fromCache (request) { + const cache = await caches.open(CACHE_NAME); + return await cache.match(request); +} + +async function fromNetwork (request) { + return await fetch(request); +} + +async function update (request) { + const cache = await caches.open(CACHE_NAME); + const response = await fetch(request); + + if (request.url.includes('/public/images/')) { + console.log('Saving to cache: ', request.url); + await cache.put(request, response.clone()); + } + + return response; +} + +/* function refresh (response) { + return self.clients.matchAll().then(clients => { + clients.forEach(client => { + const message = { + type: 'refresh', + url: response.url, + eTag: response.headers.get('ETag') + }; + + client.postMessage(JSON.stringify(message)); + }) + }); +} */ + +self.addEventListener('install', event => { + console.log('Public Folder Worker installed'); + + event.waitUntil( + caches.open(CACHE_NAME) + .then(cache => cache.addAll([ + 'public/images/icons/favicon.ico', + 'public/images/streaming-logos/amazon.svg', + 'public/images/streaming-logos/crunchyroll.svg', + 'public/images/streaming-logos/daisuki.svg', + 'public/images/streaming-logos/funimation.svg', + 'public/images/streaming-logos/hidive.svg', + 'public/images/streaming-logos/hulu.svg', + 'public/images/streaming-logos/netflix.svg', + 'public/images/streaming-logos/tubitv.svg', + 'public/images/streaming-logos/viewster.svg', + ])) + ) +}); + +self.addEventListener('activate', event => { + console.log('Public Folder Worker activated'); +}); + +// Pull css, images, and javascript from cache +self.addEventListener('fetch', event => { + // Only cache things with a file extension, + // Ignore other requests + if ( ! event.request.url.includes('/public/')) { + return; + } + + fromCache(event.request).then(cached => { + if (cached !== undefined) { + event.respondWith(cached); + } else { + event.respondWith(fromNetwork(event.request)); + } + }); + + event.waitUntil( + update(event.request) + ); +}); \ No newline at end of file diff --git a/tests/API/APIRequestBuilderTest.php b/tests/API/APIRequestBuilderTest.php index a3afc8cc..e654b5ce 100644 --- a/tests/API/APIRequestBuilderTest.php +++ b/tests/API/APIRequestBuilderTest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/tests/API/CacheTraitTest.php b/tests/API/CacheTraitTest.php index 7fc03e8f..427b14c0 100644 --- a/tests/API/CacheTraitTest.php +++ b/tests/API/CacheTraitTest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/tests/API/JsonAPITest.php b/tests/API/JsonAPITest.php index e72dd09f..ac08c1be 100644 --- a/tests/API/JsonAPITest.php +++ b/tests/API/JsonAPITest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -21,11 +21,11 @@ use Aviat\Ion\Json; use PHPUnit\Framework\TestCase; class JsonAPITest extends TestCase { - + protected $startData; protected $organizedIncludes; protected $inlineIncluded; - + public function setUp() { $dir = __DIR__ . '/../test_data/JsonAPI'; @@ -33,20 +33,24 @@ class JsonAPITest extends TestCase { $this->organizedIncludes = Json::decodeFile("{$dir}/organizedIncludes.json"); $this->inlineIncluded = Json::decodeFile("{$dir}/inlineIncluded.json"); } - + public function testOrganizeIncludes() { $expected = $this->organizedIncludes; $actual = JsonAPI::organizeIncludes($this->startData['included']); + // file_put_contents(__DIR__ . '/../test_data/JsonAPI/organizedIncludes.json', json_Encode($actual)); + $this->assertEquals($expected, $actual); } - + public function testInlineIncludedRelationships() { $expected = $this->inlineIncluded; $actual = JsonAPI::inlineIncludedRelationships($this->organizedIncludes, 'anime'); - + + // file_put_contents(__DIR__ . '/../test_data/JsonAPI/inlineIncluded.json', json_Encode($actual)); + $this->assertEquals($expected, $actual); } } \ No newline at end of file diff --git a/tests/API/Kitsu/Transformer/AnimeListTransformerTest.php b/tests/API/Kitsu/Transformer/AnimeListTransformerTest.php index 2f062339..c8ef8009 100644 --- a/tests/API/Kitsu/Transformer/AnimeListTransformerTest.php +++ b/tests/API/Kitsu/Transformer/AnimeListTransformerTest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/tests/API/Kitsu/Transformer/AnimeTransformerTest.php b/tests/API/Kitsu/Transformer/AnimeTransformerTest.php index fc418842..9eb8423e 100644 --- a/tests/API/Kitsu/Transformer/AnimeTransformerTest.php +++ b/tests/API/Kitsu/Transformer/AnimeTransformerTest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/tests/API/Kitsu/Transformer/MangaListTransformerTest.php b/tests/API/Kitsu/Transformer/MangaListTransformerTest.php index d4d1394e..7e60ef24 100644 --- a/tests/API/Kitsu/Transformer/MangaListTransformerTest.php +++ b/tests/API/Kitsu/Transformer/MangaListTransformerTest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -20,8 +20,8 @@ use Aviat\AnimeClient\API\JsonAPI; use Aviat\AnimeClient\API\Kitsu\Transformer\MangaListTransformer; use Aviat\AnimeClient\Tests\AnimeClientTestCase; use Aviat\AnimeClient\Types\{ - MangaFormItem, - MangaFormItemData + FormItem, + FormItemData }; use Aviat\Ion\Json; @@ -86,16 +86,16 @@ class MangaListTransformerTest extends AnimeClientTestCase { ]; $actual = $this->transformer->untransform($input); - $expected = new MangaFormItem([ + $expected = new FormItem([ 'id' => '15084773', 'mal_id' => '26769', - 'data' => new MangaFormItemData([ + 'data' => new FormItemData([ 'status' => 'current', 'progress' => 67, 'reconsuming' => false, 'reconsumeCount' => 0, 'notes' => '', - 'rating' => 4.5 + 'ratingTwenty' => 18, ]) ]); diff --git a/tests/API/Kitsu/Transformer/MangaTransformerTest.php b/tests/API/Kitsu/Transformer/MangaTransformerTest.php index 6185d6e4..430fc72f 100644 --- a/tests/API/Kitsu/Transformer/MangaTransformerTest.php +++ b/tests/API/Kitsu/Transformer/MangaTransformerTest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #0__1.php b/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #0__1.php index eef4044d..4b97af5e 100644 --- a/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #0__1.php +++ b/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #0__1.php @@ -1,12 +1,13 @@ - 14047981, + 'anilist_item_id' => NULL, 'mal_id' => NULL, - 'data' => - Aviat\AnimeClient\Types\AnimeFormItemData::__set_state(array( + 'data' => + Aviat\AnimeClient\Types\FormItemData::__set_state(array( 'notes' => 'Very formulaic.', 'private' => false, 'progress' => 38, - 'rating' => 4, + 'ratingTwenty' => 16, 'reconsumeCount' => 0, 'reconsuming' => false, 'status' => 'current', diff --git a/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #1__1.php b/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #1__1.php index 05b25d9f..ca751d41 100644 --- a/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #1__1.php +++ b/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #1__1.php @@ -1,12 +1,13 @@ - 14047981, + 'anilist_item_id' => NULL, 'mal_id' => '12345', - 'data' => - Aviat\AnimeClient\Types\AnimeFormItemData::__set_state(array( + 'data' => + Aviat\AnimeClient\Types\FormItemData::__set_state(array( 'notes' => 'Very formulaic.', 'private' => true, 'progress' => 38, - 'rating' => 4, + 'ratingTwenty' => 16, 'reconsumeCount' => 0, 'reconsuming' => true, 'status' => 'current', diff --git a/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #2__1.php b/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #2__1.php index 0fb53da4..9375de10 100644 --- a/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #2__1.php +++ b/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set #2__1.php @@ -1,8 +1,9 @@ - 14047983, + 'anilist_item_id' => NULL, 'mal_id' => '12347', - 'data' => - Aviat\AnimeClient\Types\AnimeFormItemData::__set_state(array( + 'data' => + Aviat\AnimeClient\Types\FormItemData::__set_state(array( 'notes' => '', 'private' => true, 'progress' => 12, diff --git a/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set 0__1.php b/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set 0__1.php new file mode 100644 index 00000000..f37516c9 --- /dev/null +++ b/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set 0__1.php @@ -0,0 +1,15 @@ + 14047981, + 'anilist_item_id' => NULL, + 'mal_id' => NULL, + 'data' => + Aviat\AnimeClient\Types\FormItemData::__set_state(array( + 'notes' => 'Very formulaic.', + 'private' => false, + 'progress' => 38, + 'ratingTwenty' => 16, + 'reconsumeCount' => 0, + 'reconsuming' => false, + 'status' => 'current', + )), +)); diff --git a/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set 1__1.php b/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set 1__1.php new file mode 100644 index 00000000..672b1d43 --- /dev/null +++ b/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set 1__1.php @@ -0,0 +1,15 @@ + 14047981, + 'anilist_item_id' => NULL, + 'mal_id' => '12345', + 'data' => + Aviat\AnimeClient\Types\FormItemData::__set_state(array( + 'notes' => 'Very formulaic.', + 'private' => true, + 'progress' => 38, + 'ratingTwenty' => 16, + 'reconsumeCount' => 0, + 'reconsuming' => true, + 'status' => 'current', + )), +)); diff --git a/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set 2__1.php b/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set 2__1.php new file mode 100644 index 00000000..e0e27ff4 --- /dev/null +++ b/tests/API/Kitsu/Transformer/__snapshots__/AnimeListTransformerTest__testUntransform with data set 2__1.php @@ -0,0 +1,14 @@ + 14047983, + 'anilist_item_id' => NULL, + 'mal_id' => '12347', + 'data' => + Aviat\AnimeClient\Types\FormItemData::__set_state(array( + 'notes' => '', + 'private' => true, + 'progress' => 12, + 'reconsumeCount' => 0, + 'reconsuming' => true, + 'status' => 'current', + )), +)); diff --git a/tests/API/Kitsu/Transformer/__snapshots__/AnimeTransformerTest__testTransform__1.php b/tests/API/Kitsu/Transformer/__snapshots__/AnimeTransformerTest__testTransform__1.php index 5b4ac5cb..5f47eb8d 100644 --- a/tests/API/Kitsu/Transformer/__snapshots__/AnimeTransformerTest__testTransform__1.php +++ b/tests/API/Kitsu/Transformer/__snapshots__/AnimeTransformerTest__testTransform__1.php @@ -8,6 +8,214 @@ array ( ), 'id' => 32344, + 'included' => + array ( + 'categories' => + array ( + 23 => + array ( + 'name' => 'Super Power', + 'slug' => 'super-power', + 'description' => NULL, + ), + 11 => + array ( + 'name' => 'Fantasy', + 'slug' => 'fantasy', + 'description' => '', + ), + 4 => + array ( + 'name' => 'Drama', + 'slug' => 'drama', + 'description' => '', + ), + 1 => + array ( + 'name' => 'Action', + 'slug' => 'action', + 'description' => '', + ), + ), + 'mappings' => + array ( + 5686 => + array ( + 'externalSite' => 'myanimelist/anime', + 'externalId' => '16498', + 'relationships' => + array ( + 'media' => + array ( + 'links' => + array ( + 'self' => 'https://kitsu.io/api/edge/mappings/5686/relationships/media', + 'related' => 'https://kitsu.io/api/edge/mappings/5686/media', + ), + ), + ), + ), + 14153 => + array ( + 'externalSite' => 'thetvdb/series', + 'externalId' => '267440', + 'relationships' => + array ( + 'media' => + array ( + 'links' => + array ( + 'self' => 'https://kitsu.io/api/edge/mappings/14153/relationships/media', + 'related' => 'https://kitsu.io/api/edge/mappings/14153/media', + ), + ), + ), + ), + 15073 => + array ( + 'externalSite' => 'thetvdb/season', + 'externalId' => '514060', + 'relationships' => + array ( + 'media' => + array ( + 'links' => + array ( + 'self' => 'https://kitsu.io/api/edge/mappings/15073/relationships/media', + 'related' => 'https://kitsu.io/api/edge/mappings/15073/media', + ), + ), + ), + ), + ), + 'streamingLinks' => + array ( + 103 => + array ( + 'url' => 'http://www.crunchyroll.com/attack-on-titan', + 'subs' => + array ( + 0 => 'en', + ), + 'dubs' => + array ( + 0 => 'ja', + ), + 'relationships' => + array ( + 'streamer' => + array ( + 'links' => + array ( + 'self' => 'https://kitsu.io/api/edge/streaming-links/103/relationships/streamer', + 'related' => 'https://kitsu.io/api/edge/streaming-links/103/streamer', + ), + ), + 'media' => + array ( + 'links' => + array ( + 'self' => 'https://kitsu.io/api/edge/streaming-links/103/relationships/media', + 'related' => 'https://kitsu.io/api/edge/streaming-links/103/media', + ), + ), + ), + ), + 102 => + array ( + 'url' => 'http://www.hulu.com/attack-on-titan', + 'subs' => + array ( + 0 => 'en', + ), + 'dubs' => + array ( + 0 => 'ja', + ), + 'relationships' => + array ( + 'streamer' => + array ( + 'links' => + array ( + 'self' => 'https://kitsu.io/api/edge/streaming-links/102/relationships/streamer', + 'related' => 'https://kitsu.io/api/edge/streaming-links/102/streamer', + ), + ), + 'media' => + array ( + 'links' => + array ( + 'self' => 'https://kitsu.io/api/edge/streaming-links/102/relationships/media', + 'related' => 'https://kitsu.io/api/edge/streaming-links/102/media', + ), + ), + ), + ), + 101 => + array ( + 'url' => 'http://www.funimation.com/shows/attack-on-titan/videos/episodes', + 'subs' => + array ( + 0 => 'en', + ), + 'dubs' => + array ( + 0 => 'ja', + ), + 'relationships' => + array ( + 'streamer' => + array ( + 'links' => + array ( + 'self' => 'https://kitsu.io/api/edge/streaming-links/101/relationships/streamer', + 'related' => 'https://kitsu.io/api/edge/streaming-links/101/streamer', + ), + ), + 'media' => + array ( + 'links' => + array ( + 'self' => 'https://kitsu.io/api/edge/streaming-links/101/relationships/media', + 'related' => 'https://kitsu.io/api/edge/streaming-links/101/media', + ), + ), + ), + ), + 100 => + array ( + 'url' => 't', + 'subs' => + array ( + 0 => 'en', + ), + 'dubs' => + array ( + 0 => 'ja', + ), + 'relationships' => + array ( + 'streamer' => + array ( + 'links' => + array ( + 'self' => 'https://kitsu.io/api/edge/streaming-links/100/relationships/streamer', + 'related' => 'https://kitsu.io/api/edge/streaming-links/100/streamer', + ), + ), + 'media' => + array ( + 'links' => + array ( + 'self' => 'https://kitsu.io/api/edge/streaming-links/100/relationships/media', + 'related' => 'https://kitsu.io/api/edge/streaming-links/100/media', + ), + ), + ), + ), + ), + ), 'show_type' => 'TV', 'slug' => 'attack-on-titan', 'status' => 'Finished Airing', @@ -35,11 +243,11 @@ array ( 'meta' => array ( - 'name' => 'Hulu', + 'name' => 'Funimation', 'link' => true, - 'image' => 'streaming-logos/hulu.svg', + 'image' => 'streaming-logos/funimation.svg', ), - 'link' => 'http://www.hulu.com/attack-on-titan', + 'link' => 'http://www.funimation.com/shows/attack-on-titan/videos/episodes', 'subs' => array ( 0 => 'en', @@ -53,11 +261,11 @@ array ( 'meta' => array ( - 'name' => 'Funimation', + 'name' => 'Hulu', 'link' => true, - 'image' => 'streaming-logos/funimation.svg', + 'image' => 'streaming-logos/hulu.svg', ), - 'link' => 'http://www.funimation.com/shows/attack-on-titan/videos/episodes', + 'link' => 'http://www.hulu.com/attack-on-titan', 'subs' => array ( 0 => 'en', @@ -92,8 +300,9 @@ 'title' => 'Attack on Titan', 'titles' => array ( - 'en_jp' => 'Shingeki no Kyojin', - 'ja_jp' => '進撃の巨人', + 0 => 'Attack on Titan', + 1 => 'Shingeki no Kyojin', + 2 => '進撃の巨人', ), 'trailer_id' => 'n4Nj6Y_SNYI', 'url' => 'https://kitsu.io/anime/attack-on-titan', diff --git a/tests/API/Kitsu/Transformer/__snapshots__/MangaListTransformerTest__testTransform__1.php b/tests/API/Kitsu/Transformer/__snapshots__/MangaListTransformerTest__testTransform__1.php index 1757e57c..8d1c950f 100644 --- a/tests/API/Kitsu/Transformer/__snapshots__/MangaListTransformerTest__testTransform__1.php +++ b/tests/API/Kitsu/Transformer/__snapshots__/MangaListTransformerTest__testTransform__1.php @@ -30,14 +30,14 @@ 'titles' => array ( ), - 'type' => 'manga', + 'type' => 'Manga', 'url' => 'https://kitsu.io/manga/bokura-wa-minna-kawaisou', )), 'reading_status' => 'current', 'notes' => '', 'rereading' => false, 'reread' => 0, - 'user_rating' => 9.0, + 'user_rating' => 9, )), 1 => Aviat\AnimeClient\Types\MangaListItem::__set_state(array( @@ -70,14 +70,14 @@ 'titles' => array ( ), - 'type' => 'manga', + 'type' => 'Manga', 'url' => 'https://kitsu.io/manga/love-hina', )), 'reading_status' => 'current', 'notes' => '', 'rereading' => false, 'reread' => 0, - 'user_rating' => 7.0, + 'user_rating' => 7, )), 2 => Aviat\AnimeClient\Types\MangaListItem::__set_state(array( @@ -113,14 +113,14 @@ array ( 0 => 'Yamada-kun and the Seven Witches', ), - 'type' => 'manga', + 'type' => 'Manga', 'url' => 'https://kitsu.io/manga/yamada-kun-to-7-nin-no-majo', )), 'reading_status' => 'current', 'notes' => '', 'rereading' => false, 'reread' => 0, - 'user_rating' => 9.0, + 'user_rating' => 9, )), 3 => Aviat\AnimeClient\Types\MangaListItem::__set_state(array( @@ -151,7 +151,7 @@ 'titles' => array ( ), - 'type' => 'manga', + 'type' => 'Manga', 'url' => 'https://kitsu.io/manga/relife', )), 'reading_status' => 'current', @@ -189,13 +189,13 @@ 'titles' => array ( ), - 'type' => 'manga', + 'type' => 'Manga', 'url' => 'https://kitsu.io/manga/joshikausei', )), 'reading_status' => 'current', 'notes' => '', 'rereading' => false, 'reread' => 0, - 'user_rating' => 8.0, + 'user_rating' => 8, )), ); diff --git a/tests/API/Kitsu/Transformer/__snapshots__/MangaTransformerTest__testTransform__1.php b/tests/API/Kitsu/Transformer/__snapshots__/MangaTransformerTest__testTransform__1.php index fa06654c..983cbbc7 100644 --- a/tests/API/Kitsu/Transformer/__snapshots__/MangaTransformerTest__testTransform__1.php +++ b/tests/API/Kitsu/Transformer/__snapshots__/MangaTransformerTest__testTransform__1.php @@ -1,16 +1,80 @@ '-', 'cover_image' => 'https://media.kitsu.io/manga/poster_images/20286/small.jpg?1434293999', - 'en_title' => NULL, 'genres' => array ( ), 'id' => '20286', - 'jp_title' => 'Bokura wa Minna Kawaisou', + 'included' => + array ( + 'genres' => + array ( + 3 => + array ( + 'attributes' => + array ( + 'name' => 'Comedy', + 'slug' => 'comedy', + 'description' => NULL, + ), + ), + 24 => + array ( + 'attributes' => + array ( + 'name' => 'School', + 'slug' => 'school', + 'description' => NULL, + ), + ), + 16 => + array ( + 'attributes' => + array ( + 'name' => 'Slice of Life', + 'slug' => 'slice-of-life', + 'description' => '', + ), + ), + 14 => + array ( + 'attributes' => + array ( + 'name' => 'Romance', + 'slug' => 'romance', + 'description' => '', + ), + ), + 18 => + array ( + 'attributes' => + array ( + 'name' => 'Thriller', + 'slug' => 'thriller', + 'description' => NULL, + ), + ), + ), + 'mappings' => + array ( + 48014 => + array ( + 'attributes' => + array ( + 'externalSite' => 'myanimelist/manga', + 'externalId' => '26769', + ), + ), + ), + ), 'manga_type' => 'manga', '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)', 'title' => 'Bokura wa Minna Kawaisou', + 'titles' => + array ( + 0 => NULL, + ), 'url' => 'https://kitsu.io/manga/bokura-wa-minna-kawaisou', 'volume_count' => '-', )); diff --git a/tests/API/KitsuTest.php b/tests/API/KitsuTest.php index b78e439d..7f31e262 100644 --- a/tests/API/KitsuTest.php +++ b/tests/API/KitsuTest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/tests/API/MAL/ListItemTest.php b/tests/API/MAL/ListItemTest.php deleted file mode 100644 index 6586ecf6..00000000 --- a/tests/API/MAL/ListItemTest.php +++ /dev/null @@ -1,40 +0,0 @@ - - * @copyright 2015 - 2018 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient\Tests\API\MAL; - -use Aviat\AnimeClient\API\MAL\ListItem; -use Aviat\AnimeClient\API\MAL\MALRequestBuilder; -use Aviat\AnimeClient\Tests\AnimeClientTestCase; -use Aviat\Ion\Di\ContainerAware; - -class ListItemTest extends AnimeClientTestCase { - - protected $listItem; - - public function setUp() - { - parent::setUp(); - $this->listItem = new ListItem(); - $this->listItem->setContainer($this->container); - $this->listItem->setRequestBuilder(new MALRequestBuilder()); - } - - public function testGet() - { - $this->assertEquals([], $this->listItem->get('foo')); - } -} \ No newline at end of file diff --git a/tests/API/MAL/MALTraitTest.php b/tests/API/MAL/MALTraitTest.php deleted file mode 100644 index 132cca59..00000000 --- a/tests/API/MAL/MALTraitTest.php +++ /dev/null @@ -1,50 +0,0 @@ - - * @copyright 2015 - 2018 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient\Tests\API\MAL; - -use Aviat\AnimeClient\API\MAL\MALRequestBuilder; -use Aviat\AnimeClient\API\MAL\MALTrait; -use Aviat\AnimeClient\Tests\AnimeClientTestCase; -use Aviat\Ion\Di\ContainerAware; - -class MALTraitTest extends AnimeClientTestCase { - - protected $obj; - - public function setUp() - { - parent::setUp(); - $this->obj = new class { - use ContainerAware; - use MALTrait; - }; - $this->obj->setContainer($this->container); - $this->obj->setRequestBuilder(new MALRequestBuilder()); - } - - public function testSetupRequest() - { - $request = $this->obj->setUpRequest('GET', 'foo', [ - 'query' => [ - 'foo' => 'bar' - ], - 'body' => '' - ]); - $this->assertInstanceOf(\Amp\Artax\Request::class, $request); - $this->assertEquals($request->getUri(), 'https://myanimelist.net/api/foo?foo=bar'); - } -} \ No newline at end of file diff --git a/tests/API/MAL/ModelTest.php b/tests/API/MAL/ModelTest.php deleted file mode 100644 index 0d430b5a..00000000 --- a/tests/API/MAL/ModelTest.php +++ /dev/null @@ -1,35 +0,0 @@ - - * @copyright 2015 - 2018 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient\Tests\API\MAL; - -use Aviat\AnimeClient\Tests\AnimeClientTestCase; - -class ModelTest extends AnimeClientTestCase { - - protected $model; - - public function setUp() - { - parent::setUp(); - $this->model = $this->container->get('mal-model'); - } - - public function testGetListItem() - { - $this->assertEquals([], $this->model->getListItem('foo')); - } -} \ No newline at end of file diff --git a/tests/API/MAL/Transformer/AnimeListTransformerTest.php b/tests/API/MAL/Transformer/AnimeListTransformerTest.php deleted file mode 100644 index 5b03dc2a..00000000 --- a/tests/API/MAL/Transformer/AnimeListTransformerTest.php +++ /dev/null @@ -1,39 +0,0 @@ - - * @copyright 2015 - 2018 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient\Tests\API\MAL\Transformer; - -use Aviat\AnimeClient\API\MAL\Transformer\AnimeListTransformer; -use Aviat\AnimeClient\Tests\AnimeClientTestCase; -use Aviat\Ion\Friend; -use Aviat\Ion\Json; - -class AnimeListTransformerTest extends AnimeClientTestCase { - - protected $transformer; - - public function setUp() - { - parent::setUp(); - $this->transformer = new AnimeListTransformer(); - } - - public function testTransform() - { - $this->assertEquals([], $this->transformer->transform([])); - } - -} \ No newline at end of file diff --git a/tests/API/MAL/Transformer/MangaListTransformerTest.php b/tests/API/MAL/Transformer/MangaListTransformerTest.php deleted file mode 100644 index 35327fca..00000000 --- a/tests/API/MAL/Transformer/MangaListTransformerTest.php +++ /dev/null @@ -1,39 +0,0 @@ - - * @copyright 2015 - 2018 Timothy J. Warren - * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 - * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient - */ - -namespace Aviat\AnimeClient\Tests\API\MAL\Transformer; - -use Aviat\AnimeClient\API\MAL\Transformer\MangaListTransformer; -use Aviat\AnimeClient\Tests\AnimeClientTestCase; -use Aviat\Ion\Friend; -use Aviat\Ion\Json; - -class MangaListTransformerTest extends AnimeClientTestCase { - - protected $transformer; - - public function setUp() - { - parent::setUp(); - $this->transformer = new MangaListTransformer(); - } - - public function testTransform() - { - $this->assertEquals([], $this->transformer->transform([])); - } - -} \ No newline at end of file diff --git a/tests/API/ParallelAPIRequestTest.php b/tests/API/ParallelAPIRequestTest.php index 6a529dda..f70fe17d 100644 --- a/tests/API/ParallelAPIRequestTest.php +++ b/tests/API/ParallelAPIRequestTest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/tests/API/XMLTest.php b/tests/API/XMLTest.php index db17f53b..3c796c8a 100644 --- a/tests/API/XMLTest.php +++ b/tests/API/XMLTest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/tests/AnimeClientTest.php b/tests/AnimeClientTest.php new file mode 100644 index 00000000..57948d1e --- /dev/null +++ b/tests/AnimeClientTest.php @@ -0,0 +1,67 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Tests; + +use function Aviat\AnimeClient\arrayToToml; +use function Aviat\AnimeClient\isSequentialArray; +use function Aviat\AnimeClient\tomlToArray; + +class AnimeClientTest extends AnimeClientTestCase +{ + public function testArrayToToml () + { + $arr = [ + 'cat' => false, + 'foo' => 'bar', + 'dateTime' => (array) new \DateTime(), + 'bar' => [ + 'a' => 1, + 'b' => 2, + 'c' => 3, + ], + 'baz' => [ + 'x' => [1, 2, 3], + 'y' => [2, 4, 6], + 'z' => [3, 6, 9], + ], + 'foobar' => [ + 'z' => 22/7, + 'a' => [ + 'aa' => -8, + 'b' => [ + 'aaa' => 4028, + 'c' => [1, 2, 3], + ], + ], + ], + ]; + + $toml = arrayToToml($arr); + + $parsedArray = tomlToArray($toml); + + $this->assertEquals($arr, $parsedArray); + } + + public function testIsSequentialArray() + { + $this->assertFalse(isSequentialArray(0)); + $this->assertFalse(isSequentialArray([50 => 'foo'])); + $this->assertTrue(isSequentialArray([])); + $this->assertTrue(isSequentialArray([1,2,3,4,5])); + } +} \ No newline at end of file diff --git a/tests/AnimeClientTestCase.php b/tests/AnimeClientTestCase.php index bbe323aa..caeca83c 100644 --- a/tests/AnimeClientTestCase.php +++ b/tests/AnimeClientTestCase.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -70,7 +70,7 @@ class AnimeClientTestCase extends TestCase { $APP_DIR = _dir($ROOT_DIR, 'app'); $config_array = [ - 'asset_path' => '//localhost/assets/', + 'asset_path' => '/assets', 'img_cache_path' => _dir(ROOT_DIR, 'public/images'), 'data_cache_path' => _dir(TEST_DATA_DIR, 'cache'), 'cache' => [ @@ -99,16 +99,9 @@ class AnimeClientTestCase extends TestCase { 'file' => ':memory:', ] ], - 'route_config' => [ - 'asset_path' => '/assets' - ], 'routes' => [ ], - 'mal' => [ - 'username' => 'foo', - 'password' => 'bar' - ] ]; // Set up DI container diff --git a/tests/Command/BaseCommandTest.php b/tests/Command/BaseCommandTest.php index c019035b..ac1d41a4 100644 --- a/tests/Command/BaseCommandTest.php +++ b/tests/Command/BaseCommandTest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/tests/ControllerTest.php b/tests/ControllerTest.php index 34fefa73..9bb418ff 100644 --- a/tests/ControllerTest.php +++ b/tests/ControllerTest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -51,7 +51,7 @@ class ControllerTest extends AnimeClientTestCase { public function testControllersSanity() { $config = $this->container->get('config'); - $config->set(['database', 'collection'], [ + $config->set('database', [ 'type' => 'sqlite', 'database' => '', 'file' => ":memory:" diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index c67df57b..a0bcf6b0 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -2,20 +2,21 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ namespace Aviat\AnimeClient\Tests; +use Aviat\AnimeClient\Controller; use Aviat\AnimeClient\Dispatcher; use Aviat\AnimeClient\UrlGenerator; use Aviat\Ion\Config; @@ -65,7 +66,7 @@ class DispatcherTest extends AnimeClientTestCase { public function testRouterSanity() { $this->doSetUp([], '/', 'localhost'); - $this->assertTrue(is_object($this->router)); + $this->assertInternalType('object', $this->router); } public function dataRoute() @@ -98,7 +99,7 @@ class DispatcherTest extends AnimeClientTestCase { ] ], ], - 'route_config' => [ + 'config' => [ 'anime_path' => 'anime', 'manga_path' => 'manga', 'default_list' => 'anime' @@ -109,14 +110,14 @@ class DispatcherTest extends AnimeClientTestCase { 'anime_default_routing_manga' => [ 'config' => $defaultConfig, 'controller' => 'manga', - 'host' => "localhost", - 'uri' => "/manga/plan_to_read", + 'host' => 'localhost', + 'uri' => '/manga/plan_to_read', ], 'manga_default_routing_anime' => [ 'config' => $defaultConfig, 'controller' => 'anime', - 'host' => "localhost", - 'uri' => "/anime/watching", + 'host' => 'localhost', + 'uri' => '/anime/watching', ], 'anime_default_routing_anime' => [ 'config' => $defaultConfig, @@ -132,8 +133,8 @@ class DispatcherTest extends AnimeClientTestCase { ] ]; - $data['manga_default_routing_anime']['config']['route_config']['default_list'] = 'manga'; - $data['manga_default_routing_manga']['config']['route_config']['default_list'] = 'manga'; + $data['manga_default_routing_anime']['config']['default_list'] = 'manga'; + $data['manga_default_routing_manga']['config']['default_list'] = 'manga'; return $data; } @@ -148,29 +149,29 @@ class DispatcherTest extends AnimeClientTestCase { $request = $this->container->get('request'); // Check route setup - $this->assertEquals($config['routes'], $this->config->get('routes'), "Incorrect route path"); - $this->assertTrue(is_array($this->router->getOutputRoutes())); + $this->assertEquals($config['routes'], $this->config->get('routes'), 'Incorrect route path'); + $this->assertInternalType('array', $this->router->getOutputRoutes()); // Check environment variables $this->assertEquals($uri, $request->getServerParams()['REQUEST_URI']); $this->assertEquals($host, $request->getServerParams()['HTTP_HOST']); // Make sure the route is an anime type - //$this->assertTrue($matcher->count() > 0, "0 routes"); - $this->assertEquals($controller, $this->router->getController(), "Incorrect Route type"); + //$this->assertTrue($matcher->count() > 0, '0 routes'); + $this->assertEquals($controller, $this->router->getController(), 'Incorrect Route type'); // Make sure the route matches, by checking that it is actually an object $route = $this->router->getRoute(); - $this->assertInstanceOf('Aura\\Router\\Route', $route, "Route is invalid, not matched"); + $this->assertInstanceOf(\Aura\Router\Route::class, $route, 'Route is invalid, not matched'); } public function testDefaultRoute() { $config = [ - 'route_config' => [ + 'config' => [ 'anime_path' => 'anime', 'manga_path' => 'manga', - 'default_anime_list_path' => "watching", + 'default_anime_list_path' => 'watching', 'default_manga_list_path' => 'all', 'default_list' => 'manga' ], @@ -192,60 +193,52 @@ class DispatcherTest extends AnimeClientTestCase { ] ]; - $this->doSetUp($config, "/", "localhost"); - $this->assertEquals('//localhost/manga/all', $this->urlGenerator->defaultUrl('manga'), "Incorrect default url"); - $this->assertEquals('//localhost/anime/watching', $this->urlGenerator->defaultUrl('anime'), "Incorrect default url"); - $this->expectException(\InvalidArgumentException::class); + + $this->doSetUp($config, '/', 'localhost'); + $this->assertEquals('//localhost/manga/all', $this->urlGenerator->defaultUrl('manga'), 'Incorrect default url'); + $this->assertEquals('//localhost/anime/watching', $this->urlGenerator->defaultUrl('anime'), 'Incorrect default url'); + $this->urlGenerator->defaultUrl('foo'); } public function dataGetControllerList() { + $expectedList = [ + 'anime' => Controller\Anime::class, + 'anime-collection' => Controller\AnimeCollection::class, + 'character' => Controller\Character::class, + 'misc' => Controller\Misc::class, + 'manga' => Controller\Manga::class, + 'manga-collection' => Controller\MangaCollection::class, + 'people' => Controller\People::class, + 'settings' => Controller\Settings::class, + 'user' => Controller\User::class, + 'images' => Controller\Images::class, + ]; + return [ 'controller_list_sanity_check' => [ 'config' => [ - 'routes' => [ - - ], - 'route_config' => [ - 'anime_path' => 'anime', - 'manga_path' => 'manga', - 'default_anime_list_path' => "watching", - 'default_manga_list_path' => 'all', - 'default_list' => 'manga' - ], + 'anime_path' => 'anime', + 'manga_path' => 'manga', + 'default_anime_list_path' => 'watching', + 'default_manga_list_path' => 'all', + 'default_list' => 'manga', + 'routes' => [], ], - 'expected' => [ - 'anime' => 'Aviat\AnimeClient\Controller\Anime', - 'manga' => 'Aviat\AnimeClient\Controller\Manga', - 'anime-collection' => 'Aviat\AnimeClient\Controller\AnimeCollection', - 'manga-collection' => 'Aviat\AnimeClient\Controller\MangaCollection', - 'character' => 'Aviat\AnimeClient\Controller\Character', - 'index' => 'Aviat\AnimeClient\Controller\Index', - ] + 'expected' => $expectedList, ], 'empty_controller_list' => [ 'config' => [ - 'routes' => [ - - ], - 'route_config' => [ - 'anime_path' => 'anime', - 'manga_path' => 'manga', - 'default_anime_path' => "/anime/watching", - 'default_manga_path' => '/manga/all', - 'default_list' => 'manga' - ], + 'anime_path' => 'anime', + 'manga_path' => 'manga', + 'default_anime_path' => '/anime/watching', + 'default_manga_path' => '/manga/all', + 'default_list' => 'manga', + 'routes' => [], ], - 'expected' => [ - 'anime' => 'Aviat\AnimeClient\Controller\Anime', - 'manga' => 'Aviat\AnimeClient\Controller\Manga', - 'anime-collection' => 'Aviat\AnimeClient\Controller\AnimeCollection', - 'manga-collection' => 'Aviat\AnimeClient\Controller\MangaCollection', - 'character' => 'Aviat\AnimeClient\Controller\Character', - 'index' => 'Aviat\AnimeClient\Controller\Index', - ] + 'expected' => $expectedList ] ]; } diff --git a/tests/Helper/MenuHelperTest.php b/tests/Helper/MenuHelperTest.php index 352aa607..d8c2e54c 100644 --- a/tests/Helper/MenuHelperTest.php +++ b/tests/Helper/MenuHelperTest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/tests/Helper/PictureHelperTest.php b/tests/Helper/PictureHelperTest.php new file mode 100644 index 00000000..22869673 --- /dev/null +++ b/tests/Helper/PictureHelperTest.php @@ -0,0 +1,151 @@ + + * @copyright 2015 - 2018 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.1 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Tests\Helper; + +use Aviat\AnimeClient\Helper\Picture as PictureHelper; +use Aviat\AnimeClient\Tests\AnimeClientTestCase; + +class PictureHelperTest extends AnimeClientTestCase { + /** + * @dataProvider dataPictureCase + */ + public function testPictureHelper($params, $expected = NULL) + { + $helper = new PictureHelper(); + $helper->setContainer($this->container); + + $actual = $helper(...$params); + + if ($expected === NULL) + { + $this->assertMatchesSnapshot($actual); + } + else + { + $this->assertEquals($expected, $actual); + } + } + + /** + * @dataProvider dataSimpleImageCase + */ + public function testSimpleImage(string $ext, bool $isSimple) + { + $helper = new PictureHelper(); + $helper->setContainer($this->container); + + $url = "https://example.com/image.{$ext}"; + $actual = $helper($url); + + $actuallySimple = strpos($actual, 'assertEquals($isSimple, $actuallySimple); + } + + public function testSimpleImageByFallback() + { + $helper = new PictureHelper(); + $helper->setContainer($this->container); + + $actual = $helper("foo.svg", 'svg'); + + $this->assertTrue(strpos($actual, ' [ + 'params' => [ + 'https://www.example.com/image.webp', + ], + ], + 'Partial webp URL' => [ + 'params' => [ + 'images/anime/15424.webp' + ], + ], + 'bmp with gif fallback' => [ + 'params' => [ + 'images/avatar/25.bmp', + 'gif', + ], + ], + 'webp placeholder image' => [ + 'params' => [ + 'images/placeholder.webp', + ] + ], + 'png placeholder image' => [ + 'params' => [ + 'images/placeholder.png', + ] + ], + 'jpeg2000' => [ + 'params' => [ + 'images/foo.jpf', + ] + ], + 'svg with png fallback and lots of attributes' => [ + 'params' => [ + 'images/example.svg', + 'png', + [ 'width' => 200, 'height' => 300 ], + [ 'alt' => 'Example text' ] + ] + ], + 'simple image with attributes' => [ + 'params' => [ + 'images/foo.jpg', + 'jpg', + [ 'x' => 1, 'y' => 1 ], + ['width' => 200, 'height' => 200, 'alt' => 'should exist'], + ] + ] + ]; + } + + public function dataSimpleImageCase() + { + return [ + 'apng' => [ + 'ext' => 'apng', + 'isSimple' => FALSE, + ], + 'gif' => [ + 'ext' => 'gif', + 'isSimple' => TRUE, + ], + 'jpg' => [ + 'ext' => 'jpg', + 'isSimple' => TRUE, + ], + 'jpeg' => [ + 'ext' => 'jpeg', + 'isSimple' => TRUE, + ], + 'png' => [ + 'ext' => 'png', + 'isSimple' => TRUE, + ], + 'webp' => [ + 'ext' => 'webp', + 'isSimple' => FALSE, + ], + ]; + } +} \ No newline at end of file diff --git a/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set Full webp URL__1.php b/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set Full webp URL__1.php new file mode 100644 index 00000000..f852d0bc --- /dev/null +++ b/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set Full webp URL__1.php @@ -0,0 +1 @@ +'; diff --git a/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set Partial webp URL__1.php b/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set Partial webp URL__1.php new file mode 100644 index 00000000..4b6711e0 --- /dev/null +++ b/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set Partial webp URL__1.php @@ -0,0 +1 @@ +'; diff --git a/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set bmp with gif fallback__1.php b/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set bmp with gif fallback__1.php new file mode 100644 index 00000000..ee4c502d --- /dev/null +++ b/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set bmp with gif fallback__1.php @@ -0,0 +1 @@ +'; diff --git a/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set jpeg2000__1.php b/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set jpeg2000__1.php new file mode 100644 index 00000000..0cecd592 --- /dev/null +++ b/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set jpeg2000__1.php @@ -0,0 +1 @@ +'; diff --git a/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set png placeholder image__1.php b/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set png placeholder image__1.php new file mode 100644 index 00000000..054958a1 --- /dev/null +++ b/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set png placeholder image__1.php @@ -0,0 +1 @@ +'; diff --git a/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set simple image with attributes__1.php b/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set simple image with attributes__1.php new file mode 100644 index 00000000..2b3023ef --- /dev/null +++ b/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set simple image with attributes__1.php @@ -0,0 +1 @@ +'; diff --git a/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set svg with png fallback and lots of attributes__1.php b/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set svg with png fallback and lots of attributes__1.php new file mode 100644 index 00000000..a8900e93 --- /dev/null +++ b/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set svg with png fallback and lots of attributes__1.php @@ -0,0 +1 @@ +Example text'; diff --git a/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set webp placeholder image__1.php b/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set webp placeholder image__1.php new file mode 100644 index 00000000..d796ce75 --- /dev/null +++ b/tests/Helper/__snapshots__/PictureHelperTest__testPictureHelper with data set webp placeholder image__1.php @@ -0,0 +1 @@ +'; diff --git a/tests/MenuGeneratorTest.php b/tests/MenuGeneratorTest.php index a1e5a82d..bf0b1922 100644 --- a/tests/MenuGeneratorTest.php +++ b/tests/MenuGeneratorTest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/tests/RequirementsTest.php b/tests/RequirementsTest.php index 45416d4a..64412c51 100644 --- a/tests/RequirementsTest.php +++ b/tests/RequirementsTest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/tests/RoutingBaseTest.php b/tests/RoutingBaseTest.php index 9b6934fe..9bf83ca9 100644 --- a/tests/RoutingBaseTest.php +++ b/tests/RoutingBaseTest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/tests/TestSessionHandler.php b/tests/TestSessionHandler.php index ee7d235a..de61ffba 100644 --- a/tests/TestSessionHandler.php +++ b/tests/TestSessionHandler.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/tests/UrlGeneratorTest.php b/tests/UrlGeneratorTest.php index a7fedf21..4f3322ab 100644 --- a/tests/UrlGeneratorTest.php +++ b/tests/UrlGeneratorTest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ @@ -28,13 +28,13 @@ class UrlGeneratorTest extends AnimeClientTestCase { 'args' => [ 'images' ], - 'expected' => '//localhost/assets/images', + 'expected' => 'https://localhost/assets/images', ], 'multiple arguments' => [ 'args' => [ 'images', 'anime', 'foo.png' ], - 'expected' => '//localhost/assets/images/anime/foo.png' + 'expected' => 'https://localhost/assets/images/anime/foo.png' ] ]; } diff --git a/tests/UtilTest.php b/tests/UtilTest.php index 226cdd13..aac2579f 100644 --- a/tests/UtilTest.php +++ b/tests/UtilTest.php @@ -2,15 +2,15 @@ /** * Hummingbird Anime List Client * - * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists + * An API client for Kitsu to manage anime and manga watch lists * - * PHP version 7 + * PHP version 7.1 * * @package HummingbirdAnimeClient * @author Timothy J. Warren * @copyright 2015 - 2018 Timothy J. Warren * @license http://www.opensource.org/licenses/mit-license.html MIT License - * @version 4.0 + * @version 4.1 * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient */ diff --git a/tests/test_data/JsonAPI/inlineIncluded.json b/tests/test_data/JsonAPI/inlineIncluded.json index f4d40970..a2ec42f5 100644 --- a/tests/test_data/JsonAPI/inlineIncluded.json +++ b/tests/test_data/JsonAPI/inlineIncluded.json @@ -1 +1 @@ -{"anime":{"11474":{"slug":"hibike-euphonium-2","synopsis":"Second season of Hibike! Euphonium.","coverImageTopOffset":120,"titles":{"en":"Sound! Euphonium 2","en_jp":"Hibike! Euphonium 2","ja_jp":"\u97ff\u3051\uff01\u30e6\u30fc\u30d5\u30a9\u30cb\u30a2\u30e0 \uff12"},"canonicalTitle":"Hibike! Euphonium 2","abbreviatedTitles":null,"averageRating":4.1684326428476,"ratingFrequencies":{"0.5":"1","1.0":"1","1.5":"2","2.0":"8","2.5":"13","3.0":"42","3.5":"90","4.0":"193","4.5":"180","5.0":"193","nil":"1972"},"startDate":"2016-10-06","endDate":null,"posterImage":{"tiny":"https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/tiny.jpg?1470781430","small":"https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/small.jpg?1470781430","medium":"https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/medium.jpg?1470781430","large":"https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/large.jpg?1470781430","original":"https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/original.jpg?1470781430"},"coverImage":{"small":"https:\/\/media.kitsu.io\/anime\/cover_images\/11474\/small.jpg?1476203965","large":"https:\/\/media.kitsu.io\/anime\/cover_images\/11474\/large.jpg?1476203965","original":"https:\/\/media.kitsu.io\/anime\/cover_images\/11474\/original.jpg?1476203965"},"episodeCount":13,"episodeLength":25,"subtype":"TV","youtubeVideoId":"d2Di5swwzxg","ageRating":"PG","ageRatingGuide":"","showType":"TV","nsfw":false,"relationships":{"genres":{"24":{"name":"School","slug":"school","description":null},"35":{"name":"Music","slug":"music","description":null},"4":{"name":"Drama","slug":"drama","description":""}},"mappings":{"3155":{"externalSite":"myanimelist\/anime","externalId":"31988","relationships":[]}}}},"10802":{"slug":"nisekoimonogatari","synopsis":"Trailer for a fake anime created by Shaft as an April Fool's Day joke.","coverImageTopOffset":80,"titles":{"en":"","en_jp":"Nisekoimonogatari","ja_jp":""},"canonicalTitle":"Nisekoimonogatari","abbreviatedTitles":null,"averageRating":3.4857993435287,"ratingFrequencies":{"0.5":"22","1.0":"10","1.5":"16","2.0":"32","2.5":"74","3.0":"97","3.5":"118","4.0":"72","4.5":"34","5.0":"136","nil":"597","0.89":"-1","3.63":"-1","4.11":"-1","0.068":"-1","0.205":"-1","0.274":"-2","0.479":"-1","0.548":"-1","1.096":"-2","1.164":"-1","1.438":"-1","1.918":"-1","2.055":"-1","3.973":"-1","4.178":"-3","4.247":"-1","4.726":"-1","4.932":"-3","1.0958904109589":"3","0.89041095890411":"2","1.02739726027397":"1","1.16438356164384":"2","1.43835616438356":"2","1.57534246575342":"1","1.91780821917808":"1","2.05479452054794":"2","2.12328767123288":"1","2.73972602739726":"1","2.80821917808219":"2","2.94520547945205":"1","3.15068493150685":"1","3.35616438356164":"2","3.63013698630137":"2","3.97260273972603":"1","4.10958904109589":"2","4.17808219178082":"3","4.24657534246575":"1","4.38356164383562":"2","4.65753424657534":"1","4.72602739726027":"2","4.86301369863014":"1","4.93150684931507":"10","0.205479452054795":"1","0.273972602739726":"2","0.479452054794521":"2","0.547945205479452":"2","0.753424657534246":"1","0.0684931506849315":"1"},"startDate":"2015-04-01","endDate":null,"posterImage":{"tiny":"https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/tiny.jpg?1427974534","small":"https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/small.jpg?1427974534","medium":"https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/medium.jpg?1427974534","large":"https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/large.jpg?1427974534","original":"https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/original.jpg?1427974534"},"coverImage":{"small":"https:\/\/media.kitsu.io\/anime\/cover_images\/10802\/small.jpg?1427928458","large":"https:\/\/media.kitsu.io\/anime\/cover_images\/10802\/large.jpg?1427928458","original":"https:\/\/media.kitsu.io\/anime\/cover_images\/10802\/original.jpg?1427928458"},"episodeCount":1,"episodeLength":1,"subtype":"ONA","youtubeVideoId":"","ageRating":"PG","ageRatingGuide":"Teens 13 or older","showType":"ONA","nsfw":false,"relationships":{"genres":{"3":{"name":"Comedy","slug":"comedy","description":null}},"mappings":{"1755":{"externalSite":"myanimelist\/anime","externalId":"30514","relationships":[]}}}},"11887":{"slug":"brave-witches","synopsis":"In September 1944, allied forces led by the 501st Joint Fighter Wing \"Strike Witches\" successfully eliminate the Neuroi threat from the skies of the Republic of Gallia, thus ensuring the security of western Europe. Taking advantage of this victory, allied forces begin a full-fledged push toward central and eastern Europe. From a base in Petersburg in the Empire of Orussia, the 502nd Joint Fighter Wing \"Brave Witches,\" upon whom mankind has placed its hopes, flies with courage in the cold skies of eastern Europe.\n\n(Source: MAL News)","coverImageTopOffset":380,"titles":{"en":"","en_jp":"Brave Witches","ja_jp":"\u30d6\u30ec\u30a4\u30d6\u30a6\u30a3\u30c3\u30c1\u30fc\u30ba"},"canonicalTitle":"Brave Witches","abbreviatedTitles":null,"averageRating":3.5846888163849,"ratingFrequencies":{"0.5":"1","1.0":"4","1.5":"8","2.0":"12","2.5":"17","3.0":"33","3.5":"41","4.0":"32","4.5":"9","5.0":"19","nil":"620"},"startDate":"2016-10-06","endDate":null,"posterImage":{"tiny":"https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/tiny.jpg?1476481854","small":"https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/small.jpg?1476481854","medium":"https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/medium.jpg?1476481854","large":"https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/large.jpg?1476481854","original":"https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/original.png?1476481854"},"coverImage":{"small":"https:\/\/media.kitsu.io\/anime\/cover_images\/11887\/small.jpg?1479834725","large":"https:\/\/media.kitsu.io\/anime\/cover_images\/11887\/large.jpg?1479834725","original":"https:\/\/media.kitsu.io\/anime\/cover_images\/11887\/original.jpg?1479834725"},"episodeCount":12,"episodeLength":24,"subtype":"TV","youtubeVideoId":"VLUqd-jEBuE","ageRating":"R","ageRatingGuide":"Mild Nudity","showType":"TV","nsfw":false,"relationships":{"genres":{"5":{"name":"Sci-Fi","slug":"sci-fi","description":null},"8":{"name":"Magic","slug":"magic","description":null},"28":{"name":"Military","slug":"military","description":null},"1":{"name":"Action","slug":"action","description":""},"25":{"name":"Ecchi","slug":"ecchi","description":""}},"mappings":{"2593":{"externalSite":"myanimelist\/anime","externalId":"32866","relationships":[]}}}},"12024":{"slug":"www-working","synopsis":"Daisuke Higashida is a serious first-year student at Higashizaka High School. He lives a peaceful everyday life even though he is not satisfied with the family who doesn't laugh at all and makes him tired. However, his father's company goes bankrupt one day, and he can no longer afford allowances, cellphone bills, and commuter tickets. When his father orders him to take up a part-time job, Daisuke decides to work at a nearby family restaurant in order to avoid traveling 15 kilometers to school by bicycle.","coverImageTopOffset":165,"titles":{"en":"WWW.WAGNARIA!!","en_jp":"WWW.Working!!","ja_jp":""},"canonicalTitle":"WWW.Working!!","abbreviatedTitles":null,"averageRating":3.8238374224378,"ratingFrequencies":{"1.0":"2","1.5":"7","2.0":"19","2.5":"28","3.0":"68","3.5":"114","4.0":"144","4.5":"78","5.0":"74","nil":"1182"},"startDate":"2016-10-01","endDate":"2016-12-24","posterImage":{"tiny":"https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/tiny.jpg?1473990267","small":"https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/small.jpg?1473990267","medium":"https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/medium.jpg?1473990267","large":"https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/large.jpg?1473990267","original":"https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/original.jpg?1473990267"},"coverImage":{"small":"https:\/\/media.kitsu.io\/anime\/cover_images\/12024\/small.jpg?1479834612","large":"https:\/\/media.kitsu.io\/anime\/cover_images\/12024\/large.jpg?1479834612","original":"https:\/\/media.kitsu.io\/anime\/cover_images\/12024\/original.png?1479834612"},"episodeCount":13,"episodeLength":23,"subtype":"TV","youtubeVideoId":"","ageRating":"PG","ageRatingGuide":"Teens 13 or older","showType":"TV","nsfw":false,"relationships":{"genres":{"3":{"name":"Comedy","slug":"comedy","description":null},"16":{"name":"Slice of Life","slug":"slice-of-life","description":""}},"mappings":{"2538":{"externalSite":"myanimelist\/anime","externalId":"33094","relationships":[]}}}},"12465":{"slug":"bishoujo-yuugi-unit-crane-game-girls-galaxy","synopsis":"Second season of Bishoujo Yuugi Unit Crane Game Girls.","coverImageTopOffset":0,"titles":{"en":"Crane Game Girls Galaxy","en_jp":"Bishoujo Yuugi Unit Crane Game Girls Galaxy","ja_jp":"\u7f8e\u5c11\u5973\u904a\u622f\u30e6\u30cb\u30c3\u30c8 \u30af\u30ec\u30fc\u30f3\u30b2\u30fc\u30eb\u30ae\u30e3\u30e9\u30af\u30b7\u30fc"},"canonicalTitle":"Bishoujo Yuugi Unit Crane Game Girls Galaxy","abbreviatedTitles":null,"averageRating":null,"ratingFrequencies":{"0.5":"2","1.0":"2","1.5":"0","2.0":"4","2.5":"6","3.0":"2","3.5":"4","4.0":"1","4.5":"2","nil":"66"},"startDate":"2016-10-05","endDate":null,"posterImage":{"tiny":"https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/tiny.jpg?1473601756","small":"https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/small.jpg?1473601756","medium":"https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/medium.jpg?1473601756","large":"https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/large.jpg?1473601756","original":"https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/original.png?1473601756"},"coverImage":null,"episodeCount":null,"episodeLength":13,"subtype":"TV","youtubeVideoId":"","ageRating":"PG","ageRatingGuide":"Children","showType":"TV","nsfw":false,"relationships":{"genres":{"3":{"name":"Comedy","slug":"comedy","description":null}},"mappings":{"9871":{"externalSite":"myanimelist\/anime","externalId":"33541","relationships":[]}}}}}} \ No newline at end of file +{"anime":{"11474":{"slug":"hibike-euphonium-2","synopsis":"Second season of Hibike! Euphonium.","coverImageTopOffset":120,"titles":{"en":"Sound! Euphonium 2","en_jp":"Hibike! Euphonium 2","ja_jp":"\u97ff\u3051\uff01\u30e6\u30fc\u30d5\u30a9\u30cb\u30a2\u30e0 \uff12"},"canonicalTitle":"Hibike! Euphonium 2","abbreviatedTitles":null,"averageRating":4.1684326428476197,"ratingFrequencies":{"0.5":"1","1.0":"1","1.5":"2","2.0":"8","2.5":"13","3.0":"42","3.5":"90","4.0":"193","4.5":"180","5.0":"193","nil":"1972"},"startDate":"2016-10-06","endDate":null,"posterImage":{"tiny":"https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/tiny.jpg?1470781430","small":"https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/small.jpg?1470781430","medium":"https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/medium.jpg?1470781430","large":"https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/large.jpg?1470781430","original":"https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/original.jpg?1470781430"},"coverImage":{"small":"https:\/\/media.kitsu.io\/anime\/cover_images\/11474\/small.jpg?1476203965","large":"https:\/\/media.kitsu.io\/anime\/cover_images\/11474\/large.jpg?1476203965","original":"https:\/\/media.kitsu.io\/anime\/cover_images\/11474\/original.jpg?1476203965"},"episodeCount":13,"episodeLength":25,"subtype":"TV","youtubeVideoId":"d2Di5swwzxg","ageRating":"PG","ageRatingGuide":"","showType":"TV","nsfw":false,"relationships":{"genres":{"24":{"name":"School","slug":"school","description":null},"35":{"name":"Music","slug":"music","description":null},"4":{"name":"Drama","slug":"drama","description":""}},"castings":[],"installments":[],"mappings":{"3155":{"externalSite":"myanimelist\/anime","externalId":"31988","relationships":{"media":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/mappings\/3155\/relationships\/media","related":"https:\/\/kitsu.io\/api\/edge\/mappings\/3155\/media"}}}}},"reviews":[],"episodes":[],"streamingLinks":[]}},"10802":{"slug":"nisekoimonogatari","synopsis":"Trailer for a fake anime created by Shaft as an April Fool's Day joke.","coverImageTopOffset":80,"titles":{"en":"","en_jp":"Nisekoimonogatari","ja_jp":""},"canonicalTitle":"Nisekoimonogatari","abbreviatedTitles":null,"averageRating":3.48579934352873,"ratingFrequencies":{"0.5":"22","1.0":"10","1.5":"16","2.0":"32","2.5":"74","3.0":"97","3.5":"118","4.0":"72","4.5":"34","5.0":"136","nil":"597","0.89":"-1","3.63":"-1","4.11":"-1","0.068":"-1","0.205":"-1","0.274":"-2","0.479":"-1","0.548":"-1","1.096":"-2","1.164":"-1","1.438":"-1","1.918":"-1","2.055":"-1","3.973":"-1","4.178":"-3","4.247":"-1","4.726":"-1","4.932":"-3","1.0958904109589":"3","0.89041095890411":"2","1.02739726027397":"1","1.16438356164384":"2","1.43835616438356":"2","1.57534246575342":"1","1.91780821917808":"1","2.05479452054794":"2","2.12328767123288":"1","2.73972602739726":"1","2.80821917808219":"2","2.94520547945205":"1","3.15068493150685":"1","3.35616438356164":"2","3.63013698630137":"2","3.97260273972603":"1","4.10958904109589":"2","4.17808219178082":"3","4.24657534246575":"1","4.38356164383562":"2","4.65753424657534":"1","4.72602739726027":"2","4.86301369863014":"1","4.93150684931507":"10","0.205479452054795":"1","0.273972602739726":"2","0.479452054794521":"2","0.547945205479452":"2","0.753424657534246":"1","0.0684931506849315":"1"},"startDate":"2015-04-01","endDate":null,"posterImage":{"tiny":"https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/tiny.jpg?1427974534","small":"https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/small.jpg?1427974534","medium":"https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/medium.jpg?1427974534","large":"https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/large.jpg?1427974534","original":"https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/original.jpg?1427974534"},"coverImage":{"small":"https:\/\/media.kitsu.io\/anime\/cover_images\/10802\/small.jpg?1427928458","large":"https:\/\/media.kitsu.io\/anime\/cover_images\/10802\/large.jpg?1427928458","original":"https:\/\/media.kitsu.io\/anime\/cover_images\/10802\/original.jpg?1427928458"},"episodeCount":1,"episodeLength":1,"subtype":"ONA","youtubeVideoId":"","ageRating":"PG","ageRatingGuide":"Teens 13 or older","showType":"ONA","nsfw":false,"relationships":{"genres":{"3":{"name":"Comedy","slug":"comedy","description":null}},"castings":[],"installments":[],"mappings":{"1755":{"externalSite":"myanimelist\/anime","externalId":"30514","relationships":{"media":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/mappings\/1755\/relationships\/media","related":"https:\/\/kitsu.io\/api\/edge\/mappings\/1755\/media"}}}}},"reviews":[],"episodes":[],"streamingLinks":[]}},"11887":{"slug":"brave-witches","synopsis":"In September 1944, allied forces led by the 501st Joint Fighter Wing \"Strike Witches\" successfully eliminate the Neuroi threat from the skies of the Republic of Gallia, thus ensuring the security of western Europe. Taking advantage of this victory, allied forces begin a full-fledged push toward central and eastern Europe. From a base in Petersburg in the Empire of Orussia, the 502nd Joint Fighter Wing \"Brave Witches,\" upon whom mankind has placed its hopes, flies with courage in the cold skies of eastern Europe.\n\n(Source: MAL News)","coverImageTopOffset":380,"titles":{"en":"","en_jp":"Brave Witches","ja_jp":"\u30d6\u30ec\u30a4\u30d6\u30a6\u30a3\u30c3\u30c1\u30fc\u30ba"},"canonicalTitle":"Brave Witches","abbreviatedTitles":null,"averageRating":3.5846888163849102,"ratingFrequencies":{"0.5":"1","1.0":"4","1.5":"8","2.0":"12","2.5":"17","3.0":"33","3.5":"41","4.0":"32","4.5":"9","5.0":"19","nil":"620"},"startDate":"2016-10-06","endDate":null,"posterImage":{"tiny":"https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/tiny.jpg?1476481854","small":"https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/small.jpg?1476481854","medium":"https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/medium.jpg?1476481854","large":"https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/large.jpg?1476481854","original":"https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/original.png?1476481854"},"coverImage":{"small":"https:\/\/media.kitsu.io\/anime\/cover_images\/11887\/small.jpg?1479834725","large":"https:\/\/media.kitsu.io\/anime\/cover_images\/11887\/large.jpg?1479834725","original":"https:\/\/media.kitsu.io\/anime\/cover_images\/11887\/original.jpg?1479834725"},"episodeCount":12,"episodeLength":24,"subtype":"TV","youtubeVideoId":"VLUqd-jEBuE","ageRating":"R","ageRatingGuide":"Mild Nudity","showType":"TV","nsfw":false,"relationships":{"genres":{"5":{"name":"Sci-Fi","slug":"sci-fi","description":null},"8":{"name":"Magic","slug":"magic","description":null},"28":{"name":"Military","slug":"military","description":null},"1":{"name":"Action","slug":"action","description":""},"25":{"name":"Ecchi","slug":"ecchi","description":""}},"castings":[],"installments":[],"mappings":{"2593":{"externalSite":"myanimelist\/anime","externalId":"32866","relationships":{"media":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/mappings\/2593\/relationships\/media","related":"https:\/\/kitsu.io\/api\/edge\/mappings\/2593\/media"}}}}},"reviews":[],"episodes":[],"streamingLinks":[]}},"12024":{"slug":"www-working","synopsis":"Daisuke Higashida is a serious first-year student at Higashizaka High School. He lives a peaceful everyday life even though he is not satisfied with the family who doesn't laugh at all and makes him tired. However, his father's company goes bankrupt one day, and he can no longer afford allowances, cellphone bills, and commuter tickets. When his father orders him to take up a part-time job, Daisuke decides to work at a nearby family restaurant in order to avoid traveling 15 kilometers to school by bicycle.","coverImageTopOffset":165,"titles":{"en":"WWW.WAGNARIA!!","en_jp":"WWW.Working!!","ja_jp":""},"canonicalTitle":"WWW.Working!!","abbreviatedTitles":null,"averageRating":3.8238374224378302,"ratingFrequencies":{"1.0":"2","1.5":"7","2.0":"19","2.5":"28","3.0":"68","3.5":"114","4.0":"144","4.5":"78","5.0":"74","nil":"1182"},"startDate":"2016-10-01","endDate":"2016-12-24","posterImage":{"tiny":"https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/tiny.jpg?1473990267","small":"https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/small.jpg?1473990267","medium":"https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/medium.jpg?1473990267","large":"https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/large.jpg?1473990267","original":"https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/original.jpg?1473990267"},"coverImage":{"small":"https:\/\/media.kitsu.io\/anime\/cover_images\/12024\/small.jpg?1479834612","large":"https:\/\/media.kitsu.io\/anime\/cover_images\/12024\/large.jpg?1479834612","original":"https:\/\/media.kitsu.io\/anime\/cover_images\/12024\/original.png?1479834612"},"episodeCount":13,"episodeLength":23,"subtype":"TV","youtubeVideoId":"","ageRating":"PG","ageRatingGuide":"Teens 13 or older","showType":"TV","nsfw":false,"relationships":{"genres":{"3":{"name":"Comedy","slug":"comedy","description":null},"16":{"name":"Slice of Life","slug":"slice-of-life","description":""}},"castings":[],"installments":[],"mappings":{"2538":{"externalSite":"myanimelist\/anime","externalId":"33094","relationships":{"media":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/mappings\/2538\/relationships\/media","related":"https:\/\/kitsu.io\/api\/edge\/mappings\/2538\/media"}}}}},"reviews":[],"episodes":[],"streamingLinks":[]}},"12465":{"slug":"bishoujo-yuugi-unit-crane-game-girls-galaxy","synopsis":"Second season of Bishoujo Yuugi Unit Crane Game Girls.","coverImageTopOffset":0,"titles":{"en":"Crane Game Girls Galaxy","en_jp":"Bishoujo Yuugi Unit Crane Game Girls Galaxy","ja_jp":"\u7f8e\u5c11\u5973\u904a\u622f\u30e6\u30cb\u30c3\u30c8 \u30af\u30ec\u30fc\u30f3\u30b2\u30fc\u30eb\u30ae\u30e3\u30e9\u30af\u30b7\u30fc"},"canonicalTitle":"Bishoujo Yuugi Unit Crane Game Girls Galaxy","abbreviatedTitles":null,"averageRating":null,"ratingFrequencies":{"0.5":"2","1.0":"2","1.5":"0","2.0":"4","2.5":"6","3.0":"2","3.5":"4","4.0":"1","4.5":"2","nil":"66"},"startDate":"2016-10-05","endDate":null,"posterImage":{"tiny":"https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/tiny.jpg?1473601756","small":"https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/small.jpg?1473601756","medium":"https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/medium.jpg?1473601756","large":"https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/large.jpg?1473601756","original":"https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/original.png?1473601756"},"coverImage":null,"episodeCount":null,"episodeLength":13,"subtype":"TV","youtubeVideoId":"","ageRating":"PG","ageRatingGuide":"Children","showType":"TV","nsfw":false,"relationships":{"genres":{"3":{"name":"Comedy","slug":"comedy","description":null}},"castings":[],"installments":[],"mappings":{"9871":{"externalSite":"myanimelist\/anime","externalId":"33541","relationships":{"media":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/mappings\/9871\/relationships\/media","related":"https:\/\/kitsu.io\/api\/edge\/mappings\/9871\/media"}}}}},"reviews":[],"episodes":[],"streamingLinks":[]}}}} \ No newline at end of file diff --git a/tests/test_data/JsonAPI/organizedIncludes.json b/tests/test_data/JsonAPI/organizedIncludes.json index 995226b1..44d5c1e2 100644 --- a/tests/test_data/JsonAPI/organizedIncludes.json +++ b/tests/test_data/JsonAPI/organizedIncludes.json @@ -1,385 +1 @@ -{ - "anime": { - "11474": { - "slug": "hibike-euphonium-2", - "synopsis": "Second season of Hibike! Euphonium.", - "coverImageTopOffset": 120, - "titles": { - "en": "Sound! Euphonium 2", - "en_jp": "Hibike! Euphonium 2", - "ja_jp": "\u97ff\u3051\uff01\u30e6\u30fc\u30d5\u30a9\u30cb\u30a2\u30e0 \uff12" - }, - "canonicalTitle": "Hibike! Euphonium 2", - "abbreviatedTitles": null, - "averageRating": 4.1684326428476, - "ratingFrequencies": { - "0.5": "1", - "1.0": "1", - "1.5": "2", - "2.0": "8", - "2.5": "13", - "3.0": "42", - "3.5": "90", - "4.0": "193", - "4.5": "180", - "5.0": "193", - "nil": "1972" - }, - "startDate": "2016-10-06", - "endDate": null, - "posterImage": { - "tiny": "https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/tiny.jpg?1470781430", - "small": "https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/small.jpg?1470781430", - "medium": "https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/medium.jpg?1470781430", - "large": "https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/large.jpg?1470781430", - "original": "https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/original.jpg?1470781430" - }, - "coverImage": { - "small": "https:\/\/media.kitsu.io\/anime\/cover_images\/11474\/small.jpg?1476203965", - "large": "https:\/\/media.kitsu.io\/anime\/cover_images\/11474\/large.jpg?1476203965", - "original": "https:\/\/media.kitsu.io\/anime\/cover_images\/11474\/original.jpg?1476203965" - }, - "episodeCount": 13, - "episodeLength": 25, - "subtype": "TV", - "youtubeVideoId": "d2Di5swwzxg", - "ageRating": "PG", - "ageRatingGuide": "", - "showType": "TV", - "nsfw": false, - "relationships": { - "genres": ["24", "35", "4"], - "mappings": ["3155"] - } - }, - "10802": { - "slug": "nisekoimonogatari", - "synopsis": "Trailer for a fake anime created by Shaft as an April Fool's Day joke.", - "coverImageTopOffset": 80, - "titles": { - "en": "", - "en_jp": "Nisekoimonogatari", - "ja_jp": "" - }, - "canonicalTitle": "Nisekoimonogatari", - "abbreviatedTitles": null, - "averageRating": 3.4857993435287, - "ratingFrequencies": { - "0.5": "22", - "1.0": "10", - "1.5": "16", - "2.0": "32", - "2.5": "74", - "3.0": "97", - "3.5": "118", - "4.0": "72", - "4.5": "34", - "5.0": "136", - "nil": "597", - "0.89": "-1", - "3.63": "-1", - "4.11": "-1", - "0.068": "-1", - "0.205": "-1", - "0.274": "-2", - "0.479": "-1", - "0.548": "-1", - "1.096": "-2", - "1.164": "-1", - "1.438": "-1", - "1.918": "-1", - "2.055": "-1", - "3.973": "-1", - "4.178": "-3", - "4.247": "-1", - "4.726": "-1", - "4.932": "-3", - "1.0958904109589": "3", - "0.89041095890411": "2", - "1.02739726027397": "1", - "1.16438356164384": "2", - "1.43835616438356": "2", - "1.57534246575342": "1", - "1.91780821917808": "1", - "2.05479452054794": "2", - "2.12328767123288": "1", - "2.73972602739726": "1", - "2.80821917808219": "2", - "2.94520547945205": "1", - "3.15068493150685": "1", - "3.35616438356164": "2", - "3.63013698630137": "2", - "3.97260273972603": "1", - "4.10958904109589": "2", - "4.17808219178082": "3", - "4.24657534246575": "1", - "4.38356164383562": "2", - "4.65753424657534": "1", - "4.72602739726027": "2", - "4.86301369863014": "1", - "4.93150684931507": "10", - "0.205479452054795": "1", - "0.273972602739726": "2", - "0.479452054794521": "2", - "0.547945205479452": "2", - "0.753424657534246": "1", - "0.0684931506849315": "1" - }, - "startDate": "2015-04-01", - "endDate": null, - "posterImage": { - "tiny": "https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/tiny.jpg?1427974534", - "small": "https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/small.jpg?1427974534", - "medium": "https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/medium.jpg?1427974534", - "large": "https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/large.jpg?1427974534", - "original": "https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/original.jpg?1427974534" - }, - "coverImage": { - "small": "https:\/\/media.kitsu.io\/anime\/cover_images\/10802\/small.jpg?1427928458", - "large": "https:\/\/media.kitsu.io\/anime\/cover_images\/10802\/large.jpg?1427928458", - "original": "https:\/\/media.kitsu.io\/anime\/cover_images\/10802\/original.jpg?1427928458" - }, - "episodeCount": 1, - "episodeLength": 1, - "subtype": "ONA", - "youtubeVideoId": "", - "ageRating": "PG", - "ageRatingGuide": "Teens 13 or older", - "showType": "ONA", - "nsfw": false, - "relationships": { - "genres": ["3"], - "mappings": ["1755"] - } - }, - "11887": { - "slug": "brave-witches", - "synopsis": "In September 1944, allied forces led by the 501st Joint Fighter Wing \"Strike Witches\" successfully eliminate the Neuroi threat from the skies of the Republic of Gallia, thus ensuring the security of western Europe. Taking advantage of this victory, allied forces begin a full-fledged push toward central and eastern Europe. From a base in Petersburg in the Empire of Orussia, the 502nd Joint Fighter Wing \"Brave Witches,\" upon whom mankind has placed its hopes, flies with courage in the cold skies of eastern Europe.\n\n(Source: MAL News)", - "coverImageTopOffset": 380, - "titles": { - "en": "", - "en_jp": "Brave Witches", - "ja_jp": "\u30d6\u30ec\u30a4\u30d6\u30a6\u30a3\u30c3\u30c1\u30fc\u30ba" - }, - "canonicalTitle": "Brave Witches", - "abbreviatedTitles": null, - "averageRating": 3.5846888163849, - "ratingFrequencies": { - "0.5": "1", - "1.0": "4", - "1.5": "8", - "2.0": "12", - "2.5": "17", - "3.0": "33", - "3.5": "41", - "4.0": "32", - "4.5": "9", - "5.0": "19", - "nil": "620" - }, - "startDate": "2016-10-06", - "endDate": null, - "posterImage": { - "tiny": "https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/tiny.jpg?1476481854", - "small": "https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/small.jpg?1476481854", - "medium": "https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/medium.jpg?1476481854", - "large": "https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/large.jpg?1476481854", - "original": "https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/original.png?1476481854" - }, - "coverImage": { - "small": "https:\/\/media.kitsu.io\/anime\/cover_images\/11887\/small.jpg?1479834725", - "large": "https:\/\/media.kitsu.io\/anime\/cover_images\/11887\/large.jpg?1479834725", - "original": "https:\/\/media.kitsu.io\/anime\/cover_images\/11887\/original.jpg?1479834725" - }, - "episodeCount": 12, - "episodeLength": 24, - "subtype": "TV", - "youtubeVideoId": "VLUqd-jEBuE", - "ageRating": "R", - "ageRatingGuide": "Mild Nudity", - "showType": "TV", - "nsfw": false, - "relationships": { - "genres": ["5", "8", "28", "1", "25"], - "mappings": ["2593"] - } - }, - "12024": { - "slug": "www-working", - "synopsis": "Daisuke Higashida is a serious first-year student at Higashizaka High School. He lives a peaceful everyday life even though he is not satisfied with the family who doesn't laugh at all and makes him tired. However, his father's company goes bankrupt one day, and he can no longer afford allowances, cellphone bills, and commuter tickets. When his father orders him to take up a part-time job, Daisuke decides to work at a nearby family restaurant in order to avoid traveling 15 kilometers to school by bicycle.", - "coverImageTopOffset": 165, - "titles": { - "en": "WWW.WAGNARIA!!", - "en_jp": "WWW.Working!!", - "ja_jp": "" - }, - "canonicalTitle": "WWW.Working!!", - "abbreviatedTitles": null, - "averageRating": 3.8238374224378, - "ratingFrequencies": { - "1.0": "2", - "1.5": "7", - "2.0": "19", - "2.5": "28", - "3.0": "68", - "3.5": "114", - "4.0": "144", - "4.5": "78", - "5.0": "74", - "nil": "1182" - }, - "startDate": "2016-10-01", - "endDate": "2016-12-24", - "posterImage": { - "tiny": "https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/tiny.jpg?1473990267", - "small": "https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/small.jpg?1473990267", - "medium": "https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/medium.jpg?1473990267", - "large": "https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/large.jpg?1473990267", - "original": "https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/original.jpg?1473990267" - }, - "coverImage": { - "small": "https:\/\/media.kitsu.io\/anime\/cover_images\/12024\/small.jpg?1479834612", - "large": "https:\/\/media.kitsu.io\/anime\/cover_images\/12024\/large.jpg?1479834612", - "original": "https:\/\/media.kitsu.io\/anime\/cover_images\/12024\/original.png?1479834612" - }, - "episodeCount": 13, - "episodeLength": 23, - "subtype": "TV", - "youtubeVideoId": "", - "ageRating": "PG", - "ageRatingGuide": "Teens 13 or older", - "showType": "TV", - "nsfw": false, - "relationships": { - "genres": ["3", "16"], - "mappings": ["2538"] - } - }, - "12465": { - "slug": "bishoujo-yuugi-unit-crane-game-girls-galaxy", - "synopsis": "Second season of Bishoujo Yuugi Unit Crane Game Girls.", - "coverImageTopOffset": 0, - "titles": { - "en": "Crane Game Girls Galaxy", - "en_jp": "Bishoujo Yuugi Unit Crane Game Girls Galaxy", - "ja_jp": "\u7f8e\u5c11\u5973\u904a\u622f\u30e6\u30cb\u30c3\u30c8 \u30af\u30ec\u30fc\u30f3\u30b2\u30fc\u30eb\u30ae\u30e3\u30e9\u30af\u30b7\u30fc" - }, - "canonicalTitle": "Bishoujo Yuugi Unit Crane Game Girls Galaxy", - "abbreviatedTitles": null, - "averageRating": null, - "ratingFrequencies": { - "0.5": "2", - "1.0": "2", - "1.5": "0", - "2.0": "4", - "2.5": "6", - "3.0": "2", - "3.5": "4", - "4.0": "1", - "4.5": "2", - "nil": "66" - }, - "startDate": "2016-10-05", - "endDate": null, - "posterImage": { - "tiny": "https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/tiny.jpg?1473601756", - "small": "https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/small.jpg?1473601756", - "medium": "https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/medium.jpg?1473601756", - "large": "https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/large.jpg?1473601756", - "original": "https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/original.png?1473601756" - }, - "coverImage": null, - "episodeCount": null, - "episodeLength": 13, - "subtype": "TV", - "youtubeVideoId": "", - "ageRating": "PG", - "ageRatingGuide": "Children", - "showType": "TV", - "nsfw": false, - "relationships": { - "genres": ["3"], - "mappings": ["9871"] - } - } - }, - "genres": { - "24": { - "name": "School", - "slug": "school", - "description": null - }, - "35": { - "name": "Music", - "slug": "music", - "description": null - }, - "4": { - "name": "Drama", - "slug": "drama", - "description": "" - }, - "3": { - "name": "Comedy", - "slug": "comedy", - "description": null - }, - "5": { - "name": "Sci-Fi", - "slug": "sci-fi", - "description": null - }, - "8": { - "name": "Magic", - "slug": "magic", - "description": null - }, - "28": { - "name": "Military", - "slug": "military", - "description": null - }, - "1": { - "name": "Action", - "slug": "action", - "description": "" - }, - "25": { - "name": "Ecchi", - "slug": "ecchi", - "description": "" - }, - "16": { - "name": "Slice of Life", - "slug": "slice-of-life", - "description": "" - } - }, - "mappings": { - "3155": { - "externalSite": "myanimelist\/anime", - "externalId": "31988", - "relationships": [] - }, - "1755": { - "externalSite": "myanimelist\/anime", - "externalId": "30514", - "relationships": [] - }, - "2593": { - "externalSite": "myanimelist\/anime", - "externalId": "32866", - "relationships": [] - }, - "2538": { - "externalSite": "myanimelist\/anime", - "externalId": "33094", - "relationships": [] - }, - "9871": { - "externalSite": "myanimelist\/anime", - "externalId": "33541", - "relationships": [] - } - } -} \ No newline at end of file +{"anime":{"11474":{"slug":"hibike-euphonium-2","synopsis":"Second season of Hibike! Euphonium.","coverImageTopOffset":120,"titles":{"en":"Sound! Euphonium 2","en_jp":"Hibike! Euphonium 2","ja_jp":"\u97ff\u3051\uff01\u30e6\u30fc\u30d5\u30a9\u30cb\u30a2\u30e0 \uff12"},"canonicalTitle":"Hibike! Euphonium 2","abbreviatedTitles":null,"averageRating":4.1684326428476197,"ratingFrequencies":{"0.5":"1","1.0":"1","1.5":"2","2.0":"8","2.5":"13","3.0":"42","3.5":"90","4.0":"193","4.5":"180","5.0":"193","nil":"1972"},"startDate":"2016-10-06","endDate":null,"posterImage":{"tiny":"https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/tiny.jpg?1470781430","small":"https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/small.jpg?1470781430","medium":"https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/medium.jpg?1470781430","large":"https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/large.jpg?1470781430","original":"https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/original.jpg?1470781430"},"coverImage":{"small":"https:\/\/media.kitsu.io\/anime\/cover_images\/11474\/small.jpg?1476203965","large":"https:\/\/media.kitsu.io\/anime\/cover_images\/11474\/large.jpg?1476203965","original":"https:\/\/media.kitsu.io\/anime\/cover_images\/11474\/original.jpg?1476203965"},"episodeCount":13,"episodeLength":25,"subtype":"TV","youtubeVideoId":"d2Di5swwzxg","ageRating":"PG","ageRatingGuide":"","showType":"TV","nsfw":false,"relationships":{"genres":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/11474\/relationships\/genres","related":"https:\/\/kitsu.io\/api\/edge\/anime\/11474\/genres"},"data":[{"type":"genres","id":"24"},{"type":"genres","id":"35"},{"type":"genres","id":"4"}],"0":"24","1":"35","2":"4"},"castings":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/11474\/relationships\/castings","related":"https:\/\/kitsu.io\/api\/edge\/anime\/11474\/castings"}},"installments":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/11474\/relationships\/installments","related":"https:\/\/kitsu.io\/api\/edge\/anime\/11474\/installments"}},"mappings":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/11474\/relationships\/mappings","related":"https:\/\/kitsu.io\/api\/edge\/anime\/11474\/mappings"},"data":[{"type":"mappings","id":"3155"}],"0":"3155"},"reviews":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/11474\/relationships\/reviews","related":"https:\/\/kitsu.io\/api\/edge\/anime\/11474\/reviews"}},"episodes":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/11474\/relationships\/episodes","related":"https:\/\/kitsu.io\/api\/edge\/anime\/11474\/episodes"}},"streamingLinks":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/11474\/relationships\/streaming-links","related":"https:\/\/kitsu.io\/api\/edge\/anime\/11474\/streaming-links"}}}},"10802":{"slug":"nisekoimonogatari","synopsis":"Trailer for a fake anime created by Shaft as an April Fool's Day joke.","coverImageTopOffset":80,"titles":{"en":"","en_jp":"Nisekoimonogatari","ja_jp":""},"canonicalTitle":"Nisekoimonogatari","abbreviatedTitles":null,"averageRating":3.48579934352873,"ratingFrequencies":{"0.5":"22","1.0":"10","1.5":"16","2.0":"32","2.5":"74","3.0":"97","3.5":"118","4.0":"72","4.5":"34","5.0":"136","nil":"597","0.89":"-1","3.63":"-1","4.11":"-1","0.068":"-1","0.205":"-1","0.274":"-2","0.479":"-1","0.548":"-1","1.096":"-2","1.164":"-1","1.438":"-1","1.918":"-1","2.055":"-1","3.973":"-1","4.178":"-3","4.247":"-1","4.726":"-1","4.932":"-3","1.0958904109589":"3","0.89041095890411":"2","1.02739726027397":"1","1.16438356164384":"2","1.43835616438356":"2","1.57534246575342":"1","1.91780821917808":"1","2.05479452054794":"2","2.12328767123288":"1","2.73972602739726":"1","2.80821917808219":"2","2.94520547945205":"1","3.15068493150685":"1","3.35616438356164":"2","3.63013698630137":"2","3.97260273972603":"1","4.10958904109589":"2","4.17808219178082":"3","4.24657534246575":"1","4.38356164383562":"2","4.65753424657534":"1","4.72602739726027":"2","4.86301369863014":"1","4.93150684931507":"10","0.205479452054795":"1","0.273972602739726":"2","0.479452054794521":"2","0.547945205479452":"2","0.753424657534246":"1","0.0684931506849315":"1"},"startDate":"2015-04-01","endDate":null,"posterImage":{"tiny":"https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/tiny.jpg?1427974534","small":"https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/small.jpg?1427974534","medium":"https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/medium.jpg?1427974534","large":"https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/large.jpg?1427974534","original":"https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/original.jpg?1427974534"},"coverImage":{"small":"https:\/\/media.kitsu.io\/anime\/cover_images\/10802\/small.jpg?1427928458","large":"https:\/\/media.kitsu.io\/anime\/cover_images\/10802\/large.jpg?1427928458","original":"https:\/\/media.kitsu.io\/anime\/cover_images\/10802\/original.jpg?1427928458"},"episodeCount":1,"episodeLength":1,"subtype":"ONA","youtubeVideoId":"","ageRating":"PG","ageRatingGuide":"Teens 13 or older","showType":"ONA","nsfw":false,"relationships":{"genres":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/10802\/relationships\/genres","related":"https:\/\/kitsu.io\/api\/edge\/anime\/10802\/genres"},"data":[{"type":"genres","id":"3"}],"0":"3"},"castings":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/10802\/relationships\/castings","related":"https:\/\/kitsu.io\/api\/edge\/anime\/10802\/castings"}},"installments":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/10802\/relationships\/installments","related":"https:\/\/kitsu.io\/api\/edge\/anime\/10802\/installments"}},"mappings":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/10802\/relationships\/mappings","related":"https:\/\/kitsu.io\/api\/edge\/anime\/10802\/mappings"},"data":[{"type":"mappings","id":"1755"}],"0":"1755"},"reviews":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/10802\/relationships\/reviews","related":"https:\/\/kitsu.io\/api\/edge\/anime\/10802\/reviews"}},"episodes":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/10802\/relationships\/episodes","related":"https:\/\/kitsu.io\/api\/edge\/anime\/10802\/episodes"}},"streamingLinks":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/10802\/relationships\/streaming-links","related":"https:\/\/kitsu.io\/api\/edge\/anime\/10802\/streaming-links"}}}},"11887":{"slug":"brave-witches","synopsis":"In September 1944, allied forces led by the 501st Joint Fighter Wing \"Strike Witches\" successfully eliminate the Neuroi threat from the skies of the Republic of Gallia, thus ensuring the security of western Europe. Taking advantage of this victory, allied forces begin a full-fledged push toward central and eastern Europe. From a base in Petersburg in the Empire of Orussia, the 502nd Joint Fighter Wing \"Brave Witches,\" upon whom mankind has placed its hopes, flies with courage in the cold skies of eastern Europe.\n\n(Source: MAL News)","coverImageTopOffset":380,"titles":{"en":"","en_jp":"Brave Witches","ja_jp":"\u30d6\u30ec\u30a4\u30d6\u30a6\u30a3\u30c3\u30c1\u30fc\u30ba"},"canonicalTitle":"Brave Witches","abbreviatedTitles":null,"averageRating":3.5846888163849102,"ratingFrequencies":{"0.5":"1","1.0":"4","1.5":"8","2.0":"12","2.5":"17","3.0":"33","3.5":"41","4.0":"32","4.5":"9","5.0":"19","nil":"620"},"startDate":"2016-10-06","endDate":null,"posterImage":{"tiny":"https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/tiny.jpg?1476481854","small":"https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/small.jpg?1476481854","medium":"https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/medium.jpg?1476481854","large":"https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/large.jpg?1476481854","original":"https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/original.png?1476481854"},"coverImage":{"small":"https:\/\/media.kitsu.io\/anime\/cover_images\/11887\/small.jpg?1479834725","large":"https:\/\/media.kitsu.io\/anime\/cover_images\/11887\/large.jpg?1479834725","original":"https:\/\/media.kitsu.io\/anime\/cover_images\/11887\/original.jpg?1479834725"},"episodeCount":12,"episodeLength":24,"subtype":"TV","youtubeVideoId":"VLUqd-jEBuE","ageRating":"R","ageRatingGuide":"Mild Nudity","showType":"TV","nsfw":false,"relationships":{"genres":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/11887\/relationships\/genres","related":"https:\/\/kitsu.io\/api\/edge\/anime\/11887\/genres"},"data":[{"type":"genres","id":"5"},{"type":"genres","id":"8"},{"type":"genres","id":"28"},{"type":"genres","id":"1"},{"type":"genres","id":"25"}],"0":"5","1":"8","2":"28","3":"1","4":"25"},"castings":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/11887\/relationships\/castings","related":"https:\/\/kitsu.io\/api\/edge\/anime\/11887\/castings"}},"installments":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/11887\/relationships\/installments","related":"https:\/\/kitsu.io\/api\/edge\/anime\/11887\/installments"}},"mappings":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/11887\/relationships\/mappings","related":"https:\/\/kitsu.io\/api\/edge\/anime\/11887\/mappings"},"data":[{"type":"mappings","id":"2593"}],"0":"2593"},"reviews":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/11887\/relationships\/reviews","related":"https:\/\/kitsu.io\/api\/edge\/anime\/11887\/reviews"}},"episodes":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/11887\/relationships\/episodes","related":"https:\/\/kitsu.io\/api\/edge\/anime\/11887\/episodes"}},"streamingLinks":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/11887\/relationships\/streaming-links","related":"https:\/\/kitsu.io\/api\/edge\/anime\/11887\/streaming-links"}}}},"12024":{"slug":"www-working","synopsis":"Daisuke Higashida is a serious first-year student at Higashizaka High School. He lives a peaceful everyday life even though he is not satisfied with the family who doesn't laugh at all and makes him tired. However, his father's company goes bankrupt one day, and he can no longer afford allowances, cellphone bills, and commuter tickets. When his father orders him to take up a part-time job, Daisuke decides to work at a nearby family restaurant in order to avoid traveling 15 kilometers to school by bicycle.","coverImageTopOffset":165,"titles":{"en":"WWW.WAGNARIA!!","en_jp":"WWW.Working!!","ja_jp":""},"canonicalTitle":"WWW.Working!!","abbreviatedTitles":null,"averageRating":3.8238374224378302,"ratingFrequencies":{"1.0":"2","1.5":"7","2.0":"19","2.5":"28","3.0":"68","3.5":"114","4.0":"144","4.5":"78","5.0":"74","nil":"1182"},"startDate":"2016-10-01","endDate":"2016-12-24","posterImage":{"tiny":"https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/tiny.jpg?1473990267","small":"https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/small.jpg?1473990267","medium":"https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/medium.jpg?1473990267","large":"https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/large.jpg?1473990267","original":"https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/original.jpg?1473990267"},"coverImage":{"small":"https:\/\/media.kitsu.io\/anime\/cover_images\/12024\/small.jpg?1479834612","large":"https:\/\/media.kitsu.io\/anime\/cover_images\/12024\/large.jpg?1479834612","original":"https:\/\/media.kitsu.io\/anime\/cover_images\/12024\/original.png?1479834612"},"episodeCount":13,"episodeLength":23,"subtype":"TV","youtubeVideoId":"","ageRating":"PG","ageRatingGuide":"Teens 13 or older","showType":"TV","nsfw":false,"relationships":{"genres":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/12024\/relationships\/genres","related":"https:\/\/kitsu.io\/api\/edge\/anime\/12024\/genres"},"data":[{"type":"genres","id":"3"},{"type":"genres","id":"16"}],"0":"3","1":"16"},"castings":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/12024\/relationships\/castings","related":"https:\/\/kitsu.io\/api\/edge\/anime\/12024\/castings"}},"installments":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/12024\/relationships\/installments","related":"https:\/\/kitsu.io\/api\/edge\/anime\/12024\/installments"}},"mappings":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/12024\/relationships\/mappings","related":"https:\/\/kitsu.io\/api\/edge\/anime\/12024\/mappings"},"data":[{"type":"mappings","id":"2538"}],"0":"2538"},"reviews":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/12024\/relationships\/reviews","related":"https:\/\/kitsu.io\/api\/edge\/anime\/12024\/reviews"}},"episodes":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/12024\/relationships\/episodes","related":"https:\/\/kitsu.io\/api\/edge\/anime\/12024\/episodes"}},"streamingLinks":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/12024\/relationships\/streaming-links","related":"https:\/\/kitsu.io\/api\/edge\/anime\/12024\/streaming-links"}}}},"12465":{"slug":"bishoujo-yuugi-unit-crane-game-girls-galaxy","synopsis":"Second season of Bishoujo Yuugi Unit Crane Game Girls.","coverImageTopOffset":0,"titles":{"en":"Crane Game Girls Galaxy","en_jp":"Bishoujo Yuugi Unit Crane Game Girls Galaxy","ja_jp":"\u7f8e\u5c11\u5973\u904a\u622f\u30e6\u30cb\u30c3\u30c8 \u30af\u30ec\u30fc\u30f3\u30b2\u30fc\u30eb\u30ae\u30e3\u30e9\u30af\u30b7\u30fc"},"canonicalTitle":"Bishoujo Yuugi Unit Crane Game Girls Galaxy","abbreviatedTitles":null,"averageRating":null,"ratingFrequencies":{"0.5":"2","1.0":"2","1.5":"0","2.0":"4","2.5":"6","3.0":"2","3.5":"4","4.0":"1","4.5":"2","nil":"66"},"startDate":"2016-10-05","endDate":null,"posterImage":{"tiny":"https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/tiny.jpg?1473601756","small":"https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/small.jpg?1473601756","medium":"https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/medium.jpg?1473601756","large":"https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/large.jpg?1473601756","original":"https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/original.png?1473601756"},"coverImage":null,"episodeCount":null,"episodeLength":13,"subtype":"TV","youtubeVideoId":"","ageRating":"PG","ageRatingGuide":"Children","showType":"TV","nsfw":false,"relationships":{"genres":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/12465\/relationships\/genres","related":"https:\/\/kitsu.io\/api\/edge\/anime\/12465\/genres"},"data":[{"type":"genres","id":"3"}],"0":"3"},"castings":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/12465\/relationships\/castings","related":"https:\/\/kitsu.io\/api\/edge\/anime\/12465\/castings"}},"installments":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/12465\/relationships\/installments","related":"https:\/\/kitsu.io\/api\/edge\/anime\/12465\/installments"}},"mappings":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/12465\/relationships\/mappings","related":"https:\/\/kitsu.io\/api\/edge\/anime\/12465\/mappings"},"data":[{"type":"mappings","id":"9871"}],"0":"9871"},"reviews":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/12465\/relationships\/reviews","related":"https:\/\/kitsu.io\/api\/edge\/anime\/12465\/reviews"}},"episodes":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/12465\/relationships\/episodes","related":"https:\/\/kitsu.io\/api\/edge\/anime\/12465\/episodes"}},"streamingLinks":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/anime\/12465\/relationships\/streaming-links","related":"https:\/\/kitsu.io\/api\/edge\/anime\/12465\/streaming-links"}}}}},"genres":{"24":{"name":"School","slug":"school","description":null},"35":{"name":"Music","slug":"music","description":null},"4":{"name":"Drama","slug":"drama","description":""},"3":{"name":"Comedy","slug":"comedy","description":null},"5":{"name":"Sci-Fi","slug":"sci-fi","description":null},"8":{"name":"Magic","slug":"magic","description":null},"28":{"name":"Military","slug":"military","description":null},"1":{"name":"Action","slug":"action","description":""},"25":{"name":"Ecchi","slug":"ecchi","description":""},"16":{"name":"Slice of Life","slug":"slice-of-life","description":""}},"mappings":{"3155":{"externalSite":"myanimelist\/anime","externalId":"31988","relationships":{"media":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/mappings\/3155\/relationships\/media","related":"https:\/\/kitsu.io\/api\/edge\/mappings\/3155\/media"}}}},"1755":{"externalSite":"myanimelist\/anime","externalId":"30514","relationships":{"media":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/mappings\/1755\/relationships\/media","related":"https:\/\/kitsu.io\/api\/edge\/mappings\/1755\/media"}}}},"2593":{"externalSite":"myanimelist\/anime","externalId":"32866","relationships":{"media":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/mappings\/2593\/relationships\/media","related":"https:\/\/kitsu.io\/api\/edge\/mappings\/2593\/media"}}}},"2538":{"externalSite":"myanimelist\/anime","externalId":"33094","relationships":{"media":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/mappings\/2538\/relationships\/media","related":"https:\/\/kitsu.io\/api\/edge\/mappings\/2538\/media"}}}},"9871":{"externalSite":"myanimelist\/anime","externalId":"33541","relationships":{"media":{"links":{"self":"https:\/\/kitsu.io\/api\/edge\/mappings\/9871\/relationships\/media","related":"https:\/\/kitsu.io\/api\/edge\/mappings\/9871\/media"}}}}}} \ No newline at end of file diff --git a/tests/test_data/Kitsu/animeListItemBeforeTransform.json b/tests/test_data/Kitsu/animeListItemBeforeTransform.json index 3421281c..223ce859 100644 --- a/tests/test_data/Kitsu/animeListItemBeforeTransform.json +++ b/tests/test_data/Kitsu/animeListItemBeforeTransform.json @@ -12,6 +12,7 @@ "notes": null, "private": false, "rating": null, + "ratingTwenty": null, "updatedAt": "2017-01-13T01:32:31.832Z" }, "relationships": { @@ -114,19 +115,19 @@ "showType": "TV", "nsfw": false, "relationships": { - "genres": { + "categories": { "3": { - "name": "Comedy", + "title": "Comedy", "slug": "comedy", "description": null }, "11": { - "name": "Fantasy", + "title": "Fantasy", "slug": "fantasy", "description": "" }, "16": { - "name": "Slice of Life", + "title": "Slice of Life", "slug": "slice-of-life", "description": "" } @@ -188,24 +189,24 @@ "showType": "TV", "nsfw": false, "relationships": { - "genres": { + "categories": { "8": { - "name": "Magic", + "title": "Magic", "slug": "magic", "description": null }, "40": { - "name": "Kids", + "title": "Kids", "slug": "kids", "description": null }, "47": { - "name": "Mahou Shoujo", + "title": "Mahou Shoujo", "slug": "mahou-shoujo", "description": "Magical Girls" }, "11": { - "name": "Fantasy", + "title": "Fantasy", "slug": "fantasy", "description": "" } @@ -264,9 +265,9 @@ "showType": "TV", "nsfw": false, "relationships": { - "genres": { + "categories": { "35": { - "name": "Music", + "title": "Music", "slug": "music", "description": null } @@ -324,19 +325,19 @@ "showType": "TV", "nsfw": false, "relationships": { - "genres": { + "categories": { "3": { - "name": "Comedy", + "title": "Comedy", "slug": "comedy", "description": null }, "9": { - "name": "Supernatural", + "title": "Supernatural", "slug": "supernatural", "description": null }, "24": { - "name": "School", + "title": "School", "slug": "school", "description": null } @@ -398,29 +399,29 @@ "showType": "TV", "nsfw": false, "relationships": { - "genres": { + "categories": { "2": { - "name": "Adventure", + "title": "Adventure", "slug": "adventure", "description": null }, "3": { - "name": "Comedy", + "title": "Comedy", "slug": "comedy", "description": null }, "8": { - "name": "Magic", + "title": "Magic", "slug": "magic", "description": null }, "9": { - "name": "Supernatural", + "title": "Supernatural", "slug": "supernatural", "description": null }, "11": { - "name": "Fantasy", + "title": "Fantasy", "slug": "fantasy", "description": "" } @@ -478,14 +479,14 @@ "showType": "TV", "nsfw": false, "relationships": { - "genres": { + "categories": { "3": { - "name": "Comedy", + "title": "Comedy", "slug": "comedy", "description": null }, "16": { - "name": "Slice of Life", + "title": "Slice of Life", "slug": "slice-of-life", "description": "" } @@ -546,24 +547,24 @@ "showType": "TV", "nsfw": false, "relationships": { - "genres": { + "categories": { "3": { - "name": "Comedy", + "title": "Comedy", "slug": "comedy", "description": null }, "24": { - "name": "School", + "title": "School", "slug": "school", "description": null }, "34": { - "name": "Harem", + "title": "Harem", "slug": "harem", "description": null }, "14": { - "name": "Romance", + "title": "Romance", "slug": "romance", "description": "" } @@ -618,19 +619,19 @@ "showType": "TV", "nsfw": false, "relationships": { - "genres": { + "categories": { "3": { - "name": "Comedy", + "title": "Comedy", "slug": "comedy", "description": null }, "9": { - "name": "Supernatural", + "title": "Supernatural", "slug": "supernatural", "description": null }, "24": { - "name": "School", + "title": "School", "slug": "school", "description": null } @@ -688,24 +689,24 @@ "showType": "special", "nsfw": false, "relationships": { - "genres": { + "categories": { "8": { - "name": "Magic", + "title": "Magic", "slug": "magic", "description": null }, "9": { - "name": "Supernatural", + "title": "Supernatural", "slug": "supernatural", "description": null }, "11": { - "name": "Fantasy", + "title": "Fantasy", "slug": "fantasy", "description": "" }, "1": { - "name": "Action", + "title": "Action", "slug": "action", "description": "" } @@ -763,14 +764,14 @@ "showType": "TV", "nsfw": false, "relationships": { - "genres": { + "categories": { "3": { - "name": "Comedy", + "title": "Comedy", "slug": "comedy", "description": null }, "35": { - "name": "Music", + "title": "Music", "slug": "music", "description": null } @@ -828,29 +829,29 @@ "showType": "TV", "nsfw": false, "relationships": { - "genres": { + "categories": { "8": { - "name": "Magic", + "title": "Magic", "slug": "magic", "description": null }, "27": { - "name": "Historical", + "title": "Historical", "slug": "historical", "description": null }, "28": { - "name": "Military", + "title": "Military", "slug": "military", "description": null }, "4": { - "name": "Drama", + "title": "Drama", "slug": "drama", "description": "" }, "1": { - "name": "Action", + "title": "Action", "slug": "action", "description": "" } @@ -912,9 +913,9 @@ "showType": "TV", "nsfw": false, "relationships": { - "genres": { + "categories": { "3": { - "name": "Comedy", + "title": "Comedy", "slug": "comedy", "description": null } @@ -976,24 +977,24 @@ "showType": "TV", "nsfw": false, "relationships": { - "genres": { + "categories": { "7": { - "name": "Mystery", + "title": "Mystery", "slug": "mystery", "description": null }, "9": { - "name": "Supernatural", + "title": "Supernatural", "slug": "supernatural", "description": null }, "4": { - "name": "Drama", + "title": "Drama", "slug": "drama", "description": "" }, "1": { - "name": "Action", + "title": "Action", "slug": "action", "description": "" } @@ -1055,29 +1056,29 @@ "showType": "TV", "nsfw": false, "relationships": { - "genres": { + "categories": { "9": { - "name": "Supernatural", + "title": "Supernatural", "slug": "supernatural", "description": null }, "21": { - "name": "Thriller", + "title": "Thriller", "slug": "thriller", "description": null }, "47": { - "name": "Mahou Shoujo", + "title": "Mahou Shoujo", "slug": "mahou-shoujo", "description": "Magical Girls" }, "11": { - "name": "Fantasy", + "title": "Fantasy", "slug": "fantasy", "description": "" }, "1": { - "name": "Action", + "title": "Action", "slug": "action", "description": "" } diff --git a/tests/test_data/Kitsu/mangaBeforeTransform.json b/tests/test_data/Kitsu/mangaBeforeTransform.json index a28dc569..6126fc64 100644 --- a/tests/test_data/Kitsu/mangaBeforeTransform.json +++ b/tests/test_data/Kitsu/mangaBeforeTransform.json @@ -10,7 +10,7 @@ "synopsis": "Usa, a high-school student aspiring to begin a bachelor lifestyle, moves into a new apartment only to discover that he not only shares a room with a perverted roommate that has an obsession for underaged girls, but also that another girl, Ritsu, a love-at-first-sight, is living in the same building as well!\n(Source: Kirei Cake)", "coverImageTopOffset": 40, "titles": { - "en": null, + "en_us": null, "en_jp": "Bokura wa Minna Kawaisou" }, "canonicalTitle": "Bokura wa Minna Kawaisou", diff --git a/tests/test_data/Kitsu/mangaListBeforeTransform.json b/tests/test_data/Kitsu/mangaListBeforeTransform.json index 9380f523..585921e5 100644 --- a/tests/test_data/Kitsu/mangaListBeforeTransform.json +++ b/tests/test_data/Kitsu/mangaListBeforeTransform.json @@ -14,6 +14,7 @@ "notes": "", "private": false, "rating": "4.5", + "ratingTwenty": "18", "updatedAt": "2017-01-09T17:51:16.691Z" }, "relationships": { @@ -85,6 +86,7 @@ "notes": "", "private": false, "rating": "3.5", + "ratingTwenty": "14", "updatedAt": "2017-01-09T17:50:19.594Z" }, "relationships": { @@ -156,6 +158,7 @@ "notes": "", "private": false, "rating": "4.5", + "ratingTwenty": "18", "updatedAt": "2016-04-07T17:10:13.022Z" }, "relationships": { @@ -227,6 +230,7 @@ "notes": "", "private": false, "rating": null, + "ratingTwenty": null, "updatedAt": "2016-03-08T15:45:45.818Z" }, "relationships": { @@ -298,6 +302,7 @@ "notes": "", "private": false, "rating": "4.0", + "ratingTwenty": "16", "updatedAt": "2016-02-02T15:06:07.166Z" }, "relationships": { @@ -414,30 +419,30 @@ "mangaType": "manga" }, "relationships": { - "genres": { + "categories": { "links": { - "self": "https://kitsu.io/api/edge/manga/20286/relationships/genres", - "related": "https://kitsu.io/api/edge/manga/20286/genres" + "self": "https://kitsu.io/api/edge/manga/20286/relationships/categories", + "related": "https://kitsu.io/api/edge/manga/20286/categories" }, "data": [ { - "type": "genres", + "type": "categories", "id": "3" }, { - "type": "genres", + "type": "categories", "id": "21" }, { - "type": "genres", + "type": "categories", "id": "24" }, { - "type": "genres", + "type": "categories", "id": "16" }, { - "type": "genres", + "type": "categories", "id": "14" } ] @@ -549,30 +554,30 @@ "mangaType": "manga" }, "relationships": { - "genres": { + "categories": { "links": { - "self": "https://kitsu.io/api/edge/manga/47/relationships/genres", - "related": "https://kitsu.io/api/edge/manga/47/genres" + "self": "https://kitsu.io/api/edge/manga/47/relationships/categories", + "related": "https://kitsu.io/api/edge/manga/47/categories" }, "data": [ { - "type": "genres", + "type": "categories", "id": "3" }, { - "type": "genres", + "type": "categories", "id": "13" }, { - "type": "genres", + "type": "categories", "id": "34" }, { - "type": "genres", + "type": "categories", "id": "14" }, { - "type": "genres", + "type": "categories", "id": "25" } ] @@ -684,38 +689,38 @@ "mangaType": "manga" }, "relationships": { - "genres": { + "categories": { "links": { - "self": "https://kitsu.io/api/edge/manga/11777/relationships/genres", - "related": "https://kitsu.io/api/edge/manga/11777/genres" + "self": "https://kitsu.io/api/edge/manga/11777/relationships/categories", + "related": "https://kitsu.io/api/edge/manga/11777/categories" }, "data": [ { - "type": "genres", + "type": "categories", "id": "3" }, { - "type": "genres", + "type": "categories", "id": "9" }, { - "type": "genres", + "type": "categories", "id": "13" }, { - "type": "genres", + "type": "categories", "id": "24" }, { - "type": "genres", + "type": "categories", "id": "45" }, { - "type": "genres", + "type": "categories", "id": "14" }, { - "type": "genres", + "type": "categories", "id": "25" } ] @@ -827,22 +832,22 @@ "mangaType": "manga" }, "relationships": { - "genres": { + "categories": { "links": { - "self": "https://kitsu.io/api/edge/manga/27175/relationships/genres", - "related": "https://kitsu.io/api/edge/manga/27175/genres" + "self": "https://kitsu.io/api/edge/manga/27175/relationships/categories", + "related": "https://kitsu.io/api/edge/manga/27175/categories" }, "data": [ { - "type": "genres", + "type": "categories", "id": "24" }, { - "type": "genres", + "type": "categories", "id": "16" }, { - "type": "genres", + "type": "categories", "id": "14" } ] @@ -949,22 +954,22 @@ "mangaType": "manga" }, "relationships": { - "genres": { + "categories": { "links": { - "self": "https://kitsu.io/api/edge/manga/25491/relationships/genres", - "related": "https://kitsu.io/api/edge/manga/25491/genres" + "self": "https://kitsu.io/api/edge/manga/25491/relationships/categories", + "related": "https://kitsu.io/api/edge/manga/25491/categories" }, "data": [ { - "type": "genres", + "type": "categories", "id": "3" }, { - "type": "genres", + "type": "categories", "id": "24" }, { - "type": "genres", + "type": "categories", "id": "16" } ] @@ -1021,120 +1026,120 @@ }, { "id": "3", - "type": "genres", + "type": "categories", "links": { - "self": "https://kitsu.io/api/edge/genres/3" + "self": "https://kitsu.io/api/edge/categories/3" }, "attributes": { - "name": "Comedy", + "title": "Comedy", "slug": "comedy", "description": null } }, { "id": "21", - "type": "genres", + "type": "categories", "links": { - "self": "https://kitsu.io/api/edge/genres/21" + "self": "https://kitsu.io/api/edge/categories/21" }, "attributes": { - "name": "Thriller", + "title": "Thriller", "slug": "thriller", "description": null } }, { "id": "24", - "type": "genres", + "type": "categories", "links": { - "self": "https://kitsu.io/api/edge/genres/24" + "self": "https://kitsu.io/api/edge/categories/24" }, "attributes": { - "name": "School", + "title": "School", "slug": "school", "description": null } }, { "id": "16", - "type": "genres", + "type": "categories", "links": { - "self": "https://kitsu.io/api/edge/genres/16" + "self": "https://kitsu.io/api/edge/categories/16" }, "attributes": { - "name": "Slice of Life", + "title": "Slice of Life", "slug": "slice-of-life", "description": "" } }, { "id": "14", - "type": "genres", + "type": "categories", "links": { - "self": "https://kitsu.io/api/edge/genres/14" + "self": "https://kitsu.io/api/edge/categories/14" }, "attributes": { - "name": "Romance", + "title": "Romance", "slug": "romance", "description": "" } }, { "id": "13", - "type": "genres", + "type": "categories", "links": { - "self": "https://kitsu.io/api/edge/genres/13" + "self": "https://kitsu.io/api/edge/categories/13" }, "attributes": { - "name": "Sports", + "title": "Sports", "slug": "sports", "description": null } }, { "id": "34", - "type": "genres", + "type": "categories", "links": { - "self": "https://kitsu.io/api/edge/genres/34" + "self": "https://kitsu.io/api/edge/categories/34" }, "attributes": { - "name": "Harem", + "title": "Harem", "slug": "harem", "description": null } }, { "id": "25", - "type": "genres", + "type": "categories", "links": { - "self": "https://kitsu.io/api/edge/genres/25" + "self": "https://kitsu.io/api/edge/categories/25" }, "attributes": { - "name": "Ecchi", + "title": "Ecchi", "slug": "ecchi", "description": "" } }, { "id": "9", - "type": "genres", + "type": "categories", "links": { - "self": "https://kitsu.io/api/edge/genres/9" + "self": "https://kitsu.io/api/edge/categories/9" }, "attributes": { - "name": "Supernatural", + "title": "Supernatural", "slug": "supernatural", "description": null } }, { "id": "45", - "type": "genres", + "type": "categories", "links": { - "self": "https://kitsu.io/api/edge/genres/45" + "self": "https://kitsu.io/api/edge/categories/45" }, "attributes": { - "name": "Gender Bender", + "title": "Gender Bender", "slug": "gender-bender", "description": "" } @@ -1239,7 +1244,7 @@ "count": 5 }, "links": { - "first": "https://kitsu.io/api/edge/library-entries?fields%5Busers%5D=id&filter%5Bmedia_type%5D=Manga&filter%5Bstatus%5D=1&filter%5Buser_id%5D=2644&include=media%2Cmedia.genres%2Cmedia.mappings&page%5Blimit%5D=200&page%5Boffset%5D=0&sort=-updated_at", - "last": "https://kitsu.io/api/edge/library-entries?fields%5Busers%5D=id&filter%5Bmedia_type%5D=Manga&filter%5Bstatus%5D=1&filter%5Buser_id%5D=2644&include=media%2Cmedia.genres%2Cmedia.mappings&page%5Blimit%5D=200&page%5Boffset%5D=0&sort=-updated_at" + "first": "https://kitsu.io/api/edge/library-entries?fields%5Busers%5D=id&filter%5Bmedia_type%5D=Manga&filter%5Bstatus%5D=1&filter%5Buser_id%5D=2644&include=media%2Cmedia.categories%2Cmedia.mappings&page%5Blimit%5D=200&page%5Boffset%5D=0&sort=-updated_at", + "last": "https://kitsu.io/api/edge/library-entries?fields%5Busers%5D=id&filter%5Bmedia_type%5D=Manga&filter%5Bstatus%5D=1&filter%5Buser_id%5D=2644&include=media%2Cmedia.categories%2Cmedia.mappings&page%5Blimit%5D=200&page%5Boffset%5D=0&sort=-updated_at" } } \ No newline at end of file