Skip to content
This repository has been archived by the owner on Sep 20, 2023. It is now read-only.

Don't error when tabs are missing #94

Merged
merged 8 commits into from
Sep 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ ytch.getChannelInfo(payload).then((response) => {
isOfficialArtist: Boolean,
tags: Array[String], // Will return null if none exist
channelIdType: Number,
channelTabs: Array[String], // The tabs that are displayed on the channel (e.g., Videos, Playlists)
alertMessage: String, // Will return a response alert message if any (e.g., "This channel does not exist."). Otherwise undefined
channelLinks: {
primaryLinks: Array[Object],
Expand Down
25 changes: 20 additions & 5 deletions app/fetchers/playlist.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const YoutubeGrabberHelper = require('../helper')
const helper = require('../helper')

class PlaylistFetcher {
Expand All @@ -24,19 +25,33 @@ class PlaylistFetcher {
if (typeof (channelPageDataResponse) === 'undefined') {
channelPageDataResponse = response.data[1].response
}
const channelMetaData = channelPageDataResponse.metadata.channelMetadataRenderer
const channelName = channelMetaData.title
const channelId = channelMetaData.externalId
if (typeof (channelPageDataResponse.alerts) !== 'undefined') {
return {
alertMessage: channelPageDataResponse.alerts[0].alertRenderer.text.simpleText
}
}
let channelName
let channelMetaData
let channelId
if ('metadata' in channelPageDataResponse) {
channelMetaData = channelPageDataResponse.metadata.channelMetadataRenderer
channelName = channelMetaData.title
channelId = channelMetaData.externalId
}
const ytGrabHelp = helper.create(httpAgent)

const channelInfo = {
channelId: channelId,
channelName: channelName,
channelUrl: `https://www.youtube.com/channel/${channelId}`
}
let playlistData
const playlistTab = YoutubeGrabberHelper.findTab(channelPageDataResponse.contents.twoColumnBrowseResultsRenderer.tabs)

const playlistData = channelPageDataResponse.contents.twoColumnBrowseResultsRenderer.tabs[2].tabRenderer.content.sectionListRenderer.contents[0].itemSectionRenderer.contents[0].gridRenderer

if (playlistTab && 'sectionListRenderer' in playlistTab.tabRenderer.content) {
const tabRenderer = playlistTab.tabRenderer
playlistData = tabRenderer.content.sectionListRenderer.contents[0].itemSectionRenderer.contents[0].gridRenderer
}
if (typeof (playlistData) === 'undefined') {
return {
continuation: null,
Expand Down
40 changes: 32 additions & 8 deletions app/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,18 @@ class YoutubeGrabberHelper {
alertMessage: channelPageDataResponse.alerts[0].alertRenderer.text.simpleText
}
}
let channelMetaData
let channelName
if ('metadata' in channelPageDataResponse) {
channelMetaData = channelPageDataResponse.metadata.channelMetadataRenderer
channelName = channelMetaData.title
}
const videoTab = YoutubeGrabberHelper.findTab(channelPageDataResponse.contents.twoColumnBrowseResultsRenderer.tabs)

const channelMetaData = channelPageDataResponse.metadata.channelMetadataRenderer
const channelName = channelMetaData.title
const channelVideoData = channelPageDataResponse.contents.twoColumnBrowseResultsRenderer.tabs[1].tabRenderer.content.sectionListRenderer.contents[0].itemSectionRenderer.contents[0].gridRenderer

let channelVideoData
if (videoTab && 'sectionListRenderer' in videoTab.tabRenderer.content) {
channelVideoData = videoTab.tabRenderer.content.sectionListRenderer.contents[0].itemSectionRenderer.contents[0].gridRenderer
}
if (typeof (channelVideoData) === 'undefined') {
// Channel has no videos
return {
Expand Down Expand Up @@ -116,7 +123,7 @@ class YoutubeGrabberHelper {
const channelUrl = author.navigationEndpoint.browseEndpoint.canonicalBaseUrl
const thumbnail = author.thumbnail.thumbnails
let videoCount = 0
if ('videoCout' in author) {
if ('videoCountText' in author) {
videoCount = author.videoCountText.runs[0].text
}
let subscriberText
Expand Down Expand Up @@ -294,9 +301,20 @@ class YoutubeGrabberHelper {

// Parse the JSON data and get the relevent array with data
let contentDataJSON = JSON.parse(contentDataString)
contentDataJSON = contentDataJSON.contents.twoColumnBrowseResultsRenderer.tabs[3].tabRenderer.content.sectionListRenderer.contents[0].itemSectionRenderer
if ('continuationItemRenderer' in contentDataJSON.contents[contentDataJSON.contents.length - 1]) {
return { items: this.createCommunityPostArray(contentDataJSON.contents), continuation: contentDataJSON.contents[contentDataJSON.contents.length - 1].continuationItemRenderer.continuationEndpoint.continuationCommand.token, innerTubeApi: innertubeAPIkey, channelIdType: channelIdType }
if (typeof (contentDataJSON.alerts) !== 'undefined') {
return {
alertMessage: contentDataJSON.alerts[0].alertRenderer.text.simpleText
}
}
const communityTab = YoutubeGrabberHelper.findTab(contentDataJSON.contents.twoColumnBrowseResultsRenderer.tabs)

if (communityTab) {
contentDataJSON = contentDataJSON.contents.twoColumnBrowseResultsRenderer.tabs[3].tabRenderer.content.sectionListRenderer.contents[0].itemSectionRenderer
if ('continuationItemRenderer' in contentDataJSON.contents[contentDataJSON.contents.length - 1]) {
return { items: this.createCommunityPostArray(contentDataJSON.contents), continuation: contentDataJSON.contents[contentDataJSON.contents.length - 1].continuationItemRenderer.continuationEndpoint.continuationCommand.token, innerTubeApi: innertubeAPIkey, channelIdType: channelIdType }
}
} else {
contentDataJSON = { contents: [] }
}
return { items: this.createCommunityPostArray(contentDataJSON.contents), continuation: null, innerTubeApi: null, channelIdType: channelIdType }
}
Expand Down Expand Up @@ -669,6 +687,12 @@ class YoutubeGrabberHelper {
return { response: channelPageResponse, channelIdType: 3 }
}

static findTab(tabs) {
return tabs.find((tab) =>
tab?.tabRenderer?.selected === true
)
}

static create(httpsAgent) {
return new YoutubeGrabberHelper(httpsAgent)
}
Expand Down
108 changes: 57 additions & 51 deletions app/youtube-grabber.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ class YoutubeGrabber {
if (channelPageResponse.data.response === undefined) {
channelPageDataResponse = channelPageResponse.data[1].response
}
const headerLinks = channelPageDataResponse.header.c4TabbedHeaderRenderer.headerLinks
let headerLinks
if ('c4TabbedHeaderRenderer' in channelPageDataResponse.header) {
headerLinks = channelPageDataResponse.header.c4TabbedHeaderRenderer.headerLinks
}
const links = {
primaryLinks: [],
secondaryLinks: []
Expand Down Expand Up @@ -55,20 +58,22 @@ class YoutubeGrabber {
}
}

const channelMetaData = channelPageDataResponse.metadata.channelMetadataRenderer
const channelHeaderData = channelPageDataResponse.header.c4TabbedHeaderRenderer
const channelMetaData = channelPageDataResponse?.metadata?.channelMetadataRenderer
let channelHeaderData = channelPageDataResponse.header.c4TabbedHeaderRenderer
if (!channelHeaderData) {
channelHeaderData = channelPageDataResponse.header.carouselHeaderRenderer.contents[1].topicChannelDetailsRenderer
// = topicChannelDetailsRenderer
}
const headerTabs = channelPageDataResponse.contents.twoColumnBrowseResultsRenderer.tabs
const channelTabs = headerTabs
.filter(tab => tab.tabRenderer !== undefined && tab.tabRenderer !== null)
.map(tab => tab.tabRenderer.title)

const channelsTab = headerTabs.filter((data) => {
if (typeof data.tabRenderer !== 'undefined') {
return data.tabRenderer.title === 'Channels'
}

return false
})

const featuredChannels = channelsTab[0].tabRenderer.content.sectionListRenderer.contents[0].itemSectionRenderer.contents[0]

const channelsTab = YoutubeGrabberHelper.findTab(headerTabs)
let featuredChannels = {}
if (channelsTab && 'sectionListRenderer' in channelsTab.tabRenderer.content) {
featuredChannels = channelsTab.tabRenderer.content.sectionListRenderer.contents[0].itemSectionRenderer.contents[0]
}
let relatedChannels = []
let relatedChannelsContinuation = null

Expand Down Expand Up @@ -131,27 +136,27 @@ class YoutubeGrabber {
isOfficialArtist = channelHeaderData.badges.some((badge) => badge.metadataBadgeRenderer.style === 'BADGE_STYLE_TYPE_VERIFIED_ARTIST')
}

const tags = channelPageDataResponse.microformat.microformatDataRenderer.tags || null

const tags = channelPageDataResponse?.microformat?.microformatDataRenderer?.tags || null
const channelInfo = {
author: channelMetaData.title,
authorId: channelMetaData.externalId,
authorUrl: channelMetaData.vanityChannelUrl,
author: channelMetaData?.title ?? channelHeaderData.title.simpleText,
authorId: channelMetaData?.externalId ?? channelHeaderData.navigationEndpoint.browseEndpoint.browseId,
authorUrl: channelMetaData?.vanityChannelUrl ?? channelHeaderData.navigationEndpoint.commandMetadata.webCommandMetadata.url,
authorBanners: bannerThumbnails,
authorThumbnails: channelHeaderData.avatar.thumbnails,
subscriberText: subscriberText,
subscriberCount: subscriberCount,
description: channelMetaData.description,
isFamilyFriendly: channelMetaData.isFamilySafe,
description: channelMetaData?.description ?? '',
isFamilyFriendly: channelMetaData?.isFamilySafe ?? false,
relatedChannels: {
items: relatedChannels,
continuation: relatedChannelsContinuation
},
allowedRegions: channelMetaData.availableCountryCodes,
allowedRegions: channelMetaData?.availableCountryCodes ?? [],
isVerified: isVerified,
isOfficialArtist: isOfficialArtist,
tags: tags,
channelLinks: links,
channelTabs: channelTabs,
channelIdType: decideResponse.channelIdType,
}

Expand Down Expand Up @@ -334,6 +339,11 @@ class YoutubeGrabber {
if (typeof channelPageDataResponse === 'undefined') {
channelPageDataResponse = channelPageResponse.data[1].response
}
if (typeof (channelPageDataResponse.alerts) !== 'undefined') {
return {
alertMessage: channelPageDataResponse.alerts[0].alertRenderer.text.simpleText
}
}
const channelMetaData = channelPageDataResponse.metadata.channelMetadataRenderer
const channelName = channelMetaData.title

Expand Down Expand Up @@ -457,28 +467,28 @@ class YoutubeGrabber {
const ytGrabHelp = YoutubeGrabberHelper.create(httpAgent)
const decideResponse = await ytGrabHelp.decideUrlRequestType(channelId, 'about?flow=grid&view=0&pbj=1', channelIdType)
const channelPageResponse = decideResponse.response
let headerTabs
if (channelPageResponse.data.response) {
headerTabs = channelPageResponse.data.response.contents.twoColumnBrowseResultsRenderer.tabs
} else {
headerTabs = channelPageResponse.data[1].response.contents.twoColumnBrowseResultsRenderer.tabs
}
const aboutTab = headerTabs.filter((data) => {
if (typeof data.tabRenderer !== 'undefined') {
return data.tabRenderer.title === 'About'
const channelPageDataResponse = channelPageResponse.data[1].response
if (typeof (channelPageDataResponse.alerts) !== 'undefined') {
return {
alertMessage: channelPageDataResponse.alerts[0].alertRenderer.text.simpleText
}
return false
})[0]
const contents = aboutTab.tabRenderer.content.sectionListRenderer.contents[0].itemSectionRenderer.contents[0]
const joined = Date.parse(contents.channelAboutFullMetadataRenderer.joinedDateText.runs[1].text)
}
const headerTabs = channelPageDataResponse.contents.twoColumnBrowseResultsRenderer.tabs
const aboutTab = YoutubeGrabberHelper.findTab(headerTabs)

let views = '0'
let location = 'unknown'
if ('viewCountText' in contents.channelAboutFullMetadataRenderer) {
views = contents.channelAboutFullMetadataRenderer.viewCountText.simpleText.replace(/\D/g, '')
}
let joined = null
if (aboutTab !== undefined) {
const contents = aboutTab.tabRenderer.content.sectionListRenderer.contents[0].itemSectionRenderer.contents[0]
joined = Date.parse(contents.channelAboutFullMetadataRenderer.joinedDateText.runs[1].text)
if ('viewCountText' in contents.channelAboutFullMetadataRenderer) {
views = contents.channelAboutFullMetadataRenderer.viewCountText.simpleText.replace(/\D/g, '')
}

if ('country' in contents.channelAboutFullMetadataRenderer) {
location = contents.channelAboutFullMetadataRenderer.country.simpleText
if ('country' in contents.channelAboutFullMetadataRenderer) {
location = contents.channelAboutFullMetadataRenderer.country.simpleText
}
}

return {
Expand All @@ -488,18 +498,19 @@ class YoutubeGrabber {
}
}

static async getChannelHome(payload) {
const channelId = payload.channelId
const channelIdType = payload.channelIdType ?? 0
const httpAgent = payload.httpAgent ?? null

static async getChannelHome({ channelId, channelIdType = 0, httpAgent = null }) {
const ytGrabHelp = YoutubeGrabberHelper.create(httpAgent)
const decideResponse = await ytGrabHelp.decideUrlRequestType(channelId, 'home?flow=grid&view=0&pbj=1', channelIdType)
const channelPageResponse = decideResponse.response
let channelPageDataResponse = channelPageResponse.data.response
if (typeof channelPageDataResponse === 'undefined') {
channelPageDataResponse = channelPageResponse.data[1].response
}
if (typeof (channelPageDataResponse.alerts) !== 'undefined') {
return {
alertMessage: channelPageDataResponse.alerts[0].alertRenderer.text.simpleText
}
}
const headerTabs = channelPageDataResponse.contents.twoColumnBrowseResultsRenderer.tabs
let channelName
let channelUrl
Expand All @@ -517,16 +528,11 @@ class YoutubeGrabber {
channelName: channelName,
channelUrl: channelUrl
}
const homeTab = headerTabs.filter((data) => {
if (typeof data.tabRenderer !== 'undefined') {
return data.tabRenderer.title === 'Home'
}

return false
})[0]
const homeTab = YoutubeGrabberHelper.findTab(headerTabs)
let featuredVideo = null
let homeItems = []
if (homeTab !== undefined) {
if ('sectionListRenderer' in homeTab.tabRenderer.content) {
homeItems = homeTab.tabRenderer.content.sectionListRenderer.contents.filter(x => {
if ('shelfRenderer' in x.itemSectionRenderer.contents[0]) {
return true
Expand Down
11 changes: 11 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,18 @@ declare module "yt-channel-info" {
isOfficialArtist: boolean;
tags: string[];
channelIdType: number;
channelTabs: string[];
alertMessage: string;
channelLinks: {
primaryLinks: ChannelLink[],
secondaryLinks: ChannelLink[]
}
}

interface ChannelLink {
url: string,
icon: string,
title: string
}

/**
Expand Down
12 changes: 12 additions & 0 deletions test/channelHome.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,16 @@ describe('Getting channel home', () => {
expect(data.items.length).not.toBe(0)
})
})
test('Rammstein - topic', () => {
const parameters = { channelId: 'UCs6GGpd9zvsYghuYe0VDFUQ', channelIdType: 1 }
return ytch.getChannelHome(parameters).then((data) => {
expect(data.items.length).not.toBe(0)
})
})
test('Deleted channel', () => {
const parameters = { channelId: 'UC59AcfHD5jOGqTxb-zAsahw', channelIdType: 1 }
return ytch.getChannelHome(parameters).then((data) => {
expect(data.alertMessage).not.toBe(undefined)
})
})
})
13 changes: 13 additions & 0 deletions test/channelInfo.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,17 @@ describe('Getting channel info', () => {
expect(data.alertMessage).toBe('This channel does not exist.')
})
})

test('Channel missing tabs', () => {
const parameters = { channelId: 'UCs6GGpd9zvsYghuYe0VDFUQ', channelIdType: 1 }
return ytch.getChannelInfo(parameters).then((data) => {
expect(data.channelTabs.length).toBe(3)
})
})
test('Deleted channel', () => {
const parameters = { channelId: 'UC59AcfHD5jOGqTxb-zAsahw', channelIdType: 1 }
return ytch.getChannelVideos(parameters).then((data) => {
expect(data.alertMessage).not.toBe(undefined)
})
})
})
13 changes: 13 additions & 0 deletions test/channelPlaylists.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,17 @@ describe('Playlists', () => {
expect(data.items.length).toBe(0)
})
})
test('Channel missing playlist tab', () => {
const parameters = { channelId: 'UCYfdidRxbB8Qhf0Nx7ioOYw', channelIdType: 1 }
return ytch.getChannelPlaylistInfo(parameters).then((data) => {
expect(data.items.length).toBe(0)
expect(data.continuation).toBe(null)
})
})
test('Deleted channel', () => {
const parameters = { channelId: 'UC59AcfHD5jOGqTxb-zAsahw', channelIdType: 1 }
return ytch.getChannelPlaylistInfo(parameters).then((data) => {
expect(data.alertMessage).not.toBe(undefined)
})
})
})
6 changes: 6 additions & 0 deletions test/channelStats.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@ describe('Channel stats', () => {
expect(data.joinedDate).toBeLessThanOrEqual(1355227200000 + 86400000)
})
})
test('Deleted channel', () => {
const parameters = { channelId: 'UC59AcfHD5jOGqTxb-zAsahw', channelIdType: 1 }
return ytch.getChannelStats(parameters).then((data) => {
expect(data.alertMessage).not.toBe(undefined)
})
})
})
Loading