Anime and Manga editing, incrementing, and deletion

This commit is contained in:
Timothy Warren 2017-01-09 20:36:48 -05:00
parent 187812576c
commit 08b4227b34
19 changed files with 192 additions and 184 deletions

View File

@ -25,9 +25,9 @@
<td><label for="status">Reading Status</label></td> <td><label for="status">Reading Status</label></td>
<td> <td>
<select name="status" id="status"> <select name="status" id="status">
<?php foreach($status_list as $status): ?> <?php foreach($status_list as $val => $status): ?>
<option <?php if($item['reading_status'] === $status): ?>selected="selected"<?php endif ?> <option <?php if($item['reading_status'] === $val): ?>selected="selected"<?php endif ?>
value="<?= $status ?>"><?= $status ?></option> value="<?= $val ?>"><?= $status ?></option>
<?php endforeach ?> <?php endforeach ?>
</select> </select>
</td> </td>

View File

@ -1174,8 +1174,8 @@ a:hover, a:active {
position:absolute; position:absolute;
top: 86px; top: 86px;
top: calc(50% - 58.5px); top: calc(50% - 58.5px);
left: 5px; left: 43.5px;
left: calc(50% - 95px); left: calc(50% - 66.5px);
} }
/* ----------------------------------------------------------------------------- /* -----------------------------------------------------------------------------
@ -1222,11 +1222,16 @@ a:hover, a:active {
position:absolute; position:absolute;
display:block; display:block;
top:0; top:0;
left:0;
height:100%; height:100%;
width:100%; width:100%;
vertical-align:middle; vertical-align:middle;
} }
#series_list .name small {
color: #fff;
}
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
Details page styles Details page styles
-----------------------------------------------------------------------------*/ -----------------------------------------------------------------------------*/

View File

@ -439,8 +439,8 @@ a:hover, a:active {
position:absolute; position:absolute;
top: 86px; top: 86px;
top: calc(50% - 58.5px); top: calc(50% - 58.5px);
left: 5px; left: 43.5px;
left: calc(50% - 95px); left: calc(50% - 66.5px);
} }
@ -484,10 +484,14 @@ a:hover, a:active {
position:absolute; position:absolute;
display:block; display:block;
top:0; top:0;
left:0;
height:100%; height:100%;
width:100%; width:100%;
vertical-align:middle; vertical-align:middle;
} }
#series_list .name small {
color: #fff;
}
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
Details page styles Details page styles

8
public/js/manga_edit.js Executable file → Normal file
View File

@ -18,12 +18,16 @@
completed = 0; completed = 0;
} }
// Setup the update data
let data = { let data = {
id: manga_id id: manga_id,
data: {
progress: completed
}
}; };
// Update the total count // Update the total count
data[type + "s_read"] = ++completed; data['data']['progress'] = ++completed;
_.ajax(_.url('/manga/update'), { _.ajax(_.url('/manga/update'), {
data: data, data: data,

View File

@ -4,10 +4,13 @@
<input type="radio" class="big-check" id="{{attributes.slug}}" name="id" value="{{id}}" /> <input type="radio" class="big-check" id="{{attributes.slug}}" name="id" value="{{id}}" />
<label for="{{attributes.slug}}"> <label for="{{attributes.slug}}">
<span class="name"> <span class="name">
{{attributes.canonicalTitle}}<br /> {{attributes.canonicalTitle}}
{{attributes.titles.en}}<br /> <br />
{{attributes.titles.en_jp}}<br /> <small>
{{attributes.titles.ja_jp}} {{#attributes.titles}}
{{.}}<br />
{{/attributes.titles}}
</small>
</span> </span>
</label> </label>
</div> </div>

View File

@ -1,10 +1,18 @@
{{#search}} {{#data}}
<article class="media search"> <article class="media search">
<div class="name" style="background-image:url({{image}})"> <div class="name" style="background-image:url({{attributes.posterImage.small}})">
<input type="radio" class="big-check" id="{{link}}" name="id" value="{{link}}" /> <input type="radio" class="big-check" id="{{attributes.slug}}" name="id" value="{{id}}" />
<label for="{{link}}"> <label for="{{attributes.slug}}">
<span>{{title}}</span> <span class="name">
{{attributes.canonicalTitle}}
<br />
<small>
{{#attributes.titles}}
{{.}}<br />
{{/attributes.titles}}
</small>
</span>
</label> </label>
</div> </div>
</article> </article>
{{/search}} {{/data}}

View File

@ -16,7 +16,11 @@
namespace Aviat\AnimeClient\API; namespace Aviat\AnimeClient\API;
use Aviat\AnimeClient\API\Kitsu\Enum\{AnimeAiringStatus, AnimeWatchingStatus}; use Aviat\AnimeClient\API\Kitsu\Enum\{
AnimeAiringStatus,
AnimeWatchingStatus,
MangaReadingStatus
};
use DateTimeImmutable; use DateTimeImmutable;
/** /**
@ -41,6 +45,17 @@ class Kitsu {
]; ];
} }
public static function getStatusToMangaSelectMap()
{
return [
MangaReadingStatus::READING => 'Currently Reading',
MangaReadingStatus::PLAN_TO_READ => 'Plan to Read',
MangaReadingStatus::COMPLETED => 'Completed',
MangaReadingStatus::ON_HOLD => 'On Hold',
MangaReadingStatus::DROPPED => 'Dropped'
];
}
/** /**
* Determine whether an anime is airing, finished airing, or has not yet aired * Determine whether an anime is airing, finished airing, or has not yet aired
* *

View File

@ -22,10 +22,10 @@ use Aviat\Ion\Enum as BaseEnum;
* Possible values for current reading status of manga * Possible values for current reading status of manga
*/ */
class MangaReadingStatus extends BaseEnum { class MangaReadingStatus extends BaseEnum {
const READING = 1; const READING = 'current';
const PLAN_TO_READ = 2; const PLAN_TO_READ = 'planned';
const DROPPED = 5; const DROPPED = 'dropped';
const ON_HOLD = 4; const ON_HOLD = 'on_hold';
const COMPLETED = 3; const COMPLETED = 'completed';
} }
// End of MangaReadingStatus.php // End of MangaReadingStatus.php

View File

@ -24,6 +24,7 @@ use Aviat\AnimeClient\API\Kitsu\Transformer\{
use Aviat\Ion\Di\ContainerAware; use Aviat\Ion\Di\ContainerAware;
use Aviat\Ion\Json; use Aviat\Ion\Json;
use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ClientException;
use PHP_CodeSniffer\Tokenizers\JS;
/** /**
* Kitsu API Model * Kitsu API Model
@ -32,9 +33,6 @@ class KitsuModel {
use ContainerAware; use ContainerAware;
use KitsuTrait; use KitsuTrait;
const CLIENT_ID = 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd';
const CLIENT_SECRET = '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151';
/** /**
* Class to map anime list items * Class to map anime list items
* to a common format used by * to a common format used by
@ -101,7 +99,8 @@ class KitsuModel {
*/ */
public function authenticate(string $username, string $password) public function authenticate(string $username, string $password)
{ {
$data = $this->postRequest(K::AUTH_URL, [ $response = $this->getResponse('POST', K::AUTH_URL, [
'headers' => [],
'form_params' => [ 'form_params' => [
'grant_type' => 'password', 'grant_type' => 'password',
'username' => $username, 'username' => $username,
@ -109,6 +108,8 @@ class KitsuModel {
] ]
]); ]);
$data = Json::decode((string)$response->getBody());
if (array_key_exists('access_token', $data)) if (array_key_exists('access_token', $data))
{ {
return $data['access_token']; return $data['access_token'];
@ -161,7 +162,6 @@ class KitsuModel {
$data = $this->getRequest('library-entries', $options); $data = $this->getRequest('library-entries', $options);
$included = K::organizeIncludes($data['included']); $included = K::organizeIncludes($data['included']);
/*?><pre><?= print_r($included, TRUE) ?></pre><?php*/
foreach($data['data'] as $i => &$item) foreach($data['data'] as $i => &$item)
{ {
@ -173,8 +173,6 @@ class KitsuModel {
{ {
$item['genres'][] = $included['genres'][$id]['name']; $item['genres'][] = $included['genres'][$id]['name'];
} }
// $item['genres'] = array_pluck($genres, 'name');
} }
$transformed = $this->animeListTransformer->transformCollection($data['data']); $transformed = $this->animeListTransformer->transformCollection($data['data']);
@ -223,7 +221,15 @@ class KitsuModel {
'include' => 'media' 'include' => 'media'
]; ];
return $this->getRequest($type, $options); $raw = $this->getRequest($type, $options);
foreach ($raw['data'] as &$item)
{
$item['attributes']['titles'] = K::filterTitles($item['attributes']);
array_shift($item['attributes']['titles']);
}
return $raw;
} }
public function getListItem(string $listId): array public function getListItem(string $listId): array
@ -264,6 +270,11 @@ class KitsuModel {
} }
} }
public function deleteListItem(string $id): bool
{
return $this->listItem->delete($id);
}
private function getUsername(): string private function getUsername(): string
{ {
return $this->getContainer() return $this->getContainer()

View File

@ -18,12 +18,12 @@ namespace Aviat\AnimeClient\API\Kitsu;
use Aviat\AnimeClient\AnimeClient; use Aviat\AnimeClient\AnimeClient;
use Aviat\AnimeClient\API\GuzzleTrait; use Aviat\AnimeClient\API\GuzzleTrait;
use Aviat\AnimeClient\API\Kitsu as K;
use Aviat\Ion\Json; use Aviat\Ion\Json;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use InvalidArgumentException; use InvalidArgumentException;
use PHP_CodeSniffer\Tokenizers\JS;
use RuntimeException; use RuntimeException;
trait KitsuTrait { trait KitsuTrait {
@ -43,7 +43,7 @@ trait KitsuTrait {
protected $defaultHeaders = [ protected $defaultHeaders = [
'User-Agent' => "Tim's Anime Client/4.0", 'User-Agent' => "Tim's Anime Client/4.0",
'Accept-Encoding' => 'application/vnd.api+json', 'Accept-Encoding' => 'application/vnd.api+json',
'Content-Type' => 'application/vnd.api+json; charset=utf-8', 'Content-Type' => 'application/vnd.api+json',
'client_id' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd', 'client_id' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd',
'client_secret' => '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151', 'client_secret' => '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151',
]; ];
@ -93,23 +93,22 @@ trait KitsuTrait {
'headers' => $this->defaultHeaders 'headers' => $this->defaultHeaders
]; ];
if ($this->getContainer()); $logger = $this->container->getLogger('request');
{ $sessionSegment = $this->getContainer()
$logger = $this->container->getLogger('request'); ->get('session')
$sessionSegment = $this->getContainer() ->getSegment(AnimeClient::SESSION_SEGMENT);
->get('session')
->getSegment(AnimeClient::SESSION_SEGMENT);
if ($sessionSegment->get('auth_token') !== null) if ($sessionSegment->get('auth_token') !== null && $url !== K::AUTH_URL)
{ {
$token = $sessionSegment->get('auth_token'); $token = $sessionSegment->get('auth_token');
$defaultOptions['headers']['Authorization'] = "bearer {$token}"; $defaultOptions['headers']['Authorization'] = "bearer {$token}";
}
$logger->debug(Json::encode(func_get_args()));
} }
$options = array_merge($defaultOptions, $options); $options = array_merge($defaultOptions, $options);
$logger->debug(Json::encode([$type, $url]));
$logger->debug(Json::encode($options));
return $this->client->request($type, $url, $options); return $this->client->request($type, $url, $options);
} }
@ -131,7 +130,7 @@ trait KitsuTrait {
$response = $this->getResponse($type, $url, $options); $response = $this->getResponse($type, $url, $options);
if ((int) $response->getStatusCode() !== 200) if ((int) $response->getStatusCode() > 299 || (int) $response->getStatusCode() < 200)
{ {
if ($logger) if ($logger)
{ {
@ -192,7 +191,7 @@ trait KitsuTrait {
$logger->warning($response->getBody()); $logger->warning($response->getBody());
} }
throw new RuntimeException($response->getBody()); // throw new RuntimeException($response->getBody());
} }
return JSON::decode($response->getBody(), TRUE); return JSON::decode($response->getBody(), TRUE);

View File

@ -37,14 +37,33 @@ class ListItem extends AbstractListItem {
public function create(array $data): bool public function create(array $data): bool
{ {
// TODO: Implement create() method. $response = $this->getResponse('post', 'library-entries', [
return false; 'body' => [
'type' => 'libraryEntries',
'attributes' => [
'status' => $data['status'],
'progress' => $data['progress'] ?? 0
],
'relationships' => [
'user' => [
'id' => $data['user_id'],
'type' => 'users'
],
'media' => [
'id' => $data['id'],
'type' => $data['type']
]
]
]
]);
return ($response->getStatusCode() === 201);
} }
public function delete(string $id): bool public function delete(string $id): bool
{ {
// TODO: Implement delete() method. $response = $this->getResponse('DELETE', "library-entries/{$id}");
return false; return ($response->getStatusCode() === 204);
} }
public function get(string $id): array public function get(string $id): array

View File

@ -91,18 +91,18 @@ class MangaListTransformer extends AbstractTransformer {
$map = [ $map = [
'id' => $item['id'], 'id' => $item['id'],
'manga_id' => $item['manga_id'], 'data' => [
'status' => $item['status'], 'status' => $item['status'],
'chapters_read' => (int)$item['chapters_read'], 'progress' => (int)$item['chapters_read'],
'volumes_read' => (int)$item['volumes_read'], 'reconsuming' => $rereading,
'rereading' => $rereading, 'reconsumeCount' => (int)$item['reread_count'],
'reread_count' => (int)$item['reread_count'], 'notes' => $item['notes'],
'notes' => $item['notes'], ],
]; ];
if ($item['new_rating'] !== $item['old_rating'] && $item['new_rating'] !== "") if ($item['new_rating'] !== $item['old_rating'] && $item['new_rating'] !== "")
{ {
$map['rating'] = ($item['new_rating'] > 0) $map['data']['rating'] = ($item['new_rating'] > 0)
? $item['new_rating'] / 2 ? $item['new_rating'] / 2
: $item['old_rating'] / 2; : $item['old_rating'] / 2;
} }

View File

@ -260,8 +260,6 @@ class Anime extends BaseController {
} }
$response = $this->model->updateLibraryItem($data); $response = $this->model->updateLibraryItem($data);
//echo JSON::encode($response);
//die();
// $this->cache->purge(); // $this->cache->purge();
$this->outputJSON($response['body'], $response['statusCode']); $this->outputJSON($response['body'], $response['statusCode']);
@ -274,12 +272,13 @@ class Anime extends BaseController {
*/ */
public function delete() public function delete()
{ {
$response = $this->model->delete($this->request->getParsedBody()); $body = $this->request->getParsedBody();
$response = $this->model->deleteLibraryItem($body['id']);
if ((bool)$response['body'] === TRUE) if ((bool)$response === TRUE)
{ {
$this->set_flash_message("Successfully deleted anime.", 'success'); $this->set_flash_message("Successfully deleted anime.", 'success');
$this->cache->purge(); // $this->cache->purge();
} }
else else
{ {

View File

@ -17,6 +17,7 @@
namespace Aviat\AnimeClient\Controller; namespace Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\Controller; use Aviat\AnimeClient\Controller;
use Aviat\AnimeClient\API\Kitsu;
use Aviat\AnimeClient\API\Kitsu\Enum\MangaReadingStatus; use Aviat\AnimeClient\API\Kitsu\Enum\MangaReadingStatus;
use Aviat\AnimeClient\API\Kitsu\Transformer\MangaListTransformer; use Aviat\AnimeClient\API\Kitsu\Transformer\MangaListTransformer;
use Aviat\AnimeClient\Model\Manga as MangaModel; use Aviat\AnimeClient\Model\Manga as MangaModel;
@ -166,7 +167,7 @@ class Manga extends Controller {
$this->outputHTML('manga/edit', [ $this->outputHTML('manga/edit', [
'title' => $title, 'title' => $title,
'status_list' => MangaReadingStatus::getConstList(), 'status_list' => Kitsu::getStatusToMangaSelectMap(),
'item' => $item, 'item' => $item,
'action' => $this->container->get('url-generator') 'action' => $this->container->get('url-generator')
->url('/manga/update_form'), ->url('/manga/update_form'),
@ -185,50 +186,54 @@ class Manga extends Controller {
} }
/** /**
* Update an anime item via a form submission * Update an manga item via a form submission
* *
* @return void * @return void
*/ */
public function form_update() public function form_update()
{ {
$post_data = $this->request->getParsedBody(); $data = $this->request->getParsedBody();
// Do some minor data manipulation for // Do some minor data manipulation for
// large form-based updates // large form-based updates
$transformer = new MangaListTransformer(); $transformer = new MangaListTransformer();
$post_data = $transformer->untransform($post_data); $post_data = $transformer->untransform($data);
$full_result = $this->model->update($post_data); $full_result = $this->model->updateLibraryItem($post_data);
$result = Json::decode((string)$full_result['body']); if ($full_result['statusCode'] === 200)
if ((int)$full_result['statusCode'] === 200)
{ {
$m =& $result['manga'][0]; $this->set_flash_message("Successfully updated manga.", 'success');
$title = ( ! empty($m['english_title'])) // $this->cache->purge();
? "{$m['romaji_title']} ({$m['english_title']})"
: "{$m['romaji_title']}";
$this->set_flash_message("Successfully updated {$title}.", 'success');
$this->cache->purge();
} }
else else
{ {
$this->set_flash_message('Failed to update manga.', 'error'); $this->set_flash_message('Failed to update manga.', 'error');
} }
$this->session_redirect(); $this->session_redirect();
} }
/** /**
* Update an anime item * Update a manga item
* *
* @return boolean|null * @return void
*/ */
public function update() public function update()
{ {
$result = $this->model->update($this->request->getParsedBody()); if ($this->request->getHeader('content-type')[0] === 'application/json')
$this->cache->purge(); {
$this->outputJSON($result['body'], $result['statusCode']); $data = JSON::decode((string)$this->request->getBody());
}
else
{
$data = $this->request->getParsedBody();
}
$response = $this->model->updateLibraryItem($data);
// $this->cache->purge();
$this->outputJSON($response['body'], $response['statusCode']);
} }
/** /**

View File

@ -38,10 +38,10 @@ class API extends Model {
*/ */
protected $cache; protected $cache;
/** /**
* Default settings for Guzzle * Default settings for Guzzle
* @var array * @var array
*/ */
protected $connectionDefaults = []; protected $connectionDefaults = [];
/** /**
@ -75,4 +75,4 @@ class API extends Model {
array_multisort($sort, SORT_ASC, $array); array_multisort($sort, SORT_ASC, $array);
} }
} }
// End of BaseApiModel.php // End of BaseApiModel.php

View File

@ -115,5 +115,10 @@ class Anime extends API {
{ {
return $this->kitsuModel->updateListItem($data); return $this->kitsuModel->updateListItem($data);
} }
public function deleteLibraryItem($id): bool
{
return $this->kitsuModel->deleteListItem($id);
}
} }
// End of AnimeModel.php // End of AnimeModel.php

View File

@ -18,6 +18,7 @@ namespace Aviat\AnimeClient\Model;
use Aviat\Ion\Di\ContainerInterface; use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Model\DB; use Aviat\Ion\Model\DB;
use PDO;
use PDOException; use PDOException;
/** /**

View File

@ -95,6 +95,28 @@ class Manga extends API
return $this->kitsuModel->getListItem($itemId); return $this->kitsuModel->getListItem($itemId);
} }
/**
* Update a list entry
*
* @param array $data
* @return array
*/
public function updateLibraryItem(array $data): array
{
return $this->kitsuModel->updateListItem($data);
}
/**
* Search for anime by name
*
* @param string $name
* @return array
*/
public function search($name)
{
return $this->kitsuModel->search('manga', $name);
}
/** /**
* Map transformed anime data to be organized by reading status * Map transformed anime data to be organized by reading status
* *

View File

@ -109,97 +109,5 @@ class Util {
{ {
return ! $this->is_view_page(); return ! $this->is_view_page();
} }
}
/**
* Get the path of the cached version of the image. Create the cached image
* if the file does not already exist
*
* @codeCoverageIgnore
* @param string $api_path - The original image url
* @param string $series_slug - The part of the url with the series name, becomes the image name
* @param string $type - Anime or Manga, controls cache path
* @return string - the frontend path for the cached image
* @throws \DomainException
*/
public function get_cached_image($api_path, $series_slug, $type = "anime")
{
$path_parts = explode('?', basename($api_path));
$path = current($path_parts);
$ext_parts = explode('.', $path);
$ext = end($ext_parts);
// Workaround for some broken file extensions
if ($ext === "jjpg")
{
$ext = "jpg";
}
// Failsafe for weird urls
if (strlen($ext) > 3)
{
return $api_path;
}
$img_cache_path = $this->config->get('img_cache_path');
$cached_image = "{$series_slug}.{$ext}";
$cached_path = "{$img_cache_path}/{$type}/{$cached_image}";
// Cache the file if it doesn't already exist
if ( ! file_exists($cached_path))
{
if (function_exists('curl_init'))
{
$ch = curl_init($api_path);
$fp = fopen($cached_path, 'wb');
curl_setopt_array($ch, [
CURLOPT_FILE => $fp,
CURLOPT_HEADER => 0
]);
curl_exec($ch);
curl_close($ch);
fclose($fp);
}
else if (ini_get('allow_url_fopen'))
{
copy($api_path, $cached_path);
}
else
{
throw new DomainException("Couldn't cache images because they couldn't be downloaded.");
}
// Resize the image
if ($type === 'anime')
{
$resize_width = 220;
$resize_height = 319;
$this->_resize($cached_path, $resize_width, $resize_height);
}
}
return "/public/images/{$type}/{$cached_image}";
}
/**
* Resize an image
*
* @codeCoverageIgnore
* @param string $path
* @param string $width
* @param string $height
* @return void
*/
private function _resize($path, $width, $height)
{
try
{
$img = new SimpleImage($path);
$img->resize($width, $height)->save();
}
catch (Exception $e)
{
// Catch image errors, since they don't otherwise affect
// functionality
}
}
}