Skip to content

Commit 85df28a

Browse files
committed
feat: add support for channels (WIP)
1 parent 9f4970b commit 85df28a

File tree

2 files changed

+91
-97
lines changed

2 files changed

+91
-97
lines changed

lib/Innertube.js

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ class Innertube {
8383

8484
this.#initMethods();
8585
} else {
86-
throw new Error('Could not retrieve Innertube session due to unknown reasons');
86+
throw new Error('No InnerTubeContext shell provided in ytconfig.');
8787
}
8888
} catch (err) {
8989
this.#retry_count += 1;
@@ -433,7 +433,95 @@ class Innertube {
433433

434434
return details;
435435
}
436-
436+
437+
/**
438+
* Gets info about a given channel. (WIP)
439+
*
440+
* @param {string} id - The id of the channel.
441+
* @return {Promise.<{ title: string; description: string; metadata: object; content: object }>}
442+
*/
443+
async getChannel(id) {
444+
const response = await Actions.browse(this, 'channel', { browse_id: id });
445+
if (!response.success) throw new Error('Could not retrieve channel info.');
446+
447+
const tabs = response.data.contents.twoColumnBrowseResultsRenderer.tabs;
448+
const metadata = response.data.metadata;
449+
450+
const home_tab = tabs.find((tab) => tab.tabRenderer.title == 'Home');
451+
const home_contents = home_tab.tabRenderer.content.sectionListRenderer.contents;
452+
const home_shelves = [];
453+
454+
home_contents.forEach((content) => {
455+
if(!content.itemSectionRenderer) return;
456+
457+
const contents = content.itemSectionRenderer.contents[0];
458+
459+
const list = contents?.shelfRenderer?.content.horizontalListRenderer;
460+
if (!list) return; // For now we'll support only videos & playlists; TODO: Handle featured channels
461+
462+
const shelf = {
463+
title: contents.shelfRenderer.title.runs[0].text,
464+
content: []
465+
};
466+
467+
shelf.content = list.items.map((item) => {
468+
const renderer = item.gridVideoRenderer || item.gridPlaylistRenderer;
469+
if (renderer.videoId) {
470+
return {
471+
id: renderer?.videoId,
472+
title: renderer?.title?.simpleText,
473+
metadata: {
474+
view_count: renderer?.viewCountText?.simpleText || 'N/A',
475+
short_view_count_text: {
476+
simple_text: renderer?.shortViewCountText?.simpleText || 'N/A',
477+
accessibility_label: renderer?.shortViewCountText?.accessibility?.accessibilityData?.label || 'N/A',
478+
},
479+
thumbnail: renderer?.thumbnail?.thumbnails?.slice(-1)[0] || {},
480+
moving_thumbnail: renderer?.richThumbnail?.movingThumbnailRenderer?.movingThumbnailDetails?.thumbnails[0] || {},
481+
published: renderer?.publishedTimeText?.simpleText || 'N/A',
482+
badges: renderer?.badges?.map((badge) => badge.metadataBadgeRenderer.label) || [],
483+
owner_badges: renderer?.ownerBadges?.map((badge) => badge.metadataBadgeRenderer.tooltip) || []
484+
}
485+
}
486+
} else {
487+
return {
488+
id: renderer?.playlistId,
489+
title: renderer?.title?.runs?.map((run) => run.text).join(''),
490+
metadata: {
491+
thumbnail: renderer?.thumbnail?.thumbnails?.slice(-1)[0] || {},
492+
video_count: renderer?.videoCountShortText?.simpleText || 'N/A',
493+
}
494+
}
495+
}
496+
});
497+
home_shelves.push(shelf);
498+
});
499+
500+
return {
501+
title: metadata.channelMetadataRenderer.title,
502+
description: metadata.channelMetadataRenderer.description,
503+
metadata: {
504+
url: metadata.channelMetadataRenderer?.channelUrl,
505+
rss_urls: metadata.channelMetadataRenderer?.rssUrl,
506+
vanity_channel_url: metadata.channelMetadataRenderer?.vanityChannelUrl,
507+
external_id: metadata.channelMetadataRenderer?.externalId,
508+
is_family_safe: metadata.channelMetadataRenderer?.isFamilySafe,
509+
keywords: metadata.channelMetadataRenderer?.keywords
510+
},
511+
content: {
512+
// Home page of the channel, always available in the first request.
513+
home_page: home_shelves,
514+
515+
// Functions— these will need additional requests and will possibly use the parser.
516+
getVideos: () => {},
517+
getPlaylists: () => {},
518+
getCommunity: () => {},
519+
getChannels: () => {},
520+
getAbout: () => {}
521+
}
522+
}
523+
}
524+
437525
/**
438526
* Retrieves the lyrics for a given song if available.
439527
*

lib/Utils.js

Lines changed: 1 addition & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use strict';
22

33
const Fs = require('fs');
4-
const Proto = require('protons');
54
const Crypto = require('crypto');
65
const UserAgent = require('user-agents');
76

@@ -81,99 +80,6 @@ function camelToSnake(string) {
8180
return string[0].toLowerCase() + string.slice(1, string.length).replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
8281
}
8382

84-
/**
85-
* Encodes notification preferences protobuf.
86-
*
87-
* @param {string} channel_id
88-
* @param {string} index
89-
* @returns {string}
90-
*/
91-
function encodeNotificationPref(channel_id, index) {
92-
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`));
93-
94-
const buf = youtube_proto.NotificationPreferences.encode({
95-
channel_id,
96-
pref_id: {
97-
index
98-
},
99-
number_0: 0,
100-
number_1: 4
101-
});
102-
103-
return encodeURIComponent(Buffer.from(buf).toString('base64'));
104-
}
105-
106-
/**
107-
* Encodes livestream message protobuf.
108-
*
109-
* @param {string} channel_id
110-
* @param {string} video_id
111-
* @returns {string}
112-
*/
113-
function encodeMessageParams(channel_id, video_id) {
114-
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`));
115-
116-
const buf = youtube_proto.LiveMessageParams.encode({
117-
params: {
118-
ids: {
119-
channel_id,
120-
video_id
121-
}
122-
},
123-
number_0: 1,
124-
number_1: 4
125-
});
126-
127-
return Buffer.from(encodeURIComponent(Buffer.from(buf).toString('base64'))).toString('base64');
128-
}
129-
130-
/**
131-
* Encodes comment params protobuf.
132-
*
133-
* @param {string} video_id
134-
* @returns {string}
135-
*/
136-
function encodeCommentParams(video_id) {
137-
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`));
138-
139-
const buf = youtube_proto.CreateCommentParams.encode({
140-
video_id,
141-
params: {
142-
index: 0
143-
},
144-
number: 7
145-
});
146-
147-
return encodeURIComponent(Buffer.from(buf).toString('base64'));
148-
}
149-
150-
/**
151-
* Encodes search filter protobuf
152-
*
153-
* @param {string} period - Period in which a video is uploaded: any | hour | day | week | month | year
154-
* @param {string} duration - The duration of a video: any | short | long
155-
* @param {string} order - The order of the search results: relevance | rating | age | views
156-
* @returns {string}
157-
*/
158-
function encodeFilter(period, duration, order) {
159-
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`));
160-
161-
const periods = { 'any': null, 'hour': 1, 'day': 2, 'week': 3, 'month': 4, 'year': 5 };
162-
const durations = { 'any': null, 'short': 1, 'long': 2 };
163-
const orders = { 'relevance': null, 'rating': 1, 'age': 2, 'views': 3 };
164-
165-
const search_filter_buff = youtube_proto.SearchFilter.encode({
166-
number: orders[order],
167-
filter: {
168-
param_0: periods[period],
169-
param_1: (period == 'hour' && order == 'relevance') ? null : 1,
170-
param_2: durations[duration]
171-
}
172-
});
173-
174-
return encodeURIComponent(Buffer.from(search_filter_buff).toString('base64'));
175-
}
176-
17783
/**
17884
* Turns the ntoken transform data into a valid json array
17985
*
@@ -190,4 +96,4 @@ function refineNTokenData(data) {
19096
.replace(/""/g, '').replace(/length]\)}"/g, 'length])}');
19197
}
19298

193-
module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, camelToSnake, timeToSeconds, encodeMessageParams, encodeCommentParams, encodeNotificationPref, encodeFilter, refineNTokenData };
99+
module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, camelToSnake, timeToSeconds, refineNTokenData };

0 commit comments

Comments
 (0)