UNPKG

rsshub

Version:
231 lines (204 loc) • 10.6 kB
import { google } from 'googleapis'; const { OAuth2 } = google.auth; import { config } from '@/config'; import utils, { getVideoUrl } from '../utils'; import cache from '@/utils/cache'; import { parseDate } from '@/utils/parse-date'; import ofetch from '@/utils/ofetch'; import * as cheerio from 'cheerio'; import NotFoundError from '@/errors/types/not-found'; import { Data } from '@/types'; import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration.js'; import { getSrtAttachmentBatch } from './subtitles'; dayjs.extend(duration); let count = 0; const youtube = {}; if (config.youtube && config.youtube.key) { const keys = config.youtube.key.split(','); for (const [index, key] of keys.entries()) { if (key) { youtube[index] = google.youtube({ version: 'v3', auth: key, }); count = index + 1; } } } let index = -1; const exec = async (func) => { let result; for (let i = 0; i < count; i++) { index++; try { // eslint-disable-next-line no-await-in-loop result = await func(youtube[index % count]); break; } catch { // console.error(error); } } return result; }; let youtubeOAuth2Client; if (config.youtube && config.youtube.clientId && config.youtube.clientSecret && config.youtube.refreshToken) { youtubeOAuth2Client = new OAuth2(config.youtube.clientId, config.youtube.clientSecret, 'https://developers.google.com/oauthplayground'); youtubeOAuth2Client.setCredentials({ refresh_token: config.youtube.refreshToken }); } export { youtubeOAuth2Client, exec }; export const getDataByUsername = async ({ username, embed, filterShorts, isJsonFeed }: { username: string; embed: boolean; filterShorts: boolean; isJsonFeed: boolean }): Promise<Data> => { let userHandleData; if (username.startsWith('@')) { userHandleData = await cache.tryGet(`youtube:handle:${username}`, async () => { const link = `https://www.youtube.com/${username}`; const response = await ofetch(link); const $ = cheerio.load(response); const ytInitialData = JSON.parse( $('script') .text() .match(/ytInitialData = ({.*?});/)?.[1] || '{}' ); const metadataRenderer = ytInitialData.metadata.channelMetadataRenderer; const channelId = metadataRenderer.externalId; const channelName = metadataRenderer.title; const image = metadataRenderer.avatar?.thumbnails?.[0]?.url; const description = metadataRenderer.description; const playlistId = (await utils.getChannelWithId(channelId, 'contentDetails', cache)).data.items[0].contentDetails.relatedPlaylists.uploads; return { channelName, image, description, playlistId, }; }); } // Get the appropriate playlist ID based on filterShorts setting const playlistId = await (async () => { if (userHandleData?.playlistId) { const origPlaylistId = userHandleData.playlistId; return utils.getPlaylistWithShortsFilter(origPlaylistId, filterShorts); } else { const channelData = await utils.getChannelWithUsername(username, 'contentDetails', cache); const items = channelData.data.items; if (!items) { throw new NotFoundError(`The channel https://www.youtube.com/user/${username} does not exist.`); } const channelId = items[0].id; return filterShorts ? utils.getPlaylistWithShortsFilter(channelId, filterShorts) : items[0].contentDetails.relatedPlaylists.uploads; } })(); const playlistItems = await utils.getPlaylistItems(playlistId, 'snippet', cache); if (!playlistItems) { throw new NotFoundError("This channel doesn't have any content."); } const videoIds = playlistItems.data.items.map((item) => item.snippet.resourceId.videoId); const videoDetails = await utils.getVideos(videoIds.join(','), 'contentDetails', cache); const subtitlesMap = isJsonFeed ? await getSrtAttachmentBatch(videoIds) : {}; return { title: `${userHandleData?.channelName || username} - YouTube`, link: username.startsWith('@') ? `https://www.youtube.com/${username}` : `https://www.youtube.com/user/${username}`, description: userHandleData?.description || `YouTube user ${username}`, image: userHandleData?.image, item: playlistItems.data.items .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); const detail = videoDetails?.data.items.find((d) => d.id === videoId); const srtAttachments = subtitlesMap ? subtitlesMap[videoId] || [] : []; 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, attachments: [ { url: getVideoUrl(videoId), mime_type: 'text/html', duration_in_seconds: detail?.contentDetails.duration ? dayjs.duration(detail.contentDetails.duration).asSeconds() : undefined, }, ...srtAttachments, ], }; }), }; }; export const getDataByChannelId = async ({ channelId, embed, filterShorts }: { channelId: string; embed: boolean; filterShorts: boolean }): Promise<Data> => { // 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; const videoIds = data.map((item) => item.snippet.resourceId.videoId); const videoDetails = await utils.getVideos(videoIds.join(','), 'contentDetails', cache); const subtitlesMap = await getSrtAttachmentBatch(videoIds); 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); const detail = videoDetails?.data.items.find((d) => d.id === videoId); const srtAttachments = subtitlesMap[videoId] || []; 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, attachments: [ { url: getVideoUrl(videoId), mime_type: 'text/html', duration_in_seconds: detail?.contentDetails.duration ? dayjs.duration(detail.contentDetails.duration).asSeconds() : undefined, }, ...srtAttachments, ], }; }), }; }; export const getDataByPlaylistId = async ({ playlistId, embed }: { playlistId: string; embed: boolean }): Promise<Data> => { const playlistTitle = (await utils.getPlaylist(playlistId, 'snippet', cache)).data.items[0].snippet.title; const data = (await utils.getPlaylistItems(playlistId, 'snippet', cache)).data.items.filter((d) => d.snippet.title !== 'Private video' && d.snippet.title !== 'Deleted video'); const videoIds = data.map((item) => item.snippet.resourceId.videoId); const videoDetails = await utils.getVideos(videoIds.join(','), 'contentDetails', cache); const subtitlesMap = await getSrtAttachmentBatch(videoIds); return { title: `${playlistTitle} by ${data[0].snippet.channelTitle} - YouTube`, link: `https://www.youtube.com/playlist?list=${playlistId}`, description: `${playlistTitle} by ${data[0].snippet.channelTitle}`, item: data.map((item) => { const snippet = item.snippet; const videoId = snippet.resourceId.videoId; const img = utils.getThumbnail(snippet.thumbnails); const detail = videoDetails?.data.items.find((d) => d.id === videoId); const srtAttachments = subtitlesMap[videoId] || []; 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, attachments: [ { url: getVideoUrl(videoId), mime_type: 'text/html', duration_in_seconds: detail?.contentDetails.duration ? dayjs.duration(detail.contentDetails.duration).asSeconds() : undefined, }, ...srtAttachments, ], }; }), }; };