Version 5.1 - All the GraphQL #32

Closed
timw4mail wants to merge 1160 commits from develop into master
14 changed files with 86 additions and 223 deletions
Showing only changes of commit 2505df6501 - Show all commits

View File

@ -31,10 +31,10 @@ A self-hosted client that allows custom formatting of data from the hummingbird
### Requirements ### Requirements
* PHP 5.5+ * PHP 7.0+
* PDO SQLite or PDO PostgreSQL (For collection tab) * PDO SQLite or PDO PostgreSQL (For collection tab)
* GD * GD
* Redis and PHP Redis extension (optional, for caching) * Redis or Memcached for caching
### Installation ### Installation
@ -48,23 +48,14 @@ or
3. Configure settings in `app/config/config.toml` to your liking 3. Configure settings in `app/config/config.toml` to your liking
4. Create the following directories if they don't exist, and make sure they are world writable 4. Create the following directories if they don't exist, and make sure they are world writable
* public/js/cache * public/js/cache
* public/images/manga
* public/images/anime
5. Make sure the `console` script is executable 5. Make sure the `console` script is executable
6. To batch-create image thumbnails, run `console cache-images`.
### Server Setup ### Server Setup
#### Caching #### Caching
To setup API caching, choose a caching method: Update `app/config/cache.toml` based on the instructions [here](https://git.timshomepage.net/timw4mail/banker/blob/master/README.md).
* Database
1. Follow the instructions for the Anime Collection setup below.
2. Set `cache_driver` in `app/config/config.toml` to 'SQLDriver'
* Redis
1. Copy `app/redis.toml.example` to `app/redis.toml`, and edit to match your setup.
2. Set `cache_driver` in `app/config/config.toml` to 'RedisDriver'
#### nginx #### nginx
Basic nginx setup Basic nginx setup

View File

@ -4,7 +4,7 @@
# See https://git.timshomepage.net/timw4mail/banker for more information # See https://git.timshomepage.net/timw4mail/banker for more information
# Available drivers are memcache, memcached, redis or null # Available drivers are apcu, memcache, memcached, redis or null
# Null cache driver means no caching # Null cache driver means no caching
driver = "redis" driver = "redis"

View File

@ -11,11 +11,11 @@
<section class="media-wrap"> <section class="media-wrap">
<?php foreach($items as $item): ?> <?php foreach($items as $item): ?>
<article class="media" id="a-<?= $item['hummingbird_id'] ?>"> <article class="media" id="a-<?= $item['hummingbird_id'] ?>">
<img src="<?= $urlGenerator->asset_url('images', 'anime', basename($item['cover_image'])) ?>" alt="<?= $item['title'] ?> cover image" /> <img src="<?= $item['cover_image'] ?>" alt="<?= $item['title'] ?> cover image" />
<div class="name"> <div class="name">
<a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>"> <a href="<?= $url->generate('anime.details', ['id' => $item['slug']]) ?>">
<?= $item['title'] ?> <?= $item['title'] ?>
<?= ($item['alternate_title'] != "") ? "<br />({$item['alternate_title']})" : ""; ?> <?= ($item['alternate_title'] != "") ? "<small><br />{$item['alternate_title']}</small>" : ""; ?>
</a> </a>
</div> </div>
<div class="table"> <div class="table">

View File

@ -8,12 +8,12 @@
<th> <th>
<h3><?= $escape->html($item['title']) ?></h3> <h3><?= $escape->html($item['title']) ?></h3>
<?php if($item['alternate_title'] != ""): ?> <?php if($item['alternate_title'] != ""): ?>
<h4><?= $escape->html($item['alternate_title']) ?></h4> <h4><?= $item['alternate_title'] ?></h4>
<?php endif ?> <?php endif ?>
</th> </th>
<th> <th>
<article class="media"> <article class="media">
<?= $helper->img($urlGenerator->asset_url('images', 'anime', basename($item['cover_image']))); ?> <?= $helper->img($item['cover_image']); ?>
</article> </article>
</th> </th>
</tr> </tr>

View File

@ -34,7 +34,7 @@
<a href="https://hummingbird.me/anime/<?= $item['slug'] ?>"> <a href="https://hummingbird.me/anime/<?= $item['slug'] ?>">
<?= $item['title'] ?> <?= $item['title'] ?>
</a> </a>
<?= ( ! empty($item['alternate_title'])) ? " &middot; " . $item['alternate_title'] : "" ?> <?= ( ! empty($item['alternate_title'])) ? " <br /><small> " . $item['alternate_title'] . "</small>" : "" ?>
</td> </td>
<td><?= $item['episode_count'] ?></td> <td><?= $item['episode_count'] ?></td>
<td><?= $item['episode_length'] ?></td> <td><?= $item['episode_length'] ?></td>

View File

@ -1,6 +1,6 @@
{ {
"name": "timw4mail/hummingbird-anime-client", "name": "timw4mail/hummingbird-anime-client",
"description": "A self-hosted anime/manga client for hummingbird.", "description": "A self-hosted anime/manga client for Kitsu.",
"license":"MIT", "license":"MIT",
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@ -17,7 +17,7 @@
"aura/html": "2.*", "aura/html": "2.*",
"aura/router": "3.*", "aura/router": "3.*",
"aura/session": "2.*", "aura/session": "2.*",
"aviat/banker": "dev-master", "aviat/banker": "^1.0.0",
"aviat/ion": "1.0.*", "aviat/ion": "1.0.*",
"filp/whoops": "2.0.*", "filp/whoops": "2.0.*",
"guzzlehttp/guzzle": "6.*", "guzzlehttp/guzzle": "6.*",

View File

@ -129,13 +129,19 @@ class KitsuModel {
/** /**
* Get information about a particular anime * Get information about a particular anime
* *
* @param string $animeId * @param string $slug
* @return array * @return array
*/ */
public function getAnime(string $animeId): array public function getAnime(string $slug): array
{ {
// @TODO catch non-existent anime // @TODO catch non-existent anime
$baseData = $this->getRawMediaData('anime', $animeId); $baseData = $this->getRawMediaData('anime', $slug);
return $this->animeTransformer->transform($baseData);
}
public function getAnimeById(string $animeId): array
{
$baseData = $this->getRawMediaDataById('anime', $animeId);
return $this->animeTransformer->transform($baseData); return $this->animeTransformer->transform($baseData);
} }
@ -212,17 +218,25 @@ class KitsuModel {
'sort' => '-updated_at' 'sort' => '-updated_at'
] ]
]; ];
$cacheItem = $this->cache->getItem($this->getHashForMethodCall($this, __METHOD__, $options));
$data = $this->getRequest('library-entries', $options); if ( ! $cacheItem->isHit())
foreach($data['data'] as $i => &$item)
{ {
$item['manga'] = $data['included'][$i]; $data = $this->getRequest('library-entries', $options);
foreach($data['data'] as $i => &$item)
{
$item['manga'] = $data['included'][$i];
}
$transformed = $this->mangaListTransformer->transformCollection($data['data']);
$cacheItem->set($transformed);
$cacheItem->save();
} }
$transformed = $this->mangaListTransformer->transformCollection($data['data']); return $cacheItem->get();
return $transformed;
} }
public function search(string $type, string $query): array public function search(string $type, string $query): array
@ -310,6 +324,22 @@ class KitsuModel {
->get('config') ->get('config')
->get(['kitsu_username']); ->get(['kitsu_username']);
} }
private function getRawMediaDataById(string $type, string $id): array
{
$options = [
'query' => [
'include' => ($type === 'anime')
? 'genres,mappings,streamingLinks'
: 'genres,mappings',
]
];
$data = $this->getRequest("{$type}/{$id}", $options);
$baseData = $data['data']['attributes'];
$baseData['included'] = $data['included'];
return $baseData;
}
private function getRawMediaData(string $type, string $slug): array private function getRawMediaData(string $type, string $slug): array
{ {

View File

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

View File

@ -1,116 +0,0 @@
<?php declare(strict_types=1);
/**
* Anime List Client
*
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
*
* PHP version 7
*
* @package AnimeListClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2017 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 4.0
* @link https://github.com/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Command;
use Aviat\AnimeClient\Util;
/**
* Generates thumbnail image cache so that cover images load faster
*/
class CacheImages extends BaseCommand {
/**
* Manga Model
*
* @var Aviat\AnimeClient\Model\Manga
*/
protected $mangaModel;
/**
* Anime Model
*
* @var Aviat\AnimeClient\Model\Anime
*/
protected $animeModel;
/**
* Miscellaneous helper methods
*
* @var Aviat\AnimeClient\Util
*/
protected $util;
/**
* Convert manga images
*
* @throws \ConsoleKit\ConsoleException
* @return void
*/
protected function getMangaImages()
{
$raw_list = $this->mangaModel->_get_list_from_api();
$manga_list = array_column($raw_list, 'manga');
$total = count($raw_list);
$current = 0;
foreach($manga_list as $item)
{
$this->util->get_cached_image($item['poster_image'], $item['id'], 'manga');
$current++;
echo "Cached {$current} of {$total} manga images. \n";
}
}
/**
* Convert anime images
*
* @throws \ConsoleKit\ConsoleException
* @return void
*/
protected function getAnimeImages()
{
$raw_list = $this->animeModel->get_raw_list();
$total = count($raw_list);
$current = 0;
foreach($raw_list as $item)
{
$this->util->get_cached_image($item['anime']['cover_image'], $item['anime']['slug'], 'anime');
$current++;
echo "Cached {$current} of {$total} anime images. \n";
}
}
/**
* Run the image conversion script
*
* @param array $args
* @param array $options
* @return void
* @throws \ConsoleKit\ConsoleException
*/
public function execute(array $args, array $options = [])
{
$this->setContainer($this->setupContainer());
$this->util = new Util($this->container);
$this->animeModel = $this->container->get('anime-model');
$this->mangaModel = $this->container->get('manga-model');
$this->echoBox('Starting image conversion');
$this->echoBox('Converting manga images');
$this->getMangaImages();
$this->echoBox('Converting anime images');
$this->getAnimeImages();
$this->echoBox('Finished image conversion');
}
}
// End of CacheImages.php

View File

@ -32,7 +32,7 @@ class ClearCache extends BaseCommand {
{ {
$this->setContainer($this->setupContainer()); $this->setContainer($this->setupContainer());
$cache = $this->container->get('cache'); $cache = $this->container->get('cache');
$cache->purge(); $cache->clear();
$this->echoBox('API Cache has been cleared.'); $this->echoBox('API Cache has been cleared.');
} }

View File

@ -71,14 +71,19 @@ class Anime extends API {
} }
/** /**
* Get information about an anime from its id * Get information about an anime from its slug
* *
* @param string $anime_id * @param string $slug
* @return array * @return array
*/ */
public function getAnime($anime_id) public function getAnime($slug)
{ {
return $this->kitsuModel->getAnime($anime_id); return $this->kitsuModel->getAnime($slug);
}
public function getAnimeById($anime_id)
{
return $this->kitsuModel->getAnimeById($anime_id);
} }
/** /**

View File

@ -16,6 +16,7 @@
namespace Aviat\AnimeClient\Model; namespace Aviat\AnimeClient\Model;
use Aviat\AnimeClient\API\Kitsu;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Json; use Aviat\Ion\Json;
use PDO; use PDO;
@ -25,19 +26,6 @@ use PDO;
*/ */
class AnimeCollection extends Collection { class AnimeCollection extends Collection {
/**
* Constructor
*
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
// Do an import if an import file exists
$this->json_import();
}
/** /**
* Get collection from the database, and organize by media type * Get collection from the database, and organize by media type
* *
@ -131,20 +119,17 @@ class AnimeCollection extends Collection {
*/ */
public function add($data) public function add($data)
{ {
$anime = (object)$this->anime_model->get_anime($data['id']); $anime = (object)$this->anime_model->getAnimeById($data['id']);
$util = $this->container->get('util'); $util = $this->container->get('util');
$this->db->set([ $this->db->set([
'hummingbird_id' => $data['id'], 'hummingbird_id' => $data['id'],
'slug' => $anime->slug, 'slug' => $anime->slug,
'title' => $anime->title, 'title' => array_shift($anime->titles),
'alternate_title' => $anime->alternate_title, 'alternate_title' => implode('<br />', $anime->titles),
'show_type' => $anime->show_type, 'show_type' => $anime->show_type,
'age_rating' => $anime->age_rating, 'age_rating' => $anime->age_rating,
'cover_image' => basename( 'cover_image' => $anime->cover_image,
$util->get_cached_image($anime->cover_image, $anime->slug, 'anime')
),
'episode_count' => $anime->episode_count, 'episode_count' => $anime->episode_count,
'episode_length' => $anime->episode_length, 'episode_length' => $anime->episode_length,
'media_id' => $data['media_id'], 'media_id' => $data['media_id'],
@ -212,46 +197,6 @@ class AnimeCollection extends Collection {
return $query->fetch(PDO::FETCH_ASSOC); return $query->fetch(PDO::FETCH_ASSOC);
} }
/**
* Import anime into collection from a json file
*
* @return void
*/
private function json_import()
{
if ( ! file_exists('import.json') OR ! $this->valid_database)
{
return;
}
$anime = Json::decodeFile("import.json");
foreach ($anime as $item)
{
$util = $this->container->get('util');
$this->db->set([
'hummingbird_id' => $item->id,
'slug' => $item->slug,
'title' => $item->title,
'alternate_title' => $item->alternate_title,
'show_type' => $item->show_type,
'age_rating' => $item->age_rating,
'cover_image' => basename(
$util->get_cached_image($item->cover_image, $item->slug, 'anime')
),
'episode_count' => $item->episode_count,
'episode_length' => $item->episode_length
])->insert('anime_set');
}
// Delete the import file
unlink('import.json');
// Update genre info
$this->update_genres();
}
/** /**
* Update genre information for selected anime * Update genre information for selected anime
* *
@ -264,17 +209,17 @@ class AnimeCollection extends Collection {
extract($genre_info); extract($genre_info);
// Get api information // Get api information
$anime = $this->anime_model->get_anime($anime_id); $anime = $this->anime_model->getAnimeById($anime_id);
foreach ($anime['genres'] as $genre) foreach ($anime['genres'] as $genre)
{ {
// Add genres that don't currently exist // Add genres that don't currently exist
if ( ! in_array($genre['name'], $genres)) if ( ! in_array($genre, $genres))
{ {
$this->db->set('genre', $genre['name']) $this->db->set('genre', $genre)
->insert('genres'); ->insert('genres');
$genres[] = $genre['name']; $genres[] = $genre;
} }
// Update link table // Update link table
@ -282,13 +227,13 @@ class AnimeCollection extends Collection {
$flipped_genres = array_flip($genres); $flipped_genres = array_flip($genres);
$insert_array = [ $insert_array = [
'hummingbird_id' => $anime['id'], 'hummingbird_id' => $anime_id,
'genre_id' => $flipped_genres[$genre['name']] 'genre_id' => $flipped_genres[$genre]
]; ];
if (array_key_exists($anime['id'], $links)) if (array_key_exists($anime_id, $links))
{ {
if ( ! in_array($flipped_genres[$genre['name']], $links[$anime['id']])) if ( ! in_array($flipped_genres[$genre], $links[$anime_id]))
{ {
$this->db->set($insert_array)->insert('genre_anime_set_link'); $this->db->set($insert_array)->insert('genre_anime_set_link');
} }

View File

@ -16,6 +16,8 @@
namespace Aviat\AnimeClient\Model; namespace Aviat\AnimeClient\Model;
use Aviat\Ion\Di\ContainerAware;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Model\DB; use Aviat\Ion\Model\DB;
use PDO; use PDO;
@ -25,6 +27,8 @@ use PDOException;
* Base model for anime and manga collections * Base model for anime and manga collections
*/ */
class Collection extends DB { class Collection extends DB {
use ContainerAware;
/** /**
* Anime API Model * Anime API Model
@ -45,6 +49,8 @@ class Collection extends DB {
*/ */
public function __construct(ContainerInterface $container) public function __construct(ContainerInterface $container)
{ {
$this->container = $container;
parent::__construct($container->get('config')); parent::__construct($container->get('config'));
try try
@ -53,8 +59,8 @@ class Collection extends DB {
} }
catch (PDOException $e) catch (PDOException $e)
{ {
$this->valid_database = FALSE; //$this->valid_database = FALSE;
return FALSE; //return FALSE;
} }
$this->anime_model = $container->get('anime-model'); $this->anime_model = $container->get('anime-model');

View File

@ -47,5 +47,6 @@
"link": "t", "link": "t",
"subs": ["en"], "subs": ["en"],
"dubs": ["ja"] "dubs": ["ja"]
}] }],
"slug": "attack-on-titan"
} }