youtubei.js
Advanced tools
Comparing version 1.3.8 to 1.4.0-b
@@ -30,3 +30,3 @@ 'use strict'; | ||
const video = await youtube.getDetails(search.videos[0].id).catch((error) => error); | ||
const video = await youtube.getDetails(search.videos[0].id); | ||
console.info('Video details:', video); | ||
@@ -33,0 +33,0 @@ |
@@ -5,14 +5,20 @@ 'use strict'; | ||
const Stream = require('stream'); | ||
const OAuth = require('./OAuth'); | ||
const Utils = require('./Utils'); | ||
const Player = require('./Player'); | ||
const Parser = require('./Parser'); | ||
const NToken = require('./NToken'); | ||
const Actions = require('./Actions'); | ||
const Livechat = require('./Livechat'); | ||
const Constants = require('./Constants'); | ||
const SigDecipher = require('./Sig'); | ||
const Parser = require('./parser'); | ||
const CancelToken = Axios.CancelToken; | ||
const EventEmitter = require('events'); | ||
const CancelToken = Axios.CancelToken; | ||
// Core | ||
const OAuth = require('./core/OAuth'); | ||
const Player = require('./core/Player'); | ||
const Actions = require('./core/Actions'); | ||
const Livechat = require('./core/Livechat'); | ||
// Utilities | ||
const Utils = require('./utils/Utils'); | ||
const Constants = require('./utils/Constants'); | ||
// Deciphers | ||
const NToken = require('./deciphers/NToken'); | ||
const SigDecipher = require('./deciphers/Sig'); | ||
class Innertube { | ||
@@ -39,55 +45,56 @@ #player; | ||
const response = await Axios.get(Constants.URLS.YT_BASE, Constants.DEFAULT_HEADERS(this)).catch((error) => error); | ||
if (response instanceof Error) throw new Error(`Could not retrieve Innertube session: ${response.message}`); | ||
if (response instanceof Error) throw new Utils.InnertubeError('Could not retrieve Innertube session', { status_code: response.status || 0 }); | ||
const data = JSON.parse(`{${Utils.getStringBetweenStrings(response.data, 'ytcfg.set({', '});') || ''}}`); | ||
if (data.INNERTUBE_CONTEXT) { | ||
this.key = data.INNERTUBE_API_KEY; | ||
this.version = data.INNERTUBE_API_VERSION; | ||
this.context = data.INNERTUBE_CONTEXT; | ||
try { | ||
const data = JSON.parse(`{${Utils.getStringBetweenStrings(response.data, 'ytcfg.set({', '});')}}`); | ||
if (data.INNERTUBE_CONTEXT) { | ||
this.key = data.INNERTUBE_API_KEY; | ||
this.version = data.INNERTUBE_API_VERSION; | ||
this.context = data.INNERTUBE_CONTEXT; | ||
this.player_url = data.PLAYER_JS_URL; | ||
this.logged_in = data.LOGGED_IN; | ||
this.sts = data.STS; | ||
this.player_url = data.PLAYER_JS_URL; | ||
this.logged_in = data.LOGGED_IN; | ||
this.sts = data.STS; | ||
this.context.client.hl = 'en'; | ||
this.context.client.gl = 'US'; | ||
this.context.client.hl = 'en'; | ||
this.context.client.gl = 'US'; | ||
/** | ||
* @event Innertube#auth - Fired when signing in to an account. | ||
* @event Innertube#update-credentials - Fired when the access token is no longer valid. | ||
* @type {EventEmitter} | ||
*/ | ||
this.ev = new EventEmitter(); | ||
/** | ||
* @event Innertube#auth - Fired when signing in to an account. | ||
* @event Innertube#update-credentials - Fired when the access token is no longer valid. | ||
* @type {EventEmitter} | ||
*/ | ||
this.ev = new EventEmitter(); | ||
this.#player = new Player(this); | ||
await this.#player.init(); | ||
this.#player = new Player(this); | ||
await this.#player.init(); | ||
if (this.logged_in && this.cookie.length) { | ||
this.auth_apisid = Utils.getStringBetweenStrings(this.cookie, 'PAPISID=', ';'); | ||
this.auth_apisid = Utils.generateSidAuth(this.auth_apisid); | ||
} | ||
if (this.logged_in && this.cookie.length) { | ||
this.auth_apisid = Utils.getStringBetweenStrings(this.cookie, 'PAPISID=', ';'); | ||
this.auth_apisid = Utils.generateSidAuth(this.auth_apisid); | ||
} | ||
// Axios instances | ||
this.YTRequester = Axios.create({ | ||
baseURL: Constants.URLS.YT_BASE_API + this.version, | ||
timeout: 15000, | ||
headers: Constants.INNERTUBE_HEADERS({ session: this, ytmusic: false }), | ||
params: { key: this.key } | ||
}); | ||
// Axios instances | ||
this.YTRequester = Axios.create({ | ||
baseURL: Constants.URLS.YT_BASE_API + this.version, | ||
timeout: 15000, | ||
headers: Constants.INNERTUBE_HEADERS({ session: this, ytmusic: false }), | ||
params: { key: this.key } | ||
}); | ||
this.YTMRequester = Axios.create({ | ||
baseURL: Constants.URLS.YT_MUSIC_BASE_API + this.version, | ||
timeout: 15000, | ||
headers: Constants.INNERTUBE_HEADERS({ session: this, ytmusic: true }), | ||
params: { key: this.key } | ||
}); | ||
this.YTMRequester = Axios.create({ | ||
baseURL: Constants.URLS.YT_MUSIC_BASE_API + this.version, | ||
timeout: 15000, | ||
headers: Constants.INNERTUBE_HEADERS({ session: this, ytmusic: true }), | ||
params: { key: this.key } | ||
}); | ||
this.#initMethods(); | ||
} else { | ||
throw new Error('No InnerTubeContext shell provided in ytconfig.'); | ||
} | ||
} catch (err) { | ||
this.#initMethods(); | ||
} else { | ||
this.#retry_count += 1; | ||
if (this.#retry_count >= 10) throw new Error(`Could not retrieve Innertube session: ${err.message}`); | ||
if (this.#retry_count >= 10) | ||
throw new Utils.ParsingError('No InnerTubeContext shell provided in ytconfig.', { | ||
data_snippet: response.data.slice(0, 300), | ||
status_code: response.status || 0 | ||
}); | ||
return this.#init(); | ||
@@ -243,12 +250,12 @@ } | ||
const response = await Actions.browse(this, type); | ||
const contents = response.data.contents.twoColumnBrowseResultsRenderer.tabs[0] | ||
.tabRenderer.content.sectionListRenderer.contents[1] | ||
.itemSectionRenderer.contents.find((content) => content.settingsOptionsRenderer.options) | ||
.settingsOptionsRenderer.options; | ||
const contents = ({ | ||
account_notifications: () => Utils.findNode(response.data, 'contents', 'Your preferences', 13, false).options, | ||
account_privacy: () => Utils.findNode(response.data, 'contents', 'settingsSwitchRenderer', 13, false).options | ||
})[type.trim()](); | ||
const option = contents.find((option) => option.settingsSwitchRenderer.enableServiceEndpoint.setSettingEndpoint.settingItemIdForClient == setting_id); | ||
const setting_item_id = option.settingsSwitchRenderer.enableServiceEndpoint.setSettingEndpoint.settingItemId; | ||
const set_setting = await Actions.account(this, 'account/set_setting', { new_value, setting_item_id }); | ||
const set_setting = await Actions.account(this, 'account/set_setting', { new_value: type == 'account_privacy' ? !new_value : new_value, setting_item_id }); | ||
@@ -323,6 +330,6 @@ return { | ||
const response = await Actions.account(this, 'account/account_menu'); | ||
if (!response.success) throw new Error('Could not get account info'); | ||
const menu = response.data.actions[0].openPopupAction.popup.multiPageMenuRenderer; | ||
if (!response.success) throw new Utils.InnertubeError('Could not get account info', response); | ||
const menu = Utils.findNode(response, 'actions', 'multiPageMenuRenderer', 6, false); | ||
return { | ||
@@ -350,13 +357,11 @@ name: menu.header.activeAccountHeaderRenderer.accountName.simpleText, | ||
const response = await Actions.search(this, options.client, { query, options }); | ||
if (!response.success) throw new Error(`Could not search on YouTube: ${response.message}`); | ||
if (!response.success) throw new Utils.InnertubeError('Could not search on YouTube', response); | ||
const data = new Parser(this, response.data, { | ||
const parsed_data = new Parser(this, response.data, { | ||
client: options.client, | ||
data_type: 'SEARCH', | ||
query | ||
query | ||
}).parse(); | ||
data.getContinuation = () => {}; | ||
return data; | ||
return parsed_data; | ||
} | ||
@@ -375,3 +380,3 @@ | ||
const response = await Actions.getYTSearchSuggestions(this, input); | ||
if (!response.success) throw new Error('Could not get search suggestions'); | ||
if (!response.success) throw new Utils.InnertubeError('Could not get search suggestions', response); | ||
@@ -386,3 +391,4 @@ return response.data[1].map((item) => { | ||
const response = await Actions.music(this, 'get_search_suggestions', { input }); | ||
if (!response.success) throw new Error('Could not get search suggestions'); | ||
if (!response.success) throw new Utils.InnertubeError('Could not get search suggestions', response); | ||
if (!response.data.contents) return []; | ||
@@ -393,9 +399,7 @@ | ||
let suggestion; | ||
item.historySuggestionRenderer && | ||
(suggestion = item.historySuggestionRenderer.suggestion) || | ||
(suggestion = item.searchSuggestionRenderer.suggestion); | ||
if (item.historySuggestionRenderer) { | ||
suggestion = item.historySuggestionRenderer.suggestion; | ||
} else { | ||
suggestion = item.searchSuggestionRenderer.suggestion; | ||
} | ||
return { | ||
@@ -416,3 +420,3 @@ text: suggestion.runs.map((run) => run.text).join('').trim(), | ||
async getDetails(video_id) { | ||
if (!video_id) throw new Error('You must provide a video id'); | ||
if (!video_id) throw new Utils.MissingParamError('Video id is missing'); | ||
@@ -424,3 +428,3 @@ const data = await Actions.getVideoInfo(this, { id: video_id }); | ||
const details = new Parser(this, data, { client: 'YOUTUBE', data_type: 'VIDEO_INFO' }).parse(); | ||
// Functions | ||
@@ -448,3 +452,3 @@ details.like = () => Actions.engage(this, 'like/like', { video_id }); | ||
const response = await Actions.browse(this, 'channel', { browse_id: id }); | ||
if (!response.success) throw new Error('Could not retrieve channel info.'); | ||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve channel info.', response); | ||
@@ -537,3 +541,4 @@ const tabs = response.data.contents.twoColumnBrowseResultsRenderer.tabs; | ||
const continuation = await Actions.next(this, { video_id: video_id, ytmusic: true }); | ||
if (!continuation.success) throw new Utils.InnertubeError('Could not retrieve lyrics', continuation); | ||
const lyrics_tab = continuation.data.contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer | ||
@@ -543,3 +548,3 @@ .watchNextTabbedResultsRenderer.tabs.find((obj) => obj.tabRenderer.title == 'Lyrics'); | ||
const response = await Actions.browse(this, 'lyrics', { ytmusic: true, browse_id: lyrics_tab.tabRenderer.endpoint.browseEndpoint.browseId }); | ||
if (!response.data.contents.sectionListRenderer) throw new Error(response.data.contents.messageRenderer.text.runs[0].text); | ||
if (!response.success || !response.data?.contents?.sectionListRenderer) throw new Utils.UnavailableContentError('Lyrics not available', { video_id }); | ||
@@ -571,3 +576,3 @@ const lyrics = response.data.contents.sectionListRenderer.contents[0].musicDescriptionShelfRenderer.description.runs[0].text; | ||
* @param {string} [data] - Video data and continuation token (optional). | ||
* @return {Promise.<[{ comments: []; comment_count: string }]> | ||
* @return {Promise.<[{ comments: []; comment_count?: string }]> | ||
*/ | ||
@@ -579,9 +584,9 @@ async getComments(video_id, data = {}) { | ||
const continuation = await Actions.next(this, { video_id }); | ||
if (!continuation.success) throw new Utils.InnertubeError('Could not fetch comments section', continuation); | ||
const item_section_renderer = continuation.data.contents.twoColumnWatchNextResults.results.results.contents.find((item) => item.itemSectionRenderer); | ||
comment_section_token = item_section_renderer.itemSectionRenderer.contents[0].continuationItemRenderer.continuationEndpoint.continuationCommand.token; | ||
const secondary_info_renderer = continuation.data.contents.twoColumnWatchNextResults | ||
.results.results.contents.find((item) => item.videoSecondaryInfoRenderer).videoSecondaryInfoRenderer; | ||
const contents = Utils.findNode(continuation.data, 'contents', 'comments-section', 5); | ||
const item_section_renderer = contents.find((item) => item.itemSectionRenderer).itemSectionRenderer; | ||
comment_section_token = item_section_renderer?.contents[0]?.continuationItemRenderer?.continuationEndpoint.continuationCommand.token; | ||
const secondary_info_renderer = contents.find((item) => item.videoSecondaryInfoRenderer).videoSecondaryInfoRenderer; | ||
data.channel_id = secondary_info_renderer.owner.videoOwnerRenderer.navigationEndpoint.browseEndpoint.browseId; | ||
@@ -591,6 +596,6 @@ } | ||
const response = await Actions.next(this, { continuation_token: comment_section_token || data.token }); | ||
if (!response.success) throw new Error('Could not fetch comments section'); | ||
if (!response.success) throw new Utils.InnertubeError('Could not fetch comments section', response); | ||
const comments_section = { comments: [] }; | ||
!data.token && (comments_section.comment_count = response.data.onResponseReceivedEndpoints[0].reloadContinuationItemsCommand.continuationItems && response.data.onResponseReceivedEndpoints[0].reloadContinuationItemsCommand.continuationItems[0].commentsHeaderRenderer.countText.runs[0].text || 'N/A'); | ||
!data.token && (comments_section.comment_count = response.data?.onResponseReceivedEndpoints[0]?.reloadContinuationItemsCommand?.continuationItems[0]?.commentsHeaderRenderer?.countText.runs[0]?.text || 'N/A'); | ||
@@ -656,46 +661,71 @@ let continuation_token; | ||
* Returns your watch history. | ||
* @returns {Promise.<[{ id: string; title: string; channel: string; metadata: {} }]>} | ||
* @returns {Promise.<{ items: [{ date: string; videos: [] }] }>} | ||
*/ | ||
async getHistory() { | ||
const response = await Actions.browse(this, 'history'); | ||
const contents = response.data.contents.twoColumnBrowseResultsRenderer.tabs[0] | ||
.tabRenderer.content.sectionListRenderer.contents; | ||
const history = []; | ||
contents.forEach((section) => { | ||
if (!section.itemSectionRenderer) return; | ||
const section_items = section.itemSectionRenderer.contents; | ||
section_items.forEach((item) => { | ||
const content = { | ||
id: item.videoRenderer.videoId, | ||
title: item.videoRenderer.title.runs.map((run) => run.text).join(' '), | ||
description: item.videoRenderer.descriptionSnippet && item.videoRenderer.descriptionSnippet.runs[0].text || 'N/A', | ||
channel: { | ||
name: item.videoRenderer.shortBylineText && item.videoRenderer.shortBylineText.runs[0].text || 'N/A', | ||
url: item.videoRenderer.shortBylineText && `${Constants.URLS.YT_BASE}${item.videoRenderer.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}` || 'N/A', | ||
}, | ||
metadata: { | ||
view_count: item.videoRenderer.viewCountText && item.videoRenderer.viewCountText.simpleText || 'N/A', | ||
short_view_count_text: { | ||
simple_text: item.videoRenderer.shortViewCountText && item.videoRenderer.shortViewCountText.simpleText || 'N/A', | ||
accessibility_label: item.videoRenderer.shortViewCountText && (item.videoRenderer.shortViewCountText.accessibility && item.videoRenderer.shortViewCountText.accessibility.accessibilityData.label || 'N/A') || 'N/A', | ||
}, | ||
thumbnail: item.videoRenderer.thumbnail && item.videoRenderer.thumbnail.thumbnails.slice(-1)[0] || [], | ||
moving_thumbnail: item.videoRenderer.richThumbnail && item.videoRenderer.richThumbnail.movingThumbnailRenderer.movingThumbnailDetails.thumbnails[0] || [], | ||
published: item.videoRenderer.publishedTimeText && item.videoRenderer.publishedTimeText.simpleText || 'N/A', | ||
duration: { | ||
seconds: Utils.timeToSeconds(item.videoRenderer.lengthText && item.videoRenderer.lengthText.simpleText || '0'), | ||
simple_text: item.videoRenderer.lengthText && item.videoRenderer.lengthText.simpleText || 'N/A', | ||
accessibility_label: item.videoRenderer.lengthText && item.videoRenderer.lengthText.accessibility.accessibilityData.label || 'N/A' | ||
}, | ||
badges: item.videoRenderer.badges && item.videoRenderer.badges.map((badge) => badge.metadataBadgeRenderer.label) || [], | ||
owner_badges: item.videoRenderer.ownerBadges && item.videoRenderer.ownerBadges.map((badge) => badge.metadataBadgeRenderer.tooltip) || [] | ||
} | ||
}; | ||
history.push(content); | ||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve watch history', response); | ||
const contents = Utils.findNode(response, 'contents', 'videoRenderer', 9, false) | ||
const history = { items: [] }; | ||
const parseItems = (contents) => { | ||
contents.forEach((section) => { | ||
if (!section.itemSectionRenderer) return; | ||
const header = section.itemSectionRenderer.header.itemSectionHeaderRenderer.title; | ||
const section_title = header?.simpleText || header?.runs.map((run) => run.text).join(''); | ||
const contents = section.itemSectionRenderer.contents; | ||
const section_items = contents.map((item) => { | ||
return { | ||
id: item?.videoRenderer?.videoId, | ||
title: item?.videoRenderer?.title?.runs?.map((run) => run.text).join(' '), | ||
description: item?.videoRenderer?.descriptionSnippet?.runs[0]?.text || 'N/A', | ||
channel: { | ||
id: item?.videoRenderer?.shortBylineText?.runs[0]?.navigationEndpoint?.browseEndpoint?.browseId, | ||
name: item?.videoRenderer?.shortBylineText?.runs[0]?.text || 'N/A', | ||
url: `${Constants.URLS.YT_BASE}${item?.videoRenderer?.shortBylineText?.runs[0]?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl}` | ||
}, | ||
metadata: { | ||
view_count: item?.videoRenderer?.viewCountText?.simpleText || 'N/A', | ||
short_view_count_text: { | ||
simple_text: item?.videoRenderer?.shortViewCountText?.simpleText || 'N/A', | ||
accessibility_label: item?.videoRenderer?.shortViewCountText?.accessibility?.accessibilityData?.label, | ||
}, | ||
thumbnail: item?.videoRenderer?.thumbnail?.thumbnails?.slice(-1)[0] || [], | ||
moving_thumbnail: item?.videoRenderer?.richThumbnail?.movingThumbnailRenderer?.movingThumbnailDetails?.thumbnails[0] || [], | ||
duration: { | ||
seconds: Utils.timeToSeconds(item?.videoRenderer?.lengthText?.simpleText || '0'), | ||
simple_text: item?.videoRenderer?.lengthText?.simpleText || 'N/A', | ||
accessibility_label: item?.videoRenderer?.lengthText?.accessibility?.accessibilityData?.label || 'N/A' | ||
}, | ||
badges: item?.videoRenderer?.badges?.map((badge) => badge.metadataBadgeRenderer.label) || [], | ||
owner_badges: item?.videoRenderer?.ownerBadges?.map((badge) => badge.metadataBadgeRenderer.tooltip) || [] | ||
} | ||
}; | ||
}); | ||
history.items.push({ | ||
date: section_title, | ||
videos: section_items | ||
}); | ||
}); | ||
}); | ||
return history; | ||
history.getContinuation = async () => { | ||
const citem = contents.find((item) => item.continuationItemRenderer); | ||
const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token; | ||
const response = await Actions.browse(this, 'continuation', { ctoken }); | ||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response); | ||
history.items = []; | ||
return parseItems(response.data.onResponseReceivedActions[0].appendContinuationItemsAction.continuationItems); | ||
} | ||
return history; | ||
} | ||
return parseItems(contents); | ||
} | ||
@@ -705,41 +735,58 @@ | ||
* Returns YouTube's home feed (aka recommendations). | ||
* @returns {Promise.<[{ id: string; title: string; channel: string; metadata: {} }]>} | ||
* @returns {Promise.<{ videos: [{ id: string; title: string; description: string; channel: string; metadata: object }] }>} | ||
*/ | ||
async getHomeFeed() { | ||
const response = await Actions.browse(this, 'home_feed'); | ||
if (!response.success) throw new Error('Could not get home feed'); | ||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve home feed', response); | ||
const contents = response.data.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.richGridRenderer.contents; | ||
const contents = Utils.findNode(response, 'contents', 'videoRenderer', 9, false) | ||
return contents.map((item) => { | ||
const content = item.richItemRenderer && item.richItemRenderer.content.videoRenderer && | ||
item.richItemRenderer.content || undefined; | ||
const parseItems = (contents) => { | ||
const videos = contents.map((item) => { | ||
const content = item.richItemRenderer && item.richItemRenderer.content.videoRenderer && | ||
item.richItemRenderer.content; | ||
if (content) return { | ||
id: content.videoRenderer.videoId, | ||
title: content.videoRenderer.title.runs.map((run) => run.text).join(' '), | ||
description: content.videoRenderer.descriptionSnippet && content.videoRenderer.descriptionSnippet.runs[0].text || 'N/A', | ||
channel: { | ||
name: content.videoRenderer.shortBylineText && content.videoRenderer.shortBylineText.runs[0].text || 'N/A', | ||
url: content.videoRenderer.shortBylineText && `${Constants.URLS.YT_BASE}${content.videoRenderer.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}` || 'N/A', | ||
}, | ||
metadata: { | ||
view_count: content.videoRenderer.viewCountText && content.videoRenderer.viewCountText.simpleText || 'N/A', | ||
short_view_count_text: { | ||
simple_text: content.videoRenderer.shortViewCountText && content.videoRenderer.shortViewCountText.simpleText || 'N/A', | ||
accessibility_label: content.videoRenderer.shortViewCountText && (content.videoRenderer.shortViewCountText.accessibility && content.videoRenderer.shortViewCountText.accessibility.accessibilityData.label || 'N/A') || 'N/A', | ||
}, | ||
thumbnail: content.videoRenderer.thumbnail && content.videoRenderer.thumbnail.thumbnails.slice(-1)[0] || {}, | ||
moving_thumbnail: content.videoRenderer.richThumbnail && content.videoRenderer.richThumbnail.movingThumbnailRenderer.movingThumbnailDetails.thumbnails[0] || {}, | ||
published: content.videoRenderer.publishedTimeText && content.videoRenderer.publishedTimeText.simpleText || 'N/A', | ||
duration: { | ||
seconds: Utils.timeToSeconds(content.videoRenderer.lengthText && content.videoRenderer.lengthText.simpleText || '0'), | ||
simple_text: content.videoRenderer.lengthText && content.videoRenderer.lengthText.simpleText || 'N/A', | ||
accessibility_label: content.videoRenderer.lengthText && content.videoRenderer.lengthText.accessibility.accessibilityData.label || 'N/A' | ||
}, | ||
badges: content.videoRenderer.badges && content.videoRenderer.badges.map((badge) => badge.metadataBadgeRenderer.label) || [], | ||
owner_badges: content.videoRenderer.ownerBadges && content.videoRenderer.ownerBadges.map((badge) => badge.metadataBadgeRenderer.tooltip) || [] | ||
if (content) return { | ||
id: content.videoRenderer.videoId, | ||
title: content.videoRenderer.title.runs.map((run) => run.text).join(' '), | ||
description: content?.videoRenderer?.descriptionSnippet?.runs[0]?.text || 'N/A', | ||
channel: { | ||
id: content?.videoRenderer?.shortBylineText?.runs[0]?.navigationEndpoint?.browseEndpoint?.browseId, | ||
name: content?.videoRenderer?.shortBylineText?.runs[0]?.text || 'N/A', | ||
url: `${Constants.URLS.YT_BASE}${content?.videoRenderer?.shortBylineText?.runs[0]?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl}` | ||
}, | ||
metadata: { | ||
view_count: content?.videoRenderer?.viewCountText?.simpleText || 'N/A', | ||
short_view_count_text: { | ||
simple_text: content?.videoRenderer?.shortViewCountText?.simpleText || 'N/A', | ||
accessibility_label: content?.videoRenderer?.shortViewCountText?.accessibility?.accessibilityData?.label || 'N/A', | ||
}, | ||
thumbnail: content?.videoRenderer?.thumbnail?.thumbnails.slice(-1)[0] || {}, | ||
moving_thumbnail: content?.videoRenderer?.richThumbnail?.movingThumbnailRenderer?.movingThumbnailDetails?.thumbnails[0] || {}, | ||
published: content?.videoRenderer?.publishedTimeText?.simpleText || 'N/A', | ||
duration: { | ||
seconds: Utils.timeToSeconds(content?.videoRenderer?.lengthText?.simpleText || '0'), | ||
simple_text: content?.videoRenderer?.lengthText?.simpleText || 'N/A', | ||
accessibility_label: content?.videoRenderer?.lengthText?.accessibility?.accessibilityData?.label || 'N/A' | ||
}, | ||
badges: content?.videoRenderer?.badges?.map((badge) => badge.metadataBadgeRenderer.label) || [], | ||
owner_badges: content?.videoRenderer?.ownerBadges?.map((badge) => badge.metadataBadgeRenderer.tooltip) || [] | ||
} | ||
} | ||
}).filter((item) => item); | ||
const getContinuation = async () => { | ||
const citem = contents.find((item) => item.continuationItemRenderer); | ||
const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token; | ||
const response = await Actions.browse(this, 'continuation', { ctoken }); | ||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response); | ||
return parseItems(response.data.onResponseReceivedActions[0].appendContinuationItemsAction.continuationItems); | ||
} | ||
}).filter((video) => video); | ||
return { videos, getContinuation }; | ||
} | ||
return parseItems(contents); | ||
} | ||
@@ -749,47 +796,67 @@ | ||
* Returns your subscription feed. | ||
* @returns {Promise.<{ today: []; yesterday: []; this_week: [] }>} | ||
* @returns {Promise.<{ items: [{ date: string; videos: [] }] }>} | ||
*/ | ||
async getSubscriptionsFeed() { | ||
const response = await Actions.browse(this, 'subscriptions_feed'); | ||
if (!response.success) throw new Error('Could not get subscriptions feed'); | ||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve subscriptions feed', response); | ||
const contents = Utils.findNode(response, 'contents', 'contents', 9, false); | ||
const subsfeed = { items: [] }; | ||
const parseItems = (contents) => { | ||
contents.forEach((section) => { | ||
if (!section.itemSectionRenderer) return; | ||
const contents = response.data.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents; | ||
const subscriptions_feed = {}; | ||
contents.forEach((section) => { | ||
if (!section.itemSectionRenderer) return; | ||
const section_contents = section.itemSectionRenderer.contents[0]; | ||
const section_items = section_contents.shelfRenderer.content.gridRenderer.items; | ||
const key = section_contents.shelfRenderer.title.runs[0].text; | ||
subscriptions_feed[key.toLowerCase().replace(/ +/g, '_')] = []; | ||
section_items.forEach((item) => { | ||
const content = { | ||
id: item.gridVideoRenderer.videoId, | ||
title: item.gridVideoRenderer.title.runs.map((run) => run.text).join(' '), | ||
channel: { | ||
name: item.gridVideoRenderer.shortBylineText && item.gridVideoRenderer.shortBylineText.runs[0].text || 'N/A', | ||
url: item.gridVideoRenderer.shortBylineText && `${Constants.URLS.YT_BASE}${item.gridVideoRenderer.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}` || 'N/A', | ||
}, | ||
metadata: { | ||
view_count: item.gridVideoRenderer.viewCountText && item.gridVideoRenderer.viewCountText.simpleText || 'N/A', | ||
short_view_count_text: { | ||
simple_text: item.gridVideoRenderer.shortViewCountText && item.gridVideoRenderer.shortViewCountText.simpleText || 'N/A', | ||
accessibility_label: item.gridVideoRenderer.shortViewCountText && (item.gridVideoRenderer.shortViewCountText.accessibility && item.gridVideoRenderer.shortViewCountText.accessibility.accessibilityData.label || 'N/A') || 'N/A', | ||
}, | ||
thumbnail: item.gridVideoRenderer.thumbnail && item.gridVideoRenderer.thumbnail.thumbnails.slice(-1)[0] || [], | ||
moving_thumbnail: item.gridVideoRenderer.richThumbnail && item.gridVideoRenderer.richThumbnail.movingThumbnailRenderer.movingThumbnailDetails.thumbnails[0] || {}, | ||
published: item.gridVideoRenderer.publishedTimeText && item.gridVideoRenderer.publishedTimeText.simpleText || 'N/A', | ||
badges: item.gridVideoRenderer.badges && item.gridVideoRenderer.badges.map((badge) => badge.metadataBadgeRenderer.label) || [], | ||
owner_badges: item.gridVideoRenderer.ownerBadges && item.gridVideoRenderer.ownerBadges.map((badge) => badge.metadataBadgeRenderer.tooltip) || [] | ||
} | ||
}; | ||
subscriptions_feed[key.toLowerCase().replace(/ +/g, '_')].push(content); | ||
const section_contents = section.itemSectionRenderer.contents[0]; | ||
const section_title = section_contents.shelfRenderer.title.runs[0].text; | ||
const section_items = section_contents.shelfRenderer.content.gridRenderer.items; | ||
const items = section_items.map((item) => { | ||
return { | ||
id: item.gridVideoRenderer.videoId, | ||
title: item?.gridVideoRenderer?.title?.runs?.map((run) => run.text).join(' '), | ||
channel: { | ||
id: item?.gridVideoRenderer?.shortBylineText?.runs[0]?.navigationEndpoint?.browseEndpoint?.browseId, | ||
name: item?.gridVideoRenderer?.shortBylineText?.runs[0]?.text || 'N/A', | ||
url: `${Constants.URLS.YT_BASE}${item?.gridVideoRenderer?.shortBylineText?.runs[0]?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl}` | ||
}, | ||
metadata: { | ||
view_count: item?.gridVideoRenderer?.viewCountText?.simpleText || 'N/A', | ||
short_view_count_text: { | ||
simple_text: item?.gridVideoRenderer?.shortViewCountText?.simpleText || 'N/A', | ||
accessibility_label: item?.gridVideoRenderer?.shortViewCountText?.accessibility?.accessibilityData?.label || 'N/A', | ||
}, | ||
thumbnail: item?.gridVideoRenderer?.thumbnail?.thumbnails.slice(-1)[0] || [], | ||
moving_thumbnail: item?.gridVideoRenderer?.richThumbnail?.movingThumbnailRenderer?.movingThumbnailDetails?.thumbnails[0] || {}, | ||
published: item?.gridVideoRenderer?.publishedTimeText?.simpleText || 'N/A', | ||
badges: item?.gridVideoRenderer?.badges?.map((badge) => badge.metadataBadgeRenderer.label) || [], | ||
owner_badges: item?.gridVideoRenderer?.ownerBadges?.map((badge) => badge.metadataBadgeRenderer.tooltip) || [] | ||
} | ||
}; | ||
}); | ||
subsfeed.items.push({ | ||
date: section_title, | ||
videos: items | ||
}); | ||
}); | ||
}); | ||
return subscriptions_feed; | ||
subsfeed.getContinuation = async () => { | ||
const citem = contents.find((item) => item.continuationItemRenderer); | ||
const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token; | ||
const response = await Actions.browse(this, 'continuation', { ctoken }); | ||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response); | ||
const ccontents = Utils.findNode(response.data, 'onResponseReceivedActions', 'itemSectionRenderer', 4, false); | ||
subsfeed.items = []; | ||
return parseItems(ccontents); | ||
} | ||
return subsfeed; | ||
}; | ||
return parseItems(contents); | ||
} | ||
@@ -799,24 +866,41 @@ | ||
* Retrieves your notifications. | ||
* @returns {Promise.<[{ title: string; sent_time: string; channel_name: string; channel_thumbnail: {}; video_thumbnail: {}; video_url: string; read: boolean; notification_id: string }]>} | ||
* @returns {Promise.<{ items: [{ title: string; sent_time: string; channel_name: string; channel_thumbnail: {}; video_thumbnail: {}; video_url: string; read: boolean; notification_id: string }] }>} | ||
*/ | ||
async getNotifications() { | ||
const response = await Actions.notifications(this, 'get_notification_menu'); | ||
if (!response.success) throw new Error('Could not fetch notifications'); | ||
if (!response.success) throw new Utils.InnertubeError('Could not fetch notifications', response); | ||
const contents = response.data.actions[0].openPopupAction.popup.multiPageMenuRenderer.sections[0]; | ||
if (!contents.multiPageMenuNotificationSectionRenderer) return { error: 'You don\'t have any notification.' }; | ||
return contents.multiPageMenuNotificationSectionRenderer.items.map((notification) => { | ||
if (!notification.notificationRenderer) return; | ||
notification = notification.notificationRenderer; | ||
return { | ||
title: notification.shortMessage.simpleText, | ||
sent_time: notification.sentTimeText.simpleText, | ||
channel_name: notification.contextualMenu.menuRenderer.items[1].menuServiceItemRenderer.text.runs[1].text, | ||
channel_thumbnail: notification.thumbnail.thumbnails[0], | ||
video_thumbnail: notification.videoThumbnail.thumbnails[0], | ||
video_url: `https://youtu.be/${notification.navigationEndpoint.watchEndpoint.videoId}`, | ||
read: notification.read, | ||
notification_id: notification.notificationId, | ||
}; | ||
}).filter((notification) => notification); | ||
if (!contents.multiPageMenuNotificationSectionRenderer) throw new Utils.InnertubeError('No notifications', response); | ||
const parseItems = (items) => { | ||
const parsed_items = items.map((notification) => { | ||
if (!notification.notificationRenderer) return; | ||
notification = notification.notificationRenderer; | ||
return { | ||
title: notification?.shortMessage?.simpleText, | ||
sent_time: notification?.sentTimeText?.simpleText, | ||
channel_name: notification?.contextualMenu?.menuRenderer?.items[1]?.menuServiceItemRenderer?.text?.runs[1]?.text || 'N/A', | ||
channel_thumbnail: notification?.thumbnail?.thumbnails[0], | ||
video_thumbnail: notification?.videoThumbnail?.thumbnails[0], | ||
video_url: notification.navigationEndpoint.watchEndpoint && `https://youtu.be/${notification.navigationEndpoint.watchEndpoint.videoId}` || 'N/A', | ||
read: notification.read, | ||
notification_id: notification.notificationId, | ||
}; | ||
}).filter((notification) => notification); | ||
const getContinuation = async () => { | ||
const citem = items.find((item) => item.continuationItemRenderer); | ||
const ctoken = citem?.continuationItemRenderer?.continuationEndpoint?.getNotificationMenuEndpoint?.ctoken; | ||
const response = await Actions.notifications(this, 'get_notification_menu', { ctoken }); | ||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response); | ||
return parseItems(response.data.actions[0].appendContinuationItemsAction.continuationItems); | ||
} | ||
return { items: parsed_items, getContinuation }; | ||
} | ||
return parseItems(contents.multiPageMenuNotificationSectionRenderer.items); | ||
} | ||
@@ -830,3 +914,3 @@ | ||
const response = await Actions.notifications(this, 'get_unseen_count'); | ||
if (!response.success) throw new Error('Could not get unseen notifications count'); | ||
if (!response.success) throw new Utils.InnertubeError('Could not get unseen notifications count', response); | ||
return response.data.unseenCount; | ||
@@ -861,3 +945,3 @@ } | ||
if (url_components.searchParams.get('n')) { | ||
url_components.searchParams.set('n', new NToken(this.#player.ntoken_sc).transform(url_components.searchParams.get('n'))); | ||
url_components.searchParams.set('n', new NToken(this.#player.ntoken_sc, url_components.searchParams.get('n')).transform()); | ||
} | ||
@@ -923,3 +1007,3 @@ | ||
const streaming_data = this.#chooseFormat(options, data); | ||
if (!streaming_data.selected_format) throw new Error('Could not find any suitable format.'); | ||
if (!streaming_data.selected_format) throw new Utils.NoStreamingDataError('Could not find any suitable format.', { id, options }); | ||
@@ -940,3 +1024,3 @@ return streaming_data; | ||
download(id, options = {}) { | ||
if (!id) throw new Error('Missing video id'); | ||
if (!id) throw new Utils.MissingParamError('Video id is missing'); | ||
@@ -961,4 +1045,4 @@ options.quality = options.quality || '360p'; | ||
return stream.emit('error', { message: 'Could not find any suitable format.', type: 'FORMAT_UNAVAILABLE' }); | ||
const video_details = new Parser(this, video_data, { client: 'YOUTUBE', data_type: 'VIDEO_INFO', desktop_v: true }).parse(); | ||
const video_details = new Parser(this, video_data, { client: 'YOUTUBE', data_type: 'VIDEO_INFO' }).parse(); | ||
stream.emit('info', { video_details, selected_format: format, formats }); | ||
@@ -965,0 +1049,0 @@ |
{ | ||
"name": "youtubei.js", | ||
"version": "1.3.8", | ||
"description": "An object-oriented library that allows you to search, get detailed info about videos, subscribe, unsubscribe, like, dislike, comment, download videos and much more!", | ||
"version": "1.4.0-b", | ||
"description": "A full-featured library that allows you to get detailed info about any video, subscribe, unsubscribe, like, dislike, comment, search, download videos/music and much more!", | ||
"main": "index.js", | ||
"author": "LuanRT <luan.lrt4@gmail.com> (https://github.com/LuanRT)", | ||
"funding": "https://ko-fi.com/luanrt", | ||
"license": "MIT", | ||
"engines": { | ||
"node": ">=14" | ||
}, | ||
"scripts": { | ||
"test": "node test" | ||
}, | ||
"author": "LuanRT", | ||
"funding": "https://ko-fi.com/luanrt", | ||
"license": "MIT", | ||
"directories": { | ||
@@ -18,2 +21,3 @@ "example": "examples", | ||
"axios": "^0.21.4", | ||
"flat": "^5.0.2", | ||
"protons": "^2.0.3", | ||
@@ -27,2 +31,6 @@ "user-agents": "^1.0.778", | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/LuanRT/YouTube.js/issues" | ||
}, | ||
"homepage": "https://github.com/LuanRT/YouTube.js#readme", | ||
"keywords": [ | ||
@@ -37,4 +45,4 @@ "yt", | ||
"innertubeapi", | ||
"downloader", | ||
"livechat", | ||
"downloader", | ||
"dislike", | ||
@@ -46,7 +54,3 @@ "search", | ||
"dl" | ||
], | ||
"bugs": { | ||
"url": "https://github.com/LuanRT/YouTube.js/issues" | ||
}, | ||
"homepage": "https://github.com/LuanRT/YouTube.js#readme" | ||
] | ||
} |
760
README.md
<h1 align=center>YouTube.js</h1> | ||
<p align=center><i>An object-oriented wrapper around the Innertube API, which is what YouTube itself uses.</i><p> | ||
<p align=center> | ||
<a href=https://github.com/LuanRT/YouTube.js/issues>Report Bug</a> | ||
<i>A full-featured wrapper around the Innertube API, which is what YouTube itself uses.</i> | ||
<p> | ||
<p align=center> | ||
<a href="https://github.com/LuanRT/YouTube.js/issues">Report Bug</a> | ||
· | ||
<a href=https://github.com/LuanRT/YouTube.js/issues>Request Feature | ||
</a> | ||
<br/> | ||
<br/> | ||
<img src=https://github.com/LuanRT/YouTube.js/actions/workflows/node.js.yml/badge.svg> | ||
<img src=https://img.shields.io/npm/v/youtubei.js?color=%2335C757> | ||
<img src=https://www.codefactor.io/repository/github/luanrt/youtube.js/badge> | ||
<a href="https://github.com/LuanRT/YouTube.js/issues">Request Feature</a> | ||
<br/> | ||
<br/> | ||
<!-- PROJECT SHIELDS --> | ||
<img src="https://github.com/LuanRT/YouTube.js/actions/workflows/node.js.yml/badge.svg"> | ||
<img src="https://img.shields.io/npm/v/youtubei.js?color=%2335C757"> | ||
<img src="https://www.codefactor.io/repository/github/luanrt/youtube.js/badge"> | ||
<img src="https://img.shields.io/npm/dm/youtubei.js"> | ||
<a href="https://saythanks.io/to/LuanRT"> | ||
<img src="https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg"> | ||
</a> | ||
</p> | ||
<!-- TABLE OF CONTENTS --> | ||
<details> | ||
<summary>Table of Contents</summary> | ||
<ol> | ||
<li> | ||
<a href="#about">About The Project</a> | ||
<ul> | ||
<li><a href="#features">Features</a></li> | ||
</ul> | ||
</li> | ||
<li> | ||
<a href="#getting-started">Getting Started</a> | ||
<ul> | ||
<li><a href="#prerequisites">Prerequisites</a></li> | ||
<li><a href="#installation">Installation</a></li> | ||
</ul> | ||
</li> | ||
<li> | ||
<a href="#usage">Usage</a> | ||
<ul> | ||
<li><a href="#interactions">Interactions</a></li> | ||
<li><a href="#live-chats">Livechats</a></li> | ||
<li><a href="#downloading-videos">Downloading videos</a></li> | ||
<li><a href="#signing-in">Signing in</a></li> | ||
</ul> | ||
</li> | ||
<li><a href="#contributing">Contributing</a></li> | ||
<li><a href="#license">License</a></li> | ||
<li><a href="#contact">Contact</a></li> | ||
<li><a href="#disclaimer">Disclaimer</a></li> | ||
</ol> | ||
</details> | ||
<!-- ABOUT THE PROJECT --> | ||
## About | ||
Innertube is an API used across all YouTube clients, it was [made to simplify](https://gizmodo.com/how-project-innertube-helped-pull-youtube-out-of-the-gu-1704946491) the internal structure of the platform and make it easy to push updates. This library takes advantage of that API, therefore providing a simple & efficient way to interact with YouTube programmatically. | ||
@@ -22,3 +65,3 @@ | ||
#### What can it do? | ||
### Features | ||
@@ -30,5 +73,2 @@ As of now, this is one of the most advanced & stable YouTube libraries out there, here's a short summary of its features: | ||
- Fetch live chat & live stats in real time | ||
- Get notifications | ||
- Get watch history | ||
- Get subscriptions/home feed | ||
- Change notification preferences for a channel | ||
@@ -38,2 +78,5 @@ - Subscribe/Unsubscribe/Like/Dislike/Comment etc | ||
- Change an account's settings. | ||
- Get subscriptions/home feed | ||
- Get notifications | ||
- Get watch history | ||
- Download videos | ||
@@ -43,26 +86,31 @@ | ||
#### Do I need an API key to use this? | ||
### Do I need an API key to use this? | ||
No, YouTube.js does not use any official API so no API keys are required. | ||
## Installation | ||
<!-- GETTING STARTED --> | ||
## Getting Started | ||
```bash | ||
npm install youtubei.js | ||
``` | ||
### Prerequisites | ||
- [NodeJS](https://nodejs.org) v14 or greater | ||
## Usage | ||
To verify things are set up | ||
properly, you can run this: | ||
```bash | ||
node --version | ||
``` | ||
[1. Getting Started](#usage) | ||
[2. Interactions](#interactions) | ||
### Installation | ||
- NPM: | ||
```bash | ||
npm install youtubei.js@latest | ||
``` | ||
- Yarn: | ||
```bash | ||
yarn add youtubei.js@latest | ||
``` | ||
[3. Live chats](#fetching-live-chats) | ||
[4. Downloading videos](#downloading-videos) | ||
[5. Signing-in](#signing-in) | ||
[6. Disclaimer](#disclaimer) | ||
<!-- USAGE --> | ||
## Usage | ||
First of all we're gonna start by initializing the Innertube instance. | ||
@@ -76,9 +124,11 @@ And to make things faster, you should do this only once and reuse the Innertube object when needed. | ||
Doing a simple search: | ||
### Doing a simple search | ||
YouTube: | ||
```js | ||
// YouTube: | ||
const search = await youtube.search('Looking for life on Mars - Documentary'); | ||
``` | ||
// YTMusic: | ||
YTMusic: | ||
```js | ||
const search = await youtube.search('Interstellar Main Theme', { client: 'YTMUSIC' }); | ||
@@ -88,3 +138,3 @@ ``` | ||
<details> | ||
<summary>YouTube Search Output</summary> | ||
<summary>YouTube Output</summary> | ||
<p> | ||
@@ -100,5 +150,5 @@ | ||
id: string, | ||
url: string, | ||
title: string, | ||
description: string, | ||
url: string, | ||
metadata:{ | ||
@@ -130,3 +180,3 @@ view_count: string, | ||
<details> | ||
<summary>YouTube Music Search Output</summary> | ||
<summary>YTMusic Output</summary> | ||
<p> | ||
@@ -137,57 +187,70 @@ | ||
{ | ||
songs: [ | ||
{ | ||
id: string, | ||
title: string, | ||
artist: string, | ||
album: string, | ||
duration: string, | ||
thumbnail: { | ||
thumbnails: [Array] | ||
} | ||
}, | ||
//... | ||
], | ||
videos: [ | ||
{ | ||
id: string, | ||
title: string, | ||
author: string, | ||
views: string, | ||
duration: string, | ||
thumbnail: { | ||
thumbnails: [Array] | ||
} | ||
} | ||
//... | ||
], | ||
albums: [ | ||
{ | ||
title: string, | ||
author: string, | ||
year: string, | ||
thumbnail: { | ||
thumbnails: [Array] | ||
} | ||
}, | ||
//... | ||
], | ||
playlists: [ | ||
{ | ||
title: string, | ||
description: string, | ||
total_items: number, | ||
duration: string, | ||
year: string, | ||
items: [ | ||
{ | ||
id: string, | ||
title: string, | ||
author: string, | ||
duration: string, | ||
thumbnail: [Array] | ||
} | ||
] | ||
} | ||
] | ||
query:string, | ||
corrected_query:string, | ||
results:{ | ||
top_result:[Array], // Can be anything; video, playlist, artist etc.. | ||
songs:[ | ||
{ | ||
id:string, | ||
title:string, | ||
artist:string, | ||
album:string, | ||
duration:string, | ||
thumbnails:[ | ||
Array | ||
] | ||
}, | ||
//... | ||
], | ||
videos:[ | ||
{ | ||
id:string, | ||
title:string, | ||
author:string, | ||
views:string, | ||
duration:string, | ||
thumbnails:[Array] | ||
}, | ||
//... | ||
], | ||
albums:[ | ||
{ | ||
id:string, | ||
title:string, | ||
author:string, | ||
year:string, | ||
thumbnails:[Array] | ||
}, | ||
//... | ||
], | ||
featured_playlists:[ | ||
{ | ||
id:string, | ||
title:string, | ||
author:string, | ||
channel_id:string, | ||
total_items:number | ||
}, | ||
//... | ||
], | ||
community_playlists:[ | ||
{ | ||
id:string, | ||
title:string, | ||
author:string, | ||
channel_id:string, | ||
total_items:number | ||
}, | ||
//... | ||
], | ||
artists:[ | ||
{ | ||
id:string, | ||
name:string, | ||
subscribers:string, | ||
thumbnails:[Array] | ||
}, | ||
//... | ||
] | ||
} | ||
} | ||
@@ -200,3 +263,3 @@ ``` | ||
Get search suggestions: | ||
### Get search suggestions: | ||
```js | ||
@@ -224,3 +287,3 @@ const suggestions = await youtube.getSearchSuggestions('QUERY', { | ||
Get details about a given video: | ||
### Get video info: | ||
@@ -285,23 +348,13 @@ ```js | ||
Get comments: | ||
### Get comments: | ||
```js | ||
const response = await youtube.getComments('VIDEO_ID'); | ||
``` | ||
Alternatively you can use: | ||
// Or: | ||
```js | ||
const video = await youtube.getDetails('VIDEO_ID'); | ||
const response = await video.getComments(); | ||
// Get comment replies: | ||
const replies = await response.comments[0].getReplies(); | ||
// Like, dislike, reply (same logic for replies): | ||
await response.comments[0].like(); | ||
await response.comments[0].dislike(); | ||
await response.comments[0].reply('Nice comment!'); | ||
// Get comments continuation (same logic for replies): | ||
const continuation = await response.getContinuation(); | ||
``` | ||
<details> | ||
@@ -347,52 +400,24 @@ <summary>Output</summary> | ||
Get home feed: | ||
Reply to, like and dislike comments: | ||
```js | ||
const homefeed = await youtube.getHomeFeed(); | ||
await response.comments[0].like(); | ||
await response.comments[0].dislike(); | ||
await response.comments[0].reply('Nice comment!'); | ||
``` | ||
<details> | ||
<summary>Output</summary> | ||
<p> | ||
Get comment replies: | ||
```js | ||
[ | ||
{ | ||
id: string, | ||
title: string, | ||
description: string, | ||
channel: string, | ||
metadata: { | ||
view_count: string, | ||
short_view_count_text: { simple_text: string, accessibility_label: string }, | ||
thumbnail: { | ||
url: string, | ||
width: number, | ||
height: number | ||
}, | ||
moving_thumbnail: { | ||
url: string, | ||
width: number, | ||
height: number | ||
}, | ||
published: string, | ||
duration: { | ||
seconds: number, | ||
simple_text: string, | ||
accessibility_label: string | ||
}, | ||
badges: string, | ||
owner_badges: [Array] | ||
} | ||
} | ||
// ... | ||
] | ||
const replies = await response.comments[0].getReplies(); | ||
``` | ||
</p> | ||
</details> | ||
Get comments/replies continuation: | ||
```js | ||
const continuation = await response.getContinuation(); | ||
const replies_continuation = await replies.getContinuation(); | ||
``` | ||
Get subscriptions feed: | ||
### Get home feed: | ||
```js | ||
const mysubsfeed = await youtube.getSubscriptionsFeed(); | ||
const homefeed = await youtube.getHomeFeed(); | ||
``` | ||
<details> | ||
@@ -404,11 +429,16 @@ <summary>Output</summary> | ||
{ | ||
today: [ | ||
{ | ||
id: string, | ||
title: string, | ||
channel: string, | ||
metadata: { | ||
view_count: string, | ||
short_view_count_text: { simple_text: string, accessibility_label: string }, | ||
thumbnail: { | ||
videos: [ | ||
{ | ||
id: string, | ||
title: string, | ||
description: string, | ||
channel: { | ||
id: string, | ||
name: string, | ||
url: string | ||
}, | ||
metadata: { | ||
view_count: string, | ||
short_view_count_text: { simple_text: string, accessibility_label: string }, | ||
thumbnail: { | ||
url: string, | ||
@@ -424,57 +454,75 @@ width: number, | ||
published: string, | ||
duration: { | ||
seconds: number, | ||
simple_text: string, | ||
accessibility_label: string | ||
}, | ||
badges: string, | ||
owner_badges: [Array] | ||
} | ||
} | ||
//... | ||
], | ||
yesterday: [ | ||
} | ||
}, | ||
// ... | ||
] | ||
} | ||
``` | ||
</p> | ||
</details> | ||
Get continuation: | ||
```js | ||
const continuation = await homefeed.getContinuation(); | ||
```` | ||
### Get watch history: | ||
```js | ||
const history = await youtube.getHistory(); | ||
``` | ||
<details> | ||
<summary>Output</summary> | ||
<p> | ||
```js | ||
{ | ||
items: [ | ||
{ | ||
id: string, | ||
title: string, | ||
channel: string, | ||
metadata: { | ||
view_count: string, | ||
short_view_count_text: { simple_text: string, accessibility_label: string }, | ||
thumbnail: { | ||
url: string, | ||
width: number, | ||
height: number | ||
date: string, | ||
videos: [ | ||
{ | ||
id: string, | ||
title: string, | ||
channel: { | ||
id: string, | ||
name: string, | ||
url: string | ||
}, | ||
metadata: { | ||
view_count: string, | ||
short_view_count_text: { | ||
simple_text: string, | ||
accessibility_label: string | ||
}, | ||
thumbnail: { | ||
url: string, | ||
width: number, | ||
height: number | ||
}, | ||
moving_thumbnail: { | ||
url: string, | ||
width: number, | ||
height: number | ||
}, | ||
published: string, | ||
badges: [Array], | ||
owner_badges: [Array] | ||
} | ||
}, | ||
moving_thumbnail: { | ||
url: string, | ||
width: number, | ||
height: number | ||
}, | ||
published: string, | ||
badges: string, | ||
owner_badges: [Array] | ||
} | ||
} | ||
//... | ||
] | ||
}, | ||
//... | ||
], | ||
this_week: [ | ||
id: string, | ||
title: string, | ||
channel: string, | ||
metadata: { | ||
view_count: string, | ||
thumbnail: { | ||
url: string, | ||
width: number, | ||
height: number | ||
}, | ||
moving_thumbnail: { | ||
url: string, | ||
width: number, | ||
height: number | ||
}, | ||
published: string, | ||
badges: string, | ||
owner_badges: [Array] | ||
} | ||
} | ||
// ... | ||
] | ||
} | ||
} | ||
``` | ||
@@ -485,8 +533,12 @@ | ||
Get watch history: | ||
Get continuation: | ||
```js | ||
const history = await youtube.getHistory(); | ||
const continuation = await history.getContinuation(); | ||
```` | ||
### Get subscriptions feed: | ||
```js | ||
const mysubsfeed = await youtube.getSubscriptionsFeed(); | ||
``` | ||
<details> | ||
@@ -497,35 +549,43 @@ <summary>Output</summary> | ||
```js | ||
[ | ||
{ | ||
id: string, | ||
title: string, | ||
description: string, | ||
channel: { | ||
name: string, | ||
url: string | ||
}, | ||
metadata: { | ||
view_count: string, | ||
short_view_count_text: { simple_text: string, accessibility_label: string }, | ||
thumbnail: { | ||
url: string, | ||
width: number, | ||
height: number | ||
{ | ||
items: [ | ||
{ | ||
date: string, | ||
videos: [ | ||
{ | ||
id: string, | ||
title: string, | ||
description: string, | ||
channel: { | ||
id: string, | ||
name: string, | ||
url: string | ||
}, | ||
metadata: { | ||
view_count: string, | ||
short_view_count_text: { | ||
simple_text: string, | ||
accessibility_label: string | ||
}, | ||
thumbnail: { | ||
url: string, | ||
width: number, | ||
height: number | ||
}, | ||
moving_thumbnail: { | ||
url: string, | ||
width: number, | ||
height: number | ||
}, | ||
published: string, | ||
badges: [Array], | ||
owner_badges: [Array] | ||
} | ||
}, | ||
//... | ||
] | ||
}, | ||
moving_thumbnail: { | ||
url: string, | ||
width: number, | ||
height: number | ||
}, | ||
published: string, | ||
duration: { | ||
seconds: number, | ||
simple_text: string, | ||
accessibility_label: string | ||
}, | ||
badges: string, | ||
owner_badges: [Array] | ||
} | ||
} | ||
] | ||
//... | ||
] | ||
} | ||
``` | ||
@@ -536,4 +596,9 @@ | ||
Get notifications: | ||
Get continuation: | ||
```js | ||
const continuation = await mysubsfeed.getContinuation(); | ||
```` | ||
### Get notifications: | ||
```js | ||
@@ -548,4 +613,5 @@ const notifications = await youtube.getNotifications(); | ||
```js | ||
[ | ||
{ | ||
{ | ||
items: [ | ||
{ | ||
title: string, | ||
@@ -567,5 +633,6 @@ sent_time: string, | ||
notification_id: string | ||
}, | ||
//... | ||
] | ||
}, | ||
//... | ||
] | ||
} | ||
``` | ||
@@ -576,4 +643,9 @@ | ||
Get unseen notifications count: | ||
Get continuation: | ||
```js | ||
const continuation = await notifications.getContinuation(); | ||
```` | ||
### Get unseen notifications count: | ||
```js | ||
@@ -583,26 +655,23 @@ const notifications = await youtube.getUnseenNotificationsCount(); | ||
Get song lyrics: | ||
### Get song lyrics: | ||
```js | ||
const search = await youtube.search('Never give you up', { client: 'YTMUSIC' }); | ||
const lyrics = await youtube.getLyrics(search.songs[0].id); | ||
const lyrics = await youtube.getLyrics(search.results.songs[0].id); | ||
``` | ||
Get playlist: | ||
### Get playlist: | ||
YouTube (default): | ||
```js | ||
const search = await youtube.search('Interstellar Soundtrack', { | ||
client: 'YTMUSIC' | ||
}); | ||
const playlist = await youtube.getPlaylist('PLAYLIST_ID'); | ||
``` | ||
// YouTube Music | ||
const playlist = await youtube.getPlaylist(search.playlists[0].id, { | ||
YouTube Music: | ||
```js | ||
const playlist = await youtube.getPlaylist('PLAYLIST_ID', { | ||
client: 'YTMUSIC' | ||
}); | ||
// YouTube (default) | ||
const playlist = await youtube.getPlaylist(search.playlists[0].id); | ||
``` | ||
<details> | ||
<summary>YouTube Music Output</summary> | ||
<summary>YouTube Output</summary> | ||
<p> | ||
@@ -614,5 +683,5 @@ | ||
description: string, | ||
total_items: number, | ||
duration: string, | ||
year: string, | ||
total_items: string, | ||
last_updated: string, | ||
views: string, | ||
items: [ | ||
@@ -623,6 +692,11 @@ { | ||
author: string, | ||
duration: string, | ||
thumbnail: [Array] | ||
duration: { | ||
seconds: number, | ||
simple_text: string, | ||
accessibility_label: string | ||
}, | ||
thumbnails: [Array] | ||
}, | ||
//... | ||
] | ||
} | ||
@@ -635,3 +709,3 @@ ``` | ||
<details> | ||
<summary>YouTube Output</summary> | ||
<summary>YouTube Music Output</summary> | ||
<p> | ||
@@ -643,5 +717,5 @@ | ||
description: string, | ||
total_items: string, | ||
last_updated: string, | ||
views: string, | ||
total_items: number, | ||
duration: string, | ||
year: string, | ||
items: [ | ||
@@ -654,9 +728,7 @@ { | ||
seconds: number, | ||
simple_text: string, | ||
accessibility_label: string | ||
simple_text: string | ||
}, | ||
thumbnail: [Array] | ||
thumbnails: [Array] | ||
}, | ||
//... | ||
] | ||
} | ||
@@ -674,25 +746,24 @@ ``` | ||
* Subscribe/Unsubscribe: | ||
```js | ||
await youtube.interact.subscribe('CHANNEL_ID'); | ||
await youtube.interact.unsubscribe('CHANNEL_ID'); | ||
``` | ||
```js | ||
await youtube.interact.subscribe('CHANNEL_ID'); | ||
await youtube.interact.unsubscribe('CHANNEL_ID'); | ||
``` | ||
* Like/Dislike: | ||
```js | ||
await youtube.interact.like('VIDEO_ID'); | ||
await youtube.interact.dislike('VIDEO_ID'); | ||
await youtube.interact.removeLike('VIDEO_ID'); | ||
``` | ||
```js | ||
await youtube.interact.like('VIDEO_ID'); | ||
await youtube.interact.dislike('VIDEO_ID'); | ||
await youtube.interact.removeLike('VIDEO_ID'); | ||
``` | ||
* Comment: | ||
```js | ||
await youtube.interact.comment('VIDEO_ID', 'Haha, nice video!'); | ||
``` | ||
```js | ||
await youtube.interact.comment('VIDEO_ID', 'Haha, nice video!'); | ||
``` | ||
* Change notification preferences: | ||
```js | ||
// Options: ALL | NONE | PERSONALIZED | ||
await youtube.interact.changeNotificationPreferences('CHANNEL_ID', 'ALL'); | ||
``` | ||
```js | ||
// Options: ALL | NONE | PERSONALIZED | ||
await youtube.interact.changeNotificationPreferences('CHANNEL_ID', 'ALL'); | ||
``` | ||
@@ -705,26 +776,26 @@ These methods will always return ```{ success: true, status_code: 200 }``` if successful. | ||
* Get account info: | ||
```js | ||
await youtube.account.info(); | ||
``` | ||
```js | ||
await youtube.account.info(); | ||
``` | ||
<details> | ||
<summary>Output</summary> | ||
<p> | ||
<details> | ||
<summary>Output</summary> | ||
<p> | ||
```js | ||
{ | ||
name: string, | ||
photo: [ | ||
{ | ||
url: string, | ||
width: number, | ||
height: number | ||
} | ||
], | ||
country: string, | ||
language: string; | ||
} | ||
``` | ||
</p> | ||
</details> | ||
```js | ||
{ | ||
name: string, | ||
photo: [ | ||
{ | ||
url: string, | ||
width: number, | ||
height: number | ||
} | ||
], | ||
country: string, | ||
language: string; | ||
} | ||
``` | ||
</p> | ||
</details> | ||
@@ -736,25 +807,25 @@ <br> | ||
* Subscription notifications: | ||
```js | ||
await youtube.account.settings.notifications.setSubscriptions(true); | ||
``` | ||
```js | ||
await youtube.account.settings.notifications.setSubscriptions(true); | ||
``` | ||
* Recommended content notifications: | ||
```js | ||
await youtube.account.settings.notifications.setRecommendedVideos(true); | ||
``` | ||
```js | ||
await youtube.account.settings.notifications.setRecommendedVideos(true); | ||
``` | ||
* Channel activity notifications: | ||
```js | ||
await youtube.account.settings.notifications.setChannelActivity(true); | ||
``` | ||
```js | ||
await youtube.account.settings.notifications.setChannelActivity(true); | ||
``` | ||
* Comment replies notifications: | ||
```js | ||
await youtube.account.settings.notifications.setCommentReplies(true); | ||
``` | ||
```js | ||
await youtube.account.settings.notifications.setCommentReplies(true); | ||
``` | ||
* Channel mention notifications: | ||
```js | ||
await youtube.account.settings.notifications.setSharedContent(true); | ||
``` | ||
```js | ||
await youtube.account.settings.notifications.setSharedContent(true); | ||
``` | ||
@@ -764,12 +835,12 @@ #### Privacy settings: | ||
* Subscriptions privacy: | ||
```js | ||
await youtube.account.settings.privacy.setSubscriptionsPrivate(true); | ||
``` | ||
```js | ||
await youtube.account.settings.privacy.setSubscriptionsPrivate(true); | ||
``` | ||
* Saved playlists privacy: | ||
```js | ||
await youtube.account.settings.privacy.setSavedPlaylistsPrivate(true); | ||
``` | ||
```js | ||
await youtube.account.settings.privacy.setSavedPlaylistsPrivate(true); | ||
``` | ||
### Fetching live chats: | ||
### Live chats: | ||
--- | ||
@@ -877,3 +948,3 @@ | ||
Cancelling a download: | ||
Cancel a download: | ||
```js | ||
@@ -957,3 +1028,3 @@ stream.cancel(); | ||
OAuth: | ||
#### OAuth: | ||
@@ -991,3 +1062,3 @@ ```js | ||
Cookies: | ||
#### Cookies: | ||
@@ -1005,2 +1076,3 @@ ```js | ||
<!-- CONTRIBUTING --> | ||
## Contributing | ||
@@ -1011,2 +1083,11 @@ Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. | ||
<!-- CONTACT --> | ||
## Contact | ||
LuanRT - [@lrt_nooneknows](https://twitter.com/lrt_nooneknows) - luan.lrt4@gmail.com | ||
Project Link: [https://github.com/LuanRT/YouTube.js](https://github.com/LuanRT/YouTube.js) | ||
<!-- DISCLAIMER --> | ||
## Disclaimer | ||
@@ -1018,3 +1099,6 @@ This project is not affiliated with, endorsed, or sponsored by YouTube or any of their affiliates or subsidiaries. | ||
<!-- LICENSE --> | ||
## License | ||
[MIT](https://choosealicense.com/licenses/mit/) | ||
Distributed under the [MIT](https://choosealicense.com/licenses/mit/) License. | ||
<p align="right">(<a href="#top">back to top</a>)</p> |
@@ -5,4 +5,4 @@ 'use strict'; | ||
const Innertube = require('..'); | ||
const NToken = require('../lib/NToken'); | ||
const SigDecipher = require('../lib/Sig'); | ||
const NToken = require('../lib/deciphers/NToken'); | ||
const SigDecipher = require('../lib/deciphers/Sig'); | ||
const Constants = require('./constants'); | ||
@@ -17,5 +17,17 @@ | ||
if (!(youtube instanceof Error)) { | ||
const search = await youtube.search('Carl Sagan - Documentary').catch((error) => error); | ||
assert(!(search instanceof Error) && search.videos.length >= 1, `should search videos`, search); | ||
const homefeed = await youtube.getHomeFeed(); | ||
assert(!(homefeed instanceof Error), `should retrieve recommendations`, homefeed); | ||
const ytsearch = await youtube.search('Carl Sagan - Documentary').catch((error) => error); | ||
assert(!(ytsearch instanceof Error) && ytsearch.videos.length, `should search on YouTube`, ytsearch); | ||
const ytmsearch = await youtube.search('Logic - Obediently Yours', { client: 'YTMUSIC' }).catch((error) => error); | ||
assert(!(ytmsearch instanceof Error), `should search on YouTube Music`, ytmsearch); | ||
const ytsearch_suggestions = await youtube.getSearchSuggestions('test', { client: 'YOUTUBE' }); | ||
assert(!(ytsearch_suggestions instanceof Error), `should retrieve YouTube search suggestions`); | ||
const ytmsearch_suggestions = await youtube.getSearchSuggestions('test', { client: 'YTMUSIC' }); | ||
assert(!(ytmsearch_suggestions instanceof Error), `should retrieve YouTube Music search suggestions`); | ||
const details = await youtube.getDetails(Constants.test_video_id).catch((error) => error); | ||
@@ -26,3 +38,12 @@ assert(!(details instanceof Error), `should retrieve details for ${Constants.test_video_id}`, details); | ||
assert(!(comments instanceof Error), `should retrieve comments for ${Constants.test_video_id}`, comments); | ||
const ytplaylist = await youtube.getPlaylist(ytmsearch.results.community_playlists[0].id, { client: 'YOUTUBE' }); | ||
assert(!(ytplaylist instanceof Error), `should retrieve and parse playlist with YouTube`, ytplaylist); | ||
const ytmplaylist = await youtube.getPlaylist(ytmsearch.results.community_playlists[0].id, { client: 'YTMUSIC' }); | ||
assert(!(ytmplaylist instanceof Error), `should retrieve and parse playlist with YouTube Music`, ytmplaylist); | ||
const lyrics = await youtube.getLyrics(ytmsearch.results.songs[0].id); | ||
assert(!(lyrics instanceof Error), `should retrieve song lyrics`, lyrics); | ||
const video = await downloadVideo(Constants.test_video_id, youtube).catch((error) => error); | ||
@@ -32,4 +53,3 @@ assert(!(video instanceof Error), `should download video (${Constants.test_video_id})`, video); | ||
const n_token = new NToken(Constants.n_scramble_sc).transform(Constants.original_ntoken); | ||
const n_token = new NToken(Constants.n_scramble_sc, Constants.original_ntoken).transform(); | ||
assert(n_token == Constants.expected_ntoken, `should transform n token into ${Constants.expected_ntoken}`, n_token); | ||
@@ -65,2 +85,2 @@ | ||
performTests(); | ||
performTests(); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
158118
30
2747
1067
5
1
1
+ Addedflat@^5.0.2
+ Addedflat@5.0.2(transitive)