HummingBirdAnimeClient/src/AnimeClient/API/Kitsu.php

403 lines
9.4 KiB
PHP
Raw Normal View History

<?php declare(strict_types=1);
/**
2017-02-15 16:13:32 -05:00
* Hummingbird Anime List Client
*
2018-08-22 13:48:27 -04:00
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
2017-02-15 16:13:32 -05:00
* @package HummingbirdAnimeClient
2017-01-06 23:34:56 -05:00
* @author Timothy J. Warren <tim@timshomepage.net>
2020-01-08 15:39:49 -05:00
* @copyright 2015 - 2020 Timothy J. Warren
2017-01-06 23:34:56 -05:00
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\API;
2017-03-01 22:07:51 -05:00
use Aviat\AnimeClient\API\Kitsu\Enum\AnimeAiringStatus;
use Aviat\AnimeClient\API\Kitsu\Enum\MangaPublishingStatus;
use DateTimeImmutable;
/**
* Data massaging helpers for the Kitsu API
*/
final class Kitsu {
2018-11-09 10:38:35 -05:00
public const AUTH_URL = 'https://kitsu.io/api/oauth/token';
public const AUTH_USER_ID_KEY = 'kitsu-auth-userid';
public const AUTH_TOKEN_CACHE_KEY = 'kitsu-auth-token';
public const AUTH_TOKEN_EXP_CACHE_KEY = 'kitsu-auth-token-expires';
public const AUTH_TOKEN_REFRESH_CACHE_KEY = 'kitsu-auth-token-refresh';
2020-05-08 19:15:21 -04:00
public const ANIME_HISTORY_LIST_CACHE_KEY = 'kitsu-anime-history-list';
public const MANGA_HISTORY_LIST_CACHE_KEY = 'kitsu-manga-history-list';
2020-07-29 15:49:16 -04:00
public const SECONDS_IN_MINUTE = 60;
public const MINUTES_IN_HOUR = 60;
public const MINUTES_IN_DAY = 1440;
public const MINUTES_IN_YEAR = 525_600;
/**
* Determine whether an anime is airing, finished airing, or has not yet aired
*
* @param string $startDate
* @param string $endDate
* @return string
*/
2017-02-17 10:55:17 -05:00
public static function getAiringStatus(string $startDate = NULL, string $endDate = NULL): string
{
$startAirDate = new DateTimeImmutable($startDate ?? 'tomorrow');
$endAirDate = new DateTimeImmutable($endDate ?? 'next year');
$now = new DateTimeImmutable();
$isDoneAiring = $now > $endAirDate;
$isCurrentlyAiring = ($now > $startAirDate) && ! $isDoneAiring;
2018-10-19 10:40:11 -04:00
if ($isCurrentlyAiring)
{
2018-10-19 10:40:11 -04:00
return AnimeAiringStatus::AIRING;
}
2018-10-19 10:40:11 -04:00
if ($isDoneAiring)
{
return AnimeAiringStatus::FINISHED_AIRING;
}
2018-10-19 10:40:11 -04:00
return AnimeAiringStatus::NOT_YET_AIRED;
}
public static function getPublishingStatus(string $kitsuStatus, string $startDate = NULL, string $endDate = NULL): string
{
$startPubDate = new DateTimeImmutable($startDate ?? 'tomorrow');
$endPubDate = new DateTimeImmutable($endDate ?? 'next year');
$now = new DateTimeImmutable();
$isDone = $now > $endPubDate;
$isCurrent = ($now > $startPubDate) && ! $isDone;
if ($kitsuStatus === 'CURRENT' || $isCurrent)
{
return MangaPublishingStatus::CURRENT;
}
if ($kitsuStatus === 'FINISHED' || $isDone)
{
return MangaPublishingStatus::FINISHED;
}
return MangaPublishingStatus::NOT_YET_PUBLISHED;
}
/**
* Reorganize streaming links
*
* @param array $included
* @return array
*/
public static function parseStreamingLinks(array $included): array
{
if (
( ! array_key_exists('streamingLinks', $included)) ||
count($included['streamingLinks']) === 0
)
{
return [];
}
$links = [];
foreach ($included['streamingLinks'] as $streamingLink)
{
$url = $streamingLink['url'];
// 'Fix' links that start with the hostname,
// rather than a protocol
if (strpos($url, '//') === FALSE)
{
$url = '//' . $url;
}
$host = parse_url($url, \PHP_URL_HOST);
$links[] = [
'meta' => static::getServiceMetaData($host),
'link' => $streamingLink['url'],
'subs' => $streamingLink['subs'],
'dubs' => $streamingLink['dubs']
];
}
2018-11-09 10:38:35 -05:00
2020-04-21 19:22:56 -04:00
usort($links, fn ($a, $b) => $a['meta']['name'] <=> $b['meta']['name']);
return $links;
}
/**
* Reorganize streaming links for the current list item
*
* @param array $included
2017-02-17 10:55:17 -05:00
* @param string $animeId
* @return array
*/
public static function parseListItemStreamingLinks(array $included, string $animeId): array
{
// Anime lists have a different structure to search through
if (array_key_exists('anime', $included) && ! array_key_exists('streamingLinks', $included))
{
$links = [];
$anime = $included['anime'][$animeId];
if (count($anime['relationships']['streamingLinks']) > 0)
{
return static::parseStreamingLinks($anime['relationships']);
}
return $links;
}
return [];
}
/**
* Get the list of titles
*
* @param array $data
* @return array
*/
public static function oldGetTitles(array $data): array
{
$raw = array_unique([
$data['canonicalTitle'],
...array_values($data['titles']),
2020-05-06 10:12:49 -04:00
...array_values($data['abbreviatedTitles'] ?? []),
]);
return array_diff($raw,[$data['canonicalTitle']]);
}
/**
* Get the list of titles
*
* @param array $titles
* @return array
*/
public static function getTitles(array $titles): array
{
$raw = array_unique([
$titles['canonical'],
...array_values($titles['localized']),
// ...array_values($data['abbreviatedTitles'] ?? []),
]);
return array_diff($raw,[$titles['canonical']]);
}
/**
* Filter out duplicate and very similar names from
*
* @param array $data The 'attributes' section of the api data response
* @return array List of alternate titles
*/
public static function filterTitles(array $data): array
{
// The 'canonical' title is always returned
$valid = [$data['canonicalTitle']];
if (array_key_exists('titles', $data) && is_array($data['titles']))
{
foreach($data['titles'] as $alternateTitle)
{
if (self::titleIsUnique($alternateTitle, $valid))
{
$valid[] = $alternateTitle;
}
}
}
return $valid;
}
/**
* Filter out duplicate and very similar titles from a GraphQL response
*
* @param array $titles
* @return array
*/
public static function filterLocalizedTitles(array $titles): array
{
// The 'canonical' title is always considered
$valid = [$titles['canonical']];
foreach (['alternatives', 'localized'] as $search)
{
if (array_key_exists($search, $titles) && is_array($titles[$search]))
{
foreach($titles[$search] as $alternateTitle)
{
if (self::titleIsUnique($alternateTitle, $valid))
{
$valid[] = $alternateTitle;
}
}
}
}
// Don't return the canonical titles
array_shift($valid);
return $valid;
}
/**
* Get the name and logo for the streaming service of the current link
*
* @param string $hostname
* @return array
*/
protected static function getServiceMetaData(string $hostname = NULL): array
{
$hostname = str_replace('www.', '', $hostname);
$serviceMap = [
'amazon.com' => [
'name' => 'Amazon Prime',
'link' => TRUE,
'image' => 'streaming-logos/amazon.svg',
],
'crunchyroll.com' => [
'name' => 'Crunchyroll',
'link' => TRUE,
'image' => 'streaming-logos/crunchyroll.svg',
],
'daisuki.net' => [
'name' => 'Daisuki',
'link' => TRUE,
'image' => 'streaming-logos/daisuki.svg'
],
'funimation.com' => [
'name' => 'Funimation',
'link' => TRUE,
'image' => 'streaming-logos/funimation.svg',
],
'hidive.com' => [
'name' => 'Hidive',
'link' => TRUE,
'image' => 'streaming-logos/hidive.svg',
],
'hulu.com' => [
'name' => 'Hulu',
'link' => TRUE,
'image' => 'streaming-logos/hulu.svg',
],
'tubitv.com' => [
'name' => 'TubiTV',
'link' => TRUE,
'image' => 'streaming-logos/tubitv.svg',
],
'viewster.com' => [
'name' => 'Viewster',
'link' => TRUE,
'image' => 'streaming-logos/viewster.svg'
],
];
if (array_key_exists($hostname, $serviceMap))
{
return $serviceMap[$hostname];
}
// Default to Netflix, because the API links are broken,
// and there's no other real identifier for Netflix
return [
'name' => 'Netflix',
'link' => FALSE,
'image' => 'streaming-logos/netflix.svg',
];
}
2020-07-29 15:49:16 -04:00
/**
* Convert a time in seconds to a more human-readable format
*
* @param int $seconds
* @return string
*/
public static function friendlyTime(int $seconds): string
{
// All the seconds left
$remSeconds = $seconds % self::SECONDS_IN_MINUTE;
$minutes = ($seconds - $remSeconds) / self::SECONDS_IN_MINUTE;
// Minutes short of a year
$years = (int)floor($minutes / self::MINUTES_IN_YEAR);
$minutes %= self::MINUTES_IN_YEAR;
// Minutes short of a day
$extraMinutes = $minutes % self::MINUTES_IN_DAY;
$days = ($minutes - $extraMinutes) / self::MINUTES_IN_DAY;
// Minutes short of an hour
$remMinutes = $extraMinutes % self::MINUTES_IN_HOUR;
$hours = ($extraMinutes - $remMinutes) / self::MINUTES_IN_HOUR;
$parts = [];
foreach ([
'year' => $years,
'day' => $days,
'hour' => $hours,
'minute' => $remMinutes,
'second' => $remSeconds
] as $label => $value)
{
if ($value === 0)
{
continue;
}
if ($value > 1)
{
$label .= 's';
}
$parts[] = "{$value} {$label}";
}
$last = array_pop($parts);
if (empty($parts))
{
return $last;
}
return (count($parts) > 1)
? implode(', ', $parts) . ", and {$last}"
: "{$parts[0]}, {$last}";
}
/**
* Determine if an alternate title is unique enough to list
*
* @param string $title
* @param array $existingTitles
* @return bool
*/
2017-02-17 10:55:17 -05:00
private static function titleIsUnique(string $title = NULL, array $existingTitles = []): bool
{
if (empty($title))
{
2017-02-17 10:55:17 -05:00
return FALSE;
}
foreach($existingTitles as $existing)
{
$isSubset = mb_substr_count($existing, $title) > 0;
$diff = levenshtein(mb_strtolower($existing), mb_strtolower($title));
2018-08-13 15:13:20 -04:00
if ($diff <= 4 || $isSubset || mb_strlen($title) > 45 || mb_strlen($existing) > 50)
{
2017-02-17 10:55:17 -05:00
return FALSE;
}
}
2017-02-17 10:55:17 -05:00
return TRUE;
}
}