Skip to content

Commit

Permalink
Add ability to directly retrieve shorts from a given YouTube channel,…
Browse files Browse the repository at this point in the history
… as requested on Discord by 334537760121815040

Used to have to list videos ids of this YouTube channel and then proceed one by one using this method: https://stackoverflow.com/a/71194751
  • Loading branch information
Benjamin-Loison committed Oct 31, 2022
1 parent a9e3fa6 commit 786616f
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 12 deletions.
72 changes: 61 additions & 11 deletions channels.php
Expand Up @@ -5,7 +5,7 @@

include_once 'common.php';

$realOptions = ['snippet', 'premieres', 'community', 'channels', 'about'];
$realOptions = ['snippet', 'premieres', 'shorts', 'community', 'channels', 'about'];

// really necessary ?
foreach ($realOptions as $realOption) {
Expand Down Expand Up @@ -44,7 +44,7 @@
$continuationToken = '';
if (isset($_GET['pageToken'])) {
$continuationToken = $_GET['pageToken'];
if (!isContinuationToken($continuationToken)) {
if (($options['shorts'] && !isContinuationTokenAndVisitorData($continuationToken)) || (!$options['shorts'] && !isContinuationToken($continuationToken))) {
die('invalid continuationToken');
}
}
Expand Down Expand Up @@ -77,6 +77,63 @@ function getItem($id, $continuationToken)
$item['premieres'] = $premieres;
}

if ($options['shorts']) {
// Note that sometimes the `SHORT` tab doesn't work (for instance with https://www.youtube.com/c/unitednations/shorts).
// If we are unlucky, we are redirected to the `HOME` tab.
if (!$continuationTokenProvided) {
$http = [
'header' => [
'Accept-Language: en',
'Cookie: __Secure-YEC=CgtuNjFmZlJlR0Qxcyjp3P-aBg==' // This magic value removes the bad luck as explained above.
]
];

$options = [
'http' => $http
];
$result = getJSONFromHTML('https://www.youtube.com/channel/' . $id . '/shorts', $options);
$visitorData = $result['responseContext']['webResponseContextExtensionData']['ytConfigData']['visitorData'];
} else {
$continuationParts = explode(',', $continuationToken);
$continuationToken = $continuationParts[0];
$visitorData = $continuationParts[1];
$rawData = '{"context":{"client":{"clientName":"WEB","clientVersion":"' . MUSIC_VERSION . '"}},"continuation":"' . $continuationToken . '"}';
$http = [
'header' => [
'Content-Type: application/json',
'X-Goog-EOM-Visitor-Id: ' . $visitorData
],
'method' => 'POST',
'content' => $rawData
];

$options = [
'http' => $http
];

$result = getJSON('https://www.youtube.com/youtubei/v1/browse?key=' . UI_KEY, $options);
}
$shorts = [];
$reelShelfRendererItems = !$continuationTokenProvided ? $result['contents']['twoColumnBrowseResultsRenderer']['tabs'][2]['tabRenderer']['content']['richGridRenderer']['contents'] : $result['onResponseReceivedActions'][0]['appendContinuationItemsAction']['continuationItems'];
foreach($reelShelfRendererItems as $reelShelfRendererItem) {
if(!array_key_exists('richItemRenderer', $reelShelfRendererItem))
continue;
$reelShelfRendererItem = $reelShelfRendererItem['richItemRenderer']['content'];
$reelItemRenderer = $reelShelfRendererItem['reelItemRenderer'];
$viewCount = getIntValue($reelItemRenderer['viewCountText']['simpleText'], 'view');
$short = [
'videoId' => $reelItemRenderer['videoId'],
'title' => $reelItemRenderer['headline']['simpleText'],
'thumbnails' => $reelItemRenderer['thumbnail']['thumbnails'],
'viewCount' => $viewCount,
];
array_push($shorts, $short);
}
$item['shorts'] = $shorts;
if($reelShelfRendererItems != null && count($reelShelfRendererItems) > 48)
$item['nextPageToken'] = str_replace('%3D', '=', $reelShelfRendererItems[48]['continuationItemRenderer']['continuationEndpoint']['continuationCommand']['token'] . ',' . $visitorData);
}

if ($options['community']) {
if (!$continuationTokenProvided) {
$http = [
Expand Down Expand Up @@ -203,15 +260,8 @@ function getItem($id, $continuationToken)
$thumbnail['url'] = 'https://' . substr($thumbnail['url'], 2);
array_push($thumbnails, $thumbnail);
}
$subscriberCount = $gridChannelRenderer['subscriberCountText']['simpleText'];
$subscriberCount = str_replace(' subscribers', '', $subscriberCount);
// Have observed this case for the channel: https://www.youtube.com/channel/UCbOoDorgVGd-4vZdIrU4C1A
$subscriberCount = str_replace(' subscriber', '', $subscriberCount);
$subscriberCount = str_replace('K', '*1000', $subscriberCount);
$subscriberCount = str_replace('M', '*1000000', $subscriberCount);
if(checkRegex('[0-9.*KM]+', $subscriberCount)) {
$subscriberCount = eval('return ' . $subscriberCount . ';');
}
$subscriberCount = getIntValue($gridChannelRenderer['subscriberCountText']['simpleText'], 'subscriber');
// Have observed the singular case for the channel: https://www.youtube.com/channel/UCbOoDorgVGd-4vZdIrU4C1A
$channel = [
'channelId' => $gridChannelRenderer['channelId'],
'title' => $gridChannelRenderer['title']['simpleText'],
Expand Down
17 changes: 17 additions & 0 deletions common.php
Expand Up @@ -91,6 +91,11 @@ function isContinuationToken($continuationToken)
return checkRegex('[A-Za-z0-9=]+', $continuationToken);
}

function isContinuationTokenAndVisitorData($continuationTokenAndVisitorData)
{
return checkRegex('[A-Za-z0-9=]+,[A-Za-z0-9=\-_]+', $continuationTokenAndVisitorData);
}

function isPlaylistId($playlistId)
{
return checkRegex('[a-zA-Z0-9-_]+', $playlistId);
Expand Down Expand Up @@ -167,6 +172,18 @@ function getValue($json, $path)
return getValue($json[$parts[0]], join('/', array_slice($parts, 1, $partsCount - 1)));
}

function getIntValue($unitCount, $unit)
{
$unitCount = str_replace(' ' . $unit . 's', '', $unitCount);
$unitCount = str_replace(' ' . $unit, '', $unitCount);
$unitCount = str_replace('K', '*1000', $unitCount);
$unitCount = str_replace('M', '*1000000', $unitCount);
if(checkRegex('[0-9.*KM]+', $unitCount)) {
$unitCount = eval('return ' . $unitCount . ';');
}
return $unitCount;
}

if (!function_exists('str_contains')) {
function str_contains($haystack, $needle)
{
Expand Down
2 changes: 1 addition & 1 deletion index.php
Expand Up @@ -35,7 +35,7 @@ function feature($feature)
}

// don't know if already written but making a table may be nice
$features = [['channels/list', 'snippet,premieres,community,channels,about&forUsername=USERNAME&id=CHANNEL_ID'], // could use ',' instead of '&' to describe that `forUsername` and `id` have the same aim
$features = [['channels/list', 'snippet,premieres,shorts,community,channels,about&forUsername=USERNAME&id=CHANNEL_ID'], // could use ',' instead of '&' to describe that `forUsername` and `id` have the same aim
['commentThreads/list', 'snippet,replies&videoId=VIDEO_ID(&pageToken=PAGE_TOKEN)'],
['playlists/list', 'statistics&id=PLAYLIST_ID'],
['playlistItems/list', 'snippet&playlistId=PLAYLIST_ID(&pageToken=PAGE_TOKEN)'],
Expand Down

1 comment on commit 786616f

@Benjamin-Loison
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.