diff --git a/bridges/YouTubeCommunityTabBridge.php b/bridges/YouTubeCommunityTabBridge.php new file mode 100644 index 00000000..39ef46de --- /dev/null +++ b/bridges/YouTubeCommunityTabBridge.php @@ -0,0 +1,267 @@ + array( + 'channel' => array( + 'name' => 'Channel ID', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'UCULkRHBdLC5ZcEQBaL0oYHQ' + ) + ), + 'By username' => array( + 'username' => array( + 'name' => 'Username', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'YouTubeUK' + ), + ) + ); + + const CACHE_TIMEOUT = 3600; // 1 hour + + private $feedUrl = ''; + private $feedName = ''; + private $itemTitle = ''; + + private $urlRegex = '/youtube\.com\/(channel|user|c)\/([\w]+)\/community/'; + private $jsonRegex = '/var ytInitialData = (.*);<\/script>urlRegex, $url, $matches)) { + if ($matches[1] === 'channel') { + $params['context'] = 'By channel ID'; + $params['channel'] = $matches[2]; + } + + if ($matches[1] === 'user') { + $params['context'] = 'By username'; + $params['username'] = $matches[2]; + } + + return $params; + } + + return null; + } + + public function collectData() { + + if (is_null($this->getInput('username')) === false) { + try { + $this->feedUrl = $this->buildCommunityUri($this->getInput('username'), 'c'); + $html = getSimpleHTMLDOM($this->feedUrl); + + } catch (Exception $e) { + $this->feedUrl = $this->buildCommunityUri($this->getInput('username'), 'user'); + $html = getSimpleHTMLDOM($this->feedUrl); + } + } else { + $this->feedUrl = $this->buildCommunityUri($this->getInput('channel'), 'channel'); + $html = getSimpleHTMLDOM($this->feedUrl); + } + + $json = $this->extractJson($html->find('body', 0)->innertext); + + $this->feedName = $json->header->c4TabbedHeaderRenderer->title; + + if ($this->hasCommunityTab($json) === false) { + returnServerError('Channel does not have a community tab'); + } + + foreach ($this->getCommunityPosts($json) as $post) { + $this->itemTitle = ''; + + if (!isset($post->backstagePostThreadRenderer)) { + continue; + } + + $details = $post->backstagePostThreadRenderer->post->backstagePostRenderer; + + $item = array(); + $item['uri'] = self::URI . '/post/' . $details->postId; + $item['author'] = $details->authorText->runs[0]->text; + $item['content'] = ''; + + if (isset($details->contentText)) { + $text = $this->getText($details->contentText->runs); + + $this->itemTitle = $this->ellipsisTitle($text); + $item['content'] = $text; + } + + $item['content'] .= $this->getAttachments($details); + $item['title'] = $this->itemTitle; + + $this->items[] = $item; + } + } + + public function getURI() { + + if (!empty($this->feedUri)) { + return $this->feedUri; + } + + return parent::getURI(); + } + + public function getName() { + + if (!empty($this->feedName)) { + return $this->feedName . ' - YouTube Community Tab'; + } + + return parent::getName(); + } + + /** + * Build Community URI + */ + private function buildCommunityUri($value, $type) { + return self::URI . '/' . $type . '/' . $value . '/community'; + } + + /** + * Extract JSON from page + */ + private function extractJson($html) { + + if (!preg_match($this->jsonRegex, $html, $parts)) { + returnServerError('Failed to extract data from page'); + } + + $data = json_decode($parts[1]); + + if ($data === false) { + returnServerError('Failed to decode extracted data'); + } + + return $data; + } + + /** + * Check if channel has a community tab + */ + private function hasCommunityTab($json) { + + foreach ($json->contents->twoColumnBrowseResultsRenderer->tabs as $tab) { + if (isset($tab->tabRenderer) && $tab->tabRenderer->title === 'Community') { + return true; + } + } + + return false; + } + + /** + * Get community tab posts + */ + private function getCommunityPosts($json) { + + foreach ($json->contents->twoColumnBrowseResultsRenderer->tabs as $tab) { + if ($tab->tabRenderer->title === 'Community') { + return $tab->tabRenderer->content->sectionListRenderer->contents[0]->itemSectionRenderer->contents; + } + } + } + + /** + * Get text content for a post + */ + private function getText($runs) { + $text = ''; + + foreach ($runs as $part) { + $text .= $this->formatUrls($part->text); + } + + return nl2br($text); + } + + /** + * Get attachments for posts + */ + private function getAttachments($details) { + $content = ''; + + if (isset($details->backstageAttachment)) { + $attachments = $details->backstageAttachment; + + // Video + if (isset($attachments->videoRenderer) && isset($attachments->videoRenderer->videoId)) { + if (empty($this->itemTitle)) { + $this->itemTitle = $this->feedName . ' posted a video'; + } + + $content = << +EOD; + } + + // Image + if (isset($attachments->backstageImageRenderer)) { + if (empty($this->itemTitle)) { + $this->itemTitle = $this->feedName . ' posted an image'; + } + + $lastThumb = end($attachments->backstageImageRenderer->image->thumbnails); + + $content = <<

+EOD; + } + + // Poll + if (isset($attachments->pollRenderer)) { + if (empty($this->itemTitle)) { + $this->itemTitle = $this->feedName . ' posted a poll'; + } + + $pollChoices = ''; + + foreach ($attachments->pollRenderer->choices as $choice) { + $pollChoices .= <<{$choice->text->runs[0]->text} +EOD; + } + + $content = <<

Poll ({$attachments->pollRenderer->totalVotes->simpleText})

    {$pollChoices}

+EOD; + } + } + + return $content; + } + + /* + Ellipsis text for title + */ + private function ellipsisTitle($text) { + $length = 100; + + if (strlen($text) > $length) { + $text = explode('
', wordwrap($text, $length, '
')); + return $text[0] . '...'; + } + + return $text; + } + + private function formatUrls($content) { + return preg_replace( + '/(http[s]{0,1}\:\/\/[a-zA-Z0-9.\/\?\&=\-_]{4,})/ims', + '$1 ', + $content + ); + } +}