HummingBirdAnimeClient/src/AnimeClient/AnimeClient.php

379 lines
7.7 KiB
PHP
Raw Normal View History

2016-10-20 22:09:36 -04:00
<?php declare(strict_types=1);
2015-11-16 19:30:04 -05:00
/**
* Hummingbird Anime List Client
2015-11-16 19:30:04 -05:00
*
2018-08-22 13:48:27 -04:00
* An API client for Kitsu to manage anime and manga watch lists
2015-11-16 19:30:04 -05:00
*
2021-02-04 11:57:01 -05:00
* PHP version 8
2016-08-30 10:01:18 -04:00
*
2015-11-16 19:30:04 -05:00
* @package HummingbirdAnimeClient
2016-08-30 10:01:18 -04:00
* @author Timothy J. Warren <tim@timshomepage.net>
2021-01-13 01:52:03 -05:00
* @copyright 2015 - 2021 Timothy J. Warren
2016-08-30 10:01:18 -04:00
* @license http://www.opensource.org/licenses/mit-license.html MIT License
2020-12-10 17:06:50 -05:00
* @version 5.2
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
2015-11-16 19:30:04 -05:00
*/
namespace Aviat\AnimeClient;
use Aviat\Ion\ImageBuilder;
2020-05-08 19:15:21 -04:00
use Psr\SimpleCache\CacheInterface;
2018-11-29 11:00:50 -05:00
use Amp\Http\Client\Request;
2020-03-11 16:26:17 -04:00
use Amp\Http\Client\Response;
use Amp\Http\Client\HttpClient;
use Amp\Http\Client\HttpClientBuilder;
2018-11-29 11:00:50 -05:00
use Aviat\Ion\ConfigInterface;
2018-08-20 13:01:16 -04:00
use Yosymfony\Toml\{Toml, TomlBuilder};
2020-05-08 19:15:21 -04:00
use Throwable;
2021-02-03 09:45:18 -05:00
use function Amp\Promise\wait;
use function Aviat\Ion\_dir;
// ----------------------------------------------------------------------------
//! TOML Functions
// ----------------------------------------------------------------------------
/**
* Load configuration options from .toml files
*
2020-12-11 15:37:55 -05:00
* @codeCoverageIgnore
* @param string $path - Path to load config
* @return array
*/
2020-12-11 15:37:55 -05:00
function loadConfig(string $path): array
2017-12-06 14:40:13 -05:00
{
$output = [];
$files = glob("{$path}/*.toml");
2021-02-10 13:59:37 -05:00
if ( ! is_array($files))
{
return [];
}
foreach ($files as $file)
{
$key = str_replace('.toml', '', basename($file));
if ($key === 'admin-override')
{
continue;
}
$config = Toml::parseFile($file);
if ($key === 'config')
{
foreach($config as $name => $value)
{
$output[$name] = $value;
}
continue;
}
$output[$key] = $config;
}
return $output;
2017-12-06 14:40:13 -05:00
}
/**
* Load config from one specific TOML file
*
2020-12-11 15:37:55 -05:00
* @codeCoverageIgnore
* @param string $filename
* @return array
*/
function loadTomlFile(string $filename): array
{
return Toml::parseFile($filename);
}
2021-02-11 19:54:22 -05:00
function _iterateToml(TomlBuilder $builder, iterable $data, mixed $parentKey = NULL): void
2018-08-20 13:01:16 -04:00
{
2020-12-11 15:37:55 -05:00
foreach ($data as $key => $value)
2018-08-20 13:01:16 -04:00
{
2020-12-11 15:37:55 -05:00
// Skip unsupported empty value
if ($value === NULL)
2018-08-20 13:01:16 -04:00
{
2020-12-11 15:37:55 -05:00
continue;
}
2018-08-20 13:01:16 -04:00
2020-12-11 15:37:55 -05:00
if (is_scalar($value) || isSequentialArray($value))
{
$builder->addValue($key, $value);
continue;
}
2018-08-20 13:01:16 -04:00
2020-12-11 15:37:55 -05:00
$newKey = ($parentKey !== NULL)
? "{$parentKey}.{$key}"
: $key;
2018-08-20 13:01:16 -04:00
2020-12-11 15:37:55 -05:00
if ( ! isSequentialArray($value))
{
$builder->addTable($newKey);
2020-08-26 15:22:14 -04:00
}
2020-12-11 15:37:55 -05:00
_iterateToml($builder, $value, $newKey);
2018-08-20 13:01:16 -04:00
}
2020-12-11 15:37:55 -05:00
}
/**
* Serialize config data into a Toml file
*
* @param iterable $data
2020-12-11 15:37:55 -05:00
* @return string
*/
function arrayToToml(iterable $data): string
{
$builder = new TomlBuilder();
2018-08-20 13:01:16 -04:00
_iterateToml($builder, $data);
return $builder->getTomlString();
}
/**
* Serialize toml back to an array
*
* @param string $toml
* @return array
*/
function tomlToArray(string $toml): array
{
return Toml::parse($toml);
}
// ----------------------------------------------------------------------------
//! Misc Functions
// ----------------------------------------------------------------------------
if ( ! function_exists('array_is_list'))
{
/**
* Polyfill for PHP 8
*
* @see https://www.php.net/manual/en/function.array-is-list
* @param array $a
* @return bool
*/
function array_is_list(array $a): bool
{
return $a === [] || (array_keys($a) === range(0, count($a) - 1));
}
}
/**
* Is the array sequential, not associative?
*
* @param mixed $array
* @return bool
*/
function isSequentialArray(mixed $array): bool
{
if ( ! is_array($array))
{
return FALSE;
}
return array_is_list($array);
}
/**
* Check that folder permissions are correct for proper operation
*
* @param ConfigInterface $config
* @return array
*/
function checkFolderPermissions(ConfigInterface $config): array
{
$errors = [];
$publicDir = $config->get('asset_dir');
2021-02-23 15:38:29 -05:00
$APP_DIR = _dir($config->get('root'), 'app');
2021-02-03 09:45:18 -05:00
$pathMap = [
2021-02-03 09:45:18 -05:00
'app/config' => "{$APP_DIR}/config",
'app/logs' => "{$APP_DIR}/logs",
'public/images/avatars' => "{$publicDir}/images/avatars",
'public/images/anime' => "{$publicDir}/images/anime",
'public/images/characters' => "{$publicDir}/images/characters",
'public/images/manga' => "{$publicDir}/images/manga",
'public/images/people' => "{$publicDir}/images/people",
];
foreach ($pathMap as $pretty => $actual)
{
// Make sure the folder exists first
if ( ! is_dir($actual))
{
$errors['missing'][] = $pretty;
continue;
}
$writable = is_writable($actual) && is_executable($actual);
if ( ! $writable)
{
2021-02-23 15:38:29 -05:00
// @codeCoverageIgnoreStart
$errors['writable'][] = $pretty;
2021-02-23 15:38:29 -05:00
// @codeCoverageIgnoreEnd
}
}
return $errors;
}
/**
* Get an API Client, with better defaults
*
2020-04-21 19:22:56 -04:00
* @return HttpClient
*/
2020-04-21 19:22:56 -04:00
function getApiClient (): HttpClient
{
static $client;
if ($client === NULL)
{
2020-03-11 16:26:17 -04:00
$client = HttpClientBuilder::buildDefault();
}
return $client;
}
2018-11-29 11:00:50 -05:00
/**
2020-03-11 16:26:17 -04:00
* Simplify making a request with Http\Client
2018-11-29 11:00:50 -05:00
*
* @param string|Request $request
2018-11-29 11:00:50 -05:00
* @return Response
2020-05-08 19:15:21 -04:00
* @throws Throwable
2018-11-29 11:00:50 -05:00
*/
2021-02-03 09:46:36 -05:00
function getResponse (Request|string $request): Response
2018-11-29 11:00:50 -05:00
{
$client = getApiClient();
if (is_string($request))
{
$request = new Request($request);
}
return wait($client->request($request));
2018-11-29 11:00:50 -05:00
}
/**
* Generate the path for the cached image from the original image
*
* @param string $kitsuUrl
* @param bool $webp
* @return string
*/
function getLocalImg (string $kitsuUrl, bool $webp = TRUE): string
{
2020-05-08 19:15:21 -04:00
if (empty($kitsuUrl) || ( ! is_string($kitsuUrl)))
{
return 'images/placeholder.webp';
}
$parts = parse_url($kitsuUrl);
if ($parts === FALSE || ! array_key_exists('path', $parts))
{
return 'images/placeholder.webp';
}
$file = basename($parts['path']);
$fileParts = explode('.', $file);
$ext = array_pop($fileParts);
$ext = $webp ? 'webp' : $ext;
$segments = explode('/', trim($parts['path'], '/'));
$type = $segments[0] === 'users' ? $segments[1] : $segments[0];
$id = $segments[count($segments) - 2];
return implode('/', ['images', $type, "{$id}.{$ext}"]);
}
/**
* Create a transparent placeholder image
*
2021-02-23 15:38:29 -05:00
* @codeCoverageIgnore
* @param string $path
* @param int $width
* @param int $height
* @param string $text
* @return bool
*/
function createPlaceholderImage (string $path, int $width = 200, int $height = 200, string $text = 'Image Unavailable'): bool
{
$img = ImageBuilder::new($width, $height)
->enableAlphaBlending(TRUE)
->addBackgroundColor(255, 255, 255, 127)
->addCenteredText($text, 64, 64, 64, 1);
$path = rtrim($path, '/');
$savedPng = $img->savePng($path . '/placeholder.png');
$savedWebp = $img->saveWebp($path . '/placeholder.webp');
$img->cleanup();
return $savedPng && $savedWebp;
2020-04-30 15:33:16 -04:00
}
/**
* Check that there is a value for at least one item in a collection with the specified key
*
* @param array $search
* @param string $key
* @return bool
*/
2020-08-26 15:22:14 -04:00
function colNotEmpty(array $search, string $key): bool
2020-04-30 15:33:16 -04:00
{
2020-12-11 15:37:55 -05:00
$items = array_filter(array_column($search, $key), static fn ($x) => ( ! empty($x)));
2020-04-30 15:33:16 -04:00
return count($items) > 0;
2020-05-08 19:15:21 -04:00
}
/**
* Clear the cache, but save user auth data
*
* @param CacheInterface $cache
* @return bool
*/
function clearCache(CacheInterface $cache): bool
{
// Save the user data, if it exists, for priming the cache
$userData = $cache->getMultiple([
Kitsu::AUTH_USER_ID_KEY,
Kitsu::AUTH_TOKEN_CACHE_KEY,
Kitsu::AUTH_TOKEN_EXP_CACHE_KEY,
Kitsu::AUTH_TOKEN_REFRESH_CACHE_KEY,
2020-12-11 15:37:55 -05:00
]);
2020-05-08 19:15:21 -04:00
2020-12-10 15:59:37 -05:00
$userData = array_filter((array)$userData, static fn ($value) => $value !== NULL);
2020-05-08 19:15:21 -04:00
$cleared = $cache->clear();
2021-02-23 15:38:29 -05:00
$saved = ( ! empty($userData)) ? $cache->setMultiple($userData) : TRUE;
2020-05-08 19:15:21 -04:00
return $cleared && $saved;
2020-08-26 17:26:42 -04:00
}
/**
* Render a PHP code template as a string
*
2020-12-11 15:37:55 -05:00
* @codeCoverageIgnore
2020-08-26 17:26:42 -04:00
* @param string $path
* @param array $data
* @return string
*/
function renderTemplate(string $path, array $data): string
{
ob_start();
extract($data, EXTR_OVERWRITE);
include $path;
2021-02-12 11:14:45 -05:00
$rawOutput = ob_get_clean();
return (is_string($rawOutput)) ? $rawOutput : '';
}