pdo = $pdo; $this->setPermalinktemplate('/tv/?v={id}'); $this->setUserAgent('FediverseVideoFetcher'); $this->setCacheDir($_SERVER['DOCUMENT_ROOT'].\DIRECTORY_SEPARATOR .'..'.\DIRECTORY_SEPARATOR .'cache'.\DIRECTORY_SEPARATOR .'robotstxt'.\DIRECTORY_SEPARATOR ); $this->setIdKey(isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : __CLASS__); } public function getVideoByItemId(int $itemId): ?array { $videoId = $this->getVideoIdByItemId($itemId); return $videoId ? $this->fetchById($videoId) : null; } public function linkItemToVideo(int $itemId, int|string $videoId): bool { if ( !is_int($videoId) && !filter_var($videoId, \FILTER_VALIDATE_URL)) { $videoId =$this->getIds($videoId)['int']; }elseif ( !is_int($videoId) && filter_var($videoId, \FILTER_VALIDATE_URL)) { $videoId =$this->getIdByVideoUrl($videoId); } $stmt = $this->pdo->prepare("\n INSERT INTO item_videos_link (item_id, video_id)\n VALUES (:item_id, :video_id)\n ON DUPLICATE KEY UPDATE video_id = VALUES(video_id)\n "); return $stmt->execute([ 'item_id' => $itemId, 'video_id' => $videoId, ]); } public function getItemIdByVideoId(int $videoId): ?int { $stmt = $this->pdo->prepare("SELECT item_id FROM item_videos_link WHERE video_id = :video_id"); $stmt->execute(['video_id' => $videoId]); $itemId = $stmt->fetchColumn(); return $itemId !== false ? (int) $itemId : null; } public function getVideoIdByItemId(int $itemId): ?int { $stmt = $this->pdo->prepare("SELECT video_id FROM item_videos_link WHERE item_id = :item_id"); $stmt->execute(['item_id' => $itemId]); $videoId = $stmt->fetchColumn(); return $videoId !== false ? (int) $videoId : null; } public function unlinkItem(int $itemId): bool { $stmt = $this->pdo->prepare("DELETE FROM item_videos_link WHERE item_id = :item_id"); return $stmt->execute(['item_id' => $itemId]); } public function getPermalinktemplate(): string { return $this->linktemplate; } public function setPermalinktemplate(string $linktemplate): self { $this->linktemplate = $linktemplate; return $this; } public function setIdKey(string $key): self { $this->idKey = $key; return $this; } public function setCacheDir(string $dir): self { $this->cacheDir = $dir; return $this; } public function setUserAgent(string $userAgent): self { $this->userAgent = $userAgent; return $this; } public function prepareVideoUrls(array $videoUrlData): array { $watchUrl = null; $thumbnailUrl = null; $mainVideoUrl = null; $videoUrls = []; foreach ($videoUrlData['video_url'] as $entry) { if (!isset($entry['type']) || $entry['type'] !== 'Link') { continue; } $mediaType = $entry['mediaType'] ?? ''; $href = $entry['href'] ?? null; if (!$href) { continue; } // 1. Klassische Watch-Seite (HTML) if ($mediaType === 'text/html' && !$watchUrl) { $watchUrl = $href; } // 2. Thumbnail eventuell aus metadata oder mp4/fragmented? if (str_ends_with($href, '.jpg') || str_contains($mediaType, 'image/')) { $thumbnailUrl ??= $href; } // 3. HLS if ($mediaType === 'application/x-mpegURL' && !$mainVideoUrl) { $mainVideoUrl = $href; } // 4. Video MP4 – Details sammeln if (str_starts_with($mediaType, 'video/')) { $videoUrls[] = [ 'href' => $href, 'mediaType' => $mediaType, 'width' => $entry['width'] ?? null, 'height' => $entry['height'] ?? null, 'fps' => $entry['fps'] ?? null, 'size' => $entry['size'] ?? null, ]; } } return [ 'watch_url' => $watchUrl, 'thumbnail_url' => $videoUrlData['thumbnail_url'] ?? $this->object_to_array(json_decode($videoUrlData['data']))['icon'][0]['url'], 'video_main_url' => $mainVideoUrl, 'video_urls' => $videoUrls, ]; } protected function object_to_array($obj) { if(is_object($obj) || is_array($obj)) { $ret = (array) $obj; foreach($ret as &$item) { //recursively process EACH element regardless of type $item = $this->object_to_array($item); } return $ret; } //otherwise (i.e. for scalar values) return without modification else { return $obj; } } public function fetchActivityPub(string $url, bool $useHttpHeaders = true): ?array { $userAgent = $this->userAgent; $context = stream_context_create([ 'http' => [ 'timeout' => 5, 'ignore_errors' => true, 'header' => $useHttpHeaders ? "Accept: application/activity+json\r\nUser-Agent: $userAgent\r\n" : "Accept: */*\r\n", ] ]); $json = @file_get_contents($url, false, $context); if (!$json) { return null; } $data = json_decode($json, true); $data = $this->object_to_array($data); if(!isset($data['video_url'])){ $data['video_url'] = $data['url']; } return is_array($data) ? $data : null; } protected function _return(?array $row = null): ?array { if(is_null($row) )return null; if(!isset($row['video_url']) && isset($row['data']) ){ $row['video_url'] = $this->object_to_array(json_decode($row['data']))['video_url']; } $row['video_url'] = $this->object_to_array(is_string($row['video_url']) ? json_decode($row['video_url']) : $row['video_url']); $row['thumbnail_url'] = $row['thumbnail_url'] ?? $this->object_to_array( json_decode($row['data']) )['icon'][0]['url']; if(!isset($row['thumbnail']) ){ $row['thumbnail'] = $row['thumbnail_url']; } $row['id'] = $this->getIds($row['id'])['alpha']; unset($row['thumbnail_url']); unset($row['data']); return $row; } public function getIds(int|string $in) { AlphaIDPatchedLowerCase::config($this->idKey); return AlphaIDPatchedLowerCase::get($in); } /** * Main fetch method (by URL or ID) */ public function fetch( string|int $videoUrl, bool $useHttpHeaders = true, ?bool $reFetch = false, ?bool $throw = false, ): ?array { if ( !is_int($videoUrl) && !filter_var($videoUrl, \FILTER_VALIDATE_URL)) { $videoUrl =$this->getIds($videoUrl)['int']; } $id = is_numeric($videoUrl) ? (int)$videoUrl : null; $url = is_string($videoUrl) ? trim($videoUrl) : null; // Wenn per ID geladen werden soll if ($id !== null) { return $this->fetchById($id); } if(true !== $reFetch){ // Prüfe, ob Video schon gecached ist $stmt = $this->pdo->prepare("SELECT * FROM fediverse_videos WHERE app_url = :url OR watch_url = :url OR original_id = :url LIMIT 1"); $stmt->execute(['url' => $url]); if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { // $row['data'] = json_decode($row['data'] ?? '[]', true); if($row['blocked'] == 1){ return null; } return $this->_return($row); } }// !$reFetch $host = parse_url($url)['host']; $hash_robots = hash('sha256', $host); $cache_file_robotstxt=$this->cacheDir .substr($hash_robots, 0,2).\DIRECTORY_SEPARATOR.$hash_robots.'.'.self::DBVERSION.'.ser.dat'; if(!is_dir(dirname($cache_file_robotstxt))){ mkdir(dirname($cache_file_robotstxt), 0755, true); } if(!Helpers::is_allowed_robots_txt_fedirectory($host, mt_rand(10800, 43000), dirname($cache_file_robotstxt), $this->userAgent) ){ if($throw)throw new Exception('The robots.txt of '.$host.' does not want to be crawled by us!'); return null; } // Hole Remote ActivityPub-Daten $activity = $this->fetchActivityPub($url, $useHttpHeaders); if (!$activity || !isset($activity['video_url'])) { throw new Exception('Could not get video_url!'); return null; } $preparedUrls = $this->prepareVideoUrls($activity); $watchUrl = $preparedUrls['watch_url'] ?? $url; // echo $watchUrl; // print_r($preparedUrls); // die('
'.print_r($activity['description'] ,true)); $data = [ 'original_id' => $activity['id'] ?? $watchUrl, 'title' => $activity['name'] ?? '', 'description' => $activity['content'] ?? '', 'app_url' => $url, // Ursprünglicher Link 'watch_url' => $watchUrl, // Neue eindeutige URL 'thumbnail' => $preparedUrls['thumbnail_url'] ?? $data['icon'][0]['url'], 'published' => $activity['published'] ?? null, 'data' => json_encode($activity, \JSON_UNESCAPED_SLASHES), 'blocked' => 0, 'block_reason'=> null, ]; $found = false; // if(true === $reFetch){ // Prüfe, ob Video schon gecached ist $stmt = $this->pdo->prepare("SELECT * FROM fediverse_videos WHERE app_url = :url OR watch_url = :url OR original_id = :url LIMIT 1"); $stmt->execute(['url' => $url]); if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { // $row['data'] = json_decode($row['data'] ?? '[]', true); $data['id'] = $this->_return($row)['id']; $found = true; } // } // Speichere in DB $stmt = $this->pdo->prepare("\n ".($found ? 'REPLACE' : 'INSERT')." INTO fediverse_videos (original_id , title, description, app_url, watch_url, thumbnail, published, data, blocked, block_reason)\n VALUES (:original_id , :title, :description, :app_url, :watch_url, :thumbnail, :published, :data, :blocked, :block_reason)\n "); $data2 = $data; unset($data2['id']); $stmt->execute($data2); // Füge IDs und prepared URLs zum Rückgabewert hinzu $data['id'] =$found ? $data['id'] : (int)$this->pdo->lastInsertId(); $data['video_urls'] = $preparedUrls['video_urls'] ?? []; $data['thumbnail_url'] = $preparedUrls['thumbnail_url'] ?? null; return $this->_return($data); } /** * Get metadata by internal DB ID */ public function fetchByID(int $id): ?array { $stmt = $this->pdo->prepare("SELECT * FROM fediverse_videos WHERE id = ? AND blocked = 0"); $stmt->execute([$id]); return $this->_return($stmt->fetch(PDO::FETCH_ASSOC) ?: null); } /** * Get internal ID by activitypub URL */ public function getIdByVideoUrl(string $url): ?int { $stmt = $this->pdo->prepare("\n SELECT id \n FROM fediverse_videos \n WHERE watch_url = :url OR app_url = :url OR original_id = :url \n LIMIT 1\n "); $stmt->execute(['url' => $url]); $row = $stmt->fetch(PDO::FETCH_ASSOC); return $row ? (int)$row['id'] : null; } /** * Render HTML embed code by ID or ActivityPub URL. * * @param int|string $videoUrl * @return string|null */ public function renderEmbed(int|string $videoUrl, string $mode = 'iframe'): ?string { $metadata = $this->fetch($videoUrl); if (!$metadata || !isset($metadata['video_url'])) { return null; } $videoData = $this->prepareVideoUrls($metadata ); // JSON-Modus if ($mode === 'json') { return json_encode($videoData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); } // Fallback auf watch_url $watch = htmlspecialchars($videoData['watch_url'] ?? '', ENT_QUOTES); $thumb = htmlspecialchars($videoData['thumbnail_url'] ?? '', ENT_QUOTES); // Videomodus: bestes MP4-Video mit