diff --git a/public/index.php b/public/index.php index c05f955..31a4476 100644 --- a/public/index.php +++ b/public/index.php @@ -12,6 +12,7 @@ if(UIH_DEBUG) $ctx->registerLookup(new \Uiharu\Lookup\TwitterLookup); $ctx->registerLookup(new \Uiharu\Lookup\YouTubeLookup); +$ctx->registerLookup(new \Uiharu\Lookup\NicoNicoLookup); // this should always come AFTER other lookups involved http(s) $ctx->registerLookup(new \Uiharu\Lookup\WebLookup); diff --git a/src/Apis/v1_0.php b/src/Apis/v1_0.php index 023f039..73edc6c 100644 --- a/src/Apis/v1_0.php +++ b/src/Apis/v1_0.php @@ -17,6 +17,7 @@ use Uiharu\Lookup\TwitterLookupResult; use Uiharu\Lookup\TwitterLookupTweetResult; use Uiharu\Lookup\TwitterLookupUserResult; use Uiharu\Lookup\YouTubeLookupResult; +use Uiharu\Lookup\NicoNicoLookupResult; use Index\MediaType; use Index\Data\IDbConnection; use Index\Http\HttpFx; @@ -154,6 +155,14 @@ final class v1_0 implements \Uiharu\IApi { } } + if($result instanceof NicoNicoLookupResult) { + $resp->nicovideo_video_id = $result->getNicoNicoVideoId(); + + if(UIH_DEBUG) { + $resp->dbg_nicovideo_thumb_info = $result->getNicoNicoThumbInfo()->ownerDocument->saveXML(); + } + } + if($result instanceof IHasMediaInfo) { if($result->isMedia()) { $resp->is_image = $result->isImage(); diff --git a/src/Lookup/NicoNicoLookup.php b/src/Lookup/NicoNicoLookup.php new file mode 100644 index 0000000..24d8a7b --- /dev/null +++ b/src/Lookup/NicoNicoLookup.php @@ -0,0 +1,63 @@ +isWeb() || ($url->getHost() !== 'www.nicovideo.jp' && $url->getHost() !== 'nicovideo.jp')) + return false; + + if(str_starts_with($url->getPath(), '/watch/sm')) + return true; + + return false; + } + + public function lookup(Url $url): NicoNicoLookupResult { + $videoId = explode('/', trim($url->getPath(), '/'))[1] ?? ''; + if(empty($videoId)) + throw new RuntimeException('Nico Nico Douga video id missing.'); + + $thumbDoc = self::lookupThumbInfo($videoId); + + $thumbResp = $thumbDoc->getElementsByTagName('nicovideo_thumb_response')[0] ?? null; + if(empty($thumbResp) || !$thumbResp->hasAttribute('status') || $thumbResp->getAttribute('status') !== 'ok') + throw new RuntimeException('Nico Nico Douga video with the given id could not be found.'); + + $thumbInfo = $thumbResp->getElementsByTagName('thumb')[0] ?? null; + if(empty($thumbInfo)) + throw new RuntimeException('Nico Nico Douga thumb info missing from API result????'); + + return new NicoNicoLookupResult($url, $videoId, $thumbInfo); + } + + private static function lookupThumbInfo(string $videoId): DOMDocument { + $curl = curl_init("https://ext.nicovideo.jp/api/getthumbinfo/{$videoId}"); + curl_setopt_array($curl, [ + CURLOPT_AUTOREFERER => false, + CURLOPT_CERTINFO => false, + CURLOPT_FAILONERROR => false, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TCP_FASTOPEN => true, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_TIMEOUT => 5, + CURLOPT_USERAGENT => 'Uiharu/' . UIH_VERSION, + ]); + $resp = curl_exec($curl); + curl_close($curl); + + if(empty($resp)) + throw new RuntimeException('Nico Nico Douga API request failed.'); + + $doc = new DOMDocument; + if(!$doc->loadXML($resp)) + throw new RuntimeException('Failed to parse Nico Nico Douga API response.'); + + return $doc; + } +} diff --git a/src/Lookup/NicoNicoLookupResult.php b/src/Lookup/NicoNicoLookupResult.php new file mode 100644 index 0000000..1c82311 --- /dev/null +++ b/src/Lookup/NicoNicoLookupResult.php @@ -0,0 +1,80 @@ +url; + } + public function getObjectType(): string { + return 'niconico:video'; + } + + public function getNicoNicoVideoId(): string { + return $this->videoId; + } + public function getNicoNicoThumbInfo(): DOMElement { + return $this->thumbInfo; + } + + 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 0x252525; + } + + public function hasTitle(): bool { + if($this->title === null) + $this->title = $this->thumbInfo->getElementsByTagName('title')[0] ?? false; + return $this->title !== false; + } + public function getTitle(): string { + return $this->hasTitle() ? trim($this->title->textContent) : 'No Title'; + } + + public function hasSiteName(): bool { + return true; + } + public function getSiteName(): string { + return 'ニコニコ動画'; + } + + public function hasDescription(): bool { + if($this->description === null) + $this->description = $this->thumbInfo->getElementsByTagName('description')[0] ?? false; + return $this->description !== false && !empty($this->description->textContent); + } + public function getDescription(): string { + return $this->hasDescription() ? trim($this->description->textContent) : ''; + } + + public function hasPreviewImage(): bool { + if($this->previewImage === null) + $this->previewImage = $this->thumbInfo->getElementsByTagName('thumbnail_url')[0] ?? false; + return $this->previewImage !== false && !empty($this->previewImage->textContent); + } + public function getPreviewImage(): string { + return $this->hasPreviewImage() ? (trim($this->previewImage->textContent) . '.L') : ''; + } +} diff --git a/src/Lookup/YouTubeLookup.php b/src/Lookup/YouTubeLookup.php index a90926d..ea2140f 100644 --- a/src/Lookup/YouTubeLookup.php +++ b/src/Lookup/YouTubeLookup.php @@ -4,7 +4,6 @@ namespace Uiharu\Lookup; use RuntimeException; use Uiharu\Config; use Uiharu\Url; -use Index\MediaType; final class YouTubeLookup implements \Uiharu\ILookup { private const SHORT_DOMAINS = [ diff --git a/uiharu.php b/uiharu.php index 89fcbc1..e45ebcf 100644 --- a/uiharu.php +++ b/uiharu.php @@ -12,7 +12,7 @@ define('UIH_DEBUG', is_file(UIH_ROOT . '/.debug')); define('UIH_PUBLIC', UIH_ROOT . '/public'); define('UIH_SOURCE', UIH_ROOT . '/src'); define('UIH_LIBRARY', UIH_ROOT . '/lib'); -define('UIH_VERSION', '20220716'); +define('UIH_VERSION', '20230125'); require_once UIH_LIBRARY . '/index/index.php';