Compare commits
2 commits
2c265fa702
...
956402ca97
Author | SHA1 | Date | |
---|---|---|---|
956402ca97 | |||
792cf74367 |
28 changed files with 1034 additions and 3595 deletions
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"require": {
|
||||
"flashwave/index": "^0.2410",
|
||||
"sentry/sdk": "^4.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Uiharu\\": "src"
|
||||
}
|
||||
}
|
||||
}
|
1359
composer.lock
generated
1359
composer.lock
generated
File diff suppressed because it is too large
Load diff
955
package-lock.json
generated
955
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"cheerio": "^1.0.0"
|
||||
"cheerio": "^1.0.0",
|
||||
"express": "^5.0.1",
|
||||
"memcache-client": "^1.0.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu;
|
||||
|
||||
require_once __DIR__ . '/../uiharu.php';
|
||||
|
||||
$ctx->registerLookup(new \Uiharu\Lookup\EEPROMLookup('eeprom', 'eeprom.flashii.net', ['i.fii.moe', 'i.flashii.net']));
|
||||
if(UIH_DEBUG)
|
||||
$ctx->registerLookup(new \Uiharu\Lookup\EEPROMLookup('devrom', 'eeprom.edgii.net', ['i.edgii.net']));
|
||||
|
||||
$ctx->registerLookup(new \Uiharu\Lookup\YouTubeLookup($cfg->scopeTo('google')));
|
||||
$ctx->registerLookup(new \Uiharu\Lookup\NicoNicoLookup);
|
||||
|
||||
// this should always come AFTER other lookups involved http(s)
|
||||
$ctx->registerLookup(new \Uiharu\Lookup\WebLookup);
|
||||
|
||||
$ctx->setupHttp();
|
||||
|
||||
$ctx->registerApi(new \Uiharu\Apis\v1_0($ctx));
|
||||
$ctx->matchApi(filter_input(INPUT_SERVER, 'REQUEST_URI'));
|
||||
|
||||
$ctx->dispatchHttp();
|
|
@ -1,349 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu\Apis;
|
||||
|
||||
use stdClass;
|
||||
use DOMDocument;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use Uiharu\Config;
|
||||
use Uiharu\FFMPEG;
|
||||
use Uiharu\IHasMediaInfo;
|
||||
use Uiharu\MediaTypeExts;
|
||||
use Uiharu\UihContext;
|
||||
use Uiharu\Url;
|
||||
use Uiharu\Lookup\EEPROMLookupResult;
|
||||
use Uiharu\Lookup\YouTubeLookupResult;
|
||||
use Uiharu\Lookup\NicoNicoLookupResult;
|
||||
use Index\MediaType;
|
||||
use Index\Cache\CacheProvider;
|
||||
use Index\Colour\{Colour,ColourRgb};
|
||||
use Index\Http\Routing\{HttpGet,HttpPost,RouteHandlerTrait};
|
||||
use Index\Performance\Stopwatch;
|
||||
|
||||
final class v1_0 implements \Uiharu\IApi {
|
||||
use RouteHandlerTrait;
|
||||
|
||||
private UihContext $ctx;
|
||||
private CacheProvider $cache;
|
||||
|
||||
public function __construct(UihContext $ctx) {
|
||||
$this->ctx = $ctx;
|
||||
$this->cache = $ctx->getCache();
|
||||
}
|
||||
|
||||
public function match(string $url): string {
|
||||
return !str_starts_with($url, '/v');
|
||||
}
|
||||
|
||||
#[HttpGet('/metadata/thumb/audio')]
|
||||
public function getThumbAudio($response, $request) {
|
||||
$targetUrl = (string)$request->getParam('url');
|
||||
|
||||
if(empty($targetUrl))
|
||||
return 400;
|
||||
|
||||
try {
|
||||
$parsedUrl = Url::parse($targetUrl);
|
||||
} catch(InvalidArgumentException $ex) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
if(!$parsedUrl->isWeb())
|
||||
return 400;
|
||||
|
||||
$response->setContentType('image/png');
|
||||
$response->setCacheControl('public', 'max-age=31536000', 'immutable');
|
||||
$response->setContent(FFMPEG::grabFirstAudioCover($parsedUrl));
|
||||
}
|
||||
|
||||
#[HttpGet('/metadata/thumb/video')]
|
||||
public function getThumbVideo($response, $request) {
|
||||
$targetUrl = (string)$request->getParam('url');
|
||||
|
||||
if(empty($targetUrl))
|
||||
return 400;
|
||||
|
||||
try {
|
||||
$parsedUrl = Url::parse($targetUrl);
|
||||
} catch(InvalidArgumentException $ex) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
if(!$parsedUrl->isWeb())
|
||||
return 400;
|
||||
|
||||
$response->setContentType('image/png');
|
||||
$response->setCacheControl('public', 'max-age=31536000', 'immutable');
|
||||
$response->setContent(FFMPEG::grabFirstVideoFrame($parsedUrl));
|
||||
}
|
||||
|
||||
#[HttpGet('/metadata')]
|
||||
public function getMetadata($response, $request) {
|
||||
if($request->getMethod() === 'HEAD') {
|
||||
$response->setTypeJson();
|
||||
return;
|
||||
}
|
||||
|
||||
return $this->handleMetadata(
|
||||
$response, $request,
|
||||
(string)$request->getParam('url')
|
||||
);
|
||||
}
|
||||
|
||||
#[HttpPost('/metadata')]
|
||||
public function postMetadata($response, $request) {
|
||||
if(!$request->isStringContent())
|
||||
return 400;
|
||||
|
||||
return $this->handleMetadata(
|
||||
$response, $request,
|
||||
(string)$request->getContent()
|
||||
);
|
||||
}
|
||||
|
||||
#[HttpGet('/metadata/batch')]
|
||||
public function getMetadataBatch($response, $request) {
|
||||
if($request->getMethod() === 'HEAD') {
|
||||
$response->setTypeJson();
|
||||
return;
|
||||
}
|
||||
|
||||
return $this->handleMetadataBatch(
|
||||
$response, $request,
|
||||
$request->getParam('url', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY)
|
||||
);
|
||||
}
|
||||
|
||||
#[HttpPost('/metadata/batch')]
|
||||
public function postMetadataBatch($response, $request) {
|
||||
if(!$request->isFormContent())
|
||||
return 400;
|
||||
|
||||
return $this->handleMetadataBatch(
|
||||
$response, $request,
|
||||
$request->getContent()->getParam('url', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY)
|
||||
);
|
||||
}
|
||||
|
||||
private function metadataLookup(string $targetUrl) {
|
||||
$sw = Stopwatch::startNew();
|
||||
$resp = new stdClass;
|
||||
|
||||
if(empty($targetUrl))
|
||||
return 400;
|
||||
|
||||
try {
|
||||
$parsedUrl = Url::parse($targetUrl);
|
||||
} catch(InvalidArgumentException $ex) {
|
||||
$resp->status = 400;
|
||||
$resp->error = 'metadata:uri';
|
||||
return $resp;
|
||||
}
|
||||
|
||||
// if no scheme is specified, try https
|
||||
if(!$parsedUrl->hasScheme())
|
||||
$parsedUrl->setScheme('https');
|
||||
|
||||
$resp->url = (string)$parsedUrl;
|
||||
$resp->cached = false;
|
||||
|
||||
$urlHash = $parsedUrl->calculateHash(false);
|
||||
$cacheKey = sprintf('uiharu:metadata:%s', $urlHash);
|
||||
|
||||
$cacheResp = $this->cache->get($cacheKey);
|
||||
if(is_string($cacheResp)) {
|
||||
$cacheResp = json_decode($cacheResp);
|
||||
if($cacheResp !== null) {
|
||||
$resp = $cacheResp;
|
||||
$resp->cached = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(empty($resp->type)) {
|
||||
$lookup = $this->ctx->matchLookup($parsedUrl);
|
||||
|
||||
if($lookup !== null)
|
||||
try {
|
||||
$result = $lookup->lookup($parsedUrl);
|
||||
|
||||
$resp->type = $result->getObjectType();
|
||||
|
||||
if($result->hasMediaType())
|
||||
$resp->content_type = MediaTypeExts::toV1($result->getMediaType());
|
||||
if($result->hasColour()) {
|
||||
$colour = $result->getColour();
|
||||
if($colour->getAlpha() < 1.0)
|
||||
$colour = new ColourRgb(
|
||||
$colour->getRed(),
|
||||
$colour->getGreen(),
|
||||
$colour->getBlue()
|
||||
);
|
||||
|
||||
$resp->color = (string)$colour;
|
||||
}
|
||||
if($result->hasTitle())
|
||||
$resp->title = $result->getTitle();
|
||||
if($result->hasSiteName())
|
||||
$resp->site_name = $result->getSiteName();
|
||||
if($result->hasDescription())
|
||||
$resp->description = $result->getDescription();
|
||||
if($result->hasPreviewImage())
|
||||
$resp->image = $result->getPreviewImage();
|
||||
|
||||
if($result instanceof YouTubeLookupResult) {
|
||||
$resp->youtube_video_id = $result->getYouTubeVideoId();
|
||||
|
||||
if($result->hasYouTubeVideoStartTime())
|
||||
$resp->youtube_start_time = $result->getYouTubeVideoStartTime();
|
||||
if($result->hasYouTubePlayListId())
|
||||
$resp->youtube_playlist = $result->getYouTubePlayListId();
|
||||
if($result->hasYouTubePlayListIndex())
|
||||
$resp->youtube_playlist_index = $result->getYouTubePlayListIndex();
|
||||
|
||||
if(UIH_DEBUG) {
|
||||
$resp->dbg_youtube_info = $result->getYouTubeVideoInfo();
|
||||
$resp->dbg_youtube_query = $result->getYouTubeUrlQuery();
|
||||
}
|
||||
}
|
||||
|
||||
if($result instanceof NicoNicoLookupResult) {
|
||||
$resp->nicovideo_video_id = $result->getNicoNicoVideoId();
|
||||
|
||||
if($result->hasNicoNicoVideoStartTime())
|
||||
$resp->nicovideo_start_time = $result->getNicoNicoVideoStartTime();
|
||||
|
||||
if(UIH_DEBUG) {
|
||||
$resp->dbg_nicovideo_thumb_info = $result->getNicoNicoThumbInfo()->ownerDocument->saveXML();
|
||||
$resp->dbg_nicovideo_query = $result->getNicoNicoUrlQuery();
|
||||
}
|
||||
}
|
||||
|
||||
if($result instanceof IHasMediaInfo) {
|
||||
if($result->isMedia()) {
|
||||
$resp->is_image = $result->isImage();
|
||||
$resp->is_audio = $result->isAudio();
|
||||
$resp->is_video = $result->isVideo();
|
||||
|
||||
if($result->hasDimensions()) {
|
||||
$resp->width = $result->getWidth();
|
||||
$resp->height = $result->getHeight();
|
||||
}
|
||||
|
||||
$resp->media = new stdClass;
|
||||
$resp->media->confidence = $result->getConfidence();
|
||||
|
||||
if($result->hasAspectRatio())
|
||||
$resp->media->aspect_ratio = $result->getAspectRatio();
|
||||
if($result->hasDuration())
|
||||
$resp->media->duration = $result->getDuration();
|
||||
if($result->hasSize())
|
||||
$resp->media->size = $result->getSize();
|
||||
if($result->hasBitRate())
|
||||
$resp->media->bitrate = $result->getBitRate();
|
||||
|
||||
if($result->hasAudioTags()) {
|
||||
$audioTags = $result->getAudioTags();
|
||||
$resp->media->tags = new stdClass;
|
||||
if($audioTags->hasTitle())
|
||||
$resp->media->tags->title = $audioTags->getTitle();
|
||||
if($audioTags->hasArtist())
|
||||
$resp->media->tags->artist = $audioTags->getArtist();
|
||||
if($audioTags->hasAlbum())
|
||||
$resp->media->tags->album = $audioTags->getAlbum();
|
||||
if($audioTags->hasDate())
|
||||
$resp->media->tags->date = $audioTags->getDate();
|
||||
if($audioTags->hasComment())
|
||||
$resp->media->tags->comment = $audioTags->getComment();
|
||||
if($audioTags->hasGenre())
|
||||
$resp->media->tags->genre = $audioTags->getGenre();
|
||||
}
|
||||
}
|
||||
|
||||
if($result instanceof EEPROMLookupResult) {
|
||||
$resp->eeprom_file_id = $result->getEEPROMId();
|
||||
$resp->eeprom_file_info = $result->getEEPROMInfo();
|
||||
}
|
||||
|
||||
if(UIH_DEBUG && $result->hasMediaInfo())
|
||||
$resp->dbg_media_info = $result->getMediaInfo();
|
||||
}
|
||||
} catch(Exception $ex) {
|
||||
$resp->status = 500;
|
||||
$resp->error = 'metadata:lookup';
|
||||
if(UIH_DEBUG) {
|
||||
$resp->dbg_msg = $ex->getMessage();
|
||||
$resp->dbg_ex = (string)$ex;
|
||||
}
|
||||
return $resp;
|
||||
}
|
||||
|
||||
$sw->stop();
|
||||
$resp->took = $sw->getElapsedTime() / 1000;
|
||||
$respJson = json_encode($resp);
|
||||
|
||||
$this->cache->set($cacheKey, $respJson, 10 * 60);
|
||||
}
|
||||
|
||||
if(!empty($respJson))
|
||||
return $respJson;
|
||||
|
||||
return $resp;
|
||||
}
|
||||
|
||||
private function handleMetadata($response, $request, string $targetUrl) {
|
||||
$result = $this->metadataLookup($targetUrl);
|
||||
if(is_int($result))
|
||||
return $result;
|
||||
|
||||
if(is_object($result)) {
|
||||
if(!empty($result->status))
|
||||
$response->setStatusCode($result->status);
|
||||
} elseif(is_string($result)) {
|
||||
$response->setTypeJson();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function handleMetadataBatch($response, $request, array $urls) {
|
||||
$sw = Stopwatch::startNew();
|
||||
|
||||
if(count($urls) > 20)
|
||||
return 400;
|
||||
|
||||
$handled = [];
|
||||
$results = [];
|
||||
|
||||
foreach($urls as $url) {
|
||||
if(!is_string($url))
|
||||
continue;
|
||||
|
||||
$cleanUrl = trim($url, '/?&# ');
|
||||
if(in_array($cleanUrl, $handled))
|
||||
continue;
|
||||
$handled[] = $cleanUrl;
|
||||
|
||||
$result = $this->metadataLookup($url);
|
||||
if(is_int($result)) {
|
||||
$status = $result;
|
||||
$result = new stdClass;
|
||||
$result->status = $status;
|
||||
$result->error = 'batch:status';
|
||||
} elseif(is_string($result)) {
|
||||
$result = json_decode($result);
|
||||
}
|
||||
|
||||
$results[] = [
|
||||
'url' => $url,
|
||||
'info' => $result,
|
||||
];
|
||||
}
|
||||
|
||||
$sw->stop();
|
||||
|
||||
return [
|
||||
'took' => $sw->getElapsedTime() / 1000,
|
||||
'results' => $results,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu;
|
||||
|
||||
class AudioTags {
|
||||
public function __construct(
|
||||
private string $title = '',
|
||||
private string $artist = '',
|
||||
private string $album = '',
|
||||
private string $date = '',
|
||||
private string $comment = '',
|
||||
private string $genre = ''
|
||||
) {}
|
||||
|
||||
public function hasTitle(): bool {
|
||||
return $this->title !== '';
|
||||
}
|
||||
public function getTitle(): string {
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function hasArtist(): bool {
|
||||
return $this->artist !== '';
|
||||
}
|
||||
public function getArtist(): string {
|
||||
return $this->artist;
|
||||
}
|
||||
|
||||
public function hasAlbum(): bool {
|
||||
return $this->album !== '';
|
||||
}
|
||||
public function getAlbum(): string {
|
||||
return $this->album;
|
||||
}
|
||||
|
||||
public function hasDate(): bool {
|
||||
return $this->date !== '';
|
||||
}
|
||||
public function getDate(): string {
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
public function hasComment(): bool {
|
||||
return $this->comment !== '';
|
||||
}
|
||||
public function getComment(): string {
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
public function hasGenre(): bool {
|
||||
return $this->genre !== '';
|
||||
}
|
||||
public function getGenre(): string {
|
||||
return $this->genre;
|
||||
}
|
||||
|
||||
public static function fromMediaInfo(object $obj): AudioTags {
|
||||
return new AudioTags(
|
||||
$obj->tagTitle ?? '',
|
||||
$obj->tagArtist ?? '',
|
||||
$obj->tagAlbum ?? '',
|
||||
$obj->tagDate ?? '',
|
||||
$obj->tagComment ?? '',
|
||||
$obj->tagGenre ?? '',
|
||||
);
|
||||
}
|
||||
}
|
175
src/FFMPEG.php
175
src/FFMPEG.php
|
@ -1,175 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu;
|
||||
|
||||
use stdClass;
|
||||
use Imagick;
|
||||
use RuntimeException;
|
||||
|
||||
final class FFMPEG {
|
||||
public static function grabFirstVideoFrame(string $url): string {
|
||||
try {
|
||||
$file = popen(sprintf('ffmpeg -i %s -f image2pipe -c:v png -frames:v 1 2>/dev/null -', escapeshellarg($url)), 'rb');
|
||||
return stream_get_contents($file);
|
||||
} finally {
|
||||
if(isset($file) && is_resource($file))
|
||||
pclose($file);
|
||||
}
|
||||
}
|
||||
|
||||
public static function grabFirstAudioCover(string $url): string {
|
||||
try {
|
||||
$file = popen(sprintf('ffmpeg -i %s -an -f image2pipe -c:v copy -frames:v 1 2>/dev/null -', escapeshellarg($url)), 'rb');
|
||||
return stream_get_contents($file);
|
||||
} finally {
|
||||
if(isset($file) && is_resource($file))
|
||||
pclose($file);
|
||||
}
|
||||
}
|
||||
|
||||
public static function probe(string $url): ?object {
|
||||
$command = sprintf(
|
||||
'ffprobe -show_streams -show_format -print_format json -v quiet -i %s',
|
||||
escapeshellarg($url)
|
||||
);
|
||||
|
||||
$ffprobe = proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
|
||||
if(!is_resource($ffprobe))
|
||||
throw new RuntimeException('Could not open ffprobe.');
|
||||
|
||||
try {
|
||||
$stderr = trim(stream_get_contents($pipes[2]));
|
||||
if(!empty($stderr))
|
||||
throw new RuntimeException('ffprobe: ' . $stderr);
|
||||
|
||||
$stdout = trim(stream_get_contents($pipes[1]));
|
||||
if(empty($stdout))
|
||||
throw new RuntimeException('ffprobe did not report any errors but exited without any output');
|
||||
} finally {
|
||||
proc_close($ffprobe);
|
||||
}
|
||||
|
||||
return json_decode($stdout);
|
||||
}
|
||||
|
||||
public static function cleanProbe(string $url): ?object {
|
||||
return self::cleanProbeResult(self::probe($url));
|
||||
}
|
||||
|
||||
public static function cleanProbeResult(?object $in): object {
|
||||
$out = new stdClass;
|
||||
|
||||
if(!empty($in->format)) {
|
||||
$out->confidence = empty($in->format->probe_score) ? 0 : (intval($in->format->probe_score) / 100);
|
||||
|
||||
if(!empty($in->format->duration))
|
||||
$out->duration = floatval($in->format->duration);
|
||||
|
||||
if(!empty($in->format->size))
|
||||
$out->size = intval($in->format->size);
|
||||
|
||||
if(!empty($in->format->bit_rate))
|
||||
$out->bitRate = intval($in->format->bit_rate);
|
||||
|
||||
if(!empty($in->format->tags)) {
|
||||
if(!empty($in->format->tags->title)) {
|
||||
$out->tagTitle = $in->format->tags->title;
|
||||
} elseif(!empty($in->format->tags->TITLE)) {
|
||||
$out->tagTitle = $in->format->tags->TITLE;
|
||||
} elseif(!empty($in->format->tags->Title)) {
|
||||
$out->tagTitle = $in->format->tags->Title;
|
||||
}
|
||||
|
||||
if(!empty($in->format->tags->artist)) {
|
||||
$out->tagArtist = $in->format->tags->artist;
|
||||
} elseif(!empty($in->format->tags->ARTIST)) {
|
||||
$out->tagArtist = $in->format->tags->ARTIST;
|
||||
} elseif(!empty($in->format->tags->Artist)) {
|
||||
$out->tagArtist = $in->format->tags->Artist;
|
||||
}
|
||||
|
||||
if(!empty($in->format->tags->album)) {
|
||||
$out->tagAlbum = $in->format->tags->album;
|
||||
} elseif(!empty($in->format->tags->ALBUM)) {
|
||||
$out->tagAlbum = $in->format->tags->ALBUM;
|
||||
} elseif(!empty($in->format->tags->Album)) {
|
||||
$out->tagAlbum = $in->format->tags->Album;
|
||||
}
|
||||
|
||||
if(!empty($in->format->tags->date)) {
|
||||
$out->tagDate = $in->format->tags->date;
|
||||
} elseif(!empty($in->format->tags->DATE)) {
|
||||
$out->tagDate = $in->format->tags->DATE;
|
||||
} elseif(!empty($in->format->tags->Date)) {
|
||||
$out->tagDate = $in->format->tags->Date;
|
||||
}
|
||||
|
||||
if(!empty($in->format->tags->comment)) {
|
||||
$out->tagComment = $in->format->tags->comment;
|
||||
} elseif(!empty($in->format->tags->COMMENT)) {
|
||||
$out->tagComment = $in->format->tags->COMMENT;
|
||||
} elseif(!empty($in->format->tags->Comment)) {
|
||||
$out->tagComment = $in->format->tags->Comment;
|
||||
}
|
||||
|
||||
if(!empty($in->format->tags->genre)) {
|
||||
$out->tagGenre = $in->format->tags->genre;
|
||||
} elseif(!empty($in->format->tags->GENRE)) {
|
||||
$out->tagGenre = $in->format->tags->GENRE;
|
||||
} elseif(!empty($in->format->tags->Genre)) {
|
||||
$out->tagGenre = $in->format->tags->Genre;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!empty($in->streams))
|
||||
foreach($in->streams as $stream) {
|
||||
$codecType = $stream->codec_type ?? null;
|
||||
|
||||
if($codecType === 'video') {
|
||||
$out->width = intval($stream->coded_width ?? $stream->width ?? -1);
|
||||
$out->height = intval($stream->coded_height ?? $stream->height ?? -1);
|
||||
|
||||
if(!empty($stream->display_aspect_ratio))
|
||||
$out->aspectRatio = $stream->display_aspect_ratio;
|
||||
} elseif($codecType === 'audio') {
|
||||
if(!empty($stream->tags->title)) {
|
||||
$out->tagTitle = $stream->tags->title;
|
||||
} elseif(!empty($stream->tags->TITLE)) {
|
||||
$out->tagTitle = $stream->tags->TITLE;
|
||||
}
|
||||
|
||||
if(!empty($stream->tags->artist)) {
|
||||
$out->tagArtist = $stream->tags->artist;
|
||||
} elseif(!empty($stream->tags->ARTIST)) {
|
||||
$out->tagArtist = $stream->tags->ARTIST;
|
||||
}
|
||||
|
||||
if(!empty($stream->tags->album)) {
|
||||
$out->tagAlbum = $stream->tags->album;
|
||||
} elseif(!empty($stream->tags->ALBUM)) {
|
||||
$out->tagAlbum = $stream->tags->ALBUM;
|
||||
}
|
||||
|
||||
if(!empty($stream->tags->date)) {
|
||||
$out->tagDate = $stream->tags->date;
|
||||
} elseif(!empty($stream->tags->DATE)) {
|
||||
$out->tagDate = $stream->tags->DATE;
|
||||
}
|
||||
|
||||
if(!empty($stream->tags->comment)) {
|
||||
$out->tagComment = $stream->tags->comment;
|
||||
} elseif(!empty($stream->tags->COMMENT)) {
|
||||
$out->tagComment = $stream->tags->COMMENT;
|
||||
}
|
||||
|
||||
if(!empty($stream->tags->genre)) {
|
||||
$out->tagGenre = $stream->tags->genre;
|
||||
} elseif(!empty($stream->tags->GENRE)) {
|
||||
$out->tagGenre = $stream->tags->GENRE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu;
|
||||
|
||||
use Index\Http\Routing\RouteHandler;
|
||||
|
||||
interface IApi extends RouteHandler {
|
||||
function match(string $url): string;
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu;
|
||||
|
||||
interface IHasMediaInfo extends ILookupResult {
|
||||
function getConfidence(): float;
|
||||
|
||||
function isMedia(): bool;
|
||||
function isImage(): bool;
|
||||
function isVideo(): bool;
|
||||
function isAudio(): bool;
|
||||
|
||||
function hasDimensions(): bool;
|
||||
function getWidth(): int;
|
||||
function getHeight(): int;
|
||||
|
||||
function hasAspectRatio(): bool;
|
||||
function getAspectRatio(): string;
|
||||
|
||||
function hasDuration(): bool;
|
||||
function getDuration(): float;
|
||||
|
||||
function hasSize(): bool;
|
||||
function getSize(): int;
|
||||
|
||||
function hasBitRate(): bool;
|
||||
function getBitRate(): int;
|
||||
|
||||
function hasAudioTags(): bool;
|
||||
function getAudioTags(): AudioTags;
|
||||
|
||||
function hasMediaInfo(): bool;
|
||||
function getMediaInfo(): object;
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu;
|
||||
|
||||
interface ILookup {
|
||||
function match(Url $url): bool;
|
||||
function lookup(Url $url): ILookupResult;
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu;
|
||||
|
||||
use Index\MediaType;
|
||||
use Index\Colour\Colour;
|
||||
|
||||
interface ILookupResult {
|
||||
function getUrl(): Url;
|
||||
function getObjectType(): string;
|
||||
|
||||
function hasMediaType(): bool;
|
||||
function getMediaType(): MediaType;
|
||||
|
||||
function hasColour(): bool;
|
||||
function getColour(): Colour;
|
||||
|
||||
function hasTitle(): bool;
|
||||
function getTitle(): string;
|
||||
|
||||
function hasSiteName(): bool;
|
||||
function getSiteName(): string;
|
||||
|
||||
function hasDescription(): bool;
|
||||
function getDescription(): string;
|
||||
|
||||
function hasPreviewImage(): bool;
|
||||
function getPreviewImage(): string;
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu\Lookup;
|
||||
|
||||
use RuntimeException;
|
||||
use Uiharu\FFMPEG;
|
||||
use Uiharu\MediaTypeExts;
|
||||
use Uiharu\Url;
|
||||
use Index\MediaType;
|
||||
|
||||
final class EEPROMLookup implements \Uiharu\ILookup {
|
||||
public function __construct(
|
||||
private string $protocol,
|
||||
private string $apiDomain,
|
||||
private array $shortDomains
|
||||
) {}
|
||||
|
||||
public function match(Url $url): bool {
|
||||
$urlHost = strtolower($url->getHost());
|
||||
return $url->getScheme() === $this->protocol || (
|
||||
$url->isWeb() && (
|
||||
in_array($urlHost, $this->shortDomains) || (
|
||||
$urlHost === $this->apiDomain && str_starts_with($url->getPath(), '/uploads')
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private function rawLookup(string $fileId): ?object {
|
||||
$curl = curl_init("https://{$this->apiDomain}/uploads/{$fileId}.json");
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_AUTOREFERER => false,
|
||||
CURLOPT_CERTINFO => false,
|
||||
CURLOPT_FAILONERROR => false,
|
||||
CURLOPT_FOLLOWLOCATION => false,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TCP_FASTOPEN => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 2,
|
||||
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
|
||||
CURLOPT_TIMEOUT => 2,
|
||||
CURLOPT_USERAGENT => 'Uiharu/' . UIH_VERSION,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Accept: application/json',
|
||||
],
|
||||
]);
|
||||
$resp = curl_exec($curl);
|
||||
curl_close($curl);
|
||||
return json_decode($resp);
|
||||
}
|
||||
|
||||
public function lookup(Url $url): EEPROMLookupResult {
|
||||
if($url->getScheme() === $this->protocol) {
|
||||
$fileId = $url->getPath();
|
||||
} elseif($url->isWeb()) {
|
||||
if($url->getHost() === $this->apiDomain) {
|
||||
if(preg_match('#^/uploads/([A-Za-z0-9-_]+)/?$#', $url->getPath(), $matches))
|
||||
$fileId = $matches[1];
|
||||
} else {
|
||||
$fileId = substr($url->getPath(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
if(!isset($fileId))
|
||||
throw new RuntimeException('Was unable to find EEPROM file id.');
|
||||
if(!preg_match('#^([A-Za-z0-9-_]+)$#', $fileId))
|
||||
throw new RuntimeException('Invalid EEPROM file id format.');
|
||||
|
||||
$fileInfo = $this->rawLookup($fileId);
|
||||
if($fileInfo === null)
|
||||
throw new RuntimeException('EEPROM file does not exist: ' . $fileId);
|
||||
|
||||
$url = Url::parse('https://' . $this->shortDomains[0] . '/' . $fileId);
|
||||
$mediaType = MediaType::parse($fileInfo->type);
|
||||
$isMedia = MediaTypeExts::isMedia($mediaType);
|
||||
$mediaInfo = $isMedia ? FFMPEG::cleanProbe($url) : null;
|
||||
|
||||
return new EEPROMLookupResult($url, $mediaType, $fileInfo, $mediaInfo);
|
||||
}
|
||||
}
|
|
@ -1,173 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu\Lookup;
|
||||
|
||||
use Uiharu\AudioTags;
|
||||
use Uiharu\Url;
|
||||
use Index\MediaType;
|
||||
use Index\Colour\{Colour,ColourRgb};
|
||||
|
||||
final class EEPROMLookupResult implements \Uiharu\ILookupResult, \Uiharu\IHasMediaInfo {
|
||||
private Url $url;
|
||||
private MediaType $type;
|
||||
private object $fileInfo;
|
||||
private ?object $mediaInfo;
|
||||
private ?AudioTags $audioTags;
|
||||
|
||||
public function __construct(
|
||||
Url $url,
|
||||
MediaType $type,
|
||||
object $fileInfo,
|
||||
?object $mediaInfo
|
||||
) {
|
||||
$this->url = $url;
|
||||
$this->type = $type;
|
||||
$this->fileInfo = $fileInfo;
|
||||
$this->mediaInfo = $mediaInfo;
|
||||
|
||||
if($this->isAudio() && $mediaInfo !== null)
|
||||
$this->audioTags = AudioTags::fromMediaInfo($mediaInfo);
|
||||
else
|
||||
$this->audioTags = null;
|
||||
}
|
||||
|
||||
public function getUrl(): Url {
|
||||
return $this->url;
|
||||
}
|
||||
public function getObjectType(): string {
|
||||
return 'eeprom:file';
|
||||
}
|
||||
|
||||
public function hasMediaType(): bool {
|
||||
return true;
|
||||
}
|
||||
public function getMediaType(): MediaType {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getEEPROMId(): string {
|
||||
return $this->fileInfo->id;
|
||||
}
|
||||
public function getEEPROMInfo(): object {
|
||||
return $this->fileInfo;
|
||||
}
|
||||
|
||||
public function hasColour(): bool {
|
||||
return true;
|
||||
}
|
||||
public function getColour(): Colour {
|
||||
return ColourRgb::fromRawRgb(0x8559A5);
|
||||
}
|
||||
|
||||
public function hasTitle(): bool {
|
||||
return true;
|
||||
}
|
||||
public function getTitle(): string {
|
||||
if($this->audioTags !== null) {
|
||||
$title = '';
|
||||
|
||||
if($this->audioTags->hasArtist())
|
||||
$title .= $this->audioTags->getArtist() . ' - ';
|
||||
|
||||
if($this->audioTags->hasTitle())
|
||||
$title .= $this->audioTags->getTitle();
|
||||
|
||||
if($this->audioTags->hasDate())
|
||||
$title .= ' (' . $this->audioTags->getDate() . ')';
|
||||
|
||||
if(!empty($title))
|
||||
return $title;
|
||||
}
|
||||
|
||||
return $this->fileInfo->name;
|
||||
}
|
||||
|
||||
public function hasSiteName(): bool {
|
||||
return true;
|
||||
}
|
||||
public function getSiteName(): string {
|
||||
return 'EEPROM';
|
||||
}
|
||||
|
||||
public function hasDescription(): bool {
|
||||
return $this->audioTags !== null && $this->audioTags->hasComment();
|
||||
}
|
||||
public function getDescription(): string {
|
||||
return $this->audioTags->getComment();
|
||||
}
|
||||
|
||||
public function hasPreviewImage(): bool {
|
||||
return true;
|
||||
}
|
||||
public function getPreviewImage(): string {
|
||||
return $this->fileInfo->thumb;
|
||||
}
|
||||
|
||||
public function getConfidence(): float {
|
||||
return $this->mediaInfo->confidence ?? 0;
|
||||
}
|
||||
|
||||
public function isMedia(): bool {
|
||||
return $this->mediaInfo !== null;
|
||||
}
|
||||
public function isImage(): bool {
|
||||
return $this->type->matchCategory('image');
|
||||
}
|
||||
public function isVideo(): bool {
|
||||
return $this->type->matchCategory('video');
|
||||
}
|
||||
public function isAudio(): bool {
|
||||
return $this->type->matchCategory('audio');
|
||||
}
|
||||
|
||||
public function hasDimensions(): bool {
|
||||
return $this->isImage() || $this->isVideo();
|
||||
}
|
||||
public function getWidth(): int {
|
||||
return $this->mediaInfo->width ?? 0;
|
||||
}
|
||||
public function getHeight(): int {
|
||||
return $this->mediaInfo->height ?? 0;
|
||||
}
|
||||
|
||||
public function hasAspectRatio(): bool {
|
||||
return isset($this->mediaInfo->aspectRatio);
|
||||
}
|
||||
public function getAspectRatio(): string {
|
||||
return $this->mediaInfo->aspectRatio;
|
||||
}
|
||||
|
||||
public function hasDuration(): bool {
|
||||
return isset($this->mediaInfo->duration);
|
||||
}
|
||||
public function getDuration(): float {
|
||||
return $this->mediaInfo->duration;
|
||||
}
|
||||
|
||||
public function hasSize(): bool {
|
||||
return isset($this->mediaInfo->size);
|
||||
}
|
||||
public function getSize(): int {
|
||||
return $this->mediaInfo->size;
|
||||
}
|
||||
|
||||
public function hasBitRate(): bool {
|
||||
return isset($this->mediaInfo->bitRate);
|
||||
}
|
||||
public function getBitRate(): int {
|
||||
return $this->mediaInfo->bitRate;
|
||||
}
|
||||
|
||||
public function hasAudioTags(): bool {
|
||||
return $this->audioTags !== null;
|
||||
}
|
||||
public function getAudioTags(): AudioTags {
|
||||
return $this->audioTags;
|
||||
}
|
||||
|
||||
public function hasMediaInfo(): bool {
|
||||
return $this->mediaInfo !== null;
|
||||
}
|
||||
public function getMediaInfo(): object {
|
||||
return $this->mediaInfo;
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu\Lookup;
|
||||
|
||||
use DOMDocument;
|
||||
use RuntimeException;
|
||||
use Uiharu\Url;
|
||||
|
||||
final class NicoNicoLookup implements \Uiharu\ILookup {
|
||||
private const SHORT_DOMAINS = [
|
||||
'nico.ms',
|
||||
'www.nico.ms',
|
||||
];
|
||||
|
||||
private const LONG_DOMAINS = [
|
||||
'www.nicovideo.jp',
|
||||
'nicovideo.jp',
|
||||
];
|
||||
|
||||
public static function isShortDomain(string $host): bool {
|
||||
return in_array($host, self::SHORT_DOMAINS);
|
||||
}
|
||||
|
||||
public static function isLongDomain(string $host): bool {
|
||||
return in_array($host, self::LONG_DOMAINS);
|
||||
}
|
||||
|
||||
public function match(Url $url): bool {
|
||||
if(!$url->isWeb())
|
||||
return false;
|
||||
|
||||
$urlHost = strtolower($url->getHost());
|
||||
|
||||
if(self::isShortDomain($urlHost))
|
||||
return true;
|
||||
|
||||
if(self::isLongDomain($urlHost) && str_starts_with($url->getPath(), '/watch/sm'))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function lookup(Url $url): NicoNicoLookupResult {
|
||||
if(self::isShortDomain($url->getHost()))
|
||||
$videoId = explode('/', trim($url->getPath(), '/'))[0] ?? '';
|
||||
else
|
||||
$videoId = explode('/', trim($url->getPath(), '/'))[1] ?? '';
|
||||
|
||||
if(empty($videoId))
|
||||
throw new RuntimeException('Nico Nico Douga video id missing.');
|
||||
|
||||
$thumbDoc = self::lookupThumbInfo($videoId);
|
||||
|
||||
$thumbResp = $thumbDoc->getElementsByTagName('nicovideo_thumb_response')[0] ?? null;
|
||||
if(empty($thumbResp) || !$thumbResp->hasAttribute('status') || $thumbResp->getAttribute('status') !== 'ok')
|
||||
throw new RuntimeException('Nico Nico Douga video with the given id could not be found.');
|
||||
|
||||
$thumbInfo = $thumbResp->getElementsByTagName('thumb')[0] ?? null;
|
||||
if(empty($thumbInfo))
|
||||
throw new RuntimeException('Nico Nico Douga thumb info missing from API result????');
|
||||
|
||||
parse_str($url->getQuery(), $urlQuery);
|
||||
|
||||
return new NicoNicoLookupResult($url, $videoId, $thumbInfo, $urlQuery);
|
||||
}
|
||||
|
||||
private static function lookupThumbInfo(string $videoId): DOMDocument {
|
||||
$curl = curl_init("https://ext.nicovideo.jp/api/getthumbinfo/{$videoId}");
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_AUTOREFERER => false,
|
||||
CURLOPT_CERTINFO => false,
|
||||
CURLOPT_FAILONERROR => false,
|
||||
CURLOPT_FOLLOWLOCATION => false,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TCP_FASTOPEN => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 2,
|
||||
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
|
||||
CURLOPT_TIMEOUT => 3,
|
||||
CURLOPT_USERAGENT => 'Uiharu/' . UIH_VERSION,
|
||||
]);
|
||||
$resp = curl_exec($curl);
|
||||
curl_close($curl);
|
||||
|
||||
if(empty($resp))
|
||||
throw new RuntimeException('Nico Nico Douga API request failed.');
|
||||
|
||||
$doc = new DOMDocument;
|
||||
if(!$doc->loadXML($resp))
|
||||
throw new RuntimeException('Failed to parse Nico Nico Douga API response.');
|
||||
|
||||
return $doc;
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu\Lookup;
|
||||
|
||||
use DOMElement;
|
||||
use Index\MediaType;
|
||||
use Uiharu\Url;
|
||||
use Index\Colour\{Colour,ColourRgb};
|
||||
|
||||
final class NicoNicoLookupResult implements \Uiharu\ILookupResult {
|
||||
private DOMElement|false|null $title = null;
|
||||
private DOMElement|false|null $description = null;
|
||||
private DOMElement|false|null $previewImage = null;
|
||||
|
||||
public function __construct(
|
||||
private Url $url,
|
||||
private string $videoId,
|
||||
private DOMElement $thumbInfo,
|
||||
private array $urlQuery,
|
||||
) {}
|
||||
|
||||
public function getUrl(): Url {
|
||||
return $this->url;
|
||||
}
|
||||
public function getObjectType(): string {
|
||||
return 'niconico:video';
|
||||
}
|
||||
|
||||
public function getNicoNicoVideoId(): string {
|
||||
return $this->videoId;
|
||||
}
|
||||
public function getNicoNicoThumbInfo(): DOMElement {
|
||||
return $this->thumbInfo;
|
||||
}
|
||||
public function getNicoNicoUrlQuery(): array {
|
||||
return $this->urlQuery;
|
||||
}
|
||||
|
||||
public function hasNicoNicoVideoStartTime(): bool {
|
||||
return isset($this->urlQuery['from']);
|
||||
}
|
||||
public function getNicoNicoVideoStartTime(): string {
|
||||
return $this->urlQuery['from'] ?? '';
|
||||
}
|
||||
|
||||
public function hasMediaType(): bool {
|
||||
return false;
|
||||
}
|
||||
public function getMediaType(): MediaType {
|
||||
throw new RuntimeException('Unsupported');
|
||||
}
|
||||
|
||||
public function hasColour(): bool {
|
||||
return true;
|
||||
}
|
||||
public function getColour(): Colour {
|
||||
return ColourRgb::fromRawRgb(0x252525);
|
||||
}
|
||||
|
||||
public function hasTitle(): bool {
|
||||
if($this->title === null)
|
||||
$this->title = $this->thumbInfo->getElementsByTagName('title')[0] ?? false;
|
||||
return $this->title !== false;
|
||||
}
|
||||
public function getTitle(): string {
|
||||
return $this->hasTitle() ? trim($this->title->textContent) : 'No Title';
|
||||
}
|
||||
|
||||
public function hasSiteName(): bool {
|
||||
return true;
|
||||
}
|
||||
public function getSiteName(): string {
|
||||
return 'ニコニコ動画';
|
||||
}
|
||||
|
||||
public function hasDescription(): bool {
|
||||
if($this->description === null)
|
||||
$this->description = $this->thumbInfo->getElementsByTagName('description')[0] ?? false;
|
||||
return $this->description !== false && !empty($this->description->textContent);
|
||||
}
|
||||
public function getDescription(): string {
|
||||
return $this->hasDescription() ? trim($this->description->textContent) : '';
|
||||
}
|
||||
|
||||
public function hasPreviewImage(): bool {
|
||||
if($this->previewImage === null)
|
||||
$this->previewImage = $this->thumbInfo->getElementsByTagName('thumbnail_url')[0] ?? false;
|
||||
return $this->previewImage !== false && !empty($this->previewImage->textContent);
|
||||
}
|
||||
public function getPreviewImage(): string {
|
||||
return $this->hasPreviewImage() ? (trim($this->previewImage->textContent) . '.L') : '';
|
||||
}
|
||||
}
|
|
@ -1,177 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu\Lookup;
|
||||
|
||||
use finfo;
|
||||
use stdClass;
|
||||
use DOMDocument;
|
||||
use RuntimeException;
|
||||
use Uiharu\Config;
|
||||
use Uiharu\FFMPEG;
|
||||
use Uiharu\MediaTypeExts;
|
||||
use Uiharu\Url;
|
||||
use Index\MediaType;
|
||||
|
||||
// TODO: Content-Disposition should be honoured for the filename (title).
|
||||
final class WebLookup implements \Uiharu\ILookup {
|
||||
public function match(Url $url): bool {
|
||||
return $url->isWeb();
|
||||
}
|
||||
|
||||
private static function reqCreate(string $url) {
|
||||
$curl = curl_init($url);
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_AUTOREFERER => true,
|
||||
CURLOPT_CERTINFO => false,
|
||||
CURLOPT_FAILONERROR => false,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 5,
|
||||
CURLOPT_PATH_AS_IS => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TCP_FASTOPEN => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 2,
|
||||
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
|
||||
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
|
||||
CURLOPT_TIMEOUT => 5,
|
||||
CURLOPT_DEFAULT_PROTOCOL => 'https',
|
||||
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; FlashiiBot/2.5; Uiharu/' . UIH_VERSION . '; +http://fii.moe/uiharu)',
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8',
|
||||
'Accept-Language: en-GB, en;q=0.9, ja-jp;q=0.6, *;q=0.5',
|
||||
],
|
||||
]);
|
||||
return $curl;
|
||||
}
|
||||
|
||||
private static function reqHead($curl): array|null {
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_NOBODY => true,
|
||||
CURLOPT_HEADER => true,
|
||||
]);
|
||||
|
||||
$headers = curl_exec($curl);
|
||||
if($headers === false)
|
||||
return null;
|
||||
|
||||
$headers = explode("\r\n", trim($headers));
|
||||
$status = 200;
|
||||
$lines = [];
|
||||
|
||||
foreach($headers as $header) {
|
||||
if(empty($header))
|
||||
continue;
|
||||
|
||||
if(strpos($header, ':') === false) {
|
||||
$headParts = explode(' ', $header);
|
||||
if(isset($headParts[1]) && is_numeric($headParts[1]))
|
||||
$status = (int)$headParts[1];
|
||||
$lines = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = explode(':', $header, 2);
|
||||
$parts[0] = mb_strtolower($parts[0]);
|
||||
if(isset($lines[$parts[0]]))
|
||||
$lines[$parts[0]] .= ', ' . trim($parts[1] ?? '');
|
||||
else
|
||||
$lines[$parts[0]] = trim($parts[1] ?? '');
|
||||
}
|
||||
|
||||
return compact('status', 'lines');
|
||||
}
|
||||
|
||||
private static function reqBody($curl): string|false {
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_NOBODY => false,
|
||||
CURLOPT_HEADER => false,
|
||||
]);
|
||||
|
||||
return curl_exec($curl);
|
||||
}
|
||||
|
||||
private static function reqError($curl): string {
|
||||
return curl_error($curl);
|
||||
}
|
||||
|
||||
private static function reqClose($curl): void {
|
||||
curl_close($curl);
|
||||
}
|
||||
|
||||
public function lookup(Url $url): WebLookupResult {
|
||||
$req = self::reqCreate($url);
|
||||
$head = self::reqHead($req);
|
||||
|
||||
if($head === null)
|
||||
throw new RuntimeException('Web request timed out: ' . self::reqError($req));
|
||||
|
||||
$mediaType = MediaType::parse('application/octet-stream');
|
||||
$hasContentType = array_key_exists('content-type', $head['lines']);
|
||||
|
||||
if($hasContentType) {
|
||||
try {
|
||||
$mediaType = MediaType::parse($head['lines']['content-type'] ?? '');
|
||||
} catch(InvalidArgumentException $ex) {}
|
||||
|
||||
if(MediaTypeExts::isMedia($mediaType)) {
|
||||
self::reqClose($req);
|
||||
return $this->lookupMedia($url, $mediaType);
|
||||
}
|
||||
}
|
||||
|
||||
$body = self::reqBody($req);
|
||||
self::reqClose($req);
|
||||
|
||||
if(!$hasContentType)
|
||||
try {
|
||||
$finfo = new finfo(FILEINFO_MIME);
|
||||
$mediaType = MediaType::parse($finfo->buffer($body));
|
||||
} catch(InvalidArgumentException $ex) {}
|
||||
|
||||
if($mediaType->equals('text/html')
|
||||
|| $mediaType->equals('application/xhtml+xml')
|
||||
|| $mediaType->equals('application/xml'))
|
||||
return $this->lookupSite($url, $req, $mediaType, $body);
|
||||
|
||||
return new WebLookupFallbackResult($url, $mediaType, $url->getHost() . ': ' . basename($url->getPath()));
|
||||
}
|
||||
|
||||
private function lookupSite(Url $url, $req, MediaType $mediaType, string $body): WebLookupResult {
|
||||
// ok hear me out
|
||||
// there's absolutely no good html scraping libraries for PHP
|
||||
// DOMDocument Exists but kinda blows at catching weird encoding events like with pixiv
|
||||
// and i'm not about to rewrite this whole fucking thing in nodejs
|
||||
// also at this point Index should probably provide a wrapper for proc_open lol
|
||||
$extract = proc_open(
|
||||
sprintf('node %s/extract.mjs', UIH_ROOT),
|
||||
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
|
||||
$pipes
|
||||
);
|
||||
if(!is_resource($extract))
|
||||
throw new RuntimeException('Could not open extract.');
|
||||
|
||||
try {
|
||||
fwrite($pipes[0], $body);
|
||||
fclose($pipes[0]);
|
||||
|
||||
$stderr = trim(stream_get_contents($pipes[2]));
|
||||
if(!empty($stderr))
|
||||
throw new RuntimeException('extract: ' . $stderr);
|
||||
|
||||
$stdout = trim(stream_get_contents($pipes[1]));
|
||||
if(empty($stdout))
|
||||
throw new RuntimeException('extract did not report any errors but exited without any output');
|
||||
} finally {
|
||||
proc_close($extract);
|
||||
}
|
||||
|
||||
$siteInfo = json_decode($stdout);
|
||||
if(empty($siteInfo))
|
||||
throw new RuntimeException('Failed to parse extract output.');
|
||||
|
||||
return new WebLookupSiteResult($url, $mediaType, $siteInfo);
|
||||
}
|
||||
|
||||
private function lookupMedia(Url $url, MediaType $mediaType): WebLookupResult {
|
||||
$mediaInfo = FFMPEG::cleanProbe($url);
|
||||
return new WebLookupMediaResult($url, $mediaType, $mediaInfo);
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu\Lookup;
|
||||
|
||||
use RuntimeException;
|
||||
use Uiharu\Url;
|
||||
use Index\MediaType;
|
||||
use Index\Colour\Colour;
|
||||
|
||||
class WebLookupFallbackResult extends WebLookupResult {
|
||||
private string $title;
|
||||
|
||||
public function __construct(Url $url, MediaType $mediaType, string $title) {
|
||||
parent::__construct($url, $mediaType);
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
public function getObjectType(): string {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
public function hasColour(): bool {
|
||||
return false;
|
||||
}
|
||||
public function getColour(): Colour {
|
||||
throw new RuntimeException('Unsupported.');
|
||||
}
|
||||
|
||||
public function hasTitle(): bool {
|
||||
return true;
|
||||
}
|
||||
public function getTitle(): string {
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function hasSiteName(): bool {
|
||||
return false;
|
||||
}
|
||||
public function getSiteName(): string {
|
||||
throw new RuntimeException('Unsupported.');
|
||||
}
|
||||
|
||||
public function hasDescription(): bool {
|
||||
return false;
|
||||
}
|
||||
public function getDescription(): string {
|
||||
throw new RuntimeException('Unsupported.');
|
||||
}
|
||||
|
||||
public function hasPreviewImage(): bool {
|
||||
return false;
|
||||
}
|
||||
public function getPreviewImage(): string {
|
||||
throw new RuntimeException('Unsupported.');
|
||||
}
|
||||
}
|
|
@ -1,152 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu\Lookup;
|
||||
|
||||
use Uiharu\AudioTags;
|
||||
use Uiharu\Url;
|
||||
use Index\MediaType;
|
||||
use Index\Colour\Colour;
|
||||
|
||||
class WebLookupMediaResult extends WebLookupResult implements \Uiharu\IHasMediaInfo {
|
||||
private object $mediaInfo;
|
||||
private ?AudioTags $audioTags;
|
||||
|
||||
public function __construct(Url $url, MediaType $mediaType, object $mediaInfo) {
|
||||
parent::__construct($url, $mediaType);
|
||||
$this->mediaInfo = $mediaInfo;
|
||||
|
||||
if($this->isAudio() && $mediaInfo !== null)
|
||||
$this->audioTags = AudioTags::fromMediaInfo($mediaInfo);
|
||||
else
|
||||
$this->audioTags = null;
|
||||
}
|
||||
|
||||
public function getObjectType(): string {
|
||||
return 'media';
|
||||
}
|
||||
|
||||
public function hasColour(): bool {
|
||||
return false;
|
||||
}
|
||||
public function getColour(): Colour {
|
||||
throw new RuntimeException('Unsupported');
|
||||
}
|
||||
|
||||
public function hasTitle(): bool {
|
||||
return true;
|
||||
}
|
||||
public function getTitle(): string {
|
||||
if($this->audioTags !== null) {
|
||||
$title = '';
|
||||
|
||||
if($this->audioTags->hasArtist())
|
||||
$title .= $this->audioTags->getArtist() . ' - ';
|
||||
|
||||
if($this->audioTags->hasTitle())
|
||||
$title .= $this->audioTags->getTitle();
|
||||
|
||||
if($this->audioTags->hasDate())
|
||||
$title .= ' (' . $this->audioTags->getDate() . ')';
|
||||
|
||||
if(!empty($title))
|
||||
return $title;
|
||||
}
|
||||
|
||||
return rawurldecode(basename($this->url->getPath()));
|
||||
}
|
||||
|
||||
public function hasSiteName(): bool {
|
||||
return true;
|
||||
}
|
||||
public function getSiteName(): string {
|
||||
return $this->url->getHost();
|
||||
}
|
||||
|
||||
public function hasDescription(): bool {
|
||||
return $this->audioTags !== null && $this->audioTags->hasComment();
|
||||
}
|
||||
public function getDescription(): string {
|
||||
return $this->audioTags->getComment();
|
||||
}
|
||||
|
||||
public function hasPreviewImage(): bool {
|
||||
return $this->isImage() || $this->isVideo() || $this->isAudio();
|
||||
}
|
||||
public function getPreviewImage(): string {
|
||||
$url = (string)$this->url;
|
||||
if($this->isVideo())
|
||||
return '//' . $_SERVER['HTTP_HOST'] . '/metadata/thumb/video?url=' . rawurlencode($url);
|
||||
if($this->isAudio())
|
||||
return '//' . $_SERVER['HTTP_HOST'] . '/metadata/thumb/audio?url=' . rawurlencode($url);
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function getConfidence(): float {
|
||||
return $this->mediaInfo->confidence;
|
||||
}
|
||||
|
||||
public function isMedia(): bool {
|
||||
return true;
|
||||
}
|
||||
public function isImage(): bool {
|
||||
return $this->mediaType->matchCategory('image');
|
||||
}
|
||||
public function isVideo(): bool {
|
||||
return $this->mediaType->matchCategory('video');
|
||||
}
|
||||
public function isAudio(): bool {
|
||||
return $this->mediaType->matchCategory('audio');
|
||||
}
|
||||
|
||||
public function hasDimensions(): bool {
|
||||
return $this->isImage() || $this->isVideo();
|
||||
}
|
||||
public function getWidth(): int {
|
||||
return $this->mediaInfo->width ?? 0;
|
||||
}
|
||||
public function getHeight(): int {
|
||||
return $this->mediaInfo->height ?? 0;
|
||||
}
|
||||
|
||||
public function hasAspectRatio(): bool {
|
||||
return isset($this->mediaInfo->aspectRatio);
|
||||
}
|
||||
public function getAspectRatio(): string {
|
||||
return $this->mediaInfo->aspectRatio;
|
||||
}
|
||||
|
||||
public function hasDuration(): bool {
|
||||
return isset($this->mediaInfo->duration);
|
||||
}
|
||||
public function getDuration(): float {
|
||||
return $this->mediaInfo->duration;
|
||||
}
|
||||
|
||||
public function hasSize(): bool {
|
||||
return isset($this->mediaInfo->size);
|
||||
}
|
||||
public function getSize(): int {
|
||||
return $this->mediaInfo->size;
|
||||
}
|
||||
|
||||
public function hasBitRate(): bool {
|
||||
return isset($this->mediaInfo->bitRate);
|
||||
}
|
||||
public function getBitRate(): int {
|
||||
return $this->mediaInfo->bitRate;
|
||||
}
|
||||
|
||||
public function hasAudioTags(): bool {
|
||||
return $this->audioTags !== null;
|
||||
}
|
||||
public function getAudioTags(): AudioTags {
|
||||
return $this->audioTags;
|
||||
}
|
||||
|
||||
public function hasMediaInfo(): bool {
|
||||
return true;
|
||||
}
|
||||
public function getMediaInfo(): object {
|
||||
return $this->mediaInfo;
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu\Lookup;
|
||||
|
||||
use Uiharu\Url;
|
||||
use Index\MediaType;
|
||||
use Index\Colour\Colour;
|
||||
|
||||
abstract class WebLookupResult implements \Uiharu\ILookupResult {
|
||||
protected Url $url;
|
||||
protected MediaType $mediaType;
|
||||
|
||||
public function __construct(Url $url, MediaType $mediaType) {
|
||||
$this->url = $url;
|
||||
$this->mediaType = $mediaType;
|
||||
}
|
||||
|
||||
public function getUrl(): Url {
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
public abstract function getObjectType(): string;
|
||||
|
||||
public function hasMediaType(): bool {
|
||||
return true;
|
||||
}
|
||||
public function getMediaType(): MediaType {
|
||||
return $this->mediaType;
|
||||
}
|
||||
|
||||
public abstract function hasColour(): bool;
|
||||
public abstract function getColour(): Colour;
|
||||
|
||||
public abstract function hasTitle(): bool;
|
||||
public abstract function getTitle(): string;
|
||||
|
||||
public abstract function hasSiteName(): bool;
|
||||
public abstract function getSiteName(): string;
|
||||
|
||||
public abstract function hasDescription(): bool;
|
||||
public abstract function getDescription(): string;
|
||||
|
||||
public abstract function hasPreviewImage(): bool;
|
||||
public abstract function getPreviewImage(): string;
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu\Lookup;
|
||||
|
||||
use Uiharu\Url;
|
||||
use Index\MediaType;
|
||||
use Index\Colour\Colour;
|
||||
|
||||
class WebLookupSiteResult extends WebLookupResult {
|
||||
private object $siteInfo;
|
||||
|
||||
public function __construct(Url $url, MediaType $mediaType, object $siteInfo) {
|
||||
parent::__construct($url, $mediaType);
|
||||
$this->siteInfo = $siteInfo;
|
||||
}
|
||||
|
||||
public function getObjectType(): string {
|
||||
return $this->siteInfo->type;
|
||||
}
|
||||
|
||||
public function hasColour(): bool {
|
||||
return !empty($this->siteInfo->colour);
|
||||
}
|
||||
public function getColour(): Colour {
|
||||
return Colour::parse($this->siteInfo->colour);
|
||||
}
|
||||
|
||||
public function hasTitle(): bool {
|
||||
return true;
|
||||
}
|
||||
public function getTitle(): string {
|
||||
if(!empty($this->siteInfo->metaTitle))
|
||||
return $this->siteInfo->metaTitle;
|
||||
if(!empty($this->siteInfo->title))
|
||||
return $this->siteInfo->title;
|
||||
return $this->siteInfo->siteName;
|
||||
}
|
||||
|
||||
public function hasSiteName(): bool {
|
||||
return !empty($this->siteInfo->siteName);
|
||||
}
|
||||
public function getSiteName(): string {
|
||||
return $this->siteInfo->siteName;
|
||||
}
|
||||
|
||||
public function hasDescription(): bool {
|
||||
return !empty($this->siteInfo->desc);
|
||||
}
|
||||
public function getDescription(): string {
|
||||
return $this->siteInfo->desc;
|
||||
}
|
||||
|
||||
public function hasPreviewImage(): bool {
|
||||
return !empty($this->siteInfo->image);
|
||||
}
|
||||
public function getPreviewImage(): string {
|
||||
return $this->siteInfo->image;
|
||||
}
|
||||
|
||||
public function getWebSiteInfo(): object {
|
||||
return $this->siteInfo;
|
||||
}
|
||||
}
|
|
@ -1,133 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu\Lookup;
|
||||
|
||||
use RuntimeException;
|
||||
use Uiharu\Url;
|
||||
use Index\Config\Config;
|
||||
|
||||
final class YouTubeLookup implements \Uiharu\ILookup {
|
||||
private const SHORT_DOMAINS = [
|
||||
'youtu.be', 'www.youtu.be', // www. doesn't work for this, but may as well cover it
|
||||
];
|
||||
|
||||
private const WATCH_PREFIXES = [
|
||||
'/watch/',
|
||||
'/live/',
|
||||
'/shorts/',
|
||||
];
|
||||
|
||||
private const VALID_TLDS = [
|
||||
'ae', 'at', 'az', 'ba', 'be', 'bg', 'bh', 'bo', 'by',
|
||||
'ca', 'cat', 'ch', 'cl', 'co', 'co.ae', 'co.at', 'co.cr', 'co.hu',
|
||||
'co.id', 'co.il', 'co.in', 'co.jp', 'co.ke', 'co.kr', 'co.ma', 'co.nz',
|
||||
'co.th', 'co.tz', 'co.ug', 'co.uk', 'co.ve', 'co.za', 'co.zw',
|
||||
'com', 'com.ar', 'com.au', 'com.az', 'com.bd', 'com.bh', 'com.bo',
|
||||
'com.br', 'com.by', 'com.co', 'com.do', 'com.ec', 'com.ee', 'com.eg',
|
||||
'com.es', 'com.gh', 'com.gr', 'com.gt', 'com.hk', 'com.hn', 'com.hr',
|
||||
'com.jm', 'com.jo', 'com.kw', 'com.lb', 'com.lv', 'com.ly', 'com.mk',
|
||||
'com.mt', 'com.mx', 'com.my', 'com.ng', 'com.ni', 'com.om', 'com.pa',
|
||||
'com.pe', 'com.ph', 'com.pk', 'com.pt', 'com.py', 'com.qa', 'com.ro',
|
||||
'com.sa', 'com.sg', 'com.sv', 'com.tn', 'com.tr', 'com.tw', 'com.ua',
|
||||
'com.uy', 'com.ve', 'cr', 'cz', 'de', 'dk', 'ee', 'es', 'fi', 'fr',
|
||||
'ge', 'gr', 'gt', 'hk', 'hr', 'hu', 'ie', 'in', 'iq', 'is', 'it', 'jo',
|
||||
'jp', 'kr', 'kz', 'lk', 'lt', 'lu', 'lv', 'ly', 'ma', 'me', 'mk', 'mx',
|
||||
'my', 'net.in', 'ng', 'ni', 'nl', 'no', 'pa', 'pe', 'ph', 'pk', 'pl',
|
||||
'pr', 'pt', 'qa', 'ro', 'rs', 'ru', 'sa', 'se', 'sg', 'si', 'sk', 'sn',
|
||||
'sv', 'tn', 'ua', 'ug', 'uy', 'vn',
|
||||
];
|
||||
|
||||
public static function isShortDomain(string $host): bool {
|
||||
return in_array($host, self::SHORT_DOMAINS);
|
||||
}
|
||||
|
||||
public function __construct(private Config $config) {}
|
||||
|
||||
public function match(Url $url): bool {
|
||||
if(!$url->isWeb())
|
||||
return false;
|
||||
|
||||
$urlHost = strtolower($url->getHost());
|
||||
if(self::isShortDomain($urlHost))
|
||||
return true;
|
||||
|
||||
$parts = array_reverse(explode('.', $urlHost));
|
||||
$partsCount = count($parts);
|
||||
|
||||
if($partsCount < 2 || $partsCount > 4)
|
||||
return false;
|
||||
|
||||
if($parts[$partsCount - 1] === 'www')
|
||||
array_pop($parts);
|
||||
|
||||
if($parts[0] === 'com') {
|
||||
if($parts[1] !== 'youtube' && $parts[1] !== 'youtube-nocookie')
|
||||
return false;
|
||||
} else {
|
||||
if(array_pop($parts) !== 'youtube')
|
||||
return false;
|
||||
|
||||
$tld = implode('.', array_reverse($parts));
|
||||
if(!in_array($tld, self::VALID_TLDS))
|
||||
return false;
|
||||
}
|
||||
|
||||
$urlPath = $url->getPath();
|
||||
if($urlPath === '/watch')
|
||||
return true;
|
||||
|
||||
foreach(self::WATCH_PREFIXES as $prefix)
|
||||
if(str_starts_with($urlPath, $prefix))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function lookupVideo(string $videoId): ?object {
|
||||
$curl = curl_init("https://www.googleapis.com/youtube/v3/videos?part=snippet%2CcontentDetails%2Cstatistics&id={$videoId}&key=" . $this->config->getString('api_key'));
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_AUTOREFERER => false,
|
||||
CURLOPT_CERTINFO => false,
|
||||
CURLOPT_FAILONERROR => false,
|
||||
CURLOPT_FOLLOWLOCATION => false,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TCP_FASTOPEN => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 2,
|
||||
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
|
||||
CURLOPT_TIMEOUT => 2,
|
||||
CURLOPT_USERAGENT => 'Uiharu/' . UIH_VERSION,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Accept: application/json',
|
||||
],
|
||||
]);
|
||||
$resp = curl_exec($curl);
|
||||
curl_close($curl);
|
||||
return json_decode($resp);
|
||||
}
|
||||
|
||||
public function lookup(Url $url): YouTubeLookupResult {
|
||||
$urlPath = $url->getPath();
|
||||
parse_str($url->getQuery(), $urlQuery);
|
||||
|
||||
if(self::isShortDomain($url->getHost())) {
|
||||
$videoId = substr($urlPath, 1);
|
||||
} elseif(array_key_exists('v', $urlQuery)) {
|
||||
$videoId = $urlQuery['v'];
|
||||
} else {
|
||||
$urlPathParts = explode('/', trim($urlPath, '/'));
|
||||
if(count($urlPathParts) > 1)
|
||||
$videoId = $urlPathParts[1];
|
||||
}
|
||||
|
||||
if(empty($videoId))
|
||||
throw new RuntimeException('YouTube video id missing.');
|
||||
|
||||
$videoInfo = $this->lookupVideo($videoId);
|
||||
if($videoInfo === null)
|
||||
throw new RuntimeException('YouTube video with given id could not be found.');
|
||||
|
||||
unset($urlQuery['v']);
|
||||
$url = Url::parse(trim('https://www.youtube.com/watch?v=' . $videoId . '&' . http_build_query($urlQuery), '&'));
|
||||
|
||||
return new YouTubeLookupResult($url, $videoId, $videoInfo, $urlQuery);
|
||||
}
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu\Lookup;
|
||||
|
||||
use RuntimeException;
|
||||
use Uiharu\Url;
|
||||
use Index\MediaType;
|
||||
use Index\Colour\{Colour,ColourRgb};
|
||||
|
||||
final class YouTubeLookupResult implements \Uiharu\ILookupResult {
|
||||
public function __construct(
|
||||
private Url $url,
|
||||
private string $videoId,
|
||||
private object $videoInfo,
|
||||
private array $urlQuery
|
||||
) {}
|
||||
|
||||
public function getUrl(): Url {
|
||||
return $this->url;
|
||||
}
|
||||
public function getObjectType(): string {
|
||||
return 'youtube:video';
|
||||
}
|
||||
|
||||
public function getYouTubeVideoId(): string {
|
||||
return $this->videoId;
|
||||
}
|
||||
public function getYouTubeVideoInfo(): object {
|
||||
return $this->videoInfo;
|
||||
}
|
||||
public function getYouTubeUrlQuery(): array {
|
||||
return $this->urlQuery;
|
||||
}
|
||||
|
||||
public function hasYouTubeVideoStartTime(): bool {
|
||||
return isset($this->urlQuery['t']);
|
||||
}
|
||||
public function getYouTubeVideoStartTime(): string {
|
||||
return $this->urlQuery['t'] ?? '';
|
||||
}
|
||||
|
||||
public function hasYouTubePlayListId(): bool {
|
||||
return isset($this->urlQuery['list']);
|
||||
}
|
||||
public function getYouTubePlayListId(): string {
|
||||
return $this->urlQuery['list'] ?? '';
|
||||
}
|
||||
|
||||
public function hasYouTubePlayListIndex(): bool {
|
||||
return isset($this->urlQuery['index']);
|
||||
}
|
||||
public function getYouTubePlayListIndex(): string {
|
||||
return $this->urlQuery['index'] ?? '';
|
||||
}
|
||||
|
||||
public function hasMediaType(): bool {
|
||||
return false;
|
||||
}
|
||||
public function getMediaType(): MediaType {
|
||||
throw new RuntimeException('Unsupported');
|
||||
}
|
||||
|
||||
public function hasColour(): bool {
|
||||
return true;
|
||||
}
|
||||
public function getColour(): Colour {
|
||||
return ColourRgb::fromRawRgb(0xFF0000);
|
||||
}
|
||||
|
||||
public function hasTitle(): bool {
|
||||
return !empty($this->videoInfo->items[0]->snippet->title);
|
||||
}
|
||||
public function getTitle(): string {
|
||||
return $this->videoInfo->items[0]->snippet->title;
|
||||
}
|
||||
|
||||
public function hasSiteName(): bool {
|
||||
return true;
|
||||
}
|
||||
public function getSiteName(): string {
|
||||
return 'YouTube';
|
||||
}
|
||||
|
||||
public function hasDescription(): bool {
|
||||
return !empty($this->videoInfo->items[0]->snippet->description);
|
||||
}
|
||||
public function getDescription(): string {
|
||||
return $this->videoInfo->items[0]->snippet->description;
|
||||
}
|
||||
|
||||
public function hasPreviewImage(): bool {
|
||||
return !empty($this->videoInfo->items[0]->snippet->thumbnails->medium->url);
|
||||
}
|
||||
public function getPreviewImage(): string {
|
||||
return $this->videoInfo->items[0]->snippet->thumbnails->medium->url;
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu;
|
||||
|
||||
use Index\MediaType;
|
||||
|
||||
final class MediaTypeExts {
|
||||
public static function toV1(MediaType $mediaType): array {
|
||||
$parts = [
|
||||
'string' => (string)$mediaType,
|
||||
'type' => $mediaType->getCategory(),
|
||||
'subtype' => $mediaType->getKind(),
|
||||
];
|
||||
|
||||
if(!empty($suffix = $mediaType->getSuffix()))
|
||||
$parts['suffix'] = $suffix;
|
||||
|
||||
if(!empty($params = $mediaType->getParams()))
|
||||
$parts['params'] = $params;
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
public static function isMedia(MediaType $mediaType): bool {
|
||||
return $mediaType->matchCategory('image')
|
||||
|| $mediaType->matchCategory('audio')
|
||||
|| $mediaType->matchCategory('video');
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu;
|
||||
|
||||
use Index\Cache\CacheProvider;
|
||||
use Index\Config\Config;
|
||||
use Index\Http\Routing\HttpRouter;
|
||||
|
||||
final class UihContext {
|
||||
private CacheProvider $cache;
|
||||
private Config $config;
|
||||
private HttpRouter $router;
|
||||
private array $apis = [];
|
||||
private array $lookups = [];
|
||||
|
||||
public function __construct(CacheProvider $cache, Config $config) {
|
||||
$this->cache = $cache;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function getCache(): CacheProvider {
|
||||
return $this->cache;
|
||||
}
|
||||
|
||||
public function getRouter(): HttpRouter {
|
||||
return $this->router;
|
||||
}
|
||||
|
||||
public function isOriginAllowed(string $origin): bool {
|
||||
$origin = mb_strtolower(parse_url($origin, PHP_URL_HOST));
|
||||
|
||||
if($origin === $_SERVER['HTTP_HOST'])
|
||||
return true;
|
||||
|
||||
$allowed = $this->config->getArray('cors:origins');
|
||||
if(empty($allowed))
|
||||
return true;
|
||||
|
||||
return in_array($origin, $allowed);
|
||||
}
|
||||
|
||||
public function setupHttp(): void {
|
||||
$this->router = new HttpRouter;
|
||||
$this->router->use('/', function($response) {
|
||||
$response->setPoweredBy('Uiharu');
|
||||
});
|
||||
|
||||
$this->router->use('/', function($response, $request) {
|
||||
$origin = $request->getHeaderLine('Origin');
|
||||
|
||||
if(!empty($origin)) {
|
||||
if(!$this->isOriginAllowed($origin))
|
||||
return 403;
|
||||
|
||||
$response->setHeader('Access-Control-Allow-Origin', $origin);
|
||||
$response->setHeader('Vary', 'Origin');
|
||||
}
|
||||
});
|
||||
|
||||
$this->router->use('/', function($response, $request) {
|
||||
if($request->getMethod() === 'OPTIONS') {
|
||||
$response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET, POST');
|
||||
return 204;
|
||||
}
|
||||
});
|
||||
|
||||
$this->router->get('/', function($response) {
|
||||
$response->accelRedirect('/index.html');
|
||||
$response->setContentType('text/html; charset=utf-8');
|
||||
});
|
||||
}
|
||||
|
||||
public function dispatchHttp(): void {
|
||||
$this->router->dispatch();
|
||||
}
|
||||
|
||||
public function registerApi(IApi $api): void {
|
||||
$this->apis[] = $api;
|
||||
}
|
||||
|
||||
public function matchApi(string $reqPath): void {
|
||||
$reqPath = '/' . trim(parse_url($reqPath, PHP_URL_PATH), '/');
|
||||
foreach($this->apis as $api)
|
||||
if($api->match($reqPath)) {
|
||||
$this->router->register($api);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function registerLookup(ILookup $lookup): void {
|
||||
$this->lookups[] = $lookup;
|
||||
}
|
||||
|
||||
public function matchLookup(Url $url): ?ILookup {
|
||||
foreach($this->lookups as $lookup)
|
||||
if($lookup->match($url))
|
||||
return $lookup;
|
||||
return null;
|
||||
}
|
||||
}
|
218
src/Url.php
218
src/Url.php
|
@ -1,218 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final class Url {
|
||||
private string $scheme = '';
|
||||
private string $host = '';
|
||||
private int $port = 0;
|
||||
private string $user = '';
|
||||
private string $pass = '';
|
||||
private string $path = '';
|
||||
private string $query = '';
|
||||
private string $fragment = '';
|
||||
|
||||
private ?string $formatted = null;
|
||||
|
||||
public function __construct(array $parts) {
|
||||
if(isset($parts['scheme']))
|
||||
$this->scheme = strtolower($parts['scheme']);
|
||||
if(isset($parts['host']))
|
||||
$this->host = $parts['host'];
|
||||
if(isset($parts['port']))
|
||||
$this->port = $parts['port'];
|
||||
if(isset($parts['user']))
|
||||
$this->user = $parts['user'];
|
||||
if(isset($parts['pass']))
|
||||
$this->pass = $parts['pass'];
|
||||
if(isset($parts['path']))
|
||||
$this->path = $parts['path'];
|
||||
if(isset($parts['query']))
|
||||
$this->query = $parts['query'];
|
||||
if(isset($parts['fragment']))
|
||||
$this->fragment = $parts['fragment'];
|
||||
}
|
||||
|
||||
public static function parse(string $urlString): Url {
|
||||
$parts = parse_url($urlString);
|
||||
if($parts === false)
|
||||
throw new InvalidArgumentException('Invalid URL provided.');
|
||||
return new Url($parts);
|
||||
}
|
||||
|
||||
public function resetString(): void {
|
||||
$this->formatted = null;
|
||||
}
|
||||
public function setScheme(string $scheme): void {
|
||||
$this->scheme = $scheme;
|
||||
$this->resetString();
|
||||
}
|
||||
public function discardFragment(): void {
|
||||
$this->fragment = '';
|
||||
$this->resetString();
|
||||
}
|
||||
|
||||
public function hasScheme(): bool {
|
||||
return $this->scheme !== '';
|
||||
}
|
||||
public function hasHost(): bool {
|
||||
return $this->host !== '';
|
||||
}
|
||||
public function hasPort(): bool {
|
||||
return $this->port !== 0;
|
||||
}
|
||||
public function hasUser(): bool {
|
||||
return $this->user !== '';
|
||||
}
|
||||
public function hasPassword(): bool {
|
||||
return $this->pass !== '';
|
||||
}
|
||||
public function hasPath(): bool {
|
||||
return $this->path !== '';
|
||||
}
|
||||
public function hasQuery(): bool {
|
||||
return $this->query !== '';
|
||||
}
|
||||
public function hasFragment(): bool {
|
||||
return $this->fragment !== '';
|
||||
}
|
||||
|
||||
public function getScheme(): string {
|
||||
return $this->scheme;
|
||||
}
|
||||
public function getHost(): string {
|
||||
return $this->host;
|
||||
}
|
||||
public function getPort(): int {
|
||||
return $this->port;
|
||||
}
|
||||
public function getUser(): string {
|
||||
return $this->user;
|
||||
}
|
||||
public function getPassword(): string {
|
||||
return $this->pass;
|
||||
}
|
||||
public function getPath(): string {
|
||||
return $this->path;
|
||||
}
|
||||
public function getQuery(): string {
|
||||
return $this->query;
|
||||
}
|
||||
public function getFragment(): string {
|
||||
return $this->fragment;
|
||||
}
|
||||
|
||||
public function hasUserInfo(): bool {
|
||||
return $this->hasUser()
|
||||
|| $this->hasPassword();
|
||||
}
|
||||
public function hasAuthority(): bool {
|
||||
return $this->hasUserInfo()
|
||||
|| $this->hasHost();
|
||||
}
|
||||
|
||||
public function getUserInfo(): string {
|
||||
$userInfo = $this->user;
|
||||
if($this->pass !== '')
|
||||
$userInfo .= ':' . $this->pass;
|
||||
return $userInfo;
|
||||
}
|
||||
public function getAuthority(): string {
|
||||
$authority = '//';
|
||||
|
||||
if($this->hasUserInfo())
|
||||
$authority .= $this->getUserInfo() . '@';
|
||||
|
||||
$authority .= $this->host;
|
||||
|
||||
if($this->port !== 0)
|
||||
$authority .= ':' . $this->port;
|
||||
|
||||
return $authority;
|
||||
}
|
||||
|
||||
public function calculateHash(bool $raw = true): string {
|
||||
return hash('sha256', (string)$this, $raw);
|
||||
}
|
||||
|
||||
public function isHTTP(): bool {
|
||||
return $this->scheme === 'http';
|
||||
}
|
||||
public function isHTTPS(): bool {
|
||||
return $this->scheme === 'https';
|
||||
}
|
||||
public function isWeb(): bool {
|
||||
return $this->scheme === 'https'
|
||||
|| $this->scheme === 'http';
|
||||
}
|
||||
|
||||
public function __toString(): string {
|
||||
if($this->formatted === null) {
|
||||
$string = '';
|
||||
|
||||
if($this->hasScheme())
|
||||
$string .= $this->getScheme() . ':';
|
||||
|
||||
$hasAuthority = $this->hasAuthority();
|
||||
if($hasAuthority)
|
||||
$string .= $this->getAuthority();
|
||||
|
||||
$hasPath = $this->hasPath();
|
||||
$path = $this->getPath();
|
||||
|
||||
if($hasAuthority && (!$hasPath || $path[0] !== '/'))
|
||||
$string .= '/';
|
||||
elseif(!$hasAuthority && $path[1] === '/')
|
||||
$path = '/' . trim($path, '/');
|
||||
|
||||
$string .= $path;
|
||||
|
||||
// is all this necessary...?
|
||||
if($this->hasQuery()) {
|
||||
$string .= '?';
|
||||
$parts = explode('&', $this->getQuery());
|
||||
|
||||
foreach($parts as $part) {
|
||||
$param = explode('=', $part, 2);
|
||||
$string .= rawurlencode($param[0]);
|
||||
if(isset($param[1]))
|
||||
$string .= '=' . rawurlencode($param[1]);
|
||||
$string .= '&';
|
||||
}
|
||||
|
||||
$string = substr($string, 0, -1);
|
||||
}
|
||||
|
||||
if($this->hasFragment())
|
||||
$string .= '#' . rawurlencode($this->getFragment());
|
||||
|
||||
$this->formatted = $string;
|
||||
}
|
||||
|
||||
return $this->formatted;
|
||||
}
|
||||
|
||||
public function toV1(): array {
|
||||
$parts = ['uri' => (string)$this];
|
||||
|
||||
if($this->hasScheme())
|
||||
$parts['scheme'] = $this->getScheme();
|
||||
if($this->hasHost())
|
||||
$parts['host'] = $this->getHost();
|
||||
if($this->hasPort())
|
||||
$parts['port'] = $this->getPort();
|
||||
if($this->hasUser())
|
||||
$parts['user'] = $this->getUser();
|
||||
if($this->hasPassword())
|
||||
$parts['pass'] = $parts['password'] = $this->getPassword();
|
||||
if($this->hasPath())
|
||||
$parts['path'] = $this->getPath();
|
||||
if($this->hasQuery())
|
||||
$parts['query'] = $this->getQuery();
|
||||
if($this->hasFragment())
|
||||
$parts['fragment'] = $this->getFragment();
|
||||
|
||||
return $parts;
|
||||
}
|
||||
}
|
77
uiharu.js
Normal file
77
uiharu.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const memcache = require('memcache-client');
|
||||
const { join: pathJoin } = require('path');
|
||||
|
||||
// todo: these should not be hardcoded lol
|
||||
const port = 3009;
|
||||
const memcacheServer = "localhost:11211";
|
||||
const allowedOrigins = [
|
||||
'edgii.net',
|
||||
'chat.edgii.net',
|
||||
'sockchat.edgii.net',
|
||||
'ajaxchat.edgii.net',
|
||||
];
|
||||
|
||||
const app = express();
|
||||
const isDebug = fs.existsSync(pathJoin(__dirname, '.debug'));
|
||||
const cache = new memcache.MemcacheClient({ server: memcacheServer });
|
||||
|
||||
const handleMetadata = (res, req, url) => {
|
||||
//
|
||||
};
|
||||
|
||||
app.use((req, res, next) => {
|
||||
res.set('X-Powered-By', 'Uiharu');
|
||||
|
||||
const origin = req.get('origin');
|
||||
if(origin !== undefined) {
|
||||
const originObj = new URL(origin);
|
||||
if(!allowedOrigins.includes(originObj.host)) {
|
||||
res.status(403).end();
|
||||
return;
|
||||
}
|
||||
|
||||
res.set('Access-Control-Allow-Origin', origin);
|
||||
res.set('Vary', 'Origin');
|
||||
}
|
||||
|
||||
if(req.method === 'OPTIONS')
|
||||
res.set('Access-Control-Allow-Methods', 'OPTIONS, GET, POST');
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(express.static('public'));
|
||||
|
||||
app.get('/metadata', (req, res) => {
|
||||
res.status(501).send('Not Implemented');
|
||||
});
|
||||
|
||||
app.post('/metadata', (req, res) => {
|
||||
res.status(501).send('Not Implemented');
|
||||
});
|
||||
|
||||
app.get('/metadata/batch', (req, res) => {
|
||||
res.type('application/json')
|
||||
.send('{"took":0,"results":[]}')
|
||||
.end();
|
||||
});
|
||||
|
||||
app.post('/metadata/batch', (req, res) => {
|
||||
res.type('application/json')
|
||||
.send('{"took":0,"results":[]}')
|
||||
.end();
|
||||
});
|
||||
|
||||
app.get('/metadata/thumb/audio', (req, res) => {
|
||||
res.status(501).send('Not Implemented');
|
||||
});
|
||||
|
||||
app.get('/metadata/thumb/video', (req, res) => {
|
||||
res.status(501).send('Not Implemented');
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Uiharu listening to port ${port}!`);
|
||||
});
|
37
uiharu.php
37
uiharu.php
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
namespace Uiharu;
|
||||
|
||||
use Index\Cache\CacheBackends;
|
||||
use Index\Config\Fs\FsConfig;
|
||||
|
||||
define('UIH_STARTUP', microtime(true));
|
||||
define('UIH_ROOT', __DIR__);
|
||||
define('UIH_DEBUG', is_file(UIH_ROOT . '/.debug'));
|
||||
define('UIH_PUBLIC', UIH_ROOT . '/public');
|
||||
define('UIH_SOURCE', UIH_ROOT . '/src');
|
||||
define('UIH_LIBRARY', UIH_ROOT . '/lib');
|
||||
define('UIH_VERSION', '20241023');
|
||||
|
||||
require_once UIH_ROOT . '/vendor/autoload.php';
|
||||
|
||||
error_reporting(UIH_DEBUG ? -1 : 0);
|
||||
mb_internal_encoding('UTF-8');
|
||||
date_default_timezone_set('GMT');
|
||||
|
||||
$cfg = FsConfig::fromFile(UIH_ROOT . '/uiharu.cfg');
|
||||
|
||||
if($cfg->hasValues('sentry:dsn'))
|
||||
(function($cfg) {
|
||||
\Sentry\init([
|
||||
'dsn' => $cfg->getString('dsn'),
|
||||
'traces_sample_rate' => $cfg->getFloat('tracesRate', 0.2),
|
||||
'profiles_sample_rate' => $cfg->getFloat('profilesRate', 0.2),
|
||||
]);
|
||||
|
||||
set_exception_handler(function(\Throwable $ex) {
|
||||
\Sentry\captureException($ex);
|
||||
});
|
||||
})($cfg->scopeTo('sentry'));
|
||||
|
||||
$cache = CacheBackends::create($cfg->getString('cache:dsn', 'array:'));
|
||||
$ctx = new UihContext($cache, $cfg);
|
Loading…
Reference in a new issue