From d90927469fd781bd23796ea42ae88fcf447f580b Mon Sep 17 00:00:00 2001 From: flashwave Date: Sat, 16 Jul 2022 23:19:25 +0000 Subject: [PATCH] Rewrote web lookup handler + various fixes. --- public/index.php | 3 + src/Apis/v1_0.php | 268 +------------- src/Colour.php | 461 +++++++++++++++++++++++++ src/FFMPEG.php | 84 ++++- src/Lookup/EEPROMLookup.php | 2 +- src/Lookup/EEPROMLookupResult.php | 3 +- src/Lookup/WebLookup.php | 209 +++++++++++ src/Lookup/WebLookupFallbackResult.php | 54 +++ src/Lookup/WebLookupMediaResult.php | 145 ++++++++ src/Lookup/WebLookupResult.php | 43 +++ src/Lookup/WebLookupSiteResult.php | 62 ++++ 11 files changed, 1068 insertions(+), 266 deletions(-) create mode 100644 src/Lookup/WebLookup.php create mode 100644 src/Lookup/WebLookupFallbackResult.php create mode 100644 src/Lookup/WebLookupMediaResult.php create mode 100644 src/Lookup/WebLookupResult.php create mode 100644 src/Lookup/WebLookupSiteResult.php diff --git a/public/index.php b/public/index.php index 709c759..c05f955 100644 --- a/public/index.php +++ b/public/index.php @@ -13,6 +13,9 @@ if(UIH_DEBUG) $ctx->registerLookup(new \Uiharu\Lookup\TwitterLookup); $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->registerApi(new \Uiharu\Apis\v1_0($ctx)); diff --git a/src/Apis/v1_0.php b/src/Apis/v1_0.php index 0844283..023f039 100644 --- a/src/Apis/v1_0.php +++ b/src/Apis/v1_0.php @@ -107,7 +107,7 @@ final class v1_0 implements \Uiharu\IApi { if(empty($resp->type)) { $lookup = $this->ctx->matchLookup($parsedUrl); - if($lookup !== null) { + if($lookup !== null) try { $result = $lookup->lookup($parsedUrl); @@ -176,6 +176,23 @@ final class v1_0 implements \Uiharu\IApi { $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) { @@ -195,255 +212,6 @@ final class v1_0 implements \Uiharu\IApi { $response->setStatusCode(500); 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(); $resp->took = $sw->getElapsedTime() / 1000; diff --git a/src/Colour.php b/src/Colour.php index ac2c5d3..b053ea8 100644 --- a/src/Colour.php +++ b/src/Colour.php @@ -5,4 +5,465 @@ final class Colour { public static function toHexString(int $colour): string { 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; } diff --git a/src/FFMPEG.php b/src/FFMPEG.php index 9afe863..252f923 100644 --- a/src/FFMPEG.php +++ b/src/FFMPEG.php @@ -34,15 +34,44 @@ final class FFMPEG { $out->size = intval($in->format->size); 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)) { - $out->tagTitle = $in->format->tags->title ?? $in->format->tags->TITLE; - $out->tagArtist = $in->format->tags->artist ?? $in->format->tags->ARTIST; - $out->tagAlbum = $in->format->tags->album ?? $in->format->tags->ALBUM; - $out->tagDate = $in->format->tags->date ?? $in->format->tags->DATE; - $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->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; + } + + 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)) $out->aspectRatio = $stream->display_aspect_ratio; } elseif($codecType === 'audio') { - $out->tagTitle = $stream->tags->title ?? $stream->tags->TITLE; - $out->tagArtist = $stream->tags->artist ?? $stream->tags->ARTIST; - $out->tagAlbum = $stream->tags->album ?? $stream->tags->ALBUM; - $out->tagDate = $stream->tags->date ?? $stream->tags->DATE; - $out->tagComment = $stream->tags->comment ?? $stream->tags->COMMENT; - $out->tagGenre = $stream->tags->genre ?? $stream->tags->GENRE; + 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; + } } } diff --git a/src/Lookup/EEPROMLookup.php b/src/Lookup/EEPROMLookup.php index d9b5ba7..f97051a 100644 --- a/src/Lookup/EEPROMLookup.php +++ b/src/Lookup/EEPROMLookup.php @@ -65,7 +65,7 @@ final class EEPROMLookup implements \Uiharu\ILookup { $fileInfo = $this->rawLookup($fileId); 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); $mediaType = MediaType::parse($fileInfo->type); diff --git a/src/Lookup/EEPROMLookupResult.php b/src/Lookup/EEPROMLookupResult.php index e77ad45..688100d 100644 --- a/src/Lookup/EEPROMLookupResult.php +++ b/src/Lookup/EEPROMLookupResult.php @@ -73,7 +73,6 @@ final class EEPROMLookupResult implements \Uiharu\ILookupResult, \Uiharu\IHasMed if($this->audioTags->hasDate()) $title .= ' (' . $this->audioTags->getDate() . ')'; - $title = trim($title, " \n\r\t\v\x00()-"); if(!empty($title)) return $title; } @@ -120,7 +119,7 @@ final class EEPROMLookupResult implements \Uiharu\ILookupResult, \Uiharu\IHasMed } public function hasDimensions(): bool { - return $this->isMedia(); + return $this->isImage() || $this->isVideo(); } public function getWidth(): int { return $this->mediaInfo->width ?? 0; diff --git a/src/Lookup/WebLookup.php b/src/Lookup/WebLookup.php new file mode 100644 index 0000000..3c97338 --- /dev/null +++ b/src/Lookup/WebLookup.php @@ -0,0 +1,209 @@ +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); + } +} diff --git a/src/Lookup/WebLookupFallbackResult.php b/src/Lookup/WebLookupFallbackResult.php new file mode 100644 index 0000000..3deb2d9 --- /dev/null +++ b/src/Lookup/WebLookupFallbackResult.php @@ -0,0 +1,54 @@ +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.'); + } +} diff --git a/src/Lookup/WebLookupMediaResult.php b/src/Lookup/WebLookupMediaResult.php new file mode 100644 index 0000000..a4b18cf --- /dev/null +++ b/src/Lookup/WebLookupMediaResult.php @@ -0,0 +1,145 @@ +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; + } +} diff --git a/src/Lookup/WebLookupResult.php b/src/Lookup/WebLookupResult.php new file mode 100644 index 0000000..9c489b3 --- /dev/null +++ b/src/Lookup/WebLookupResult.php @@ -0,0 +1,43 @@ +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; +} diff --git a/src/Lookup/WebLookupSiteResult.php b/src/Lookup/WebLookupSiteResult.php new file mode 100644 index 0000000..a5b9033 --- /dev/null +++ b/src/Lookup/WebLookupSiteResult.php @@ -0,0 +1,62 @@ +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; + } +}