This repository has been archived by the owner on Aug 27, 2022. It is now read-only.
forked from RSS-Bridge/rss-bridge
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[YouTubeCommunityTabBridge] Add Bridge (RSS-Bridge#1594)
- Loading branch information
1 parent
e52eab3
commit 0a9ed2c
Showing
1 changed file
with
267 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,267 @@ | ||
<?php | ||
class YouTubeCommunityTabBridge extends BridgeAbstract { | ||
const NAME = 'YouTube Community Tab Bridge'; | ||
const URI = 'https://www.youtube.com'; | ||
const DESCRIPTION = 'Returns posts from a channel\'s community tab'; | ||
const MAINTAINER = 'VerifiedJoseph'; | ||
const PARAMETERS = array( | ||
'By channel ID' => 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><link rel="canonical"/'; | ||
|
||
public function detectParameters($url) { | ||
$params = array(); | ||
|
||
if(preg_match($this->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 | ||
<iframe width="100%" height="410" src="https://www.youtube.com/embed/{$attachments->videoRenderer->videoId}" | ||
frameborder="0" allow="encrypted-media;" allowfullscreen></iframe> | ||
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 | ||
<p><img src="{$lastThumb->url}"></p> | ||
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 .= <<<EOD | ||
<li>{$choice->text->runs[0]->text}</li> | ||
EOD; | ||
} | ||
|
||
$content = <<<EOD | ||
<hr><p>Poll ({$attachments->pollRenderer->totalVotes->simpleText})<br><ul>{$pollChoices}</ul><p> | ||
EOD; | ||
} | ||
} | ||
|
||
return $content; | ||
} | ||
|
||
/* | ||
Ellipsis text for title | ||
*/ | ||
private function ellipsisTitle($text) { | ||
$length = 100; | ||
|
||
if (strlen($text) > $length) { | ||
$text = explode('<br>', wordwrap($text, $length, '<br>')); | ||
return $text[0] . '...'; | ||
} | ||
|
||
return $text; | ||
} | ||
|
||
private function formatUrls($content) { | ||
return preg_replace( | ||
'/(http[s]{0,1}\:\/\/[a-zA-Z0-9.\/\?\&=\-_]{4,})/ims', | ||
'<a target="_blank" href="$1" target="_blank">$1</a> ', | ||
$content | ||
); | ||
} | ||
} |