Removed PHP code.

This commit is contained in:
flash 2024-10-23 20:13:53 +00:00
parent 2c265fa702
commit 792cf74367
25 changed files with 0 additions and 3593 deletions

View file

@ -1,11 +0,0 @@
{
"require": {
"flashwave/index": "^0.2410",
"sentry/sdk": "^4.0"
},
"autoload": {
"psr-4": {
"Uiharu\\": "src"
}
}
}

1359
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,21 +0,0 @@
<?php
namespace Uiharu;
require_once __DIR__ . '/../uiharu.php';
$ctx->registerLookup(new \Uiharu\Lookup\EEPROMLookup('eeprom', 'eeprom.flashii.net', ['i.fii.moe', 'i.flashii.net']));
if(UIH_DEBUG)
$ctx->registerLookup(new \Uiharu\Lookup\EEPROMLookup('devrom', 'eeprom.edgii.net', ['i.edgii.net']));
$ctx->registerLookup(new \Uiharu\Lookup\YouTubeLookup($cfg->scopeTo('google')));
$ctx->registerLookup(new \Uiharu\Lookup\NicoNicoLookup);
// this should always come AFTER other lookups involved http(s)
$ctx->registerLookup(new \Uiharu\Lookup\WebLookup);
$ctx->setupHttp();
$ctx->registerApi(new \Uiharu\Apis\v1_0($ctx));
$ctx->matchApi(filter_input(INPUT_SERVER, 'REQUEST_URI'));
$ctx->dispatchHttp();

View file

@ -1,349 +0,0 @@
<?php
namespace Uiharu\Apis;
use stdClass;
use DOMDocument;
use Exception;
use InvalidArgumentException;
use Uiharu\Config;
use Uiharu\FFMPEG;
use Uiharu\IHasMediaInfo;
use Uiharu\MediaTypeExts;
use Uiharu\UihContext;
use Uiharu\Url;
use Uiharu\Lookup\EEPROMLookupResult;
use Uiharu\Lookup\YouTubeLookupResult;
use Uiharu\Lookup\NicoNicoLookupResult;
use Index\MediaType;
use Index\Cache\CacheProvider;
use Index\Colour\{Colour,ColourRgb};
use Index\Http\Routing\{HttpGet,HttpPost,RouteHandlerTrait};
use Index\Performance\Stopwatch;
final class v1_0 implements \Uiharu\IApi {
use RouteHandlerTrait;
private UihContext $ctx;
private CacheProvider $cache;
public function __construct(UihContext $ctx) {
$this->ctx = $ctx;
$this->cache = $ctx->getCache();
}
public function match(string $url): string {
return !str_starts_with($url, '/v');
}
#[HttpGet('/metadata/thumb/audio')]
public function getThumbAudio($response, $request) {
$targetUrl = (string)$request->getParam('url');
if(empty($targetUrl))
return 400;
try {
$parsedUrl = Url::parse($targetUrl);
} catch(InvalidArgumentException $ex) {
return 404;
}
if(!$parsedUrl->isWeb())
return 400;
$response->setContentType('image/png');
$response->setCacheControl('public', 'max-age=31536000', 'immutable');
$response->setContent(FFMPEG::grabFirstAudioCover($parsedUrl));
}
#[HttpGet('/metadata/thumb/video')]
public function getThumbVideo($response, $request) {
$targetUrl = (string)$request->getParam('url');
if(empty($targetUrl))
return 400;
try {
$parsedUrl = Url::parse($targetUrl);
} catch(InvalidArgumentException $ex) {
return 404;
}
if(!$parsedUrl->isWeb())
return 400;
$response->setContentType('image/png');
$response->setCacheControl('public', 'max-age=31536000', 'immutable');
$response->setContent(FFMPEG::grabFirstVideoFrame($parsedUrl));
}
#[HttpGet('/metadata')]
public function getMetadata($response, $request) {
if($request->getMethod() === 'HEAD') {
$response->setTypeJson();
return;
}
return $this->handleMetadata(
$response, $request,
(string)$request->getParam('url')
);
}
#[HttpPost('/metadata')]
public function postMetadata($response, $request) {
if(!$request->isStringContent())
return 400;
return $this->handleMetadata(
$response, $request,
(string)$request->getContent()
);
}
#[HttpGet('/metadata/batch')]
public function getMetadataBatch($response, $request) {
if($request->getMethod() === 'HEAD') {
$response->setTypeJson();
return;
}
return $this->handleMetadataBatch(
$response, $request,
$request->getParam('url', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY)
);
}
#[HttpPost('/metadata/batch')]
public function postMetadataBatch($response, $request) {
if(!$request->isFormContent())
return 400;
return $this->handleMetadataBatch(
$response, $request,
$request->getContent()->getParam('url', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY)
);
}
private function metadataLookup(string $targetUrl) {
$sw = Stopwatch::startNew();
$resp = new stdClass;
if(empty($targetUrl))
return 400;
try {
$parsedUrl = Url::parse($targetUrl);
} catch(InvalidArgumentException $ex) {
$resp->status = 400;
$resp->error = 'metadata:uri';
return $resp;
}
// if no scheme is specified, try https
if(!$parsedUrl->hasScheme())
$parsedUrl->setScheme('https');
$resp->url = (string)$parsedUrl;
$resp->cached = false;
$urlHash = $parsedUrl->calculateHash(false);
$cacheKey = sprintf('uiharu:metadata:%s', $urlHash);
$cacheResp = $this->cache->get($cacheKey);
if(is_string($cacheResp)) {
$cacheResp = json_decode($cacheResp);
if($cacheResp !== null) {
$resp = $cacheResp;
$resp->cached = true;
}
}
if(empty($resp->type)) {
$lookup = $this->ctx->matchLookup($parsedUrl);
if($lookup !== null)
try {
$result = $lookup->lookup($parsedUrl);
$resp->type = $result->getObjectType();
if($result->hasMediaType())
$resp->content_type = MediaTypeExts::toV1($result->getMediaType());
if($result->hasColour()) {
$colour = $result->getColour();
if($colour->getAlpha() < 1.0)
$colour = new ColourRgb(
$colour->getRed(),
$colour->getGreen(),
$colour->getBlue()
);
$resp->color = (string)$colour;
}
if($result->hasTitle())
$resp->title = $result->getTitle();
if($result->hasSiteName())
$resp->site_name = $result->getSiteName();
if($result->hasDescription())
$resp->description = $result->getDescription();
if($result->hasPreviewImage())
$resp->image = $result->getPreviewImage();
if($result instanceof YouTubeLookupResult) {
$resp->youtube_video_id = $result->getYouTubeVideoId();
if($result->hasYouTubeVideoStartTime())
$resp->youtube_start_time = $result->getYouTubeVideoStartTime();
if($result->hasYouTubePlayListId())
$resp->youtube_playlist = $result->getYouTubePlayListId();
if($result->hasYouTubePlayListIndex())
$resp->youtube_playlist_index = $result->getYouTubePlayListIndex();
if(UIH_DEBUG) {
$resp->dbg_youtube_info = $result->getYouTubeVideoInfo();
$resp->dbg_youtube_query = $result->getYouTubeUrlQuery();
}
}
if($result instanceof NicoNicoLookupResult) {
$resp->nicovideo_video_id = $result->getNicoNicoVideoId();
if($result->hasNicoNicoVideoStartTime())
$resp->nicovideo_start_time = $result->getNicoNicoVideoStartTime();
if(UIH_DEBUG) {
$resp->dbg_nicovideo_thumb_info = $result->getNicoNicoThumbInfo()->ownerDocument->saveXML();
$resp->dbg_nicovideo_query = $result->getNicoNicoUrlQuery();
}
}
if($result instanceof IHasMediaInfo) {
if($result->isMedia()) {
$resp->is_image = $result->isImage();
$resp->is_audio = $result->isAudio();
$resp->is_video = $result->isVideo();
if($result->hasDimensions()) {
$resp->width = $result->getWidth();
$resp->height = $result->getHeight();
}
$resp->media = new stdClass;
$resp->media->confidence = $result->getConfidence();
if($result->hasAspectRatio())
$resp->media->aspect_ratio = $result->getAspectRatio();
if($result->hasDuration())
$resp->media->duration = $result->getDuration();
if($result->hasSize())
$resp->media->size = $result->getSize();
if($result->hasBitRate())
$resp->media->bitrate = $result->getBitRate();
if($result->hasAudioTags()) {
$audioTags = $result->getAudioTags();
$resp->media->tags = new stdClass;
if($audioTags->hasTitle())
$resp->media->tags->title = $audioTags->getTitle();
if($audioTags->hasArtist())
$resp->media->tags->artist = $audioTags->getArtist();
if($audioTags->hasAlbum())
$resp->media->tags->album = $audioTags->getAlbum();
if($audioTags->hasDate())
$resp->media->tags->date = $audioTags->getDate();
if($audioTags->hasComment())
$resp->media->tags->comment = $audioTags->getComment();
if($audioTags->hasGenre())
$resp->media->tags->genre = $audioTags->getGenre();
}
}
if($result instanceof EEPROMLookupResult) {
$resp->eeprom_file_id = $result->getEEPROMId();
$resp->eeprom_file_info = $result->getEEPROMInfo();
}
if(UIH_DEBUG && $result->hasMediaInfo())
$resp->dbg_media_info = $result->getMediaInfo();
}
} catch(Exception $ex) {
$resp->status = 500;
$resp->error = 'metadata:lookup';
if(UIH_DEBUG) {
$resp->dbg_msg = $ex->getMessage();
$resp->dbg_ex = (string)$ex;
}
return $resp;
}
$sw->stop();
$resp->took = $sw->getElapsedTime() / 1000;
$respJson = json_encode($resp);
$this->cache->set($cacheKey, $respJson, 10 * 60);
}
if(!empty($respJson))
return $respJson;
return $resp;
}
private function handleMetadata($response, $request, string $targetUrl) {
$result = $this->metadataLookup($targetUrl);
if(is_int($result))
return $result;
if(is_object($result)) {
if(!empty($result->status))
$response->setStatusCode($result->status);
} elseif(is_string($result)) {
$response->setTypeJson();
}
return $result;
}
private function handleMetadataBatch($response, $request, array $urls) {
$sw = Stopwatch::startNew();
if(count($urls) > 20)
return 400;
$handled = [];
$results = [];
foreach($urls as $url) {
if(!is_string($url))
continue;
$cleanUrl = trim($url, '/?&# ');
if(in_array($cleanUrl, $handled))
continue;
$handled[] = $cleanUrl;
$result = $this->metadataLookup($url);
if(is_int($result)) {
$status = $result;
$result = new stdClass;
$result->status = $status;
$result->error = 'batch:status';
} elseif(is_string($result)) {
$result = json_decode($result);
}
$results[] = [
'url' => $url,
'info' => $result,
];
}
$sw->stop();
return [
'took' => $sw->getElapsedTime() / 1000,
'results' => $results,
];
}
}

View file

@ -1,66 +0,0 @@
<?php
namespace Uiharu;
class AudioTags {
public function __construct(
private string $title = '',
private string $artist = '',
private string $album = '',
private string $date = '',
private string $comment = '',
private string $genre = ''
) {}
public function hasTitle(): bool {
return $this->title !== '';
}
public function getTitle(): string {
return $this->title;
}
public function hasArtist(): bool {
return $this->artist !== '';
}
public function getArtist(): string {
return $this->artist;
}
public function hasAlbum(): bool {
return $this->album !== '';
}
public function getAlbum(): string {
return $this->album;
}
public function hasDate(): bool {
return $this->date !== '';
}
public function getDate(): string {
return $this->date;
}
public function hasComment(): bool {
return $this->comment !== '';
}
public function getComment(): string {
return $this->comment;
}
public function hasGenre(): bool {
return $this->genre !== '';
}
public function getGenre(): string {
return $this->genre;
}
public static function fromMediaInfo(object $obj): AudioTags {
return new AudioTags(
$obj->tagTitle ?? '',
$obj->tagArtist ?? '',
$obj->tagAlbum ?? '',
$obj->tagDate ?? '',
$obj->tagComment ?? '',
$obj->tagGenre ?? '',
);
}
}

View file

@ -1,175 +0,0 @@
<?php
namespace Uiharu;
use stdClass;
use Imagick;
use RuntimeException;
final class FFMPEG {
public static function grabFirstVideoFrame(string $url): string {
try {
$file = popen(sprintf('ffmpeg -i %s -f image2pipe -c:v png -frames:v 1 2>/dev/null -', escapeshellarg($url)), 'rb');
return stream_get_contents($file);
} finally {
if(isset($file) && is_resource($file))
pclose($file);
}
}
public static function grabFirstAudioCover(string $url): string {
try {
$file = popen(sprintf('ffmpeg -i %s -an -f image2pipe -c:v copy -frames:v 1 2>/dev/null -', escapeshellarg($url)), 'rb');
return stream_get_contents($file);
} finally {
if(isset($file) && is_resource($file))
pclose($file);
}
}
public static function probe(string $url): ?object {
$command = sprintf(
'ffprobe -show_streams -show_format -print_format json -v quiet -i %s',
escapeshellarg($url)
);
$ffprobe = proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
if(!is_resource($ffprobe))
throw new RuntimeException('Could not open ffprobe.');
try {
$stderr = trim(stream_get_contents($pipes[2]));
if(!empty($stderr))
throw new RuntimeException('ffprobe: ' . $stderr);
$stdout = trim(stream_get_contents($pipes[1]));
if(empty($stdout))
throw new RuntimeException('ffprobe did not report any errors but exited without any output');
} finally {
proc_close($ffprobe);
}
return json_decode($stdout);
}
public static function cleanProbe(string $url): ?object {
return self::cleanProbeResult(self::probe($url));
}
public static function cleanProbeResult(?object $in): object {
$out = new stdClass;
if(!empty($in->format)) {
$out->confidence = empty($in->format->probe_score) ? 0 : (intval($in->format->probe_score) / 100);
if(!empty($in->format->duration))
$out->duration = floatval($in->format->duration);
if(!empty($in->format->size))
$out->size = intval($in->format->size);
if(!empty($in->format->bit_rate))
$out->bitRate = intval($in->format->bit_rate);
if(!empty($in->format->tags)) {
if(!empty($in->format->tags->title)) {
$out->tagTitle = $in->format->tags->title;
} elseif(!empty($in->format->tags->TITLE)) {
$out->tagTitle = $in->format->tags->TITLE;
} elseif(!empty($in->format->tags->Title)) {
$out->tagTitle = $in->format->tags->Title;
}
if(!empty($in->format->tags->artist)) {
$out->tagArtist = $in->format->tags->artist;
} elseif(!empty($in->format->tags->ARTIST)) {
$out->tagArtist = $in->format->tags->ARTIST;
} elseif(!empty($in->format->tags->Artist)) {
$out->tagArtist = $in->format->tags->Artist;
}
if(!empty($in->format->tags->album)) {
$out->tagAlbum = $in->format->tags->album;
} elseif(!empty($in->format->tags->ALBUM)) {
$out->tagAlbum = $in->format->tags->ALBUM;
} elseif(!empty($in->format->tags->Album)) {
$out->tagAlbum = $in->format->tags->Album;
}
if(!empty($in->format->tags->date)) {
$out->tagDate = $in->format->tags->date;
} elseif(!empty($in->format->tags->DATE)) {
$out->tagDate = $in->format->tags->DATE;
} elseif(!empty($in->format->tags->Date)) {
$out->tagDate = $in->format->tags->Date;
}
if(!empty($in->format->tags->comment)) {
$out->tagComment = $in->format->tags->comment;
} elseif(!empty($in->format->tags->COMMENT)) {
$out->tagComment = $in->format->tags->COMMENT;
} elseif(!empty($in->format->tags->Comment)) {
$out->tagComment = $in->format->tags->Comment;
}
if(!empty($in->format->tags->genre)) {
$out->tagGenre = $in->format->tags->genre;
} elseif(!empty($in->format->tags->GENRE)) {
$out->tagGenre = $in->format->tags->GENRE;
} elseif(!empty($in->format->tags->Genre)) {
$out->tagGenre = $in->format->tags->Genre;
}
}
}
if(!empty($in->streams))
foreach($in->streams as $stream) {
$codecType = $stream->codec_type ?? null;
if($codecType === 'video') {
$out->width = intval($stream->coded_width ?? $stream->width ?? -1);
$out->height = intval($stream->coded_height ?? $stream->height ?? -1);
if(!empty($stream->display_aspect_ratio))
$out->aspectRatio = $stream->display_aspect_ratio;
} elseif($codecType === 'audio') {
if(!empty($stream->tags->title)) {
$out->tagTitle = $stream->tags->title;
} elseif(!empty($stream->tags->TITLE)) {
$out->tagTitle = $stream->tags->TITLE;
}
if(!empty($stream->tags->artist)) {
$out->tagArtist = $stream->tags->artist;
} elseif(!empty($stream->tags->ARTIST)) {
$out->tagArtist = $stream->tags->ARTIST;
}
if(!empty($stream->tags->album)) {
$out->tagAlbum = $stream->tags->album;
} elseif(!empty($stream->tags->ALBUM)) {
$out->tagAlbum = $stream->tags->ALBUM;
}
if(!empty($stream->tags->date)) {
$out->tagDate = $stream->tags->date;
} elseif(!empty($stream->tags->DATE)) {
$out->tagDate = $stream->tags->DATE;
}
if(!empty($stream->tags->comment)) {
$out->tagComment = $stream->tags->comment;
} elseif(!empty($stream->tags->COMMENT)) {
$out->tagComment = $stream->tags->COMMENT;
}
if(!empty($stream->tags->genre)) {
$out->tagGenre = $stream->tags->genre;
} elseif(!empty($stream->tags->GENRE)) {
$out->tagGenre = $stream->tags->GENRE;
}
}
}
return $out;
}
}

View file

@ -1,8 +0,0 @@
<?php
namespace Uiharu;
use Index\Http\Routing\RouteHandler;
interface IApi extends RouteHandler {
function match(string $url): string;
}

View file

@ -1,33 +0,0 @@
<?php
namespace Uiharu;
interface IHasMediaInfo extends ILookupResult {
function getConfidence(): float;
function isMedia(): bool;
function isImage(): bool;
function isVideo(): bool;
function isAudio(): bool;
function hasDimensions(): bool;
function getWidth(): int;
function getHeight(): int;
function hasAspectRatio(): bool;
function getAspectRatio(): string;
function hasDuration(): bool;
function getDuration(): float;
function hasSize(): bool;
function getSize(): int;
function hasBitRate(): bool;
function getBitRate(): int;
function hasAudioTags(): bool;
function getAudioTags(): AudioTags;
function hasMediaInfo(): bool;
function getMediaInfo(): object;
}

View file

@ -1,7 +0,0 @@
<?php
namespace Uiharu;
interface ILookup {
function match(Url $url): bool;
function lookup(Url $url): ILookupResult;
}

View file

@ -1,28 +0,0 @@
<?php
namespace Uiharu;
use Index\MediaType;
use Index\Colour\Colour;
interface ILookupResult {
function getUrl(): Url;
function getObjectType(): string;
function hasMediaType(): bool;
function getMediaType(): MediaType;
function hasColour(): bool;
function getColour(): Colour;
function hasTitle(): bool;
function getTitle(): string;
function hasSiteName(): bool;
function getSiteName(): string;
function hasDescription(): bool;
function getDescription(): string;
function hasPreviewImage(): bool;
function getPreviewImage(): string;
}

View file

@ -1,78 +0,0 @@
<?php
namespace Uiharu\Lookup;
use RuntimeException;
use Uiharu\FFMPEG;
use Uiharu\MediaTypeExts;
use Uiharu\Url;
use Index\MediaType;
final class EEPROMLookup implements \Uiharu\ILookup {
public function __construct(
private string $protocol,
private string $apiDomain,
private array $shortDomains
) {}
public function match(Url $url): bool {
$urlHost = strtolower($url->getHost());
return $url->getScheme() === $this->protocol || (
$url->isWeb() && (
in_array($urlHost, $this->shortDomains) || (
$urlHost === $this->apiDomain && str_starts_with($url->getPath(), '/uploads')
)
)
);
}
private function rawLookup(string $fileId): ?object {
$curl = curl_init("https://{$this->apiDomain}/uploads/{$fileId}.json");
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): EEPROMLookupResult {
if($url->getScheme() === $this->protocol) {
$fileId = $url->getPath();
} elseif($url->isWeb()) {
if($url->getHost() === $this->apiDomain) {
if(preg_match('#^/uploads/([A-Za-z0-9-_]+)/?$#', $url->getPath(), $matches))
$fileId = $matches[1];
} else {
$fileId = substr($url->getPath(), 1);
}
}
if(!isset($fileId))
throw new RuntimeException('Was unable to find EEPROM file id.');
if(!preg_match('#^([A-Za-z0-9-_]+)$#', $fileId))
throw new RuntimeException('Invalid EEPROM file id format.');
$fileInfo = $this->rawLookup($fileId);
if($fileInfo === null)
throw new RuntimeException('EEPROM file does not exist: ' . $fileId);
$url = Url::parse('https://' . $this->shortDomains[0] . '/' . $fileId);
$mediaType = MediaType::parse($fileInfo->type);
$isMedia = MediaTypeExts::isMedia($mediaType);
$mediaInfo = $isMedia ? FFMPEG::cleanProbe($url) : null;
return new EEPROMLookupResult($url, $mediaType, $fileInfo, $mediaInfo);
}
}

View file

@ -1,173 +0,0 @@
<?php
namespace Uiharu\Lookup;
use Uiharu\AudioTags;
use Uiharu\Url;
use Index\MediaType;
use Index\Colour\{Colour,ColourRgb};
final class EEPROMLookupResult implements \Uiharu\ILookupResult, \Uiharu\IHasMediaInfo {
private Url $url;
private MediaType $type;
private object $fileInfo;
private ?object $mediaInfo;
private ?AudioTags $audioTags;
public function __construct(
Url $url,
MediaType $type,
object $fileInfo,
?object $mediaInfo
) {
$this->url = $url;
$this->type = $type;
$this->fileInfo = $fileInfo;
$this->mediaInfo = $mediaInfo;
if($this->isAudio() && $mediaInfo !== null)
$this->audioTags = AudioTags::fromMediaInfo($mediaInfo);
else
$this->audioTags = null;
}
public function getUrl(): Url {
return $this->url;
}
public function getObjectType(): string {
return 'eeprom:file';
}
public function hasMediaType(): bool {
return true;
}
public function getMediaType(): MediaType {
return $this->type;
}
public function getEEPROMId(): string {
return $this->fileInfo->id;
}
public function getEEPROMInfo(): object {
return $this->fileInfo;
}
public function hasColour(): bool {
return true;
}
public function getColour(): Colour {
return ColourRgb::fromRawRgb(0x8559A5);
}
public function hasTitle(): bool {
return true;
}
public function getTitle(): string {
if($this->audioTags !== null) {
$title = '';
if($this->audioTags->hasArtist())
$title .= $this->audioTags->getArtist() . ' - ';
if($this->audioTags->hasTitle())
$title .= $this->audioTags->getTitle();
if($this->audioTags->hasDate())
$title .= ' (' . $this->audioTags->getDate() . ')';
if(!empty($title))
return $title;
}
return $this->fileInfo->name;
}
public function hasSiteName(): bool {
return true;
}
public function getSiteName(): string {
return 'EEPROM';
}
public function hasDescription(): bool {
return $this->audioTags !== null && $this->audioTags->hasComment();
}
public function getDescription(): string {
return $this->audioTags->getComment();
}
public function hasPreviewImage(): bool {
return true;
}
public function getPreviewImage(): string {
return $this->fileInfo->thumb;
}
public function getConfidence(): float {
return $this->mediaInfo->confidence ?? 0;
}
public function isMedia(): bool {
return $this->mediaInfo !== null;
}
public function isImage(): bool {
return $this->type->matchCategory('image');
}
public function isVideo(): bool {
return $this->type->matchCategory('video');
}
public function isAudio(): bool {
return $this->type->matchCategory('audio');
}
public function hasDimensions(): bool {
return $this->isImage() || $this->isVideo();
}
public function getWidth(): int {
return $this->mediaInfo->width ?? 0;
}
public function getHeight(): int {
return $this->mediaInfo->height ?? 0;
}
public function hasAspectRatio(): bool {
return isset($this->mediaInfo->aspectRatio);
}
public function getAspectRatio(): string {
return $this->mediaInfo->aspectRatio;
}
public function hasDuration(): bool {
return isset($this->mediaInfo->duration);
}
public function getDuration(): float {
return $this->mediaInfo->duration;
}
public function hasSize(): bool {
return isset($this->mediaInfo->size);
}
public function getSize(): int {
return $this->mediaInfo->size;
}
public function hasBitRate(): bool {
return isset($this->mediaInfo->bitRate);
}
public function getBitRate(): int {
return $this->mediaInfo->bitRate;
}
public function hasAudioTags(): bool {
return $this->audioTags !== null;
}
public function getAudioTags(): AudioTags {
return $this->audioTags;
}
public function hasMediaInfo(): bool {
return $this->mediaInfo !== null;
}
public function getMediaInfo(): object {
return $this->mediaInfo;
}
}

View file

@ -1,92 +0,0 @@
<?php
namespace Uiharu\Lookup;
use DOMDocument;
use RuntimeException;
use Uiharu\Url;
final class NicoNicoLookup implements \Uiharu\ILookup {
private const SHORT_DOMAINS = [
'nico.ms',
'www.nico.ms',
];
private const LONG_DOMAINS = [
'www.nicovideo.jp',
'nicovideo.jp',
];
public static function isShortDomain(string $host): bool {
return in_array($host, self::SHORT_DOMAINS);
}
public static function isLongDomain(string $host): bool {
return in_array($host, self::LONG_DOMAINS);
}
public function match(Url $url): bool {
if(!$url->isWeb())
return false;
$urlHost = strtolower($url->getHost());
if(self::isShortDomain($urlHost))
return true;
if(self::isLongDomain($urlHost) && str_starts_with($url->getPath(), '/watch/sm'))
return true;
return false;
}
public function lookup(Url $url): NicoNicoLookupResult {
if(self::isShortDomain($url->getHost()))
$videoId = explode('/', trim($url->getPath(), '/'))[0] ?? '';
else
$videoId = explode('/', trim($url->getPath(), '/'))[1] ?? '';
if(empty($videoId))
throw new RuntimeException('Nico Nico Douga video id missing.');
$thumbDoc = self::lookupThumbInfo($videoId);
$thumbResp = $thumbDoc->getElementsByTagName('nicovideo_thumb_response')[0] ?? null;
if(empty($thumbResp) || !$thumbResp->hasAttribute('status') || $thumbResp->getAttribute('status') !== 'ok')
throw new RuntimeException('Nico Nico Douga video with the given id could not be found.');
$thumbInfo = $thumbResp->getElementsByTagName('thumb')[0] ?? null;
if(empty($thumbInfo))
throw new RuntimeException('Nico Nico Douga thumb info missing from API result????');
parse_str($url->getQuery(), $urlQuery);
return new NicoNicoLookupResult($url, $videoId, $thumbInfo, $urlQuery);
}
private static function lookupThumbInfo(string $videoId): DOMDocument {
$curl = curl_init("https://ext.nicovideo.jp/api/getthumbinfo/{$videoId}");
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 => 3,
CURLOPT_USERAGENT => 'Uiharu/' . UIH_VERSION,
]);
$resp = curl_exec($curl);
curl_close($curl);
if(empty($resp))
throw new RuntimeException('Nico Nico Douga API request failed.');
$doc = new DOMDocument;
if(!$doc->loadXML($resp))
throw new RuntimeException('Failed to parse Nico Nico Douga API response.');
return $doc;
}
}

View file

@ -1,92 +0,0 @@
<?php
namespace Uiharu\Lookup;
use DOMElement;
use Index\MediaType;
use Uiharu\Url;
use Index\Colour\{Colour,ColourRgb};
final class NicoNicoLookupResult implements \Uiharu\ILookupResult {
private DOMElement|false|null $title = null;
private DOMElement|false|null $description = null;
private DOMElement|false|null $previewImage = null;
public function __construct(
private Url $url,
private string $videoId,
private DOMElement $thumbInfo,
private array $urlQuery,
) {}
public function getUrl(): Url {
return $this->url;
}
public function getObjectType(): string {
return 'niconico:video';
}
public function getNicoNicoVideoId(): string {
return $this->videoId;
}
public function getNicoNicoThumbInfo(): DOMElement {
return $this->thumbInfo;
}
public function getNicoNicoUrlQuery(): array {
return $this->urlQuery;
}
public function hasNicoNicoVideoStartTime(): bool {
return isset($this->urlQuery['from']);
}
public function getNicoNicoVideoStartTime(): string {
return $this->urlQuery['from'] ?? '';
}
public function hasMediaType(): bool {
return false;
}
public function getMediaType(): MediaType {
throw new RuntimeException('Unsupported');
}
public function hasColour(): bool {
return true;
}
public function getColour(): Colour {
return ColourRgb::fromRawRgb(0x252525);
}
public function hasTitle(): bool {
if($this->title === null)
$this->title = $this->thumbInfo->getElementsByTagName('title')[0] ?? false;
return $this->title !== false;
}
public function getTitle(): string {
return $this->hasTitle() ? trim($this->title->textContent) : 'No Title';
}
public function hasSiteName(): bool {
return true;
}
public function getSiteName(): string {
return 'ニコニコ動画';
}
public function hasDescription(): bool {
if($this->description === null)
$this->description = $this->thumbInfo->getElementsByTagName('description')[0] ?? false;
return $this->description !== false && !empty($this->description->textContent);
}
public function getDescription(): string {
return $this->hasDescription() ? trim($this->description->textContent) : '';
}
public function hasPreviewImage(): bool {
if($this->previewImage === null)
$this->previewImage = $this->thumbInfo->getElementsByTagName('thumbnail_url')[0] ?? false;
return $this->previewImage !== false && !empty($this->previewImage->textContent);
}
public function getPreviewImage(): string {
return $this->hasPreviewImage() ? (trim($this->previewImage->textContent) . '.L') : '';
}
}

View file

@ -1,177 +0,0 @@
<?php
namespace Uiharu\Lookup;
use finfo;
use stdClass;
use DOMDocument;
use RuntimeException;
use Uiharu\Config;
use Uiharu\FFMPEG;
use Uiharu\MediaTypeExts;
use Uiharu\Url;
use Index\MediaType;
// TODO: Content-Disposition should be honoured for the filename (title).
final class WebLookup implements \Uiharu\ILookup {
public function match(Url $url): bool {
return $url->isWeb();
}
private static function reqCreate(string $url) {
$curl = curl_init($url);
curl_setopt_array($curl, [
CURLOPT_AUTOREFERER => true,
CURLOPT_CERTINFO => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_PATH_AS_IS => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 5,
CURLOPT_DEFAULT_PROTOCOL => 'https',
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; FlashiiBot/2.5; Uiharu/' . UIH_VERSION . '; +http://fii.moe/uiharu)',
CURLOPT_HTTPHEADER => [
'Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8',
'Accept-Language: en-GB, en;q=0.9, ja-jp;q=0.6, *;q=0.5',
],
]);
return $curl;
}
private static function reqHead($curl): array|null {
curl_setopt_array($curl, [
CURLOPT_NOBODY => true,
CURLOPT_HEADER => true,
]);
$headers = curl_exec($curl);
if($headers === false)
return null;
$headers = explode("\r\n", trim($headers));
$status = 200;
$lines = [];
foreach($headers as $header) {
if(empty($header))
continue;
if(strpos($header, ':') === false) {
$headParts = explode(' ', $header);
if(isset($headParts[1]) && is_numeric($headParts[1]))
$status = (int)$headParts[1];
$lines = [];
continue;
}
$parts = explode(':', $header, 2);
$parts[0] = mb_strtolower($parts[0]);
if(isset($lines[$parts[0]]))
$lines[$parts[0]] .= ', ' . trim($parts[1] ?? '');
else
$lines[$parts[0]] = trim($parts[1] ?? '');
}
return compact('status', 'lines');
}
private static function reqBody($curl): string|false {
curl_setopt_array($curl, [
CURLOPT_NOBODY => false,
CURLOPT_HEADER => false,
]);
return curl_exec($curl);
}
private static function reqError($curl): string {
return curl_error($curl);
}
private static function reqClose($curl): void {
curl_close($curl);
}
public function lookup(Url $url): WebLookupResult {
$req = self::reqCreate($url);
$head = self::reqHead($req);
if($head === null)
throw new RuntimeException('Web request timed out: ' . self::reqError($req));
$mediaType = MediaType::parse('application/octet-stream');
$hasContentType = array_key_exists('content-type', $head['lines']);
if($hasContentType) {
try {
$mediaType = MediaType::parse($head['lines']['content-type'] ?? '');
} catch(InvalidArgumentException $ex) {}
if(MediaTypeExts::isMedia($mediaType)) {
self::reqClose($req);
return $this->lookupMedia($url, $mediaType);
}
}
$body = self::reqBody($req);
self::reqClose($req);
if(!$hasContentType)
try {
$finfo = new finfo(FILEINFO_MIME);
$mediaType = MediaType::parse($finfo->buffer($body));
} catch(InvalidArgumentException $ex) {}
if($mediaType->equals('text/html')
|| $mediaType->equals('application/xhtml+xml')
|| $mediaType->equals('application/xml'))
return $this->lookupSite($url, $req, $mediaType, $body);
return new WebLookupFallbackResult($url, $mediaType, $url->getHost() . ': ' . basename($url->getPath()));
}
private function lookupSite(Url $url, $req, MediaType $mediaType, string $body): WebLookupResult {
// ok hear me out
// there's absolutely no good html scraping libraries for PHP
// DOMDocument Exists but kinda blows at catching weird encoding events like with pixiv
// and i'm not about to rewrite this whole fucking thing in nodejs
// also at this point Index should probably provide a wrapper for proc_open lol
$extract = proc_open(
sprintf('node %s/extract.mjs', UIH_ROOT),
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes
);
if(!is_resource($extract))
throw new RuntimeException('Could not open extract.');
try {
fwrite($pipes[0], $body);
fclose($pipes[0]);
$stderr = trim(stream_get_contents($pipes[2]));
if(!empty($stderr))
throw new RuntimeException('extract: ' . $stderr);
$stdout = trim(stream_get_contents($pipes[1]));
if(empty($stdout))
throw new RuntimeException('extract did not report any errors but exited without any output');
} finally {
proc_close($extract);
}
$siteInfo = json_decode($stdout);
if(empty($siteInfo))
throw new RuntimeException('Failed to parse extract output.');
return new WebLookupSiteResult($url, $mediaType, $siteInfo);
}
private function lookupMedia(Url $url, MediaType $mediaType): WebLookupResult {
$mediaInfo = FFMPEG::cleanProbe($url);
return new WebLookupMediaResult($url, $mediaType, $mediaInfo);
}
}

View file

@ -1,55 +0,0 @@
<?php
namespace Uiharu\Lookup;
use RuntimeException;
use Uiharu\Url;
use Index\MediaType;
use Index\Colour\Colour;
class WebLookupFallbackResult extends WebLookupResult {
private string $title;
public function __construct(Url $url, MediaType $mediaType, string $title) {
parent::__construct($url, $mediaType);
$this->title = $title;
}
public function getObjectType(): string {
return 'unknown';
}
public function hasColour(): bool {
return false;
}
public function getColour(): Colour {
throw new RuntimeException('Unsupported.');
}
public function hasTitle(): bool {
return true;
}
public function getTitle(): string {
return $this->title;
}
public function hasSiteName(): bool {
return false;
}
public function getSiteName(): string {
throw new RuntimeException('Unsupported.');
}
public function hasDescription(): bool {
return false;
}
public function getDescription(): string {
throw new RuntimeException('Unsupported.');
}
public function hasPreviewImage(): bool {
return false;
}
public function getPreviewImage(): string {
throw new RuntimeException('Unsupported.');
}
}

View file

@ -1,152 +0,0 @@
<?php
namespace Uiharu\Lookup;
use Uiharu\AudioTags;
use Uiharu\Url;
use Index\MediaType;
use Index\Colour\Colour;
class WebLookupMediaResult extends WebLookupResult implements \Uiharu\IHasMediaInfo {
private object $mediaInfo;
private ?AudioTags $audioTags;
public function __construct(Url $url, MediaType $mediaType, object $mediaInfo) {
parent::__construct($url, $mediaType);
$this->mediaInfo = $mediaInfo;
if($this->isAudio() && $mediaInfo !== null)
$this->audioTags = AudioTags::fromMediaInfo($mediaInfo);
else
$this->audioTags = null;
}
public function getObjectType(): string {
return 'media';
}
public function hasColour(): bool {
return false;
}
public function getColour(): Colour {
throw new RuntimeException('Unsupported');
}
public function hasTitle(): bool {
return true;
}
public function getTitle(): string {
if($this->audioTags !== null) {
$title = '';
if($this->audioTags->hasArtist())
$title .= $this->audioTags->getArtist() . ' - ';
if($this->audioTags->hasTitle())
$title .= $this->audioTags->getTitle();
if($this->audioTags->hasDate())
$title .= ' (' . $this->audioTags->getDate() . ')';
if(!empty($title))
return $title;
}
return rawurldecode(basename($this->url->getPath()));
}
public function hasSiteName(): bool {
return true;
}
public function getSiteName(): string {
return $this->url->getHost();
}
public function hasDescription(): bool {
return $this->audioTags !== null && $this->audioTags->hasComment();
}
public function getDescription(): string {
return $this->audioTags->getComment();
}
public function hasPreviewImage(): bool {
return $this->isImage() || $this->isVideo() || $this->isAudio();
}
public function getPreviewImage(): string {
$url = (string)$this->url;
if($this->isVideo())
return '//' . $_SERVER['HTTP_HOST'] . '/metadata/thumb/video?url=' . rawurlencode($url);
if($this->isAudio())
return '//' . $_SERVER['HTTP_HOST'] . '/metadata/thumb/audio?url=' . rawurlencode($url);
return $url;
}
public function getConfidence(): float {
return $this->mediaInfo->confidence;
}
public function isMedia(): bool {
return true;
}
public function isImage(): bool {
return $this->mediaType->matchCategory('image');
}
public function isVideo(): bool {
return $this->mediaType->matchCategory('video');
}
public function isAudio(): bool {
return $this->mediaType->matchCategory('audio');
}
public function hasDimensions(): bool {
return $this->isImage() || $this->isVideo();
}
public function getWidth(): int {
return $this->mediaInfo->width ?? 0;
}
public function getHeight(): int {
return $this->mediaInfo->height ?? 0;
}
public function hasAspectRatio(): bool {
return isset($this->mediaInfo->aspectRatio);
}
public function getAspectRatio(): string {
return $this->mediaInfo->aspectRatio;
}
public function hasDuration(): bool {
return isset($this->mediaInfo->duration);
}
public function getDuration(): float {
return $this->mediaInfo->duration;
}
public function hasSize(): bool {
return isset($this->mediaInfo->size);
}
public function getSize(): int {
return $this->mediaInfo->size;
}
public function hasBitRate(): bool {
return isset($this->mediaInfo->bitRate);
}
public function getBitRate(): int {
return $this->mediaInfo->bitRate;
}
public function hasAudioTags(): bool {
return $this->audioTags !== null;
}
public function getAudioTags(): AudioTags {
return $this->audioTags;
}
public function hasMediaInfo(): bool {
return true;
}
public function getMediaInfo(): object {
return $this->mediaInfo;
}
}

View file

@ -1,44 +0,0 @@
<?php
namespace Uiharu\Lookup;
use Uiharu\Url;
use Index\MediaType;
use Index\Colour\Colour;
abstract class WebLookupResult implements \Uiharu\ILookupResult {
protected Url $url;
protected MediaType $mediaType;
public function __construct(Url $url, MediaType $mediaType) {
$this->url = $url;
$this->mediaType = $mediaType;
}
public function getUrl(): Url {
return $this->url;
}
public abstract function getObjectType(): string;
public function hasMediaType(): bool {
return true;
}
public function getMediaType(): MediaType {
return $this->mediaType;
}
public abstract function hasColour(): bool;
public abstract function getColour(): Colour;
public abstract function hasTitle(): bool;
public abstract function getTitle(): string;
public abstract function hasSiteName(): bool;
public abstract function getSiteName(): string;
public abstract function hasDescription(): bool;
public abstract function getDescription(): string;
public abstract function hasPreviewImage(): bool;
public abstract function getPreviewImage(): string;
}

View file

@ -1,62 +0,0 @@
<?php
namespace Uiharu\Lookup;
use Uiharu\Url;
use Index\MediaType;
use Index\Colour\Colour;
class WebLookupSiteResult extends WebLookupResult {
private object $siteInfo;
public function __construct(Url $url, MediaType $mediaType, object $siteInfo) {
parent::__construct($url, $mediaType);
$this->siteInfo = $siteInfo;
}
public function getObjectType(): string {
return $this->siteInfo->type;
}
public function hasColour(): bool {
return !empty($this->siteInfo->colour);
}
public function getColour(): Colour {
return Colour::parse($this->siteInfo->colour);
}
public function hasTitle(): bool {
return true;
}
public function getTitle(): string {
if(!empty($this->siteInfo->metaTitle))
return $this->siteInfo->metaTitle;
if(!empty($this->siteInfo->title))
return $this->siteInfo->title;
return $this->siteInfo->siteName;
}
public function hasSiteName(): bool {
return !empty($this->siteInfo->siteName);
}
public function getSiteName(): string {
return $this->siteInfo->siteName;
}
public function hasDescription(): bool {
return !empty($this->siteInfo->desc);
}
public function getDescription(): string {
return $this->siteInfo->desc;
}
public function hasPreviewImage(): bool {
return !empty($this->siteInfo->image);
}
public function getPreviewImage(): string {
return $this->siteInfo->image;
}
public function getWebSiteInfo(): object {
return $this->siteInfo;
}
}

View file

@ -1,133 +0,0 @@
<?php
namespace Uiharu\Lookup;
use RuntimeException;
use Uiharu\Url;
use Index\Config\Config;
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 WATCH_PREFIXES = [
'/watch/',
'/live/',
'/shorts/',
];
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 Config $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();
if($urlPath === '/watch')
return true;
foreach(self::WATCH_PREFIXES as $prefix)
if(str_starts_with($urlPath, $prefix))
return true;
return false;
}
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);
} elseif(array_key_exists('v', $urlQuery)) {
$videoId = $urlQuery['v'];
} else {
$urlPathParts = explode('/', trim($urlPath, '/'));
if(count($urlPathParts) > 1)
$videoId = $urlPathParts[1];
}
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);
}
}

View file

@ -1,96 +0,0 @@
<?php
namespace Uiharu\Lookup;
use RuntimeException;
use Uiharu\Url;
use Index\MediaType;
use Index\Colour\{Colour,ColourRgb};
final class YouTubeLookupResult implements \Uiharu\ILookupResult {
public function __construct(
private Url $url,
private string $videoId,
private object $videoInfo,
private array $urlQuery
) {}
public function getUrl(): Url {
return $this->url;
}
public function getObjectType(): string {
return 'youtube:video';
}
public function getYouTubeVideoId(): string {
return $this->videoId;
}
public function getYouTubeVideoInfo(): object {
return $this->videoInfo;
}
public function getYouTubeUrlQuery(): array {
return $this->urlQuery;
}
public function hasYouTubeVideoStartTime(): bool {
return isset($this->urlQuery['t']);
}
public function getYouTubeVideoStartTime(): string {
return $this->urlQuery['t'] ?? '';
}
public function hasYouTubePlayListId(): bool {
return isset($this->urlQuery['list']);
}
public function getYouTubePlayListId(): string {
return $this->urlQuery['list'] ?? '';
}
public function hasYouTubePlayListIndex(): bool {
return isset($this->urlQuery['index']);
}
public function getYouTubePlayListIndex(): string {
return $this->urlQuery['index'] ?? '';
}
public function hasMediaType(): bool {
return false;
}
public function getMediaType(): MediaType {
throw new RuntimeException('Unsupported');
}
public function hasColour(): bool {
return true;
}
public function getColour(): Colour {
return ColourRgb::fromRawRgb(0xFF0000);
}
public function hasTitle(): bool {
return !empty($this->videoInfo->items[0]->snippet->title);
}
public function getTitle(): string {
return $this->videoInfo->items[0]->snippet->title;
}
public function hasSiteName(): bool {
return true;
}
public function getSiteName(): string {
return 'YouTube';
}
public function hasDescription(): bool {
return !empty($this->videoInfo->items[0]->snippet->description);
}
public function getDescription(): string {
return $this->videoInfo->items[0]->snippet->description;
}
public function hasPreviewImage(): bool {
return !empty($this->videoInfo->items[0]->snippet->thumbnails->medium->url);
}
public function getPreviewImage(): string {
return $this->videoInfo->items[0]->snippet->thumbnails->medium->url;
}
}

View file

@ -1,28 +0,0 @@
<?php
namespace Uiharu;
use Index\MediaType;
final class MediaTypeExts {
public static function toV1(MediaType $mediaType): array {
$parts = [
'string' => (string)$mediaType,
'type' => $mediaType->getCategory(),
'subtype' => $mediaType->getKind(),
];
if(!empty($suffix = $mediaType->getSuffix()))
$parts['suffix'] = $suffix;
if(!empty($params = $mediaType->getParams()))
$parts['params'] = $params;
return $parts;
}
public static function isMedia(MediaType $mediaType): bool {
return $mediaType->matchCategory('image')
|| $mediaType->matchCategory('audio')
|| $mediaType->matchCategory('video');
}
}

View file

@ -1,99 +0,0 @@
<?php
namespace Uiharu;
use Index\Cache\CacheProvider;
use Index\Config\Config;
use Index\Http\Routing\HttpRouter;
final class UihContext {
private CacheProvider $cache;
private Config $config;
private HttpRouter $router;
private array $apis = [];
private array $lookups = [];
public function __construct(CacheProvider $cache, Config $config) {
$this->cache = $cache;
$this->config = $config;
}
public function getCache(): CacheProvider {
return $this->cache;
}
public function getRouter(): HttpRouter {
return $this->router;
}
public function isOriginAllowed(string $origin): bool {
$origin = mb_strtolower(parse_url($origin, PHP_URL_HOST));
if($origin === $_SERVER['HTTP_HOST'])
return true;
$allowed = $this->config->getArray('cors:origins');
if(empty($allowed))
return true;
return in_array($origin, $allowed);
}
public function setupHttp(): void {
$this->router = new HttpRouter;
$this->router->use('/', function($response) {
$response->setPoweredBy('Uiharu');
});
$this->router->use('/', function($response, $request) {
$origin = $request->getHeaderLine('Origin');
if(!empty($origin)) {
if(!$this->isOriginAllowed($origin))
return 403;
$response->setHeader('Access-Control-Allow-Origin', $origin);
$response->setHeader('Vary', 'Origin');
}
});
$this->router->use('/', function($response, $request) {
if($request->getMethod() === 'OPTIONS') {
$response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET, POST');
return 204;
}
});
$this->router->get('/', function($response) {
$response->accelRedirect('/index.html');
$response->setContentType('text/html; charset=utf-8');
});
}
public function dispatchHttp(): void {
$this->router->dispatch();
}
public function registerApi(IApi $api): void {
$this->apis[] = $api;
}
public function matchApi(string $reqPath): void {
$reqPath = '/' . trim(parse_url($reqPath, PHP_URL_PATH), '/');
foreach($this->apis as $api)
if($api->match($reqPath)) {
$this->router->register($api);
break;
}
}
public function registerLookup(ILookup $lookup): void {
$this->lookups[] = $lookup;
}
public function matchLookup(Url $url): ?ILookup {
foreach($this->lookups as $lookup)
if($lookup->match($url))
return $lookup;
return null;
}
}

View file

@ -1,218 +0,0 @@
<?php
namespace Uiharu;
use InvalidArgumentException;
final class Url {
private string $scheme = '';
private string $host = '';
private int $port = 0;
private string $user = '';
private string $pass = '';
private string $path = '';
private string $query = '';
private string $fragment = '';
private ?string $formatted = null;
public function __construct(array $parts) {
if(isset($parts['scheme']))
$this->scheme = strtolower($parts['scheme']);
if(isset($parts['host']))
$this->host = $parts['host'];
if(isset($parts['port']))
$this->port = $parts['port'];
if(isset($parts['user']))
$this->user = $parts['user'];
if(isset($parts['pass']))
$this->pass = $parts['pass'];
if(isset($parts['path']))
$this->path = $parts['path'];
if(isset($parts['query']))
$this->query = $parts['query'];
if(isset($parts['fragment']))
$this->fragment = $parts['fragment'];
}
public static function parse(string $urlString): Url {
$parts = parse_url($urlString);
if($parts === false)
throw new InvalidArgumentException('Invalid URL provided.');
return new Url($parts);
}
public function resetString(): void {
$this->formatted = null;
}
public function setScheme(string $scheme): void {
$this->scheme = $scheme;
$this->resetString();
}
public function discardFragment(): void {
$this->fragment = '';
$this->resetString();
}
public function hasScheme(): bool {
return $this->scheme !== '';
}
public function hasHost(): bool {
return $this->host !== '';
}
public function hasPort(): bool {
return $this->port !== 0;
}
public function hasUser(): bool {
return $this->user !== '';
}
public function hasPassword(): bool {
return $this->pass !== '';
}
public function hasPath(): bool {
return $this->path !== '';
}
public function hasQuery(): bool {
return $this->query !== '';
}
public function hasFragment(): bool {
return $this->fragment !== '';
}
public function getScheme(): string {
return $this->scheme;
}
public function getHost(): string {
return $this->host;
}
public function getPort(): int {
return $this->port;
}
public function getUser(): string {
return $this->user;
}
public function getPassword(): string {
return $this->pass;
}
public function getPath(): string {
return $this->path;
}
public function getQuery(): string {
return $this->query;
}
public function getFragment(): string {
return $this->fragment;
}
public function hasUserInfo(): bool {
return $this->hasUser()
|| $this->hasPassword();
}
public function hasAuthority(): bool {
return $this->hasUserInfo()
|| $this->hasHost();
}
public function getUserInfo(): string {
$userInfo = $this->user;
if($this->pass !== '')
$userInfo .= ':' . $this->pass;
return $userInfo;
}
public function getAuthority(): string {
$authority = '//';
if($this->hasUserInfo())
$authority .= $this->getUserInfo() . '@';
$authority .= $this->host;
if($this->port !== 0)
$authority .= ':' . $this->port;
return $authority;
}
public function calculateHash(bool $raw = true): string {
return hash('sha256', (string)$this, $raw);
}
public function isHTTP(): bool {
return $this->scheme === 'http';
}
public function isHTTPS(): bool {
return $this->scheme === 'https';
}
public function isWeb(): bool {
return $this->scheme === 'https'
|| $this->scheme === 'http';
}
public function __toString(): string {
if($this->formatted === null) {
$string = '';
if($this->hasScheme())
$string .= $this->getScheme() . ':';
$hasAuthority = $this->hasAuthority();
if($hasAuthority)
$string .= $this->getAuthority();
$hasPath = $this->hasPath();
$path = $this->getPath();
if($hasAuthority && (!$hasPath || $path[0] !== '/'))
$string .= '/';
elseif(!$hasAuthority && $path[1] === '/')
$path = '/' . trim($path, '/');
$string .= $path;
// is all this necessary...?
if($this->hasQuery()) {
$string .= '?';
$parts = explode('&', $this->getQuery());
foreach($parts as $part) {
$param = explode('=', $part, 2);
$string .= rawurlencode($param[0]);
if(isset($param[1]))
$string .= '=' . rawurlencode($param[1]);
$string .= '&';
}
$string = substr($string, 0, -1);
}
if($this->hasFragment())
$string .= '#' . rawurlencode($this->getFragment());
$this->formatted = $string;
}
return $this->formatted;
}
public function toV1(): array {
$parts = ['uri' => (string)$this];
if($this->hasScheme())
$parts['scheme'] = $this->getScheme();
if($this->hasHost())
$parts['host'] = $this->getHost();
if($this->hasPort())
$parts['port'] = $this->getPort();
if($this->hasUser())
$parts['user'] = $this->getUser();
if($this->hasPassword())
$parts['pass'] = $parts['password'] = $this->getPassword();
if($this->hasPath())
$parts['path'] = $this->getPath();
if($this->hasQuery())
$parts['query'] = $this->getQuery();
if($this->hasFragment())
$parts['fragment'] = $this->getFragment();
return $parts;
}
}

View file

@ -1,37 +0,0 @@
<?php
namespace Uiharu;
use Index\Cache\CacheBackends;
use Index\Config\Fs\FsConfig;
define('UIH_STARTUP', microtime(true));
define('UIH_ROOT', __DIR__);
define('UIH_DEBUG', is_file(UIH_ROOT . '/.debug'));
define('UIH_PUBLIC', UIH_ROOT . '/public');
define('UIH_SOURCE', UIH_ROOT . '/src');
define('UIH_LIBRARY', UIH_ROOT . '/lib');
define('UIH_VERSION', '20241023');
require_once UIH_ROOT . '/vendor/autoload.php';
error_reporting(UIH_DEBUG ? -1 : 0);
mb_internal_encoding('UTF-8');
date_default_timezone_set('GMT');
$cfg = FsConfig::fromFile(UIH_ROOT . '/uiharu.cfg');
if($cfg->hasValues('sentry:dsn'))
(function($cfg) {
\Sentry\init([
'dsn' => $cfg->getString('dsn'),
'traces_sample_rate' => $cfg->getFloat('tracesRate', 0.2),
'profiles_sample_rate' => $cfg->getFloat('profilesRate', 0.2),
]);
set_exception_handler(function(\Throwable $ex) {
\Sentry\captureException($ex);
});
})($cfg->scopeTo('sentry'));
$cache = CacheBackends::create($cfg->getString('cache:dsn', 'array:'));
$ctx = new UihContext($cache, $cfg);