Big Work in progress commit
timw4mail/HummingBirdAnimeClient/pipeline/head There was a failure building this commit Details

This commit is contained in:
Timothy Warren 2020-04-21 19:22:56 -04:00
parent 17fb2b4db4
commit da570d5167
28 changed files with 601 additions and 92 deletions

View File

@ -193,6 +193,16 @@ $routes = [
'username' => '.*?'
]
],
'anime_history' => [
'controller' => 'history',
'path' => '/history/anime',
'action' => 'anime',
],
'manga_history' => [
'controller' => 'history',
'path' => '/history/manga',
'action' => 'manga',
],
// ---------------------------------------------------------------------
// Default / Shared routes
// ---------------------------------------------------------------------

View File

@ -0,0 +1,20 @@
<main class="details fixed">
<?php if (empty($items)): ?>
<h3>No recent watch history.</h3>
<?php else: ?>
<section>
<?php foreach ($items as $name => $item): ?>
<article class="flex flex-no-wrap flex-justify-start">
<section class="flex-self-center history-img"><?= $helper->picture(
$item['coverImg'],
'jpg',
['width' => '110px', 'height' => '156px'],
['width' => '110px', 'height' => '156px']
) ?></section>
<section class="flex-self-center"><?= $item['action'] ?></section>
</article>
<?php endforeach ?>
</section>
<pre><?= print_r($items, TRUE) ?></pre>
<?php endif ?>
</main>

View File

@ -5,8 +5,8 @@ namespace Aviat\AnimeClient;
$whose = $config->get('whose_list') . "'s ";
$lastSegment = $urlGenerator->lastSegment();
$extraSegment = $lastSegment === 'list' ? '/list' : '';
$hasAnime = stripos($_SERVER['REQUEST_URI'], 'anime') !== FALSE;
$hasManga = stripos($_SERVER['REQUEST_URI'], 'manga') !== FALSE;
$hasAnime = stripos($_SERVER['REQUEST_URI'], 'anime') === 1;
$hasManga = stripos($_SERVER['REQUEST_URI'], 'manga') === 1;
?>
<div id="main-nav" class="flex flex-align-end flex-wrap">

View File

@ -888,6 +888,11 @@ aside picture, aside img {
filter: drop-shadow(0 -1px 4px #fff);
}
.history-img {
width: 110px;
height: 156px;
}
/* ----------------------------------------------------------------------------
Settings Form
-----------------------------------------------------------------------------*/

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

@ -16,6 +16,8 @@
namespace Aviat\AnimeClient\API;
use function in_array;
/**
* Class encapsulating Json API data structure for a request or response
*/
@ -105,7 +107,7 @@ final class JsonAPI {
$relationship =& $item['relationships'][$relType];
unset($relationship['data']);
if (\in_array($relType, $singular, TRUE))
if (in_array($relType, $singular, TRUE))
{
$relationship = $included[$dataType][$idKey];
continue;
@ -202,11 +204,11 @@ final class JsonAPI {
{
foreach($items as $id => $item)
{
if (array_key_exists('relationships', $item) && \is_array($item['relationships']))
if (array_key_exists('relationships', $item) && is_array($item['relationships']))
{
foreach($item['relationships'] as $relType => $props)
{
if (array_key_exists('data', $props) && \is_array($props['data']) && array_key_exists('id', $props['data']))
if (array_key_exists('data', $props) && is_array($props['data']) && array_key_exists('id', $props['data']))
{
$idKey = $props['data']['id'];
$dataType = $props['data']['type'];
@ -340,7 +342,7 @@ final class JsonAPI {
foreach ($data['data'] as $item)
{
if (\is_array($item) && array_key_exists('id', $item))
if (is_array($item) && array_key_exists('id', $item))
{
$organized[$key][] = $item['id'];
}

View File

@ -164,9 +164,7 @@ final class Kitsu {
];
}
usort($links, function ($a, $b) {
return $a['meta']['name'] <=> $b['meta']['name'];
});
usort($links, fn ($a, $b) => $a['meta']['name'] <=> $b['meta']['name']);
return $links;
}

View File

@ -91,9 +91,9 @@ final class Auth {
$cacheItem->save();
// Set the token expiration in the cache
$expire_time = $auth['created_at'] + $auth['expires_in'];
$expireTime = $auth['created_at'] + $auth['expires_in'];
$cacheItem = $this->cache->getItem(K::AUTH_TOKEN_EXP_CACHE_KEY);
$cacheItem->set($expire_time);
$cacheItem->set($expireTime);
$cacheItem->save();
// Set the refresh token in the cache
@ -103,7 +103,7 @@ final class Auth {
// Set the session values
$this->segment->set('auth_token', $auth['access_token']);
$this->segment->set('auth_token_expires', $expire_time);
$this->segment->set('auth_token_expires', $expireTime);
$this->segment->set('refresh_token', $auth['refresh_token']);
return TRUE;

View File

@ -176,7 +176,7 @@ trait KitsuTrait {
$logger->warning('Non 200 response for api call', (array)$response);
}
throw new FailedResponseException('Failed to get the proper response from the API');
// throw new FailedResponseException('Failed to get the proper response from the API');
}
try

View File

@ -31,6 +31,7 @@ use Aviat\AnimeClient\API\Enum\{
};
use Aviat\AnimeClient\API\Mapping\{AnimeWatchingStatus, MangaReadingStatus};
use Aviat\AnimeClient\API\Kitsu\Transformer\{
AnimeHistoryTransformer,
AnimeTransformer,
AnimeListTransformer,
MangaTransformer,
@ -173,6 +174,38 @@ final class Model {
return FALSE;
}
/**
* Retrieve the data for the anime watch history page
*
* @return array
* @throws InvalidArgumentException
* @throws Throwable
*/
public function getAnimeHistory(): array
{
$raw = $this->getRawHistoryList('anime');
$organized = JsonAPI::organizeData($raw);
$transformer = new AnimeHistoryTransformer();
$transformer->setContainer($this->getContainer());
return $transformer->transform($organized);
}
/**
* Retrieve the data for the manga read history page
*
* @return array
* @throws InvalidArgumentException
* @throws Throwable
*/
public function getMangaHistory(): array
{
$raw = $this->getRawHistoryList('manga');
return JsonAPI::organizeData($raw);
}
/**
* Get the userid for a username from Kitsu
*
@ -455,7 +488,7 @@ final class Model {
'query' => [
'filter' => [
'user_id' => $this->getUserIdByUsername(),
'media_type' => 'Anime'
'kind' => 'anime'
],
'page' => [
'limit' => 1
@ -584,7 +617,7 @@ final class Model {
$defaultOptions = [
'filter' => [
'user_id' => $this->getUserIdByUsername($this->getUsername()),
'media_type' => 'Anime'
'kind' => 'anime'
],
'page' => [
'offset' => $offset,
@ -610,7 +643,7 @@ final class Model {
$options = [
'filter' => [
'user_id' => $this->getUserIdByUsername($this->getUsername()),
'media_type' => 'Anime',
'kind' => 'anime',
'status' => $status,
],
'include' => 'media,media.categories,media.mappings,anime.streamingLinks',
@ -669,7 +702,7 @@ final class Model {
'query' => [
'filter' => [
'user_id' => $this->getUserIdByUsername($this->getUsername()),
'media_type' => 'Manga',
'kind' => 'manga',
'status' => $status,
],
'include' => 'media,media.categories,media.mappings',
@ -724,7 +757,7 @@ final class Model {
'query' => [
'filter' => [
'user_id' => $this->getUserIdByUsername(),
'media_type' => 'Manga'
'kind' => 'manga'
],
'page' => [
'limit' => 1
@ -817,7 +850,7 @@ final class Model {
$defaultOptions = [
'filter' => [
'user_id' => $this->getUserIdByUsername($this->getUsername()),
'media_type' => 'Manga'
'kind' => 'manga'
],
'page' => [
'offset' => $offset,
@ -942,6 +975,71 @@ final class Model {
return $this->listItem->delete($id);
}
/**
* Get the aggregated pages of anime or manga history
*
* @param string $type
* @param int $entries
* @return array
* @throws InvalidArgumentException
* @throws Throwable
*/
protected function getRawHistoryList(string $type = 'anime', int $entries = 60): array
{
$size = 20;
$pages = ceil($entries / $size);
$requester = new ParallelAPIRequest();
// Set up requests
for ($i = 0; $i < $pages; $i++)
{
$offset = $i * $size;
$requester->addRequest($this->getRawHistoryPage($type, $offset, $size));
}
$responses = $requester->makeRequests();
$output = [];
foreach($responses as $response)
{
$data = Json::decode($response);
$output[] = $data;
}
return array_merge_recursive(...$output);
}
/**
* Retrieve one page of the anime or manga history
*
* @param string $type
* @param int $offset
* @param int $limit
* @return Request
* @throws InvalidArgumentException
*/
protected function getRawHistoryPage(string $type, int $offset, int $limit = 20): Request
{
return $this->setUpRequest('GET', 'library-events', [
'query' => [
'filter' => [
'kind' => 'progressed,updated',
'userId' => $this->getUserIdByUsername($this->getUsername()),
],
'page' => [
'offset' => $offset,
'limit' => $limit,
],
'fields' => ($type === 'anime')
? ['anime' => 'canonicalTitle,titles,slug,posterImage']
: ['manga' => 'canonicalTitle,titles,slug,posterImage'],
'sort' => '-updated_at',
'include' => $type,
],
]);
}
/**
* Get the kitsu username from config
*

View File

@ -0,0 +1,210 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API\Kitsu\Transformer;
use Aviat\AnimeClient\API\Mapping\AnimeWatchingStatus;
use Aviat\AnimeClient\Types\HistoryItem;
use Aviat\Ion\Di\ContainerAware;
class AnimeHistoryTransformer {
use ContainerAware;
protected array $skipList = [];
/**
* Convert raw history
*
* @param array $data
* @return array
*/
public function transform(array $data): array
{
$output = [];
foreach ($data as $id => $entry)
{
if ( ! isset($entry['relationships']['anime']))
{
continue;
}
if (in_array($id, $this->skipList, FALSE))
{
continue;
}
if ($entry['attributes']['kind'] === 'progressed')
{
$output[] = $this->transformProgress($entry);
}
else if ($entry['attributes']['kind'] === 'updated')
{
$output[] = $this->transformUpdated($entry);
}
}
return $this->aggregate($output);
}
/**
* Combine consecutive 'progressed' events
*
* @param array $singles
* @return array
*/
protected function aggregate (array $singles): array
{
$output = [];
$prevTitle = '';
$count = count($singles);
for ($i = 0; $i < $count; $i++)
{
$entry = $singles[$i];
$nextId = $i + 1;
if ($nextId < $count)
{
$entries = [];
$next = $singles[$nextId];
while (
$next['kind'] === 'progressed' &&
$next['title'] === $prevTitle
) {
$entries[] = $next;
$prevTitle = $next['title'];
if ($nextId + 1 < $count)
{
$nextId++;
$next = $singles[$nextId];
}
else
{
break;
}
}
}
if (count($entries) > 1)
{
$episodes = [];
foreach ($entries as $e)
{
$episodes[] = max($e['original']['attributes']['changedData']['progress']);
}
$firstEpisode = min($episodes);
$lastEpisode = max($episodes);
$title = $entries[0]['title'];
// Get rid of the single entry added before aggregating
// array_pop($output);
$action = (count($entries) > 3)
? "Marathoned episodes {$firstEpisode}-{$lastEpisode} of {$title}"
: "Watched episodes {$firstEpisode}-{$lastEpisode} of {$title}";
$output[] = HistoryItem::check([
'title' => $title,
'action' => $action,
'coverImg' => $entries[0]['coverImg'],
'isAggregate' => true,
'updated' => $entries[0]['updated'],
]);
// Skip the rest of the aggregate in the main loop
$i += count($entries);
$prevTitle = $title;
continue;
}
else
{
$prevTitle = $entry['title'];
$output[] = $entry;
}
}
return $output;
}
protected function transformProgress ($entry): array
{
$animeId = array_keys($entry['relationships']['anime'])[0];
$animeData = $entry['relationships']['anime'][$animeId]['attributes'];
$title = $this->linkTitle($animeData);
$imgUrl = 'images/anime/' . $animeId . '.webp';
$episode = max($entry['attributes']['changedData']['progress']);
return HistoryItem::check([
'action' => "Watched episode {$episode} of {$title}",
'coverImg' => $imgUrl,
'kind' => 'progressed',
'original' => $entry,
'title' => $title,
'updated' => $entry['attributes']['updatedAt'],
]);
}
protected function transformUpdated($entry): array
{
$animeId = array_keys($entry['relationships']['anime'])[0];
$animeData = $entry['relationships']['anime'][$animeId]['attributes'];
$title = $this->linkTitle($animeData);
$imgUrl = 'images/anime/' . $animeId . '.webp';
$kind = array_key_first($entry['attributes']['changedData']);
if ($kind === 'status')
{
$status = array_pop($entry['attributes']['changedData']['status']);
$statusName = AnimeWatchingStatus::KITSU_TO_TITLE[$status];
if ($statusName === 'Completed')
{
return HistoryItem::check([
'action' => "Completed {$title}",
'coverImg' => $imgUrl,
'kind' => 'updated',
'original' => $entry,
'title' => $title,
'updated' => $entry['attributes']['updatedAt'],
]);
}
return HistoryItem::check([
'action' => "Set status of {$title} to {$statusName}",
'coverImg' => $imgUrl,
'kind' => 'updated',
'original' => $entry,
'title' => $title,
'updated' => $entry['attributes']['updatedAt'],
]);
}
return $entry;
}
protected function linkTitle (array $animeData): string
{
$url = '/anime/details/' . $animeData['slug'];
$helper = $this->getContainer()->get('html-helper');
return $helper->a($url, $animeData['canonicalTitle'], ['id' => $animeData['slug']]);
}
}

View File

@ -213,9 +213,9 @@ function checkFolderPermissions(ConfigInterface $config): array
/**
* Get an API Client, with better defaults
*
* @return DefaultClient
* @return HttpClient
*/
function getApiClient ()
function getApiClient (): HttpClient
{
static $client;
@ -290,7 +290,7 @@ function getLocalImg ($kitsuUrl, $webp = TRUE): string
* @param int $height
* @param string $text
*/
function createPlaceholderImage ($path, $width, $height, $text = 'Image Unavailable'): void
function createPlaceholderImage ($path, ?int $width, ?int $height, $text = 'Image Unavailable'): void
{
$width = $width ?? 200;
$height = $height ?? 200;

View File

@ -30,6 +30,7 @@ use Aviat\Ion\Json;
use InvalidArgumentException;
use Throwable;
use TypeError;
/**
* Controller for Anime-related pages
@ -338,7 +339,7 @@ final class Anime extends BaseController {
'data' => $data,
]);
}
catch (\TypeError $e)
catch (TypeError $e)
{
$this->notFound(
$this->config->get('whose_list') .
@ -348,15 +349,5 @@ final class Anime extends BaseController {
);
}
}
/**
* Find anime matching the selected genre
*
* @param string $genre
*/
public function genre(string $genre): void
{
// @TODO: implement
}
}
// End of AnimeController.php

View File

@ -0,0 +1,84 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @link https://git.timshomepage.net/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\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\ContainerException;
use Aviat\Ion\Di\Exception\NotFoundException;
/**
* Controller for Anime-related pages
*/
final class History extends BaseController {
/**
* The anime list model
* @var AnimeModel
*/
protected AnimeModel $animeModel;
/**
* The manga list model
* @var MangaModel
*/
protected MangaModel $mangaModel;
/**
* Constructor
*
* @param ContainerInterface $container
* @throws ContainerException
* @throws NotFoundException
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->animeModel = $container->get('anime-model');
$this->mangaModel = $container->get('manga-model');
}
public function anime(): void
{
// $this->outputJSON($this->animeModel->getHistory());
// return;
$this->outputHTML('history/anime', [
'title' => $this->formatTitle(
$this->config->get('whose_list') . "'s Anime List",
'Anime',
'Watching History'
),
'items' => $this->animeModel->getHistory(),
]);
}
public function manga(): void
{
$this->outputJSON($this->mangaModel->getHistory());
return;
$this->outputHTML('history/manga', [
'title' => $this->formatTitle(
$this->config->get('whose_list') . "'s Manga List",
'Manga',
'Reading History'
),
'items' => $this->mangaModel->getHistory(),
]);
}
}

View File

@ -337,14 +337,11 @@ final class Manga extends Controller {
]);
}
/**
* Find manga matching the selected genre
*
* @param string $genre
*/
public function genre(string $genre): void
public function history(): void
{
// @TODO: implement
$data = $this->model->getHistory();
$this->outputJSON($data);
}
}
// End of MangaController.php

View File

@ -29,6 +29,7 @@ use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Json;
use Throwable;
use function is_array;
/**
* Model for handling requests dealing with the anime list
@ -128,6 +129,16 @@ class Anime extends API {
return $this->kitsuModel->getAnimeById($animeId);
}
/**
* Get recent watch history
*
* @return array
*/
public function getHistory(): array
{
return $this->kitsuModel->getAnimeHistory();
}
/**
* Search for anime by name
*
@ -151,7 +162,7 @@ class Anime extends API {
$item = $this->kitsuModel->getListItem($itemId);
$array = $item->toArray();
if (\is_array($array['notes']))
if (is_array($array['notes']))
{
$array['notes'] = '';
}

View File

@ -367,7 +367,7 @@ final class AnimeCollection extends Collection {
}
catch (PDOException $e) {}
$this->db->reset_query();
$this->db->resetQuery();
return $output;
}
@ -446,7 +446,7 @@ final class AnimeCollection extends Collection {
try
{
$this->db->insert_batch('genres', $insert);
$this->db->insertBatch('genres', $insert);
}
catch (PDOException $e)
{
@ -486,7 +486,7 @@ final class AnimeCollection extends Collection {
$genres[$genre['id']] = $genre['genre'];
}
$this->db->reset_query();
$this->db->resetQuery();
return $genres;
}
@ -509,13 +509,14 @@ final class AnimeCollection extends Collection {
if (array_key_exists($link['hummingbird_id'], $links))
{
$links[$link['hummingbird_id']][] = $link['genre_id'];
} else
}
else
{
$links[$link['hummingbird_id']] = [$link['genre_id']];
}
}
$this->db->reset_query();
$this->db->resetQuery();
return $links;
}

View File

@ -19,7 +19,7 @@ namespace Aviat\AnimeClient\Model;
use Aviat\Ion\Di\ContainerInterface;
use PDOException;
use Query\QueryBuilder;
use Query\QueryBuilderInterface;
use function Query;
/**
@ -29,9 +29,9 @@ class Collection extends DB {
/**
* The query builder object
* @var QueryBuilder
* @var QueryBuilderInterface
*/
protected QueryBuilder $db;
protected QueryBuilderInterface $db;
/**
* Whether the database is valid for querying

View File

@ -232,7 +232,7 @@ class Manga extends API {
}
/**
* Search for anime by name
* Search for manga by name
*
* @param string $name
* @return array
@ -242,6 +242,16 @@ class Manga extends API {
return $this->kitsuModel->search('manga', $name);
}
/**
* Get recent reading history
*
* @return array
*/
public function getHistory(): array
{
return $this->kitsuModel->getMangaHistory();
}
/**
* Map transformed anime data to be organized by reading status
*

View File

@ -31,6 +31,24 @@ abstract class AbstractType implements ArrayAccess, Countable {
return new static($properties);
}
/**
* Check the shape of the object, and return the array equivalent
*
* @param array $data
* @return array|null
*/
final public static function check($data = []): ?array
{
$currentClass = static::class;
if (get_parent_class($currentClass) !== FALSE)
{
return (new $currentClass($data))->toArray();
}
return NULL;
}
/**
* Sets the properties by using the constructor
*
@ -61,7 +79,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
* @param $name
* @return bool
*/
public function __isset($name): bool
final public function __isset($name): bool
{
return property_exists($this, $name) && isset($this->$name);
}
@ -73,7 +91,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
* @param mixed $value
* @return void
*/
public function __set($name, $value): void
final public function __set($name, $value): void
{
$setterMethod = 'set' . ucfirst($name);
@ -99,7 +117,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
* @param string $name
* @return mixed
*/
public function __get($name)
final public function __get($name)
{
// Be a bit more lenient here, so that you can easily typecast missing
// values to reasonable defaults, and not have to resort to array indexes
@ -122,7 +140,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
* @param $offset
* @return bool
*/
public function offsetExists($offset): bool
final public function offsetExists($offset): bool
{
return $this->__isset($offset);
}
@ -133,7 +151,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
* @param $offset
* @return mixed
*/
public function offsetGet($offset)
final public function offsetGet($offset)
{
return $this->__get($offset);
}
@ -144,7 +162,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
* @param $offset
* @param $value
*/
public function offsetSet($offset, $value): void
final public function offsetSet($offset, $value): void
{
$this->__set($offset, $value);
}
@ -154,7 +172,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
*
* @param $offset
*/
public function offsetUnset($offset): void
final public function offsetUnset($offset): void
{
if ($this->offsetExists($offset))
{
@ -167,7 +185,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
*
* @return int
*/
public function count(): int
final public function count(): int
{
$keys = array_keys($this->toArray());
return count($keys);
@ -179,7 +197,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
* @param mixed $parent
* @return mixed
*/
public function toArray($parent = null)
final public function toArray($parent = null)
{
$object = $parent ?? $this;
@ -205,7 +223,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
*
* @return bool
*/
public function isEmpty(): bool
final public function isEmpty(): bool
{
foreach ($this as $value)
{

View File

@ -25,85 +25,85 @@ class Anime extends AbstractType {
/**
* @var string
*/
public $age_rating;
public string $age_rating = '';
/**
* @var string
*/
public $age_rating_guide;
public ?string $age_rating_guide = '';
/**
* @var string
*/
public $cover_image;
public string $cover_image = '';
/**
* @var string|int
*/
public $episode_count;
public ?int $episode_count = 13;
/**
* @var string|int
*/
public $episode_length;
public ?int $episode_length = 24;
/**
* @var array
*/
public $genres;
public array $genres = [];
/**
* @var string
*/
public $id;
public string $id = '';
/**
* @var array
*/
public $included;
public array $included = [];
/**
* @var string
*/
public $show_type;
public string $show_type = '';
/**
* @var string
*/
public $slug;
public string $slug = '';
/**
* @var AnimeAiringStatus::NOT_YET_AIRED | AnimeAiringStatus::AIRING | AnimeAiringStatus::FINISHED_AIRING
* @var AnimeAiringStatus
*/
public $status;
public string $status = AnimeAiringStatus::FINISHED_AIRING;
/**
* @var array
*/
public $streaming_links;
public ?array $streaming_links = [];
/**
* @var string
*/
public $synopsis;
public string $synopsis = '';
/**
* @var string
*/
public $title;
public string $title = '';
/**
* @var array
*/
public $titles;
public array $titles = [];
/**
* @var string
*/
public $trailer_id;
public ?string $trailer_id = '';
/**
* @var string
*/
public $url;
public string $url = '';
}

View File

@ -0,0 +1,51 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Types;
class HistoryItem extends AbstractType {
/**
* @var string Title of the anime/manga
*/
public string $title = '';
/**
* @var string The url of the cover image
*/
public string $coverImg = '';
/**
* @var string The type of action done
*/
public string $action = '';
/**
* @var bool Is this item a combination of items?
*/
public bool $isAggregate = FALSE;
/**
* @var string The kind of history event
*/
public string $kind = '';
/**
* @var string When the item was last updated
*/
public string $updated = '';
public $original;
}

View File

@ -20,6 +20,8 @@ use Psr\Http\Message\ResponseInterface;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Exception\DoubleRenderException;
use InvalidArgumentException;
use RuntimeException;
/**
* Base view response class
@ -91,7 +93,7 @@ abstract class View
*
* @param string $name
* @param string|string[] $value
* @throws \InvalidArgumentException
* @throws InvalidArgumentException
* @return ViewInterface
*/
public function addHeader(string $name, $value): ViewInterface
@ -104,8 +106,8 @@ abstract class View
* Set the output string
*
* @param mixed $string
* @throws \InvalidArgumentException
* @throws \RuntimeException
* @throws InvalidArgumentException
* @throws RuntimeException
* @return ViewInterface
*/
public function setOutput($string): ViewInterface
@ -119,8 +121,8 @@ abstract class View
* Append additional output.
*
* @param string $string
* @throws \InvalidArgumentException
* @throws \RuntimeException
* @throws InvalidArgumentException
* @throws RuntimeException
* @return ViewInterface
*/
public function appendOutput(string $string): ViewInterface

View File

@ -31,14 +31,14 @@ class HtmlView extends HttpView {
*
* @var HelperLocator
*/
protected $helper;
protected HelperLocator $helper;
/**
* Response mime type
*
* @var string
*/
protected $contentType = 'text/html';
protected string $contentType = 'text/html';
/**
* Create the Html View
@ -73,7 +73,7 @@ class HtmlView extends HttpView {
// Very basic html minify, that won't affect content between html tags
$buffer = preg_replace('/>\s+</', '> <', $buffer);
// $buffer = preg_replace('/>\s+</', '> <', $buffer);
return $buffer;
}

View File

@ -32,7 +32,7 @@ class HttpView extends BaseView {
*
* @var string
*/
protected $contentType = '';
protected string $contentType = '';
/**
* Do a redirect

View File

@ -19,6 +19,7 @@ namespace Aviat\Ion\View;
use Aviat\Ion\Json;
use Aviat\Ion\JsonException;
use Aviat\Ion\ViewInterface;
use function is_string;
/**
* View class to serialize Json
@ -30,7 +31,7 @@ class JsonView extends HttpView {
*
* @var string
*/
protected $contentType = 'application/json';
protected string $contentType = 'application/json';
/**
* Set the output string
@ -43,7 +44,7 @@ class JsonView extends HttpView {
*/
public function setOutput($string): ViewInterface
{
if ( ! \is_string($string))
if ( ! is_string($string))
{
$string = Json::encode($string);
}