uiharu/src/Lookup/YouTubeLookup.php

122 lines
4.6 KiB
PHP

<?php
namespace Uiharu\Lookup;
use RuntimeException;
use Syokuhou\IConfig;
use Uiharu\Config;
use Uiharu\Url;
final class YouTubeLookup implements \Uiharu\ILookup {
private const SHORT_DOMAINS = [
'youtu.be', 'www.youtu.be', // www. doesn't work for this, but may as well cover it
];
private const VALID_TLDS = [
'ae', 'at', 'az', 'ba', 'be', 'bg', 'bh', 'bo', 'by',
'ca', 'cat', 'ch', 'cl', 'co', 'co.ae', 'co.at', 'co.cr', 'co.hu',
'co.id', 'co.il', 'co.in', 'co.jp', 'co.ke', 'co.kr', 'co.ma', 'co.nz',
'co.th', 'co.tz', 'co.ug', 'co.uk', 'co.ve', 'co.za', 'co.zw',
'com', 'com.ar', 'com.au', 'com.az', 'com.bd', 'com.bh', 'com.bo',
'com.br', 'com.by', 'com.co', 'com.do', 'com.ec', 'com.ee', 'com.eg',
'com.es', 'com.gh', 'com.gr', 'com.gt', 'com.hk', 'com.hn', 'com.hr',
'com.jm', 'com.jo', 'com.kw', 'com.lb', 'com.lv', 'com.ly', 'com.mk',
'com.mt', 'com.mx', 'com.my', 'com.ng', 'com.ni', 'com.om', 'com.pa',
'com.pe', 'com.ph', 'com.pk', 'com.pt', 'com.py', 'com.qa', 'com.ro',
'com.sa', 'com.sg', 'com.sv', 'com.tn', 'com.tr', 'com.tw', 'com.ua',
'com.uy', 'com.ve', 'cr', 'cz', 'de', 'dk', 'ee', 'es', 'fi', 'fr',
'ge', 'gr', 'gt', 'hk', 'hr', 'hu', 'ie', 'in', 'iq', 'is', 'it', 'jo',
'jp', 'kr', 'kz', 'lk', 'lt', 'lu', 'lv', 'ly', 'ma', 'me', 'mk', 'mx',
'my', 'net.in', 'ng', 'ni', 'nl', 'no', 'pa', 'pe', 'ph', 'pk', 'pl',
'pr', 'pt', 'qa', 'ro', 'rs', 'ru', 'sa', 'se', 'sg', 'si', 'sk', 'sn',
'sv', 'tn', 'ua', 'ug', 'uy', 'vn',
];
public static function isShortDomain(string $host): bool {
return in_array($host, self::SHORT_DOMAINS);
}
public function __construct(private IConfig $config) {}
public function match(Url $url): bool {
if(!$url->isWeb())
return false;
$urlHost = strtolower($url->getHost());
if(self::isShortDomain($urlHost))
return true;
$parts = array_reverse(explode('.', $urlHost));
$partsCount = count($parts);
if($partsCount < 2 || $partsCount > 4)
return false;
if($parts[$partsCount - 1] === 'www')
array_pop($parts);
if($parts[0] === 'com') {
if($parts[1] !== 'youtube' && $parts[1] !== 'youtube-nocookie')
return false;
} else {
if(array_pop($parts) !== 'youtube')
return false;
$tld = implode('.', array_reverse($parts));
if(!in_array($tld, self::VALID_TLDS))
return false;
}
$urlPath = $url->getPath();
return $urlPath === '/watch'
|| str_starts_with($urlPath, '/watch/');
}
private function lookupVideo(string $videoId): ?object {
$curl = curl_init("https://www.googleapis.com/youtube/v3/videos?part=snippet%2CcontentDetails%2Cstatistics&id={$videoId}&key=" . $this->config->getString('api_key'));
curl_setopt_array($curl, [
CURLOPT_AUTOREFERER => false,
CURLOPT_CERTINFO => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 2,
CURLOPT_USERAGENT => 'Uiharu/' . UIH_VERSION,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
],
]);
$resp = curl_exec($curl);
curl_close($curl);
return json_decode($resp);
}
public function lookup(Url $url): YouTubeLookupResult {
$urlPath = $url->getPath();
parse_str($url->getQuery(), $urlQuery);
if(self::isShortDomain($url->getHost())) {
$videoId = substr($urlPath, 1);
} else {
if(str_starts_with($urlPath, '/watch/'))
$videoId = explode('/', trim($urlPath, '/'))[1] ?? '';
else
$videoId = $urlQuery['v'] ?? '';
}
if(empty($videoId))
throw new RuntimeException('YouTube video id missing.');
$videoInfo = $this->lookupVideo($videoId);
if($videoInfo === null)
throw new RuntimeException('YouTube video with given id could not be found.');
unset($urlQuery['v']);
$url = Url::parse(trim('https://www.youtube.com/watch?v=' . $videoId . '&' . http_build_query($urlQuery), '&'));
return new YouTubeLookupResult($url, $videoId, $videoInfo, $urlQuery);
}
}