diff --git a/lib/routes/youtube/api/google.ts b/lib/routes/youtube/api/google.ts index 3ad8de9034..453b1b267c 100644 --- a/lib/routes/youtube/api/google.ts +++ b/lib/routes/youtube/api/google.ts @@ -131,3 +131,34 @@ export const getDataByUsername = async ({ username, embed, filterShorts }: { use }), }; }; + +export const getDataByChannelId = async ({ channelId, embed, filterShorts }: { channelId: string; embed: boolean; filterShorts: boolean }): Promise => { + // Get original uploads playlist ID if needed + const originalPlaylistId = filterShorts ? null : (await utils.getChannelWithId(channelId, 'contentDetails', cache)).data.items[0].contentDetails.relatedPlaylists.uploads; + + // Use the utility function to get the appropriate playlist ID based on filterShorts setting + const playlistId = filterShorts ? utils.getPlaylistWithShortsFilter(channelId) : originalPlaylistId; + + const data = (await utils.getPlaylistItems(playlistId, 'snippet', cache)).data.items; + + return { + title: `${data[0].snippet.channelTitle} - YouTube`, + link: `https://www.youtube.com/channel/${channelId}`, + description: `YouTube channel ${data[0].snippet.channelTitle}`, + item: data + .filter((d) => d.snippet.title !== 'Private video' && d.snippet.title !== 'Deleted video') + .map((item) => { + const snippet = item.snippet; + const videoId = snippet.resourceId.videoId; + const img = utils.getThumbnail(snippet.thumbnails); + return { + title: snippet.title, + description: utils.renderDescription(embed, videoId, img, utils.formatDescription(snippet.description)), + pubDate: parseDate(snippet.publishedAt), + link: `https://www.youtube.com/watch?v=${videoId}`, + author: snippet.videoOwnerChannelTitle, + image: img.url, + }; + }), + }; +}; diff --git a/lib/routes/youtube/api/youtubei.ts b/lib/routes/youtube/api/youtubei.ts index e206eea1d7..27dc74a43f 100644 --- a/lib/routes/youtube/api/youtubei.ts +++ b/lib/routes/youtube/api/youtubei.ts @@ -12,16 +12,19 @@ export const getChannelIdByUsername = (username: string) => return navigationEndpoint.payload.browseId; }); -export const getDataByUsername = async ({ username, embed }: { username: string; embed: boolean; filterShorts: boolean }): Promise => { - const innertube = await innertubePromise; +export const getDataByUsername = async ({ username, embed, filterShorts }: { username: string; embed: boolean; filterShorts: boolean }): Promise => { const channelId = (await getChannelIdByUsername(username)) as string; + return getDataByChannelId({ channelId, embed, filterShorts }); +}; +export const getDataByChannelId = async ({ channelId, embed }: { channelId: string; embed: boolean; filterShorts: boolean }): Promise => { + const innertube = await innertubePromise; const channel = await innertube.getChannel(channelId); const videos = await channel.getVideos(); return { - title: `${channel.metadata.title || username} - YouTube`, - link: `https://www.youtube.com/${username}`, + title: `${channel.metadata.title || channelId} - YouTube`, + link: `https://www.youtube.com/channel/${channelId}`, image: channel.metadata.avatar?.[0].url, description: channel.metadata.description, diff --git a/lib/routes/youtube/channel.ts b/lib/routes/youtube/channel.ts index 386f396e1e..01b28853b6 100644 --- a/lib/routes/youtube/channel.ts +++ b/lib/routes/youtube/channel.ts @@ -1,10 +1,8 @@ import { Route } from '@/types'; -import cache from '@/utils/cache'; -import utils from './utils'; -import { config } from '@/config'; -import { parseDate } from '@/utils/parse-date'; -import ConfigNotFoundError from '@/errors/types/config-not-found'; +import utils, { callApi } from './utils'; import InvalidParameterError from '@/errors/types/invalid-parameter'; +import { getDataByChannelId as getDataByChannelIdYoutubei } from './api/youtubei'; +import { getDataByChannelId as getDataByChannelIdGoogle } from './api/google'; export const route: Route = { path: '/channel/:id/:routeParams?', @@ -38,6 +36,7 @@ YouTube provides official RSS feeds for channels, for instance [https://www.yout { name: 'YOUTUBE_KEY', description: ' YouTube API Key, support multiple keys, split them with `,`, [API Key application](https://console.developers.google.com/)', + optional: true, }, ], requirePuppeteer: false, @@ -49,9 +48,6 @@ YouTube provides official RSS feeds for channels, for instance [https://www.yout }; async function handler(ctx) { - if (!config.youtube || !config.youtube.key) { - throw new ConfigNotFoundError('YouTube RSS is disabled due to the lack of relevant config'); - } const id = ctx.req.param('id'); // Parse route parameters @@ -69,32 +65,11 @@ async function handler(ctx) { throw new InvalidParameterError(`Invalid YouTube channel ID. \nYou may want to use /youtube/user/:id instead.`); } - // Get original uploads playlist ID if needed - const originalPlaylistId = filterShorts ? null : (await utils.getChannelWithId(id, 'contentDetails', cache)).data.items[0].contentDetails.relatedPlaylists.uploads; + const data = await callApi({ + googleApi: getDataByChannelIdGoogle, + youtubeiApi: getDataByChannelIdYoutubei, + params: { channelId: id, embed, filterShorts }, + }); - // Use the utility function to get the appropriate playlist ID based on filterShorts setting - const playlistId = filterShorts ? utils.getPlaylistWithShortsFilter(id) : originalPlaylistId; - - const data = (await utils.getPlaylistItems(playlistId, 'snippet', cache)).data.items; - - return { - title: `${data[0].snippet.channelTitle} - YouTube`, - link: `https://www.youtube.com/channel/${id}`, - description: `YouTube channel ${data[0].snippet.channelTitle}`, - item: data - .filter((d) => d.snippet.title !== 'Private video' && d.snippet.title !== 'Deleted video') - .map((item) => { - const snippet = item.snippet; - const videoId = snippet.resourceId.videoId; - const img = utils.getThumbnail(snippet.thumbnails); - return { - title: snippet.title, - description: utils.renderDescription(embed, videoId, img, utils.formatDescription(snippet.description)), - pubDate: parseDate(snippet.publishedAt), - link: `https://www.youtube.com/watch?v=${videoId}`, - author: snippet.videoOwnerChannelTitle, - image: img.url, - }; - }), - }; + return data; } diff --git a/lib/routes/youtube/user.ts b/lib/routes/youtube/user.ts index 117eaba27a..1cbb6598a7 100644 --- a/lib/routes/youtube/user.ts +++ b/lib/routes/youtube/user.ts @@ -1,6 +1,4 @@ import { Route, ViewType } from '@/types'; -import { config } from '@/config'; -import ConfigNotFoundError from '@/errors/types/config-not-found'; import { getDataByUsername as getDataByUsernameYoutubei } from './api/youtubei'; import { getDataByUsername as getDataByUsernameGoogle } from './api/google'; import { callApi } from './utils'; @@ -25,6 +23,7 @@ export const route: Route = { { name: 'YOUTUBE_KEY', description: ' YouTube API Key, support multiple keys, split them with `,`, [API Key application](https://console.developers.google.com/)', + optional: true, }, ], requirePuppeteer: false, @@ -45,9 +44,6 @@ export const route: Route = { }; async function handler(ctx) { - if (!config.youtube || !config.youtube.key) { - throw new ConfigNotFoundError('YouTube RSS is disabled due to the lack of relevant config'); - } const username = ctx.req.param('username'); // Parse route parameters diff --git a/package.json b/package.json index dfdf68b3fb..78b95975b6 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "uuid": "11.1.0", "winston": "3.17.0", "xxhash-wasm": "1.1.0", - "youtubei.js": "^13.4.0", + "youtubei.js": "13.4.0", "zod": "3.24.4" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da0858a5df..3ac8465684 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -281,7 +281,7 @@ importers: specifier: 1.1.0 version: 1.1.0 youtubei.js: - specifier: ^13.4.0 + specifier: 13.4.0 version: 13.4.0 zod: specifier: 3.24.4