Merge remote-tracking branch 'origin/develop'
timw4mail/HummingBirdAnimeClient/pipeline/head This commit looks good Details

This commit is contained in:
Timothy Warren 2023-03-17 09:17:23 -04:00
commit 5076657b38
267 changed files with 11161 additions and 8999 deletions

6
.gitignore vendored
View File

@ -150,4 +150,8 @@ public/mal_mappings.json
.is-dev
tmp
tmp
tools/vendor/
tools/phinx/vendor/
/.php-cs-fixer.php
/.php-cs-fixer.cache

528
.php-cs-fixer.dist.php Normal file
View File

@ -0,0 +1,528 @@
<?php declare(strict_types=1);
use Nexus\CsConfig\Factory;
use PhpCsFixer\{Config, Finder};
$finder = Finder::create()
->in([
__DIR__,
__DIR__ . '/app',
__DIR__ . '/tools',
])
->exclude([
'apidocs',
'build',
'coverage',
'frontEndSrc',
'phinx',
'public',
'tools',
'tmp',
'vendor',
'views',
'templates',
]);
return (new Config())
->setRiskyAllowed(TRUE)
->setFinder($finder)
->setIndent(' ')
->setRules([
'align_multiline_comment' => false,
'array_indentation' => true,
'array_push' => true,
'array_syntax' => ['syntax' => 'short'],
'assign_null_coalescing_to_coalesce_equal' => true,
'backtick_to_shell_exec' => true,
'binary_operator_spaces' => [
'default' => 'single_space',
'operators' => [
'=' => NULL,
'&' => NULL,
]
],
'blank_line_after_namespace' => true,
'blank_line_after_opening_tag' => false,
'blank_line_before_statement' => [
'statements' => [
'case',
'continue',
'declare',
'default',
'do',
'exit',
'for',
'foreach',
'goto',
'return',
'switch',
'throw',
'try',
'while',
'yield',
'yield_from',
],
],
'braces' => [
'allow_single_line_anonymous_class_with_empty_body' => true,
'allow_single_line_closure' => true,
'position_after_anonymous_constructs' => 'same',
'position_after_control_structures' => 'next',
'position_after_functions_and_oop_constructs' => 'next',
],
'cast_spaces' => ['space' => 'single'],
'class_attributes_separation' => [
'elements' => [
'const' => 'none',
'property' => 'none',
'method' => 'one',
'trait_import' => 'none',
],
],
'class_definition' => [
'multi_line_extends_each_single_line' => true,
'single_item_single_line' => true,
'single_line' => true,
'space_before_parenthesis' => true,
],
'class_reference_name_casing' => true,
'clean_namespace' => true,
'combine_consecutive_issets' => true,
'combine_consecutive_unsets' => true,
'combine_nested_dirname' => true,
'comment_to_phpdoc' => [
'ignored_tags' => [
'todo',
'codeCoverageIgnore',
'codeCoverageIgnoreStart',
'codeCoverageIgnoreEnd',
'phpstan-ignore-line',
'phpstan-ignore-next-line',
],
],
'compact_nullable_typehint' => true,
'concat_space' => ['spacing' => 'one'],
'constant_case' => ['case' => 'upper'],
'control_structure_continuation_position' => ['position' => 'next_line'],
'date_time_immutable' => false,
'declare_equal_normalize' => ['space' => 'none'],
'declare_parentheses' => true,
'declare_strict_types' => true,
'dir_constant' => true,
'doctrine_annotation_array_assignment' => false,
'doctrine_annotation_braces' => false,
'doctrine_annotation_indentation' => false,
'doctrine_annotation_spaces' => false,
'echo_tag_syntax' => [
'format' => 'short',
'long_function' => 'echo',
'shorten_simple_statements_only' => false,
],
'elseif' => true,
'empty_loop_body' => ['style' => 'braces'],
'empty_loop_condition' => ['style' => 'while'],
'encoding' => true,
'error_suppression' => [
'mute_deprecation_error' => true,
'noise_remaining_usages' => false,
'noise_remaining_usages_exclude' => [],
],
'escape_implicit_backslashes' => [
'double_quoted' => true,
'heredoc_syntax' => true,
'single_quoted' => false,
],
'explicit_indirect_variable' => true,
'explicit_string_variable' => true,
'final_class' => false,
'final_internal_class' => [
'annotation_exclude' => ['@no-final'],
'annotation_include' => ['@internal'],
'consider_absent_docblock_as_internal_class' => false,
],
'final_public_method_for_abstract_class' => false,
'fopen_flag_order' => true,
'fopen_flags' => ['b_mode' => true],
'full_opening_tag' => true,
'fully_qualified_strict_types' => true,
'function_declaration' => ['closure_function_spacing' => 'one'],
'function_to_constant' => [
'functions' => [
'get_called_class',
'get_class',
'get_class_this',
'php_sapi_name',
'phpversion',
'pi',
],
],
'function_typehint_space' => true,
'general_phpdoc_annotation_remove' => false,
'general_phpdoc_tag_rename' => false,
'get_class_to_class_keyword' => false,
'global_namespace_import' => [
'import_constants' => true,
'import_functions' => true,
'import_classes' => true,
],
'group_import' => true,
'header_comment' => false, // false by default
'heredoc_indentation' => ['indentation' => 'start_plus_one'],
'heredoc_to_nowdoc' => true,
'implode_call' => true,
'include' => true,
'increment_style' => ['style' => 'post'],
'indentation_type' => true,
'integer_literal_case' => true,
'is_null' => true,
'lambda_not_used_import' => true,
'line_ending' => true,
'linebreak_after_opening_tag' => false,
'list_syntax' => ['syntax' => 'short'],
'logical_operators' => true,
'lowercase_cast' => true,
'lowercase_keywords' => true,
'lowercase_static_reference' => true,
'magic_constant_casing' => true,
'magic_method_casing' => true,
'mb_str_functions' => false,
'method_argument_space' => [
'after_heredoc' => false,
'keep_multiple_spaces_after_comma' => false,
'on_multiline' => 'ensure_fully_multiline',
],
'method_chaining_indentation' => true,
'modernize_strpos' => false, // requires 8.0+
'modernize_types_casting' => true,
'multiline_comment_opening_closing' => true,
'multiline_whitespace_before_semicolons' => ['strategy' => 'no_multi_line'],
'native_constant_invocation' => false,
'native_function_casing' => true,
'native_function_invocation' => false,
'native_function_type_declaration_casing' => true,
'new_with_braces' => true,
'no_alias_functions' => ['sets' => ['@all']],
'no_alias_language_construct_call' => true,
'no_alternative_syntax' => ['fix_non_monolithic_code' => false],
'no_binary_string' => true,
'no_blank_lines_after_class_opening' => true,
'no_blank_lines_after_phpdoc' => true,
'no_blank_lines_before_namespace' => false, // conflicts with `single_blank_line_before_namespace`
'no_break_comment' => ['comment_text' => 'no break'],
'no_closing_tag' => true,
'no_empty_comment' => true,
'no_empty_phpdoc' => true,
'no_empty_statement' => true,
'no_extra_blank_lines' => ['tokens' => ['extra']],
'no_homoglyph_names' => true,
'no_leading_import_slash' => true,
'no_leading_namespace_whitespace' => true,
'no_mixed_echo_print' => ['use' => 'echo'],
'no_multiline_whitespace_around_double_arrow' => true,
'no_null_property_initialization' => true,
'no_short_bool_cast' => true,
'no_singleline_whitespace_before_semicolons' => true,
'no_space_around_double_colon' => true,
'no_spaces_after_function_name' => true,
'no_spaces_around_offset' => ['positions' => ['inside', 'outside']],
'no_spaces_inside_parenthesis' => true,
'no_superfluous_elseif' => true,
'no_superfluous_phpdoc_tags' => [
'allow_mixed' => true,
'allow_unused_params' => true,
'remove_inheritdoc' => false,
],
'no_trailing_comma_in_list_call' => true,
'no_trailing_comma_in_singleline_array' => true,
'no_trailing_whitespace' => true,
'no_trailing_whitespace_in_comment' => true,
'no_trailing_whitespace_in_string' => true,
'no_unneeded_control_parentheses' => [
'statements' => [
'break',
'clone',
'continue',
'echo_print',
'return',
'switch_case',
'yield',
],
],
'no_unneeded_curly_braces' => ['namespaces' => true],
'no_unneeded_final_method' => ['private_methods' => true],
'no_unneeded_import_alias' => true,
'no_unreachable_default_argument_value' => true,
'no_unset_cast' => true,
'no_unset_on_property' => false,
'no_unused_imports' => true,
'no_useless_else' => true,
'no_useless_return' => true,
'no_useless_sprintf' => true,
'no_whitespace_before_comma_in_array' => ['after_heredoc' => true],
'no_whitespace_in_blank_line' => true,
'non_printable_character' => ['use_escape_sequences_in_strings' => true],
'normalize_index_brace' => true,
'not_operator_with_space' => true,
'not_operator_with_successor_space' => true,
'nullable_type_declaration_for_default_null_value' => ['use_nullable_type_declaration' => true],
'object_operator_without_whitespace' => true,
'operator_linebreak' => ['only_booleans' => true, 'position' => 'beginning'],
'ordered_class_elements' => [
'order' => [
'use_trait',
'constant',
'property',
'method',
],
'sort_algorithm' => 'none',
],
'ordered_imports' => [
'sort_algorithm' => 'alpha',
'imports_order' => ['class', 'function', 'const'],
],
'ordered_interfaces' => false,
'ordered_traits' => false,
'php_unit_construct' => [
'assertions' => [
'assertSame',
'assertEquals',
'assertNotEquals',
'assertNotSame',
],
],
'php_unit_dedicate_assert' => ['target' => 'newest'],
'php_unit_dedicate_assert_internal_type' => ['target' => 'newest'],
'php_unit_expectation' => ['target' => 'newest'],
'php_unit_fqcn_annotation' => true,
'php_unit_internal_class' => ['types' => ['final']],
'php_unit_method_casing' => ['case' => 'camel_case'],
'php_unit_mock' => ['target' => 'newest'],
'php_unit_mock_short_will_return' => true,
'php_unit_namespaced' => ['target' => 'newest'],
'php_unit_no_expectation_annotation' => [
'target' => 'newest',
'use_class_const' => true,
],
'php_unit_set_up_tear_down_visibility' => true,
'php_unit_size_class' => false,
// 'php_unit_strict' => [
// 'assertions' => [
// 'assertAttributeEquals',
// 'assertAttributeNotEquals',
// 'assertEquals',
// 'assertNotEquals',
// ],
// ],
'php_unit_test_annotation' => ['style' => 'prefix'],
'php_unit_test_case_static_method_calls' => [
'call_type' => 'this',
'methods' => [],
],
'php_unit_test_class_requires_covers' => false,
'phpdoc_add_missing_param_annotation' => ['only_untyped' => true],
'phpdoc_align' => [
'align' => 'left'
],
'phpdoc_annotation_without_dot' => false,
'phpdoc_indent' => true,
'phpdoc_inline_tag_normalizer' => [
'tags' => [
'example',
'id',
'internal',
'inheritdoc',
'inheritdocs',
'link',
'source',
'toc',
'tutorial',
],
],
'phpdoc_line_span' => [
'const' => 'multi',
'method' => 'multi',
'property' => 'multi',
],
'phpdoc_no_access' => true,
'phpdoc_no_empty_return' => false,
'phpdoc_no_package' => false,
'phpdoc_no_useless_inheritdoc' => true,
'phpdoc_order' => true,
'phpdoc_order_by_value' => [
'annotations' => [
'author',
'covers',
'coversNothing',
'dataProvider',
'depends',
'group',
'internal',
'method',
'property',
'property-read',
'property-write',
'requires',
'throws',
'uses',
],
],
'phpdoc_return_self_reference' => [
'replacements' => [
'this' => '$this',
'@this' => '$this',
'$self' => 'self',
'@self' => 'self',
'$static' => 'static',
'@static' => 'static',
],
],
'phpdoc_scalar' => [
'types' => [
'boolean',
'callback',
'double',
'integer',
'real',
'str',
],
],
'phpdoc_separation' => false,
'phpdoc_single_line_var_spacing' => true,
'phpdoc_summary' => false,
'phpdoc_tag_casing' => ['tags' => ['inheritDoc']],
'phpdoc_tag_type' => ['tags' => ['inheritDoc' => 'inline']],
'phpdoc_to_comment' => false,
'phpdoc_to_param_type' => false,
'phpdoc_to_property_type' => false,
'phpdoc_to_return_type' => false,
'phpdoc_trim' => true,
'phpdoc_trim_consecutive_blank_line_separation' => true,
'phpdoc_types' => ['groups' => ['simple', 'alias', 'meta']],
'phpdoc_types_order' => [
'null_adjustment' => 'always_last',
'sort_algorithm' => 'alpha',
],
'phpdoc_var_annotation_correct_order' => true,
'phpdoc_var_without_name' => true,
'pow_to_exponentiation' => true,
'protected_to_private' => true,
'psr_autoloading' => ['dir' => null],
'random_api_migration' => [
'replacements' => [
'getrandmax' => 'mt_getrandmax',
'rand' => 'mt_rand',
'srand' => 'mt_srand',
],
],
'regular_callable_call' => true,
'return_assignment' => true,
'return_type_declaration' => ['space_before' => 'none'],
'self_accessor' => false,
'self_static_accessor' => true,
'semicolon_after_instruction' => false,
'set_type_to_cast' => true,
'short_scalar_cast' => true,
'simple_to_complex_string_variable' => true,
'simplified_if_return' => true,
'simplified_null_return' => false,
'single_blank_line_at_eof' => true,
'single_blank_line_before_namespace' => true,
'single_class_element_per_statement' => ['elements' => ['const', 'property']],
'single_import_per_statement' => false,
'single_line_after_imports' => true,
'single_line_comment_style' => ['comment_types' => ['asterisk', 'hash']],
'single_line_throw' => false,
'single_quote' => ['strings_containing_single_quote_chars' => false],
'single_space_after_construct' => [
'constructs' => [
'abstract',
'as',
'attribute',
'break',
'case',
'catch',
'class',
'clone',
'comment',
'const',
'const_import',
'continue',
'do',
'echo',
'else',
'elseif',
'extends',
'final',
'finally',
'for',
'foreach',
'function',
'function_import',
'global',
'goto',
'if',
'implements',
'include',
'include_once',
'instanceof',
'insteadof',
'interface',
'match',
'named_argument',
'new',
'open_tag_with_echo',
'php_doc',
'php_open',
'print',
'private',
'protected',
'public',
'require',
'require_once',
'return',
'static',
'throw',
'trait',
'try',
'use',
'use_lambda',
'use_trait',
'var',
'while',
'yield',
'yield_from',
],
],
'single_trait_insert_per_statement' => true,
'space_after_semicolon' => ['remove_in_empty_for_expressions' => true],
'standardize_increment' => true,
'standardize_not_equals' => true,
'static_lambda' => true,
'strict_comparison' => true,
'strict_param' => true,
'string_length_to_empty' => true,
'string_line_ending' => true,
'switch_case_semicolon_to_colon' => true,
'switch_case_space' => true,
'switch_continue_to_break' => true,
'ternary_operator_spaces' => true,
'ternary_to_elvis_operator' => true,
'ternary_to_null_coalescing' => true,
'trailing_comma_in_multiline' => [
'after_heredoc' => true,
'elements' => ['arrays'],
],
'trim_array_spaces' => true,
'types_spaces' => ['space' => 'none'],
'unary_operator_spaces' => false,
'use_arrow_functions' => true,
'visibility_required' => ['elements' => ['const', 'method', 'property']],
'void_return' => false, // changes method signature
'whitespace_after_comma_in_array' => true,
'yoda_style' => [
'equal' => false,
'identical' => null,
'less_and_greater' => false,
'always_move_variable' => false,
],
]);

View File

@ -57,4 +57,4 @@ return array_merge($tomlConfig, [
// Included config files
'routes' => require 'routes.php',
]);
]);

View File

@ -16,10 +16,10 @@
use const Aviat\AnimeClient\{
ALPHA_SLUG_PATTERN,
NUM_PATTERN,
SLUG_PATTERN,
DEFAULT_CONTROLLER,
DEFAULT_CONTROLLER_METHOD,
DEFAULT_CONTROLLER
NUM_PATTERN,
SLUG_PATTERN
};
// -------------------------------------------------------------------------
@ -190,14 +190,14 @@ $routes = [
'character' => [
'path' => '/character/{slug}',
'tokens' => [
'slug' => SLUG_PATTERN
]
'slug' => SLUG_PATTERN,
],
],
'person' => [
'path' => '/people/{slug}',
'tokens' => [
'slug' => SLUG_PATTERN,
]
],
],
'default_user_info' => [
'path' => '/me',
@ -209,8 +209,8 @@ $routes = [
'controller' => 'user',
'action' => 'about',
'tokens' => [
'username' => '.*?'
]
'username' => '.*?',
],
],
// ---------------------------------------------------------------------
// Default / Shared routes
@ -231,8 +231,8 @@ $routes = [
'controller' => 'images',
'tokens' => [
'type' => SLUG_PATTERN,
'file' => '[a-z0-9\-]+\.[a-z]{3,4}'
]
'file' => '[a-z0-9\-]+\.[a-z]{3,4}',
],
],
'settings' => [
'path' => '/settings',
@ -259,8 +259,8 @@ $routes = [
'controller' => 'history',
'path' => '/history/{type}',
'tokens' => [
'type' => SLUG_PATTERN
]
'type' => SLUG_PATTERN,
],
],
'increment' => [
'path' => '/{controller}/increment',
@ -316,7 +316,7 @@ $defaultMap = [
foreach ($routes as &$route)
{
foreach($defaultMap as $key => $val)
foreach ($defaultMap as $key => $val)
{
if ( ! array_key_exists($key, $route))
{

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient;
@ -20,12 +18,10 @@ use Aura\Html\HelperLocatorFactory;
use Aura\Router\RouterContainer;
use Aura\Session\SessionFactory;
use Aviat\AnimeClient\API\{Anilist, Kitsu};
use Aviat\AnimeClient\Component;
use Aviat\AnimeClient\Model;
use Aviat\AnimeClient\{Component, Model};
use Aviat\Banker\Teller;
use Aviat\Ion\Config;
use Aviat\Ion\Di\Container;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\{Container, ContainerInterface};
use Laminas\Diactoros\ServerRequestFactory;
use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\RotatingFileHandler;
@ -38,7 +34,7 @@ if ( ! defined('HB_APP_DIR'))
{
define('HB_APP_DIR', __DIR__);
define('ROOT_DIR', dirname(HB_APP_DIR));
define('TEMPLATE_DIR', _dir(HB_APP_DIR, 'templates'));
define('TEMPLATE_DIR', _dir(HB_APP_DIR, 'templates'));
}
// -----------------------------------------------------------------------------
@ -74,18 +70,19 @@ return static function (array $configArray = []): Container {
$container->set('config', static fn () => new Config($configArray));
// Create Cache Object
$container->set('cache', static function(ContainerInterface $container): CacheInterface {
$container->set('cache', static function (ContainerInterface $container): CacheInterface {
$logger = $container->getLogger();
$config = $container->get('config')->get('cache');
return new Teller($config, $logger);
});
// Create Aura Router Object
$container->set('aura-router', static fn() => new RouterContainer);
$container->set('aura-router', static fn () => new RouterContainer());
// Create Html helpers
$container->set('html-helper', static function(ContainerInterface $container) {
$htmlHelper = (new HelperLocatorFactory)->newInstance();
$container->set('html-helper', static function (ContainerInterface $container) {
$htmlHelper = (new HelperLocatorFactory())->newInstance();
$helpers = [
'menu' => Helper\Menu::class,
'field' => Helper\Form::class,
@ -94,9 +91,10 @@ return static function (array $configArray = []): Container {
foreach ($helpers as $name => $class)
{
$htmlHelper->set($name, static function() use ($class, $container) {
$helper = new $class;
$htmlHelper->set($name, static function () use ($class, $container) {
$helper = new $class();
$helper->setContainer($container);
return $helper;
});
}
@ -106,7 +104,7 @@ return static function (array $configArray = []): Container {
// Create Component helpers
$container->set('component-helper', static function (ContainerInterface $container) {
$helper = (new HelperLocatorFactory)->newInstance();
$helper = (new HelperLocatorFactory())->newInstance();
$components = [
'animeCover' => Component\AnimeCover::class,
'mangaCover' => Component\MangaCover::class,
@ -119,8 +117,9 @@ return static function (array $configArray = []): Container {
foreach ($components as $name => $componentClass)
{
$helper->set($name, static function () use ($container, $componentClass) {
$helper = new $componentClass;
$helper = new $componentClass();
$helper->setContainer($container);
return $helper;
});
}
@ -144,7 +143,7 @@ return static function (array $configArray = []): Container {
$container->set('util', static fn ($container) => new Util($container));
// Models
$container->set('kitsu-model', static function(ContainerInterface $container): Kitsu\Model {
$container->set('kitsu-model', static function (ContainerInterface $container): Kitsu\Model {
$requestBuilder = new Kitsu\RequestBuilder($container);
$requestBuilder->setLogger($container->getLogger('kitsu-request'));
@ -158,9 +157,10 @@ return static function (array $configArray = []): Container {
$cache = $container->get('cache');
$model->setCache($cache);
return $model;
});
$container->set('anilist-model', static function(ContainerInterface $container): Anilist\Model {
$container->set('anilist-model', static function (ContainerInterface $container): Anilist\Model {
$requestBuilder = new Anilist\RequestBuilder($container);
$requestBuilder->setLogger($container->getLogger('anilist-request'));
@ -178,9 +178,10 @@ return static function (array $configArray = []): Container {
$container->set('manga-model', static fn ($container) => new Model\Manga($container));
$container->set('anime-collection-model', static fn ($container) => new Model\AnimeCollection($container));
$container->set('manga-collection-model', static fn ($container) => new Model\MangaCollection($container));
$container->set('settings-model', static function($container) {
$container->set('settings-model', static function ($container) {
$model = new Model\Settings($container->get('config'));
$model->setContainer($container);
return $model;
});
@ -196,4 +197,4 @@ return static function (array $configArray = []): Container {
return $container;
};
// End of bootstrap.php
// End of bootstrap.php

View File

@ -1,6 +1,7 @@
<article
class="media"
data-kitsu-id="<?= $item['id'] ?>"
data-anilist-id="<?= $item['anilist_id'] ?>"
data-mal-id="<?= $item['mal_id'] ?>"
>
<?php if ($auth->isAuthenticated()): ?>

View File

@ -1,4 +1,16 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 8
*
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient;

View File

@ -1,99 +0,0 @@
<?php
declare(strict_types=1);
$file_patterns = [
'app/appConf/*.php',
'app/bootstrap.php',
'migrations/*.php',
'src/**/*.php',
'src/*.php',
'tests/**/*.php',
'tests/*.php',
'index.php',
'Robofile.php'
];
if ( ! function_exists('glob_recursive'))
{
// Does not support flag GLOB_BRACE
function glob_recursive(string $pattern, int $flags = 0): array
{
$files = glob($pattern, $flags);
foreach (glob(dirname($pattern) . '/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir)
{
$files = array_merge($files, glob_recursive($dir . '/' . basename($pattern), $flags));
}
return $files;
}
}
function get_text_to_replace(array $tokens): string
{
$output = '';
// Tokens have the follow structure if arrays:
// [0] => token type constant
// [1] => raw syntax parsed to that token
// [2] => line number
foreach($tokens as $token)
{
// Since we only care about opening docblocks,
// bail out when we get to the namespace token
if (is_array($token) && $token[0] === T_NAMESPACE)
{
break;
}
if (is_array($token))
{
$token = $token[1];
}
$output .= $token;
}
return $output;
}
function get_tokens(string $source): array
{
return token_get_all($source);
}
function replace_files(array $files, string $template): void
{
print_r($files);
foreach ($files as $file)
{
$source = file_get_contents($file);
if ($source === FALSE)
{
continue;
}
if (stripos($source, 'namespace') === FALSE)
{
continue;
}
$tokens = get_tokens($source);
$text_to_replace = get_text_to_replace($tokens);
$header = file_get_contents(__DIR__ . $template);
$new_text = "<?php declare(strict_types=1);\n{$header}";
$new_source = str_replace($text_to_replace, $new_text, $source);
file_put_contents($file, $new_source);
}
}
foreach ($file_patterns as $glob)
{
$files = glob_recursive($glob);
replace_files($files, '/header_comment.txt');
}
echo "Successfully updated headers \n";

View File

@ -27,10 +27,7 @@
}
},
"config": {
"lock": false,
"platform": {
"php": "8"
}
"lock": false
},
"require": {
"amphp/amp": "^2.5.0",
@ -38,22 +35,20 @@
"aura/html": "^2.5.0",
"aura/router": "^3.1.0",
"aura/session": "^2.1.0",
"aviat/banker": "^3.0.0 || ^4.0.0",
"aviat/query": "^3.0.0",
"danielstjules/stringy": "^3.1.0",
"aviat/banker": "^4.1.2",
"aviat/query": "^4.0.0",
"ext-dom": "*",
"ext-gd": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-gd": "*",
"ext-mbstring": "*",
"ext-pdo": "*",
"laminas/laminas-diactoros": "^2.5.0",
"laminas/laminas-httphandlerrunner": "^2.1.0",
"maximebf/consolekit": "^1.0.3",
"monolog/monolog": "^2.0.2",
"php": ">= 8.0.0",
"monolog/monolog": "^3.0.0",
"php": ">= 8.1.0",
"psr/http-message": "^1.0.1",
"psr/log": "*",
"robmorgan/phinx": "^0.12.4",
"symfony/polyfill-mbstring": "^1.0.0",
"symfony/polyfill-util": "^1.0.0",
"tracy/tracy": "^2.8.0",
@ -68,7 +63,7 @@
"scripts": {
"build:css": "cd public && npm run build:css && cd ..",
"build:js": "cd public && npm run build:js && cd ..",
"coverage": "phpdbg -qrr -- vendor/bin/phpunit -c build",
"coverage": "php -dpcov.enabled=1 -dpcov.directory=. -dpcov.exclude=\"~vendor~\" ./vendor/bin/phpunit -c build",
"phpstan": "phpstan analyse -c phpstan.neon",
"watch:css": "cd public && npm run watch:css",
"watch:js": "cd public && npm run watch:js",

View File

@ -63,6 +63,7 @@ _.on('body.anime.list', 'click', '.plus-one', (e) => {
// Setup the update data
let data = {
id: parentSel.dataset.kitsuId,
anilist_id: parentSel.dataset.anilistId,
mal_id: parentSel.dataset.malId,
data: {
progress: watchedCount + 1
@ -94,11 +95,18 @@ _.on('body.anime.list', 'click', '.plus-one', (e) => {
_.hide('#loading-shadow');
_.showMessage('error', `Failed to update ${title}. `);
_.scrollToTop();
return;
}
// We've completed the series
if (resData.data.libraryEntry.update.libraryEntry.status === 'COMPLETED') {
_.hide(parentSel);
_.hide('#loading-shadow');
_.showMessage('success', `Successfully completed ${title}`);
_.scrollToTop();
return;
}
_.hide('#loading-shadow');

View File

@ -83,6 +83,11 @@ _.on('.manga.list', 'click', '.edit-buttons button', (e) => {
if (String(data.data.status).toUpperCase() === 'COMPLETED') {
_.hide(parentSel);
_.hide('#loading-shadow');
_.showMessage('success', `Successfully completed ${mangaName}`);
_.scrollToTop();
return;
}
_.hide('#loading-shadow');

View File

@ -5,6 +5,7 @@ import _ from './anime-client.js';
_.on('main', 'change', '.big-check', (e) => {
const id = e.target.id;
document.getElementById(`mal_${id}`).checked = true;
document.getElementById(`anilist_${id}`).checked = true;
});
/**
@ -55,6 +56,7 @@ export function renderSearchResults (type, data, isCollection = false) {
return `
<article class="media search ${disabled}">
<div class="name">
<input type="radio" class="mal-check" id="anilist_${item.slug}" name="anilist_id" value="${item.anilist_id}" ${disabled} />
<input type="radio" class="mal-check" id="mal_${item.slug}" name="mal_id" value="${item.mal_id}" ${disabled} />
<input type="radio" class="big-check" id="${item.slug}" name="id" value="${item.id}" ${disabled} />
<label for="${item.slug}">

View File

@ -11,11 +11,11 @@
"devDependencies": {
"@swc/cli": "^0.1.39",
"@swc/core": "^1.2.54",
"concurrently": "^6.0.2",
"concurrently": "^7.4.0",
"cssnano": "^5.0.1",
"postcss": "^8.2.6",
"postcss-import": "^14.0.0",
"postcss-preset-env": "^6.7.0",
"postcss-import": "^15.0.0",
"postcss-preset-env": "^7.8.2",
"watch": "^1.0.2"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient;
@ -25,8 +23,8 @@ setlocale(LC_CTYPE, 'en_US');
// Load composer autoloader
require_once __DIR__ . '/vendor/autoload.php';
Debugger::$strictMode = E_ALL & ~E_DEPRECATED; // all errors except deprecated notices
Debugger::$showBar = false;
Debugger::$strictMode = E_ALL;
Debugger::$showBar = FALSE;
Debugger::enable(Debugger::DEVELOPMENT, __DIR__ . '/app/logs');
// Define base directories
@ -37,7 +35,7 @@ $CONF_DIR = _dir($APP_DIR, 'config');
// -----------------------------------------------------------------------------
// Dependency Injection setup
// -----------------------------------------------------------------------------
$baseConfig = require "{$APPCONF_DIR}/base_config.php";
$baseConfig = require _dir($APPCONF_DIR, 'base_config.php');
$di = require "{$APP_DIR}/bootstrap.php";
$config = loadConfig($CONF_DIR);
@ -59,7 +57,7 @@ if (is_array($checkedConfig) && array_key_exists('timezone', $checkedConfig) &&
{
date_default_timezone_set($checkedConfig['timezone']);
}
else if (is_string($timezone) && $timezone !== '')
elseif (is_string($timezone) && $timezone !== '')
{
date_default_timezone_set($timezone);
}
@ -76,4 +74,4 @@ unset($APP_DIR, $CONF_DIR, $APPCONF_DIR);
// -----------------------------------------------------------------------------
// Dispatch to the current route
// -----------------------------------------------------------------------------
$container->get('dispatcher')();
$container->get('dispatcher')();

35
justfile Normal file
View File

@ -0,0 +1,35 @@
# Lists the available actions
default:
@just --list
# Runs rector, showing what changes will be make
rector-dry-run:
tools/vendor/bin/rector process --config=tools/rector.php --dry-run src
# Runs rector, and updates the files
rector:
tools/vendor/bin/rector process --config=tools/rector.php src
# Check code formatting
check-fmt:
tools/vendor/bin/php-cs-fixer fix --dry-run --verbose
# Fix code formatting
fmt:
tools/vendor/bin/php-cs-fixer fix --verbose
# Run tests
test:
composer run-script test
# Run tests, update snapshots
test-update:
composer run-script test-update
# Update the per-file header comments
update-headers:
php tools/update_header_comments.php
# Run unit tests and generate test-coverage report
coverage:
composer run-script coverage

View File

@ -2,7 +2,8 @@
use Phinx\Migration\AbstractMigration;
class FirstMigration extends AbstractMigration {
class FirstMigration extends AbstractMigration
{
/**
* Migrate up
*/
@ -16,7 +17,7 @@ class FirstMigration extends AbstractMigration {
// Add items to media table
if ($this->hasTable('media'))
{
foreach(['DVD & Blu-ray', 'Blu-ray', 'DVD', 'Bootleg DVD'] as $type)
foreach (['DVD & Blu-ray', 'Blu-ray', 'DVD', 'Bootleg DVD'] as $type)
{
$this->execute('INSERT INTO "media" ("type") VALUES (\'' . $type . '\')');
}
@ -25,11 +26,11 @@ class FirstMigration extends AbstractMigration {
// Create anime_set table
$anime_set = $this->table('anime_set', ['id' => FALSE, 'primary_key' => ['hummingbird_id']]);
$anime_set->addColumn('hummingbird_id', 'biginteger')
->addColumn('slug', 'string', ['comment' => "URL slug used for image caching and generating links"])
->addColumn('slug', 'string', ['comment' => 'URL slug used for image caching and generating links'])
->addColumn('title', 'string')
->addColumn('alternate_title', 'string', ['null' => TRUE])
->addColumn('media_id', 'integer', ['default' => 3, 'null' => TRUE])
->addColumn('show_type', 'string', ['default' => 'TV', 'null' => TRUE, 'comment' => "TV Series/OVA/etc"])
->addColumn('show_type', 'string', ['default' => 'TV', 'null' => TRUE, 'comment' => 'TV Series/OVA/etc'])
->addColumn('age_rating', 'string', ['default' => 'PG13', 'null' => TRUE])
->addColumn('cover_image', 'string', ['null' => TRUE])
->addColumn('episode_count', 'integer', ['null' => TRUE])

View File

@ -1,35 +1,35 @@
<?php
<?php declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
class CacheMigration extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* renameColumn
* addIndex
* addForeignKey
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* renameColumn
* addIndex
* addForeignKey
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$cacheTable = $this->table('cache', ['id' => FALSE, 'primary_key' => ['key']]);
$cacheTable->addColumn('key', 'text')
->addColumn('value', 'text')
->create();
}
}
}

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
@ -11,7 +11,7 @@ class ReorganizeAnimeCollectionMedia extends AbstractMigration
{
$newLinkTable = $this->table('anime_set_media_link', [
'id' => FALSE,
'primary_key' => ['hummingbird_id', 'media_id']
'primary_key' => ['hummingbird_id', 'media_id'],
]);
$newLinkTable->addColumn('hummingbird_id', 'biginteger')
@ -31,6 +31,7 @@ class ReorganizeAnimeCollectionMedia extends AbstractMigration
foreach ($rows as $row)
{
$keys = array_keys($row);
foreach ($keys as $k)
{
if (is_numeric($k))
@ -49,6 +50,7 @@ class ReorganizeAnimeCollectionMedia extends AbstractMigration
// and replace those rows with the individual entries
$linkRows = $this->fetchAll('SELECT hummingbird_id FROM anime_set_media_link WHERE media_id=1');
$insertRows = [];
foreach ($linkRows as $row)
{
$insertRows[] = [

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
@ -18,6 +18,7 @@ class AnimeCollectionRefactorCleanup extends AbstractMigration
{
// Add some new media types
$moreMediaTypes = [];
foreach ($this->newMediaTypes as $id => $medium)
{
$moreMediaTypes[] = [
@ -47,7 +48,7 @@ class AnimeCollectionRefactorCleanup extends AbstractMigration
$this->execute("UPDATE media SET type='Bootleg DVD' WHERE id=4");
// Remove the new media types
$values = array_map(fn ($medium) => "'{$medium}'", $this->newMediaTypes);
$values = array_map(static fn ($medium) => "'{$medium}'", $this->newMediaTypes);
$valueList = implode(',', $values);
$this->execute("DELETE FROM media WHERE type IN ({$valueList})");
}

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
use Phinx\Migration\AbstractMigration;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
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);console.log("Comparing "+textA+" and "+textB);if(th.classList.contains("numeric")){var arrayA=textA.replace('episodes: ','').replace('-',0).split("/");var arrayB=textB.replace('episodes: ','').replace('-',0).split("/");if(arrayA.length>1){textA=parseInt(arrayA[0],10)/parseInt(arrayA[1],10);textB=parseInt(arrayB[0],10)/parseInt(arrayB[1],10)}else{textA=parseInt(arrayA[0],10);textB=parseInt(arrayB[0],10)}}else if(parseInt(textA,10)){textA=parseInt(textA,10);textB=parseInt(textB,10)}if(textA>textB)return 1;if(textA<textB)return -1;return 0};var toggle=function(){var c=order!=='sorting-asc'?'sorting-asc':'sorting-desc';th.className=(th.className.replace(order,'')+' '+c).trim();return order=c};var reset=function(){th.classList.remove('sorting-asc','sorting-desc');th.classList.add('sorting');return order=''};var onClickEvent=function(e){if(th&&cellIndex!==e.target.cellIndex)reset();th=e.target;if(th.nodeName.toLowerCase()==='th'){cellIndex=th.cellIndex;var tbody=th.offsetParent.getElementsByTagName('tbody')[0];var rows=Array.from(tbody.rows);if(rows){rows.sort(sort);if(order==='sorting-asc')rows.reverse();toggle();tbody.innerHtml='';rows.forEach(function(row){tbody.appendChild(row)})}}};return{init:function(){var ths=document.getElementsByTagName('th');var results=[];for(var i=0,len=ths.length;i<len;i++){var th=ths[i];th.classList.add('sorting');th.classList.add('testing');results.push(th.onclick=onClickEvent)}return results}}}();LightTableSorter.init()
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);console.log("Comparing "+textA+" and "+textB);if(th.classList.contains("numeric")){var arrayA=textA.replace("episodes: ","").replace("-",0).split("/");var arrayB=textB.replace("episodes: ","").replace("-",0).split("/");if(arrayA.length>1){textA=parseInt(arrayA[0],10)/parseInt(arrayA[1],10);textB=parseInt(arrayB[0],10)/parseInt(arrayB[1],10)}else{textA=parseInt(arrayA[0],10);textB=parseInt(arrayB[0],10)}}else if(parseInt(textA,10)){textA=parseInt(textA,10);textB=parseInt(textB,10)}if(textA>textB)return 1;if(textA<textB)return -1;return 0};var toggle=function(){var c=order!=="sorting-asc"?"sorting-asc":"sorting-desc";th.className=(th.className.replace(order,"")+" "+c).trim();return order=c};var reset=function(){th.classList.remove("sorting-asc","sorting-desc");th.classList.add("sorting");return order=""};var onClickEvent=function(e){if(th&&cellIndex!==e.target.cellIndex)reset();th=e.target;if(th.nodeName.toLowerCase()==="th"){cellIndex=th.cellIndex;var tbody=th.offsetParent.getElementsByTagName("tbody")[0];var rows=Array.from(tbody.rows);if(rows){rows.sort(sort);if(order==="sorting-asc")rows.reverse();toggle();tbody.innerHtml="";rows.forEach(function(row){tbody.appendChild(row)})}}};return{init:function(){var ths=document.getElementsByTagName("th");var results=[];for(var i=0,len=ths.length;i<len;i++){var th=ths[i];th.classList.add("sorting");th.classList.add("testing");results.push(th.onclick=onClickEvent)}return results}}}();LightTableSorter.init();

File diff suppressed because one or more lines are too long

View File

@ -6,31 +6,33 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API;
use const Aviat\AnimeClient\USER_AGENT;
use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use Amp\Http\Client\Body\FormBody;
use Amp\Http\Client\Request;
use Amp\Http\Client\Body\FormBody;
use Aviat\Ion\Json;
use Error;
use InvalidArgumentException;
use Psr\Log\LoggerAwareTrait;
use Throwable;
use TypeError;
use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use const Aviat\AnimeClient\USER_AGENT;
/**
* Wrapper around Http\Client to make it easier to build API requests
*/
abstract class APIRequestBuilder {
abstract class APIRequestBuilder
{
use LoggerAwareTrait;
/**
@ -70,9 +72,6 @@ abstract class APIRequestBuilder {
/**
* Do a basic minimal GET request
*
* @param string $uri
* @return Request
*/
public static function simpleRequest(string $uri): Request
{
@ -89,7 +88,6 @@ abstract class APIRequestBuilder {
*
* @param string $type The type of authorization, eg, basic, bearer, etc.
* @param string $value The authorization value
* @return self
*/
public function setAuth(string $type, string $value): self
{
@ -101,26 +99,21 @@ abstract class APIRequestBuilder {
/**
* Set a basic authentication header
*
* @param string $username
* @param string $password
* @return self
*/
public function setBasicAuth(string $username, string $password): self
{
$this->setAuth('basic', base64_encode($username . ':' . $password));
return $this;
}
/**
* Set the request body
*
* @param FormBody|string $body
* @return self
*/
public function setBody(FormBody|string $body): self
{
$this->request->setBody($body);
return $this;
}
@ -128,7 +121,6 @@ abstract class APIRequestBuilder {
* Set body as form fields
*
* @param array $fields Mapping of field names to values
* @return self
*/
public function setFormFields(array $fields): self
{
@ -140,24 +132,18 @@ abstract class APIRequestBuilder {
/**
* Unset a request header
*
* @param string $name
* @return self
*/
public function unsetHeader(string $name): self
{
$this->request->removeHeader($name);
return $this;
}
/**
* Set a request header
*
* @param string $name
* @param string|null $value
* @return self
*/
public function setHeader(string $name, string $value = NULL): self
public function setHeader(string $name, ?string $value = NULL): self
{
if (NULL === $value)
{
@ -175,9 +161,6 @@ abstract class APIRequestBuilder {
* Set multiple request headers
*
* name => value
*
* @param array $headers
* @return self
*/
public function setHeaders(array $headers): self
{
@ -191,36 +174,30 @@ abstract class APIRequestBuilder {
/**
* Set the request body
*
* @param mixed $body
* @return self
*/
public function setJsonBody(mixed $body): self
{
$requestBody = ( ! is_string($body))
? Json::encode($body)
: $body;
$requestBody = (is_string($body))
? $body
: Json::encode($body);
return $this->setBody($requestBody);
}
/**
* Append a query string in array format
*
* @param array $params
* @return self
*/
public function setQuery(array $params): self
{
$this->query = http_build_query($params);
return $this;
}
/**
* Return the promise for the current request
*
* @return Request
* @throws \Throwable
* @throws Throwable
*/
public function getFullRequest(): Request
{
@ -235,7 +212,7 @@ abstract class APIRequestBuilder {
$this->request->getBody()
->createBodyStream()
->read()
)
),
]);
}
@ -245,25 +222,22 @@ abstract class APIRequestBuilder {
/**
* Get the data from the response of the passed request
*
* @param Request $request
* @throws Error
* @throws Throwable
* @throws TypeError
* @return mixed
* @throws \Error
* @throws \Throwable
* @throws \TypeError
*/
public function getResponseData(Request $request)
{
$response = getResponse($request);
return wait($response->getBody()->buffer());
}
/**
* Create a new http request
*
* @param string $type
* @param string $uri
* @throws InvalidArgumentException
* @return self
*/
public function newRequest(string $type, string $uri): self
{
@ -292,8 +266,6 @@ abstract class APIRequestBuilder {
/**
* Create the full request url
*
* @return Request
*/
private function buildUri(): Request
{
@ -313,10 +285,6 @@ abstract class APIRequestBuilder {
/**
* Reset the class state for a new request
*
* @param string|null $url
* @param string $type
* @return void
*/
private function resetState(?string $url, string $type = 'GET'): void
{
@ -330,4 +298,4 @@ abstract class APIRequestBuilder {
$this->request->setTcpConnectTimeout(300000);
$this->request->setTransferTimeout(300000);
}
}
}

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API;
@ -22,21 +20,17 @@ use Aviat\AnimeClient\Types\FormItemData;
/**
* Common interface for anime and manga list item CRUD
*/
abstract class AbstractListItem {
abstract class AbstractListItem
{
/**
* Create a list item
*
* @param array $data -
* @return Request
*/
abstract public function create(array $data): Request;
/**
* Create a full list item for syncing
*
* @param array $data
* @return Request
*/
abstract public function createFull(array $data): Request;
@ -44,16 +38,12 @@ abstract class AbstractListItem {
* Retrieve a list item
*
* @param string $id - The id of the list item
* @return array
* @return mixed[]
*/
abstract public function get(string $id): array;
/**
* Increase progress on a list item
*
* @param string $id
* @param FormItemData $data
* @return Request
*/
abstract public function increment(string $id, FormItemData $data): Request;
@ -62,7 +52,6 @@ abstract class AbstractListItem {
*
* @param string $id - The id of the list item to update
* @param FormItemData $data - The data with which to update the list item
* @return Request
*/
abstract public function update(string $id, FormItemData $data): Request;
@ -70,7 +59,6 @@ abstract class AbstractListItem {
* Delete a list item
*
* @param string $id - The id of the list item to delete
* @return Request|null
*/
abstract public function delete(string $id):?Request;
}
abstract public function delete(string $id): ?Request;
}

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Anilist;
@ -26,39 +24,32 @@ use Aviat\AnimeClient\Types\FormItemData;
/**
* CRUD operations for MAL list items
*/
final class ListItem extends AbstractListItem {
final class ListItem extends AbstractListItem
{
use RequestBuilderTrait;
/**
* Create a minimal list item
*
* @param array $data
* @return Request
*/
public function create(array $data): Request
{
$checkedData = Types\MediaListEntry::check($data);
return $this->requestBuilder->mutateRequest('CreateMediaListEntry', $checkedData ?? []);
}
/**
* Create a fleshed-out list item
*
* @param array $data
* @return Request
*/
public function createFull(array $data): Request
{
$checkedData = Types\MediaListEntry::check($data);
return $this->requestBuilder->mutateRequest('CreateFullMediaListEntry', $checkedData ?? []);
}
/**
* Delete a list item
*
* @param string $id
* @param string $type
* @return Request
*/
public function delete(string $id, string $type = 'anime'): Request
{
@ -67,9 +58,6 @@ final class ListItem extends AbstractListItem {
/**
* Get the data for a list item
*
* @param string $id
* @return array
*/
public function get(string $id): array
{
@ -78,10 +66,6 @@ final class ListItem extends AbstractListItem {
/**
* Increase the progress on the medium by 1
*
* @param string $id
* @param FormItemData $data
* @return Request
*/
public function increment(string $id, FormItemData $data): Request
{
@ -95,31 +79,27 @@ final class ListItem extends AbstractListItem {
/**
* Update a list item
*
* @param string $id
* @param FormItemData $data
* @return Request
*/
public function update(string $id, FormItemData $data): Request
{
$notes = $data->notes ?? '';
$progress = (int)$data->progress;
$private = (bool)$data->private;
$progress = (int) $data->progress;
$private = (bool) $data->private;
$rating = $data->ratingTwenty;
$status = ($data->reconsuming === TRUE)
? AnilistStatus::REPEATING
: AnimeWatchingStatus::KITSU_TO_ANILIST[$data->status];
$updateData = Types\MediaListEntry::check([
'id' => (int)$id,
'id' => (int) $id,
'status' => $status,
'score' => $rating * 5,
'progress' => $progress,
'repeat' => (int)$data['reconsumeCount'],
'repeat' => (int) $data['reconsumeCount'],
'private' => $private,
'notes' => $notes,
]);
return $this->requestBuilder->mutateRequest('UpdateMediaListEntry', $updateData ?? []);
}
}
}

View File

@ -6,16 +6,16 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Anilist;
use InvalidArgumentException;
class MissingIdException extends InvalidArgumentException {}
class MissingIdException extends InvalidArgumentException
{
}

View File

@ -6,29 +6,25 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Anilist;
use function Amp\Promise\wait;
use InvalidArgumentException;
use Amp\Http\Client\Request;
use Aviat\AnimeClient\Anilist;
use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus};
use Aviat\AnimeClient\Types\FormItem;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
use Aviat\Ion\Json;
use Aviat\Ion\Di\Exception\ContainerException;
use Aviat\Ion\Di\Exception\NotFoundException;
use InvalidArgumentException;
use Throwable;
use function Amp\Promise\wait;
/**
* Anilist API Model
@ -36,32 +32,24 @@ use Throwable;
final class Model
{
use RequestBuilderTrait;
/**
* @var ListItem
*/
private ListItem $listItem;
/**
* Constructor
*
* @param ListItem $listItem
*/
public function __construct(ListItem $listItem)
public function __construct(private 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
* @throws Throwable
* @return mixed[]
*/
public function authenticate(string $code, string $redirectUri): array
{
@ -84,8 +72,6 @@ final class Model
/**
* Check auth status with simple API call
*
* @return array
*/
public function checkAuth(): array
{
@ -95,8 +81,6 @@ final class Model
/**
* Get user list data for syncing with Kitsu
*
* @param string $type
* @return array
* @throws ContainerException
* @throws NotFoundException
*/
@ -118,20 +102,10 @@ final class Model
/**
* Create a list item
*
* @param array $data
* @param string $type
* @return Request
*/
public function createListItem(array $data, string $type = 'anime'): ?Request
{
if ($data['mal_id'] === NULL)
{
return NULL;
}
$mediaId = $this->getMediaIdFromMalId($data['mal_id'], mb_strtoupper($type));
$mediaId = $this->getMediaId($data, $type);
if ($mediaId === NULL)
{
return NULL;
@ -159,15 +133,11 @@ final class Model
/**
* Create a list item with all the relevant data
*
* @param array $data
* @param string $type
* @return Request
*/
public function createFullListItem(array $data, string $type): Request
{
$createData = $data['data'];
$mediaId = $this->getMediaIdFromMalId($data['mal_id'], strtoupper($type));
$mediaId = $this->getMediaId($data, $type);
if (empty($mediaId))
{
@ -179,39 +149,14 @@ final class Model
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
* @param string $type - Them media type (anime/manga)
*
* @return array
*/
public function getListItem(string $malId, string $type): array
{
$id = $this->getListIdFromMalId($malId, $type);
if ($id === NULL)
{
return [];
}
$data = $this->listItem->get($id)['data'];
return ($data !== null)
? $data['MediaList']
: [];
}
/**
* Increase the watch count for the current list item
*
* @param FormItem $data
* @param string $type - Them media type (anime/manga)
* @return Request|null
*/
public function incrementListItem(FormItem $data, string $type): ?Request
{
$id = $this->getListIdFromMalId($data['mal_id'], $type);
$id = $this->getListIdFromData($data, $type);
if ($id === NULL)
{
return NULL;
@ -223,14 +168,11 @@ final class Model
/**
* Modify a list item
*
* @param FormItem $data
* @param string $type - Them media type (anime/manga)
* @return Request|null
*/
public function updateListItem(FormItem $data, string $type): ?Request
{
$id = $this->getListIdFromMalId($data['mal_id'], mb_strtoupper($type));
$id = $this->getListIdFromData($data, $type);
if ($id === NULL)
{
return NULL;
@ -242,31 +184,32 @@ final class Model
/**
* Remove a list item
*
* @param string $malId - The id of the list item to remove
* @param string $type - Them media type (anime/manga)
* @return Request|null
* @param FormItem $data - The entry to remove
* @param string $type - The media type (anime/manga)
*/
public function deleteListItem(string $malId, string $type): ?Request
public function deleteItem(FormItem $data, string $type): ?Request
{
$id = $this->getListIdFromMalId($malId, $type);
if ($id === NULL)
$mediaId = $this->getMediaId((array)$data, $type);
if ($mediaId === NULL)
{
return NULL;
}
return $this->listItem->delete($id);
$id = $this->getListIdFromMediaId($mediaId);
if (is_string($id))
{
return $this->listItem->delete($id);
}
return NULL;
}
/**
* Get the id of the specific list entry from the malId
*
* @param string $malId
* @param string $type - The media type (anime/manga)
* @return string|null
* Get the id of the specific list entry from the data
*/
public function getListIdFromMalId(string $malId, string $type): ?string
public function getListIdFromData(FormItem $data, string $type = 'ANIME'): ?string
{
$mediaId = $this->getMediaIdFromMalId($malId, $type);
$mediaId = $this->getMediaId((array)$data, $type);
if ($mediaId === NULL)
{
return NULL;
@ -279,9 +222,6 @@ final class Model
* Get the Anilist list item id from the media id from its MAL id
* this way is more accurate than getting the list item id
* directly from the MAL id
*
* @param string $mediaId
* @return string|null
*/
private function getListIdFromMediaId(string $mediaId): ?string
{
@ -298,15 +238,26 @@ final class Model
return NULL;
}
return (string)$info['data']['MediaList']['id'];
return (string) $info['data']['MediaList']['id'];
}
/**
* Find the id to update by
*/
private function getMediaId (array $data, string $type = 'ANIME'): ?string
{
if (isset($data['anilist_id']))
{
return $data['anilist_id'];
}
return (isset($data['mal_id']))
? $this->getMediaIdFromMalId($data['mal_id'], mb_strtoupper($type))
: NULL;
}
/**
* Get the Anilist media id from the malId
*
* @param string $malId
* @param string $type
* @return string|null
*/
private function getMediaIdFromMalId(string $malId, string $type = 'ANIME'): ?string
{
@ -325,6 +276,6 @@ final class Model
return NULL;
}
return (string)$info['data']['Media']['id'];
return (string) $info['data']['Media']['id'];
}
}
}

View File

@ -6,52 +6,44 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Anilist;
use Amp\Http\Client\Request;
use Amp\Http\Client\Response;
use Amp\Http\Client\{Request, Response};
use Aviat\AnimeClient\Anilist;
use Aviat\Ion\Di\ContainerAware;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Json;
use Aviat\Ion\JsonException;
use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use const Aviat\AnimeClient\USER_AGENT;
use Aviat\AnimeClient\API\APIRequestBuilder;
use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
use Aviat\Ion\{Json, JsonException};
use LogicException;
use Throwable;
final class RequestBuilder extends APIRequestBuilder {
use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use function in_array;
use const Aviat\AnimeClient\USER_AGENT;
final class RequestBuilder extends APIRequestBuilder
{
use ContainerAware;
/**
* The base url for api requests
* @var string $base_url
*/
protected string $baseUrl = Anilist::BASE_URL;
/**
* Valid HTTP request methods
* @var array
*/
protected array $validMethods = ['POST'];
/**
* HTTP headers to send with every request
*
* @var array
*/
protected array $defaultHeaders = [
'Accept' => 'application/json',
@ -67,9 +59,6 @@ final class RequestBuilder extends APIRequestBuilder {
/**
* Create a request object
* @param string $url
* @param array $options
* @return Request
* @throws Throwable
*/
public function setUpRequest(string $url, array $options = []): Request
@ -111,10 +100,6 @@ final class RequestBuilder extends APIRequestBuilder {
/**
* Run a GraphQL API query
*
* @param string $name
* @param array $variables
* @return array
*/
public function runQuery(string $name, array $variables = []): array
{
@ -126,30 +111,28 @@ final class RequestBuilder extends APIRequestBuilder {
$query = file_get_contents($file);
$body = [
'query' => $query
'query' => $query,
];
if ( ! empty($variables))
{
$body['variables'] = [];
foreach($variables as $key => $val)
foreach ($variables as $key => $val)
{
$body['variables'][$key] = $val;
}
}
return $this->postRequest([
'body' => $body
'body' => $body,
]);
}
/**
* @param string $name
* @param array $variables
* @return Request
* @throws Throwable
*/
public function mutateRequest (string $name, array $variables = []): Request
public function mutateRequest(string $name, array $variables = []): Request
{
$file = __DIR__ . "/Mutations/{$name}.graphql";
if ( ! file_exists($file))
@ -160,11 +143,13 @@ final class RequestBuilder extends APIRequestBuilder {
$query = file_get_contents($file);
$body = [
'query' => $query
'query' => $query,
];
if (!empty($variables)) {
if ( ! empty($variables))
{
$body['variables'] = [];
foreach ($variables as $key => $val)
{
$body['variables'][$key] = $val;
@ -177,12 +162,10 @@ final class RequestBuilder extends APIRequestBuilder {
}
/**
* @param string $name
* @param array $variables
* @return array
* @throws Throwable
* @return mixed[]
*/
public function mutate (string $name, array $variables = []): array
public function mutate(string $name, array $variables = []): array
{
$request = $this->mutateRequest($name, $variables);
$response = $this->getResponseFromRequest($request);
@ -193,9 +176,6 @@ final class RequestBuilder extends APIRequestBuilder {
/**
* Make a request
*
* @param string $url
* @param array $options
* @return Response
* @throws Throwable
*/
private function getResponse(string $url, array $options = []): Response
@ -220,8 +200,6 @@ final class RequestBuilder extends APIRequestBuilder {
}
/**
* @param Request $request
* @return Response
* @throws Throwable
*/
public function getResponseFromRequest(Request $request): Response
@ -247,8 +225,6 @@ final class RequestBuilder extends APIRequestBuilder {
/**
* Remove some boilerplate for post requests
*
* @param array $options
* @return array
* @throws Throwable
*/
protected function postRequest(array $options = []): array
@ -265,13 +241,13 @@ final class RequestBuilder extends APIRequestBuilder {
//'requestHeaders' => $request->getHeaders(),
]);
if ( ! \in_array($response->getStatus(), $validResponseCodes, TRUE))
if ( ! in_array($response->getStatus(), $validResponseCodes, TRUE))
{
$logger?->warning('Non 200 response for POST api call', (array)$response->getBody());
$logger?->warning('Non 200 response for POST api call', (array) $response->getBody());
}
$rawBody = wait($response->getBody()->buffer());
try
{
return Json::decode($rawBody);
@ -280,7 +256,8 @@ final class RequestBuilder extends APIRequestBuilder {
{
dump($e);
dump($rawBody);
die();
exit();
}
}
}
}

View File

@ -6,19 +6,18 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Anilist;
use Aviat\Ion\Di\ContainerAware;
trait RequestBuilderTrait {
trait RequestBuilderTrait
{
use ContainerAware;
/**
@ -28,13 +27,11 @@ trait RequestBuilderTrait {
/**
* Set the request builder object
*
* @param RequestBuilder $requestBuilder
* @return self
*/
public function setRequestBuilder(RequestBuilder $requestBuilder): self
{
$this->requestBuilder = $requestBuilder;
return $this;
}
}
}

View File

@ -6,19 +6,15 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Anilist\Transformer;
use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\Anilist as AnilistStatus;
use Aviat\AnimeClient\API\Enum\AnimeWatchingStatus\Kitsu as KitsuStatus;
use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
use Aviat\AnimeClient\API\{Enum, Mapping};
use Aviat\AnimeClient\Types\{AnimeListItem, FormItem};
use Aviat\Ion\Transformer\AbstractTransformer;
@ -26,8 +22,8 @@ use Aviat\Ion\Transformer\AbstractTransformer;
use DateTime;
use DateTimeInterface;
class AnimeListTransformer extends AbstractTransformer {
class AnimeListTransformer extends AbstractTransformer
{
public function transform(array|object $item): AnimeListItem
{
return AnimeListItem::from([]);
@ -35,13 +31,10 @@ class AnimeListTransformer extends AbstractTransformer {
/**
* Transform Anilist list item to Kitsu form update format
*
* @param array $item
* @return FormItem
*/
public function untransform(array $item): FormItem
{
$reconsuming = $item['status'] === AnilistStatus::REPEATING;
$reconsuming = $item['status'] === Enum\AnimeWatchingStatus\Anilist::REPEATING;
return FormItem::from([
'id' => $item['id'],
@ -54,12 +47,12 @@ class AnimeListTransformer extends AbstractTransformer {
'reconsumeCount' => $item['repeat'],
'reconsuming' => $reconsuming,
'status' => $reconsuming
? KitsuStatus::WATCHING
: AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']],
? Enum\AnimeWatchingStatus\Kitsu::WATCHING
: Mapping\AnimeWatchingStatus::ANILIST_TO_KITSU[$item['status']],
'updatedAt' => (new DateTime())
->setTimestamp($item['updatedAt'])
->format(DateTimeInterface::W3C)
->format(DateTimeInterface::W3C),
],
]);
}
}
}

View File

@ -6,29 +6,23 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Anilist\Transformer;
use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Anilist as AnilistStatus;
use Aviat\AnimeClient\API\Enum\MangaReadingStatus\Kitsu as KitsuStatus;
use Aviat\AnimeClient\API\Mapping\MangaReadingStatus;
use Aviat\AnimeClient\Types\MangaListItem;
use Aviat\AnimeClient\Types\FormItem;
use Aviat\AnimeClient\API\{Enum, Mapping};
use Aviat\AnimeClient\Types\{FormItem, MangaListItem};
use Aviat\Ion\Transformer\AbstractTransformer;
use DateTime;
use DateTimeInterface;
class MangaListTransformer extends AbstractTransformer {
class MangaListTransformer extends AbstractTransformer
{
public function transform(array|object $item): MangaListItem
{
return MangaListItem::from([]);
@ -36,13 +30,10 @@ class MangaListTransformer extends AbstractTransformer {
/**
* Transform Anilist list item to Kitsu form update format
*
* @param array $item
* @return FormItem
*/
public function untransform(array $item): FormItem
{
$reconsuming = $item['status'] === AnilistStatus::REPEATING;
$reconsuming = $item['status'] === Enum\MangaReadingStatus\Anilist::REPEATING;
return FormItem::from([
'id' => $item['id'],
@ -55,12 +46,12 @@ class MangaListTransformer extends AbstractTransformer {
'reconsumeCount' => $item['repeat'],
'reconsuming' => $reconsuming,
'status' => $reconsuming
? KitsuStatus::READING
: MangaReadingStatus::ANILIST_TO_KITSU[$item['status']],
? Enum\MangaReadingStatus\Kitsu::READING
: Mapping\MangaReadingStatus::ANILIST_TO_KITSU[$item['status']],
'updatedAt' => (new DateTime())
->setTimestamp($item['updatedAt'])
->format(DateTimeInterface::W3C),
]
],
]);
}
}
}

View File

@ -6,31 +6,23 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Anilist\Types;
use Aviat\AnimeClient\Types\AbstractType;
class MediaListEntry extends AbstractType {
class MediaListEntry extends AbstractType
{
public int|string $id;
public ?string $notes;
public ?bool $private;
public int $progress;
public ?int $repeat;
public string $status;
public ?int $score;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API;
@ -21,29 +19,22 @@ use Psr\SimpleCache\CacheInterface;
/**
* Helper methods for dealing with the Cache
*/
trait CacheTrait {
/**
* @var CacheInterface
*/
trait CacheTrait
{
protected CacheInterface $cache;
/**
* Inject the cache object
*
* @param CacheInterface $cache
* @return self
*/
public function setCache(CacheInterface $cache): self
{
$this->cache = $cache;
return $this;
}
/**
* Get the cache object if it exists
*
* @return CacheInterface
*/
public function getCache(): CacheInterface
{
@ -53,11 +44,6 @@ trait CacheTrait {
/**
* Get the cached value if it exists, otherwise set the cache value
* and return it.
*
* @param string $key
* @param callable $primer
* @param array|null $primeArgs
* @return mixed
*/
public function getCached(string $key, callable $primer, ?array $primeArgs = []): mixed
{
@ -77,4 +63,4 @@ trait CacheTrait {
return $value;
}
}
}

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Enum\AnimeWatchingStatus;
@ -21,11 +19,12 @@ use Aviat\Ion\Enum;
/**
* Possible values for watching status for the current anime
*/
final class Anilist extends Enum {
final class Anilist extends Enum
{
public const WATCHING = 'CURRENT';
public const COMPLETED = 'COMPLETED';
public const ON_HOLD = 'PAUSED';
public const DROPPED = 'DROPPED';
public const PLAN_TO_WATCH = 'PLANNING';
public const REPEATING = 'REPEATING';
}
}

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Enum\AnimeWatchingStatus;
@ -21,10 +19,11 @@ use Aviat\Ion\Enum;
/**
* Possible values for watching status for the current anime
*/
final class Kitsu extends Enum {
final class Kitsu extends Enum
{
public const WATCHING = 'current';
public const PLAN_TO_WATCH = 'planned';
public const ON_HOLD = 'on_hold';
public const DROPPED = 'dropped';
public const COMPLETED = 'completed';
}
}

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Enum\AnimeWatchingStatus;
@ -21,7 +19,8 @@ use Aviat\Ion\Enum;
/**
* Possible values for current watching status of anime
*/
final class Route extends Enum {
final class Route extends Enum
{
public const ALL = 'all';
public const WATCHING = 'watching';
public const PLAN_TO_WATCH = 'plan_to_watch';

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Enum\AnimeWatchingStatus;
@ -21,7 +19,8 @@ use Aviat\Ion\Enum;
/**
* Possible values for current watching status of anime
*/
final class Title extends Enum {
final class Title extends Enum
{
public const ALL = 'All';
public const WATCHING = 'Currently Watching';
public const PLAN_TO_WATCH = 'Plan to Watch';

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Enum\MangaReadingStatus;
@ -21,11 +19,12 @@ use Aviat\Ion\Enum;
/**
* Possible values for watching status for the current anime
*/
final class Anilist extends Enum {
final class Anilist extends Enum
{
public const READING = 'CURRENT';
public const COMPLETED = 'COMPLETED';
public const ON_HOLD = 'PAUSED';
public const DROPPED = 'DROPPED';
public const PLAN_TO_READ = 'PLANNING';
public const REPEATING = 'REPEATING';
}
}

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Enum\MangaReadingStatus;
@ -21,10 +19,11 @@ use Aviat\Ion\Enum;
/**
* Possible values for current reading status of manga
*/
final class Kitsu extends Enum {
final class Kitsu extends Enum
{
public const READING = 'current';
public const PLAN_TO_READ = 'planned';
public const DROPPED = 'dropped';
public const ON_HOLD = 'on_hold';
public const COMPLETED = 'completed';
}
}

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Enum\MangaReadingStatus;
@ -21,7 +19,8 @@ use Aviat\Ion\Enum;
/**
* Possible values for current reading status of manga
*/
final class Route extends Enum {
final class Route extends Enum
{
public const ALL = 'all';
public const READING = 'reading';
public const PLAN_TO_READ = 'plan_to_read';

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Enum\MangaReadingStatus;
@ -21,7 +19,8 @@ use Aviat\Ion\Enum;
/**
* Possible values for current reading status of manga
*/
final class Title extends Enum {
final class Title extends Enum
{
public const ALL = 'All';
public const READING = 'Currently Reading';
public const PLAN_TO_READ = 'Plan to Read';

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API;
@ -21,6 +19,6 @@ use UnexpectedValueException;
/**
* Exception for an API Request that fails validation
*/
class FailedResponseException extends UnexpectedValueException {
}
class FailedResponseException extends UnexpectedValueException
{
}

View File

@ -6,50 +6,43 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu;
use Aura\Session\Segment;
use const Aviat\AnimeClient\SESSION_SEGMENT;
use Aviat\AnimeClient\API\CacheTrait;
use Aviat\AnimeClient\Kitsu as K;
use Aviat\AnimeClient\API\CacheTrait;
use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
use Aviat\Ion\Event;
use const Aviat\AnimeClient\SESSION_SEGMENT;
/**
* Kitsu API Authentication
*/
final class Auth {
final class Auth
{
use CacheTrait;
use ContainerAware;
/**
* Anime API Model
*
* @var Model
*/
private Model $model;
/**
* Session object
*
* @var Segment
*/
private Segment $segment;
/**
* Constructor
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
@ -65,9 +58,6 @@ final class Auth {
/**
* Make the appropriate authentication call,
* and save the resulting auth token if successful
*
* @param string $password
* @return boolean
*/
public function authenticate(string $password): bool
{
@ -81,9 +71,6 @@ final class Auth {
/**
* Make the call to re-authenticate with the existing refresh token
*
* @param string|null $refreshToken
* @return boolean
*/
public function reAuthenticate(?string $refreshToken = NULL): bool
{
@ -101,18 +88,14 @@ final class Auth {
/**
* Check whether the current user is authenticated
*
* @return boolean
*/
public function isAuthenticated(): bool
{
return ($this->getAuthToken() !== NULL);
return $this->getAuthToken() !== NULL;
}
/**
* Clear authentication values
*
* @return void
*/
public function logout(): void
{
@ -121,8 +104,6 @@ final class Auth {
/**
* Retrieve the authentication token from the session
*
* @return string|null
*/
public function getAuthToken(): ?string
{
@ -137,8 +118,6 @@ final class Auth {
/**
* Retrieve the refresh token
*
* @return string|null
*/
private function getRefreshToken(): ?string
{
@ -153,11 +132,8 @@ final class Auth {
/**
* Save the new authentication information
*
* @param array|false $auth
* @return bool
*/
private function storeAuth(array|false $auth): bool
private function storeAuth(array|FALSE $auth): bool
{
if (FALSE !== $auth)
{
@ -178,6 +154,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;
}
}
@ -185,4 +162,5 @@ final class Auth {
return FALSE;
}
}
// End of KitsuAuth.php
// End of KitsuAuth.php

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu\Enum;
@ -21,7 +19,8 @@ use Aviat\Ion\Enum as BaseEnum;
/**
* Status of when anime is being/was/will be aired
*/
final class AnimeAiringStatus extends BaseEnum {
final class AnimeAiringStatus extends BaseEnum
{
public const NOT_YET_AIRED = 'Not Yet Aired';
public const AIRING = 'Currently Airing';
public const FINISHED_AIRING = 'Finished Airing';

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu\Enum;
@ -21,7 +19,8 @@ use Aviat\Ion\Enum as BaseEnum;
/**
* Status of when anime is being/was/will be aired
*/
final class MangaPublishingStatus extends BaseEnum {
final class MangaPublishingStatus extends BaseEnum
{
public const NOT_YET_PUBLISHED = 'Not Yet Published';
public const FINISHED = 'Completed';
public const CURRENT = 'Current';

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu;
@ -26,13 +24,12 @@ use Throwable;
/**
* CRUD operations for Kitsu list items
*/
final class ListItem extends AbstractListItem {
final class ListItem extends AbstractListItem
{
use ContainerAware;
use RequestBuilderTrait;
/**
* @param array $data
* @return Request
* @throws Throwable
*/
public function create(array $data): Request
@ -52,23 +49,23 @@ final class ListItem extends AbstractListItem {
'type' => 'libraryEntries',
'attributes' => [
'status' => $data['status'],
'progress' => $data['progress'] ?? 0
'progress' => $data['progress'] ?? 0,
],
'relationships' => [
'user' => [
'data' => [
'id' => $data['user_id'],
'type' => 'users'
]
'type' => 'users',
],
],
'media' => [
'data' => [
'id' => $data['id'],
'type' => $data['type']
]
]
]
]
'type' => $data['type'],
],
],
],
],
];
if (array_key_exists('notes', $data))
@ -90,21 +87,18 @@ final class ListItem extends AbstractListItem {
}
/**
* @param string $id
* @return Request
* @throws Throwable
*/
public function delete(string $id): Request
{
return $this->requestBuilder->mutateRequest('DeleteLibraryItem', [
'id' => $id
'id' => $id,
]);
}
/**
* @param string $id
* @return array
* @throws Throwable
* @return mixed[]
*/
public function get(string $id): array
{
@ -115,23 +109,16 @@ final class ListItem extends AbstractListItem {
/**
* 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->requestBuilder->mutateRequest('IncrementLibraryItem', [
'id' => $id,
'progress' => $data->progress
'progress' => $data->progress,
]);
}
/**
* @param string $id
* @param FormItemData $data
* @return Request
* @throws Throwable
*/
public function update(string $id, FormItemData $data): Request
@ -140,20 +127,21 @@ final class ListItem extends AbstractListItem {
$updateData = [
'id' => $id,
'notes' => $data['notes'],
'private' => (bool)$data['private'],
'reconsumeCount' => (int)$data['reconsumeCount'],
'reconsuming' => (bool)$data['reconsuming'],
'private' => (bool) $data['private'],
'reconsumeCount' => (int) $data['reconsumeCount'],
'reconsuming' => (bool) $data['reconsuming'],
'status' => strtoupper($data['status']),
];
// Only send these variables if they have a value
if ($data['progress'] !== NULL)
{
$updateData['progress'] = (int)$data['progress'];
$updateData['progress'] = (int) $data['progress'];
}
if ($data['ratingTwenty'] !== NULL)
{
$updateData['ratingTwenty'] = (int)$data['ratingTwenty'];
$updateData['ratingTwenty'] = (int) $data['ratingTwenty'];
}
return $this->requestBuilder->mutateRequest('UpdateLibraryItem', $updateData);
@ -164,10 +152,11 @@ final class ListItem extends AbstractListItem {
$auth = $this->getContainer()->get('auth');
$token = $auth->getAuthToken();
if ( ! empty($token)) {
if ( ! empty($token))
{
return "bearer {$token}";
}
return NULL;
}
}
}

View File

@ -6,24 +6,15 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu;
use Amp;
use Aviat\AnimeClient\API\{
CacheTrait,
Enum\AnimeWatchingStatus\Kitsu as KitsuWatchingStatus,
Enum\MangaReadingStatus\Kitsu as KitsuReadingStatus,
Mapping\AnimeWatchingStatus,
Mapping\MangaReadingStatus
};
use Aviat\AnimeClient\API\Kitsu\Transformer\{
AnimeHistoryTransformer,
AnimeListTransformer,
@ -33,10 +24,16 @@ use Aviat\AnimeClient\API\Kitsu\Transformer\{
MangaListTransformer,
MangaTransformer
};
use Aviat\AnimeClient\API\{
CacheTrait,
Enum\AnimeWatchingStatus\Kitsu as KitsuWatchingStatus,
Enum\MangaReadingStatus\Kitsu as KitsuReadingStatus,
Mapping\AnimeWatchingStatus,
Mapping\MangaReadingStatus
};
use Aviat\AnimeClient\Enum\MediaType;
use Aviat\AnimeClient\Kitsu as K;
use Aviat\AnimeClient\Types\Anime;
use Aviat\AnimeClient\Types\MangaPage;
use Aviat\AnimeClient\Types\{Anime, MangaPage};
use Aviat\Ion\{
Di\ContainerAware,
Json
@ -49,7 +46,8 @@ use const Aviat\AnimeClient\SESSION_SEGMENT;
/**
* Kitsu API Model
*/
final class Model {
final class Model
{
use CacheTrait;
use ContainerAware;
use RequestBuilderTrait;
@ -57,40 +55,20 @@ final class Model {
protected const LIST_PAGE_SIZE = 100;
/**
* @var AnimeTransformer
*/
protected AnimeTransformer $animeTransformer;
/**
* @var MangaTransformer
*/
protected MangaTransformer $mangaTransformer;
/**
* @var ListItem
*/
protected ListItem $listItem;
/**
* Constructor
*
* @param ListItem $listItem
*/
public function __construct(ListItem $listItem)
public function __construct(protected ListItem $listItem)
{
$this->animeTransformer = new AnimeTransformer();
$this->mangaTransformer = new MangaTransformer();
$this->listItem = $listItem;
}
/**
* Get the access token from the Kitsu API
*
* @param string $username
* @param string $password
* @return array|false
*/
public function authenticate(string $username, string $password): array|false
{
@ -100,24 +78,25 @@ final class Model {
'accept' => NULL,
'Content-type' => 'application/x-www-form-urlencoded',
'client_id' => NULL,
'client_secret' => NULL
'client_secret' => NULL,
],
'form_params' => [
'grant_type' => 'password',
'username' => $username,
'password' => $password
]
'password' => $password,
],
]);
$data = Json::decode(wait($response->getBody()->buffer()));
if (array_key_exists('error', $data))
{
dump([
'method' => __CLASS__ . '\\' . __METHOD__,
'method' => self::class . '\\' . __METHOD__,
'error' => $data['error'],
'response' => $response,
]);
die();
exit();
}
if (array_key_exists('access_token', $data))
@ -130,9 +109,6 @@ final class Model {
/**
* Extend the current session with a refresh token
*
* @param string $token
* @return array|false
*/
public function reAuthenticate(string $token): array|false
{
@ -140,23 +116,24 @@ final class Model {
'headers' => [
'accept' => NULL,
'Content-type' => 'application/x-www-form-urlencoded',
'Accept-encoding' => '*'
'Accept-encoding' => '*',
],
'form_params' => [
'grant_type' => 'refresh_token',
'refresh_token' => $token
]
'refresh_token' => $token,
],
]);
$data = Json::decode(wait($response->getBody()->buffer()));
if (array_key_exists('error', $data))
{
dump([
'method' => __CLASS__ . '\\' . __METHOD__,
'method' => self::class . '\\' . __METHOD__,
'error' => $data['error'],
'response' => $response,
]);
die();
exit();
}
if (array_key_exists('access_token', $data))
@ -169,20 +146,17 @@ final class Model {
/**
* Get the userid for a username from Kitsu
*
* @param string|null $username
* @return string
*/
public function getUserIdByUsername(string $username = NULL): string
public function getUserIdByUsername(?string $username = NULL): string
{
if ($username === NULL)
{
$username = $this->getUsername();
}
return $this->getCached(K::AUTH_USER_ID_KEY, function(string $username) {
return $this->getCached(K::AUTH_USER_ID_KEY, function (string $username) {
$data = $this->requestBuilder->runQuery('GetUserId', [
'slug' => $username
'slug' => $username,
]);
return $data['data']['findProfileBySlug']['id'] ?? NULL;
@ -192,34 +166,31 @@ final class Model {
/**
* Get information about a character
*
* @param string $slug
* @return array
* @return mixed[]
*/
public function getCharacter(string $slug): array
{
return $this->requestBuilder->runQuery('CharacterDetails', [
'slug' => $slug
'slug' => $slug,
]);
}
/**
* Get information about a person
*
* @param string $slug
* @return array
* @return mixed[]
*/
public function getPerson(string $slug): array
{
return $this->getCached("kitsu-person-{$slug}", fn () => $this->requestBuilder->runQuery('PersonDetails', [
'slug' => $slug
'slug' => $slug,
]));
}
/**
* Get profile information for the configured user
*
* @param string $username
* @return array
* @return mixed[]
*/
public function getUserData(string $username): array
{
@ -231,17 +202,13 @@ final class Model {
// -------------------------------------------------------------------------
// ! Anime-specific methods
// -------------------------------------------------------------------------
/**
* Get information about a particular anime
*
* @param string $slug
* @return Anime
*/
public function getAnime(string $slug): Anime
{
$baseData = $this->requestBuilder->runQuery('AnimeDetails', [
'slug' => $slug
'slug' => $slug,
]);
if (empty($baseData))
@ -255,7 +222,7 @@ final class Model {
public function getRandomAnime(): Anime
{
$baseData = $this->requestBuilder->runQuery('RandomMedia', [
'type' => 'ANIME'
'type' => 'ANIME',
]);
return $this->animeTransformer->transform($baseData);
@ -269,22 +236,20 @@ final class Model {
/**
* Get information about a particular anime
*
* @param string $animeId
* @return Anime
*/
public function getAnimeById(string $animeId): Anime
{
$baseData = $this->requestBuilder->runQuery('AnimeDetailsById', [
'id' => $animeId,
]);
return $this->animeTransformer->transform($baseData);
}
/**
* Retrieve the data for the anime watch history page
*
* @return array
* @return mixed[]
*/
public function getAnimeHistory(): array
{
@ -298,7 +263,6 @@ final class Model {
$list = (new AnimeHistoryTransformer())->transform($raw);
$this->cache->set($key, $list);
}
return $list;
@ -308,7 +272,7 @@ final class Model {
* Get the anime list for the configured user
*
* @param string $status - The watching status to filter the list with
* @return array
* @return mixed[]
*/
public function getAnimeList(string $status): array
{
@ -330,7 +294,7 @@ final class Model {
$transformed = $transformer->transformCollection($data);
$keyed = [];
foreach($transformed as $item)
foreach ($transformed as $item)
{
$keyed[$item['id']] = $item;
}
@ -346,9 +310,8 @@ final class Model {
* Get the number of anime list items
*
* @param string $status - Optional status to filter by
* @return int
*/
public function getAnimeListCount(string $status = '') : int
public function getAnimeListCount(string $status = ''): int
{
return $this->getListCount(MediaType::ANIME, $status);
}
@ -356,7 +319,7 @@ final class Model {
/**
* Get all the anime entries, that are organized for output to html
*
* @return array
* @return array<string, mixed[]>
*/
public function getFullOrganizedAnimeList(): array
{
@ -364,7 +327,7 @@ final class Model {
$statuses = KitsuWatchingStatus::getConstList();
foreach ($statuses as $key => $status)
foreach ($statuses as $status)
{
$mappedStatus = AnimeWatchingStatus::KITSU_TO_TITLE[$status];
$output[$mappedStatus] = $this->getAnimeList($status) ?? [];
@ -376,17 +339,13 @@ final class Model {
// -------------------------------------------------------------------------
// ! Manga-specific methods
// -------------------------------------------------------------------------
/**
* Get information about a particular manga
*
* @param string $slug
* @return MangaPage
*/
public function getManga(string $slug): MangaPage
{
$baseData = $this->requestBuilder->runQuery('MangaDetails', [
'slug' => $slug
'slug' => $slug,
]);
if (empty($baseData))
@ -400,7 +359,7 @@ final class Model {
public function getRandomManga(): MangaPage
{
$baseData = $this->requestBuilder->runQuery('RandomMedia', [
'type' => 'MANGA'
'type' => 'MANGA',
]);
return $this->mangaTransformer->transform($baseData);
@ -408,22 +367,20 @@ final class Model {
/**
* Get information about a particular manga
*
* @param string $mangaId
* @return MangaPage
*/
public function getMangaById(string $mangaId): MangaPage
{
$baseData = $this->requestBuilder->runQuery('MangaDetailsById', [
'id' => $mangaId,
]);
return $this->mangaTransformer->transform($baseData);
}
/**
* Retrieve the data for the manga read history page
*
* @return array
* @return mixed[]
*/
public function getMangaHistory(): array
{
@ -445,7 +402,7 @@ final class Model {
* Get the manga list for the configured user
*
* @param string $status - The reading status by which to filter the list
* @return array
* @return mixed[]
*/
public function getMangaList(string $status): array
{
@ -467,7 +424,7 @@ final class Model {
$transformed = $transformer->transformCollection($data);
$keyed = [];
foreach($transformed as $item)
foreach ($transformed as $item)
{
$keyed[$item['id']] = $item;
}
@ -483,9 +440,8 @@ final class Model {
* Get the number of manga list items
*
* @param string $status - Optional status to filter by
* @return int
*/
public function getMangaListCount(string $status = '') : int
public function getMangaListCount(string $status = ''): int
{
return $this->getListCount(MediaType::MANGA, $status);
}
@ -493,12 +449,13 @@ final class Model {
/**
* Get all Manga lists
*
* @return array
* @return array<string, mixed[]>
*/
public function getFullOrganizedMangaList(): array
{
$statuses = KitsuReadingStatus::getConstList();
$output = [];
foreach ($statuses as $status)
{
$mappedStatus = MangaReadingStatus::KITSU_TO_TITLE[$status];
@ -511,13 +468,12 @@ final class Model {
// ------------------------------------------------------------------------
// Base methods
// ------------------------------------------------------------------------
/**
* Search for an anime or manga
*
* @param string $type - 'anime' or 'manga'
* @param string $query - name of the item to search for
* @return array
* @return array<int, array<string, mixed>>
*/
public function search(string $type, string $query): array
{
@ -543,9 +499,9 @@ final class Model {
// Search for MAL mapping
if (is_array($item['mappings']['nodes']))
{
foreach($item['mappings']['nodes'] as $mapping)
foreach ($item['mappings']['nodes'] as $mapping)
{
if ($mapping['externalSite'] === "MYANIMELIST_" . strtoupper($type))
if ($mapping['externalSite'] === 'MYANIMELIST_' . strtoupper($type))
{
$searchItem['mal_id'] = $mapping['externalId'];
break;
@ -554,7 +510,6 @@ final class Model {
}
$data[] = $searchItem;
}
return $data;
@ -563,11 +518,9 @@ final class Model {
/**
* Find a media item on Kitsu by its associated MAL id
*
* @param string $malId
* @param string $type "anime" or "manga"
* @return string|NULL
*/
public function getKitsuIdFromMALId(string $malId, string $type='anime'): ?string
public function getKitsuIdFromMALId(string $malId, string $type = 'anime'): ?string
{
$raw = $this->requestBuilder->runQuery('GetIdByMapping', [
'id' => $malId,
@ -594,6 +547,9 @@ final class Model {
return (new LibraryEntryTransformer())->transform($baseData['data']['findLibraryEntryById']);
}
/**
* @return mixed[]
*/
public function getThumbList(string $type): array
{
$statuses = [
@ -620,11 +576,9 @@ final class Model {
}
/**
*
* Get the data to sync Kitsu anime/manga list with another API
*
* @param string $type
* @return array
* @return mixed[]
*/
public function getSyncList(string $type): array
{
@ -654,7 +608,7 @@ final class Model {
/**
* Get the aggregated pages of anime or manga history
*
* @return array
* @return mixed[]
*/
protected function getHistoryList(): array
{
@ -666,9 +620,7 @@ final class Model {
/**
* Get the raw anime/manga list from GraphQL
*
* @param string $type
* @param string $status
* @return array
* @return mixed[]
*/
protected function getList(string $type, string $status = ''): array
{
@ -687,7 +639,7 @@ final class Model {
$cursor = '';
$username = $this->getUsername();
return new Amp\Producer(function (callable $emit) use ($type, $status, $cursor, $username) {
return new Amp\Producer(function (callable $emit) use ($type, $status, $cursor, $username): Generator {
while (TRUE)
{
$vars = [
@ -698,6 +650,7 @@ final class Model {
{
$vars['status'] = $status;
}
if ($cursor !== '')
{
$vars['after'] = $cursor;
@ -719,7 +672,8 @@ final class Model {
// @TODO Proper Error logging
dump($rawData);
die();
exit();
}
$cursor = $page['endCursor'];
@ -734,11 +688,12 @@ final class Model {
});
}
private function getSyncPages(string $type, string $status): Amp\Iterator {
private function getSyncPages(string $type, string $status): Amp\Iterator
{
$cursor = '';
$username = $this->getUsername();
return new Amp\Producer(function (callable $emit) use ($type, $status, $cursor, $username) {
return new Amp\Producer(function (callable $emit) use ($type, $status, $cursor, $username): Generator {
while (TRUE)
{
$vars = [
@ -761,7 +716,8 @@ final class Model {
if (empty($data))
{
dump($rawData);
die();
exit();
}
$cursor = $page['endCursor'];
@ -781,7 +737,7 @@ final class Model {
$cursor = '';
$username = $this->getUsername();
return new Amp\Producer(function (callable $emit) use ($type, $status, $cursor, $username) {
return new Amp\Producer(function (callable $emit) use ($type, $status, $cursor, $username): Generator {
while (TRUE)
{
$vars = [
@ -804,7 +760,8 @@ final class Model {
if (empty($data))
{
dump($rawData);
die();
exit();
}
$cursor = $page['endCursor'];
@ -843,8 +800,6 @@ final class Model {
/**
* Get the kitsu username from config
*
* @return string
*/
private function getUsername(): string
{
@ -857,7 +812,7 @@ final class Model {
{
$args = [
'type' => strtoupper($type),
'slug' => $this->getUsername()
'slug' => $this->getUsername(),
];
if ($status !== '')
{
@ -868,4 +823,4 @@ final class Model {
return $res['data']['findProfileBySlug']['library']['all']['totalCount'];
}
}
}

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu;
@ -22,16 +20,13 @@ use Aviat\AnimeClient\Types\FormItem;
/**
* Kitsu API calls that mutate data, C/U/D parts of CRUD
*/
trait MutationTrait {
trait MutationTrait
{
// -------------------------------------------------------------------------
// ! Generic API calls
// -------------------------------------------------------------------------
/**
* Create a list item
*
* @param array $data
* @return Request|null
*/
public function createListItem(array $data): ?Request
{
@ -46,9 +41,6 @@ trait MutationTrait {
/**
* Increase the progress count for a list item
*
* @param FormItem $data
* @return Request
*/
public function incrementListItem(FormItem $data): Request
{
@ -57,9 +49,6 @@ trait MutationTrait {
/**
* Modify a list item
*
* @param FormItem $data
* @return Request
*/
public function updateListItem(FormItem $data): Request
{
@ -70,10 +59,20 @@ trait MutationTrait {
* Remove a list item
*
* @param string $id - The id of the list item to remove
* @return Request
*/
public function deleteListItem(string $id): Request
{
return $this->listItem->delete($id);
}
}
/**
* Remove a list item
*
* @param FormItem $data
* @return Request
*/
public function deleteItem(FormItem $data): Request
{
return $this->listItem->delete($data['id']);
}
}

View File

@ -6,57 +6,46 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu;
use const Aviat\AnimeClient\SESSION_SEGMENT;
use const Aviat\AnimeClient\USER_AGENT;
use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use Amp\Http\Client\Request;
use Amp\Http\Client\Response;
use Aviat\AnimeClient\Kitsu as K;
use Amp\Http\Client\{Request, Response};
use Aviat\AnimeClient\API\APIRequestBuilder;
use Aviat\AnimeClient\Enum\EventType;
use Aviat\Ion\Di\ContainerAware;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Event;
use Aviat\Ion\Json;
use Aviat\Ion\JsonException;
use Aviat\AnimeClient\Kitsu as K;
use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
use Aviat\Ion\{Event, Json, JsonException};
use LogicException;
use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use function in_array;
use const Aviat\AnimeClient\{SESSION_SEGMENT, USER_AGENT};
final class RequestBuilder extends APIRequestBuilder {
final class RequestBuilder extends APIRequestBuilder
{
use ContainerAware;
/**
* The base url for api requests
* @var string $base_url
*/
protected string $baseUrl = K::GRAPHQL_ENDPOINT;
/**
* Where to look for GraphQL request files
* @var string
*/
protected string $filePath = __DIR__;
/**
* HTTP headers to send with every request
*
* @var array
*/
protected array $defaultHeaders = [
'User-Agent' => USER_AGENT,
'User-Agent' => USER_AGENT,
'Accept' => 'application/vnd.api+json',
'Content-Type' => 'application/vnd.api+json',
'CLIENT_ID' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd',
@ -70,11 +59,6 @@ final class RequestBuilder extends APIRequestBuilder {
/**
* Create a request object
*
* @param string $type
* @param string $url
* @param array $options
* @return Request
*/
public function setUpRequest(string $type, string $url, array $options = []): Request
{
@ -85,13 +69,13 @@ final class RequestBuilder extends APIRequestBuilder {
->getSegment(SESSION_SEGMENT);
$cache = $this->getContainer()->get('cache');
$token = null;
$token = NULL;
if ($cache->has(K::AUTH_TOKEN_CACHE_KEY))
{
$token = $cache->get(K::AUTH_TOKEN_CACHE_KEY);
}
else if ($url !== K::AUTH_URL && $sessionSegment->get('auth_token') !== NULL)
elseif ($url !== K::AUTH_URL && $sessionSegment->get('auth_token') !== NULL)
{
$token = $sessionSegment->get('auth_token');
if ( ! (empty($token) || $cache->has(K::AUTH_TOKEN_CACHE_KEY)))
@ -131,9 +115,7 @@ final class RequestBuilder extends APIRequestBuilder {
/**
* Run a GraphQL API query
*
* @param string $name
* @param array $variables
* @return array
* @return mixed[]
*/
public function runQuery(string $name, array $variables = []): array
{
@ -141,12 +123,12 @@ final class RequestBuilder extends APIRequestBuilder {
$response = getResponse($request);
$validResponseCodes = [200, 201];
if ( ! \in_array($response->getStatus(), $validResponseCodes, TRUE))
if ( ! in_array($response->getStatus(), $validResponseCodes, TRUE))
{
$logger = $this->container->getLogger('kitsu-graphql');
if ($logger !== NULL)
{
$logger->warning('Non 200 response for GraphQL call', (array)$response->getBody());
$logger->warning('Non 200 response for GraphQL call', (array) $response->getBody());
}
}
@ -156,9 +138,7 @@ final class RequestBuilder extends APIRequestBuilder {
/**
* Run a GraphQL mutation
*
* @param string $name
* @param array $variables
* @return array
* @return mixed[]
*/
public function mutate(string $name, array $variables = []): array
{
@ -166,12 +146,12 @@ final class RequestBuilder extends APIRequestBuilder {
$response = getResponse($request);
$validResponseCodes = [200, 201];
if ( ! \in_array($response->getStatus(), $validResponseCodes, TRUE))
if ( ! in_array($response->getStatus(), $validResponseCodes, TRUE))
{
$logger = $this->container->getLogger('kitsu-graphql');
if ($logger !== NULL)
{
$logger->warning('Non 200 response for GraphQL call', (array)$response->getBody());
$logger->warning('Non 200 response for GraphQL call', (array) $response->getBody());
}
}
@ -180,11 +160,6 @@ final class RequestBuilder extends APIRequestBuilder {
/**
* Make a request
*
* @param string $type
* @param string $url
* @param array $options
* @return Response
*/
public function getResponse(string $type, string $url, array $options = []): Response
{
@ -205,10 +180,6 @@ final class RequestBuilder extends APIRequestBuilder {
/**
* Create a GraphQL query and return the Request object
*
* @param string $name
* @param array $variables
* @return Request
*/
public function queryRequest(string $name, array $variables = []): Request
{
@ -220,13 +191,14 @@ final class RequestBuilder extends APIRequestBuilder {
$query = file_get_contents($file);
$body = [
'query' => $query
'query' => $query,
];
if ( ! empty($variables))
{
$body['variables'] = [];
foreach($variables as $key => $val)
foreach ($variables as $key => $val)
{
$body['variables'][$key] = $val;
}
@ -239,12 +211,8 @@ final class RequestBuilder extends APIRequestBuilder {
/**
* Create a GraphQL mutation request, and return the Request object
*
* @param string $name
* @param array $variables
* @return Request
*/
public function mutateRequest (string $name, array $variables = []): Request
public function mutateRequest(string $name, array $variables = []): Request
{
$file = realpath("{$this->filePath}/Mutations/{$name}.graphql");
if ($file === FALSE || ! file_exists($file))
@ -254,11 +222,13 @@ final class RequestBuilder extends APIRequestBuilder {
$query = file_get_contents($file);
$body = [
'query' => $query
'query' => $query,
];
if (!empty($variables)) {
if ( ! empty($variables))
{
$body['variables'] = [];
foreach ($variables as $key => $val)
{
$body['variables'][$key] = $val;
@ -272,11 +242,6 @@ final class RequestBuilder extends APIRequestBuilder {
/**
* Make a request
*
* @param string $type
* @param string $url
* @param array $options
* @return array
*/
private function request(string $type, string $url, array $options = []): array
{
@ -297,7 +262,7 @@ final class RequestBuilder extends APIRequestBuilder {
{
if ($logger !== NULL)
{
$logger->warning('Non 2xx response for api call', (array)$response);
$logger->warning('Non 2xx response for api call', (array) $response);
}
}
@ -305,11 +270,12 @@ final class RequestBuilder extends APIRequestBuilder {
{
return Json::decode($rawBody);
}
catch (JsonException $e)
catch (JsonException)
{
// dump($e);
dump($rawBody);
die();
exit();
}
}
}
}

View File

@ -6,32 +6,30 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu;
trait RequestBuilderTrait {
trait RequestBuilderTrait
{
/**
* The request builder for the Kitsu API
* @var RequestBuilder
*/
protected RequestBuilder $requestBuilder;
/**
* Set the request builder object
*
* @param RequestBuilder $requestBuilder
* @return RequestBuilderTrait|ListItem|Model
* @return ListItem|Model|RequestBuilderTrait
*/
public function setRequestBuilder(RequestBuilder $requestBuilder): self
{
$this->requestBuilder = $requestBuilder;
return $this;
}
}
}

View File

@ -6,28 +6,22 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
class AnimeHistoryTransformer extends HistoryTransformer {
class AnimeHistoryTransformer extends HistoryTransformer
{
protected string $type = 'anime';
protected string $progressAction = 'Watched episode';
protected string $reconsumeAction = 'Rewatched episode';
protected string $largeAggregateAction = 'Marathoned episodes';
protected string $reconsumingStatus = 'Rewatching';
protected array $statusMap = AnimeWatchingStatus::KITSU_TO_TITLE;
}
}

View File

@ -6,20 +6,18 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\Kitsu;
use Aviat\AnimeClient\Types\{
FormItem,
AnimeListItem
AnimeListItem,
FormItem
};
use Aviat\Ion\Transformer\AbstractTransformer;
use Aviat\Ion\Type\StringType;
@ -27,31 +25,31 @@ use Aviat\Ion\Type\StringType;
/**
* Transformer for anime list
*/
final class AnimeListTransformer extends AbstractTransformer {
final class AnimeListTransformer extends AbstractTransformer
{
/**
* Convert raw api response to a more
* logical and workable structure
*
* @param array|object $item API library item
* @return AnimeListItem
* @param array|object $item API library item
*/
public function transform(array|object $item): AnimeListItem
{
$item = (array)$item;
$item = (array) $item;
$animeId = $item['media']['id'];
$anime = $item['media'];
$genres = [];
$rating = (int) $item['rating'] !== 0
? (int)$item['rating'] / 2
? (int) $item['rating'] / 2
: '-';
$total_episodes = (int) $anime['episodeCount'] !== 0
? (int) $anime['episodeCount']
: '-';
$AnilistId = NULL;
$MALid = NULL;
$mappings = $anime['mappings']['nodes'] ?? [];
@ -62,7 +60,11 @@ final class AnimeListTransformer extends AbstractTransformer {
if ($mapping['externalSite'] === 'MYANIMELIST_ANIME')
{
$MALid = $mapping['externalId'];
break;
}
if ($mapping['externalSite'] === 'ANILIST_ANIME')
{
$AnilistId = $mapping['externalId'];
}
}
}
@ -76,6 +78,7 @@ final class AnimeListTransformer extends AbstractTransformer {
return AnimeListItem::from([
'id' => $item['id'],
'anilist_id' => $AnilistId,
'mal_id' => $MALid,
'episodes' => [
'watched' => (int) $item['progress'] !== 0
@ -87,7 +90,7 @@ final class AnimeListTransformer extends AbstractTransformer {
'airing' => [
'status' => Kitsu::getAiringStatus($anime['startDate'], $anime['endDate']),
'started' => $anime['startDate'],
'ended' => $anime['endDate']
'ended' => $anime['endDate'],
],
'anime' => [
'id' => $animeId,
@ -95,7 +98,7 @@ final class AnimeListTransformer extends AbstractTransformer {
'title' => $title,
'titles' => $titles,
'slug' => $anime['slug'],
'show_type' => (string)StringType::from($anime['subtype'])->upperCaseFirst(),
'show_type' => (string) StringType::from($anime['subtype'])->upperCaseFirst(),
'cover_image' => Kitsu::getPosterImage($anime),
'genres' => $genres,
'streaming_links' => $streamingLinks,
@ -123,14 +126,15 @@ final class AnimeListTransformer extends AbstractTransformer {
$untransformed = FormItem::from([
'id' => $item['id'],
'anilist_id' => $item['anilist_id'] ?? NULL,
'mal_id' => $item['mal_id'] ?? NULL,
'data' => [
'status' => $item['watching_status'],
'reconsuming' => $rewatching,
'reconsumeCount' => $item['rewatched'],
'notes' => $item['notes'],
'private' => $privacy
]
'private' => $privacy,
],
]);
if (is_numeric($item['episodes_watched']) && $item['episodes_watched'] > 0)
@ -146,4 +150,5 @@ final class AnimeListTransformer extends AbstractTransformer {
return $untransformed;
}
}
// End of AnimeListTransformer.php
// End of AnimeListTransformer.php

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
@ -23,23 +21,22 @@ use Aviat\Ion\Transformer\AbstractTransformer;
/**
* Transformer for anime description page
*/
final class AnimeTransformer extends AbstractTransformer {
final class AnimeTransformer extends AbstractTransformer
{
/**
* Convert raw api response to a more
* logical and workable structure
*
* @param array|object $item API library item
* @return AnimePage
* @param array|object $item API library item
*/
public function transform(array|object $item): AnimePage
{
$item = (array)$item;
$item = (array) $item;
$base = $item['data']['findAnimeBySlug'] ?? $item['data']['findAnimeById'] ?? $item['data']['randomMedia'];
$characters = [];
$links = [];
$staff = [];
$genres = array_map(fn ($genre) => $genre['title']['en'], $base['categories']['nodes']);
$genres = array_map(static fn ($genre) => $genre['title']['en'], $base['categories']['nodes']);
sort($genres);
@ -47,7 +44,7 @@ final class AnimeTransformer extends AbstractTransformer {
$titles = Kitsu::getTitles($base['titles']);
$titles_more = Kitsu::filterLocalizedTitles($base['titles']);
if (count($base['characters']['nodes']) > 0)
if ((is_countable($base['characters']['nodes']) ? count($base['characters']['nodes']) : 0) > 0)
{
foreach ($base['characters']['nodes'] as $rawCharacter)
{
@ -73,14 +70,14 @@ final class AnimeTransformer extends AbstractTransformer {
}
else
{
uasort($characters[$type], fn($a, $b) => $a['name'] <=> $b['name']);
uasort($characters[$type], static fn ($a, $b) => $a['name'] <=> $b['name']);
}
}
krsort($characters);
}
if (count($base['staff']['nodes']) > 0)
if ((is_countable($base['staff']['nodes']) ? count($base['staff']['nodes']) : 0) > 0)
{
foreach ($base['staff']['nodes'] as $staffing)
{
@ -90,7 +87,7 @@ final class AnimeTransformer extends AbstractTransformer {
// If this person object is so broken as to not have a proper image object,
// just skip it. No point in showing a role with nothing in it.
if ($person === null || $person['id'] === null || $person['image'] === null)
if ($person === NULL || $person['id'] === NULL || $person['image'] === NULL)
{
continue;
}
@ -103,17 +100,17 @@ final class AnimeTransformer extends AbstractTransformer {
$staff[$role][$person['id']] = [
'id' => $person['id'],
'name' => $name,
'image' => $person['image']['original']['url'],
'image' => $person['image']['original']['url'],
'slug' => $person['slug'],
];
usort($staff[$role], fn ($a, $b) => $a['name'] <=> $b['name']);
usort($staff[$role], static fn ($a, $b) => $a['name'] <=> $b['name']);
}
ksort($staff);
}
if (count($base['mappings']['nodes']) > 0)
if ((is_countable($base['mappings']['nodes']) ? count($base['mappings']['nodes']) : 0) > 0)
{
$links = Kitsu::mappingsToUrls($base['mappings']['nodes'], "https://kitsu.io/anime/{$base['slug']}");
}
@ -143,4 +140,4 @@ final class AnimeTransformer extends AbstractTransformer {
'url' => "https://kitsu.io/anime/{$base['slug']}",
]);
}
}
}

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
@ -25,15 +23,11 @@ use Locale;
/**
* Data transformation class for character pages
*/
final class CharacterTransformer extends AbstractTransformer {
/**
* @param array|object $item
* @return Character
*/
final class CharacterTransformer extends AbstractTransformer
{
public function transform(array|object $item): Character
{
$item = (array)$item;
$item = (array) $item;
$data = $item['data']['findCharacterBySlug'] ?? [];
$castings = [];
$media = [
@ -42,10 +36,7 @@ final class CharacterTransformer extends AbstractTransformer {
];
$names = array_unique(
array_merge(
[$data['names']['canonical']],
array_values($data['names']['localized'])
)
[...[$data['names']['canonical']], ...array_values($data['names']['localized'])]
);
$name = array_shift($names);
@ -66,19 +57,22 @@ final class CharacterTransformer extends AbstractTransformer {
]);
}
protected function organizeMediaAndVoices (array $data): array
/**
* @return array<int, mixed[]>
*/
protected function organizeMediaAndVoices(array $data): array
{
if (empty($data))
{
return [[], []];
}
$titleSort = fn ($a, $b) => $a['title'] <=> $b['title'];
$titleSort = static fn ($a, $b) => $a['title'] <=> $b['title'];
// First, let's deal with related media
$rawMedia = array_column($data, 'media');
$rawAnime = array_filter($rawMedia, fn ($item) => $item['type'] === 'Anime');
$rawManga = array_filter($rawMedia, fn ($item) => $item['type'] === 'Manga');
$rawAnime = array_filter($rawMedia, static fn ($item) => $item['type'] === 'Anime');
$rawManga = array_filter($rawMedia, static fn ($item) => $item['type'] === 'Manga');
$anime = array_map(static function ($item) {
$output = $item;
@ -106,7 +100,7 @@ final class CharacterTransformer extends AbstractTransformer {
];
// And now, reorganize voice actor relationships
$rawVoices = array_filter($data, fn($item) => (! empty($item['voices'])) && count((array)$item['voices']['nodes']) > 0);
$rawVoices = array_filter($data, static fn ($item) => ( ! empty($item['voices'])) && (array) $item['voices']['nodes'] !== []);
if (empty($rawVoices))
{
@ -139,7 +133,7 @@ final class CharacterTransformer extends AbstractTransformer {
'image' => $voice['person']['image']['original']['url'],
'name' => $voice['person']['name'],
],
'series' => []
'series' => [],
];
}
@ -158,4 +152,4 @@ final class CharacterTransformer extends AbstractTransformer {
return [$media, $castings];
}
}
}

View File

@ -6,23 +6,22 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\Types\HistoryItem;
use Aviat\AnimeClient\Kitsu;
use Aviat\AnimeClient\Types\HistoryItem;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
abstract class HistoryTransformer {
abstract class HistoryTransformer
{
/**
* @var string The media type
*/
@ -51,13 +50,10 @@ abstract class HistoryTransformer {
/**
* @var array The mapping of api status to display status
*/
protected array $statusMap;
protected array $statusMap = [];
/**
* Convert raw history
*
* @param array $data
* @return array
*/
public function transform(array $data): array
{
@ -73,7 +69,7 @@ abstract class HistoryTransformer {
}
// Hide private library entries
if ($entry['libraryEntry']['private'] === true)
if ($entry['libraryEntry']['private'] === TRUE)
{
continue;
}
@ -88,7 +84,7 @@ abstract class HistoryTransformer {
$output[] = $transformed;
}
}
else if ($kind === 'updated')
elseif ($kind === 'updated')
{
$output[] = $this->transformUpdated($entry);
}
@ -99,15 +95,13 @@ abstract class HistoryTransformer {
/**
* Combine consecutive 'progressed' events
*
* @param array $singles
* @return array
*/
protected function aggregate (array $singles): array
protected function aggregate(array $singles): array
{
$output = [];
$count = count($singles);
for ($i = 0; $i < $count; $i++)
{
$entries = [];
@ -115,9 +109,10 @@ abstract class HistoryTransformer {
$prevTitle = $entry['title'];
$nextId = $i;
$next = $singles[$nextId];
while (
$next['kind'] === 'progressed' &&
$next['title'] === $prevTitle
$next['kind'] === 'progressed'
&& $next['title'] === $prevTitle
) {
$entries[] = $next;
$prevTitle = $next['title'];
@ -126,6 +121,7 @@ abstract class HistoryTransformer {
{
$nextId++;
$next = $singles[$nextId];
continue;
}
@ -143,6 +139,7 @@ abstract class HistoryTransformer {
$items[] = array_pop($progressItem);
$updated[] = $e['updated'];
}
$firstItem = min($items);
$lastItem = max($items);
$firstUpdate = min($updated);
@ -165,7 +162,7 @@ abstract class HistoryTransformer {
'action' => $action,
'coverImg' => $entries[0]['coverImg'],
'dateRange' => [$firstUpdate, $lastUpdate],
'isAggregate' => true,
'isAggregate' => TRUE,
'original' => $entries,
'title' => $title,
'updated' => $entries[0]['updated'],
@ -174,6 +171,7 @@ abstract class HistoryTransformer {
// Skip the rest of the aggregate in the main loop
$i += count($entries) - 1;
continue;
}
@ -183,14 +181,14 @@ abstract class HistoryTransformer {
return $output;
}
protected function transformProgress (array $entry): ?HistoryItem
protected function transformProgress(array $entry): ?HistoryItem
{
$data = $entry['media'];
$title = $this->linkTitle($data);
$item = end($entry['changedData']['progress']);
// No showing episode 0 nonsense
if (((int)$item) === 0)
if (((int) $item) === 0)
{
return NULL;
}
@ -256,12 +254,12 @@ abstract class HistoryTransformer {
return HistoryItem::from($entry);
}
protected function linkTitle (array $data): string
protected function linkTitle(array $data): string
{
return $data['titles']['canonical'];
}
protected function parseDate (string $date): DateTimeImmutable
protected function parseDate(string $date): DateTimeImmutable
{
$dateTime = DateTimeImmutable::createFromFormat(
DateTimeInterface::RFC3339,
@ -276,13 +274,13 @@ abstract class HistoryTransformer {
return $dateTime->setTimezone(new DateTimeZone(date_default_timezone_get()));
}
protected function getUrl (array $data): string
protected function getUrl(array $data): string
{
return "/{$this->type}/details/{$data['slug']}";
}
protected function isReconsuming (array $entry): bool
protected function isReconsuming(array $entry): bool
{
return $entry['libraryEntry']['reconsuming'];
}
}
}

View File

@ -6,18 +6,16 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\Kitsu;
use Aviat\AnimeClient\Types\{FormItem, AnimeListItem, MangaListItem, MangaListItemDetail};
use Aviat\AnimeClient\Types\{AnimeListItem, FormItem, MangaListItem, MangaListItemDetail};
use Aviat\Ion\Transformer\AbstractTransformer;
use Aviat\Ion\Type\StringType;
@ -28,7 +26,7 @@ final class LibraryEntryTransformer extends AbstractTransformer
{
public function transform(array|object $item): AnimeListItem|MangaListItem
{
$item = (array)$item;
$item = (array) $item;
$type = $item['media']['type'] ?? '';
$genres = [];
@ -60,6 +58,7 @@ final class LibraryEntryTransformer extends AbstractTransformer
: '-';
$MALid = NULL;
$AnilistId = NULL;
if (isset($anime['mappings']['nodes']))
{
@ -68,7 +67,11 @@ final class LibraryEntryTransformer extends AbstractTransformer
if ($mapping['externalSite'] === 'MYANIMELIST_ANIME')
{
$MALid = $mapping['externalId'];
break;
}
if ($mapping['externalSite'] === 'ANILIST_ANIME')
{
$AnilistId = $mapping['externalId'];
}
}
}
@ -82,6 +85,7 @@ final class LibraryEntryTransformer extends AbstractTransformer
return AnimeListItem::from([
'id' => $item['id'],
'anilist_id' => $AnilistId,
'mal_id' => $MALid,
'episodes' => [
'watched' => (int) $item['progress'] !== 0
@ -93,7 +97,7 @@ final class LibraryEntryTransformer extends AbstractTransformer
'airing' => [
'status' => Kitsu::getAiringStatus($anime['startDate'], $anime['endDate']),
'started' => $anime['startDate'],
'ended' => $anime['endDate']
'ended' => $anime['endDate'],
],
'anime' => [
'id' => $animeId,
@ -101,7 +105,7 @@ final class LibraryEntryTransformer extends AbstractTransformer
'title' => $title,
'titles' => $titles,
'slug' => $anime['slug'],
'show_type' => (string)StringType::from($anime['subtype'])->upperCaseFirst(),
'show_type' => (string) StringType::from($anime['subtype'])->upperCaseFirst(),
'cover_image' => Kitsu::getPosterImage($anime),
'genres' => $genres,
'streaming_links' => $streamingLinks,
@ -137,6 +141,7 @@ final class LibraryEntryTransformer extends AbstractTransformer
: '-';
$MALid = NULL;
$AnilistId = NULL;
if (isset($manga['mappings']['nodes']))
{
@ -145,7 +150,11 @@ final class LibraryEntryTransformer extends AbstractTransformer
if ($mapping['externalSite'] === 'MYANIMELIST_MANGA')
{
$MALid = $mapping['externalId'];
break;
}
if ($mapping['externalSite'] === 'ANILIST_MANGA')
{
$AnilistId = $mapping['externalId'];
}
}
}
@ -155,14 +164,15 @@ final class LibraryEntryTransformer extends AbstractTransformer
return MangaListItem::from([
'id' => $item['id'],
'anilist_id' => $AnilistId,
'mal_id' => $MALid,
'chapters' => [
'read' => $readChapters,
'total' => $totalChapters
'total' => $totalChapters,
],
'volumes' => [
'read' => '-', //$item['attributes']['volumes_read'],
'total' => $totalVolumes
'total' => $totalVolumes,
],
'manga' => MangaListItemDetail::from([
'genres' => $genres,
@ -171,14 +181,14 @@ final class LibraryEntryTransformer extends AbstractTransformer
'slug' => $manga['slug'],
'title' => $title,
'titles' => $titles,
'type' => (string)StringType::from($manga['subtype'])->upperCaseFirst(),
'type' => (string) StringType::from($manga['subtype'])->upperCaseFirst(),
'url' => 'https://kitsu.io/manga/' . $manga['slug'],
]),
'reading_status' => strtolower($item['status']),
'notes' => $item['notes'],
'rereading' => (bool)$item['reconsuming'],
'rereading' => (bool) $item['reconsuming'],
'reread' => $item['reconsumeCount'],
'user_rating' => $rating,
]);
}
}
}

View File

@ -6,28 +6,22 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\Mapping\MangaReadingStatus;
class MangaHistoryTransformer extends HistoryTransformer {
class MangaHistoryTransformer extends HistoryTransformer
{
protected string $type = 'manga';
protected string $progressAction = 'Read chapter';
protected string $reconsumeAction = 'Reread chapter';
protected string $largeAggregateAction = 'Blew through chapters';
protected string $reconsumingStatus = 'Rereading';
protected array $statusMap = MangaReadingStatus::KITSU_TO_TITLE;
}
}

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
@ -27,16 +25,16 @@ use Aviat\Ion\Type\StringType;
/**
* Data transformation class for zippered Hummingbird manga
*/
final class MangaListTransformer extends AbstractTransformer {
final class MangaListTransformer extends AbstractTransformer
{
/**
* Remap zipped anime data to a more logical form
*
* @param array|object $item manga entry item
* @return MangaListItem
* @param array|object $item manga entry item
*/
public function transform(array|object $item): MangaListItem
{
$item = (array)$item;
$item = (array) $item;
$mangaId = $item['media']['id'];
$manga = $item['media'];
@ -82,11 +80,11 @@ final class MangaListTransformer extends AbstractTransformer {
'mal_id' => $MALid,
'chapters' => [
'read' => $readChapters,
'total' => $totalChapters
'total' => $totalChapters,
],
'volumes' => [
'read' => '-', //$item['attributes']['volumes_read'],
'total' => $totalVolumes
'total' => $totalVolumes,
],
'manga' => MangaListItemDetail::from([
'genres' => $genres,
@ -95,12 +93,12 @@ final class MangaListTransformer extends AbstractTransformer {
'slug' => $manga['slug'],
'title' => $title,
'titles' => $titles,
'type' => (string)StringType::from($manga['subtype'])->toLowerCase()->upperCaseFirst(),
'type' => (string) StringType::from($manga['subtype'])->toLowerCase()->upperCaseFirst(),
'url' => 'https://kitsu.io/manga/' . $manga['slug'],
]),
'reading_status' => strtolower($item['status']),
'notes' => $item['notes'],
'rereading' => (bool)$item['reconsuming'],
'rereading' => (bool) $item['reconsuming'],
'reread' => $item['reconsumeCount'],
'user_rating' => $rating,
]);
@ -109,12 +107,11 @@ final class MangaListTransformer extends AbstractTransformer {
/**
* Untransform data to update the api
*
* @param array $item
* @return FormItem
* @param array $item
*/
public function untransform($item): FormItem
{
$rereading = array_key_exists('rereading', $item) && (bool)$item['rereading'];
$rereading = array_key_exists('rereading', $item) && (bool) $item['rereading'];
$map = FormItem::from([
'id' => $item['id'],
@ -122,14 +119,14 @@ final class MangaListTransformer extends AbstractTransformer {
'data' => FormItemData::from([
'status' => $item['status'],
'reconsuming' => $rereading,
'reconsumeCount' => (int)$item['reread_count'],
'reconsumeCount' => (int) $item['reread_count'],
'notes' => $item['notes'],
]),
]);
if (is_numeric($item['chapters_read']) && $item['chapters_read'] > 0)
{
$map['data']['progress'] = (int)$item['chapters_read'];
$map['data']['progress'] = (int) $item['chapters_read'];
}
if (is_numeric($item['new_rating']) && $item['new_rating'] > 0)
@ -140,4 +137,5 @@ final class MangaListTransformer extends AbstractTransformer {
return $map;
}
}
// End of MangaListTransformer.php
// End of MangaListTransformer.php

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
@ -23,30 +21,29 @@ use Aviat\Ion\Transformer\AbstractTransformer;
/**
* Transformer for manga description page
*/
final class MangaTransformer extends AbstractTransformer {
final class MangaTransformer extends AbstractTransformer
{
/**
* Convert raw api response to a more
* logical and workable structure
*
* @param array|object $item API library item
* @return MangaPage
* @param array|object $item API library item
*/
public function transform(array|object $item): MangaPage
{
$item = (array)$item;
$item = (array) $item;
$base = $item['data']['findMangaBySlug'] ?? $item['data']['findMangaById'] ?? $item['data']['randomMedia'];
$characters = [];
$links = [];
$staff = [];
$genres = array_map(fn ($genre) => $genre['title']['en'], $base['categories']['nodes']);
$genres = array_map(static fn ($genre) => $genre['title']['en'], $base['categories']['nodes']);
sort($genres);
$title = $base['titles']['canonical'];
$titles = Kitsu::getTitles($base['titles']);
$titles_more = Kitsu::filterLocalizedTitles($base['titles']);
if (count($base['characters']['nodes']) > 0)
if ((is_countable($base['characters']['nodes']) ? count($base['characters']['nodes']) : 0) > 0)
{
foreach ($base['characters']['nodes'] as $rawCharacter)
{
@ -57,11 +54,14 @@ final class MangaTransformer extends AbstractTransformer {
}
$details = $rawCharacter['character'];
$characters[$type][$details['id']] = [
'image' => $details['image']['original']['url'],
'name' => $details['names']['canonical'],
'slug' => $details['slug'],
];
if (array_key_exists($details['id'], $characters[$type]))
{
$characters[$type][$details['id']] = [
'image' => $details['image']['original']['url'],
'name' => $details['names']['canonical'],
'slug' => $details['slug'],
];
}
}
foreach (array_keys($characters) as $type)
@ -72,14 +72,14 @@ final class MangaTransformer extends AbstractTransformer {
}
else
{
uasort($characters[$type], fn($a, $b) => $a['name'] <=> $b['name']);
uasort($characters[$type], static fn ($a, $b) => $a['name'] <=> $b['name']);
}
}
krsort($characters);
}
if (count($base['staff']['nodes']) > 0)
if ((is_countable($base['staff']['nodes']) ? count($base['staff']['nodes']) : 0) > 0)
{
foreach ($base['staff']['nodes'] as $staffing)
{
@ -89,7 +89,7 @@ final class MangaTransformer extends AbstractTransformer {
// If this person object is so broken as to not have a proper image object,
// just skip it. No point in showing a role with nothing in it.
if ($person === null || $person['id'] === null || $person['image'] === null)
if ($person === NULL || $person['id'] === NULL || $person['image'] === NULL)
{
continue;
}
@ -106,13 +106,13 @@ final class MangaTransformer extends AbstractTransformer {
'image' => $person['image']['original']['url'],
];
usort($staff[$role], fn ($a, $b) => $a['name'] <=> $b['name']);
usort($staff[$role], static fn ($a, $b) => $a['name'] <=> $b['name']);
}
ksort($staff);
}
if (count($base['mappings']['nodes']) > 0)
if ((is_countable($base['mappings']['nodes']) ? count($base['mappings']['nodes']) : 0) > 0)
{
$links = Kitsu::mappingsToUrls($base['mappings']['nodes'], "https://kitsu.io/manga/{$base['slug']}");
}
@ -139,4 +139,4 @@ final class MangaTransformer extends AbstractTransformer {
return MangaPage::from($data);
}
}
}

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
@ -23,15 +21,11 @@ use Aviat\Ion\Transformer\AbstractTransformer;
/**
* Data transformation class for people pages
*/
final class PersonTransformer extends AbstractTransformer {
/**
* @param array|object $item
* @return Person
*/
final class PersonTransformer extends AbstractTransformer
{
public function transform(array|object $item): Person
{
$item = (array)$item;
$item = (array) $item;
$data = $item['data']['findPersonBySlug'] ?? [];
$canonicalName = $data['names']['localized'][$data['names']['canonical']]
?? array_shift($data['names']['localized']);
@ -49,6 +43,9 @@ final class PersonTransformer extends AbstractTransformer {
]);
}
/**
* @return array<string, array<int|string, array<int|string, array<int|string, array<int|string, mixed>>>>>
*/
protected function organizeData(array $data): array
{
$output = [
@ -59,13 +56,15 @@ final class PersonTransformer extends AbstractTransformer {
$characters = [];
$staff = [];
if (count($data['mediaStaff']['nodes']) > 0)
if ((is_countable($data['mediaStaff']['nodes']) ? count($data['mediaStaff']['nodes']) : 0) > 0)
{
$roles = array_unique(array_column($data['mediaStaff']['nodes'], 'role'));
foreach ($roles as $role)
{
$staff[$role] = [];
}
ksort($staff);
foreach ($data['mediaStaff']['nodes'] as $staffing)
@ -88,13 +87,13 @@ final class PersonTransformer extends AbstractTransformer {
'slug' => $media['slug'],
];
uasort($staff[$role][$type], fn ($a, $b) => $a['title'] <=> $b['title']);
uasort($staff[$role][$type], static fn ($a, $b) => $a['title'] <=> $b['title']);
}
$output['staff'] = $staff;
}
if (count($data['voices']['nodes']) > 0)
if ((is_countable($data['voices']['nodes']) ? count($data['voices']['nodes']) : 0) > 0)
{
foreach ($data['voices']['nodes'] as $voicing)
{
@ -128,7 +127,7 @@ final class PersonTransformer extends AbstractTransformer {
'canonicalName' => $character['names']['canonical'],
],
'media' => [
$media['id'] => $media
$media['id'] => $media,
],
];
}
@ -143,7 +142,7 @@ final class PersonTransformer extends AbstractTransformer {
// Sort the characters by name
uasort(
$characters[$role],
fn($a, $b) => $a['character']['canonicalName'] <=> $b['character']['canonicalName']
static fn ($a, $b) => $a['character']['canonicalName'] <=> $b['character']['canonicalName']
);
// Sort the media for the character
@ -151,7 +150,7 @@ final class PersonTransformer extends AbstractTransformer {
{
uasort(
$characters[$role][$charId]['media'],
fn ($a, $b) => $a['titles'][0] <=> $b['titles'][0]
static fn ($a, $b) => $a['titles'][0] <=> $b['titles'][0]
);
}
}
@ -163,4 +162,4 @@ final class PersonTransformer extends AbstractTransformer {
return $output;
}
}
}

View File

@ -6,18 +6,15 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\Kitsu;
use function Aviat\AnimeClient\getLocalImg;
use Aviat\AnimeClient\Types\User;
use Aviat\Ion\Transformer\AbstractTransformer;
@ -28,10 +25,11 @@ use Aviat\Ion\Transformer\AbstractTransformer;
* @param array|object $profileData
* @return User
*/
final class UserTransformer extends AbstractTransformer {
final class UserTransformer extends AbstractTransformer
{
public function transform(array|object $item): User
{
$item = (array)$item;
$item = (array) $item;
$base = $item['data']['findProfileBySlug'] ?? [];
$favorites = $base['favorites']['nodes'] ?? [];
$stats = $base['stats'] ?? [];
@ -56,8 +54,7 @@ final class UserTransformer extends AbstractTransformer {
/**
* Reorganize favorites data to be more useful
*
* @param array $rawFavorites
* @return array
* @return array<string, array<int|string, mixed>>
*/
private function organizeFavorites(array $rawFavorites): array
{
@ -72,6 +69,9 @@ final class UserTransformer extends AbstractTransformer {
return $output;
}
/**
* @return array<string, string>
*/
private function organizeStats(array $stats, array $data = []): array
{
$animeStats = [];
@ -106,4 +106,4 @@ final class UserTransformer extends AbstractTransformer {
return array_merge($animeStats, $mangaStats, $otherStats);
}
}
}

View File

@ -73,8 +73,10 @@ interface Media {
ageRatingGuide: String
"The average rating of this media amongst all Kitsu users"
averageRating: Float
"The rank of this media by rating"
averageRatingRank: Int
"A large banner image for this media"
bannerImage: Image!
bannerImage: Image
"A list of categories for this media"
categories(
"Returns the elements in the list that come after the specified cursor."
@ -133,10 +135,26 @@ interface Media {
): WikiSubmissionConnection!
"The time of the next release of this media"
nextRelease: ISO8601DateTime
"The countries in which the media was originally primarily produced"
originCountries: [String!]!
"The languages the media was originally produced in"
originLanguages: [String!]!
"The country in which the media was primarily produced"
originalLocale: String
originalLocale: String @deprecated(reason: "Replaced with originCountries and originLanguages")
"The poster image of this media"
posterImage: Image!
posterImage: Image
"All posts that tag this media."
posts(
"Returns the elements in the list that come after the specified cursor."
after: String,
"Returns the elements in the list that come before the specified cursor."
before: String,
"Returns the first _n_ elements from the list."
first: Int,
"Returns the last _n_ elements from the list."
last: Int,
sort: [PostSortOption]
): PostConnection!
"The companies which helped to produce this media"
productions(
"Returns the elements in the list that come after the specified cursor."
@ -198,6 +216,8 @@ interface Media {
type: String!
"The number of users with this in their library"
userCount: Int
"The rank of this media by popularity"
userCountRank: Int
}
"Media that is streamable."
@ -230,6 +250,8 @@ interface WithTimestamps {
updatedAt: ISO8601DateTime!
}
union AccountCreateErrorsUnion = ValidationError
"Objects which are Favoritable"
union FavoriteItemUnion = Anime | Character | Manga | Person
@ -274,6 +296,12 @@ type Account implements WithTimestamps {
updatedAt: ISO8601DateTime!
}
"Autogenerated return type of AccountCreate"
type AccountCreatePayload {
errors: [AccountCreateErrorsUnion!]
result: Account
}
type AccountMutations {
"Send a password reset email"
sendPasswordReset(
@ -294,8 +322,10 @@ type Anime implements Episodic & Media & WithTimestamps {
ageRatingGuide: String
"The average rating of this media amongst all Kitsu users"
averageRating: Float
"The rank of this media by rating"
averageRatingRank: Int
"A large banner image for this media"
bannerImage: Image!
bannerImage: Image
"A list of categories for this media"
categories(
"Returns the elements in the list that come after the specified cursor."
@ -371,10 +401,26 @@ type Anime implements Episodic & Media & WithTimestamps {
): WikiSubmissionConnection!
"The time of the next release of this media"
nextRelease: ISO8601DateTime
"The countries in which the media was originally primarily produced"
originCountries: [String!]!
"The languages the media was originally produced in"
originLanguages: [String!]!
"The country in which the media was primarily produced"
originalLocale: String
originalLocale: String @deprecated(reason: "Replaced with originCountries and originLanguages")
"The poster image of this media"
posterImage: Image!
posterImage: Image
"All posts that tag this media."
posts(
"Returns the elements in the list that come after the specified cursor."
after: String,
"Returns the elements in the list that come before the specified cursor."
before: String,
"Returns the first _n_ elements from the list."
first: Int,
"Returns the last _n_ elements from the list."
last: Int,
sort: [PostSortOption]
): PostConnection!
"The companies which helped to produce this media"
productions(
"Returns the elements in the list that come after the specified cursor."
@ -454,6 +500,8 @@ type Anime implements Episodic & Media & WithTimestamps {
updatedAt: ISO8601DateTime!
"The number of users with this in their library"
userCount: Int
"The rank of this media by popularity"
userCountRank: Int
"Video id for a trailer on YouTube"
youtubeTrailerVideoId: String
}
@ -598,6 +646,8 @@ type Chapter implements Unit & WithTimestamps {
"A brief summary or description of the unit"
description(locales: [String!]): Map!
id: ID!
"Number of pages in chapter."
length: Int
"The manga this chapter is in."
manga: Manga!
"The sequence number of this unit"
@ -906,7 +956,11 @@ type FranchiseEdge {
node: Franchise
}
type Generic implements Error {
type GenericDelete {
id: ID!
}
type GenericError implements Error {
"The error code."
code: String
"A description of the error"
@ -915,10 +969,6 @@ type Generic implements Error {
path: [String!]
}
type GenericDelete {
id: ID!
}
type Image {
"A blurhash-encoded version of this image"
blurhash: String
@ -1273,8 +1323,10 @@ type Manga implements Media & WithTimestamps {
ageRatingGuide: String
"The average rating of this media amongst all Kitsu users"
averageRating: Float
"The rank of this media by rating"
averageRatingRank: Int
"A large banner image for this media"
bannerImage: Image!
bannerImage: Image
"A list of categories for this media"
categories(
"Returns the elements in the list that come after the specified cursor."
@ -1350,10 +1402,26 @@ type Manga implements Media & WithTimestamps {
): WikiSubmissionConnection!
"The time of the next release of this media"
nextRelease: ISO8601DateTime
"The countries in which the media was originally primarily produced"
originCountries: [String!]!
"The languages the media was originally produced in"
originLanguages: [String!]!
"The country in which the media was primarily produced"
originalLocale: String
originalLocale: String @deprecated(reason: "Replaced with originCountries and originLanguages")
"The poster image of this media"
posterImage: Image!
posterImage: Image
"All posts that tag this media."
posts(
"Returns the elements in the list that come after the specified cursor."
after: String,
"Returns the elements in the list that come before the specified cursor."
before: String,
"Returns the first _n_ elements from the list."
first: Int,
"Returns the last _n_ elements from the list."
last: Int,
sort: [PostSortOption]
): PostConnection!
"The companies which helped to produce this media"
productions(
"Returns the elements in the list that come after the specified cursor."
@ -1418,6 +1486,8 @@ type Manga implements Media & WithTimestamps {
updatedAt: ISO8601DateTime!
"The number of users with this in their library"
userCount: Int
"The rank of this media by popularity"
userCountRank: Int
"The number of volumes in this manga."
volumeCount: Int
}
@ -1639,6 +1709,8 @@ type MediaReaction implements WithTimestamps {
"The author who wrote this reaction."
author: Profile!
createdAt: ISO8601DateTime!
"Whether you have liked this media reaction"
hasLiked: Boolean!
id: ID!
"The library entry related to this reaction."
libraryEntry: LibraryEntry!
@ -1718,6 +1790,8 @@ type MediaStaffEdge {
type Mutation {
account: AccountMutations!
"Create a new Kitsu account"
accountCreate(input: AccountCreateInput!): AccountCreatePayload
anime: AnimeMutations!
episode: EpisodeMutations!
libraryEntry: LibraryEntryMutations!
@ -1852,6 +1926,12 @@ type PostConnection {
totalCount: Int!
}
"Autogenerated return type of PostCreate"
type PostCreatePayload {
errors: [Error!]
post: Post
}
"An edge in a connection."
type PostEdge {
"A cursor for use in pagination."
@ -1867,6 +1947,11 @@ type PostLockPayload {
}
type PostMutations {
"Create a Post."
create(
"Create a Post"
input: PostCreateInput!
): PostCreatePayload
"Lock a Post."
lock(
"Lock a Post."
@ -2116,6 +2201,19 @@ type ProfileEdge {
node: Profile
}
"An external site that can be linked to a user."
type ProfileLinkSite implements WithTimestamps {
createdAt: ISO8601DateTime!
id: ID!
"Name of the external profile website."
name: String!
updatedAt: ISO8601DateTime!
"Regex pattern used to validate the profile link."
validateFind: String!
"Pattern to be replaced after validation."
validateReplace: String!
}
"The different types of user stats that we calculate."
type ProfileStats {
"The total amount of anime you have watched over your whole life."
@ -2185,6 +2283,8 @@ type Query {
findMangaById(id: ID!): Manga
"Find a single Manga by Slug"
findMangaBySlug(slug: String!): Manga
"Find a single Media by ID and Type"
findMediaByIdAndType(id: ID!, mediaType: MediaTypeEnum!): Media
"Find a single Person by ID"
findPersonById(id: ID!): Person
"Find a single Person by Slug"
@ -2471,7 +2571,7 @@ type Report implements WithTimestamps {
"The moderator who responded to this report"
moderator: Profile
"The entity that the report is related to"
naughty: ReportItemUnion!
naughty: ReportItemUnion
"The reason for why the report was made"
reason: ReportReasonEnum!
"The user who made this report"
@ -2573,6 +2673,8 @@ type SiteLink implements WithTimestamps {
author: Profile!
createdAt: ISO8601DateTime!
id: ID!
"The actual linked website."
site: ProfileLinkSite!
updatedAt: ISO8601DateTime!
"A fully qualified URL of the user profile on an external site."
url: String!
@ -2672,11 +2774,34 @@ type TitlesList {
"A list of additional, alternative, abbreviated, or unofficial titles"
alternatives: [String!]
"The official or de facto international title"
canonical: String
canonical: String!
"The locale code that identifies which title is used as the canonical title"
canonicalLocale: String
"The list of localized titles keyed by locale"
localized(locales: [String!]): Map!
"The original title of the media in the original language"
original: String
"The locale code that identifies which title is used as the original title"
originalLocale: String
"The title that best matches the user's preferred settings"
preferred: String!
"The original title, romanized into latin script"
romanized: String
"The locale code that identifies which title is used as the romanized title"
romanizedLocale: String
"The title translated into the user's locale"
translated: String
"The locale code that identifies which title is used as the translated title"
translatedLocale: String
}
type ValidationError implements Error {
"The error code."
code: String
"A description of the error"
message: String!
"Which input value this error came from"
path: [String!]
}
"The media video."
@ -2875,6 +3000,11 @@ enum EpisodeSortEnum {
UPDATED_AT
}
enum ExternalIdentityProviderEnum {
"Facebook identity"
FACEBOOK
}
enum FollowSortEnum {
CREATED_AT
FOLLOWING_FOLLOWED
@ -3029,9 +3159,9 @@ enum PostSortEnum {
enum ProTierEnum {
"Aozora Pro (only hides ads)"
AO_PRO @deprecated(reason : "No longer for sale")
AO_PRO @deprecated(reason: "No longer for sale")
"Aozora Pro+ (only hides ads)"
AO_PRO_PLUS @deprecated(reason : "No longer for sale")
AO_PRO_PLUS @deprecated(reason: "No longer for sale")
"Top tier of Kitsu Pro"
PATRON
"Basic tier of Kitsu Pro"
@ -3134,6 +3264,39 @@ enum WikiSubmissionStatusEnum {
REJECTED
}
"A date, expressed as an ISO8601 string"
scalar Date
"An ISO 8601-encoded date"
scalar ISO8601Date
"An ISO 8601-encoded datetime"
scalar ISO8601DateTime
"Represents untyped JSON"
scalar JSON
"A loose key-value map in GraphQL"
scalar Map
scalar Upload
input AccountCreateInput {
"The email address to reset the password for"
email: String!
"An external identity to associate with the account on creation"
externalIdentity: AccountExternalIdentityInput
"The name of the user"
name: String!
"The password for the user"
password: String!
}
input AccountExternalIdentityInput {
id: String!
provider: ExternalIdentityProviderEnum!
}
input AnimeCreateInput {
ageRating: AgeRatingEnum
ageRatingGuide: String
@ -3324,6 +3487,16 @@ input MediaReactionVoteSortOption {
on: MediaReactionVoteSortEnum!
}
input PostCreateInput {
content: String!
isNsfw: Boolean = false
isSpoiler: Boolean = false
mediaId: ID
mediaType: MediaTypeEnum
spoiledUnitId: ID
spoiledUnitType: String
}
input PostLikeSortOption {
direction: SortDirection!
on: PostLikeSortEnum!
@ -3373,21 +3546,3 @@ input WikiSubmissionUpdateDraftInput {
id: ID!
notes: String
}
"A date, expressed as an ISO8601 string"
scalar Date
"An ISO 8601-encoded date"
scalar ISO8601Date
"An ISO 8601-encoded datetime"
scalar ISO8601DateTime
"Represents untyped JSON"
scalar JSON
"A loose key-value map in GraphQL"
scalar Map
scalar Upload

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Mapping;
@ -23,54 +21,50 @@ use Aviat\Ion\Enum;
* Anime watching status mappings, among Kitsu, MAL, Page titles
* and url route segments
*/
final class AnimeWatchingStatus extends Enum {
final class AnimeWatchingStatus extends Enum
{
public const ANILIST_TO_KITSU = [
Anilist::WATCHING => Kitsu::WATCHING,
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,
];
public const KITSU_TO_ANILIST = [
Kitsu::WATCHING => Anilist::WATCHING,
Kitsu::PLAN_TO_WATCH => Anilist::PLAN_TO_WATCH,
Kitsu::COMPLETED => Anilist::COMPLETED,
Kitsu::ON_HOLD => Anilist::ON_HOLD,
Kitsu::DROPPED => Anilist::DROPPED
Kitsu::DROPPED => Anilist::DROPPED,
];
public const KITSU_TO_TITLE = [
Kitsu::WATCHING => Title::WATCHING,
Kitsu::PLAN_TO_WATCH => Title::PLAN_TO_WATCH,
Kitsu::ON_HOLD => Title::ON_HOLD,
Kitsu::DROPPED => Title::DROPPED,
Kitsu::COMPLETED => Title::COMPLETED
Kitsu::COMPLETED => Title::COMPLETED,
];
public const ROUTE_TO_KITSU = [
Route::WATCHING => Kitsu::WATCHING,
Route::PLAN_TO_WATCH => Kitsu::PLAN_TO_WATCH,
Route::ON_HOLD => Kitsu::ON_HOLD,
Route::DROPPED => Kitsu::DROPPED,
Route::COMPLETED => Kitsu::COMPLETED
Route::COMPLETED => Kitsu::COMPLETED,
];
public const ROUTE_TO_TITLE = [
Route::ALL => Title::ALL,
Route::WATCHING => Title::WATCHING,
Route::PLAN_TO_WATCH => Title::PLAN_TO_WATCH,
Route::ON_HOLD => Title::ON_HOLD,
Route::DROPPED => Title::DROPPED,
Route::COMPLETED => Title::COMPLETED
Route::COMPLETED => Title::COMPLETED,
];
public const TITLE_TO_ROUTE = [
Title::ALL => Route::ALL,
Title::WATCHING => Route::WATCHING,
Title::PLAN_TO_WATCH => Route::PLAN_TO_WATCH,
Title::ON_HOLD => Route::ON_HOLD,
Title::DROPPED => Route::DROPPED,
Title::COMPLETED => Route::COMPLETED
Title::COMPLETED => Route::COMPLETED,
];
}

View File

@ -6,40 +6,37 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Mapping;
use Aviat\AnimeClient\API\Enum\MangaReadingStatus\{Anilist, Kitsu, Title, Route};
use Aviat\AnimeClient\API\Enum\MangaReadingStatus\{Anilist, Kitsu, Route, Title};
use Aviat\Ion\Enum;
/**
* Manga reading status mappings, among Kitsu, MAL, Page titles
* and url route segments
*/
final class MangaReadingStatus extends Enum {
final class MangaReadingStatus extends Enum
{
public const ANILIST_TO_KITSU = [
Anilist::READING => Kitsu::READING,
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,
];
public const KITSU_TO_ANILIST = [
Kitsu::READING => Anilist::READING,
Kitsu::PLAN_TO_READ => Anilist::PLAN_TO_READ,
Kitsu::COMPLETED => Anilist::COMPLETED,
Kitsu::ON_HOLD => Anilist::ON_HOLD,
Kitsu::DROPPED => Anilist::DROPPED
Kitsu::DROPPED => Anilist::DROPPED,
];
public const KITSU_TO_TITLE = [
Kitsu::READING => Title::READING,
Kitsu::PLAN_TO_READ => Title::PLAN_TO_READ,
@ -47,15 +44,13 @@ final class MangaReadingStatus extends Enum {
Kitsu::ON_HOLD => Title::ON_HOLD,
Kitsu::DROPPED => Title::DROPPED,
];
public const ROUTE_TO_KITSU = [
public const ROUTE_TO_KITSU = [
Route::PLAN_TO_READ => Kitsu::PLAN_TO_READ,
Route::READING => Kitsu::READING,
Route::COMPLETED => Kitsu::COMPLETED,
Route::DROPPED => Kitsu::DROPPED,
Route::ON_HOLD => Kitsu::ON_HOLD,
];
public const ROUTE_TO_TITLE = [
Route::ALL => Title::ALL,
Route::PLAN_TO_READ => Title::PLAN_TO_READ,
@ -64,7 +59,6 @@ final class MangaReadingStatus extends Enum {
Route::DROPPED => Title::DROPPED,
Route::ON_HOLD => Title::ON_HOLD,
];
public const TITLE_TO_KITSU = [
Title::PLAN_TO_READ => Kitsu::PLAN_TO_READ,
Title::READING => Kitsu::READING,
@ -72,4 +66,4 @@ final class MangaReadingStatus extends Enum {
Title::DROPPED => Kitsu::DROPPED,
Title::ON_HOLD => Kitsu::ON_HOLD,
];
}
}

View File

@ -6,71 +6,66 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API;
use Amp\Http\Client\Request;
use Generator;
use Throwable;
use function Amp\call;
use function Amp\Promise\{all, wait};
use function Aviat\AnimeClient\getApiClient;
use Throwable;
/**
* Class to simplify making and validating simultaneous requests
*/
final class ParallelAPIRequest {
final class ParallelAPIRequest
{
/**
* Set of requests to make in parallel
*
* @var array
*/
private array $requests = [];
/**
* Add a request
*
* @param string|Request $request
* @param string|int|null $key
* @return self
*/
public function addRequest(string|Request $request, string|int|null $key = NULL): self
public function addRequest(string|Request $request, string|int|NULL $key = NULL): self
{
if ($key !== NULL)
{
$this->requests[$key] = $request;
return $this;
}
$this->requests[] = $request;
return $this;
}
/**
* Add multiple requests
*
* @param string[]|Request[] $requests
* @return self
* @param Request[]|string[] $requests
*/
public function addRequests(array $requests): self
{
array_walk($requests, [$this, 'addRequest']);
return $this;
}
/**
* Make the requests, and return the body for each
*
* @return array
* @throws Throwable
* @return mixed[]
*/
public function makeRequests(): array
{
@ -80,7 +75,7 @@ final class ParallelAPIRequest {
foreach ($this->requests as $key => $url)
{
$promises[$key] = call(static function () use ($client, $url) {
$promises[$key] = call(static function () use ($client, $url): Generator {
$response = yield $client->request($url);
return yield $response->getBody()->buffer();
});
@ -92,8 +87,8 @@ final class ParallelAPIRequest {
/**
* Make the requests and return the response objects
*
* @return array
* @throws Throwable
* @return mixed[]
*/
public function getResponses(): array
{
@ -103,9 +98,9 @@ final class ParallelAPIRequest {
foreach ($this->requests as $key => $url)
{
$promises[$key] = call(fn () => yield $client->request($url));
$promises[$key] = call(static fn () => yield $client->request($url));
}
return wait(all($promises));
}
}
}

View File

@ -6,33 +6,31 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient;
use Aviat\AnimeClient\API\Enum\{
AnimeWatchingStatus\Kitsu as KAWS,
MangaReadingStatus\Kitsu as KMRS
};
use Aviat\AnimeClient\API\Enum\{
AnimeWatchingStatus\Anilist as AnimeWatchingStatus,
MangaReadingStatus\Anilist as MangaReadingStatus
};
use Aviat\AnimeClient\API\Enum\{
AnimeWatchingStatus\Kitsu as KAWS,
MangaReadingStatus\Kitsu as KMRS
};
/**
* Constants and mappings for the Anilist API
*/
final class Anilist {
final class Anilist
{
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';
public const KITSU_ANILIST_WATCHING_STATUS_MAP = [
KAWS::WATCHING => AnimeWatchingStatus::WATCHING,
KAWS::COMPLETED => AnimeWatchingStatus::COMPLETED,
@ -40,7 +38,6 @@ final class Anilist {
KAWS::DROPPED => AnimeWatchingStatus::DROPPED,
KAWS::PLAN_TO_WATCH => AnimeWatchingStatus::PLAN_TO_WATCH,
];
public const ANILIST_KITSU_WATCHING_STATUS_MAP = [
AnimeWatchingStatus::WATCHING => KAWS::WATCHING,
AnimeWatchingStatus::COMPLETED => KAWS::COMPLETED,
@ -48,7 +45,6 @@ final class Anilist {
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,
@ -56,7 +52,6 @@ final class Anilist {
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,
@ -64,4 +59,4 @@ final class Anilist {
MangaReadingStatus::DROPPED => KMRS::DROPPED,
MangaReadingStatus::PLAN_TO_READ => KMRS::PLAN_TO_READ,
];
}
}

View File

@ -6,41 +6,33 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient;
use Aviat\Ion\ImageBuilder;
use Amp\Http\Client\{HttpClient, HttpClientBuilder, Request, Response};
use Aviat\Ion\{ConfigInterface, ImageBuilder};
use Psr\SimpleCache\CacheInterface;
use Amp\Http\Client\Request;
use Amp\Http\Client\Response;
use Amp\Http\Client\HttpClient;
use Amp\Http\Client\HttpClientBuilder;
use Aviat\Ion\ConfigInterface;
use Yosymfony\Toml\{Toml, TomlBuilder};
use Throwable;
use Yosymfony\Toml\{Toml, TomlBuilder};
use function Amp\Promise\wait;
use function Aviat\Ion\_dir;
// ----------------------------------------------------------------------------
//! TOML Functions
// ----------------------------------------------------------------------------
/**
* Load configuration options from .toml files
*
* @codeCoverageIgnore
* @param string $path - Path to load config
* @return array
*/
function loadConfig(string $path): array
{
@ -64,7 +56,7 @@ function loadConfig(string $path): array
if ($key === 'config')
{
foreach($config as $name => $value)
foreach ($config as $name => $value)
{
$output[$name] = $value;
}
@ -82,8 +74,6 @@ function loadConfig(string $path): array
* Load config from one specific TOML file
*
* @codeCoverageIgnore
* @param string $filename
* @return array
*/
function loadTomlFile(string $filename): array
{
@ -100,10 +90,10 @@ function _iterateToml(TomlBuilder $builder, iterable $data, mixed $parentKey = N
continue;
}
if (is_scalar($value) || isSequentialArray($value))
{
$builder->addValue($key, $value);
continue;
}
@ -111,10 +101,7 @@ function _iterateToml(TomlBuilder $builder, iterable $data, mixed $parentKey = N
? "{$parentKey}.{$key}"
: $key;
if ( ! isSequentialArray($value))
{
$builder->addTable($newKey);
}
$builder->addTable($newKey);
_iterateToml($builder, $value, $newKey);
}
@ -122,9 +109,6 @@ function _iterateToml(TomlBuilder $builder, iterable $data, mixed $parentKey = N
/**
* Serialize config data into a Toml file
*
* @param iterable $data
* @return string
*/
function arrayToToml(iterable $data): string
{
@ -137,9 +121,6 @@ function arrayToToml(iterable $data): string
/**
* Serialize toml back to an array
*
* @param string $toml
* @return array
*/
function tomlToArray(string $toml): array
{
@ -156,8 +137,6 @@ if ( ! function_exists('array_is_list'))
* Polyfill for PHP 8
*
* @see https://www.php.net/manual/en/function.array-is-list
* @param array $a
* @return bool
*/
function array_is_list(array $a): bool
{
@ -167,25 +146,14 @@ if ( ! function_exists('array_is_list'))
/**
* Is the array sequential, not associative?
*
* @param mixed $array
* @return bool
*/
function isSequentialArray(mixed $array): bool
{
if ( ! is_array($array))
{
return FALSE;
}
return array_is_list($array);
return is_array($array) && array_is_list($array);
}
/**
* Check that folder permissions are correct for proper operation
*
* @param ConfigInterface $config
* @return array
*/
function checkFolderPermissions(ConfigInterface $config): array
{
@ -206,6 +174,7 @@ function checkFolderPermissions(ConfigInterface $config): array
if ( ! is_dir($actual))
{
$errors['missing'][] = $pretty;
continue;
}
@ -224,10 +193,8 @@ function checkFolderPermissions(ConfigInterface $config): array
/**
* Get an API Client, with better defaults
*
* @return HttpClient
*/
function getApiClient (): HttpClient
function getApiClient(): HttpClient
{
static $client;
@ -242,11 +209,9 @@ function getApiClient (): HttpClient
/**
* Simplify making a request with Http\Client
*
* @param string|Request $request
* @return Response
* @throws Throwable
*/
function getResponse (Request|string $request): Response
function getResponse(Request|string $request): Response
{
$client = getApiClient();
@ -260,12 +225,8 @@ function getResponse (Request|string $request): Response
/**
* Generate the path for the cached image from the original image
*
* @param string $kitsuUrl
* @param bool $webp
* @return string
*/
function getLocalImg (string $kitsuUrl, bool $webp = TRUE): string
function getLocalImg(string $kitsuUrl, bool $webp = TRUE): string
{
if (empty($kitsuUrl) || ( ! is_string($kitsuUrl)))
{
@ -297,13 +258,8 @@ function getLocalImg (string $kitsuUrl, bool $webp = TRUE): string
* Create a transparent placeholder image
*
* @codeCoverageIgnore
* @param string $path
* @param int $width
* @param int $height
* @param string $text
* @return bool
*/
function createPlaceholderImage (string $path, int $width = 200, int $height = 200, string $text = 'Image Unavailable'): bool
function createPlaceholderImage(string $path, int $width = 200, int $height = 200, string $text = 'Image Unavailable'): bool
{
$img = ImageBuilder::new($width, $height)
->enableAlphaBlending(TRUE)
@ -322,22 +278,16 @@ function createPlaceholderImage (string $path, int $width = 200, int $height = 2
/**
* Check that there is a value for at least one item in a collection with the specified key
*
* @param array $search
* @param string $key
* @return bool
*/
function colNotEmpty(array $search, string $key): bool
{
$items = array_filter(array_column($search, $key), static fn ($x) => ( ! empty($x)));
return count($items) > 0;
return $items !== [];
}
/**
* Clear the cache, but save user auth data
*
* @param CacheInterface $cache
* @return bool
*/
function clearCache(CacheInterface $cache): bool
{
@ -349,10 +299,11 @@ function clearCache(CacheInterface $cache): bool
Kitsu::AUTH_TOKEN_REFRESH_CACHE_KEY,
]);
$userData = array_filter((array)$userData, static fn ($value) => $value !== NULL);
$userData = array_filter((array) $userData, static fn ($value) => $value !== NULL);
$cleared = $cache->clear();
$saved = ( ! empty($userData)) ? $cache->setMultiple($userData) : TRUE;
$saved = (empty($userData)) ? TRUE : $cache->setMultiple($userData);
return $cleared && $saved;
}
@ -361,9 +312,6 @@ function clearCache(CacheInterface $cache): bool
* Render a PHP code template as a string
*
* @codeCoverageIgnore
* @param string $path
* @param array $data
* @return string
*/
function renderTemplate(string $path, array $data): string
{
@ -371,5 +319,6 @@ function renderTemplate(string $path, array $data): string
extract($data, EXTR_OVERWRITE);
include $path;
$rawOutput = ob_get_clean();
return (is_string($rawOutput)) ? $rawOutput : '';
}
}

View File

@ -6,53 +6,45 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Command;
use Monolog\Formatter\JsonFormatter;
use function Aviat\Ion\_dir;
use const Aviat\AnimeClient\SRC_DIR;
use function Aviat\AnimeClient\loadConfig;
use function Aviat\AnimeClient\loadTomlFile;
use Aura\Router\RouterContainer;
use Aura\Session\SessionFactory;
use Aviat\AnimeClient\{Model, UrlGenerator, Util};
use Aviat\AnimeClient\API\{Anilist, CacheTrait, Kitsu};
use Aviat\AnimeClient\{Model, UrlGenerator, Util};
use Aviat\Banker\Teller;
use Aviat\Ion\Config;
use Aviat\Ion\Di\{Container, ContainerInterface, ContainerAware};
use ConsoleKit\{Colors, Command, ConsoleException};
use Aviat\Ion\Di\{Container, ContainerAware, ContainerInterface};
use ConsoleKit\Widgets\Box;
use ConsoleKit\{Colors, Command, ConsoleException};
use Laminas\Diactoros\{Response, ServerRequestFactory};
use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Logger;
use function Aviat\AnimeClient\{loadConfig, loadTomlFile};
use function Aviat\Ion\_dir;
use const Aviat\AnimeClient\SRC_DIR;
/**
* Base class for console command setup
*/
abstract class BaseCommand extends Command {
abstract class BaseCommand extends Command
{
use CacheTrait;
use ContainerAware;
/**
* Echo text in a box
*
* @param string|array $message
* @param string|int|null $fgColor
* @param string|int|null $bgColor
* @return void
*/
public function echoBox(string|array $message, string|int|null $fgColor = NULL, string|int|null $bgColor = NULL): void
public function echoBox(string|array $message, string|int|NULL $fgColor = NULL, string|int|NULL $bgColor = NULL): void
{
if (is_array($message))
{
@ -61,11 +53,12 @@ abstract class BaseCommand extends Command {
if ($fgColor !== NULL)
{
$fgColor = (int)$fgColor;
$fgColor = (int) $fgColor;
}
if ($bgColor !== NULL)
{
$bgColor = (int)$bgColor;
$bgColor = (int) $bgColor;
}
// Colorize the CLI output
@ -118,15 +111,13 @@ abstract class BaseCommand extends Command {
/**
* Setup the Di container
*
* @return Containerinterface
*/
public function setupContainer(): ContainerInterface
{
$APP_DIR = _dir(dirname(dirname(SRC_DIR)), 'app');
$APP_DIR = _dir(dirname(SRC_DIR, 2), 'app');
$APPCONF_DIR = _dir($APP_DIR, 'appConf');
$CONF_DIR = _dir($APP_DIR, 'config');
$baseConfig = require _dir($APPCONF_DIR, 'base_config.php');
$baseConfig = require _dir($APPCONF_DIR, 'base_config.php');
$config = loadConfig($CONF_DIR);
@ -140,15 +131,16 @@ abstract class BaseCommand extends Command {
return $this->_di($configArray, $APP_DIR);
}
private function _line(string $message, int|string|null $fgColor = NULL, int|string|null $bgColor = NULL): void
private function _line(string $message, int|string|NULL $fgColor = NULL, int|string|NULL $bgColor = NULL): void
{
if ($fgColor !== NULL)
{
$fgColor = (int)$fgColor;
$fgColor = (int) $fgColor;
}
if ($bgColor !== NULL)
{
$bgColor = (int)$bgColor;
$bgColor = (int) $bgColor;
}
// Colorize the CLI output
@ -168,12 +160,13 @@ abstract class BaseCommand extends Command {
$appLogger = new Logger('animeclient');
$appLogger->pushHandler(new RotatingFileHandler($APP_DIR . '/logs/app-cli.log', 2, Logger::WARNING));
$container->setLogger($appLogger);
foreach (['kitsu-request', 'anilist-request', 'anilist-request-cli', 'kitsu-request-cli'] as $channel)
{
$logger = new Logger($channel);
$handler = new RotatingFileHandler( "{$APP_DIR}/logs/{$channel}.log", 2, Logger::WARNING);
$handler = new RotatingFileHandler("{$APP_DIR}/logs/{$channel}.log", 2, Logger::WARNING);
$handler->setFormatter(new JsonFormatter());
$logger->pushHandler($handler);
@ -181,33 +174,34 @@ abstract class BaseCommand extends Command {
}
// Create Config Object
$container->set('config', fn () => new Config($configArray));
$container->set('config', static fn () => new Config($configArray));
// Create Cache Object
$container->set('cache', static function($container) {
$container->set('cache', static function ($container): Teller {
$logger = $container->getLogger();
$config = $container->get('config')->get('cache');
return new Teller($config, $logger);
});
// Create Aura Router Object
$container->set('aura-router', fn () => new RouterContainer);
$container->set('aura-router', static fn () => new RouterContainer());
// Create Request/Response Objects
$container->set('request', fn () => ServerRequestFactory::fromGlobals(
$container->set('request', static fn () => ServerRequestFactory::fromGlobals(
$GLOBALS['_SERVER'],
$_GET,
$_POST,
$_COOKIE,
$_FILES
));
$container->set('response', fn () => new Response);
$container->set('response', static fn () => new Response());
// Create session Object
$container->set('session', fn () => (new SessionFactory())->newInstance($_COOKIE));
$container->set('session', static fn () => (new SessionFactory())->newInstance($_COOKIE));
// Models
$container->set('kitsu-model', static function($container): Kitsu\Model {
$container->set('kitsu-model', static function ($container): Kitsu\Model {
$requestBuilder = new Kitsu\RequestBuilder($container);
$requestBuilder->setLogger($container->getLogger('kitsu-request'));
@ -221,6 +215,7 @@ abstract class BaseCommand extends Command {
$cache = $container->get('cache');
$model->setCache($cache);
return $model;
});
$container->set('anilist-model', static function ($container): Anilist\Model {
@ -237,18 +232,19 @@ abstract class BaseCommand extends Command {
return $model;
});
$container->set('settings-model', static function($container): Model\Settings {
$model = new Model\Settings($container->get('config'));
$container->set('settings-model', static function ($container): Model\Settings {
$model = new Model\Settings($container->get('config'));
$model->setContainer($container);
return $model;
});
$container->set('auth', fn ($container) => new Kitsu\Auth($container));
$container->set('auth', static fn ($container) => new Kitsu\Auth($container));
$container->set('url-generator', fn ($container) => new UrlGenerator($container));
$container->set('url-generator', static fn ($container) => new UrlGenerator($container));
$container->set('util', fn ($container) => new Util($container));
$container->set('util', static fn ($container) => new Util($container));
return $container;
}
}
}

View File

@ -6,32 +6,27 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Command;
use Aviat\Ion\Di\Exception\ContainerException;
use Aviat\Ion\Di\Exception\NotFoundException;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
use function Aviat\AnimeClient\clearCache;
/**
* Clears the API Cache
*/
final class CacheClear extends BaseCommand {
final class CacheClear extends BaseCommand
{
/**
* Clear the API cache
*
* @param array $args
* @param array $options
* @throws ContainerException
* @throws NotFoundException
* @return void
*/
public function execute(array $args, array $options = []): void
{

View File

@ -6,32 +6,27 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Command;
use Aviat\Ion\Di\Exception\ContainerException;
use Aviat\Ion\Di\Exception\NotFoundException;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
use function Aviat\AnimeClient\clearCache;
/**
* Clears the API Cache
*/
final class CachePrime extends BaseCommand {
final class CachePrime extends BaseCommand
{
/**
* Clear, then prime the API cache
*
* @param array $args
* @param array $options
* @throws ContainerException
* @throws NotFoundException
* @return void
*/
public function execute(array $args, array $options = []): void
{
@ -42,6 +37,7 @@ final class CachePrime extends BaseCommand {
if ( ! $cleared)
{
$this->echoErrorBox('Failed to clear cache.');
return;
}

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Command;
@ -19,8 +17,8 @@ namespace Aviat\AnimeClient\Command;
/**
* Clears out image cache directories
*/
class ClearThumbnails extends BaseCommand {
class ClearThumbnails extends BaseCommand
{
public function execute(array $args, array $options = []): void
{
$this->clearThumbs();
@ -50,10 +48,10 @@ class ClearThumbnails extends BaseCommand {
'people/*.webp',
];
foreach($paths as $path)
foreach ($paths as $path)
{
$cmd = "find {$imgDir} -path \"*/{$path}\" | xargs rm -f";
exec($cmd);
}
}
}
}

View File

@ -6,74 +6,64 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Command;
use Aviat\Ion\JsonException;
use ConsoleKit\Widgets;
use Aviat\AnimeClient\API\Anilist;
use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus};
use Aviat\AnimeClient\API\{
Anilist\MissingIdException,
ParallelAPIRequest
};
use Aviat\AnimeClient\API;
use Aviat\AnimeClient\API\Anilist;
use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus};
use Aviat\AnimeClient\Enum;
use Aviat\AnimeClient\Enum\{MediaType, SyncAction};
use Aviat\AnimeClient\Types\FormItem;
use Aviat\Ion\Di\Exception\ContainerException;
use Aviat\Ion\Di\Exception\NotFoundException;
use Aviat\Ion\Json;
use Aviat\AnimeClient\{API, Enum};
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
use Aviat\Ion\{Json, JsonException};
use ConsoleKit\Widgets;
use DateTime;
use Throwable;
use function in_array;
/**
* Syncs list data between Anilist and Kitsu
*/
final class SyncLists extends BaseCommand {
final class SyncLists extends BaseCommand
{
protected const KITSU_GREATER = 1;
protected const ANILIST_GREATER = -1;
protected const SAME = 0;
/**
* Model for making requests to Anilist API
* @var Anilist\Model
*/
private Anilist\Model $anilistModel;
/**
* Model for making requests to Kitsu API
* @var API\Kitsu\Model
*/
private API\Kitsu\Model $kitsuModel;
/**
* Does the Kitsu API have valid authentication?
* @var bool
*/
private bool $isKitsuAuthenticated = FALSE;
/**
* Sync Kitsu <=> Anilist
*
* @param array $args
* @param array $options
* @throws ContainerException
* @throws NotFoundException
* @throws Throwable
*/
public function execute(array $args, array $options = []): void
{
$this->init();
$canRun = $this->init();
if ( ! $canRun)
{
return;
}
foreach ([MediaType::MANGA, MediaType::ANIME] as $type)
{
@ -96,7 +86,7 @@ final class SyncLists extends BaseCommand {
* @throws ContainerException
* @throws NotFoundException
*/
protected function init(): void
protected function init(): bool
{
$this->setContainer($this->setupContainer());
$this->setCache($this->container->get('cache'));
@ -108,28 +98,32 @@ final class SyncLists extends BaseCommand {
if ( ! $anilistEnabled)
{
$this->echoErrorBox('Anlist API is not enabled. Can not sync.');
exit();
return false;
}
// Authentication is required to update Kitsu
$this->isKitsuAuthenticated = $this->container->get('auth')->isAuthenticated();
if ( ! $this->isKitsuAuthenticated)
$isKitsuAuthenticated = $this->container->get('auth')->isAuthenticated();
if ( !$isKitsuAuthenticated)
{
$this->echoWarningBox('Kitsu is not authenticated. Kitsu list can not be updated.');
$this->echoErrorBox('Kitsu is not authenticated. Kitsu list can not be updated.');
return false;
}
$this->anilistModel = $this->container->get('anilist-model');
$this->kitsuModel = $this->container->get('kitsu-model');
return true;
}
/**
* Get and display the count of items for each API
*
* @param string $type
*/
protected function fetchCount(string $type): void
{
$this->echo('Fetching List Counts');
// This pulls too much data from Anilist, so skipping this step should result
// in fewer instances of API throttling
/* $this->echo('Fetching List Counts');
$progress = new Widgets\ProgressBar($this->getConsole(), 2, 50, FALSE);
$displayLines = [];
@ -144,18 +138,17 @@ final class SyncLists extends BaseCommand {
$this->clearLine();
$this->echoBox($displayLines);
$this->echoBox($displayLines); */
}
/**
* Get the list data
*
* @param string $type
* @return array
* @return array<string, mixed[]>
*/
protected function fetch(string $type): array
{
$this->echo('Fetching List Data');
$this->echo("Fetching $type List Data");
$progress = new Widgets\ProgressBar($this->getConsole(), 2, 50, FALSE);
$anilist = $this->fetchAnilist($type);
@ -175,9 +168,7 @@ final class SyncLists extends BaseCommand {
/**
* Normalize the list data for comparison
*
* @param string $type
* @param array $data
* @return array
* @return array<string, mixed[]>
*/
protected function transform(string $type, array $data): array
{
@ -201,9 +192,7 @@ final class SyncLists extends BaseCommand {
/**
* Compare the lists data
*
* @param string $type
* @param array $data
* @return array
* @return array<string, mixed[]>
*/
protected function compare(string $type, array $data): array
{
@ -215,45 +204,36 @@ final class SyncLists extends BaseCommand {
/**
* Updated outdated list items
*
* @param string $type
* @param array $data
* @throws Throwable
*/
protected function update(string $type, array $data): void
{
if ( ! empty($data['addToAnilist']))
{
$count = count($data['addToAnilist']);
$count = is_countable($data['addToAnilist']) ? count($data['addToAnilist']) : 0;
$this->echoBox("Adding {$count} missing {$type} list items to Anilist");
$this->updateAnilistListItems($data['addToAnilist'], SyncAction::CREATE, $type);
}
if ( ! empty($data['updateAnilist']))
{
$count = count($data['updateAnilist']);
$count = is_countable($data['updateAnilist']) ? count($data['updateAnilist']) : 0;
$this->echoBox("Updating {$count} outdated Anilist {$type} list items");
$this->updateAnilistListItems($data['updateAnilist'], SyncAction::UPDATE, $type);
}
if ($this->isKitsuAuthenticated)
if ( ! empty($data['addToKitsu']))
{
if ( ! empty($data['addToKitsu']))
{
$count = count($data['addToKitsu']);
$this->echoBox("Adding {$count} missing {$type} list items to Kitsu");
$this->updateKitsuListItems($data['addToKitsu'], SyncAction::CREATE, $type);
}
if ( ! empty($data['updateKitsu']))
{
$count = count($data['updateKitsu']);
$this->echoBox("Updating {$count} outdated Kitsu {$type} list items");
$this->updateKitsuListItems($data['updateKitsu'], SyncAction::UPDATE, $type);
}
$count = is_countable($data['addToKitsu']) ? count($data['addToKitsu']) : 0;
$this->echoBox("Adding {$count} missing {$type} list items to Kitsu");
$this->updateKitsuListItems($data['addToKitsu'], SyncAction::CREATE, $type);
}
else
if ( ! empty($data['updateKitsu']))
{
$this->echoErrorBox('Kitsu is not authenticated, so lists can not be updated');
$count = is_countable($data['updateKitsu']) ? count($data['updateKitsu']) : 0;
$this->echoBox("Updating {$count} outdated Kitsu {$type} list items");
$this->updateKitsuListItems($data['updateKitsu'], SyncAction::UPDATE, $type);
}
}
@ -273,12 +253,15 @@ final class SyncLists extends BaseCommand {
foreach ($list['data']['MediaListCollection']['lists'] as $subList)
{
$count += array_reduce($subList, fn ($carry, $item) => $carry + count(array_values($item)), 0);
$count += array_reduce($subList, static fn ($carry, $item) => $carry + count(array_values($item)), 0);
}
return $count;
}
/**
* @return mixed[]
*/
private function fetchAnilist(string $type): array
{
static $list = [
@ -296,7 +279,8 @@ final class SyncLists extends BaseCommand {
catch (JsonException)
{
$this->echoErrorBox('Anlist API exception. Can not sync.');
die();
exit();
}
}
@ -310,6 +294,9 @@ final class SyncLists extends BaseCommand {
return $this->kitsuModel->{"get{$uType}ListCount"}() ?? 0;
}
/**
* @return mixed[]
*/
private function fetchKitsu(string $type): array
{
return $this->kitsuModel->getSyncList($type);
@ -318,7 +305,9 @@ final class SyncLists extends BaseCommand {
// ------------------------------------------------------------------------
// Transform Helpers
// ------------------------------------------------------------------------
/**
* @return mixed[]
*/
private function transformKitsu(string $type, array $data): array
{
if (empty($data))
@ -328,13 +317,14 @@ final class SyncLists extends BaseCommand {
$output = [];
foreach($data as $listItem)
foreach ($data as $listItem)
{
// If there's no mapping, we can't sync, so continue
if ( ! is_array($listItem['media']['mappings']['nodes']))
{
continue;
}
$malId = NULL;
foreach ($listItem['media']['mappings']['nodes'] as $mapping)
@ -368,18 +358,21 @@ final class SyncLists extends BaseCommand {
'reconsuming' => $listItem['reconsuming'],
'status' => strtolower($listItem['status']),
'updatedAt' => $listItem['progressedAt'],
]
],
];
}
return $output;
}
/**
* @return array<int|string, mixed>
*/
private function transformAnilist(string $type, array $data): array
{
$uType = ucfirst($type);
$className = "\\Aviat\\AnimeClient\\API\\Anilist\\Transformer\\{$uType}ListTransformer";
$transformer = new $className;
$transformer = new $className();
$firstTransformed = [];
@ -392,6 +385,7 @@ final class SyncLists extends BaseCommand {
// Key the array by mal_id
$output = [];
foreach ($transformed as $item)
{
$output[$item['mal_id']] = $item->toArray();
@ -403,7 +397,9 @@ final class SyncLists extends BaseCommand {
// ------------------------------------------------------------------------
// Compare Helpers
// ------------------------------------------------------------------------
/**
* @return array<string, mixed[]>
*/
private function compareLists(string $type, array $anilistList, array $kitsuList): array
{
$itemsToAddToAnilist = [];
@ -413,10 +409,10 @@ final class SyncLists extends BaseCommand {
$malIds = array_keys($anilistList);
$kitsuMalIds = array_map('intval', array_column($kitsuList, 'malId'));
$missingMalIds = array_filter($malIds, fn ($id) => ! in_array($id, $kitsuMalIds));
$missingMalIds = array_filter($malIds, static fn ($id) => ! in_array($id, $kitsuMalIds, TRUE));
// Add items on Anilist, but not Kitsu to Kitsu
foreach($missingMalIds as $mid)
foreach ($missingMalIds as $mid)
{
if ( ! array_key_exists($mid, $anilistList))
{
@ -424,13 +420,13 @@ final class SyncLists extends BaseCommand {
}
$data = $anilistList[$mid]['data'];
$data['id'] = $this->kitsuModel->getKitsuIdFromMALId((string)$mid, $type);
$data['id'] = $this->kitsuModel->getKitsuIdFromMALId((string) $mid, $type);
$data['type'] = $type;
$itemsToAddToKitsu[] = $data;
}
foreach($kitsuList as $kitsuItem)
foreach ($kitsuList as $kitsuItem)
{
$malId = $kitsuItem['malId'];
@ -462,7 +458,7 @@ final class SyncLists extends BaseCommand {
// Looks like this item only exists on Kitsu
$kItem = $kitsuItem['data'];
$newItemStatus = ($kItem['reconsuming'] === true) ? 'REPEATING' : $statusMap::KITSU_TO_ANILIST[$kItem['status']];
$newItemStatus = ($kItem['reconsuming'] === TRUE) ? 'REPEATING' : $statusMap::KITSU_TO_ANILIST[$kItem['status']];
$itemsToAddToAnilist[] = [
'mal_id' => $malId,
'data' => [
@ -480,16 +476,12 @@ final class SyncLists extends BaseCommand {
'addToAnilist' => $itemsToAddToAnilist,
'updateAnilist' => $anilistUpdateItems,
'addToKitsu' => $itemsToAddToKitsu,
'updateKitsu' => $kitsuUpdateItems
'updateKitsu' => $kitsuUpdateItems,
];
}
/**
* Compare two list items, and return the out of date one, if one exists
*
* @param array $kitsuItem
* @param array $anilistItem
* @return array|null
*/
private function compareListItems(array $kitsuItem, array $anilistItem): ?array
{
@ -503,10 +495,10 @@ final class SyncLists extends BaseCommand {
];
$diff = [];
$dateDiff = ($kitsuItem['data']['updatedAt'] !== NULL)
? new DateTime($kitsuItem['data']['updatedAt']) <=> new DateTime((string)$anilistItem['data']['updatedAt'])
? new DateTime($kitsuItem['data']['updatedAt']) <=> new DateTime((string) $anilistItem['data']['updatedAt'])
: 0;
foreach($compareKeys as $key)
foreach ($compareKeys as $key)
{
$diff[$key] = $kitsuItem['data'][$key] <=> $anilistItem['data'][$key];
}
@ -522,10 +514,10 @@ final class SyncLists extends BaseCommand {
$update = [
'id' => $kitsuItem['id'],
'mal_id' => $kitsuItem['malId'],
'data' => []
'data' => [],
];
$return = [
'updateType' => []
'updateType' => [],
];
$sameNotes = $diff['notes'] === 0;
@ -549,7 +541,7 @@ final class SyncLists extends BaseCommand {
$update['data']['progress'] = $kitsuItem['data']['progress'];
$return['updateType'][] = Enum\API::ANILIST;
}
else if($diff['progress'] === self::ANILIST_GREATER)
elseif ($diff['progress'] === self::ANILIST_GREATER)
{
$update['data']['progress'] = $anilistItem['data']['progress'];
$return['updateType'][] = Enum\API::KITSU;
@ -564,7 +556,7 @@ final class SyncLists extends BaseCommand {
$update['data']['status'] = $kitsuItem['data']['status'];
$return['updateType'][] = Enum\API::ANILIST;
}
else if ($dateDiff === self::ANILIST_GREATER)
elseif ($dateDiff === self::ANILIST_GREATER)
{
$update['data']['status'] = $anilistItem['data']['status'];
$return['updateType'][] = Enum\API::KITSU;
@ -579,18 +571,18 @@ final class SyncLists extends BaseCommand {
{
$update['data']['status'] = $kitsuItem['data']['status'];
if ((int)$kitsuItem['data']['progress'] !== 0)
if ((int) $kitsuItem['data']['progress'] !== 0)
{
$update['data']['progress'] = $kitsuItem['data']['progress'];
}
$return['updateType'][] = Enum\API::ANILIST;
}
else if($dateDiff === self::ANILIST_GREATER)
elseif ($dateDiff === self::ANILIST_GREATER)
{
$update['data']['status'] = $anilistItem['data']['status'];
if ((int)$anilistItem['data']['progress'] !== 0)
if ((int) $anilistItem['data']['progress'] !== 0)
{
$update['data']['progress'] = $kitsuItem['data']['progress'];
}
@ -603,15 +595,14 @@ final class SyncLists extends BaseCommand {
if ( ! $sameRating)
{
if (
$dateDiff === self::KITSU_GREATER &&
$kitsuItem['data']['rating'] !== 0 &&
$kitsuItem['data']['ratingTwenty'] !== 0
)
{
$dateDiff === self::KITSU_GREATER
&& $kitsuItem['data']['rating'] !== 0
&& $kitsuItem['data']['ratingTwenty'] !== 0
) {
$update['data']['ratingTwenty'] = $kitsuItem['data']['rating'];
$return['updateType'][] = Enum\API::ANILIST;
}
else if($dateDiff === self::ANILIST_GREATER && $anilistItem['data']['rating'] !== 0)
elseif ($dateDiff === self::ANILIST_GREATER && $anilistItem['data']['rating'] !== 0)
{
$update['data']['ratingTwenty'] = $anilistItem['data']['rating'] * 2;
$return['updateType'][] = Enum\API::KITSU;
@ -641,7 +632,7 @@ final class SyncLists extends BaseCommand {
$update['data']['reconsumeCount'] = $kitsuItem['data']['reconsumeCount'];
$return['updateType'][] = Enum\API::ANILIST;
}
else if ($diff['reconsumeCount'] === self::ANILIST_GREATER)
elseif ($diff['reconsumeCount'] === self::ANILIST_GREATER)
{
$update['data']['reconsumeCount'] = $anilistItem['data']['reconsumeCount'];
$return['updateType'][] = Enum\API::KITSU;
@ -683,7 +674,7 @@ final class SyncLists extends BaseCommand {
$return['data']['data'] = array_merge($prevData, $return['data']['data']);
}
else if ($return['updateType'][0] === Enum\API::KITSU)
elseif ($return['updateType'][0] === Enum\API::KITSU)
{
$prevData = [
'notes' => $anilistItem['data']['notes'],
@ -691,7 +682,7 @@ final class SyncLists extends BaseCommand {
'progress' => $anilistItem['data']['progress'] ?? 0,
// Anilist returns a rating between 1-100
// Kitsu expects a rating from 1-20
'rating' => (((int)$anilistItem['data']['rating']) > 0)
'rating' => (((int) $anilistItem['data']['rating']) > 0)
? (int) $anilistItem['data']['rating'] / 5
: 0,
'reconsumeCount' => $anilistItem['data']['reconsumeCount'],
@ -708,19 +699,16 @@ final class SyncLists extends BaseCommand {
// ------------------------------------------------------------------------
// Update Helpers
// ------------------------------------------------------------------------
/**
* Create/Update list items on Kitsu
*
* @param array $itemsToUpdate
* @param string $action
* @param string $type
* @throws Throwable
*/
private function updateKitsuListItems(array $itemsToUpdate, string $action = SyncAction::UPDATE, string $type = MediaType::ANIME): void
{
$requester = new ParallelAPIRequest();
foreach($itemsToUpdate as $item)
foreach ($itemsToUpdate as $item)
{
if ($action === SyncAction::UPDATE)
{
@ -728,21 +716,23 @@ final class SyncLists extends BaseCommand {
$this->kitsuModel->updateListItem(FormItem::from($item))
);
}
else if ($action === SyncAction::CREATE)
elseif ($action === SyncAction::CREATE)
{
$maybeRequest = $this->kitsuModel->createListItem($item);
if ($maybeRequest === NULL)
{
$this->echoWarning("Skipped creating Kitsu {$type} due to missing id ¯\_(ツ)_/¯");
$this->echoWarning("Skipped creating Kitsu {$type} due to missing id ¯\\_(ツ)_/¯");
continue;
}
$requester->addRequest($maybeRequest);
}
}
$responses = $requester->makeRequests();
foreach($responses as $key => $response)
foreach ($responses as $key => $response)
{
$responseData = Json::decode($response);
@ -752,6 +742,7 @@ final class SyncLists extends BaseCommand {
{
$verb = ($action === SyncAction::UPDATE) ? 'updated' : 'created';
$this->echoSuccess("Successfully {$verb} Kitsu {$type} list item with id: {$id}");
continue;
}
@ -763,6 +754,7 @@ final class SyncLists extends BaseCommand {
if ($errorTitle === 'cannot exceed length of media')
{
$this->echoWarning("Skipped Kitsu {$type} {$id} due to episode count mismatch with other API");
continue;
}
}
@ -774,23 +766,19 @@ final class SyncLists extends BaseCommand {
]);
$verb = ($action === SyncAction::UPDATE) ? SyncAction::UPDATE : SyncAction::CREATE;
$this->echoError("Failed to {$verb} Kitsu {$type} list item with id: {$id}, and mal_id: {$mal_id}");
}
}
/**
* Create/Update list items on Anilist
*
* @param array $itemsToUpdate
* @param string $action
* @param string $type
* @throws Throwable
*/
private function updateAnilistListItems(array $itemsToUpdate, string $action = SyncAction::UPDATE, string $type = MediaType::ANIME): void
{
$requester = new ParallelAPIRequest();
foreach($itemsToUpdate as $item)
foreach ($itemsToUpdate as $item)
{
if ($action === SyncAction::UPDATE)
{
@ -800,24 +788,27 @@ final class SyncLists extends BaseCommand {
$requester->addRequest($maybeRequest);
}
}
else if ($action === SyncAction::CREATE)
else
{
try
if ($action === SyncAction::CREATE)
{
$requester->addRequest($this->anilistModel->createFullListItem($item, $type));
}
catch (MissingIdException $e)
{
// Case where there's a MAL mapping from Kitsu, but no equivalent Anlist item
$id = $item['mal_id'];
$this->echoWarning("Skipping Anilist ${type} with MAL id: {$id} due to missing mapping");
try
{
$requester->addRequest($this->anilistModel->createFullListItem($item, $type));
}
catch (MissingIdException)
{
// Case where there's a MAL mapping from Kitsu, but no equivalent Anlist item
$id = $item['mal_id'];
$this->echoWarning("Skipping Anilist {$type} with MAL id: {$id} due to missing mapping");
}
}
}
}
$responses = $requester->makeRequests();
foreach($responses as $key => $response)
foreach ($responses as $key => $response)
{
$id = $itemsToUpdate[$key]['mal_id'];

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Command;
@ -23,10 +21,10 @@ use Aviat\AnimeClient\Controller\Images;
* Clears out image cache directories, then re-creates the image cache
* for manga and anime
*/
final class UpdateThumbnails extends ClearThumbnails {
final class UpdateThumbnails extends ClearThumbnails
{
/**
* Model for making requests to Kitsu API
* @var KitsuModel
*/
protected KitsuModel $kitsuModel;
@ -49,7 +47,7 @@ final class UpdateThumbnails extends ClearThumbnails {
$ids = $this->getImageList();
// Resave the images
foreach($ids as $type => $typeIds)
foreach ($ids as $type => $typeIds)
{
foreach ($typeIds as $id)
{
@ -82,4 +80,4 @@ final class UpdateThumbnails extends ClearThumbnails {
'manga' => $mangaIds,
];
}
}
}

View File

@ -6,19 +6,18 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Component;
use Aviat\AnimeClient\Types\AnimeListItem;
final class AnimeCover {
final class AnimeCover
{
use ComponentTrait;
public function __invoke(AnimeListItem $item): string
@ -27,4 +26,4 @@ final class AnimeCover {
'item' => $item,
]);
}
}
}

View File

@ -6,17 +6,16 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Component;
final class Character {
final class Character
{
use ComponentTrait;
public function __invoke(string $name, string $link, string $picture, string $className = 'character'): string
@ -28,4 +27,4 @@ final class Character {
'className' => $className,
]);
}
}
}

View File

@ -6,32 +6,26 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Component;
use Aviat\Ion\Di\ContainerAware;
use const TEMPLATE_DIR;
use function Aviat\AnimeClient\renderTemplate;
/**
* Shared logic for component-based functionality, like Tabs
*/
trait ComponentTrait {
trait ComponentTrait
{
use ContainerAware;
/**
* Render a template with common container values
*
* @param string $path
* @param array $data
* @return string
*/
public function render(string $path, array $data): string
{
@ -47,4 +41,4 @@ trait ComponentTrait {
return renderTemplate(TEMPLATE_DIR . '/' . $path, array_merge($baseData, $data));
}
}
}

View File

@ -6,19 +6,18 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Component;
use Aviat\AnimeClient\Types\MangaListItem;
final class MangaCover {
final class MangaCover
{
use ComponentTrait;
public function __invoke(MangaListItem $item, string $name): string
@ -28,4 +27,4 @@ final class MangaCover {
'name' => $name,
]);
}
}
}

View File

@ -6,17 +6,16 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Component;
final class Media {
final class Media
{
use ComponentTrait;
public function __invoke(array $titles, string $link, string $picture, string $className = 'media'): string
@ -28,4 +27,4 @@ final class Media {
'className' => $className,
]);
}
}
}

View File

@ -6,38 +6,33 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Component;
final class Tabs {
final class Tabs
{
use ComponentTrait;
/**
* Creates a tabbed content view
*
* @param string $name the name attribute for the input[type-option] form elements
* also used to generate id attributes
* also used to generate id attributes
* @param array $tabData The data used to create the tab content, indexed by the tab label
* @param callable $cb The function to generate the tab content
* @param string $className
* @param bool $hasSectionWrapper
* @return string
*/
public function __invoke(
string $name,
array $tabData,
callable $cb,
string $className = 'content media-wrap flex flex-wrap flex-justify-start',
bool $hasSectionWrapper = false
): string
{
bool $hasSectionWrapper = FALSE
): string {
if (count($tabData) < 2)
{
return $this->render('single-tab.php', [
@ -57,4 +52,4 @@ final class Tabs {
'hasSectionWrapper' => $hasSectionWrapper,
]);
}
}
}

View File

@ -6,36 +6,32 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Component;
final class VerticalTabs {
final class VerticalTabs
{
use ComponentTrait;
/**
* Creates a vertical tab content view
*
* @param string $name the name attribute for the input[type-option] form elements
* also used to generate id attributes
* also used to generate id attributes
* @param array $tabData The data used to create the tab content, indexed by the tab label
* @param callable $cb The function to generate the tab content
* @param string $className
* @return string
*/
public function __invoke(
string $name,
array $tabData,
callable $cb,
string $className='content media-wrap flex flex-wrap flex-justify-start'
): string
{
string $className = 'content media-wrap flex flex-wrap flex-justify-start'
): string {
return $this->render('vertical-tabs.php', [
'name' => $name,
'data' => $tabData,
@ -43,4 +39,4 @@ final class VerticalTabs {
'className' => $className,
]);
}
}
}

View File

@ -6,42 +6,40 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient;
use function Aviat\Ion\_dir;
use Aviat\AnimeClient\Enum\EventType;
use Aura\Router\Generator;
use Aura\Session\Segment;
use Aviat\AnimeClient\API\Kitsu\Auth;
use Aviat\Ion\ConfigInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\SimpleCache\CacheInterface;
use Aviat\AnimeClient\Enum\EventType;
use Aviat\Ion\Di\{
ContainerAware,
ContainerInterface,
Exception\ContainerException,
Exception\NotFoundException
};
use Aviat\Ion\Event;
use Aviat\Ion\Exception\DoubleRenderException;
use Aviat\Ion\View\{HtmlView, HttpView, JsonView};
use Aviat\Ion\{ConfigInterface, Event};
use InvalidArgumentException;
use Psr\Http\Message\ServerRequestInterface;
use Psr\SimpleCache\CacheInterface;
use function Aviat\Ion\_dir;
use function is_array;
/**
* Controller base, defines output methods
*/
class Controller {
class Controller
{
use ContainerAware;
/**
@ -87,7 +85,6 @@ class Controller {
/**
* Controller constructor.
*
* @param ContainerInterface $container
* @throws ContainerException
* @throws NotFoundException
*/
@ -100,7 +97,7 @@ class Controller {
$urlGenerator = $container->get('url-generator');
$this->auth = $container->get('auth');
$this->cache = $container->get('cache');
$this->cache = $container->get('cache');
$this->config = $container->get('config');
$this->request = $container->get('request');
$this->session = $session->getSegment(SESSION_SEGMENT);
@ -127,11 +124,10 @@ class Controller {
* Set the current url in the session as the target of a future redirect
*
* @codeCoverageIgnore
* @param string|NULL $url
* @throws ContainerException
* @throws NotFoundException
*/
public function setSessionRedirect(string $url = NULL): void
public function setSessionRedirect(?string $url = NULL): void
{
$serverParams = $this->request->getServerParams();
@ -169,7 +165,6 @@ class Controller {
*
* @codeCoverageIgnore
* @throws InvalidArgumentException
* @return void
*/
public function sessionRedirect(): void
{
@ -199,10 +194,6 @@ class Controller {
* Get the string output of a partial template
*
* @codeCoverageIgnore
* @param HtmlView $view
* @param string $template
* @param array $data
* @return string
*/
protected function loadPartial(HtmlView $view, string $template, array $data = []): string
{
@ -216,7 +207,6 @@ class Controller {
$route = $router->getRoute();
$data['route_path'] = $route !== FALSE ? $route->path : '';
$templatePath = _dir($this->config->get('view_path'), "{$template}.php");
if ( ! is_file($templatePath))
@ -231,10 +221,6 @@ class Controller {
* Render a template with header and footer
*
* @codeCoverageIgnore
* @param HtmlView $view
* @param string $template
* @param array $data
* @return HtmlView
*/
protected function renderFullPage(HtmlView $view, string $template, array $data): HtmlView
{
@ -247,7 +233,7 @@ class Controller {
$view->addHeader('Content-Security-Policy', implode('; ', $csp));
$view->appendOutput($this->loadPartial($view, 'header', $data));
if (array_key_exists('message', $data) && \is_array($data['message']))
if (array_key_exists('message', $data) && is_array($data['message']))
{
$view->appendOutput($this->loadPartial($view, 'message', $data['message']));
}
@ -262,20 +248,17 @@ class Controller {
* 404 action
*
* @codeCoverageIgnore
* @param string $title
* @param string $message
* @throws InvalidArgumentException
* @return void
*/
public function notFound(
string $title = 'Sorry, page not found',
string $message = 'Page Not Found'
): void
{
): void {
$this->outputHTML('404', [
'title' => $title,
'message' => $message,
], NULL, 404);
exit();
}
@ -283,19 +266,14 @@ class Controller {
* Display a generic error page
*
* @codeCoverageIgnore
* @param int $httpCode
* @param string $title
* @param string $message
* @param string $longMessage
* @throws InvalidArgumentException
* @return void
*/
public function errorPage(int $httpCode, string $title, string $message, string $longMessage = ''): void
{
$this->outputHTML('error', [
'title' => $title,
'message' => $message,
'long_message' => $longMessage
'long_message' => $longMessage,
], NULL, $httpCode);
}
@ -304,7 +282,6 @@ class Controller {
*
* @codeCoverageIgnore
* @throws InvalidArgumentException
* @return void
*/
public function redirectToDefaultRoute(): void
{
@ -317,9 +294,6 @@ class Controller {
* next page load
*
* @codeCoverageIgnore
* @param string $message
* @param string $type
* @return void
*/
public function setFlashMessage(string $message, string $type = 'info'): void
{
@ -332,7 +306,7 @@ class Controller {
$messages[] = [
'message_type' => $type,
'message' => $message
'message' => $message,
];
$this->session->setFlash('message', $messages);
@ -342,9 +316,8 @@ class Controller {
* Helper for consistent page titles
*
* @param string ...$parts Title segments
* @return string
*/
public function formatTitle(string ...$parts) : string
public function formatTitle(string ...$parts): string
{
return implode(' &middot; ', $parts);
}
@ -353,17 +326,13 @@ class Controller {
* Add a message box to the page
*
* @codeCoverageIgnore
* @param HtmlView $view
* @param string $type
* @param string $message
* @throws InvalidArgumentException
* @return string
*/
protected function showMessage(HtmlView $view, string $type, string $message): string
{
return $this->loadPartial($view, 'message', [
'message_type' => $type,
'message' => $message
'message' => $message,
]);
}
@ -371,14 +340,9 @@ class Controller {
* Output a template to HTML, using the provided data
*
* @codeCoverageIgnore
* @param string $template
* @param array $data
* @param HtmlView|NULL $view
* @param int $code
* @return void
*@throws InvalidArgumentException
* @throws InvalidArgumentException
*/
protected function outputHTML(string $template, array $data = [], HtmlView $view = NULL, int $code = 200): void
protected function outputHTML(string $template, array $data = [], ?HtmlView $view = NULL, int $code = 200): void
{
if (NULL === $view)
{
@ -393,14 +357,12 @@ class Controller {
* Output a JSON Response
*
* @codeCoverageIgnore
* @param mixed $data
* @param int $code - the http status code
* @throws DoubleRenderException
* @return void
*/
protected function outputJSON(mixed $data, int $code): void
{
(new JsonView())
JsonView::new()
->setOutput($data)
->setStatusCode($code)
->send();
@ -410,17 +372,13 @@ class Controller {
* Redirect to the selected page
*
* @codeCoverageIgnore
* @param string $url
* @param int $code
* @return void
*/
protected function redirect(string $url, int $code): void
{
try
{
(new HttpView())->redirect($url, $code)->send();
}
catch (\Throwable) {}
HttpView::new()
->redirect($url, $code)
->send();
}
}
// End of BaseController.php
// End of BaseController.php

View File

@ -6,26 +6,25 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Controller;
use Aura\Router\Exception\RouteNotFound;
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\Kitsu\Transformer\AnimeListTransformer;
use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\Model\Anime as AnimeModel;
use Aviat\AnimeClient\Types\FormItem;
use Aviat\Ion\Attribute\Controller;
use Aviat\Ion\Attribute\Route;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\ContainerException;
use Aviat\Ion\Di\Exception\NotFoundException;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
use Aviat\Ion\Json;
use InvalidArgumentException;
@ -35,18 +34,17 @@ use TypeError;
/**
* Controller for Anime-related pages
*/
final class Anime extends BaseController {
#[Controller('anime')]
final class Anime extends BaseController
{
/**
* The anime list model
* @var AnimeModel $model
*/
protected AnimeModel $model;
/**
* Constructor
*
* @param ContainerInterface $container
* @throws ContainerException
* @throws NotFoundException
*/
@ -68,10 +66,10 @@ final class Anime extends BaseController {
*
* @param int|string $status - The section of the list
* @param string|null $view - List or cover view
* @return void
* @throws InvalidArgumentException
* @throws Throwable
*/
#[Route('anime.list', '/anime/{status}{/view}')]
public function index(int|string $status = KitsuWatchingStatus::WATCHING, ?string $view = NULL): void
{
if ( ! in_array($status, [
@ -95,7 +93,7 @@ final class Anime extends BaseController {
$viewMap = [
'' => 'cover',
'list' => 'list'
'list' => 'list',
];
$data = ($status !== 'all')
@ -104,7 +102,7 @@ final class Anime extends BaseController {
$this->outputHTML('anime/' . $viewMap[$view], [
'title' => $title,
'sections' => $data
'sections' => $data,
]);
}
@ -112,12 +110,12 @@ final class Anime extends BaseController {
* Form to add an anime
*
* @throws ContainerException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws RouteNotFound
* @throws InvalidArgumentException
* @throws Throwable
* @return void
*/
#[Route('anime.add.get', '/anime/add')]
public function addForm(): void
{
$this->checkAuth();
@ -129,7 +127,7 @@ final class Anime extends BaseController {
'Add'
),
'action_url' => $this->url->generate('anime.add.post'),
'status_list' => AnimeWatchingStatus::KITSU_TO_TITLE
'status_list' => AnimeWatchingStatus::KITSU_TO_TITLE,
]);
}
@ -137,13 +135,13 @@ final class Anime extends BaseController {
* Add an anime to the list
*
* @throws Throwable
* @return void
*/
#[Route('anime.add.post', '/anime/add', Route::POST)]
public function add(): void
{
$this->checkAuth();
$data = (array)$this->request->getParsedBody();
$data = (array) $this->request->getParsedBody();
if (empty($data['mal_id']))
{
@ -155,7 +153,7 @@ final class Anime extends BaseController {
$this->redirect('anime/add', 303);
}
$result = $this->model->createLibraryItem($data);
$result = $this->model->createItem($data);
if ($result)
{
@ -172,15 +170,13 @@ final class Anime extends BaseController {
/**
* Form to edit details about a series
*
* @param string $id
* @param string $status
*/
#[Route('anime.edit', '/anime/edit/{id}/{status}')]
public function edit(string $id, string $status = 'all'): void
{
$this->checkAuth();
$item = $this->model->getLibraryItem($id);
$item = $this->model->getItem($id);
$this->setSessionRedirect();
$this->outputHTML('anime/edit', [
@ -191,16 +187,15 @@ final class Anime extends BaseController {
'item' => $item,
'statuses' => AnimeWatchingStatus::KITSU_TO_TITLE,
'action' => $this->url->generate('update.post', [
'controller' => 'anime'
'controller' => 'anime',
]),
]);
}
/**
* Search for anime
*
* @return void
*/
#[Route('anime.search', '/anime/search')]
public function search(): void
{
$queryParams = $this->request->getQueryParams();
@ -212,19 +207,19 @@ final class Anime extends BaseController {
* Update an anime item via a form submission
*
* @throws Throwable
* @return void
*/
#[Route('anime.update.post', '/anime/update_form', Route::POST)]
public function formUpdate(): void
{
$this->checkAuth();
$data = (array)$this->request->getParsedBody();
$data = (array) $this->request->getParsedBody();
// Do some minor data manipulation for
// large form-based updates
$transformer = new AnimeListTransformer();
$postData = $transformer->untransform($data);
$fullResult = $this->model->updateLibraryItem(FormItem::from($postData));
$fullResult = $this->model->updateItem(FormItem::from($postData));
if ($fullResult['statusCode'] === 200)
{
@ -243,28 +238,24 @@ final class Anime extends BaseController {
* Increase the watched count for an anime item
*
* @throws Throwable
* @return void
*/
#[Route('anime.increment', '/anime/increment', Route::POST)]
public function increment(): void
{
$this->checkAuth();
if (str_contains($this->request->getHeader('content-type')[0], 'application/json'))
{
$data = Json::decode((string)$this->request->getBody());
}
else
{
$data = (array)$this->request->getParsedBody();
}
$data = str_contains($this->request->getHeader('content-type')[0], 'application/json')
? Json::decode((string) $this->request->getBody())
: (array) $this->request->getParsedBody();
if (empty($data))
{
$this->errorPage(400, 'Bad Request', '');
exit();
}
$response = $this->model->incrementLibraryItem(FormItem::from($data));
$response = $this->model->incrementItem(FormItem::from($data));
$this->cache->clear();
$this->outputJSON($response['body'], $response['statusCode']);
@ -274,14 +265,14 @@ final class Anime extends BaseController {
* Remove an anime from the list
*
* @throws Throwable
* @return void
*/
#[Route('anime.delete', '/anime/delete', Route::POST)]
public function delete(): void
{
$this->checkAuth();
$body = (array)$this->request->getParsedBody();
$response = $this->model->deleteLibraryItem($body['id'], $body['mal_id']);
$body = (array) $this->request->getParsedBody();
$response = $this->model->deleteItem(FormItem::from($body));
if ($response === TRUE)
{
@ -299,10 +290,9 @@ final class Anime extends BaseController {
/**
* View details of an anime
*
* @param string $id
* @throws InvalidArgumentException
* @return void
*/
#[Route('anime.details', '/anime/details/{id}')]
public function details(string $id): void
{
try
@ -341,6 +331,7 @@ final class Anime extends BaseController {
}
}
#[Route('anime.random', '/anime/details/random')]
public function random(): void
{
try
@ -379,4 +370,5 @@ final class Anime extends BaseController {
}
}
}
// End of AnimeController.php
// End of AnimeController.php

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Controller;
@ -22,9 +20,11 @@ use Aviat\AnimeClient\Model\{
Anime as AnimeModel,
AnimeCollection as AnimeCollectionModel
};
use Aviat\Ion\Attribute\Controller;
use Aviat\Ion\Attribute\Route;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\ContainerException;
use Aviat\Ion\Di\Exception\NotFoundException;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
use Aviat\Ion\Json;
use Aviat\Ion\Exception\DoubleRenderException;
use InvalidArgumentException;
@ -32,24 +32,22 @@ use InvalidArgumentException;
/**
* Controller for Anime collection pages
*/
final class AnimeCollection extends BaseController {
#[Controller('anime.collection')]
final class AnimeCollection extends BaseController
{
/**
* The anime collection model
* @var AnimeCollectionModel $animeCollectionModel
*/
private AnimeCollectionModel $animeCollectionModel;
/**
* The anime API model
* @var AnimeModel $animeModel
*/
private AnimeModel $animeModel;
/**
* Constructor
*
* @param ContainerInterface $container
* @throws ContainerException
* @throws NotFoundException
*/
@ -67,6 +65,8 @@ final class AnimeCollection extends BaseController {
]);
}
#[Route('anime.collection.redirect', '/anime-collection')]
#[Route('anime.collection.redirect2', '/anime-collection/')]
public function index(): void
{
$this->redirect('/anime-collection/view', 303);
@ -76,8 +76,8 @@ final class AnimeCollection extends BaseController {
* Search for anime
*
* @throws DoubleRenderException
* @return void
*/
#[Route('anime.collection.search', '/anime-collection/search')]
public function search(): void
{
$queryParams = $this->request->getQueryParams();
@ -88,17 +88,16 @@ final class AnimeCollection extends BaseController {
/**
* Show the anime collection page
*
* @param string|null $view
* @throws ContainerException
* @throws NotFoundException
* @throws InvalidArgumentException
* @return void
* @throws NotFoundException
*/
#[Route('anime.collection.view', '/anime-collection/view{/view}')]
public function view(?string $view = ''): void
{
$viewMap = [
'' => 'cover',
'list' => 'list'
'list' => 'list',
];
$sections = array_merge(
@ -115,14 +114,15 @@ final class AnimeCollection extends BaseController {
/**
* Show the anime collection add/edit form
*
* @param integer|null $id
* @param int|null $id
* @throws ContainerException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws RouteNotFound
* @throws InvalidArgumentException
* @return void
*/
public function form($id = NULL): void
#[Route('anime.collection.add.get', '/anime-collection/add')]
#[Route('anime.collection.edit.get', '/anime-collection/edit/{id}')]
public function form(?int $id = NULL): void
{
$this->checkAuth();
@ -139,7 +139,7 @@ final class AnimeCollection extends BaseController {
$action
),
'media_items' => $this->animeCollectionModel->getMediaTypeList(),
'item' => ($action === 'Edit' && $id !== NULL) ? $this->animeCollectionModel->get($id) : []
'item' => ($action === 'Edit' && $id !== NULL) ? $this->animeCollectionModel->get($id) : [],
]);
}
@ -147,29 +147,29 @@ final class AnimeCollection extends BaseController {
* Update a collection item
*
* @throws ContainerException
* @throws NotFoundException
* @throws InvalidArgumentException
* @return void
* @throws NotFoundException
*/
#[Route('anime.collection.edit.post', '/anime-collection/edit', Route::POST)]
public function edit(): void
{
$this->checkAuth();
$this->update((array)$this->request->getParsedBody());
$this->update((array) $this->request->getParsedBody());
}
/**
* Add a collection item
*
* @throws ContainerException
* @throws NotFoundException
* @throws InvalidArgumentException
* @return void
* @throws NotFoundException
*/
#[Route('anime.collection.add.post', '/anime-collection/add', Route::POST)]
public function add(): void
{
$this->checkAuth();
$data = (array)$this->request->getParsedBody();
$data = (array) $this->request->getParsedBody();
if (array_key_exists('id', $data))
{
// Check for existing entry
@ -191,6 +191,7 @@ final class AnimeCollection extends BaseController {
}
$this->update($data);
return;
}
@ -201,6 +202,7 @@ final class AnimeCollection extends BaseController {
{
$this->setFlashMessage('Successfully added collection item', 'success');
$this->sessionRedirect();
return;
}
}
@ -211,14 +213,13 @@ final class AnimeCollection extends BaseController {
/**
* Remove a collection item
*
* @return void
*/
#[Route('anime.collection.delete', '/anime-collection/delete', Route::POST)]
public function delete(): void
{
$this->checkAuth();
$data = (array)$this->request->getParsedBody();
$data = (array) $this->request->getParsedBody();
if ( ! array_key_exists('hummingbird_id', $data))
{
$this->setFlashMessage("Can't delete item that doesn't exist", 'error');
@ -237,8 +238,6 @@ final class AnimeCollection extends BaseController {
/**
* Update a collection item
*
* @param array $data
*/
protected function update(array $data): void
{
@ -259,4 +258,5 @@ final class AnimeCollection extends BaseController {
$this->sessionRedirect();
}
}
// End of AnimeCollection.php
// End of AnimeCollection.php

View File

@ -6,38 +6,34 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\API\Kitsu\Model;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\API\Kitsu\Transformer\CharacterTransformer;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\Ion\Attribute\Controller;
use Aviat\Ion\Attribute\Route;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\ContainerException;
use Aviat\Ion\Di\Exception\NotFoundException;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
/**
* Controller for character description pages
*/
final class Character extends BaseController {
/**
* @var Model
*/
#[Controller]
final class Character extends BaseController
{
private Model $model;
/**
* Character constructor.
*
* @param ContainerInterface $container
* @throws ContainerException
* @throws NotFoundException
*/
@ -49,10 +45,8 @@ final class Character extends BaseController {
/**
* Show information about a character
*
* @param string $slug
* @return void
*/
#[Route('character', '/character/{slug}')]
public function index(string $slug): void
{
$rawData = $this->model->getCharacter($slug);
@ -80,4 +74,4 @@ final class Character extends BaseController {
'data' => $data,
]);
}
}
}

View File

@ -6,43 +6,39 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\Model\Anime as AnimeModel;
use Aviat\AnimeClient\Model\Manga as MangaModel;
use Aviat\Ion\Attribute\Controller;
use Aviat\Ion\Attribute\Route;
use Aviat\AnimeClient\{Controller as BaseController, Model};
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\ContainerException;
use Aviat\Ion\Di\Exception\NotFoundException;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
/**
* Controller for Anime-related pages
*/
final class History extends BaseController {
#[Controller]
final class History extends BaseController
{
/**
* The anime list model
* @var AnimeModel
*/
protected AnimeModel $animeModel;
protected Model\Anime $animeModel;
/**
* The manga list model
* @var MangaModel
*/
protected MangaModel $mangaModel;
protected Model\Manga $mangaModel;
/**
* Constructor
*
* @param ContainerInterface $container
* @throws ContainerException
* @throws NotFoundException
*/
@ -54,11 +50,13 @@ final class History extends BaseController {
$this->mangaModel = $container->get('manga-model');
}
#[Route('history', '/history/{type}')]
public function index(string $type = 'anime'): void
{
if (method_exists($this, $type))
{
$this->$type();
$this->{$type}();
return;
}

View File

@ -6,40 +6,42 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Controller;
use function Amp\Promise\wait;
use function Aviat\AnimeClient\getResponse;
use function Aviat\AnimeClient\createPlaceholderImage;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\Ion\Attribute\Controller;
use Aviat\Ion\Attribute\Route;
use Throwable;
use function Amp\Promise\wait;
use function Aviat\AnimeClient\{createPlaceholderImage, getResponse};
use function imagepalletetotruecolor;
use function in_array;
/**
* Controller for handling routes that don't fit elsewhere
*/
final class Images extends BaseController {
#[Controller]
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
* @return void
* @throws Throwable
*/
#[Route('image_proxy', '/public/images/{type}/{file}')]
public function cache(string $type, string $file, bool $display = TRUE): void
{
$currentUrl = (string)$this->request->getUri();
$currentUrl = (string) $this->request->getUri();
$kitsuUrl = 'https://media.kitsu.io/';
$fileName = str_replace('-original', '', $file);
@ -49,8 +51,8 @@ final class Images extends BaseController {
// 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))
$tryJpg = ['anime', 'characters', 'manga', 'people'];
if ($ext === 'webp' && in_array($type, $tryJpg, TRUE))
{
$ext = 'jpg';
$currentUrl = str_replace('webp', 'jpg', $currentUrl);
@ -64,8 +66,8 @@ final class Images extends BaseController {
],
'avatars' => [
'kitsuUrl' => "users/avatars/{$id}/original.{$ext}",
'width' => null,
'height' => null,
'width' => NULL,
'height' => NULL,
],
'characters' => [
'kitsuUrl' => "characters/images/{$id}/original.{$ext}",
@ -79,8 +81,8 @@ final class Images extends BaseController {
],
'people' => [
'kitsuUrl' => "people/images/{$id}/original.{$ext}",
'width' => null,
'height' => null,
'width' => NULL,
'height' => NULL,
],
];
@ -89,6 +91,7 @@ final class Images extends BaseController {
if (NULL === $imageType)
{
$this->getPlaceholder($baseSavePath, 200, 200);
return;
}
@ -112,6 +115,7 @@ final class Images extends BaseController {
{
$newUrl = str_replace($ext, $nextType[$ext], $currentUrl);
$this->redirect($newUrl, 303);
return;
}
@ -123,6 +127,7 @@ final class Images extends BaseController {
{
createPlaceholderImage("{$baseSavePath}/{$type}", $width, $height);
}
return;
}
@ -144,7 +149,7 @@ final class Images extends BaseController {
if ($ext === 'gif')
{
file_put_contents("{$filePrefix}.gif", $data);
\imagepalletetotruecolor($gdImg);
imagepalletetotruecolor($gdImg);
}
// save the webp versions
@ -177,14 +182,10 @@ final class Images extends BaseController {
/**
* 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
private function getPlaceholder(string $path, ?int $width = 200, ?int $height = NULL): void
{
$height = $height ?? $width;
$height ??= $width;
$filename = $path . '/placeholder.png';
@ -196,4 +197,4 @@ final class Images extends BaseController {
header('Content-Type: image/png');
echo file_get_contents($filename);
}
}
}

View File

@ -6,22 +6,22 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Controller;
use Aura\Router\Exception\RouteNotFound;
use Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\API\Kitsu\Transformer\MangaListTransformer;
use Aviat\AnimeClient\API\Mapping\MangaReadingStatus;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\Model\Manga as MangaModel;
use Aviat\AnimeClient\Types\FormItem;
use Aviat\Ion\Attribute\Controller;
use Aviat\Ion\Attribute\Route;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
use Aviat\Ion\Json;
@ -32,18 +32,17 @@ use Throwable;
/**
* Controller for manga list
*/
final class Manga extends Controller {
#[Controller('manga')]
final class Manga extends BaseController
{
/**
* The manga model
* @var MangaModel $model
*/
protected MangaModel $model;
/**
* Constructor
*
* @param ContainerInterface $container
* @throws ContainerException
* @throws NotFoundException
*/
@ -61,12 +60,8 @@ final class Manga extends Controller {
/**
* Get a section of the manga list
*
* @param string $status
* @param string $view
* @return void
*@throws InvalidArgumentException
*/
#[Route('manga.list', '/list/{status}{/view}')]
public function index(string $status = 'all', ?string $view = ''): void
{
if ( ! in_array($status, [
@ -90,11 +85,11 @@ final class Manga extends Controller {
$view_map = [
'' => 'cover',
'list' => 'list'
'list' => 'list',
];
$data = ($status !== 'all')
? [ $statusTitle => $this->model->getList($statusTitle) ]
? [$statusTitle => $this->model->getList($statusTitle)]
: $this->model->getList('All');
$this->outputHTML('manga/' . $view_map[$view], [
@ -104,14 +99,9 @@ final class Manga extends Controller {
}
/**
* Form to add an manga
*
* @throws ContainerException
* @throws NotFoundException
* @throws RouteNotFound
* @throws InvalidArgumentException
* @return void
* Form to add a manga
*/
#[Route('manga.add.get', '/manga/add')]
public function addForm(): void
{
$this->checkAuth();
@ -125,21 +115,19 @@ final class Manga extends Controller {
'Add'
),
'action_url' => $this->url->generate('manga.add.post'),
'status_list' => $statuses
'status_list' => $statuses,
]);
}
/**
* Add an manga to the list
*
* @return void
* @throws Throwable
* Add a manga to the list
*/
#[Route('manage.add.post', '/manga/add', Route::POST)]
public function add(): void
{
$this->checkAuth();
$data = (array)$this->request->getParsedBody();
$data = (array) $this->request->getParsedBody();
if ( ! array_key_exists('id', $data))
{
$this->redirect('manga/add', 303);
@ -150,7 +138,7 @@ final class Manga extends Controller {
unset($data['mal_id']);
}
$result = $this->model->createLibraryItem($data);
$result = $this->model->createItem($data);
if ($result)
{
@ -167,21 +155,14 @@ final class Manga extends Controller {
/**
* Show the manga edit form
*
* @param string $id
* @param string $status
* @throws ContainerException
* @throws NotFoundException
* @throws RouteNotFound
* @throws InvalidArgumentException
* @return void
*/
#[Route('manga.edit', '/manga/edit/{id}/{status}')]
public function edit(string $id, string $status = 'All'): void
{
$this->checkAuth();
$this->setSessionRedirect();
$item = $this->model->getLibraryItem($id);
$item = $this->model->getItem($id);
$title = $this->formatTitle(
$this->config->get('whose_list') . "'s Manga List",
'Edit'
@ -192,16 +173,15 @@ final class Manga extends Controller {
'status_list' => MangaReadingStatus::KITSU_TO_TITLE,
'item' => $item,
'action' => $this->url->generate('update.post', [
'controller' => 'manga'
'controller' => 'manga',
]),
]);
}
/**
* Search for a manga to add to the list
*
* @return void
*/
#[Route('manga.search', '/manga/search')]
public function search(): void
{
$queryParams = $this->request->getQueryParams();
@ -211,21 +191,19 @@ final class Manga extends Controller {
/**
* Update an manga item via a form submission
*
* @return void
* @throws Throwable
*/
#[Route('manga.update.post', '/manga/update', Route::POST)]
public function formUpdate(): void
{
$this->checkAuth();
$data = (array)$this->request->getParsedBody();
$data = (array) $this->request->getParsedBody();
// Do some minor data manipulation for
// large form-based updates
$transformer = new MangaListTransformer();
$post_data = $transformer->untransform($data);
$full_result = $this->model->updateLibraryItem(FormItem::from($post_data));
$full_result = $this->model->updateItem(FormItem::from($post_data));
if ($full_result['statusCode'] === 200)
{
@ -235,7 +213,6 @@ final class Manga extends Controller {
else
{
$this->setFlashMessage('Failed to update manga.', 'error');
}
$this->sessionRedirect();
@ -243,22 +220,22 @@ final class Manga extends Controller {
/**
* Increment the progress of a manga item
* @throws Throwable
*/
#[Route('manga.increment', '/manga/increment', Route::POST)]
public function increment(): void
{
$this->checkAuth();
if (str_contains($this->request->getHeader('content-type')[0], 'application/json'))
{
$data = Json::decode((string)$this->request->getBody());
$data = Json::decode((string) $this->request->getBody());
}
else
{
$data = $this->request->getParsedBody();
}
$res = $this->model->incrementLibraryItem(FormItem::from($data));
$res = $this->model->incrementItem(FormItem::from($data));
$body = $res['body'];
$statusCode = $res['statusCode'];
@ -268,16 +245,14 @@ final class Manga extends Controller {
/**
* Remove an manga from the list
*
* @throws Throwable
* @return void
*/
#[Route('manga.delete', '/manga/delete', Route::POST)]
public function delete(): void
{
$this->checkAuth();
$body = (array)$this->request->getParsedBody();
$response = $this->model->deleteLibraryItem($body['id'], $body['mal_id']);
$body = (array) $this->request->getParsedBody();
$response = $this->model->deleteItem(FormItem::from($body));
if ($response)
{
@ -294,12 +269,8 @@ final class Manga extends Controller {
/**
* View details of an manga
*
* @param string $id
* @throws InvalidArgumentException
* @throws Throwable
* @return void
*/
#[Route('manga.details', '/manga/details/{id}')]
public function details(string $id): void
{
$data = $this->model->getManga($id);
@ -311,6 +282,7 @@ final class Manga extends Controller {
'Manga not found',
'Manga Not Found'
);
return;
}
@ -326,11 +298,8 @@ final class Manga extends Controller {
/**
* View details of a random manga
*
* @throws InvalidArgumentException
* @throws Throwable
* @return void
*/
#[Route('manga.random', '/manga/details/random')]
public function random(): void
{
$data = $this->model->getRandomManga();
@ -342,6 +311,7 @@ final class Manga extends Controller {
'Manga not found',
'Manga Not Found'
);
return;
}
@ -355,4 +325,5 @@ final class Manga extends Controller {
]);
}
}
// End of MangaController.php

View File

@ -6,30 +6,52 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\API\Kitsu\Model;
use Aviat\AnimeClient\API\Kitsu\Transformer\CharacterTransformer;
use Aviat\AnimeClient\API\Kitsu\Transformer\PersonTransformer;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\Enum\EventType;
use Aviat\Ion\Attribute\DefaultController;
use Aviat\Ion\Attribute\Route;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Event;
use Aviat\Ion\View\HtmlView;
/**
* Controller for handling routes that don't fit elsewhere
*/
final class Misc extends BaseController {
#[DefaultController]
final class Misc extends BaseController
{
private Model $model;
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->model = $container->get('kitsu-model');
}
/**
* Redirect to the default controller/url from an empty path
*/
#[Route('index_redirect', '/')]
public function index(): void
{
parent::redirectToDefaultRoute();
}
/**
* Purges the API cache
*
* @return void
*/
#[Route('cache_purge', '/cache_purge')]
public function clearCache(): void
{
$this->checkAuth();
@ -37,16 +59,14 @@ final class Misc extends BaseController {
Event::emit(EventType::CLEAR_CACHE);
$this->outputHTML('blank', [
'title' => 'Cache cleared'
'title' => 'Cache cleared',
]);
}
/**
* Show the login form
*
* @param string $status
* @return void
*/
#[Route('login', '/login')]
public function login(string $status = ''): void
{
$message = '';
@ -63,22 +83,22 @@ final class Misc extends BaseController {
$this->outputHTML('login', [
'title' => 'Api login',
'message' => $message
'message' => $message,
], $view);
}
/**
* Attempt login authentication
*
* @return void
*/
#[Route('login.post', '/login', Route::POST)]
public function loginAction(): void
{
$post = (array)$this->request->getParsedBody();
$post = (array) $this->request->getParsedBody();
if ($this->auth->authenticate($post['password']))
{
$this->sessionRedirect();
return;
}
@ -92,9 +112,8 @@ final class Misc extends BaseController {
/**
* Deauthorize the current user
*
* @return void
*/
#[Route('logout', '/logout')]
public function logout(): void
{
$this->auth->logout();
@ -105,8 +124,72 @@ final class Misc extends BaseController {
/**
* Check if the current user is logged in
*/
#[Route('heartbeat', '/heartbeat')]
public function heartbeat(): void
{
$this->outputJSON(['hasAuth' => $this->auth->isAuthenticated()], 200);
}
}
/**
* Show information about a character
*/
#[Route('character', '/character/{slug}')]
public function character(string $slug): void
{
$rawData = $this->model->getCharacter($slug);
if (( ! array_key_exists('data', $rawData)) || empty($rawData['data']))
{
$this->notFound(
$this->formatTitle(
'Characters',
'Character not found'
),
'Character Not Found'
);
return;
}
$data = (new CharacterTransformer())->transform($rawData)->toArray();
$this->outputHTML('character/details', [
'title' => $this->formatTitle(
'Characters',
$data['name']
),
'data' => $data,
]);
}
/**
* Show information about a person
*/
#[Route('person', '/people/{slug}')]
public function person(string $slug): void
{
$rawData = $this->model->getPerson($slug);
$data = (new PersonTransformer())->transform($rawData)->toArray();
if (( ! array_key_exists('data', $rawData)) || empty($rawData['data']))
{
$this->notFound(
$this->formatTitle(
'People',
'Person not found'
),
'Person Not Found'
);
return;
}
$this->outputHTML('person/details', [
'title' => $this->formatTitle(
'People',
$data['name']
),
'data' => $data,
]);
}
}

View File

@ -6,38 +6,34 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\API\Kitsu\Model;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\API\Kitsu\Transformer\PersonTransformer;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\Ion\Attribute\Controller;
use Aviat\Ion\Attribute\Route;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\ContainerException;
use Aviat\Ion\Di\Exception\NotFoundException;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
/**
* Controller for People pages
*/
final class People extends BaseController {
/**
* @var Model
*/
#[Controller]
final class People extends BaseController
{
private Model $model;
/**
* People constructor.
*
* @param ContainerInterface $container
* @throws ContainerException
* @throws NotFoundException
*/
@ -49,10 +45,8 @@ final class People extends BaseController {
/**
* Show information about a person
*
* @param string $slug
* @return void
*/
#[Route('person', '/people/{slug}')]
public function index(string $slug): void
{
$rawData = $this->model->getPerson($slug);
@ -79,4 +73,4 @@ final class People extends BaseController {
'data' => $data,
]);
}
}
}

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Controller;
@ -20,29 +18,23 @@ use Aura\Router\Exception\RouteNotFound;
use Aviat\AnimeClient\API\Anilist\Model as AnilistModel;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\Model\Settings as SettingsModel;
use Aviat\Ion\Attribute\Controller;
use Aviat\Ion\Attribute\Route;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\ContainerException;
use Aviat\Ion\Di\Exception\NotFoundException;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
/**
* Controller for user settings
*/
final class Settings extends BaseController {
/**
* @var AnilistModel
*/
#[Controller]
final class Settings extends BaseController
{
private AnilistModel $anilistModel;
/**
* @var SettingsModel
*/
private SettingsModel $settingsModel;
/**
* Settings constructor.
*
* @param ContainerInterface $container
* @throws ContainerException
* @throws NotFoundException
*/
@ -60,6 +52,7 @@ final class Settings extends BaseController {
/**
* Show the user settings, if logged in
*/
#[Route('settings', '/settings')]
public function index(): void
{
$auth = $this->container->get('auth');
@ -82,9 +75,10 @@ final class Settings extends BaseController {
*
* @throws RouteNotFound
*/
#[Route('settings-post', '/settings/update', Route::POST)]
public function update(): void
{
$post = (array)$this->request->getParsedBody();
$post = (array) $this->request->getParsedBody();
unset($post['settings-tabs']);
$saved = $this->settingsModel->saveSettingsFile($post);
@ -102,6 +96,7 @@ final class Settings extends BaseController {
/**
* Redirect to Anilist to start Oauth flow
*/
#[Route('anilist-redirect', '/anilist-redirect')]
public function anilistRedirect(): void
{
$query = http_build_query([
@ -118,6 +113,7 @@ final class Settings extends BaseController {
/**
* Oauth callback for Anilist API
*/
#[Route('anilist-callback', '/anilist-oauth')]
public function anilistCallback(): void
{
$query = $this->request->getQueryParams();
@ -130,6 +126,7 @@ final class Settings extends BaseController {
if (array_key_exists('error', $authData))
{
$this->errorPage(400, 'Error Linking Account', $authData['hint']);
return;
}
@ -147,6 +144,7 @@ final class Settings extends BaseController {
{
$newSettings[$key] = $value;
}
unset($newSettings['config']);
$saved = $this->settingsModel->saveSettingsFile($newSettings);
@ -160,4 +158,4 @@ final class Settings extends BaseController {
$this->redirect($redirectUrl, 303);
}
}
}

View File

@ -6,12 +6,10 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Controller;
@ -20,24 +18,22 @@ use Aviat\AnimeClient\API\Kitsu\Model;
use Aviat\AnimeClient\API\Kitsu\Transformer\UserTransformer;
use Aviat\AnimeClient\Controller as BaseController;
use Aviat\Ion\Attribute\Controller;
use Aviat\Ion\Attribute\Route;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\ContainerException;
use Aviat\Ion\Di\Exception\NotFoundException;
use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException};
/**
* Controller for handling routes that don't fit elsewhere
*/
final class User extends BaseController {
/**
* @var Model
*/
#[Controller]
final class User extends BaseController
{
private Model $kitsuModel;
/**
* User constructor.
*
* @param ContainerInterface $container
* @throws ContainerException
* @throws NotFoundException
*/
@ -51,6 +47,7 @@ final class User extends BaseController {
/**
* Show the user profile page for the configured user
*/
#[Route('default_user_info', '/me')]
public function me(): void
{
$this->about('me');
@ -58,10 +55,8 @@ final class User extends BaseController {
/**
* Show the user profile page
*
* @param string $username
* @return void
*/
#[Route('user_info', '/user/{username}')]
public function about(string $username): void
{
$isMainUser = $username === 'me';
@ -82,4 +77,4 @@ final class User extends BaseController {
'data' => $data,
]);
}
}
}

View File

@ -6,19 +6,14 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient;
use Aviat\AnimeClient\Enum\EventType;
use Aviat\Ion\Event;
use Aviat\Ion\Json;
use Aura\Router\{
Map,
Matcher,
@ -26,9 +21,10 @@ use Aura\Router\{
Rule,
};
use Aviat\AnimeClient\API\FailedResponseException;
use Aviat\AnimeClient\Enum\EventType;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Friend;
use Aviat\Ion\Type\StringType;
use Aviat\Ion\{Event, Friend, Json};
use LogicException;
use ReflectionException;
@ -37,42 +33,37 @@ use function Aviat\Ion\_dir;
/**
* Basic routing/ dispatch
*/
final class Dispatcher extends RoutingBase {
final class Dispatcher extends RoutingBase
{
/**
* The route-matching object
* @var Map $router
*/
protected Map $router;
/**
* The route matcher
* @var Matcher $matcher
*/
protected Matcher $matcher;
/**
* Routing array
* @var array
*/
protected array $routes;
protected array $routes = [];
/**
* Routes added to router
* @var array $outputRoutes
*/
protected array $outputRoutes;
protected array $outputRoutes = [];
/**
* Constructor
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$router = $this->container->get('aura-router');
$this->router = $router->getMap();
$this->matcher = $router->getMatcher();
$this->routes = $this->config->get('routes');
$this->outputRoutes = $this->setupRoutes();
@ -80,10 +71,8 @@ final class Dispatcher extends RoutingBase {
/**
* Get the current route object, if one matches
*
* @return Route|false
*/
public function getRoute(): Route | false
public function getRoute(): Route|false
{
$logger = $this->container->getLogger();
@ -94,7 +83,7 @@ final class Dispatcher extends RoutingBase {
{
$logger->info('Dispatcher - Routing data from get_route method');
$logger->info(print_r([
'route_path' => $routePath
'route_path' => $routePath,
], TRUE));
}
@ -104,7 +93,7 @@ final class Dispatcher extends RoutingBase {
/**
* Get list of routes applied
*
* @return array
* @return mixed[]
*/
public function getOutputRoutes(): array
{
@ -114,11 +103,9 @@ final class Dispatcher extends RoutingBase {
/**
* Handle the current route
*
* @param object|null $route
* @return void
* @throws ReflectionException
*/
public function __invoke(object $route = NULL): void
public function __invoke(?object $route = NULL): void
{
$logger = $this->container->getLogger();
@ -142,6 +129,7 @@ final class Dispatcher extends RoutingBase {
$actionMethod = $errorRoute['action_method'];
$params = $errorRoute['params'];
$this->call($controllerName, $actionMethod, $params);
return;
}
@ -157,9 +145,8 @@ final class Dispatcher extends RoutingBase {
* Parse out the arguments for the appropriate controller for
* the current route
*
* @param Friend $route
* @throws LogicException
* @return array
* @return array<string, mixed>
*/
protected function processRoute(Friend $route): array
{
@ -185,6 +172,7 @@ final class Dispatcher extends RoutingBase {
if ( ! empty($route->__get('tokens')))
{
$tokens = array_keys($route->__get('tokens'));
foreach ($tokens as $param)
{
if (array_key_exists($param, $route->attributes))
@ -193,6 +181,7 @@ final class Dispatcher extends RoutingBase {
}
}
}
$logger = $this->container->getLogger();
if ($logger !== NULL)
{
@ -202,14 +191,12 @@ final class Dispatcher extends RoutingBase {
return [
'controller_name' => $controllerName,
'action_method' => $actionMethod,
'params' => $params
'params' => $params,
];
}
/**
* Get the type of route, to select the current controller
*
* @return string
*/
public function getController(): string
{
@ -237,7 +224,7 @@ final class Dispatcher extends RoutingBase {
/**
* Get the list of controllers in the default namespace
*
* @return array
* @return mixed[]
*/
public function getControllerList(): array
{
@ -247,6 +234,7 @@ final class Dispatcher extends RoutingBase {
$path = str_replace($find, $replace, $defaultNamespace);
$path = trim($path, '/');
$actualPath = realpath(_dir(SRC_DIR, $path));
$classFiles = glob("{$actualPath}/*.php");
if ($classFiles === FALSE)
@ -259,7 +247,7 @@ final class Dispatcher extends RoutingBase {
foreach ($classFiles as $file)
{
$rawClassName = basename(str_replace('.php', '', $file));
$path = (string)StringType::from($rawClassName)->dasherize();
$path = (string) StringType::from($rawClassName)->dasherize();
$className = trim($defaultNamespace . '\\' . $rawClassName, '\\');
$controllers[$path] = $className;
@ -272,10 +260,7 @@ final class Dispatcher extends RoutingBase {
* Create the controller object and call the appropriate
* method
*
* @param string $controllerName - The full namespace of the controller class
* @param string $method
* @param array $params
* @return void
* @param string $controllerName - The full namespace of the controller class
*/
protected function call(string $controllerName, string $method, array $params): void
{
@ -289,16 +274,19 @@ final class Dispatcher extends RoutingBase {
$logger?->debug('Dispatcher - controller arguments', $params);
$params = array_values($params);
$controller->$method(...$params);
$controller->{$method}(...$params);
}
catch (FailedResponseException)
{
$controllerName = DEFAULT_CONTROLLER;
$controller = new $controllerName($this->container);
$controller->errorPage(500,
$controller->errorPage(
500,
'API request timed out',
'Failed to retrieve data from API (╯°□°)╯︵ ┻━┻');
'Failed to retrieve data from API (╯°□°)╯︵ ┻━┻'
);
}
/* finally
{
// Log out on session/api token expiration
@ -312,6 +300,7 @@ final class Dispatcher extends RoutingBase {
/**
* Get the appropriate params for the error page
* passed on the failed route
* @return mixed[][]
*/
protected function getErrorParams(): array
{
@ -328,12 +317,12 @@ final class Dispatcher extends RoutingBase {
$params = [];
switch($failure->failedRule) {
switch ($failure->failedRule) {
case Rule\Allows::class:
$params = [
'http_code' => 405,
'title' => '405 Method Not Allowed',
'message' => 'Invalid HTTP Verb'
'message' => 'Invalid HTTP Verb',
];
break;
@ -341,7 +330,7 @@ final class Dispatcher extends RoutingBase {
$params = [
'http_code' => 406,
'title' => '406 Not Acceptable',
'message' => 'Unacceptable content type'
'message' => 'Unacceptable content type',
];
break;
@ -353,14 +342,14 @@ final class Dispatcher extends RoutingBase {
return [
'params' => $params,
'action_method' => $actionMethod
'action_method' => $actionMethod,
];
}
/**
* Select controller based on the current url, and apply its relevant routes
*
* @return array
* @return mixed[]
*/
protected function setupRoutes(): array
{
@ -368,6 +357,7 @@ final class Dispatcher extends RoutingBase {
// Add routes
$routes = [];
foreach ($this->routes as $name => &$route)
{
$path = $route['path'];
@ -400,14 +390,15 @@ final class Dispatcher extends RoutingBase {
// Add the route to the router object
if ( ! array_key_exists('tokens', $route))
{
$routes[] = $this->router->$verb($name, $path)->defaults($route);
$routes[] = $this->router->{$verb}($name, $path)->defaults($route);
continue;
}
$tokens = $route['tokens'];
unset($route['tokens']);
$routes[] = $this->router->$verb($name, $path)
$routes[] = $this->router->{$verb}($name, $path)
->defaults($route)
->tokens($tokens);
}
@ -415,4 +406,5 @@ final class Dispatcher extends RoutingBase {
return $routes;
}
}
// End of Dispatcher.php
// End of Dispatcher.php

View File

@ -6,19 +6,18 @@
*
* PHP version 8
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2021 Timothy J. Warren
* @copyright 2015 - 2022 Timothy J. Warren <tim@timshome.page>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
* @link https://git.timshome.page/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Enum;
use Aviat\Ion\Enum;
final class API extends Enum {
final class API extends Enum
{
public const ANILIST = 'anilist';
public const KITSU = 'kitsu';
}
}

Some files were not shown because too many files have changed in this diff Show More