Version 5.1 - All the GraphQL #32

Closed
timw4mail wants to merge 1160 commits from develop into master
24 changed files with 631 additions and 220 deletions
Showing only changes of commit 5e5434d057 - Show all commits

View File

@ -31,14 +31,14 @@ A self-hosted client that allows custom formatting of data from the hummingbird
### Requirements ### Requirements
* PHP 5.4+ * PHP 5.5+
* PDO SQLite (For collection tab) * PDO SQLite (For collection tab)
* GD * GD
### Installation ### Installation
1. Install dependencies via composer: `composer install` 1. Install dependencies via composer: `composer install`
2. Configure settings in `app/config/config.php` and `app/config/routing.php` to your liking 2. Configure settings in `app/config/config.php` to your liking
3. Create the following directories if they don't exist, and make sure they are world writable 3. Create the following directories if they don't exist, and make sure they are world writable
* app/cache * app/cache
* public/images/manga * public/images/manga

View File

@ -7,6 +7,7 @@ namespace Aviat\AnimeClient;
use \Whoops\Handler\PrettyPageHandler; use \Whoops\Handler\PrettyPageHandler;
use \Whoops\Handler\JsonResponseHandler; use \Whoops\Handler\JsonResponseHandler;
use Aura\Html\HelperLocatorFactory;
use \Aura\Web\WebFactory; use \Aura\Web\WebFactory;
use \Aura\Router\RouterFactory; use \Aura\Router\RouterFactory;
use \Aura\Session\SessionFactory; use \Aura\Session\SessionFactory;
@ -51,6 +52,15 @@ $di = function() {
$aura_router = (new RouterFactory())->newInstance(); $aura_router = (new RouterFactory())->newInstance();
$container->set('aura-router', $aura_router); $container->set('aura-router', $aura_router);
// Create Html helper Object
$html_helper = (new HelperLocatorFactory)->newInstance();
$html_helper->set('menu', function() use ($container) {
$menu_helper = new Helper\Menu();
$menu_helper->setContainer($container);
return $menu_helper;
});
$container->set('html-helper', $html_helper);
// Create Request/Response Objects // Create Request/Response Objects
$web_factory = new WebFactory([ $web_factory = new WebFactory([
'_GET' => $_GET, '_GET' => $_GET,
@ -79,4 +89,4 @@ $di = function() {
$di()->get('router')->dispatch(); $di()->get('router')->dispatch();
// End of bootstrap.php // End of bootstrap.php

View File

@ -13,6 +13,7 @@ $base_config = [
'img_cache_path' => _dir(ROOT_DIR, 'public/images'), 'img_cache_path' => _dir(ROOT_DIR, 'public/images'),
// Included config files // Included config files
'routes' => require __DIR__ . '/routes.php',
'database' => require __DIR__ . '/database.php', 'database' => require __DIR__ . '/database.php',
'menus' => require __DIR__ . '/menus.php',
'routes' => require __DIR__ . '/routes.php',
]; ];

View File

@ -14,9 +14,12 @@ $config = [
// General config // General config
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// do you wish to show the anime collection tab? // do you wish to show the anime collection?
'show_anime_collection' => TRUE, 'show_anime_collection' => TRUE,
// do you wish to show the manga collection?
'show_manga_collection' => TRUE,
// path to public directory on the server // path to public directory on the server
'asset_dir' => realpath(__DIR__ . '/../../public'), 'asset_dir' => realpath(__DIR__ . '/../../public'),

61
app/config/menus.php Normal file
View File

@ -0,0 +1,61 @@
<?php
return [
'top' => [
'default' => '',
'items' => [
'anime_list' => '{anime_list}',
'manga_list' => '{manga_list}',
'collection' => '{collection}'
]
],
'view_type' => [
'is_parent' => FALSE,
'default' => 'cover_view',
'items' => [
'cover_view' => '{parent}',
'list_view' => '{parent}/list'
]
],
'anime_list' => [
'default' => '',
'route_prefix' => '/anime',
'items' => [
'watching' => '/watching',
'plan_to_watch' => '/plan_to_watch',
'on_hold' => '/on_hold',
'dropped' => '/dropped',
'completed' => '/completed',
'all' => '/all'
],
'children' => [
'view_type'
]
],
'manga_list' => [
'default' => '',
'route_prefix' => '/manga',
'items' => [
'reading' => '/reading',
'plan_to_read' => '/plan_to_read',
'on_hold' => '/on_hold',
'dropped' => '/dropped',
'completed' => '/completed',
'all' => '/all'
],
'children' => [
'view_type'
]
],
'collection' => [
'default' => '',
'route_prefix' => '/collection',
'items' => [
'anime' => '/anime',
'manga' => '/manga',
],
'children' => [
'view_type'
]
]
];

View File

@ -9,99 +9,56 @@ return [
// Routes on all controllers // Routes on all controllers
'common' => [ 'common' => [
'update' => [ 'update' => [
'path' => '/update', 'path' => '/{controller}/update',
'action' => ['update'], 'action' => 'update',
'verb' => 'post' 'verb' => 'post'
], ],
'login_form' => [ 'login_form' => [
'path' => '/login', 'path' => '/{controller}/login',
'action' => ['login'], 'action' => 'login',
'verb' => 'get' 'verb' => 'get'
], ],
'login_action' => [ 'login_action' => [
'path' => '/login', 'path' => '/{controller}/login',
'action' => ['login_action'], 'action' => 'login_action',
'verb' => 'post' 'verb' => 'post'
], ],
'logout' => [ 'logout' => [
'path' => '/logout', 'path' => '/{controller}/logout',
'action' => ['logout'] 'action' => 'logout'
], ],
], ],
// Routes on collection controller // Routes on collection controller
'collection' => [ 'collection' => [
'collection_add_form' => [ 'collection_add_form' => [
'path' => '/collection/add', 'path' => '/collection/add',
'action' => ['form'], 'action' => 'form',
'params' => [], 'params' => [],
], ],
'collection_edit_form' => [ 'collection_edit_form' => [
'path' => '/collection/edit/{id}', 'path' => '/collection/edit/{id}',
'action' => ['form'], 'action' => 'form',
'tokens' => [ 'tokens' => [
'id' => '[0-9]+' 'id' => '[0-9]+'
] ]
], ],
'collection_add' => [ 'collection_add' => [
'path' => '/collection/add', 'path' => '/collection/add',
'action' => ['add'], 'action' => 'add',
'verb' => 'post' 'verb' => 'post'
], ],
'collection_edit' => [ 'collection_edit' => [
'path' => '/collection/edit', 'path' => '/collection/edit',
'action' => ['edit'], 'action' => 'edit',
'verb' => 'post' 'verb' => 'post'
], ],
'collection' => [ 'collection' => [
'path' => '/collection/view{/view}', 'path' => '/collection/view{/view}',
'action' => ['index'], 'action' => 'index',
'params' => [], 'params' => [],
'tokens' => [ 'tokens' => [
'view' => '[a-z_]+' 'view' => '[a-z_]+'
] ]
], ],
], ],
// Routes on anime controller
'anime' => [
'index' => [
'path' => '/',
'action' => ['redirect'],
'params' => [
'url' => '', // Determined by config
'code' => '301',
'type' => 'anime'
]
],
'search' => [
'path' => '/anime/search',
'action' => ['search'],
],
'anime_list' => [
'path' => '/anime/{type}{/view}',
'action' => ['anime_list'],
'tokens' => [
'type' => '[a-z_]+',
'view' => '[a-z_]+'
]
],
],
'manga' => [
'index' => [
'path' => '/',
'action' => ['redirect'],
'params' => [
'url' => '', // Determined by config
'code' => '301',
'type' => 'manga'
]
],
'manga_list' => [
'path' => '/manga/{type}{/view}',
'action' => ['manga_list'],
'tokens' => [
'type' => '[a-z_]+',
'view' => '[a-z_]+'
]
]
]
]; ];

View File

@ -5,7 +5,7 @@
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
return [ return [
// Subfolder prefix for url // Subfolder prefix for url, if in a subdirectory of the web root
'subfolder_prefix' => '', 'subfolder_prefix' => '',
// Path to public directory, where images/css/javascript are located, // Path to public directory, where images/css/javascript are located,
@ -16,9 +16,9 @@ return [
'default_list' => 'anime', // anime or manga 'default_list' => 'anime', // anime or manga
// Default pages for anime/manga // Default pages for anime/manga
'default_anime_path' => "/anime/watching", 'default_anime_list_path' => "watching", // watching|plan_to_watch|on_hold|dropped|completed|all
'default_manga_path' => '/manga/all', 'default_manga_list_path' => "all", // reading|plan_to_read|on_hold|dropped|completed|all
// Default to list view? // Default view type (cover_view/list_view)
'default_to_list_view' => FALSE, 'default_view_type' => 'cover_view',
]; ];

View File

@ -11,6 +11,6 @@
<template name="clean" /> <template name="clean" />
</transformations> </transformations>
<files> <files>
<directory>src/Aviat/AnimeClient</directory> <directory>src/Aviat</directory>
</files> </files>
</phpdoc> </phpdoc>

View File

@ -4,8 +4,6 @@
*/ */
namespace Aviat\AnimeClient; namespace Aviat\AnimeClient;
use Aura\Web\ResponseSender;
use \Aviat\Ion\Di\ContainerInterface; use \Aviat\Ion\Di\ContainerInterface;
use \Aviat\Ion\View\HttpView; use \Aviat\Ion\View\HttpView;
use \Aviat\Ion\View\HtmlView; use \Aviat\Ion\View\HtmlView;
@ -97,6 +95,7 @@ class Controller {
public function load_partial($view, $template, $data=[]) public function load_partial($view, $template, $data=[])
{ {
$errorHandler = $this->container->get('error-handler'); $errorHandler = $this->container->get('error-handler');
$errorHandler->addDataTable('Template Data', $data);
$router = $this->container->get('router'); $router = $this->container->get('router');
if (isset($this->base_data)) if (isset($this->base_data))
@ -107,7 +106,7 @@ class Controller {
$route = $router->get_route(); $route = $router->get_route();
$data['route_path'] = ($route) ? $router->get_route()->path : ""; $data['route_path'] = ($route) ? $router->get_route()->path : "";
$errorHandler->addDataTable('Template Data', $data);
$template_path = _dir($this->config->__get('view_path'), "{$template}.php"); $template_path = _dir($this->config->__get('view_path'), "{$template}.php");
if ( ! is_file($template_path)) if ( ! is_file($template_path))
@ -170,7 +169,7 @@ class Controller {
{ {
$url = $this->urlGenerator->full_url($path, $type); $url = $this->urlGenerator->full_url($path, $type);
$http = new HttpView($this->container); $http = new HttpView($this->container);
$http->redirect($url, $code); $http->redirect($url, $code);
} }

View File

@ -7,6 +7,7 @@ namespace Aviat\AnimeClient\Controller;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\AnimeClient\Controller as BaseController; use Aviat\AnimeClient\Controller as BaseController;
use Aviat\AnimeClient\Enum\Hummingbird\AnimeWatchingStatus;
use Aviat\AnimeClient\Model\Anime as AnimeModel; use Aviat\AnimeClient\Model\Anime as AnimeModel;
use Aviat\AnimeClient\Model\AnimeCollection as AnimeCollectionModel; use Aviat\AnimeClient\Model\AnimeCollection as AnimeCollectionModel;
@ -72,6 +73,11 @@ class Anime extends BaseController {
]); ]);
} }
public function index($type="watching", $view='')
{
return $this->anime_list($type, $view);
}
/** /**
* Search for anime * Search for anime
* *
@ -87,11 +93,10 @@ class Anime extends BaseController {
* Show a portion, or all of the anime list * Show a portion, or all of the anime list
* *
* @param string $type - The section of the list * @param string $type - The section of the list
* @param string $title - The title of the page
* @param string $view - List or cover view * @param string $view - List or cover view
* @return void * @return void
*/ */
public function anime_list($type, $view) protected function anime_list($type, $view)
{ {
$type_title_map = [ $type_title_map = [
'all' => 'All', 'all' => 'All',
@ -103,12 +108,12 @@ class Anime extends BaseController {
]; ];
$model_map = [ $model_map = [
'watching' => 'currently-watching', 'watching' => AnimeWatchingStatus::WATCHING,
'plan_to_watch' => 'plan-to-watch', 'plan_to_watch' => AnimeWatchingStatus::PLAN_TO_WATCH,
'on_hold' => 'on-hold', 'on_hold' => AnimeWatchingStatus::ON_HOLD,
'all' => 'all', 'all' => 'all',
'dropped' => 'dropped', 'dropped' => AnimeWatchingStatus::DROPPED,
'completed' => 'completed' 'completed' => AnimeWatchingStatus::COMPLETED
]; ];
$title = $this->config->whose_list . "'s Anime List &middot; {$type_title_map[$type]}"; $title = $this->config->whose_list . "'s Anime List &middot; {$type_title_map[$type]}";

View File

@ -58,6 +58,11 @@ class Manga extends Controller {
]); ]);
} }
public function index($status="all", $view="")
{
return $this->manga_list($status, $view);
}
/** /**
* Update an anime item * Update an anime item
* *
@ -75,15 +80,15 @@ class Manga extends Controller {
* @param string $view * @param string $view
* @return void * @return void
*/ */
public function manga_list($status, $view) protected function manga_list($status, $view)
{ {
$map = [ $map = [
'all' => 'All', 'all' => 'All',
'plan_to_read' => 'Plan to Read', 'plan_to_read' => MangaModel::PLAN_TO_READ,
'reading' => 'Reading', 'reading' => MangaModel::READING,
'completed' => 'Completed', 'completed' => MangaModel::COMPLETED,
'dropped' => 'Dropped', 'dropped' => MangaModel::DROPPED,
'on_hold' => 'On Hold' 'on_hold' => MangaModel::ON_HOLD
]; ];
$title = $this->config->whose_list . "'s Manga List &middot; {$map[$status]}"; $title = $this->config->whose_list . "'s Manga List &middot; {$map[$status]}";
@ -97,10 +102,12 @@ class Manga extends Controller {
? [$map[$status] => $this->model->get_list($map[$status])] ? [$map[$status] => $this->model->get_list($map[$status])]
: $this->model->get_all_lists(); : $this->model->get_all_lists();
//throw new \ErrorException("Data :" . print_r($data, TRUE));
$this->outputHTML('manga/' . $view_map[$view], [ $this->outputHTML('manga/' . $view_map[$view], [
'title' => $title, 'title' => $title,
'sections' => $data, 'sections' => $data,
]); ]);
} }
} }
// End of MangaController.php // End of MangaController.php

View File

@ -0,0 +1,20 @@
<?php
namespace Aviat\AnimeClient\Helper;
use Aura\Html\Helper\AbstractHelper;
use Aviat\AnimeClient\MenuGenerator;
class Menu extends AbstractHelper {
use \Aviat\Ion\Di\ContainerAware;
public function __invoke($menu_name)
{
$generator = new MenuGenerator($this->container);
return $generator->generate($menu_name);
}
}
// End of Menu.php

View File

@ -0,0 +1,19 @@
<?php
namespace Aviat\AnimeClient\Helper;
use Aura\Html\Helper\AbstractHelper;
use Aviat\AnimeClient\UrlGenerator;
class UrlHelper extends AbstractHelper {
/**
* Helper entry point
*
* @return UrlHelper
*/
public function __invoke()
{
return $this;
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace Aviat\AnimeClient;
use Aviat\Ion\Di\ContainerInterface;
/**
* Helper object to manage menu creation and selection
*/
class MenuGenerator extends RoutingBase {
use \Aviat\Ion\Di\ContainerAware;
use \Aviat\Ion\StringWrapper;
use \Aviat\Ion\ArrayWrapper;
/**
* Html generation helper
*
* @var Aura\Html\HelperLocator
*/
protected $helper;
/**
* Menu config array
*
* @var array
*/
protected $menus;
/**
* Create menu generator
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->menus = $this->config->menus;
$this->helper = $container->get('html-helper');
}
/**
* Generate the full menu structure from the config files
*
* @return array
*/
protected function parse_config()
{
// Note: Children menus have urls based on the
// current url path
/*
$parsed = [
'menu_name' => [
'items' => [
'title' => 'full_url_path',
],
'children' => [
'title' => 'full_url_path'
]
]
]
*/
$parsed = [];
foreach($this->menus as $name => $menu)
{
$parsed[$name] = [];
foreach($menu['items'] as $path_name => $partial_path)
{
$title = $this->string($path_name)->humanize()->titleize();
$parsed[$name]['items'][$title] = $this->string($menu['route_prefix'])->append($partial_path);
}
// @TODO: Handle child menu(s)
if (count($menu['children']) > 0)
{
}
}
return $parsed;
}
/**
* Generate the html structure of the menu selected
*
* @param string $menu
* @return string
*/
public function generate($menu)
{
$parsed_config = $this->parse_config();
$menu_config = $parsed_config[$menu];
// Array of list items to add to the main menu
$main_menu = [];
// Start the menu list
$helper->ul();
}
}
// End of MenuGenerator.php

View File

@ -4,14 +4,16 @@
*/ */
namespace Aviat\AnimeClient; namespace Aviat\AnimeClient;
use Aviat\Ion\Di\ContainerInterface;
use abeautifulsite\SimpleImage; use abeautifulsite\SimpleImage;
use Aviat\Ion\Di\ContainerInterface;
/** /**
* Common base for all Models * Common base for all Models
*/ */
class Model { class Model {
use \Aviat\Ion\StringWrapper;
/** /**
* The global configuration object * The global configuration object
* @var Config * @var Config
@ -65,11 +67,11 @@ class Model {
// Cache the file if it doesn't already exist // Cache the file if it doesn't already exist
if ( ! file_exists($cached_path)) if ( ! file_exists($cached_path))
{ {
if (ini_get('allow_url_fopen')) /*if (ini_get('allow_url_fopen'))
{ {
copy($api_path, $cached_path); copy($api_path, $cached_path);
} }
elseif (function_exists('curl_init')) else*/if (function_exists('curl_init'))
{ {
$ch = curl_init($api_path); $ch = curl_init($api_path);
$fp = fopen($cached_path, 'wb'); $fp = fopen($cached_path, 'wb');
@ -79,7 +81,7 @@ class Model {
]); ]);
curl_exec($ch); curl_exec($ch);
curl_close($ch); curl_close($ch);
fclose($ch); fclose($fp);
} }
else else
{ {

View File

@ -4,8 +4,10 @@
*/ */
namespace Aviat\AnimeClient\Model; namespace Aviat\AnimeClient\Model;
use \GuzzleHttp\Client; use GuzzleHttp\Client;
use \GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Psr7\Request;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\AnimeClient\Model as BaseModel; use Aviat\AnimeClient\Model as BaseModel;
@ -42,7 +44,8 @@ class API extends BaseModel {
parent::__construct($container); parent::__construct($container);
$this->cookieJar = new CookieJar(); $this->cookieJar = new CookieJar();
$this->client = new Client([ $this->client = new Client([
'base_url' => $this->base_url, 'base_uri' => $this->base_url,
'cookies' => TRUE,
'defaults' => [ 'defaults' => [
'cookies' => $this->cookieJar, 'cookies' => $this->cookieJar,
'headers' => [ 'headers' => [

View File

@ -27,6 +27,18 @@ class Anime extends API {
*/ */
protected $base_url = "https://hummingbird.me/api/v1/"; protected $base_url = "https://hummingbird.me/api/v1/";
/**
* Map of API status constants to display constants
* @var array
*/
protected $const_map = [
AnimeWatchingStatus::WATCHING => self::WATCHING,
AnimeWatchingStatus::PLAN_TO_WATCH => self::PLAN_TO_WATCH,
AnimeWatchingStatus::ON_HOLD => self::ON_HOLD,
AnimeWatchingStatus::DROPPED => self::DROPPED,
AnimeWatchingStatus::COMPLETED => self::COMPLETED,
];
/** /**
* Update the selected anime * Update the selected anime
* *
@ -59,32 +71,11 @@ class Anime extends API {
self::COMPLETED => [], self::COMPLETED => [],
]; ];
$data = $this->_get_list(); $data = $this->_get_list_from_api();
foreach($data as $datum) foreach($data as $datum)
{ {
switch($datum['status']) $output[$this->const_map[$datum['watching_status']]][] = $datum;
{
case AnimeWatchingStatus::COMPLETED:
$output[self::COMPLETED][] = $datum;
break;
case AnimeWatchingStatus::PLAN_TO_WATCH:
$output[self::PLAN_TO_WATCH][] = $datum;
break;
case AnimeWatchingStatus::DROPPED:
$output[self::DROPPED][] = $datum;
break;
case AnimeWatchingStatus::ON_HOLD:
$output[self::ON_HOLD][] = $datum;
break;
case AnimeWatchingStatus::WATCHING:
$output[self::WATCHING][] = $datum;
break;
}
} }
// Sort anime by name // Sort anime by name
@ -104,19 +95,11 @@ class Anime extends API {
*/ */
public function get_list($status) public function get_list($status)
{ {
$map = [
AnimeWatchingStatus::WATCHING => self::WATCHING,
AnimeWatchingStatus::PLAN_TO_WATCH => self::PLAN_TO_WATCH,
AnimeWatchingStatus::ON_HOLD => self::ON_HOLD,
AnimeWatchingStatus::DROPPED => self::DROPPED,
AnimeWatchingStatus::COMPLETED => self::COMPLETED,
];
$data = $this->_get_list_From_api($status); $data = $this->_get_list_From_api($status);
$this->sort_by_name($data); $this->sort_by_name($data);
$output = []; $output = [];
$output[$map[$status]] = $data; $output[$this->const_map[$status]] = $data;
return $output; return $output;
} }
@ -170,10 +153,11 @@ class Anime extends API {
/** /**
* Retrieve data from the api * Retrieve data from the api
* *
* @codeCoverageIgnore
* @param string $status * @param string $status
* @return array * @return array
*/ */
private function _get_list_from_api($status="all") protected function _get_list_from_api($status="all")
{ {
$config = [ $config = [
'allow_redirects' => FALSE 'allow_redirects' => FALSE
@ -198,29 +182,30 @@ class Anime extends API {
/** /**
* Handle caching of transformed api data * Handle caching of transformed api data
* *
* @codeCoverageIgnore
* @param string $status * @param string $status
* @param \GuzzleHttp\Message\Response * @param \GuzzleHttp\Message\Response
* @return array * @return array
*/ */
private function _check_cache($status, $response) protected function _check_cache($status, $response)
{ {
$cache_file = "{$this->config->data_cache_path}/anime-{$status}.json"; $cache_file = _dir($this->config->data_cache_path, "anime-{$status}.json");
$transformed_cache_file = "{$this->config->data_cache_path}/anime-{$status}-transformed.json"; $transformed_cache_file = _dir($this->config->data_cache_path, "anime-{$status}-transformed.json");
$cached = json_decode(file_get_contents($cache_file), TRUE); $cached = json_decode(file_get_contents($cache_file), TRUE);
$api = $response->json(); $api_data = json_decode($response->getBody(), TRUE);
if ($api !== $cached) if ($api_data === $cached && file_exists($transformed_cache_file))
{ {
file_put_contents($cache_file, json_encode($api)); return json_decode(file_get_contents($transformed_cache_file),TRUE);
$transformer = new AnimeListTransformer();
$transformed = $transformer->transform_collection($api);
file_put_contents($transformed_cache_file, json_encode($transformed));
return $transformed;
} }
else else
{ {
return json_decode(file_get_contents($transformed_cache_file),TRUE); file_put_contents($cache_file, json_encode($api_data));
$transformer = new AnimeListTransformer();
$transformed = $transformer->transform_collection($api_data);
file_put_contents($transformed_cache_file, json_encode($transformed));
return $transformed;
} }
} }
@ -230,7 +215,7 @@ class Anime extends API {
* @param array $array * @param array $array
* @return void * @return void
*/ */
private function sort_by_name(&$array) protected function sort_by_name(&$array)
{ {
$sort = array(); $sort = array();

View File

@ -6,12 +6,31 @@ namespace Aviat\AnimeClient\Model;
use Aviat\AnimeClient\Model\API; use Aviat\AnimeClient\Model\API;
use Aviat\AnimeClient\Transformer\Hummingbird; use Aviat\AnimeClient\Transformer\Hummingbird;
use Aviat\AnimeClient\Enum\Hummingbird\MangaReadingStatus;
/** /**
* Model for handling requests dealing with the manga list * Model for handling requests dealing with the manga list
*/ */
class Manga extends API { class Manga extends API {
const READING = 'Reading';
const PLAN_TO_READ = 'Plan to Read';
const DROPPED = 'Dropped';
const ON_HOLD = 'On Hold';
const COMPLETED = 'Completed';
/**
* Map API constants to display constants
* @var array
*/
protected $const_map = [
MangaReadingStatus::READING => self::READING,
MangaReadingStatus::PLAN_TO_READ => self::PLAN_TO_READ,
MangaReadingStatus::ON_HOLD => self::ON_HOLD,
MangaReadingStatus::DROPPED => self::DROPPED,
MangaReadingStatus::COMPLETED => self::COMPLETED
];
/** /**
* The base url for api requests * The base url for api requests
* @var string * @var string
@ -44,9 +63,9 @@ class Manga extends API {
*/ */
public function get_all_lists() public function get_all_lists()
{ {
$data = $this->_get_list(); $data = $this->_get_list_from_api();
foreach ($data as $key => &$val) foreach($data as $key => &$val)
{ {
$this->sort_by_name($val); $this->sort_by_name($val);
} }
@ -62,24 +81,14 @@ class Manga extends API {
*/ */
public function get_list($status) public function get_list($status)
{ {
$data = $this->_get_list($status); $data = $this->_get_list_from_api($status);
$this->sort_by_name($data); $this->sort_by_name($data);
return $data; return $data;
} }
/** private function _get_list_from_api($status="All")
* Massage the list of manga entries into something more usable
*
* @param string $status
* @return array
*/
private function _get_list($status="all")
{ {
$errorHandler = $this->container->get('error-handler');
$cache_file = _dir($this->config->data_cache_path, 'manga.json');
$config = [ $config = [
'query' => [ 'query' => [
@ -89,81 +98,70 @@ class Manga extends API {
]; ];
$response = $this->client->get('manga_library_entries', $config); $response = $this->client->get('manga_library_entries', $config);
$data = $this->_check_cache($status, $response);
$output = $this->map_by_status($data);
$errorHandler->addDataTable('response', (array)$response); return (array_key_exists($status, $output)) ? $output[$status] : $output;
}
if ($response->getStatusCode() != 200) /**
* Check the status of the cache and return the appropriate response
*
* @param string $status
* @param \GuzzleHttp\Message\Response $response
* @return array
*/
private function _check_cache($status, $response)
{
// Bail out early if there isn't any manga data
$api_data = json_decode($response->getBody(), TRUE);
if ( ! array_key_exists('manga', $api_data)) return [];
$cache_file = _dir($this->config->data_cache_path, 'manga.json');
$transformed_cache_file = _dir($this->config->data_cache_path, 'manga-transformed.json');
$cached_data = json_decode(file_get_contents($cache_file), TRUE);
if ($cached_data === $api_data && file_exists($transformed_cache_file))
{ {
if ( ! file_exists($cache_file)) return json_decode(file_get_contents($transformed_cache_file), TRUE);
{
throw new DomainException($response->getEffectiveUrl());
}
else
{
$raw_data = json_decode(file_get_contents($cache_file), TRUE);
}
} }
else else
{ {
// Reorganize data to be more usable file_put_contents($cache_file, json_encode($api_data));
$raw_data = $response->json();
// Attempt to create the cache dir if it doesn't exist $zippered_data = $this->zipper_lists($api_data);
if ( ! is_dir($this->config->data_cache_path)) $transformer = new Hummingbird\MangaListTransformer();
{ $transformed_data = $transformer->transform_collection($zippered_data);
mkdir($this->config->data_cache_path); file_put_contents($transformed_cache_file, json_encode($transformed_data));
} return $transformed_data;
// Cache data in case of downtime
file_put_contents($cache_file, json_encode($raw_data));
} }
}
// Bail out early if there isn't any manga data /**
if ( ! array_key_exists('manga', $raw_data)) return []; * Map transformed anime data to be organized by reading status
*
$data = [ * @param array $data
'Reading' => [], * @return array
'Plan to Read' => [], */
'On Hold' => [], private function map_by_status($data)
'Dropped' => [], {
'Completed' => [], $output = [
self::READING => [],
self::PLAN_TO_READ => [],
self::ON_HOLD => [],
self::DROPPED => [],
self::COMPLETED => [],
]; ];
// Massage the two lists into one foreach($data as &$entry)
$manga_data = $this->zipper_lists($raw_data);
// Filter data by status
foreach($manga_data as &$entry)
{ {
// Cache poster images $entry['manga']['image'] = $this->get_cached_image($entry['manga']['image'], $entry['manga']['slug'], 'manga');
$entry['manga']['poster_image'] = $this->get_cached_image($entry['manga']['poster_image'], $entry['manga']['id'], 'manga'); $key = $this->const_map[$entry['reading_status']];
$output[$key][] = $entry;
switch($entry['status'])
{
case "Plan to Read":
$data['Plan to Read'][] = $entry;
break;
case "Dropped":
$data['Dropped'][] = $entry;
break;
case "On Hold":
$data['On Hold'][] = $entry;
break;
case "Currently Reading":
$data['Reading'][] = $entry;
break;
case "Completed":
default:
$data['Completed'][] = $entry;
break;
}
} }
return (array_key_exists($status, $data)) ? $data[$status] : $data; return $output;
} }
/** /**
@ -189,10 +187,10 @@ class Manga extends API {
foreach($array as $key => $item) foreach($array as $key => $item)
{ {
$sort[$key] = $item['manga']['romaji_title']; $sort[$key] = $item['manga']['title'];
} }
array_multisort($sort, SORT_ASC, $array); array_multisort($sort, SORT_ASC, $array);
} }
} }
// End of MangaModel.php // End of MangaModel.php

View File

@ -0,0 +1,21 @@
<?php
namespace Aviat\Ion;
use Aviat\Ion\Type\ArrayType;
trait ArrayWrapper {
/**
* Convenience method for wrapping an array
* with the array type class
*
* @param array $arr
* @return ArrayType
*/
public function arr(array $arr)
{
return new ArrayType($arr);
}
}
// End of ArrayWrapper.php

View File

@ -2,7 +2,7 @@
namespace Aviat\Ion; namespace Aviat\Ion;
use Stringy\Stringy as S; use Aviat\Ion\Type\StringType;
trait StringWrapper { trait StringWrapper {
@ -10,11 +10,11 @@ trait StringWrapper {
* Wrap the String in the Stringy class * Wrap the String in the Stringy class
* *
* @param string $str * @param string $str
* @return Stringy\Stringy * @return StringType
*/ */
public function string($str) public function string($str)
{ {
return S::create($str); return StringType::create($str);
} }
} }
// End of StringWrapper.php // End of StringWrapper.php

View File

@ -0,0 +1,142 @@
<?php
namespace Aviat\Ion\Type;
/**
* Wrapper class for native array methods for convenience
*/
class ArrayType {
/**
* The current array
*
* @var array
*/
protected $arr;
/**
* Map generated methods to their native implementations
*
* @var array
*/
protected $native_methods = [
'chunk' => 'array_chunk',
'pluck' => 'array_column',
'assoc_diff' => 'array_diff_assoc',
'key_diff' => 'array_diff_key',
'diff' => 'array_diff',
'filter' => 'array_filter',
'flip' => 'array_flip',
'intersect' => 'array_intersect',
'has_key' => 'array_key_exists',
'keys' => 'array_keys',
'merge' => 'array_merge',
'pad' => 'array_pad',
'pop' => 'array_pop',
'product' => 'array_product',
'push' => 'array_push',
'random' => 'array_rand',
'reduce' => 'array_reduce',
'reverse' => 'array_reverse',
'shift' => 'array_shift',
'sum' => 'array_sum',
'unique' => 'array_unique',
'unshift' => 'array_unshift',
'values' => 'array_values',
];
/**
* Native methods that modify the passed in array
*
* @var array
*/
protected $native_in_place_methods = [
'shuffle' => 'shuffle',
];
/**
* Create an ArrayType wrapper class
*
* @param array $arr
*/
public function __construct(Array $arr)
{
$this->arr =& $arr;
}
/**
* Call one of the dynamically created methods
*
* @param string $method
* @param array $args
* @return mixed
*/
public function __call($method, $args)
{
// Simple mapping for the majority of methods
if (array_key_exists($method, $this->native_methods))
{
$func = $this->native_methods[$method];
// Set the current array as the first argument of the method
array_unshift($args, $this->arr);
return call_user_func_array($func, $args);
}
// Mapping for in-place methods
if (array_key_exists($method, $this->native_in_place_methods))
{
$func = $this->native_in_place_methods[$method];
$func($this->arr);
return $this->arr;
}
}
/**
* Fill an array with the specified value
*
* @param int $start_index
* @param int $num
* @param mixed $value
* @return array
*/
public function fill($start_index, $num, $value)
{
return array_fill($start_index, $num, $value);
}
/**
* Call a callback on each item of the array
*
* @param callable $callback
* @return array
*/
public function map(callable $callback)
{
return array_map($callback, $this->arr);
}
/**
* Find an array key by its associated value
*
* @param mixed $value
* @param bool $strict
* @return string
*/
public function search($value, $strict=FALSE)
{
return array_search($value, $this->arr, $strict);
}
/**
* Determine if the array has the passed value
*
* @param mixed $value
* @param bool $strict
* @return bool
*/
public function has($value, $strict=FALSE)
{
return in_array($value, $this->arr, $strict);
}
}
// End of ArrayType.php

View File

@ -0,0 +1,10 @@
<?php
namespace Aviat\Ion\Type;
use Stringy\Stringy;
class StringType extends Stringy {
}
// End of StringType.php

View File

@ -2,19 +2,27 @@
namespace Aviat\Ion\View; namespace Aviat\Ion\View;
use Aura\Html\HelperLocatorFactory;
use Aviat\Ion\View\HttpView; use Aviat\Ion\View\HttpView;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
class HtmlView extends HttpView { class HtmlView extends HttpView {
/**
* HTML generator/escaper helper
*
* @var Aura\Html\HelperLocator
*/
protected $helper; protected $helper;
/**
* Create the Html View
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container) public function __construct(ContainerInterface $container)
{ {
parent::__construct($container); parent::__construct($container);
$this->helper = (new HelperLocatorFactory)->newInstance(); $this->helper = $container->get('html-helper');
} }
/** /**

View File

@ -0,0 +1,54 @@
<?php
class ArrayTypeTest extends AnimeClient_TestCase {
use Aviat\Ion\ArrayWrapper;
public function setUp()
{
parent::setUp();
}
public function testMerge()
{
$obj = $this->arr([1, 3, 5, 7]);
$even_array = [2, 4, 6, 8];
$expected = [1, 3, 5, 7, 2, 4, 6, 8];
$actual = $obj->merge($even_array);
$this->assertEquals($expected, $actual);
}
public function testShuffle()
{
$original = [1, 2, 3, 4];
$test = [1, 2, 3, 4];
$obj = $this->arr($test);
$actual = $obj->shuffle();
$this->assertNotEquals($actual, $original);
$this->assertTrue(is_array($actual));
}
public function testHas()
{
$obj = $this->arr([1, 2, 6, 8, 11]);
$this->assertTrue($obj->has(8));
$this->assertFalse($obj->has(8745));
}
public function testSearch()
{
$obj = $this->arr([1, 2, 5, 7, 47]);
$actual = $obj->search(47);
$this->assertEquals(4, $actual);
}
public function testFill()
{
$obj = $this->arr([]);
$expected = ['?', '?', '?'];
$actual = $obj->fill(0, 3, '?');
$this->assertEquals($actual, $expected);
}
}