From 4ad19c6363ef479b455babe1d35ddf16e2635370 Mon Sep 17 00:00:00 2001 From: flashwave Date: Sat, 16 Jul 2022 01:40:44 +0000 Subject: [PATCH] Reimplemented EEPROM and Twitter lookups. --- public/index.php | 6 + src/Apis/v1_0.php | 749 +++++++++++------------- src/AudioTags.php | 66 +++ src/Colour.php | 8 + src/FFMPEG.php | 71 +++ src/IHasMediaInfo.php | 33 ++ src/ILookup.php | 7 + src/ILookupResult.php | 27 + src/Lookup/EEPROMLookup.php | 77 +++ src/Lookup/EEPROMLookupResult.php | 173 ++++++ src/Lookup/TwitterLookup.php | 89 +++ src/Lookup/TwitterLookupResult.php | 51 ++ src/Lookup/TwitterLookupTweetResult.php | 46 ++ src/Lookup/TwitterLookupUserResult.php | 46 ++ src/MediaTypeExts.php | 6 + src/UihContext.php | 12 + src/Url.php | 13 +- 17 files changed, 1078 insertions(+), 402 deletions(-) create mode 100644 src/AudioTags.php create mode 100644 src/Colour.php create mode 100644 src/FFMPEG.php create mode 100644 src/IHasMediaInfo.php create mode 100644 src/ILookup.php create mode 100644 src/ILookupResult.php create mode 100644 src/Lookup/EEPROMLookup.php create mode 100644 src/Lookup/EEPROMLookupResult.php create mode 100644 src/Lookup/TwitterLookup.php create mode 100644 src/Lookup/TwitterLookupResult.php create mode 100644 src/Lookup/TwitterLookupTweetResult.php create mode 100644 src/Lookup/TwitterLookupUserResult.php diff --git a/public/index.php b/public/index.php index 1d770c5..0e76496 100644 --- a/public/index.php +++ b/public/index.php @@ -6,6 +6,12 @@ require_once __DIR__ . '/../uiharu.php'; // should be in a cron job $db->execute('DELETE FROM `uih_metadata_cache` WHERE `metadata_created` < NOW() - INTERVAL 7 DAY'); +$ctx->registerLookup(new \Uiharu\Lookup\EEPROMLookup('eeprom', 'eeprom.flashii.net', ['i.fii.moe', 'i.flashii.net'])); +if(UIH_DEBUG) + $ctx->registerLookup(new \Uiharu\Lookup\EEPROMLookup('devrom', 'eeprom.edgii.net', ['i.edgii.net'])); + +$ctx->registerLookup(new \Uiharu\Lookup\TwitterLookup); + $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 cf6efaf..c0e9005 100644 --- a/src/Apis/v1_0.php +++ b/src/Apis/v1_0.php @@ -3,20 +3,30 @@ namespace Uiharu\APIs; use stdClass; use DOMDocument; +use Exception; use InvalidArgumentException; +use Uiharu\Colour; use Uiharu\Config; +use Uiharu\FFMPEG; +use Uiharu\IHasMediaInfo; use Uiharu\MediaTypeExts; use Uiharu\UihContext; use Uiharu\Url; +use Uiharu\Lookup\EEPROMLookupResult; +use Uiharu\Lookup\TwitterLookupResult; +use Uiharu\Lookup\TwitterLookupTweetResult; +use Uiharu\Lookup\TwitterLookupUserResult; use Index\MediaType; use Index\Data\IDbConnection; use Index\Http\HttpFx; use Index\Performance\Stopwatch; final class v1_0 implements \Uiharu\IApi { + private UihContext $ctx; private IDbConnection $db; public function __construct(UihContext $ctx) { + $this->ctx = $ctx; $this->db = $ctx->getDatabase(); } @@ -29,36 +39,6 @@ final class v1_0 implements \Uiharu\IApi { $router->post('/metadata', [$this, 'handlePOST']); } - public function eepromLookup(stdClass $resp, string $eepromFileId, string $domain = 'flashii'): void { - $resp->type = 'eeprom:file'; - $resp->color = '#8559a5'; - $resp->eeprom_file_id = $eepromFileId; - $curl = curl_init("https://eeprom.{$domain}.net/uploads/{$resp->eeprom_file_id}.json"); - curl_setopt_array($curl, [ - CURLOPT_AUTOREFERER => false, - CURLOPT_CERTINFO => false, - CURLOPT_FAILONERROR => false, - CURLOPT_FOLLOWLOCATION => false, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TCP_FASTOPEN => true, - CURLOPT_CONNECTTIMEOUT => 2, - CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, - CURLOPT_TIMEOUT => 5, - CURLOPT_USERAGENT => 'Uiharu/' . UIH_VERSION, - CURLOPT_HTTPHEADER => [ - 'Accept: application/json', - ], - ]); - $eepromResp = curl_exec($curl); - curl_close($curl); - $resp->eeprom_file_info = json_decode($eepromResp); - if(isset($resp->eeprom_file_info->name)) - $resp->title = $resp->eeprom_file_info->name; - if(isset($resp->eeprom_file_info->thumb)) - $resp->image = $resp->eeprom_file_info->thumb; - $resp->site_name = 'Flashii EEPROM'; - } - public function handleGET($response, $request) { if($request->getMethod() === 'HEAD') { $response->setTypeJson(); @@ -124,411 +104,378 @@ final class v1_0 implements \Uiharu\IApi { } if(empty($resp->type)) { - $urlScheme = strtolower($parsedUrl->getScheme()); - $urlHost = strtolower($parsedUrl->getHost()); - $urlPath = '/' . trim($parsedUrl->getPath(), '/'); + $lookup = $this->ctx->matchLookup($parsedUrl); - if($urlScheme === 'eeprom') { - if(preg_match('#^([A-Za-z0-9-_]+)$#', $parsedUrl->getPath(), $matches)) { - $parsedUrl = Url::parse('https://i.fii.moe/' . $matches[1]); - $resp->uri = $parsedUrl->toV1(); - $continueRaw = true; - $this->eepromLookup($resp, $matches[1]); - } - } elseif($urlScheme === 'devrom') { - if(preg_match('#^([A-Za-z0-9-_]+)$#', $parsedUrl->getPath(), $matches)) { - $parsedUrl = Url::parse('https://i.edgii.net/' . $matches[1]); - $resp->uri = $parsedUrl->toV1(); - $continueRaw = true; - $this->eepromLookup($resp, $matches[1], 'edgii'); - } - } elseif($urlScheme === 'http' || $urlScheme === 'https') { - switch($urlHost) { - case 'i.flashii.net': - case 'i.fii.moe': - $eepromFileId = substr($urlPath, 1); - case 'eeprom.flashii.net': - if(!isset($eepromFileId) && preg_match('#^/uploads/([A-Za-z0-9-_]+)/?$#', $urlPath, $matches)) - $eepromFileId = $matches[1]; + if($lookup !== null) { + try { + $result = $lookup->lookup($parsedUrl); - if(!empty($eepromFileId)) { - $continueRaw = true; - $this->eepromLookup($resp, $eepromFileId); - } - break; + $resp->uri = $result->getUrl()->toV1(); + $resp->type = $result->getObjectType(); - case 'i.edgii.net': - $eepromFileId = substr($urlPath, 1); - case 'eeprom.edgii.net': - if(!isset($eepromFileId) && preg_match('#^/uploads/([A-Za-z0-9-_]+)/?$#', $urlPath, $matches)) - $eepromFileId = $matches[1]; + if($result->hasMediaType()) + $resp->content_type = MediaTypeExts::toV1($result->getMediaType()); + if($result->hasColour()) + $resp->color = Colour::toHexString($result->getColour()); + if($result->hasTitle()) + $resp->title = $result->getTitle(); + if($result->hasSiteName()) + $resp->site_name = $result->getSiteName(); + if($result->hasDescription()) + $resp->description = $result->getDescription(); + if($result->hasPreviewImage()) + $resp->image = $result->getPreviewImage(); - if(!empty($eepromFileId)) { - $continueRaw = true; - $this->eepromLookup($resp, $eepromFileId, 'edgii'); - } - break; + if($result instanceof TwitterLookupResult) { + if($result instanceof TwitterLookupTweetResult) + $resp->tweet_id = $result->getTwitterTweetId(); - case 'twitter.com': case 'www.twitter.com': - case 'm.twitter.com': case 'mobile.twitter.com': - case 'nitter.net': case 'www.nitter.net': - if(preg_match('#^/@?(?:[A-Za-z0-9_]{1,20})/status(?:es)?/([0-9]+)/?$#', $urlPath, $matches)) { - $resp->type = 'twitter:tweet'; - $resp->color = '#1da1f2'; - $resp->tweet_id = strval($matches[1] ?? '0'); - $curl = curl_init("https://api.twitter.com/2/tweets?ids={$resp->tweet_id}&expansions=attachments.media_keys,author_id,entities.mentions.username,referenced_tweets.id,referenced_tweets.id.author_id&media.fields=height,width,media_key,preview_image_url,url,type&tweet.fields=attachments,conversation_id,text,source,possibly_sensitive,created_at&user.fields=id,name,profile_image_url,protected,username,verified"); - curl_setopt_array($curl, [ - CURLOPT_AUTOREFERER => false, - CURLOPT_CERTINFO => false, - CURLOPT_FAILONERROR => false, - CURLOPT_FOLLOWLOCATION => false, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TCP_FASTOPEN => true, - CURLOPT_CONNECTTIMEOUT => 2, - CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, - CURLOPT_TIMEOUT => 5, - CURLOPT_USERAGENT => 'Uiharu/' . UIH_VERSION, - CURLOPT_HTTPHEADER => [ - 'Authorization: Bearer ' . Config::get('Twitter', 'apiToken'), - 'Accept: application/json', - ], - ]); - $tweetResp = curl_exec($curl); - curl_close($curl); - $resp->tweet_info = json_decode($tweetResp); - if(isset($resp->tweet_info->includes->users[0]->name)) - $resp->title = $resp->tweet_info->includes->users[0]->name; - if(isset($resp->tweet_info->includes->users[0]->profile_image_url)) - $resp->image = $resp->tweet_info->includes->users[0]->profile_image_url; - if(isset($resp->tweet_info->data[0]->text)) - $resp->description = $resp->tweet_info->data[0]->text; - $resp->site_name = 'Twitter'; - break; + if($result instanceof TwitterLookupUserResult) + $resp->twitter_user_name = $result->getTwitterUserName(); + + if(UIH_DEBUG) + $resp->dbg_twitter_info = $result->getTwitterResult(); + } + + if($result instanceof IHasMediaInfo) { + if($result->isMedia()) { + $resp->is_image = $result->isImage(); + $resp->is_audio = $result->isAudio(); + $resp->is_video = $result->isVideo(); + + if($result->hasDimensions()) { + $resp->width = $result->getWidth(); + $resp->height = $result->getHeight(); + } + + $resp->media = new stdClass; + $resp->media->confidence = $result->getConfidence(); + + if($result->hasAspectRatio()) + $resp->media->aspect_ratio = $result->getAspectRatio(); + if($result->hasDuration()) + $resp->media->duration = $result->getDuration(); + if($result->hasSize()) + $resp->media->size = $result->getSize(); + if($result->hasBitRate()) + $resp->media->bitrate = $result->getBitRate(); } - if(preg_match('#^/@?([A-Za-z0-9_]{1,20})/?$#', $urlPath, $matches)) { - $resp->type = 'twitter:user'; - $resp->color = '#1da1f2'; - $resp->twitter_user_name = strval($matches[1] ?? ''); - $curl = curl_init("https://api.twitter.com/2/users/by?usernames={$resp->twitter_user_name}&user.fields=description,entities,id,name,profile_image_url,protected,url,username,verified"); - curl_setopt_array($curl, [ - CURLOPT_AUTOREFERER => false, - CURLOPT_CERTINFO => false, - CURLOPT_FAILONERROR => false, - CURLOPT_FOLLOWLOCATION => false, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TCP_FASTOPEN => true, - CURLOPT_CONNECTTIMEOUT => 2, - CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, - CURLOPT_TIMEOUT => 5, - CURLOPT_USERAGENT => 'Uiharu/' . UIH_VERSION, - CURLOPT_HTTPHEADER => [ - 'Authorization: Bearer ' . Config::get('Twitter', 'apiToken'), - 'Accept: application/json', - ], - ]); - $twitUserResp = curl_exec($curl); - curl_close($curl); - $resp->twitter_user_info = json_decode($twitUserResp); - if(isset($resp->twitter_user_info->data[0]->name)) - $resp->title = $resp->twitter_user_info->data[0]->name; - if(isset($resp->twitter_user_info->data[0]->profile_image_url)) - $resp->image = $resp->twitter_user_info->data[0]->profile_image_url; - if(isset($resp->twitter_user_info->data[0]->description)) - $resp->description = $resp->twitter_user_info->data[0]->description; - $resp->site_name = 'Twitter'; - break; + if($result instanceof EEPROMLookupResult) { + $resp->eeprom_file_id = $result->getEEPROMId(); + $resp->eeprom_file_info = $result->getEEPROMInfo(); } - break; - case 'youtu.be': case 'www.youtu.be': // www. doesn't work for this, but may as well cover it - $youtubeVideoId = substr($urlPath, 1); - case 'youtube.com': case 'www.youtube.com': - case 'youtube-nocookie.com': case 'www.youtube-nocookie.com': - parse_str($parsedUrl->getQuery(), $queryString); - - if(!isset($youtubeVideoId) && $urlPath === '/watch') - $youtubeVideoId = $queryString['v'] ?? null; - - if(!empty($youtubeVideoId)) { - $resp->type = 'youtube:video'; - $resp->color = '#f00'; - $resp->youtube_video_id = $youtubeVideoId; - - if(isset($queryString['t'])) - $resp->youtube_start_time = $queryString['t']; - if(isset($queryString['list'])) - $resp->youtube_playlist = $queryString['list']; - if(isset($queryString['index'])) - $resp->youtube_playlist_index = $queryString['index']; - - $curl = curl_init("https://www.googleapis.com/youtube/v3/videos?part=snippet%2CcontentDetails%2Cstatistics&id={$resp->youtube_video_id}&key=" . Config::get('Google', 'apiKey')); - curl_setopt_array($curl, [ - CURLOPT_AUTOREFERER => false, - CURLOPT_CERTINFO => false, - CURLOPT_FAILONERROR => false, - CURLOPT_FOLLOWLOCATION => false, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TCP_FASTOPEN => true, - CURLOPT_CONNECTTIMEOUT => 2, - CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, - CURLOPT_TIMEOUT => 5, - CURLOPT_USERAGENT => 'Uiharu/' . UIH_VERSION, - CURLOPT_HTTPHEADER => [ - 'Accept: application/json', - ], - ]); - $youtubeResp = curl_exec($curl); - curl_close($curl); - $resp->youtube_video_info = json_decode($youtubeResp); - if(isset($resp->youtube_video_info->items[0]->snippet->title)) - $resp->title = $resp->youtube_video_info->items[0]->snippet->title; - if(isset($resp->youtube_video_info->items[0]->snippet->thumbnails->medium->url)) - $resp->image = $resp->youtube_video_info->items[0]->snippet->thumbnails->medium->url; - if(isset($resp->youtube_video_info->items[0]->snippet->description)) - $resp->description = $resp->youtube_video_info->items[0]->snippet->description; - $resp->site_name = 'YouTube'; - } - break; + if(UIH_DEBUG && $result->hasMediaInfo()) + $resp->dbg_media_info = $result->getMediaInfo(); + } + } catch(Exception $ex) { + $resp->error = 'metadata:lookup'; + if(UIH_DEBUG) { + $resp->dbg_msg = $ex->getMessage(); + $resp->dbg_ex = (string)$ex; + } + $response->setStatusCode(500); + return $resp; } } else { - $resp->error = 'metadata:scheme'; - $response->setStatusCode(400); - return $resp; - } + $urlScheme = strtolower($parsedUrl->getScheme()); + $urlHost = strtolower($parsedUrl->getHost()); + $urlPath = '/' . trim($parsedUrl->getPath(), '/'); - 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($urlScheme === 'http' || $urlScheme === 'https') { + switch($urlHost) { + case 'youtu.be': case 'www.youtu.be': // www. doesn't work for this, but may as well cover it + $youtubeVideoId = substr($urlPath, 1); + case 'youtube.com': case 'www.youtube.com': + case 'youtube-nocookie.com': case 'www.youtube-nocookie.com': + parse_str($parsedUrl->getQuery(), $queryString); - 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] ?? ''); - } + if(!isset($youtubeVideoId) && $urlPath === '/watch') + $youtubeVideoId = $queryString['v'] ?? null; - try { - $contentType = MediaType::parse($headers['content-type'] ?? ''); - } catch(InvalidArgumentException $ex) { - $contentType = MediaType::parse('application/octet-stream'); - } + if(!empty($youtubeVideoId)) { + $resp->type = 'youtube:video'; + $resp->color = '#f00'; + $resp->youtube_video_id = $youtubeVideoId; - $resp->content_type = MediaTypeExts::toV1($contentType); + if(isset($queryString['t'])) + $resp->youtube_start_time = $queryString['t']; + if(isset($queryString['list'])) + $resp->youtube_playlist = $queryString['list']; + if(isset($queryString['index'])) + $resp->youtube_playlist_index = $queryString['index']; - $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; + $curl = curl_init("https://www.googleapis.com/youtube/v3/videos?part=snippet%2CcontentDetails%2Cstatistics&id={$resp->youtube_video_id}&key=" . Config::get('Google', 'apiKey')); + curl_setopt_array($curl, [ + CURLOPT_AUTOREFERER => false, + CURLOPT_CERTINFO => false, + CURLOPT_FAILONERROR => false, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TCP_FASTOPEN => true, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_TIMEOUT => 5, + CURLOPT_USERAGENT => 'Uiharu/' . UIH_VERSION, + CURLOPT_HTTPHEADER => [ + 'Accept: application/json', + ], + ]); + $youtubeResp = curl_exec($curl); + curl_close($curl); + $resp->youtube_video_info = json_decode($youtubeResp); + if(isset($resp->youtube_video_info->items[0]->snippet->title)) + $resp->title = $resp->youtube_video_info->items[0]->snippet->title; + if(isset($resp->youtube_video_info->items[0]->snippet->thumbnails->medium->url)) + $resp->image = $resp->youtube_video_info->items[0]->snippet->thumbnails->medium->url; + if(isset($resp->youtube_video_info->items[0]->snippet->description)) + $resp->description = $resp->youtube_video_info->items[0]->snippet->description; + $resp->site_name = 'YouTube'; } - } + break; + } + } else { + $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 { - $resp->is_image = $isImage = $contentType->matchCategory('image'); - $resp->is_audio = $isAudio = $contentType->matchCategory('audio'); - $resp->is_video = $isVideo = $contentType->matchCategory('video'); + $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] ?? ''); + } - if($isImage || $isAudio || $isVideo) { + 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); - $resp->media = new stdClass; - $ffmpeg = json_decode(shell_exec(sprintf('ffprobe -show_streams -show_format -print_format json -v quiet -i %s', escapeshellarg((string)$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; - } - } + $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(); + } - 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; + $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; - 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)) + 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(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($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($resp->description) && !empty($resp->media->tags->comment)) - $resp->description = $resp->media->tags->comment; + 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); + if($includeRawResult) + $resp->ffmpeg = $ffmpeg; + } else curl_close($curl); + } } } } diff --git a/src/AudioTags.php b/src/AudioTags.php new file mode 100644 index 0000000..2c5b425 --- /dev/null +++ b/src/AudioTags.php @@ -0,0 +1,66 @@ +title !== ''; + } + public function getTitle(): string { + return $this->title; + } + + public function hasArtist(): bool { + return $this->artist !== ''; + } + public function getArtist(): string { + return $this->artist; + } + + public function hasAlbum(): bool { + return $this->album !== ''; + } + public function getAlbum(): string { + return $this->album; + } + + public function hasDate(): bool { + return $this->date !== ''; + } + public function getDate(): string { + return $this->date; + } + + public function hasComment(): bool { + return $this->comment !== ''; + } + public function getComment(): string { + return $this->comment; + } + + public function hasGenre(): bool { + return $this->genre !== ''; + } + public function getGenre(): string { + return $this->genre; + } + + public static function fromMediaInfo(object $obj): AudioTags { + return new AudioTags( + $obj->tagTitle ?? '', + $obj->tagArtist ?? '', + $obj->tagAlbum ?? '', + $obj->tagDate ?? '', + $obj->tagComment ?? '', + $obj->tagGenre ?? '', + ); + } +} diff --git a/src/Colour.php b/src/Colour.php new file mode 100644 index 0000000..ac2c5d3 --- /dev/null +++ b/src/Colour.php @@ -0,0 +1,8 @@ +format)) { + $out->confidence = empty($in->format->probe_score) ? 0 : (intval($in->format->probe_score) / 100); + + if(!empty($in->format->duration)) + $out->duration = floatval($in->format->duration); + + if(!empty($in->format->size)) + $out->size = intval($in->format->size); + + if(!empty($in->format->bit_rate)) + $out->bitRate = intval($int->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->streams)) + foreach($in->streams as $stream) { + $codecType = $stream->codec_type ?? null; + + if($codecType === 'video') { + $out->width = intval($stream->coded_width ?? $stream->width ?? -1); + $out->height = intval($stream->coded_height ?? $stream->height ?? -1); + + if(!empty($stream->display_aspect_ratio)) + $out->aspectRatio = $stream->display_aspect_ratio; + } elseif($codecType === 'audio') { + $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; + } + } + + return $out; + } +} diff --git a/src/IHasMediaInfo.php b/src/IHasMediaInfo.php new file mode 100644 index 0000000..a6ac9ec --- /dev/null +++ b/src/IHasMediaInfo.php @@ -0,0 +1,33 @@ +getScheme() === $this->protocol || ( + $url->isWeb() && ( + in_array($url->getHost(), $this->shortDomains) || ( + $url->getHost() === $this->apiDomain && str_starts_with($url->getPath(), '/uploads') + ) + ) + ); + } + + private function rawLookup(string $fileId): ?object { + $curl = curl_init("https://{$this->apiDomain}/uploads/{$fileId}.json"); + curl_setopt_array($curl, [ + CURLOPT_AUTOREFERER => false, + CURLOPT_CERTINFO => false, + CURLOPT_FAILONERROR => false, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TCP_FASTOPEN => true, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_TIMEOUT => 5, + CURLOPT_USERAGENT => 'Uiharu/' . UIH_VERSION, + CURLOPT_HTTPHEADER => [ + 'Accept: application/json', + ], + ]); + $resp = curl_exec($curl); + curl_close($curl); + return json_decode($resp); + } + + public function lookup(Url $url): EEPROMLookupResult { + if($url->getScheme() === $this->protocol) { + $fileId = $url->getPath(); + } elseif($url->isWeb()) { + if($url->getHost() === $this->apiDomain) { + if(preg_match('#^/uploads/([A-Za-z0-9-_]+)/?$#', $url->getPath(), $matches)) + $fileId = $matches[1]; + } else { + $fileId = substr($url->getPath(), 1); + } + } + + if(!isset($fileId)) + throw new RuntimeException('Was unable to find EEPROM file id.'); + if(!preg_match('#^([A-Za-z0-9-_]+)$#', $fileId)) + throw new RuntimeException('Invalid EEPROM file id format.'); + + $fileInfo = $this->rawLookup($fileId); + if($fileInfo === null) + throw new RuntimeException('EEPROM file does not exist.'); + + $url = Url::parse('https://' . $this->shortDomains[0] . '/' . $fileId); + $mediaType = MediaType::parse($fileInfo->type); + $isMedia = MediaTypeExts::isMedia($mediaType); + $mediaInfo = $isMedia ? FFMPEG::cleanProbe($url) : null; + + return new EEPROMLookupResult($url, $mediaType, $fileInfo, $mediaInfo); + } +} diff --git a/src/Lookup/EEPROMLookupResult.php b/src/Lookup/EEPROMLookupResult.php new file mode 100644 index 0000000..e77ad45 --- /dev/null +++ b/src/Lookup/EEPROMLookupResult.php @@ -0,0 +1,173 @@ +url = $url; + $this->type = $type; + $this->fileInfo = $fileInfo; + $this->mediaInfo = $mediaInfo; + + if($this->isAudio() && $mediaInfo !== null) + $this->audioTags = AudioTags::fromMediaInfo($mediaInfo); + else + $this->audioTags = null; + } + + public function getUrl(): Url { + return $this->url; + } + public function getObjectType(): string { + return 'eeprom:file'; + } + + public function hasMediaType(): bool { + return true; + } + public function getMediaType(): MediaType { + return $this->type; + } + + public function getEEPROMId(): string { + return $this->fileInfo->id; + } + public function getEEPROMInfo(): object { + return $this->fileInfo; + } + + public function hasColour(): bool { + return true; + } + public function getColour(): int { + return 0x8559A5; + } + + public function hasTitle(): bool { + return true; + } + public function getTitle(): string { + if($this->audioTags !== null) { + $title = ''; + + if($this->audioTags->hasArtist()) + $title .= $this->audioTags->getArtist() . ' - '; + + if($this->audioTags->hasTitle()) + $title .= $this->audioTags->getTitle(); + + if($this->audioTags->hasDate()) + $title .= ' (' . $this->audioTags->getDate() . ')'; + + $title = trim($title, " \n\r\t\v\x00()-"); + if(!empty($title)) + return $title; + } + + return $this->fileInfo->name; + } + + public function hasSiteName(): bool { + return true; + } + public function getSiteName(): string { + return 'EEPROM'; + } + + public function hasDescription(): bool { + return $this->audioTags !== null && $this->audioTags->hasComment(); + } + public function getDescription(): string { + return $this->audioTags->getComment(); + } + + public function hasPreviewImage(): bool { + return true; + } + public function getPreviewImage(): string { + return $this->fileInfo->thumb; + } + + public function getConfidence(): float { + return $this->mediaInfo->confidence ?? 0; + } + + public function isMedia(): bool { + return $this->mediaInfo !== null; + } + public function isImage(): bool { + return $this->type->matchCategory('image'); + } + public function isVideo(): bool { + return $this->type->matchCategory('video'); + } + public function isAudio(): bool { + return $this->type->matchCategory('audio'); + } + + public function hasDimensions(): bool { + return $this->isMedia(); + } + public function getWidth(): int { + return $this->mediaInfo->width ?? 0; + } + public function getHeight(): int { + return $this->mediaInfo->height ?? 0; + } + + public function hasAspectRatio(): bool { + return isset($this->mediaInfo->aspectRatio); + } + public function getAspectRatio(): string { + return $this->mediaInfo->aspectRatio; + } + + public function hasDuration(): bool { + return isset($this->mediaInfo->duration); + } + public function getDuration(): float { + return $this->mediaInfo->duration; + } + + public function hasSize(): bool { + return isset($this->mediaInfo->size); + } + public function getSize(): int { + return $this->mediaInfo->size; + } + + public function hasBitRate(): bool { + return isset($this->mediaInfo->bitRate); + } + public function getBitRate(): int { + return $this->mediaInfo->bitRate; + } + + public function hasAudioTags(): bool { + return $this->audioTags !== null; + } + public function getAudioTags(): AudioTags { + return $this->audioTags; + } + + public function hasMediaInfo(): bool { + return $this->mediaInfo !== null; + } + public function getMediaInfo(): object { + return $this->mediaInfo; + } +} diff --git a/src/Lookup/TwitterLookup.php b/src/Lookup/TwitterLookup.php new file mode 100644 index 0000000..4b78f49 --- /dev/null +++ b/src/Lookup/TwitterLookup.php @@ -0,0 +1,89 @@ +getHost(), self::TWITTER_DOMAINS)) + return false; + + return preg_match('#^/@?(?:[A-Za-z0-9_]{1,20})/status(?:es)?/([0-9]+)/?$#', $url->getPath()) + || preg_match('#^/@?([A-Za-z0-9_]{1,20})/?$#', $url->getPath()); + } + + private function lookupUser(string $userName): ?object { + $curl = curl_init("https://api.twitter.com/2/users/by?usernames={$userName}&user.fields=description,entities,id,name,profile_image_url,protected,url,username,verified"); + curl_setopt_array($curl, [ + CURLOPT_AUTOREFERER => false, + CURLOPT_CERTINFO => false, + CURLOPT_FAILONERROR => false, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TCP_FASTOPEN => true, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_TIMEOUT => 5, + CURLOPT_USERAGENT => 'Uiharu/' . UIH_VERSION, + CURLOPT_HTTPHEADER => [ + 'Authorization: Bearer ' . Config::get('Twitter', 'apiToken'), + 'Accept: application/json', + ], + ]); + $resp = curl_exec($curl); + curl_close($curl); + return json_decode($resp); + } + + private function lookupTweet(string $tweetId): ?object { + $curl = curl_init("https://api.twitter.com/2/tweets?ids={$tweetId}&expansions=attachments.media_keys,author_id,entities.mentions.username,referenced_tweets.id,referenced_tweets.id.author_id&media.fields=height,width,media_key,preview_image_url,url,type&tweet.fields=attachments,conversation_id,text,source,possibly_sensitive,created_at&user.fields=id,name,profile_image_url,protected,username,verified"); + curl_setopt_array($curl, [ + CURLOPT_AUTOREFERER => false, + CURLOPT_CERTINFO => false, + CURLOPT_FAILONERROR => false, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TCP_FASTOPEN => true, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_TIMEOUT => 5, + CURLOPT_USERAGENT => 'Uiharu/' . UIH_VERSION, + CURLOPT_HTTPHEADER => [ + 'Authorization: Bearer ' . Config::get('Twitter', 'apiToken'), + 'Accept: application/json', + ], + ]); + $resp = curl_exec($curl); + curl_close($curl); + return json_decode($resp); + } + + public function lookup(Url $url): TwitterLookupResult { + if(preg_match('#^/@?(?:[A-Za-z0-9_]{1,20})/status(?:es)?/([0-9]+)/?$#', $url->getPath(), $matches)) { + $tweetId = strval($matches[1] ?? '0'); + $tweetInfo = $this->lookupTweet($tweetId); + if($tweetInfo === null) + throw new RuntimeException('Tweet lookup failed.'); + return new TwitterLookupTweetResult($url, $tweetInfo); + } + + if(preg_match('#^/@?([A-Za-z0-9_]{1,20})/?$#', $url->getPath(), $matches)) { + $userName = strval($matches[1] ?? ''); + $userInfo = $this->lookupUser($userName); + if($userInfo === null) + throw new RuntimeException('Twitter user lookup failed.'); + return new TwitterLookupUserResult($url, $userInfo); + } + + throw new RuntimeException('Unknown Twitter URL format.'); + } +} diff --git a/src/Lookup/TwitterLookupResult.php b/src/Lookup/TwitterLookupResult.php new file mode 100644 index 0000000..3b1db64 --- /dev/null +++ b/src/Lookup/TwitterLookupResult.php @@ -0,0 +1,51 @@ +url = $url; + } + + public function getUrl(): Url { + return $this->url; + } + public abstract function getObjectType(): string; + + public function hasMediaType(): bool { + return false; + } + public function getMediaType(): MediaType { + throw new RuntimeException('Unsupported'); + } + + public function hasColour(): bool { + return true; + } + public function getColour(): int { + return 0x1DA1F2; + } + + public abstract function hasTitle(): bool; + public abstract function getTitle(): string; + + public function hasSiteName(): bool { + return true; + } + public function getSiteName(): string { + return 'Twitter'; + } + + public abstract function hasDescription(): bool; + public abstract function getDescription(): string; + + public abstract function hasPreviewImage(): bool; + public abstract function getPreviewImage(): string; + + public abstract function getTwitterResult(): object; +} diff --git a/src/Lookup/TwitterLookupTweetResult.php b/src/Lookup/TwitterLookupTweetResult.php new file mode 100644 index 0000000..a036e7e --- /dev/null +++ b/src/Lookup/TwitterLookupTweetResult.php @@ -0,0 +1,46 @@ +tweetInfo = $tweetInfo; + } + + public function getObjectType(): string { + return 'twitter:tweet'; + } + + public function getTwitterTweetId(): string { + return $this->tweetInfo->data[0]->id; + } + + public function hasTitle(): bool { + return isset($this->tweetInfo->includes->users[0]->name); + } + public function getTitle(): string { + return $this->tweetInfo->includes->users[0]->name; + } + + public function hasDescription(): bool { + return isset($this->tweetInfo->data[0]->text); + } + public function getDescription(): string { + return $this->tweetInfo->data[0]->text; + } + + public function hasPreviewImage(): bool { + return isset($this->tweetInfo->includes->users[0]->profile_image_url); + } + public function getPreviewImage(): string { + return $this->tweetInfo->includes->users[0]->profile_image_url; + } + + public function getTwitterResult(): object { + return $this->tweetInfo; + } +} diff --git a/src/Lookup/TwitterLookupUserResult.php b/src/Lookup/TwitterLookupUserResult.php new file mode 100644 index 0000000..13d8f6f --- /dev/null +++ b/src/Lookup/TwitterLookupUserResult.php @@ -0,0 +1,46 @@ +userInfo = $userInfo; + } + + public function getObjectType(): string { + return 'twitter:user'; + } + + public function getTwitterUserName(): string { + return $this->userInfo->data[0]->username; + } + + public function hasTitle(): bool { + return isset($this->userInfo->data[0]->name); + } + public function getTitle(): string { + return $this->userInfo->data[0]->name; + } + + public function hasDescription(): bool { + return isset($this->userInfo->data[0]->description); + } + public function getDescription(): string { + return $this->userInfo->data[0]->description; + } + + public function hasPreviewImage(): bool { + return isset($this->userInfo->data[0]->profile_image_url); + } + public function getPreviewImage(): string { + return $this->userInfo->data[0]->profile_image_url; + } + + public function getTwitterResult(): object { + return $this->userInfo; + } +} diff --git a/src/MediaTypeExts.php b/src/MediaTypeExts.php index 81a410a..8322bb8 100644 --- a/src/MediaTypeExts.php +++ b/src/MediaTypeExts.php @@ -19,4 +19,10 @@ final class MediaTypeExts { return $parts; } + + public static function isMedia(MediaType $mediaType): bool { + return $mediaType->matchCategory('image') + || $mediaType->matchCategory('audio') + || $mediaType->matchCategory('video'); + } } diff --git a/src/UihContext.php b/src/UihContext.php index 133b14b..9eb69a8 100644 --- a/src/UihContext.php +++ b/src/UihContext.php @@ -8,6 +8,7 @@ final class UihContext { private IDbConnection $database; private HttpFx $router; private array $apis = []; + private array $lookups = []; public function __construct(IDbConnection $database) { $this->database = $database; @@ -81,4 +82,15 @@ final class UihContext { break; } } + + public function registerLookup(ILookup $lookup): void { + $this->lookups[] = $lookup; + } + + public function matchLookup(Url $url): ?ILookup { + foreach($this->lookups as $lookup) + if($lookup->match($url)) + return $lookup; + return null; + } } diff --git a/src/Url.php b/src/Url.php index 8595359..a3f2dd0 100644 --- a/src/Url.php +++ b/src/Url.php @@ -17,7 +17,7 @@ final class Url { public function __construct(array $parts) { if(isset($parts['scheme'])) - $this->scheme = $parts['scheme']; + $this->scheme = strtolower($parts['scheme']); if(isset($parts['host'])) $this->host = $parts['host']; if(isset($parts['port'])) @@ -136,6 +136,17 @@ final class Url { return hash('sha256', (string)$this, $raw); } + public function isHTTP(): bool { + return $this->scheme === 'http'; + } + public function isHTTPS(): bool { + return $this->scheme === 'https'; + } + public function isWeb(): bool { + return $this->scheme === 'https' + || $this->scheme === 'http'; + } + public function __toString(): string { if($this->formatted === null) { $string = '';