mirror of https://github.com/FreeTubeApp/FreeTube
Support YouTube using PageHeader on user channels not just auto-generated ones (#4543)
* Support YouTube using PageHeader on user channels not just auto-generated ones * Bump YouTube.js to 9.0.2 as requested
This commit is contained in:
parent
3bce022616
commit
30248d6bbc
|
@ -1,5 +1,8 @@
|
|||
{
|
||||
"vueCompilerOptions": {
|
||||
"target": 2.7
|
||||
},
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
"vue-observe-visibility": "^1.0.0",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuex": "^3.6.2",
|
||||
"youtubei.js": "^8.2.0"
|
||||
"youtubei.js": "^9.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.23.9",
|
||||
|
|
|
@ -290,7 +290,9 @@ export async function getLocalChannelVideos(id) {
|
|||
// if the channel doesn't have a videos tab, YouTube returns the home tab instead
|
||||
// so we need to check that we got the right tab
|
||||
if (videosTab.current_tab?.endpoint.metadata.url?.endsWith('/videos')) {
|
||||
return parseLocalChannelVideos(videosTab.videos, videosTab.header.author)
|
||||
const { id: channelId = id, name } = parseLocalChannelHeader(videosTab)
|
||||
|
||||
return parseLocalChannelVideos(videosTab.videos, channelId, name)
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
|
@ -320,7 +322,9 @@ export async function getLocalChannelLiveStreams(id) {
|
|||
// if the channel doesn't have a live tab, YouTube returns the home tab instead
|
||||
// so we need to check that we got the right tab
|
||||
if (liveStreamsTab.current_tab?.endpoint.metadata.url?.endsWith('/streams')) {
|
||||
return parseLocalChannelVideos(liveStreamsTab.videos, liveStreamsTab.header.author)
|
||||
const { id: channelId = id, name } = parseLocalChannelHeader(liveStreamsTab)
|
||||
|
||||
return parseLocalChannelVideos(liveStreamsTab.videos, channelId, name)
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
|
@ -365,16 +369,158 @@ export async function getLocalChannelCommunity(id) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {import('youtubei.js').YTNodes.Video[]} videos
|
||||
* @param {Misc.Author} author
|
||||
* @param {YT.Channel} channel
|
||||
*/
|
||||
export function parseLocalChannelVideos(videos, author) {
|
||||
export function parseLocalChannelHeader(channel) {
|
||||
/** @type {string=} */
|
||||
let id
|
||||
/** @type {string} */
|
||||
let name
|
||||
/** @type {string=} */
|
||||
let thumbnailUrl
|
||||
/** @type {string=} */
|
||||
let bannerUrl
|
||||
/** @type {string=} */
|
||||
let subscriberText
|
||||
/** @type {string[]} */
|
||||
const tags = []
|
||||
|
||||
switch (channel.header.type) {
|
||||
case 'C4TabbedHeader': {
|
||||
// example: Linus Tech Tips
|
||||
// https://www.youtube.com/channel/UCXuqSBlHAE6Xw-yeJA0Tunw
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.C4TabbedHeader}
|
||||
*/
|
||||
const header = channel.header
|
||||
|
||||
id = header.author.id
|
||||
name = header.author.name
|
||||
thumbnailUrl = header.author.best_thumbnail.url
|
||||
bannerUrl = header.banner?.[0]?.url
|
||||
subscriberText = header.subscribers?.text
|
||||
break
|
||||
}
|
||||
case 'CarouselHeader': {
|
||||
// examples: Music and YouTube Gaming
|
||||
// https://www.youtube.com/channel/UC-9-kyTW8ZkZNDHQJ6FgpwQ
|
||||
// https://www.youtube.com/channel/UCOpNcN46UbXVtpKMrmU4Abg
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.CarouselHeader}
|
||||
*/
|
||||
const header = channel.header
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.TopicChannelDetails}
|
||||
*/
|
||||
const topicChannelDetails = header.contents.find(node => node.type === 'TopicChannelDetails')
|
||||
name = topicChannelDetails.title.text
|
||||
subscriberText = topicChannelDetails.subtitle.text
|
||||
thumbnailUrl = topicChannelDetails.avatar[0].url
|
||||
|
||||
if (channel.metadata.external_id) {
|
||||
id = channel.metadata.external_id
|
||||
} else {
|
||||
id = topicChannelDetails.subscribe_button.channel_id
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'InteractiveTabbedHeader': {
|
||||
// example: Minecraft - Topic
|
||||
// https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.InteractiveTabbedHeader}
|
||||
*/
|
||||
const header = channel.header
|
||||
name = header.title.text
|
||||
thumbnailUrl = header.box_art.at(-1).url
|
||||
bannerUrl = header.banner[0]?.url
|
||||
|
||||
const badges = header.badges.map(badge => badge.label).filter(tag => tag)
|
||||
tags.push(...badges)
|
||||
|
||||
id = channel.current_tab?.endpoint.payload.browseId
|
||||
break
|
||||
}
|
||||
case 'PageHeader': {
|
||||
// example: YouTube Gaming
|
||||
// https://www.youtube.com/channel/UCOpNcN46UbXVtpKMrmU4Abg
|
||||
|
||||
// User channels (an A/B test at the time of writing)
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.PageHeader}
|
||||
*/
|
||||
const header = channel.header
|
||||
|
||||
name = header.content.title.text.text
|
||||
if (header.content.image) {
|
||||
if (header.content.image.type === 'ContentPreviewImageView') {
|
||||
/** @type {import('youtubei.js').YTNodes.ContentPreviewImageView} */
|
||||
const image = header.content.image
|
||||
|
||||
thumbnailUrl = image.image[0].url
|
||||
} else {
|
||||
/** @type {import('youtubei.js').YTNodes.DecoratedAvatarView} */
|
||||
const image = header.content.image
|
||||
thumbnailUrl = image.avatar?.image[0].url
|
||||
}
|
||||
}
|
||||
|
||||
if (!thumbnailUrl && channel.metadata.thumbnail) {
|
||||
thumbnailUrl = channel.metadata.thumbnail[0].url
|
||||
}
|
||||
|
||||
if (header.content.banner) {
|
||||
bannerUrl = header.content.banner.image[0]?.url
|
||||
}
|
||||
|
||||
if (header.content.actions) {
|
||||
const modal = header.content.actions.actions_rows[0].actions[0].on_tap.modal
|
||||
|
||||
if (modal && modal.type === 'ModalWithTitleAndButton') {
|
||||
/** @type {import('youtubei.js').YTNodes.ModalWithTitleAndButton} */
|
||||
const typedModal = modal
|
||||
|
||||
id = typedModal.button.endpoint.next_endpoint?.payload.browseId
|
||||
}
|
||||
} else if (channel.metadata.external_id) {
|
||||
id = channel.metadata.external_id
|
||||
}
|
||||
|
||||
if (header.content.metadata) {
|
||||
subscriberText = header.content.metadata.metadata_rows[0].metadata_parts[1].text.text
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
thumbnailUrl,
|
||||
bannerUrl,
|
||||
subscriberText,
|
||||
tags
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('youtubei.js').YTNodes.Video[]} videos
|
||||
* @param {string} channelId
|
||||
* @param {string} channelName
|
||||
*/
|
||||
export function parseLocalChannelVideos(videos, channelId, channelName) {
|
||||
const parsedVideos = videos.map(parseLocalListVideo)
|
||||
|
||||
// fix empty author info
|
||||
parsedVideos.forEach(video => {
|
||||
video.author = author.name
|
||||
video.authorId = author.id
|
||||
video.author = channelName
|
||||
video.authorId = channelId
|
||||
})
|
||||
|
||||
return parsedVideos
|
||||
|
@ -382,16 +528,17 @@ export function parseLocalChannelVideos(videos, author) {
|
|||
|
||||
/**
|
||||
* @param {import('youtubei.js').YTNodes.ReelItem[]} shorts
|
||||
* @param {Misc.Author} author
|
||||
* @param {string} channelId
|
||||
* @param {string} channelName
|
||||
*/
|
||||
export function parseLocalChannelShorts(shorts, author) {
|
||||
export function parseLocalChannelShorts(shorts, channelId, channelName) {
|
||||
return shorts.map(short => {
|
||||
return {
|
||||
type: 'video',
|
||||
videoId: short.id,
|
||||
title: short.title.text,
|
||||
author: author.name,
|
||||
authorId: author.id,
|
||||
author: channelName,
|
||||
authorId: channelId,
|
||||
viewCount: parseLocalSubscriberCount(short.views.text),
|
||||
lengthSeconds: ''
|
||||
}
|
||||
|
@ -405,40 +552,43 @@ export function parseLocalChannelShorts(shorts, author) {
|
|||
|
||||
/**
|
||||
* @param {Playlist|GridPlaylist} playlist
|
||||
* @param {Misc.Author} author
|
||||
* @param {string} channelId
|
||||
* @param {string} chanelName
|
||||
*/
|
||||
export function parseLocalListPlaylist(playlist, author = undefined) {
|
||||
let channelName
|
||||
let channelId = null
|
||||
/** @type {import('youtubei.js').YTNodes.PlaylistVideoThumbnail} */
|
||||
const thumbnailRenderer = playlist.thumbnail_renderer
|
||||
export function parseLocalListPlaylist(playlist, channelId = undefined, channelName = undefined) {
|
||||
let internalChannelName
|
||||
let internalChannelId = null
|
||||
|
||||
if (playlist.author && playlist.author.id !== 'N/A') {
|
||||
if (playlist.author instanceof Misc.Text) {
|
||||
channelName = playlist.author.text
|
||||
internalChannelName = playlist.author.text
|
||||
|
||||
if (author) {
|
||||
channelId = author.id
|
||||
if (channelId) {
|
||||
internalChannelId = channelId
|
||||
}
|
||||
} else {
|
||||
channelName = playlist.author.name
|
||||
channelId = playlist.author.id
|
||||
internalChannelName = playlist.author.name
|
||||
internalChannelId = playlist.author.id
|
||||
}
|
||||
} else if (author) {
|
||||
channelName = author.name
|
||||
channelId = author.id
|
||||
} else if (channelId || channelName) {
|
||||
internalChannelName = channelName
|
||||
internalChannelId = channelId
|
||||
} else if (playlist.author?.name) {
|
||||
// auto-generated album playlists don't have an author
|
||||
// so in search results, the author text is "Playlist" and doesn't have a link or channel ID
|
||||
channelName = playlist.author.name
|
||||
internalChannelName = playlist.author.name
|
||||
}
|
||||
|
||||
/** @type {import('youtubei.js').YTNodes.PlaylistVideoThumbnail} */
|
||||
const thumbnailRenderer = playlist.thumbnail_renderer
|
||||
|
||||
return {
|
||||
type: 'playlist',
|
||||
dataSource: 'local',
|
||||
title: playlist.title.text,
|
||||
thumbnail: thumbnailRenderer ? thumbnailRenderer.thumbnail[0].url : playlist.thumbnails[0].url,
|
||||
channelName,
|
||||
channelId,
|
||||
channelName: internalChannelName,
|
||||
channelId: internalChannelId,
|
||||
playlistId: playlist.id,
|
||||
videoCount: extractNumberFromString(playlist.video_count.text)
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
import {
|
||||
getLocalChannel,
|
||||
getLocalChannelId,
|
||||
parseLocalChannelHeader,
|
||||
parseLocalChannelShorts,
|
||||
parseLocalChannelVideos,
|
||||
parseLocalCommunityPosts,
|
||||
|
@ -532,90 +533,22 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
let channelId
|
||||
let subscriberText = null
|
||||
let tags = []
|
||||
const parsedHeader = parseLocalChannelHeader(channel)
|
||||
|
||||
switch (channel.header.type) {
|
||||
case 'C4TabbedHeader': {
|
||||
// example: Linus Tech Tips
|
||||
// https://www.youtube.com/channel/UCXuqSBlHAE6Xw-yeJA0Tunw
|
||||
const channelId = parsedHeader.id ?? this.id
|
||||
const subscriberText = parsedHeader.subscriberText ?? null
|
||||
let tags = parsedHeader.tags
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.C4TabbedHeader}
|
||||
*/
|
||||
const header = channel.header
|
||||
channelThumbnailUrl = parsedHeader.thumbnailUrl ?? this.subscriptionInfo?.thumbnail
|
||||
channelName = parsedHeader.name ?? this.subscriptionInfo?.name
|
||||
|
||||
channelId = header.author.id
|
||||
channelName = header.author.name
|
||||
channelThumbnailUrl = header.author.best_thumbnail.url
|
||||
subscriberText = header.subscribers?.text
|
||||
break
|
||||
}
|
||||
case 'CarouselHeader': {
|
||||
// examples: Music and YouTube Gaming
|
||||
// https://www.youtube.com/channel/UC-9-kyTW8ZkZNDHQJ6FgpwQ
|
||||
// https://www.youtube.com/channel/UCOpNcN46UbXVtpKMrmU4Abg
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.CarouselHeader}
|
||||
*/
|
||||
const header = channel.header
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.TopicChannelDetails}
|
||||
*/
|
||||
const topicChannelDetails = header.contents.find(node => node.type === 'TopicChannelDetails')
|
||||
channelName = topicChannelDetails.title.text
|
||||
subscriberText = topicChannelDetails.subtitle.text
|
||||
channelThumbnailUrl = topicChannelDetails.avatar[0].url
|
||||
|
||||
if (channel.metadata.external_id) {
|
||||
channelId = channel.metadata.external_id
|
||||
} else {
|
||||
channelId = topicChannelDetails.subscribe_button.channel_id
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'InteractiveTabbedHeader': {
|
||||
// example: Minecraft - Topic
|
||||
// https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.InteractiveTabbedHeader}
|
||||
*/
|
||||
const header = channel.header
|
||||
channelName = header.title.text
|
||||
channelId = this.id
|
||||
channelThumbnailUrl = header.box_art.at(-1).url
|
||||
|
||||
const badges = header.badges.map(badge => badge.label).filter(tag => tag)
|
||||
tags.push(...badges)
|
||||
break
|
||||
}
|
||||
case 'PageHeader': {
|
||||
// example: YouTube Gaming (an A/B test at the time of writing)
|
||||
// https://www.youtube.com/channel/UCOpNcN46UbXVtpKMrmU4Abg
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.PageHeader}
|
||||
*/
|
||||
const header = channel.header
|
||||
|
||||
channelName = header.content.title.text
|
||||
channelThumbnailUrl = header.content.image.image[0].url
|
||||
channelId = this.id
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (channelThumbnailUrl.startsWith('//')) {
|
||||
if (channelThumbnailUrl?.startsWith('//')) {
|
||||
channelThumbnailUrl = `https:${channelThumbnailUrl}`
|
||||
}
|
||||
|
||||
this.channelName = channelName
|
||||
this.thumbnailUrl = channelThumbnailUrl
|
||||
this.bannerUrl = parsedHeader.bannerUrl ?? null
|
||||
this.isFamilyFriendly = !!channel.metadata.is_family_safe
|
||||
|
||||
if (channel.metadata.tags) {
|
||||
|
@ -646,12 +579,6 @@ export default defineComponent({
|
|||
|
||||
this.updateSubscriptionDetails({ channelThumbnailUrl, channelName, channelId })
|
||||
|
||||
if (channel.header.banner?.length > 0) {
|
||||
this.bannerUrl = channel.header.banner[0].url
|
||||
} else {
|
||||
this.bannerUrl = null
|
||||
}
|
||||
|
||||
let relatedChannels = channel.channels.map(({ author }) => ({
|
||||
name: author.name,
|
||||
id: author.id,
|
||||
|
@ -837,7 +764,7 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
this.latestVideos = parseLocalChannelVideos(videosTab.videos, channel.header.author)
|
||||
this.latestVideos = parseLocalChannelVideos(videosTab.videos, this.id, this.channelName)
|
||||
this.videoContinuationData = videosTab.has_continuation ? videosTab : null
|
||||
this.isElementListLoading = false
|
||||
} catch (err) {
|
||||
|
@ -862,7 +789,7 @@ export default defineComponent({
|
|||
*/
|
||||
const continuation = await this.videoContinuationData.getContinuation()
|
||||
|
||||
this.latestVideos = this.latestVideos.concat(parseLocalChannelVideos(continuation.videos, this.channelInstance.header.author))
|
||||
this.latestVideos = this.latestVideos.concat(parseLocalChannelVideos(continuation.videos, this.id, this.channelName))
|
||||
this.videoContinuationData = continuation.has_continuation ? continuation : null
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
|
@ -895,7 +822,7 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
this.latestShorts = parseLocalChannelShorts(shortsTab.videos, channel.header.author)
|
||||
this.latestShorts = parseLocalChannelShorts(shortsTab.videos, this.id, this.channelName)
|
||||
this.shortContinuationData = shortsTab.has_continuation ? shortsTab : null
|
||||
this.isElementListLoading = false
|
||||
} catch (err) {
|
||||
|
@ -920,7 +847,7 @@ export default defineComponent({
|
|||
*/
|
||||
const continuation = await this.shortContinuationData.getContinuation()
|
||||
|
||||
this.latestShorts.push(...parseLocalChannelShorts(continuation.videos, this.channelInstance.header.author))
|
||||
this.latestShorts.push(...parseLocalChannelShorts(continuation.videos, this.id, this.channelName))
|
||||
this.shortContinuationData = continuation.has_continuation ? continuation : null
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
|
@ -953,7 +880,7 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
this.latestLive = parseLocalChannelVideos(liveTab.videos, channel.header.author)
|
||||
this.latestLive = parseLocalChannelVideos(liveTab.videos, this.id, this.channelName)
|
||||
this.liveContinuationData = liveTab.has_continuation ? liveTab : null
|
||||
this.isElementListLoading = false
|
||||
} catch (err) {
|
||||
|
@ -978,7 +905,7 @@ export default defineComponent({
|
|||
*/
|
||||
const continuation = await this.liveContinuationData.getContinuation()
|
||||
|
||||
this.latestLive.push(...parseLocalChannelVideos(continuation.videos, this.channelInstance.header.author))
|
||||
this.latestLive.push(...parseLocalChannelVideos(continuation.videos, this.id, this.channelName))
|
||||
this.liveContinuationData = continuation.has_continuation ? continuation : null
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
|
@ -1270,7 +1197,7 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
this.latestPlaylists = playlistsTab.playlists.map(playlist => parseLocalListPlaylist(playlist, channel.header.author))
|
||||
this.latestPlaylists = playlistsTab.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
|
||||
this.playlistContinuationData = playlistsTab.has_continuation ? playlistsTab : null
|
||||
this.isElementListLoading = false
|
||||
} catch (err) {
|
||||
|
@ -1295,7 +1222,7 @@ export default defineComponent({
|
|||
*/
|
||||
const continuation = await this.playlistContinuationData.getContinuation()
|
||||
|
||||
const parsedPlaylists = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.channelInstance.header.author))
|
||||
const parsedPlaylists = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
|
||||
this.latestPlaylists = this.latestPlaylists.concat(parsedPlaylists)
|
||||
this.playlistContinuationData = continuation.has_continuation ? continuation : null
|
||||
} catch (err) {
|
||||
|
@ -1393,7 +1320,7 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
this.latestReleases = releaseTab.playlists.map(playlist => parseLocalListPlaylist(playlist, channel.header.author))
|
||||
this.latestReleases = releaseTab.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
|
||||
this.releaseContinuationData = releaseTab.has_continuation ? releaseTab : null
|
||||
this.isElementListLoading = false
|
||||
} catch (err) {
|
||||
|
@ -1418,7 +1345,7 @@ export default defineComponent({
|
|||
*/
|
||||
const continuation = await this.releaseContinuationData.getContinuation()
|
||||
|
||||
const parsedReleases = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.channelInstance.header.author))
|
||||
const parsedReleases = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
|
||||
this.latestReleases = this.latestReleases.concat(parsedReleases)
|
||||
this.releaseContinuationData = continuation.has_continuation ? continuation : null
|
||||
} catch (err) {
|
||||
|
@ -1506,7 +1433,7 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
this.latestPodcasts = podcastTab.playlists.map(playlist => parseLocalListPlaylist(playlist, channel.header.author))
|
||||
this.latestPodcasts = podcastTab.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
|
||||
this.podcastContinuationData = podcastTab.has_continuation ? podcastTab : null
|
||||
this.isElementListLoading = false
|
||||
} catch (err) {
|
||||
|
@ -1531,7 +1458,7 @@ export default defineComponent({
|
|||
*/
|
||||
const continuation = await this.podcastContinuationData.getContinuation()
|
||||
|
||||
const parsedPodcasts = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.channelInstance.header.author))
|
||||
const parsedPodcasts = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
|
||||
this.latestPodcasts = this.latestPodcasts.concat(parsedPodcasts)
|
||||
this.releaseContinuationData = continuation.has_continuation ? continuation : null
|
||||
} catch (err) {
|
||||
|
@ -1857,7 +1784,7 @@ export default defineComponent({
|
|||
if (item.type === 'Video') {
|
||||
return parseLocalListVideo(item)
|
||||
} else {
|
||||
return parseLocalListPlaylist(item, this.channelInstance.header.author)
|
||||
return parseLocalListPlaylist(item, this.id, this.channelName)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -8850,10 +8850,10 @@ yocto-queue@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
|
||||
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
|
||||
|
||||
youtubei.js@^8.2.0:
|
||||
version "8.2.0"
|
||||
resolved "https://registry.yarnpkg.com/youtubei.js/-/youtubei.js-8.2.0.tgz#5b173f41fbe6240bb44cb733ce2c1f24e0b072ca"
|
||||
integrity sha512-i/F4PEURSQmSYCQCo4dWKxOCZXhqkgAuGzNG2RUCtGSmlMX8TvwNewVD/JBjH/czdNmh9SJ00onNZMMxHbt+YA==
|
||||
youtubei.js@^9.0.2:
|
||||
version "9.0.2"
|
||||
resolved "https://registry.yarnpkg.com/youtubei.js/-/youtubei.js-9.0.2.tgz#77592a1144cdd51bb4258472265f5031b3966162"
|
||||
integrity sha512-D7GoJmupYaJxTNQyHRWYw8MUdQTxRaa3c7nzM9etWQjaexepFGVlVtwl3CybLx7GopBNtBvr7RxSUUIUyNnYIg==
|
||||
dependencies:
|
||||
jintr "^1.1.0"
|
||||
tslib "^2.5.0"
|
||||
|
|
Loading…
Reference in New Issue