aboutsummaryrefslogtreecommitdiff
path: root/bridges/YoutubeBridge.php
diff options
context:
space:
mode:
Diffstat (limited to 'bridges/YoutubeBridge.php')
-rw-r--r--bridges/YoutubeBridge.php853
1 files changed, 436 insertions, 417 deletions
diff --git a/bridges/YoutubeBridge.php b/bridges/YoutubeBridge.php
index 536d6c29..31414472 100644
--- a/bridges/YoutubeBridge.php
+++ b/bridges/YoutubeBridge.php
@@ -1,4 +1,5 @@
<?php
+
/**
* RssBridgeYoutube
* Returns the newest videos
@@ -6,424 +7,442 @@
* change: define('MAX_FILE_SIZE', 600000);
* into: define('MAX_FILE_SIZE', 900000); (or more)
*/
-class YoutubeBridge extends BridgeAbstract {
-
- const NAME = 'YouTube Bridge';
- const URI = 'https://www.youtube.com/';
- const CACHE_TIMEOUT = 10800; // 3h
- const DESCRIPTION = 'Returns the 10 newest videos by username/channel/playlist or search';
- const MAINTAINER = 'em92';
-
- const PARAMETERS = array(
- 'By username' => array(
- 'u' => array(
- 'name' => 'username',
- 'exampleValue' => 'LinusTechTips',
- 'required' => true
- )
- ),
- 'By channel id' => array(
- 'c' => array(
- 'name' => 'channel id',
- 'exampleValue' => 'UCw38-8_Ibv_L6hlKChHO9dQ',
- 'required' => true
- )
- ),
- 'By custom name' => array(
- 'custom' => array(
- 'name' => 'custom name',
- 'exampleValue' => 'LinusTechTips',
- 'required' => true
- )
- ),
- 'By playlist Id' => array(
- 'p' => array(
- 'name' => 'playlist id',
- 'exampleValue' => 'PL8mG-RkN2uTzJc8N0EoyhdC54prvBBLpj',
- 'required' => true
- )
- ),
- 'Search result' => array(
- 's' => array(
- 'name' => 'search keyword',
- 'exampleValue' => 'LinusTechTips',
- 'required' => true
- ),
- 'pa' => array(
- 'name' => 'page',
- 'type' => 'number',
- 'title' => 'This option is not work anymore, as YouTube will always return the same page',
- 'exampleValue' => 1
- )
- ),
- 'global' => array(
- 'duration_min' => array(
- 'name' => 'min. duration (minutes)',
- 'type' => 'number',
- 'title' => 'Minimum duration for the video in minutes',
- 'exampleValue' => 5
- ),
- 'duration_max' => array(
- 'name' => 'max. duration (minutes)',
- 'type' => 'number',
- 'title' => 'Maximum duration for the video in minutes',
- 'exampleValue' => 10
- )
- )
- );
-
- private $feedName = '';
- private $feeduri = '';
- private $channel_name = '';
- // This took from repo BetterVideoRss of VerifiedJoseph.
+class YoutubeBridge extends BridgeAbstract
+{
+ const NAME = 'YouTube Bridge';
+ const URI = 'https://www.youtube.com/';
+ const CACHE_TIMEOUT = 10800; // 3h
+ const DESCRIPTION = 'Returns the 10 newest videos by username/channel/playlist or search';
+ const MAINTAINER = 'em92';
+
+ const PARAMETERS = [
+ 'By username' => [
+ 'u' => [
+ 'name' => 'username',
+ 'exampleValue' => 'LinusTechTips',
+ 'required' => true
+ ]
+ ],
+ 'By channel id' => [
+ 'c' => [
+ 'name' => 'channel id',
+ 'exampleValue' => 'UCw38-8_Ibv_L6hlKChHO9dQ',
+ 'required' => true
+ ]
+ ],
+ 'By custom name' => [
+ 'custom' => [
+ 'name' => 'custom name',
+ 'exampleValue' => 'LinusTechTips',
+ 'required' => true
+ ]
+ ],
+ 'By playlist Id' => [
+ 'p' => [
+ 'name' => 'playlist id',
+ 'exampleValue' => 'PL8mG-RkN2uTzJc8N0EoyhdC54prvBBLpj',
+ 'required' => true
+ ]
+ ],
+ 'Search result' => [
+ 's' => [
+ 'name' => 'search keyword',
+ 'exampleValue' => 'LinusTechTips',
+ 'required' => true
+ ],
+ 'pa' => [
+ 'name' => 'page',
+ 'type' => 'number',
+ 'title' => 'This option is not work anymore, as YouTube will always return the same page',
+ 'exampleValue' => 1
+ ]
+ ],
+ 'global' => [
+ 'duration_min' => [
+ 'name' => 'min. duration (minutes)',
+ 'type' => 'number',
+ 'title' => 'Minimum duration for the video in minutes',
+ 'exampleValue' => 5
+ ],
+ 'duration_max' => [
+ 'name' => 'max. duration (minutes)',
+ 'type' => 'number',
+ 'title' => 'Maximum duration for the video in minutes',
+ 'exampleValue' => 10
+ ]
+ ]
+ ];
+
+ private $feedName = '';
+ private $feeduri = '';
+ private $channel_name = '';
+ // This took from repo BetterVideoRss of VerifiedJoseph.
const URI_REGEX = '/(https?:\/\/(?:www\.)?(?:[a-zA-Z0-9-.]{2,256}\.[a-z]{2,20})(\:[0-9]{2 ,4})?(?:\/[a-zA-Z0-9@:%_\+.,~#"\'!?&\/\/=\-*]+|\/)?)/ims'; //phpcs:ignore
- private function ytBridgeQueryVideoInfo($vid, &$author, &$desc, &$time){
- $html = $this->ytGetSimpleHTMLDOM(self::URI . "watch?v=$vid", true);
-
- // Skip unavailable videos
- if(strpos($html->innertext, 'IS_UNAVAILABLE_PAGE') !== false) {
- return;
- }
-
- $elAuthor = $html->find('span[itemprop=author] > link[itemprop=name]', 0);
- if (!is_null($elAuthor)) {
- $author = $elAuthor->getAttribute('content');
- }
-
- $elDatePublished = $html->find('meta[itemprop=datePublished]', 0);
- if(!is_null($elDatePublished))
- $time = strtotime($elDatePublished->getAttribute('content'));
-
- $jsonData = $this->getJSONData($html);
- $jsonData = $jsonData->contents->twoColumnWatchNextResults->results->results->contents;
-
- $videoSecondaryInfo = null;
- foreach($jsonData as $item) {
- if (isset($item->videoSecondaryInfoRenderer)) {
- $videoSecondaryInfo = $item->videoSecondaryInfoRenderer;
- break;
- }
- }
- if (!$videoSecondaryInfo) {
- returnServerError('Could not find videoSecondaryInfoRenderer. Error at: ' . $vid);
- }
-
- if(isset($videoSecondaryInfo->description)) {
- foreach($videoSecondaryInfo->description->runs as $description) {
- if(isset($description->navigationEndpoint)) {
- $metadata = $description->navigationEndpoint->commandMetadata->webCommandMetadata;
- $web_type = $metadata->webPageType;
- $url = $metadata->url;
- $text = '';
- switch ($web_type) {
- case 'WEB_PAGE_TYPE_UNKNOWN':
- $url_components = parse_url($url);
- if(isset($url_components['query']) && strpos($url_components['query'], '&q=') !== false) {
- parse_str($url_components['query'], $params);
- $url = urldecode($params['q']);
- }
- $text = $url;
- break;
- case 'WEB_PAGE_TYPE_WATCH':
- case 'WEB_PAGE_TYPE_BROWSE':
- $url = 'https://www.youtube.com' . $url;
- $text = $description->text;
- break;
- }
- $desc .= "<a href=\"$url\" target=\"_blank\">$text</a>";
- } else {
- $desc .= nl2br($description->text);
- }
- }
- }
- }
-
- private function ytBridgeAddItem($vid, $title, $author, $desc, $time, $thumbnail = ''){
- $item = array();
- $item['id'] = $vid;
- $item['title'] = $title;
- $item['author'] = $author;
- $item['timestamp'] = $time;
- $item['uri'] = self::URI . 'watch?v=' . $vid;
- if(!$thumbnail) {
- $thumbnail = '0'; // Fallback to default thumbnail if there aren't any provided.
- }
- $thumbnailUri = str_replace('/www.', '/img.', self::URI) . 'vi/' . $vid . '/' . $thumbnail . '.jpg';
- $item['content'] = '<a href="' . $item['uri'] . '"><img src="' . $thumbnailUri . '" /></a><br />' . $desc;
- $this->items[] = $item;
- }
-
- private function ytBridgeParseXmlFeed($xml) {
- foreach($xml->find('entry') as $element) {
- $title = $this->ytBridgeFixTitle($element->find('title', 0)->plaintext);
- $author = $element->find('name', 0)->plaintext;
- $desc = $element->find('media:description', 0)->innertext;
-
- // Make sure the description is easy on the eye :)
- $desc = htmlspecialchars($desc);
- $desc = nl2br($desc);
- $desc = preg_replace(self::URI_REGEX,
- '<a href="$1" target="_blank">$1</a> ',
- $desc);
-
- $vid = str_replace('yt:video:', '', $element->find('id', 0)->plaintext);
- $time = strtotime($element->find('published', 0)->plaintext);
- if(strpos($vid, 'googleads') === false)
- $this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
- }
- $this->feedName = $this->ytBridgeFixTitle($xml->find('feed > title', 0)->plaintext); // feedName will be used by getName()
- }
-
- private function ytBridgeFixTitle($title) {
- // convert both &#1234; and &quot; to UTF-8
- return html_entity_decode($title, ENT_QUOTES, 'UTF-8');
- }
-
- private function ytGetSimpleHTMLDOM($url, $cached = false){
- $header = array(
- 'Accept-Language: en-US'
- );
- $opts = array();
- $lowercase = true;
- $forceTagsClosed = true;
- $target_charset = DEFAULT_TARGET_CHARSET;
- $stripRN = false;
- $defaultBRText = DEFAULT_BR_TEXT;
- $defaultSpanText = DEFAULT_SPAN_TEXT;
- if ($cached) {
- return getSimpleHTMLDOMCached($url,
- 86400,
- $header,
- $opts,
- $lowercase,
- $forceTagsClosed,
- $target_charset,
- $stripRN,
- $defaultBRText,
- $defaultSpanText);
- }
- return getSimpleHTMLDOM($url,
- $header,
- $opts,
- $lowercase,
- $forceTagsClosed,
- $target_charset,
- $stripRN,
- $defaultBRText,
- $defaultSpanText);
- }
-
- private function getJSONData($html) {
- $scriptRegex = '/var ytInitialData = (.*?);<\/script>/';
- preg_match($scriptRegex, $html, $matches) or returnServerError('Could not find ytInitialData');
- return json_decode($matches[1]);
- }
-
- private function parseJSONListing($jsonData) {
- $duration_min = $this->getInput('duration_min') ?: -1;
- $duration_min = $duration_min * 60;
-
- $duration_max = $this->getInput('duration_max') ?: INF;
- $duration_max = $duration_max * 60;
-
- if($duration_max < $duration_min) {
- returnClientError('Max duration must be greater than min duration!');
- }
-
- // $vid_list = '';
-
- foreach($jsonData as $item) {
- $wrapper = null;
- if(isset($item->gridVideoRenderer)) {
- $wrapper = $item->gridVideoRenderer;
- } elseif(isset($item->videoRenderer)) {
- $wrapper = $item->videoRenderer;
- } elseif(isset($item->playlistVideoRenderer)) {
- $wrapper = $item->playlistVideoRenderer;
- } else
- continue;
-
- $vid = $wrapper->videoId;
- $title = $wrapper->title->runs[0]->text;
- if(isset($wrapper->ownerText)) {
- $this->channel_name = $wrapper->ownerText->runs[0]->text;
- } elseif(isset($wrapper->shortBylineText)) {
- $this->channel_name = $wrapper->shortBylineText->runs[0]->text;
- }
-
- $author = '';
- $desc = '';
- $time = '';
-
- // The duration comes in one of the formats:
- // hh:mm:ss / mm:ss / m:ss
- // 01:03:30 / 15:06 / 1:24
- $durationText = 0;
- if(isset($wrapper->lengthText)) {
- $durationText = $wrapper->lengthText;
- } else {
- foreach($wrapper->thumbnailOverlays as $overlay) {
- if(isset($overlay->thumbnailOverlayTimeStatusRenderer)) {
- $durationText = $overlay->thumbnailOverlayTimeStatusRenderer->text;
- break;
- }
- }
- }
-
- if(isset($durationText->simpleText)) {
- $durationText = trim($durationText->simpleText);
- } else {
- $durationText = 0;
- }
-
- if(preg_match('/([\d]{1,2}):([\d]{1,2})\:([\d]{2})/', $durationText)) {
- $durationText = preg_replace('/([\d]{1,2}):([\d]{1,2})\:([\d]{2})/', '$1:$2:$3', $durationText);
- } else {
- $durationText = preg_replace('/([\d]{1,2})\:([\d]{2})/', '00:$1:$2', $durationText);
- }
- sscanf($durationText, '%d:%d:%d', $hours, $minutes, $seconds);
- $duration = $hours * 3600 + $minutes * 60 + $seconds;
- if($duration < $duration_min || $duration > $duration_max) {
- continue;
- }
-
- // $vid_list .= $vid . ',';
- $this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time);
- $this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
- }
- }
-
- public function collectData(){
-
- $xml = '';
- $html = '';
- $url_feed = '';
- $url_listing = '';
-
- if($this->getInput('u')) { /* User and Channel modes */
- $this->request = $this->getInput('u');
- $url_feed = self::URI . 'feeds/videos.xml?user=' . urlencode($this->request);
- $url_listing = self::URI . 'user/' . urlencode($this->request) . '/videos';
- } elseif($this->getInput('c')) {
- $this->request = $this->getInput('c');
- $url_feed = self::URI . 'feeds/videos.xml?channel_id=' . urlencode($this->request);
- $url_listing = self::URI . 'channel/' . urlencode($this->request) . '/videos';
- } elseif($this->getInput('custom')) {
- $this->request = $this->getInput('custom');
- $url_listing = self::URI . urlencode($this->request) . '/videos';
- }
-
- if(!empty($url_feed) || !empty($url_listing)) {
- $this->feeduri = $url_listing;
- if(!empty($this->getInput('custom'))) {
- $html = $this->ytGetSimpleHTMLDOM($url_listing);
- $jsonData = $this->getJSONData($html);
- $url_feed = $jsonData->metadata->channelMetadataRenderer->rssUrl;
- }
- if(!$this->skipFeeds()) {
- $html = $this->ytGetSimpleHTMLDOM($url_feed);
- $this->ytBridgeParseXmlFeed($html);
- } else {
- if(empty($this->getInput('custom'))) {
- $html = $this->ytGetSimpleHTMLDOM($url_listing);
- $jsonData = $this->getJSONData($html);
- }
- $channel_id = '';
- if(isset($jsonData->contents)) {
- $channel_id = $jsonData->metadata->channelMetadataRenderer->externalId;
- $jsonData = $jsonData->contents->twoColumnBrowseResultsRenderer->tabs[1];
- $jsonData = $jsonData->tabRenderer->content->sectionListRenderer->contents[0];
- $jsonData = $jsonData->itemSectionRenderer->contents[0]->gridRenderer->items;
- $this->parseJSONListing($jsonData);
- } else {
- returnServerError('Unable to get data from YouTube. Username/Channel: ' . $this->request);
- }
- }
- $this->feedName = str_replace(' - YouTube', '', $html->find('title', 0)->plaintext);
- } elseif($this->getInput('p')) { /* playlist mode */
- // TODO: this mode makes a lot of excess video query requests.
- // To make less requests, we need to cache following dictionary "videoId -> datePublished, duration"
- // This cache will be used to find out, which videos to fetch
- // to make feed of 15 items or more, if there a lot of videos published on that date.
- $this->request = $this->getInput('p');
- $url_feed = self::URI . 'feeds/videos.xml?playlist_id=' . urlencode($this->request);
- $url_listing = self::URI . 'playlist?list=' . urlencode($this->request);
- $html = $this->ytGetSimpleHTMLDOM($url_listing);
- $jsonData = $this->getJSONData($html);
- // TODO: this method returns only first 100 video items
- // if it has more videos, playlistVideoListRenderer will have continuationItemRenderer as last element
- $jsonData = $jsonData->contents->twoColumnBrowseResultsRenderer->tabs[0];
- $jsonData = $jsonData->tabRenderer->content->sectionListRenderer->contents[0]->itemSectionRenderer;
- $jsonData = $jsonData->contents[0]->playlistVideoListRenderer->contents;
- $item_count = count($jsonData);
-
- if ($item_count <= 15 && !$this->skipFeeds() && ($xml = $this->ytGetSimpleHTMLDOM($url_feed))) {
- $this->ytBridgeParseXmlFeed($xml);
- } else {
- $this->parseJSONListing($jsonData);
- }
- $this->feedName = 'Playlist: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext); // feedName will be used by getName()
- usort($this->items, function ($item1, $item2) {
- if(!is_int($item1['timestamp']) && !is_int($item2['timestamp'])) {
- $item1['timestamp'] = strtotime($item1['timestamp']);
- $item2['timestamp'] = strtotime($item2['timestamp']);
- }
- return $item2['timestamp'] - $item1['timestamp'];
- });
- } elseif($this->getInput('s')) { /* search mode */
- $this->request = $this->getInput('s');
- $url_listing = self::URI
- . 'results?search_query='
- . urlencode($this->request)
- . '&sp=CAI%253D';
-
- $html = $this->ytGetSimpleHTMLDOM($url_listing);
-
- $jsonData = $this->getJSONData($html);
- $jsonData = $jsonData->contents->twoColumnSearchResultsRenderer->primaryContents;
- $jsonData = $jsonData->sectionListRenderer->contents;
- foreach($jsonData as $data) { // Search result includes some ads, have to filter them
- if(isset($data->itemSectionRenderer->contents[0]->videoRenderer)) {
- $jsonData = $data->itemSectionRenderer->contents;
- break;
- }
- }
- $this->parseJSONListing($jsonData);
- $this->feeduri = $url_listing;
- $this->feedName = 'Search: ' . $this->request; // feedName will be used by getName()
- } else { /* no valid mode */
- returnClientError("You must either specify either:\n - YouTube
+ private function ytBridgeQueryVideoInfo($vid, &$author, &$desc, &$time)
+ {
+ $html = $this->ytGetSimpleHTMLDOM(self::URI . "watch?v=$vid", true);
+
+ // Skip unavailable videos
+ if (strpos($html->innertext, 'IS_UNAVAILABLE_PAGE') !== false) {
+ return;
+ }
+
+ $elAuthor = $html->find('span[itemprop=author] > link[itemprop=name]', 0);
+ if (!is_null($elAuthor)) {
+ $author = $elAuthor->getAttribute('content');
+ }
+
+ $elDatePublished = $html->find('meta[itemprop=datePublished]', 0);
+ if (!is_null($elDatePublished)) {
+ $time = strtotime($elDatePublished->getAttribute('content'));
+ }
+
+ $jsonData = $this->getJSONData($html);
+ $jsonData = $jsonData->contents->twoColumnWatchNextResults->results->results->contents;
+
+ $videoSecondaryInfo = null;
+ foreach ($jsonData as $item) {
+ if (isset($item->videoSecondaryInfoRenderer)) {
+ $videoSecondaryInfo = $item->videoSecondaryInfoRenderer;
+ break;
+ }
+ }
+ if (!$videoSecondaryInfo) {
+ returnServerError('Could not find videoSecondaryInfoRenderer. Error at: ' . $vid);
+ }
+
+ if (isset($videoSecondaryInfo->description)) {
+ foreach ($videoSecondaryInfo->description->runs as $description) {
+ if (isset($description->navigationEndpoint)) {
+ $metadata = $description->navigationEndpoint->commandMetadata->webCommandMetadata;
+ $web_type = $metadata->webPageType;
+ $url = $metadata->url;
+ $text = '';
+ switch ($web_type) {
+ case 'WEB_PAGE_TYPE_UNKNOWN':
+ $url_components = parse_url($url);
+ if (isset($url_components['query']) && strpos($url_components['query'], '&q=') !== false) {
+ parse_str($url_components['query'], $params);
+ $url = urldecode($params['q']);
+ }
+ $text = $url;
+ break;
+ case 'WEB_PAGE_TYPE_WATCH':
+ case 'WEB_PAGE_TYPE_BROWSE':
+ $url = 'https://www.youtube.com' . $url;
+ $text = $description->text;
+ break;
+ }
+ $desc .= "<a href=\"$url\" target=\"_blank\">$text</a>";
+ } else {
+ $desc .= nl2br($description->text);
+ }
+ }
+ }
+ }
+
+ private function ytBridgeAddItem($vid, $title, $author, $desc, $time, $thumbnail = '')
+ {
+ $item = [];
+ $item['id'] = $vid;
+ $item['title'] = $title;
+ $item['author'] = $author;
+ $item['timestamp'] = $time;
+ $item['uri'] = self::URI . 'watch?v=' . $vid;
+ if (!$thumbnail) {
+ $thumbnail = '0'; // Fallback to default thumbnail if there aren't any provided.
+ }
+ $thumbnailUri = str_replace('/www.', '/img.', self::URI) . 'vi/' . $vid . '/' . $thumbnail . '.jpg';
+ $item['content'] = '<a href="' . $item['uri'] . '"><img src="' . $thumbnailUri . '" /></a><br />' . $desc;
+ $this->items[] = $item;
+ }
+
+ private function ytBridgeParseXmlFeed($xml)
+ {
+ foreach ($xml->find('entry') as $element) {
+ $title = $this->ytBridgeFixTitle($element->find('title', 0)->plaintext);
+ $author = $element->find('name', 0)->plaintext;
+ $desc = $element->find('media:description', 0)->innertext;
+
+ // Make sure the description is easy on the eye :)
+ $desc = htmlspecialchars($desc);
+ $desc = nl2br($desc);
+ $desc = preg_replace(
+ self::URI_REGEX,
+ '<a href="$1" target="_blank">$1</a> ',
+ $desc
+ );
+
+ $vid = str_replace('yt:video:', '', $element->find('id', 0)->plaintext);
+ $time = strtotime($element->find('published', 0)->plaintext);
+ if (strpos($vid, 'googleads') === false) {
+ $this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
+ }
+ }
+ $this->feedName = $this->ytBridgeFixTitle($xml->find('feed > title', 0)->plaintext); // feedName will be used by getName()
+ }
+
+ private function ytBridgeFixTitle($title)
+ {
+ // convert both &#1234; and &quot; to UTF-8
+ return html_entity_decode($title, ENT_QUOTES, 'UTF-8');
+ }
+
+ private function ytGetSimpleHTMLDOM($url, $cached = false)
+ {
+ $header = [
+ 'Accept-Language: en-US'
+ ];
+ $opts = [];
+ $lowercase = true;
+ $forceTagsClosed = true;
+ $target_charset = DEFAULT_TARGET_CHARSET;
+ $stripRN = false;
+ $defaultBRText = DEFAULT_BR_TEXT;
+ $defaultSpanText = DEFAULT_SPAN_TEXT;
+ if ($cached) {
+ return getSimpleHTMLDOMCached(
+ $url,
+ 86400,
+ $header,
+ $opts,
+ $lowercase,
+ $forceTagsClosed,
+ $target_charset,
+ $stripRN,
+ $defaultBRText,
+ $defaultSpanText
+ );
+ }
+ return getSimpleHTMLDOM(
+ $url,
+ $header,
+ $opts,
+ $lowercase,
+ $forceTagsClosed,
+ $target_charset,
+ $stripRN,
+ $defaultBRText,
+ $defaultSpanText
+ );
+ }
+
+ private function getJSONData($html)
+ {
+ $scriptRegex = '/var ytInitialData = (.*?);<\/script>/';
+ preg_match($scriptRegex, $html, $matches) or returnServerError('Could not find ytInitialData');
+ return json_decode($matches[1]);
+ }
+
+ private function parseJSONListing($jsonData)
+ {
+ $duration_min = $this->getInput('duration_min') ?: -1;
+ $duration_min = $duration_min * 60;
+
+ $duration_max = $this->getInput('duration_max') ?: INF;
+ $duration_max = $duration_max * 60;
+
+ if ($duration_max < $duration_min) {
+ returnClientError('Max duration must be greater than min duration!');
+ }
+
+ // $vid_list = '';
+
+ foreach ($jsonData as $item) {
+ $wrapper = null;
+ if (isset($item->gridVideoRenderer)) {
+ $wrapper = $item->gridVideoRenderer;
+ } elseif (isset($item->videoRenderer)) {
+ $wrapper = $item->videoRenderer;
+ } elseif (isset($item->playlistVideoRenderer)) {
+ $wrapper = $item->playlistVideoRenderer;
+ } else {
+ continue;
+ }
+
+ $vid = $wrapper->videoId;
+ $title = $wrapper->title->runs[0]->text;
+ if (isset($wrapper->ownerText)) {
+ $this->channel_name = $wrapper->ownerText->runs[0]->text;
+ } elseif (isset($wrapper->shortBylineText)) {
+ $this->channel_name = $wrapper->shortBylineText->runs[0]->text;
+ }
+
+ $author = '';
+ $desc = '';
+ $time = '';
+
+ // The duration comes in one of the formats:
+ // hh:mm:ss / mm:ss / m:ss
+ // 01:03:30 / 15:06 / 1:24
+ $durationText = 0;
+ if (isset($wrapper->lengthText)) {
+ $durationText = $wrapper->lengthText;
+ } else {
+ foreach ($wrapper->thumbnailOverlays as $overlay) {
+ if (isset($overlay->thumbnailOverlayTimeStatusRenderer)) {
+ $durationText = $overlay->thumbnailOverlayTimeStatusRenderer->text;
+ break;
+ }
+ }
+ }
+
+ if (isset($durationText->simpleText)) {
+ $durationText = trim($durationText->simpleText);
+ } else {
+ $durationText = 0;
+ }
+
+ if (preg_match('/([\d]{1,2}):([\d]{1,2})\:([\d]{2})/', $durationText)) {
+ $durationText = preg_replace('/([\d]{1,2}):([\d]{1,2})\:([\d]{2})/', '$1:$2:$3', $durationText);
+ } else {
+ $durationText = preg_replace('/([\d]{1,2})\:([\d]{2})/', '00:$1:$2', $durationText);
+ }
+ sscanf($durationText, '%d:%d:%d', $hours, $minutes, $seconds);
+ $duration = $hours * 3600 + $minutes * 60 + $seconds;
+ if ($duration < $duration_min || $duration > $duration_max) {
+ continue;
+ }
+
+ // $vid_list .= $vid . ',';
+ $this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time);
+ $this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
+ }
+ }
+
+ public function collectData()
+ {
+ $xml = '';
+ $html = '';
+ $url_feed = '';
+ $url_listing = '';
+
+ if ($this->getInput('u')) { /* User and Channel modes */
+ $this->request = $this->getInput('u');
+ $url_feed = self::URI . 'feeds/videos.xml?user=' . urlencode($this->request);
+ $url_listing = self::URI . 'user/' . urlencode($this->request) . '/videos';
+ } elseif ($this->getInput('c')) {
+ $this->request = $this->getInput('c');
+ $url_feed = self::URI . 'feeds/videos.xml?channel_id=' . urlencode($this->request);
+ $url_listing = self::URI . 'channel/' . urlencode($this->request) . '/videos';
+ } elseif ($this->getInput('custom')) {
+ $this->request = $this->getInput('custom');
+ $url_listing = self::URI . urlencode($this->request) . '/videos';
+ }
+
+ if (!empty($url_feed) || !empty($url_listing)) {
+ $this->feeduri = $url_listing;
+ if (!empty($this->getInput('custom'))) {
+ $html = $this->ytGetSimpleHTMLDOM($url_listing);
+ $jsonData = $this->getJSONData($html);
+ $url_feed = $jsonData->metadata->channelMetadataRenderer->rssUrl;
+ }
+ if (!$this->skipFeeds()) {
+ $html = $this->ytGetSimpleHTMLDOM($url_feed);
+ $this->ytBridgeParseXmlFeed($html);
+ } else {
+ if (empty($this->getInput('custom'))) {
+ $html = $this->ytGetSimpleHTMLDOM($url_listing);
+ $jsonData = $this->getJSONData($html);
+ }
+ $channel_id = '';
+ if (isset($jsonData->contents)) {
+ $channel_id = $jsonData->metadata->channelMetadataRenderer->externalId;
+ $jsonData = $jsonData->contents->twoColumnBrowseResultsRenderer->tabs[1];
+ $jsonData = $jsonData->tabRenderer->content->sectionListRenderer->contents[0];
+ $jsonData = $jsonData->itemSectionRenderer->contents[0]->gridRenderer->items;
+ $this->parseJSONListing($jsonData);
+ } else {
+ returnServerError('Unable to get data from YouTube. Username/Channel: ' . $this->request);
+ }
+ }
+ $this->feedName = str_replace(' - YouTube', '', $html->find('title', 0)->plaintext);
+ } elseif ($this->getInput('p')) { /* playlist mode */
+ // TODO: this mode makes a lot of excess video query requests.
+ // To make less requests, we need to cache following dictionary "videoId -> datePublished, duration"
+ // This cache will be used to find out, which videos to fetch
+ // to make feed of 15 items or more, if there a lot of videos published on that date.
+ $this->request = $this->getInput('p');
+ $url_feed = self::URI . 'feeds/videos.xml?playlist_id=' . urlencode($this->request);
+ $url_listing = self::URI . 'playlist?list=' . urlencode($this->request);
+ $html = $this->ytGetSimpleHTMLDOM($url_listing);
+ $jsonData = $this->getJSONData($html);
+ // TODO: this method returns only first 100 video items
+ // if it has more videos, playlistVideoListRenderer will have continuationItemRenderer as last element
+ $jsonData = $jsonData->contents->twoColumnBrowseResultsRenderer->tabs[0];
+ $jsonData = $jsonData->tabRenderer->content->sectionListRenderer->contents[0]->itemSectionRenderer;
+ $jsonData = $jsonData->contents[0]->playlistVideoListRenderer->contents;
+ $item_count = count($jsonData);
+
+ if ($item_count <= 15 && !$this->skipFeeds() && ($xml = $this->ytGetSimpleHTMLDOM($url_feed))) {
+ $this->ytBridgeParseXmlFeed($xml);
+ } else {
+ $this->parseJSONListing($jsonData);
+ }
+ $this->feedName = 'Playlist: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext); // feedName will be used by getName()
+ usort($this->items, function ($item1, $item2) {
+ if (!is_int($item1['timestamp']) && !is_int($item2['timestamp'])) {
+ $item1['timestamp'] = strtotime($item1['timestamp']);
+ $item2['timestamp'] = strtotime($item2['timestamp']);
+ }
+ return $item2['timestamp'] - $item1['timestamp'];
+ });
+ } elseif ($this->getInput('s')) { /* search mode */
+ $this->request = $this->getInput('s');
+ $url_listing = self::URI
+ . 'results?search_query='
+ . urlencode($this->request)
+ . '&sp=CAI%253D';
+
+ $html = $this->ytGetSimpleHTMLDOM($url_listing);
+
+ $jsonData = $this->getJSONData($html);
+ $jsonData = $jsonData->contents->twoColumnSearchResultsRenderer->primaryContents;
+ $jsonData = $jsonData->sectionListRenderer->contents;
+ foreach ($jsonData as $data) { // Search result includes some ads, have to filter them
+ if (isset($data->itemSectionRenderer->contents[0]->videoRenderer)) {
+ $jsonData = $data->itemSectionRenderer->contents;
+ break;
+ }
+ }
+ $this->parseJSONListing($jsonData);
+ $this->feeduri = $url_listing;
+ $this->feedName = 'Search: ' . $this->request; // feedName will be used by getName()
+ } else { /* no valid mode */
+ returnClientError("You must either specify either:\n - YouTube
username (?u=...)\n - Channel id (?c=...)\n - Playlist id (?p=...)\n - Search (?s=...)");
- }
- }
-
- private function skipFeeds() {
- return ($this->getInput('duration_min') || $this->getInput('duration_max'));
- }
-
- public function getURI()
- {
- if (!is_null($this->getInput('p'))) {
- return static::URI . 'playlist?list=' . $this->getInput('p');
- } elseif($this->feeduri) {
- return $this->feeduri;
- }
-
- return parent::getURI();
- }
-
- public function getName(){
- // Name depends on queriedContext:
- switch($this->queriedContext) {
- case 'By username':
- case 'By channel id':
- case 'By custom name':
- case 'By playlist Id':
- case 'Search result':
- return htmlspecialchars_decode($this->feedName) . ' - YouTube'; // We already know it's a bridge, right?
- default:
- return parent::getName();
- }
- }
+ }
+ }
+
+ private function skipFeeds()
+ {
+ return ($this->getInput('duration_min') || $this->getInput('duration_max'));
+ }
+
+ public function getURI()
+ {
+ if (!is_null($this->getInput('p'))) {
+ return static::URI . 'playlist?list=' . $this->getInput('p');
+ } elseif ($this->feeduri) {
+ return $this->feeduri;
+ }
+
+ return parent::getURI();
+ }
+
+ public function getName()
+ {
+ // Name depends on queriedContext:
+ switch ($this->queriedContext) {
+ case 'By username':
+ case 'By channel id':
+ case 'By custom name':
+ case 'By playlist Id':
+ case 'Search result':
+ return htmlspecialchars_decode($this->feedName) . ' - YouTube'; // We already know it's a bridge, right?
+ default:
+ return parent::getName();
+ }
+ }
}