2017-01-05 13:41:32 -05:00
|
|
|
<?php declare(strict_types=1);
|
|
|
|
/**
|
2017-02-15 16:13:32 -05:00
|
|
|
* Hummingbird Anime List Client
|
2017-01-05 13:41:32 -05:00
|
|
|
*
|
2018-08-22 13:48:27 -04:00
|
|
|
* An API client for Kitsu to manage anime and manga watch lists
|
2017-01-05 13:41:32 -05:00
|
|
|
*
|
2021-02-04 11:57:01 -05:00
|
|
|
* PHP version 8
|
2017-01-05 13:41:32 -05:00
|
|
|
*
|
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>
|
2021-01-13 01:52:03 -05:00
|
|
|
* @copyright 2015 - 2021 Timothy J. Warren
|
2017-01-06 23:34:56 -05:00
|
|
|
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
2020-12-10 17:06:50 -05:00
|
|
|
* @version 5.2
|
2017-03-07 20:53:58 -05:00
|
|
|
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
2017-01-11 10:34:24 -05:00
|
|
|
*/
|
|
|
|
|
2020-08-26 15:22:14 -04:00
|
|
|
namespace Aviat\AnimeClient;
|
2017-01-05 13:41:32 -05:00
|
|
|
|
2022-03-03 17:26:09 -05:00
|
|
|
use Aviat\AnimeClient\API\Kitsu\Enum\{AnimeAiringStatus, MangaPublishingStatus};
|
2017-01-05 22:24:45 -05:00
|
|
|
use DateTimeImmutable;
|
2022-03-03 17:26:09 -05:00
|
|
|
use const PHP_URL_HOST;
|
2017-01-05 13:41:32 -05:00
|
|
|
|
|
|
|
/**
|
2017-01-13 16:53:56 -05:00
|
|
|
* Data massaging helpers for the Kitsu API
|
2017-01-05 13:41:32 -05:00
|
|
|
*/
|
2022-03-03 17:26:09 -05:00
|
|
|
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-31 19:03:27 -04:00
|
|
|
public const GRAPHQL_ENDPOINT = 'https://kitsu.io/api/graphql';
|
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;
|
|
|
|
|
2017-01-05 22:24:45 -05:00
|
|
|
/**
|
|
|
|
* Determine whether an anime is airing, finished airing, or has not yet aired
|
|
|
|
*/
|
2022-03-03 17:26:09 -05:00
|
|
|
public static function getAiringStatus(?string $startDate = NULL, ?string $endDate = NULL): string
|
2017-01-05 22:24:45 -05:00
|
|
|
{
|
|
|
|
$startAirDate = new DateTimeImmutable($startDate ?? 'tomorrow');
|
2017-01-27 15:41:52 -05:00
|
|
|
$endAirDate = new DateTimeImmutable($endDate ?? 'next year');
|
2017-01-05 22:24:45 -05:00
|
|
|
$now = new DateTimeImmutable();
|
|
|
|
|
|
|
|
$isDoneAiring = $now > $endAirDate;
|
|
|
|
$isCurrentlyAiring = ($now > $startAirDate) && ! $isDoneAiring;
|
|
|
|
|
2018-10-19 10:40:11 -04:00
|
|
|
if ($isCurrentlyAiring)
|
2017-01-05 22:24:45 -05:00
|
|
|
{
|
2018-10-19 10:40:11 -04:00
|
|
|
return AnimeAiringStatus::AIRING;
|
|
|
|
}
|
2017-01-05 22:24:45 -05:00
|
|
|
|
2018-10-19 10:40:11 -04:00
|
|
|
if ($isDoneAiring)
|
|
|
|
{
|
|
|
|
return AnimeAiringStatus::FINISHED_AIRING;
|
2017-01-05 22:24:45 -05:00
|
|
|
}
|
2018-10-19 10:40:11 -04:00
|
|
|
|
|
|
|
return AnimeAiringStatus::NOT_YET_AIRED;
|
2017-01-05 22:24:45 -05:00
|
|
|
}
|
2017-02-08 15:48:20 -05:00
|
|
|
|
2021-10-08 19:31:40 -04:00
|
|
|
/**
|
|
|
|
* Reformat the airing date range for an Anime
|
|
|
|
*/
|
2022-03-03 17:26:09 -05:00
|
|
|
public static function formatAirDates(?string $startDate = NULL, ?string $endDate = NULL): string
|
2021-10-08 19:31:40 -04:00
|
|
|
{
|
|
|
|
if (empty($startDate))
|
|
|
|
{
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
$monthMap = [
|
|
|
|
'01' => 'Jan',
|
|
|
|
'02' => 'Feb',
|
|
|
|
'03' => 'Mar',
|
|
|
|
'04' => 'Apr',
|
|
|
|
'05' => 'May',
|
|
|
|
'06' => 'Jun',
|
|
|
|
'07' => 'Jul',
|
|
|
|
'08' => 'Aug',
|
|
|
|
'09' => 'Sep',
|
|
|
|
'10' => 'Oct',
|
|
|
|
'11' => 'Nov',
|
|
|
|
'12' => 'Dec',
|
|
|
|
];
|
|
|
|
|
|
|
|
[$startYear, $startMonth, $startDay] = explode('-', $startDate);
|
|
|
|
|
|
|
|
if ($startDate === $endDate)
|
|
|
|
{
|
2022-03-03 13:25:10 -05:00
|
|
|
return "{$monthMap[$startMonth]} {$startDay}, {$startYear}";
|
2021-10-08 19:31:40 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if (empty($endDate))
|
|
|
|
{
|
|
|
|
return "{$monthMap[$startMonth]} {$startYear} - ";
|
|
|
|
}
|
|
|
|
|
|
|
|
[$endYear, $endMonth] = explode('-', $endDate);
|
|
|
|
|
|
|
|
if ($startYear === $endYear)
|
|
|
|
{
|
2022-03-03 13:25:10 -05:00
|
|
|
return "{$monthMap[$startMonth]} - {$monthMap[$endMonth]} {$startYear}";
|
2021-10-08 19:31:40 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return "{$monthMap[$startMonth]} {$startYear} - {$monthMap[$endMonth]} {$endYear}";
|
|
|
|
}
|
|
|
|
|
2022-03-03 17:26:09 -05:00
|
|
|
public static function getPublishingStatus(string $kitsuStatus, ?string $startDate = NULL, ?string $endDate = NULL): string
|
2020-07-29 17:51:58 -04:00
|
|
|
{
|
|
|
|
$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;
|
|
|
|
}
|
|
|
|
|
2022-03-03 13:25:10 -05:00
|
|
|
/**
|
|
|
|
* @return array<string, string>
|
|
|
|
*/
|
2020-08-24 15:20:07 -04:00
|
|
|
public static function mappingsToUrls(array $mappings, string $kitsuLink = ''): array
|
|
|
|
{
|
|
|
|
$output = [];
|
2020-09-10 15:36:34 -04:00
|
|
|
|
|
|
|
$urlMap = [
|
|
|
|
'ANIDB' => [
|
|
|
|
'key' => 'AniDB',
|
|
|
|
'url' => 'https://anidb.net/anime/{}',
|
|
|
|
],
|
|
|
|
'ANILIST_ANIME' => [
|
|
|
|
'key' => 'Anilist',
|
|
|
|
'url' => 'https://anilist.co/anime/{}/',
|
|
|
|
],
|
|
|
|
'ANILIST_MANGA' => [
|
|
|
|
'key' => 'Anilist',
|
|
|
|
'url' => 'https://anilist.co/anime/{}/',
|
|
|
|
],
|
|
|
|
'ANIMENEWSNETWORK' => [
|
|
|
|
'key' => 'AnimeNewsNetwork',
|
|
|
|
'url' => 'https://www.animenewsnetwork.com/encyclopedia/anime.php?id={}',
|
|
|
|
],
|
|
|
|
'MANGAUPDATES' => [
|
|
|
|
'key' => 'MangaUpdates',
|
|
|
|
'url' => 'https://www.mangaupdates.com/series.html?id={}',
|
|
|
|
],
|
|
|
|
'MYANIMELIST_ANIME' => [
|
|
|
|
'key' => 'MyAnimeList',
|
|
|
|
'url' => 'https://myanimelist.net/anime/{}',
|
|
|
|
],
|
|
|
|
'MYANIMELIST_CHARACTERS' => [
|
|
|
|
'key' => 'MyAnimeList',
|
|
|
|
'url' => 'https://myanimelist.net/character/{}',
|
|
|
|
],
|
|
|
|
'MYANIMELIST_MANGA' => [
|
|
|
|
'key' => 'MyAnimeList',
|
|
|
|
'url' => 'https://myanimelist.net/manga/{}',
|
|
|
|
],
|
|
|
|
'MYANIMELIST_PEOPLE' => [
|
|
|
|
'key' => 'MyAnimeList',
|
|
|
|
'url' => 'https://myanimelist.net/people/{}',
|
|
|
|
],
|
|
|
|
];
|
|
|
|
|
2020-08-24 15:20:07 -04:00
|
|
|
foreach ($mappings as $mapping)
|
|
|
|
{
|
2020-09-10 15:36:34 -04:00
|
|
|
if ( ! array_key_exists($mapping['externalSite'], $urlMap))
|
2020-08-24 15:20:07 -04:00
|
|
|
{
|
2020-09-10 15:36:34 -04:00
|
|
|
continue;
|
|
|
|
}
|
2020-08-24 15:20:07 -04:00
|
|
|
|
2020-09-10 15:36:34 -04:00
|
|
|
$uMap = $urlMap[$mapping['externalSite']];
|
|
|
|
$key = $uMap['key'];
|
|
|
|
$url = str_replace('{}', $mapping['externalId'], $uMap['url']);
|
2020-08-24 15:20:07 -04:00
|
|
|
|
2020-09-10 15:36:34 -04:00
|
|
|
$output[$key] = $url;
|
2020-08-24 15:20:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($kitsuLink !== '')
|
|
|
|
{
|
|
|
|
$output['Kitsu'] = $kitsuLink;
|
|
|
|
}
|
|
|
|
|
|
|
|
ksort($output);
|
|
|
|
|
|
|
|
return $output;
|
|
|
|
}
|
|
|
|
|
2020-08-17 10:45:17 -04:00
|
|
|
/**
|
|
|
|
* Reorganize streaming links
|
|
|
|
*
|
2022-03-03 13:25:10 -05:00
|
|
|
* @return mixed[]
|
2020-08-17 10:45:17 -04:00
|
|
|
*/
|
|
|
|
public static function parseStreamingLinks(array $nodes): array
|
|
|
|
{
|
2021-02-10 17:17:51 -05:00
|
|
|
if (empty($nodes))
|
2020-08-17 10:45:17 -04:00
|
|
|
{
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
$links = [];
|
|
|
|
|
|
|
|
foreach ($nodes as $streamingLink)
|
|
|
|
{
|
|
|
|
$url = $streamingLink['url'];
|
|
|
|
|
|
|
|
// 'Fix' links that start with the hostname,
|
|
|
|
// rather than a protocol
|
2021-02-10 17:17:51 -05:00
|
|
|
if ( ! str_contains($url, '//'))
|
2020-08-17 10:45:17 -04:00
|
|
|
{
|
|
|
|
$url = '//' . $url;
|
|
|
|
}
|
|
|
|
|
2022-03-03 17:26:09 -05:00
|
|
|
$host = parse_url($url, PHP_URL_HOST);
|
2021-02-10 17:17:51 -05:00
|
|
|
if ($host === FALSE)
|
|
|
|
{
|
|
|
|
return [];
|
|
|
|
}
|
2020-08-17 10:45:17 -04:00
|
|
|
|
|
|
|
$links[] = [
|
2020-12-10 15:59:37 -05:00
|
|
|
'meta' => self::getServiceMetaData($host),
|
2020-08-17 10:45:17 -04:00
|
|
|
'link' => $streamingLink['url'],
|
|
|
|
'subs' => $streamingLink['subs'],
|
2022-03-03 17:26:09 -05:00
|
|
|
'dubs' => $streamingLink['dubs'],
|
2020-08-17 10:45:17 -04:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2020-12-10 15:59:37 -05:00
|
|
|
usort($links, static fn ($a, $b) => $a['meta']['name'] <=> $b['meta']['name']);
|
2020-08-17 10:45:17 -04:00
|
|
|
|
|
|
|
return $links;
|
|
|
|
}
|
|
|
|
|
2020-07-29 16:25:57 -04:00
|
|
|
/**
|
|
|
|
* Get the list of titles
|
|
|
|
*
|
2022-03-03 13:25:10 -05:00
|
|
|
* @return mixed[]
|
2020-07-29 16:25:57 -04:00
|
|
|
*/
|
|
|
|
public static function getTitles(array $titles): array
|
|
|
|
{
|
|
|
|
$raw = array_unique([
|
|
|
|
$titles['canonical'],
|
|
|
|
...array_values($titles['localized']),
|
|
|
|
]);
|
|
|
|
|
2022-03-03 17:26:09 -05:00
|
|
|
return array_diff($raw, [$titles['canonical']]);
|
2020-07-29 16:25:57 -04:00
|
|
|
}
|
|
|
|
|
2020-07-28 17:46:18 -04:00
|
|
|
/**
|
|
|
|
* Filter out duplicate and very similar titles from a GraphQL response
|
|
|
|
*
|
2022-03-03 13:25:10 -05:00
|
|
|
* @return mixed[]
|
2020-07-28 17:46:18 -04:00
|
|
|
*/
|
|
|
|
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]))
|
|
|
|
{
|
2022-03-03 17:26:09 -05:00
|
|
|
foreach ($titles[$search] as $alternateTitle)
|
2020-07-28 17:46:18 -04:00
|
|
|
{
|
|
|
|
if (self::titleIsUnique($alternateTitle, $valid))
|
|
|
|
{
|
|
|
|
$valid[] = $alternateTitle;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-25 15:11:08 -04:00
|
|
|
// Don't return the canonical title
|
2020-07-28 17:46:18 -04:00
|
|
|
array_shift($valid);
|
|
|
|
|
|
|
|
return $valid;
|
|
|
|
}
|
2020-05-18 12:52:32 -04:00
|
|
|
|
2020-08-17 16:36:55 -04:00
|
|
|
/**
|
|
|
|
* Filter out duplicate and very similar titles from a GraphQL response
|
|
|
|
*
|
2022-03-03 13:25:10 -05:00
|
|
|
* @return mixed[]
|
2020-08-17 16:36:55 -04:00
|
|
|
*/
|
|
|
|
public static function getFilteredTitles(array $titles): array
|
|
|
|
{
|
|
|
|
// The 'canonical' title is always considered
|
|
|
|
$valid = [$titles['canonical']];
|
|
|
|
|
|
|
|
if (array_key_exists('localized', $titles) && is_array($titles['localized']))
|
|
|
|
{
|
2022-03-03 17:26:09 -05:00
|
|
|
foreach ($titles['localized'] as $locale => $alternateTitle)
|
2020-08-17 16:36:55 -04:00
|
|
|
{
|
2021-10-08 18:28:30 -04:00
|
|
|
// Really don't care about languages that aren't english
|
|
|
|
// or Japanese for titles
|
2022-03-03 17:26:09 -05:00
|
|
|
if ( ! in_array($locale, ['en', 'en_us', 'en_jp', 'ja_jp'], TRUE))
|
2021-10-08 18:28:30 -04:00
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-08-17 16:36:55 -04:00
|
|
|
if (self::titleIsUnique($alternateTitle, $valid))
|
|
|
|
{
|
|
|
|
$valid[] = $alternateTitle;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-25 15:11:08 -04:00
|
|
|
// Don't return the canonical title
|
2020-08-17 16:36:55 -04:00
|
|
|
array_shift($valid);
|
|
|
|
|
|
|
|
return $valid;
|
|
|
|
}
|
|
|
|
|
2022-01-12 18:23:40 -05:00
|
|
|
/**
|
|
|
|
* Get the url of the posterImage from Kitsu, with fallbacks
|
|
|
|
*/
|
2022-01-17 09:59:27 -05:00
|
|
|
public static function getPosterImage(array $base, int $size = 1): string
|
2022-01-12 18:23:40 -05:00
|
|
|
{
|
2022-03-03 17:26:09 -05:00
|
|
|
$rawUrl = $base['posterImage']['views'][$size]['url']
|
2022-01-12 18:23:40 -05:00
|
|
|
?? $base['posterImage']['original']['url']
|
|
|
|
?? '/public/images/placeholder.png';
|
2022-01-17 08:37:00 -05:00
|
|
|
|
|
|
|
$parts = explode('?', $rawUrl);
|
|
|
|
|
2022-03-03 17:26:09 -05:00
|
|
|
return (empty($parts)) ? $rawUrl : $parts[0];
|
2022-01-12 18:23:40 -05:00
|
|
|
}
|
|
|
|
|
2020-05-18 12:52:32 -04:00
|
|
|
/**
|
|
|
|
* Get the name and logo for the streaming service of the current link
|
|
|
|
*
|
2022-03-03 17:26:09 -05:00
|
|
|
* @return bool[]|string[]
|
2020-05-18 12:52:32 -04:00
|
|
|
*/
|
2022-03-03 17:26:09 -05:00
|
|
|
private static function getServiceMetaData(?string $hostname = NULL): array
|
2020-05-18 12:52:32 -04:00
|
|
|
{
|
2020-12-10 15:59:37 -05:00
|
|
|
$hostname = str_replace('www.', '', $hostname ?? '');
|
2020-05-18 12:52:32 -04:00
|
|
|
|
|
|
|
$serviceMap = [
|
2020-09-10 15:36:34 -04:00
|
|
|
'animelab.com' => [
|
|
|
|
'name' => 'Animelab',
|
|
|
|
'link' => TRUE,
|
|
|
|
'image' => 'streaming-logos/animelab.svg',
|
|
|
|
],
|
2020-05-18 12:52:32 -04:00
|
|
|
'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,
|
2022-03-03 17:26:09 -05:00
|
|
|
'image' => 'streaming-logos/daisuki.svg',
|
2020-05-18 12:52:32 -04:00
|
|
|
],
|
|
|
|
'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,
|
2022-03-03 17:26:09 -05:00
|
|
|
'image' => 'streaming-logos/viewster.svg',
|
2020-05-18 12:52:32 -04:00
|
|
|
],
|
2020-09-09 13:25:27 -04:00
|
|
|
'vrv.co' => [
|
|
|
|
'name' => 'VRV',
|
|
|
|
'link' => TRUE,
|
|
|
|
'image' => 'streaming-logos/vrv.svg',
|
2022-03-03 17:26:09 -05:00
|
|
|
],
|
2020-05-18 12:52:32 -04:00
|
|
|
];
|
|
|
|
|
|
|
|
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
|
|
|
|
*/
|
|
|
|
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
|
2022-03-03 17:26:09 -05:00
|
|
|
$years = (int) floor($minutes / self::MINUTES_IN_YEAR);
|
2020-07-29 15:49:16 -04:00
|
|
|
$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 = [];
|
2022-03-03 17:26:09 -05:00
|
|
|
|
2020-07-29 15:49:16 -04:00
|
|
|
foreach ([
|
|
|
|
'year' => $years,
|
|
|
|
'day' => $days,
|
|
|
|
'hour' => $hours,
|
|
|
|
'minute' => $remMinutes,
|
2022-03-03 17:26:09 -05:00
|
|
|
'second' => $remSeconds,
|
|
|
|
] as $label => $value)
|
2020-07-29 15:49:16 -04:00
|
|
|
{
|
|
|
|
if ($value === 0)
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($value > 1)
|
|
|
|
{
|
|
|
|
$label .= 's';
|
|
|
|
}
|
|
|
|
|
|
|
|
$parts[] = "{$value} {$label}";
|
|
|
|
}
|
|
|
|
|
|
|
|
$last = array_pop($parts);
|
|
|
|
|
|
|
|
if (empty($parts))
|
|
|
|
{
|
2022-03-03 13:25:10 -05:00
|
|
|
return $last ?? '';
|
2020-07-29 15:49:16 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return (count($parts) > 1)
|
|
|
|
? implode(', ', $parts) . ", and {$last}"
|
|
|
|
: "{$parts[0]}, {$last}";
|
|
|
|
}
|
|
|
|
|
2017-01-05 22:24:45 -05:00
|
|
|
/**
|
|
|
|
* Determine if an alternate title is unique enough to list
|
|
|
|
*/
|
2022-03-03 17:26:09 -05:00
|
|
|
private static function titleIsUnique(?string $title = '', array $existingTitles = []): bool
|
2017-01-05 22:24:45 -05:00
|
|
|
{
|
2021-02-03 09:45:18 -05:00
|
|
|
if (empty($title))
|
|
|
|
{
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
2022-03-03 17:26:09 -05:00
|
|
|
foreach ($existingTitles as $existing)
|
2017-01-05 22:24:45 -05:00
|
|
|
{
|
2017-04-07 16:58:08 -04:00
|
|
|
$isSubset = mb_substr_count($existing, $title) > 0;
|
2018-08-10 20:10:19 -04:00
|
|
|
$diff = levenshtein(mb_strtolower($existing), mb_strtolower($title));
|
2017-01-05 22:24:45 -05:00
|
|
|
|
2018-08-13 15:13:20 -04:00
|
|
|
if ($diff <= 4 || $isSubset || mb_strlen($title) > 45 || mb_strlen($existing) > 50)
|
2017-01-05 22:24:45 -05:00
|
|
|
{
|
2017-02-17 10:55:17 -05:00
|
|
|
return FALSE;
|
2017-01-05 22:24:45 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-17 10:55:17 -05:00
|
|
|
return TRUE;
|
2017-01-05 22:24:45 -05:00
|
|
|
}
|
2022-03-03 17:26:09 -05:00
|
|
|
}
|