Rewrote web lookup handler + various fixes.
This commit is contained in:
parent
960a791394
commit
d90927469f
11 changed files with 1068 additions and 266 deletions
|
@ -13,6 +13,9 @@ if(UIH_DEBUG)
|
||||||
$ctx->registerLookup(new \Uiharu\Lookup\TwitterLookup);
|
$ctx->registerLookup(new \Uiharu\Lookup\TwitterLookup);
|
||||||
$ctx->registerLookup(new \Uiharu\Lookup\YouTubeLookup);
|
$ctx->registerLookup(new \Uiharu\Lookup\YouTubeLookup);
|
||||||
|
|
||||||
|
// this should always come AFTER other lookups involved http(s)
|
||||||
|
$ctx->registerLookup(new \Uiharu\Lookup\WebLookup);
|
||||||
|
|
||||||
$ctx->setupHttp();
|
$ctx->setupHttp();
|
||||||
|
|
||||||
$ctx->registerApi(new \Uiharu\Apis\v1_0($ctx));
|
$ctx->registerApi(new \Uiharu\Apis\v1_0($ctx));
|
||||||
|
|
|
@ -107,7 +107,7 @@ final class v1_0 implements \Uiharu\IApi {
|
||||||
if(empty($resp->type)) {
|
if(empty($resp->type)) {
|
||||||
$lookup = $this->ctx->matchLookup($parsedUrl);
|
$lookup = $this->ctx->matchLookup($parsedUrl);
|
||||||
|
|
||||||
if($lookup !== null) {
|
if($lookup !== null)
|
||||||
try {
|
try {
|
||||||
$result = $lookup->lookup($parsedUrl);
|
$result = $lookup->lookup($parsedUrl);
|
||||||
|
|
||||||
|
@ -176,6 +176,23 @@ final class v1_0 implements \Uiharu\IApi {
|
||||||
$resp->media->size = $result->getSize();
|
$resp->media->size = $result->getSize();
|
||||||
if($result->hasBitRate())
|
if($result->hasBitRate())
|
||||||
$resp->media->bitrate = $result->getBitRate();
|
$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) {
|
if($result instanceof EEPROMLookupResult) {
|
||||||
|
@ -195,255 +212,6 @@ final class v1_0 implements \Uiharu\IApi {
|
||||||
$response->setStatusCode(500);
|
$response->setStatusCode(500);
|
||||||
return $resp;
|
return $resp;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
$urlScheme = strtolower($parsedUrl->getScheme());
|
|
||||||
$urlHost = strtolower($parsedUrl->getHost());
|
|
||||||
$urlPath = '/' . trim($parsedUrl->getPath(), '/');
|
|
||||||
|
|
||||||
if($urlScheme !== 'http' && $urlScheme !== 'https') {
|
|
||||||
$resp->error = 'metadata:scheme';
|
|
||||||
$response->setStatusCode(400);
|
|
||||||
return $resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
if((empty($resp->type) || isset($continueRaw)) && in_array($parsedUrl->getScheme(), ['http', 'https'])) {
|
|
||||||
$curl = curl_init((string)$parsedUrl);
|
|
||||||
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_NOBODY => true,
|
|
||||||
CURLOPT_HEADER => 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) Uiharu/' . UIH_VERSION,
|
|
||||||
CURLOPT_HTTPHEADER => [
|
|
||||||
'Accept: text/html,application/xhtml+xml',
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
$headers = curl_exec($curl);
|
|
||||||
|
|
||||||
if($headers === false) {
|
|
||||||
$resp->error = 'metadata:timeout';
|
|
||||||
$resp->errorMessage = curl_error($curl);
|
|
||||||
} else {
|
|
||||||
$headersRaw = explode("\r\n", trim($headers));
|
|
||||||
$statusCode = 200;
|
|
||||||
$headers = [];
|
|
||||||
foreach($headersRaw as $header) {
|
|
||||||
if(empty($header))
|
|
||||||
continue;
|
|
||||||
if(strpos($header, ':') === false) {
|
|
||||||
$headParts = explode(' ', $header);
|
|
||||||
if(isset($headParts[1]) && is_numeric($headParts[1]))
|
|
||||||
$statusCode = (int)$headParts[1];
|
|
||||||
$headers = [];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$headerParts = explode(':', $header, 2);
|
|
||||||
$headerParts[0] = mb_strtolower($headerParts[0]);
|
|
||||||
if(isset($headers[$headerParts[0]]))
|
|
||||||
$headers[$headerParts[0]] .= ', ' . trim($headerParts[1] ?? '');
|
|
||||||
else
|
|
||||||
$headers[$headerParts[0]] = trim($headerParts[1] ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$contentType = MediaType::parse($headers['content-type'] ?? '');
|
|
||||||
} catch(InvalidArgumentException $ex) {
|
|
||||||
$contentType = MediaType::parse('application/octet-stream');
|
|
||||||
}
|
|
||||||
|
|
||||||
$resp->content_type = MediaTypeExts::toV1($contentType);
|
|
||||||
|
|
||||||
$isHTML = $contentType->equals('text/html');
|
|
||||||
$isXHTML = $contentType->equals('application/xhtml+xml');
|
|
||||||
|
|
||||||
if($isHTML || $isXHTML) {
|
|
||||||
curl_setopt_array($curl, [
|
|
||||||
CURLOPT_NOBODY => false,
|
|
||||||
CURLOPT_HEADER => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$body = curl_exec($curl);
|
|
||||||
curl_close($curl);
|
|
||||||
|
|
||||||
$document = new DOMDocument;
|
|
||||||
if($isXHTML) {
|
|
||||||
$document->loadXML($body, LIBXML_NOERROR | LIBXML_NONET | LIBXML_NOWARNING);
|
|
||||||
} else {
|
|
||||||
$document->loadHTML($body, LIBXML_NOERROR | LIBXML_NONET | LIBXML_NOWARNING);
|
|
||||||
foreach($document->childNodes as $child)
|
|
||||||
if($child->nodeType === XML_PI_NODE) {
|
|
||||||
$document->removeChild($child);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$document->encoding = $contentType->getCharset();
|
|
||||||
}
|
|
||||||
|
|
||||||
$charSet = $document->encoding;
|
|
||||||
|
|
||||||
$resp->type = 'website';
|
|
||||||
$resp->title = '';
|
|
||||||
|
|
||||||
$isMetaTitle = false;
|
|
||||||
$titleTag = $document->getElementsByTagName('title');
|
|
||||||
foreach($titleTag as $tag) {
|
|
||||||
$resp->title = trim(mb_convert_encoding($tag->textContent, 'utf-8', $charSet));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$metaTags = $document->getElementsByTagName('meta');
|
|
||||||
foreach($metaTags as $tag) {
|
|
||||||
$nameAttr = $tag->hasAttribute('name') ? $tag->getAttribute('name') : (
|
|
||||||
$tag->hasAttribute('property') ? $tag->getAttribute('property') : ''
|
|
||||||
);
|
|
||||||
$valueAttr = $tag->hasAttribute('value') ? $tag->getAttribute('value') : (
|
|
||||||
$tag->hasAttribute('content') ? $tag->getAttribute('content') : ''
|
|
||||||
);
|
|
||||||
$nameAttr = trim(mb_convert_encoding($nameAttr, 'utf-8', $charSet));
|
|
||||||
$valueAttr = trim(mb_convert_encoding($valueAttr, 'utf-8', $charSet));
|
|
||||||
if(empty($nameAttr) || empty($valueAttr))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
switch($nameAttr) {
|
|
||||||
case 'og:title':
|
|
||||||
case 'twitter:title':
|
|
||||||
if(!$isMetaTitle) {
|
|
||||||
$isMetaTitle = true;
|
|
||||||
$resp->title = $valueAttr;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'description':
|
|
||||||
case 'og:description':
|
|
||||||
case 'twitter:description':
|
|
||||||
if(!isset($resp->description))
|
|
||||||
$resp->description = $valueAttr;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'og:site_name':
|
|
||||||
$resp->site_name = $valueAttr;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'og:image':
|
|
||||||
case 'twitter:image':
|
|
||||||
$resp->image = $valueAttr;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'theme-color':
|
|
||||||
$resp->color = $valueAttr;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'og:type':
|
|
||||||
$resp->type = $valueAttr;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(empty($resp->type))
|
|
||||||
$resp->type = 'media';
|
|
||||||
$resp->is_image = $isImage = $contentType->matchCategory('image');
|
|
||||||
$resp->is_audio = $isAudio = $contentType->matchCategory('audio');
|
|
||||||
$resp->is_video = $isVideo = $contentType->matchCategory('video');
|
|
||||||
|
|
||||||
if($isImage || $isAudio || $isVideo) {
|
|
||||||
curl_close($curl);
|
|
||||||
$resp->media = new stdClass;
|
|
||||||
$ffmpeg = FFMPEG::probe($parsedUrl);
|
|
||||||
|
|
||||||
if(!empty($ffmpeg)) {
|
|
||||||
if(!empty($ffmpeg->format)) {
|
|
||||||
$resp->media->confidence = empty($ffmpeg->format->probe_score) ? 0 : (intval($ffmpeg->format->probe_score) / 100);
|
|
||||||
if(!empty($ffmpeg->format->duration))
|
|
||||||
$resp->media->duration = floatval($ffmpeg->format->duration);
|
|
||||||
if(!empty($ffmpeg->format->size))
|
|
||||||
$resp->media->size = intval($ffmpeg->format->size);
|
|
||||||
if(!empty($ffmpeg->format->bit_rate))
|
|
||||||
$resp->media->bitrate = intval($ffmpeg->format->bit_rate);
|
|
||||||
|
|
||||||
if($isVideo || $isImage) {
|
|
||||||
if(!empty($ffmpeg->streams)) {
|
|
||||||
foreach($ffmpeg->streams as $stream) {
|
|
||||||
if(($stream->codec_type ?? null) !== 'video')
|
|
||||||
continue;
|
|
||||||
|
|
||||||
$resp->width = intval($stream->coded_width ?? $stream->width ?? -1);
|
|
||||||
$resp->height = intval($stream->coded_height ?? $stream->height ?? -1);
|
|
||||||
|
|
||||||
if(!empty($stream->display_aspect_ratio))
|
|
||||||
$resp->media->aspect_ratio = $stream->display_aspect_ratio;
|
|
||||||
|
|
||||||
if($isImage)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if($isAudio) {
|
|
||||||
function eat_tags(stdClass $dest, stdClass $source): void {
|
|
||||||
if(!empty($source->title) || !empty($source->TITLE))
|
|
||||||
$dest->title = $source->title ?? $source->TITLE;
|
|
||||||
if(!empty($source->artist) || !empty($source->ARTIST))
|
|
||||||
$dest->artist = $source->artist ?? $source->ARTIST;
|
|
||||||
if(!empty($source->album) || !empty($source->ALBUM))
|
|
||||||
$dest->album = $source->album ?? $source->ALBUM;
|
|
||||||
if(!empty($source->date) || !empty($source->DATE))
|
|
||||||
$dest->date = $source->date ?? $source->DATE;
|
|
||||||
if(!empty($source->comment) || !empty($source->COMMENT))
|
|
||||||
$dest->comment = $source->comment ?? $source->COMMENT;
|
|
||||||
if(!empty($source->genre) || !empty($source->GENRE))
|
|
||||||
$dest->genre = $source->genre ?? $source->GENRE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!empty($ffmpeg->format->tags)) {
|
|
||||||
$resp->media->tags = new stdClass;
|
|
||||||
eat_tags($resp->media->tags, $ffmpeg->format->tags);
|
|
||||||
} elseif(!empty($ffmpeg->streams)) {
|
|
||||||
// iterate over streams, fuck ogg
|
|
||||||
$resp->media->tags = new stdClass;
|
|
||||||
foreach($ffmpeg->streams as $stream) {
|
|
||||||
if(($stream->codec_type ?? null) === 'audio' && !empty($stream->tags)) {
|
|
||||||
eat_tags($resp->media->tags, $stream->tags);
|
|
||||||
if(!empty($resp->media->tags))
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(empty($resp->title)) {
|
|
||||||
$audioTitle = '';
|
|
||||||
if(!empty($resp->media->tags->artist))
|
|
||||||
$audioTitle .= $resp->media->tags->artist . ' - ';
|
|
||||||
if(!empty($resp->media->tags->title))
|
|
||||||
$audioTitle .= $resp->media->tags->title;
|
|
||||||
if(!empty($resp->media->tags->date))
|
|
||||||
$audioTitle .= ' (' . $resp->media->tags->date . ')';
|
|
||||||
if(!empty($audioTitle))
|
|
||||||
$resp->title = $audioTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(empty($resp->description) && !empty($resp->media->tags->comment))
|
|
||||||
$resp->description = $resp->media->tags->comment;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if($includeRawResult)
|
|
||||||
$resp->ffmpeg = $ffmpeg;
|
|
||||||
} else curl_close($curl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$sw->stop();
|
$sw->stop();
|
||||||
$resp->took = $sw->getElapsedTime() / 1000;
|
$resp->took = $sw->getElapsedTime() / 1000;
|
||||||
|
|
461
src/Colour.php
461
src/Colour.php
|
@ -5,4 +5,465 @@ final class Colour {
|
||||||
public static function toHexString(int $colour): string {
|
public static function toHexString(int $colour): string {
|
||||||
return '#' . str_pad(dechex($colour), 6, '0', STR_PAD_LEFT);
|
return '#' . str_pad(dechex($colour), 6, '0', STR_PAD_LEFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function convertFromCSS(string $input): int {
|
||||||
|
$input = strtolower(trim($input));
|
||||||
|
if(empty($input))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
switch($input) {
|
||||||
|
// CSS Level 1
|
||||||
|
case @"black":
|
||||||
|
return self::BLACK;
|
||||||
|
case @"silver":
|
||||||
|
return self::SILVER;
|
||||||
|
case @"gray":
|
||||||
|
case @"grey": // CSS Level 3
|
||||||
|
return self::GREY;
|
||||||
|
case @"white":
|
||||||
|
return self::WHITE;
|
||||||
|
case @"maroon":
|
||||||
|
return self::MAROON;
|
||||||
|
case @"red":
|
||||||
|
return self::RED;
|
||||||
|
case @"purple":
|
||||||
|
return self::PURPLE;
|
||||||
|
case @"fuchsia":
|
||||||
|
case @"magenta": // CSS Level 3
|
||||||
|
return self::MAGENTA;
|
||||||
|
case @"green":
|
||||||
|
return self::GREEN;
|
||||||
|
case @"lime":
|
||||||
|
return self::LIME;
|
||||||
|
case @"olive":
|
||||||
|
return self::OLIVE;
|
||||||
|
case @"yellow":
|
||||||
|
return self::YELLOW;
|
||||||
|
case @"navy":
|
||||||
|
return self::NAVY;
|
||||||
|
case @"blue":
|
||||||
|
return self::BLUE;
|
||||||
|
case @"teal":
|
||||||
|
return self::TEAL;
|
||||||
|
case @"aqua":
|
||||||
|
case @"cyan": // CSS Level 3
|
||||||
|
return self::CYAN;
|
||||||
|
|
||||||
|
// CSS Level 2
|
||||||
|
case @"orange":
|
||||||
|
return self::ORANGE;
|
||||||
|
|
||||||
|
// CSS Level 3
|
||||||
|
case @"aliceblue":
|
||||||
|
return self::ALICEBLUE;
|
||||||
|
case @"antiquewhite":
|
||||||
|
return self::ANTIQUEWHITE;
|
||||||
|
case @"aquamarine":
|
||||||
|
return self::AQUAMARINE;
|
||||||
|
case @"azure":
|
||||||
|
return self::AZURE;
|
||||||
|
case @"beige":
|
||||||
|
return self::BEIGE;
|
||||||
|
case @"bisque":
|
||||||
|
return self::BISQUE;
|
||||||
|
case @"blanchedalmond":
|
||||||
|
return self::BLANCHEDALMOND;
|
||||||
|
case @"blueviolet":
|
||||||
|
return self::BLUEVIOLET;
|
||||||
|
case @"brown":
|
||||||
|
return self::BROWN;
|
||||||
|
case @"burlywood":
|
||||||
|
return self::BURLYWOOD;
|
||||||
|
case @"cadetblue":
|
||||||
|
return self::CADETBLUE;
|
||||||
|
case @"chartreuse":
|
||||||
|
return self::CHARTREUSE;
|
||||||
|
case @"chocolate":
|
||||||
|
return self::CHOCOLATE;
|
||||||
|
case @"coral":
|
||||||
|
return self::CORAL;
|
||||||
|
case @"cornflowerblue":
|
||||||
|
return self::CORNFLOWERBLUE;
|
||||||
|
case @"cornsilk":
|
||||||
|
return self::CORNSILK;
|
||||||
|
case @"crimson":
|
||||||
|
return self::CRIMSON;
|
||||||
|
case @"darkblue":
|
||||||
|
return self::DARKBLUE;
|
||||||
|
case @"darkcyan":
|
||||||
|
return self::DARKCYAN;
|
||||||
|
case @"darkgoldenrod":
|
||||||
|
return self::DARKGOLDENROD;
|
||||||
|
case @"darkgrey":
|
||||||
|
case @"darkgray":
|
||||||
|
return self::DARKGREY;
|
||||||
|
case @"darkgreen":
|
||||||
|
return self::DARKGREEN;
|
||||||
|
case @"darkkhaki":
|
||||||
|
return self::DARKKHAKI;
|
||||||
|
case @"darkmagenta":
|
||||||
|
return self::DARKMAGENTA;
|
||||||
|
case @"darkolivegreen":
|
||||||
|
return self::DARKOLIVEGREEN;
|
||||||
|
case @"darkorange":
|
||||||
|
return self::DARKORANGE;
|
||||||
|
case @"darkorchid":
|
||||||
|
return self::DARKORCHID;
|
||||||
|
case @"darkred":
|
||||||
|
return self::DARKRED;
|
||||||
|
case @"darksalmon":
|
||||||
|
return self::DARKSALMON;
|
||||||
|
case @"darkseagreen":
|
||||||
|
return self::DARKSEAGREEN;
|
||||||
|
case @"darkslateblue":
|
||||||
|
return self::DARKSLATEBLUE;
|
||||||
|
case @"darkslategrey":
|
||||||
|
case @"darkslategray":
|
||||||
|
return self::DARKSLATEGREY;
|
||||||
|
case @"darkturquoise":
|
||||||
|
return self::DARKTURQUOISE;
|
||||||
|
case @"darkviolet":
|
||||||
|
return self::DARKVIOLET;
|
||||||
|
case @"deeppink":
|
||||||
|
return self::DEEPPINK;
|
||||||
|
case @"deepskyblue":
|
||||||
|
return self::DEEPSKYBLUE;
|
||||||
|
case @"dimgray":
|
||||||
|
case @"dimgrey":
|
||||||
|
return self::DIMGREY;
|
||||||
|
case @"dodgerblue":
|
||||||
|
return DodgerBluself::DODGERBLUE;
|
||||||
|
case @"firebrick":
|
||||||
|
return self::FIREBRICK;
|
||||||
|
case @"floralwhite":
|
||||||
|
return self::FLORALWHITE;
|
||||||
|
case @"forestgreen":
|
||||||
|
return self::FORESTGREEN;
|
||||||
|
case @"gainsboro":
|
||||||
|
return self::GAINSBORO;
|
||||||
|
case @"ghostwhite":
|
||||||
|
return self::GHOSTWHITE;
|
||||||
|
case @"gold":
|
||||||
|
return self::GOLD;
|
||||||
|
case @"goldenrod":
|
||||||
|
return self::GOLDENROD;
|
||||||
|
case @"greenyellow":
|
||||||
|
return self::GREENYELLOW;
|
||||||
|
case @"honeydew":
|
||||||
|
return self::HONEYDEW;
|
||||||
|
case @"hotpink":
|
||||||
|
return self::HOTPINK;
|
||||||
|
case @"indianred":
|
||||||
|
return self::INDIANRED;
|
||||||
|
case @"indigo":
|
||||||
|
return self::INDIGO;
|
||||||
|
case @"ivory":
|
||||||
|
return self::IVORY;
|
||||||
|
case @"khaki":
|
||||||
|
return self::KHAKI;
|
||||||
|
case @"lavender":
|
||||||
|
return self::LAVENDER;
|
||||||
|
case @"lavenderblush":
|
||||||
|
return self::LAVENDERBLUSH;
|
||||||
|
case @"lawngreen":
|
||||||
|
return self::LAWNGREEN;
|
||||||
|
case @"lemonchiffon":
|
||||||
|
return self::LEMONCHIFFON;
|
||||||
|
case @"lightblue":
|
||||||
|
return self::LIGHTBLUE;
|
||||||
|
case @"lightcoral":
|
||||||
|
return self::LIGHTCORAL;
|
||||||
|
case @"lightcyan":
|
||||||
|
return self::LIGHTCYAN;
|
||||||
|
case @"lightgoldenrodyellow":
|
||||||
|
return self::LIGHTGOLDENRODYELLOW;
|
||||||
|
case @"lightgray":
|
||||||
|
case @"lightgrey":
|
||||||
|
return self::LIGHTGREY;
|
||||||
|
case @"lightgreen":
|
||||||
|
return self::LIGHTGREEN;
|
||||||
|
case @"lightpink":
|
||||||
|
return self::LIGHTPINK;
|
||||||
|
case @"lightsalmon":
|
||||||
|
return self::LIGHTSALMON;
|
||||||
|
case @"lightseagreen":
|
||||||
|
return self::LIGHTSEAGREEN;
|
||||||
|
case @"lightskyblue":
|
||||||
|
return self::LIGHTSEAGREEN;
|
||||||
|
case @"lightslategray":
|
||||||
|
case @"lightslategrey":
|
||||||
|
return self::LIGHTSLATEGREY;
|
||||||
|
case @"lightsteelblue":
|
||||||
|
return self::LIGHTSTEELBLUE;
|
||||||
|
case @"lightyellow":
|
||||||
|
return self::LIGHTYELLOW;
|
||||||
|
case @"limegreen":
|
||||||
|
return self::LIMEGREEN;
|
||||||
|
case @"linen":
|
||||||
|
return self::LINEN;
|
||||||
|
case @"mediumaquamarine":
|
||||||
|
return self::MEDIUMAQUAMARINE;
|
||||||
|
case @"mediumblue":
|
||||||
|
return self::MEDIUMBLUE;
|
||||||
|
case @"mediumorchid":
|
||||||
|
return self::MEDIUMORCHID;
|
||||||
|
case @"mediumpurple":
|
||||||
|
return self::MEDIUMPURPLE;
|
||||||
|
case @"mediumseagreen":
|
||||||
|
return self::MEDIUMSEAGREEN;
|
||||||
|
case @"mediumslateblue":
|
||||||
|
return self::MEDIUMSLATEBLUE;
|
||||||
|
case @"mediumspringgreen":
|
||||||
|
return self::MEDIUMSPRINGGREEN;
|
||||||
|
case @"mediumturquoise":
|
||||||
|
return self::MEDIUMTURQUOISE;
|
||||||
|
case @"mediumvioletred":
|
||||||
|
return self::MEDIUMVIOLETRED;
|
||||||
|
case @"midnightblue":
|
||||||
|
return self::MIDNIGHTBLUE;
|
||||||
|
case @"mintcream":
|
||||||
|
return self::MINTCREAM;
|
||||||
|
case @"mistyrose":
|
||||||
|
return self::MISTYROSE;
|
||||||
|
case @"moccasin":
|
||||||
|
return self::MOCCASIN;
|
||||||
|
case @"navajowhite":
|
||||||
|
return self::NAVAJOWHITE;
|
||||||
|
case @"oldlace":
|
||||||
|
return self::OLDLACE;
|
||||||
|
case @"olivedrab":
|
||||||
|
return self::OLIVEDRAB;
|
||||||
|
case @"orangered":
|
||||||
|
return self::ORANGERED;
|
||||||
|
case @"orchid":
|
||||||
|
return self::ORCHID;
|
||||||
|
case @"palegoldenrod":
|
||||||
|
return self::PALEGOLDENROD;
|
||||||
|
case @"palegreen":
|
||||||
|
return self::PALEGREEN;
|
||||||
|
case @"paleturquoise":
|
||||||
|
return self::PALETURQUOISE;
|
||||||
|
case @"palevioletred":
|
||||||
|
return self::PALEVIOLETRED;
|
||||||
|
case @"papayawhip":
|
||||||
|
return self::PAPAYAWHIP;
|
||||||
|
case @"peachpuff":
|
||||||
|
return self::PEACHPUFF;
|
||||||
|
case @"peru":
|
||||||
|
return self::PERU;
|
||||||
|
case @"pink":
|
||||||
|
return self::PINK;
|
||||||
|
case @"plum":
|
||||||
|
return self::PLUM;
|
||||||
|
case @"powderblue":
|
||||||
|
return self::POWDERBLUE;
|
||||||
|
case @"rosybrown":
|
||||||
|
return self::ROSYBROWN;
|
||||||
|
case @"royalblue":
|
||||||
|
return self::ROYALBLUE;
|
||||||
|
case @"saddlebrown":
|
||||||
|
return self::SADDLEBROWN;
|
||||||
|
case @"salmon":
|
||||||
|
return self::SALMON;
|
||||||
|
case @"sandybrown":
|
||||||
|
return self::SANDYBROWN;
|
||||||
|
case @"seagreen":
|
||||||
|
return self::SEAGREEN;
|
||||||
|
case @"seashell":
|
||||||
|
return self::SEASHELL;
|
||||||
|
case @"sienna":
|
||||||
|
return self::SIENNA;
|
||||||
|
case @"skyblue":
|
||||||
|
return self::SKYBLUE;
|
||||||
|
case @"slateblue":
|
||||||
|
return self::SLATEBLUE;
|
||||||
|
case @"slategray":
|
||||||
|
case @"slategrey":
|
||||||
|
return self::SLATEGREY;
|
||||||
|
case @"snow":
|
||||||
|
return self::SNOW;
|
||||||
|
case @"springgreen":
|
||||||
|
return self::SPRINGGREEN;
|
||||||
|
case @"steelblue":
|
||||||
|
return self::STEELBLUE;
|
||||||
|
case @"tan":
|
||||||
|
return self::TAN;
|
||||||
|
case @"thistle":
|
||||||
|
return self::THISTLE;
|
||||||
|
case @"tomato":
|
||||||
|
return self::TOMATO;
|
||||||
|
case @"turquoise":
|
||||||
|
return self::TURQUOISE;
|
||||||
|
case @"violet":
|
||||||
|
return self::VIOLET;
|
||||||
|
case @"wheat":
|
||||||
|
return self::WHEAT;
|
||||||
|
case @"whitesmoke":
|
||||||
|
return self::WHITESMOKE;
|
||||||
|
case @"yellowgreen":
|
||||||
|
return self::YELLOWGREEN;
|
||||||
|
|
||||||
|
// CSS Level 4
|
||||||
|
case @"rebeccapurple":
|
||||||
|
return self::REBECCAPURPLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #xxxxxx
|
||||||
|
if(preg_match('/^#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})$/', $input, $matches)) {
|
||||||
|
return (hexdec($matches[1]) << 16)
|
||||||
|
| (hexdec($matches[2]) << 8)
|
||||||
|
| hexdec($matches[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #xxx
|
||||||
|
if(preg_match('/^#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])$/', $input, $matches)) {
|
||||||
|
return (hexdec($matches[1] . $matches[1]) << 16)
|
||||||
|
| (hexdec($matches[2] . $matches[2]) << 8)
|
||||||
|
| hexdec($matches[3] . $matches[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: bother with rgb and rgba someday
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public const BLACK = 0x000000;
|
||||||
|
public const SILVER = 0xC0C0C0;
|
||||||
|
public const GREY = 0x808080;
|
||||||
|
public const WHITE = 0xFFFFFF;
|
||||||
|
public const MAROON = 0x800000;
|
||||||
|
public const RED = 0xFF0000;
|
||||||
|
public const PURPLE = 0x800080;
|
||||||
|
public const MAGENTA = 0xFF00FF;
|
||||||
|
public const GREEN = 0x008000;
|
||||||
|
public const LIME = 0x00FF00;
|
||||||
|
public const OLIVE = 0x808000;
|
||||||
|
public const YELLOW = 0xFFFF00;
|
||||||
|
public const NAVY = 0x000080;
|
||||||
|
public const BLUE = 0x0000FF;
|
||||||
|
public const TEAL = 0x008080;
|
||||||
|
public const CYAN = 0x00FFFF;
|
||||||
|
public const ORANGE = 0xFFA500;
|
||||||
|
public const ALICEBLUE = 0xF0F8FF;
|
||||||
|
public const ANTIQUEWHITE = 0xFAEBD7;
|
||||||
|
public const AQUAMARINE = 0x7FFFD4;
|
||||||
|
public const AZURE = 0xF0FFFF;
|
||||||
|
public const BEIGE = 0xF5F5DC;
|
||||||
|
public const BISQUE = 0xFFE4C4;
|
||||||
|
public const BLANCHEDALMOND = 0xFFEBCD;
|
||||||
|
public const BLUEVIOLET = 0x8A2BE2;
|
||||||
|
public const BROWN = 0xA52A2A;
|
||||||
|
public const BURLYWOOD = 0xDEB887;
|
||||||
|
public const CADETBLUE = 0x5F9EA0;
|
||||||
|
public const CHARTREUSE = 0x7FFF00;
|
||||||
|
public const CHOCOLATE = 0xD2691E;
|
||||||
|
public const CORAL = 0xFF7F50;
|
||||||
|
public const CORNFLOWERBLUE = 0x6495ED;
|
||||||
|
public const CORNSILK = 0xFFF8DC;
|
||||||
|
public const CRIMSON = 0xDC143C;
|
||||||
|
public const DARKBLUE = 0x00008B;
|
||||||
|
public const DARKCYAN = 0x008B8B;
|
||||||
|
public const DARKGOLDENROD = 0xB8860B;
|
||||||
|
public const DARKGREY = 0xA9A9A9;
|
||||||
|
public const DARKGREEN = 0x006400;
|
||||||
|
public const DARKKHAKI = 0xBDB76B;
|
||||||
|
public const DARKMAGENTA = 0x8B008B;
|
||||||
|
public const DARKOLIVEGREEN = 0x556B2F;
|
||||||
|
public const DARKORANGE = 0xFF8C00;
|
||||||
|
public const DARKORCHID = 0x9932CC;
|
||||||
|
public const DARKRED = 0x8B0000;
|
||||||
|
public const DARKSALMON = 0xE9967A;
|
||||||
|
public const DARKSEAGREEN = 0x8FBC8F;
|
||||||
|
public const DARKSLATEBLUE = 0x483D8B;
|
||||||
|
public const DARKSLATEGREY = 0x2F4F4F;
|
||||||
|
public const DARKTURQUOISE = 0x00CED1;
|
||||||
|
public const DARKVIOLET = 0x9400D3;
|
||||||
|
public const DEEPPINK = 0xFF1493;
|
||||||
|
public const DEEPSKYBLUE = 0x00BFFF;
|
||||||
|
public const DIMGREY = 0x696969;
|
||||||
|
public const DODGERBLUE = 0x1E90FF;
|
||||||
|
public const FIREBRICK = 0xB22222;
|
||||||
|
public const FLORALWHITE = 0xFFFAF0;
|
||||||
|
public const FORESTGREEN = 0x228B22;
|
||||||
|
public const GAINSBORO = 0xDCDCDC;
|
||||||
|
public const GHOSTWHITE = 0xF8F8FF;
|
||||||
|
public const GOLD = 0xFFD700;
|
||||||
|
public const GOLDENROD = 0xDAA520;
|
||||||
|
public const GREENYELLOW = 0xADFF2F;
|
||||||
|
public const HONEYDEW = 0xF0FFF0;
|
||||||
|
public const HOTPINK = 0xFF69B4;
|
||||||
|
public const INDIANRED = 0xCD5C5C;
|
||||||
|
public const INDIGO = 0x4B0082;
|
||||||
|
public const IVORY = 0xFFFFF0;
|
||||||
|
public const KHAKI = 0xF0E68C;
|
||||||
|
public const LAVENDER = 0xE6E6FA;
|
||||||
|
public const LAVENDERBLUSH = 0xFFF0F5;
|
||||||
|
public const LAWNGREEN = 0x7CFC00;
|
||||||
|
public const LEMONCHIFFON = 0xFFFACD;
|
||||||
|
public const LIGHTBLUE = 0xADD8E6;
|
||||||
|
public const LIGHTCORAL = 0xF08080;
|
||||||
|
public const LIGHTCYAN = 0xE0FFFF;
|
||||||
|
public const LIGHTGOLDENRODYELLOW = 0xFAFAD2;
|
||||||
|
public const LIGHTGREY = 0xD3D3D3;
|
||||||
|
public const LIGHTGREEN = 0x90EE90;
|
||||||
|
public const LIGHTPINK = 0xFFB6C1;
|
||||||
|
public const LIGHTSALMON = 0xFFA07A;
|
||||||
|
public const LIGHTSEAGREEN = 0x20B2AA;
|
||||||
|
public const LIGHTSKYBLUE = 0x87CEFA;
|
||||||
|
public const LIGHTSLATEGREY = 0x778899;
|
||||||
|
public const LIGHTSTEELBLUE = 0xB0C4DE;
|
||||||
|
public const LIGHTYELLOW = 0xFFFFE0;
|
||||||
|
public const LIMEGREEN = 0x32CD32;
|
||||||
|
public const LINEN = 0xFAF0E6;
|
||||||
|
public const MEDIUMAQUAMARINE = 0x66CDAA;
|
||||||
|
public const MEDIUMBLUE = 0x0000CD;
|
||||||
|
public const MEDIUMORCHID = 0xBA55D3;
|
||||||
|
public const MEDIUMPURPLE = 0x9370DB;
|
||||||
|
public const MEDIUMSEAGREEN = 0x3CB371;
|
||||||
|
public const MEDIUMSLATEBLUE = 0x7B68EE;
|
||||||
|
public const MEDIUMSPRINGGREEN = 0x00FA9A;
|
||||||
|
public const MEDIUMTURQUOISE = 0x48D1CC;
|
||||||
|
public const MEDIUMVIOLETRED = 0xC71585;
|
||||||
|
public const MIDNIGHTBLUE = 0x191970;
|
||||||
|
public const MINTCREAM = 0xF5FFFA;
|
||||||
|
public const MISTYROSE = 0xFFE4E1;
|
||||||
|
public const MOCCASIN = 0xFFE4B5;
|
||||||
|
public const NAVAJOWHITE = 0xFFDEAD;
|
||||||
|
public const OLDLACE = 0xFDF5E6;
|
||||||
|
public const OLIVEDRAB = 0x6B8E23;
|
||||||
|
public const ORANGERED = 0xFF4500;
|
||||||
|
public const ORCHID = 0xDA70D6;
|
||||||
|
public const PALEGOLDENROD = 0xEEE8AA;
|
||||||
|
public const PALEGREEN = 0x98FB98;
|
||||||
|
public const PALETURQUOISE = 0xAFEEEE;
|
||||||
|
public const PALEVIOLETRED = 0xDB7093;
|
||||||
|
public const PAPAYAWHIP = 0xFFEFD5;
|
||||||
|
public const PEACHPUFF = 0xFFDAB9;
|
||||||
|
public const PERU = 0xCD853F;
|
||||||
|
public const PINK = 0xFFC0CD;
|
||||||
|
public const PLUM = 0xDDA0DD;
|
||||||
|
public const POWDERBLUE = 0xB0E0E6;
|
||||||
|
public const ROSYBROWN = 0xBC8F8F;
|
||||||
|
public const ROYALBLUE = 0x4169E1;
|
||||||
|
public const SADDLEBROWN = 0x8B4513;
|
||||||
|
public const SALMON = 0xFA8072;
|
||||||
|
public const SANDYBROWN = 0xF4A460;
|
||||||
|
public const SEAGREEN = 0x2E8B57;
|
||||||
|
public const SEASHELL = 0xFFF5EE;
|
||||||
|
public const SIENNA = 0xA0522D;
|
||||||
|
public const SKYBLUE = 0x87CEEB;
|
||||||
|
public const SLATEBLUE = 0x6A5ACD;
|
||||||
|
public const SLATEGREY = 0x708090;
|
||||||
|
public const SNOW = 0xFFFAFA;
|
||||||
|
public const SPRINGGREEN = 0x00FF7F;
|
||||||
|
public const STEELBLUE = 0x4682B4;
|
||||||
|
public const TAN = 0xD2B48C;
|
||||||
|
public const THISTLE = 0xD8BFD8;
|
||||||
|
public const TOMATO = 0xFF6347;
|
||||||
|
public const TURQUOISE = 0x40E0D0;
|
||||||
|
public const VIOLET = 0xEE82EE;
|
||||||
|
public const WHEAT = 0xF5DEB3;
|
||||||
|
public const WHITESMOKE = 0xF5F5F5;
|
||||||
|
public const YELLOWGREEN = 0x9ACD32;
|
||||||
|
public const REBECCAPURPLE = 0x663399;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,15 +34,44 @@ final class FFMPEG {
|
||||||
$out->size = intval($in->format->size);
|
$out->size = intval($in->format->size);
|
||||||
|
|
||||||
if(!empty($in->format->bit_rate))
|
if(!empty($in->format->bit_rate))
|
||||||
$out->bitRate = intval($int->format->bit_rate);
|
$out->bitRate = intval($in->format->bit_rate);
|
||||||
|
|
||||||
if(!empty($in->format->tags)) {
|
if(!empty($in->format->tags)) {
|
||||||
$out->tagTitle = $in->format->tags->title ?? $in->format->tags->TITLE;
|
if(!empty($in->format->tags->title)) {
|
||||||
$out->tagArtist = $in->format->tags->artist ?? $in->format->tags->ARTIST;
|
$out->tagTitle = $in->format->tags->title;
|
||||||
$out->tagAlbum = $in->format->tags->album ?? $in->format->tags->ALBUM;
|
} elseif(!empty($in->format->tags->TITLE)) {
|
||||||
$out->tagDate = $in->format->tags->date ?? $in->format->tags->DATE;
|
$out->tagTitle = $in->format->tags->TITLE;
|
||||||
$out->tagComment = $in->format->tags->comment ?? $in->format->tags->COMMENT;
|
}
|
||||||
$out->tagGenre = $in->format->tags->genre ?? $in->format->tags->GENRE;
|
|
||||||
|
if(!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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!empty($in->format->tags->genre)) {
|
||||||
|
$out->tagGenre = $in->format->tags->genre;
|
||||||
|
} elseif(!empty($in->format->tags->GENRE)) {
|
||||||
|
$out->tagGenre = $in->format->tags->GENRE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,12 +86,41 @@ final class FFMPEG {
|
||||||
if(!empty($stream->display_aspect_ratio))
|
if(!empty($stream->display_aspect_ratio))
|
||||||
$out->aspectRatio = $stream->display_aspect_ratio;
|
$out->aspectRatio = $stream->display_aspect_ratio;
|
||||||
} elseif($codecType === 'audio') {
|
} elseif($codecType === 'audio') {
|
||||||
$out->tagTitle = $stream->tags->title ?? $stream->tags->TITLE;
|
if(!empty($stream->tags->title)) {
|
||||||
$out->tagArtist = $stream->tags->artist ?? $stream->tags->ARTIST;
|
$out->tagTitle = $stream->tags->title;
|
||||||
$out->tagAlbum = $stream->tags->album ?? $stream->tags->ALBUM;
|
} elseif(!empty($stream->tags->TITLE)) {
|
||||||
$out->tagDate = $stream->tags->date ?? $stream->tags->DATE;
|
$out->tagTitle = $stream->tags->TITLE;
|
||||||
$out->tagComment = $stream->tags->comment ?? $stream->tags->COMMENT;
|
}
|
||||||
$out->tagGenre = $stream->tags->genre ?? $stream->tags->GENRE;
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,7 @@ final class EEPROMLookup implements \Uiharu\ILookup {
|
||||||
|
|
||||||
$fileInfo = $this->rawLookup($fileId);
|
$fileInfo = $this->rawLookup($fileId);
|
||||||
if($fileInfo === null)
|
if($fileInfo === null)
|
||||||
throw new RuntimeException('EEPROM file does not exist.');
|
throw new RuntimeException('EEPROM file does not exist: ' . $fileId);
|
||||||
|
|
||||||
$url = Url::parse('https://' . $this->shortDomains[0] . '/' . $fileId);
|
$url = Url::parse('https://' . $this->shortDomains[0] . '/' . $fileId);
|
||||||
$mediaType = MediaType::parse($fileInfo->type);
|
$mediaType = MediaType::parse($fileInfo->type);
|
||||||
|
|
|
@ -73,7 +73,6 @@ final class EEPROMLookupResult implements \Uiharu\ILookupResult, \Uiharu\IHasMed
|
||||||
if($this->audioTags->hasDate())
|
if($this->audioTags->hasDate())
|
||||||
$title .= ' (' . $this->audioTags->getDate() . ')';
|
$title .= ' (' . $this->audioTags->getDate() . ')';
|
||||||
|
|
||||||
$title = trim($title, " \n\r\t\v\x00()-");
|
|
||||||
if(!empty($title))
|
if(!empty($title))
|
||||||
return $title;
|
return $title;
|
||||||
}
|
}
|
||||||
|
@ -120,7 +119,7 @@ final class EEPROMLookupResult implements \Uiharu\ILookupResult, \Uiharu\IHasMed
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hasDimensions(): bool {
|
public function hasDimensions(): bool {
|
||||||
return $this->isMedia();
|
return $this->isImage() || $this->isVideo();
|
||||||
}
|
}
|
||||||
public function getWidth(): int {
|
public function getWidth(): int {
|
||||||
return $this->mediaInfo->width ?? 0;
|
return $this->mediaInfo->width ?? 0;
|
||||||
|
|
209
src/Lookup/WebLookup.php
Normal file
209
src/Lookup/WebLookup.php
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
<?php
|
||||||
|
namespace Uiharu\Lookup;
|
||||||
|
|
||||||
|
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) Uiharu/' . UIH_VERSION,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Accept: text/html,application/xhtml+xml',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
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));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$mediaType = MediaType::parse($head['lines']['content-type'] ?? '');
|
||||||
|
} catch(InvalidArgumentException $ex) {
|
||||||
|
$mediaType = MediaType::parse('application/octet-stream');
|
||||||
|
}
|
||||||
|
|
||||||
|
$isXHTML = $mediaType->equals('application/xhtml+xml');
|
||||||
|
if($isXHTML || $mediaType->equals('text/html'))
|
||||||
|
return $this->lookupSite($url, $req, $mediaType, $isXHTML);
|
||||||
|
|
||||||
|
self::reqClose($req);
|
||||||
|
|
||||||
|
if(MediaTypeExts::isMedia($mediaType))
|
||||||
|
return $this->lookupMedia($url, $mediaType);
|
||||||
|
|
||||||
|
return new WebLookupFallbackResult($url, $mediaType, $url->getHost() . ': ' . basename($url->getPath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function lookupSite(Url $url, $req, MediaType $mediaType, bool $isXHTML): WebLookupResult {
|
||||||
|
$body = self::reqBody($req);
|
||||||
|
self::reqClose($req);
|
||||||
|
|
||||||
|
$document = new DOMDocument;
|
||||||
|
if($isXHTML) {
|
||||||
|
$document->loadXML($body, LIBXML_NOERROR | LIBXML_NONET | LIBXML_NOWARNING);
|
||||||
|
} else {
|
||||||
|
$document->loadHTML($body, LIBXML_NOERROR | LIBXML_NONET | LIBXML_NOWARNING);
|
||||||
|
foreach($document->childNodes as $child)
|
||||||
|
if($child->nodeType === XML_PI_NODE) {
|
||||||
|
$document->removeChild($child);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$document->encoding = $mediaType->getCharset();
|
||||||
|
}
|
||||||
|
|
||||||
|
$charSet = $document->encoding;
|
||||||
|
|
||||||
|
$siteInfo = new stdClass;
|
||||||
|
$siteInfo->title = '';
|
||||||
|
$siteInfo->metaTitle = '';
|
||||||
|
$siteInfo->desc = '';
|
||||||
|
$siteInfo->siteName = '';
|
||||||
|
$siteInfo->image = '';
|
||||||
|
$siteInfo->colour = '';
|
||||||
|
$siteInfo->type = 'website';
|
||||||
|
|
||||||
|
$titleTag = $document->getElementsByTagName('title');
|
||||||
|
foreach($titleTag as $tag) {
|
||||||
|
$siteInfo->title = trim(mb_convert_encoding($tag->textContent, 'utf-8', $charSet));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$metaTags = $document->getElementsByTagName('meta');
|
||||||
|
foreach($metaTags as $tag) {
|
||||||
|
$nameAttr = $tag->hasAttribute('name') ? $tag->getAttribute('name') : (
|
||||||
|
$tag->hasAttribute('property') ? $tag->getAttribute('property') : ''
|
||||||
|
);
|
||||||
|
$valueAttr = $tag->hasAttribute('value') ? $tag->getAttribute('value') : (
|
||||||
|
$tag->hasAttribute('content') ? $tag->getAttribute('content') : ''
|
||||||
|
);
|
||||||
|
$nameAttr = trim(mb_convert_encoding($nameAttr, 'utf-8', $charSet));
|
||||||
|
$valueAttr = trim(mb_convert_encoding($valueAttr, 'utf-8', $charSet));
|
||||||
|
if(empty($nameAttr) || empty($valueAttr))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
switch($nameAttr) {
|
||||||
|
case 'og:title':
|
||||||
|
case 'twitter:title':
|
||||||
|
$siteInfo->metaTitle = $valueAttr;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'description':
|
||||||
|
case 'og:description':
|
||||||
|
case 'twitter:description':
|
||||||
|
if(empty($siteInfo->desc))
|
||||||
|
$siteInfo->desc = $valueAttr;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'og:site_name':
|
||||||
|
$siteInfo->siteName = $valueAttr;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'og:image':
|
||||||
|
case 'twitter:image':
|
||||||
|
$siteInfo->image = $valueAttr;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'theme-color':
|
||||||
|
$siteInfo->colour = $valueAttr;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'og:type':
|
||||||
|
$siteInfo->type = 'website:' . $valueAttr;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WebLookupSiteResult($url, $mediaType, $siteInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function lookupMedia(Url $url, MediaType $mediaType): WebLookupResult {
|
||||||
|
$mediaInfo = FFMPEG::cleanProbe($url);
|
||||||
|
return new WebLookupMediaResult($url, $mediaType, $mediaInfo);
|
||||||
|
}
|
||||||
|
}
|
54
src/Lookup/WebLookupFallbackResult.php
Normal file
54
src/Lookup/WebLookupFallbackResult.php
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
namespace Uiharu\Lookup;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use Uiharu\Url;
|
||||||
|
use Index\MediaType;
|
||||||
|
|
||||||
|
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(): int {
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
}
|
145
src/Lookup/WebLookupMediaResult.php
Normal file
145
src/Lookup/WebLookupMediaResult.php
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
<?php
|
||||||
|
namespace Uiharu\Lookup;
|
||||||
|
|
||||||
|
use Uiharu\AudioTags;
|
||||||
|
use Uiharu\Url;
|
||||||
|
use Index\MediaType;
|
||||||
|
|
||||||
|
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(): int {
|
||||||
|
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 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();
|
||||||
|
}
|
||||||
|
public function getPreviewImage(): string {
|
||||||
|
return (string)$this->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;
|
||||||
|
}
|
||||||
|
}
|
43
src/Lookup/WebLookupResult.php
Normal file
43
src/Lookup/WebLookupResult.php
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
namespace Uiharu\Lookup;
|
||||||
|
|
||||||
|
use Uiharu\Url;
|
||||||
|
use Index\MediaType;
|
||||||
|
|
||||||
|
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(): int;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
62
src/Lookup/WebLookupSiteResult.php
Normal file
62
src/Lookup/WebLookupSiteResult.php
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
namespace Uiharu\Lookup;
|
||||||
|
|
||||||
|
use Uiharu\Colour;
|
||||||
|
use Uiharu\Url;
|
||||||
|
use Index\MediaType;
|
||||||
|
|
||||||
|
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(): int {
|
||||||
|
return Colour::convertFromCSS($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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue