diff --git a/README.md b/README.md index 83df08e..cd3283e 100644 --- a/README.md +++ b/README.md @@ -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], diff --git a/app/fetchers/playlist.js b/app/fetchers/playlist.js index 94937d5..106d0df 100644 --- a/app/fetchers/playlist.js +++ b/app/fetchers/playlist.js @@ -1,3 +1,4 @@ +const YoutubeGrabberHelper = require('../helper') const helper = require('../helper') class PlaylistFetcher { @@ -24,9 +25,19 @@ 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 = { @@ -34,9 +45,13 @@ class PlaylistFetcher { 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, diff --git a/app/helper.js b/app/helper.js index e0fdacb..a3309c6 100644 --- a/app/helper.js +++ b/app/helper.js @@ -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 { @@ -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 @@ -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 } } @@ -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) } diff --git a/app/youtube-grabber.js b/app/youtube-grabber.js index c599401..64e6882 100644 --- a/app/youtube-grabber.js +++ b/app/youtube-grabber.js @@ -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: [] @@ -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 @@ -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, } @@ -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 @@ -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 { @@ -488,11 +498,7 @@ 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 @@ -500,6 +506,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 headerTabs = channelPageDataResponse.contents.twoColumnBrowseResultsRenderer.tabs let channelName let channelUrl @@ -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 diff --git a/index.d.ts b/index.d.ts index f6702d7..f8b2b9f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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 } /** diff --git a/test/channelHome.test.js b/test/channelHome.test.js index 3a53297..9b78d87 100644 --- a/test/channelHome.test.js +++ b/test/channelHome.test.js @@ -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) + }) + }) }) diff --git a/test/channelInfo.test.js b/test/channelInfo.test.js index 46c4570..3ef06c0 100644 --- a/test/channelInfo.test.js +++ b/test/channelInfo.test.js @@ -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) + }) + }) }) diff --git a/test/channelPlaylists.test.js b/test/channelPlaylists.test.js index 123b834..83fa76c 100644 --- a/test/channelPlaylists.test.js +++ b/test/channelPlaylists.test.js @@ -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) + }) + }) }) diff --git a/test/channelStats.test.js b/test/channelStats.test.js index 3af1fd6..5064ab7 100644 --- a/test/channelStats.test.js +++ b/test/channelStats.test.js @@ -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) + }) + }) }) diff --git a/test/channelVideos.test.js b/test/channelVideos.test.js index cb99cf1..d750502 100644 --- a/test/channelVideos.test.js +++ b/test/channelVideos.test.js @@ -52,4 +52,18 @@ describe('Getting channel videos', () => { expect(data.items.length).toBeGreaterThan(0) }) }) + + test('Channel missing video tab', () => { + const parameters = { channelId: 'UCs6GGpd9zvsYghuYe0VDFUQ', channelIdType: 1 } + return ytch.getChannelVideos(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.getChannelVideos(parameters).then((data) => { + expect(data.alertMessage).not.toBe(undefined) + }) + }) }) diff --git a/test/communityPosts.test.js b/test/communityPosts.test.js index 02d8df1..91fdf22 100644 --- a/test/communityPosts.test.js +++ b/test/communityPosts.test.js @@ -14,4 +14,10 @@ describe('Community Posts', () => { }) }) }) + test('Deleted channel', () => { + const parameters = { channelId: 'UC59AcfHD5jOGqTxb-zAsahw', channelIdType: 1 } + return ytch.getChannelCommunityPosts(parameters).then((data) => { + expect(data.alertMessage).not.toBe(undefined) + }) + }) }) diff --git a/test/searchChannel.test.js b/test/searchChannel.test.js index 5f243e3..c4c96db 100644 --- a/test/searchChannel.test.js +++ b/test/searchChannel.test.js @@ -14,4 +14,10 @@ describe('Searching channels', () => { }) }) }) + test('Deleted channel', () => { + const parameters = { channelId: 'UC59AcfHD5jOGqTxb-zAsahw', channelIdType: 1 } + return ytch.getChannelVideos(parameters).then((data) => { + expect(data.alertMessage).not.toBe(undefined) + }) + }) })