UNPKG

rsshub

Version:
331 lines (327 loc) • 14 kB
import { n as init_esm_shims, t as __dirname } from "./esm-shims-CzJ_djXG.mjs"; import { t as config } from "./config-C37vj7VH.mjs"; import { t as ofetch_default } from "./ofetch-BIyrKU3Y.mjs"; import { t as parseDate } from "./parse-date-BrP7mxXf.mjs"; import { t as not_found_default } from "./not-found-Z_3JX2qs.mjs"; import { t as cache_default } from "./cache-Bo__VnGm.mjs"; import { t as art } from "./render-BQo6B4tL.mjs"; import path from "node:path"; import dayjs from "dayjs"; import duration from "dayjs/plugin/duration.js"; import * as cheerio from "cheerio"; import pMap from "p-map"; import { google } from "googleapis"; import { getSubtitles } from "youtube-caption-extractor"; //#region lib/routes/youtube/utils.ts init_esm_shims(); const getPlaylistItems = (id, part, cache) => cache.tryGet(`youtube:getPlaylistItems:${id}`, async () => { return await exec((youtube$1) => youtube$1.playlistItems.list({ part, playlistId: id, maxResults: 50 })); }, config.cache.routeExpire, false); const getPlaylist = (id, part, cache) => cache.tryGet(`youtube:getPlaylist:${id}`, async () => { return await exec((youtube$1) => youtube$1.playlists.list({ part, id })); }); const getChannelWithId = (id, part, cache) => cache.tryGet(`youtube:getChannelWithId:${id}`, async () => { return await exec((youtube$1) => youtube$1.channels.list({ part, id })); }); const getChannelWithUsername = (username, part, cache) => cache.tryGet(`youtube:getChannelWithUsername:${username}`, async () => { return await exec((youtube$1) => youtube$1.channels.list({ part, forUsername: username })); }); const getVideos = (id, part, cache) => cache.tryGet(`youtube:getVideos:${id}`, async () => { return await exec((youtube$1) => youtube$1.videos.list({ part, id })); }); const getThumbnail = (thumbnails) => thumbnails.maxres || thumbnails.standard || thumbnails.high || thumbnails.medium || thumbnails.default; const formatDescription = (description) => description?.replaceAll(/\r\n|\r|\n/g, "<br>"); const renderDescription = (embed, videoId, img, description) => art(path.join(__dirname, "templates/description-d8937611.art"), { embed, videoId, img, description }); const getSubscriptions = async (part, cache) => { let accessToken = await cache.get("youtube:accessToken", false); if (!accessToken) { accessToken = (await youtubeOAuth2Client.getAccessToken()).token; await cache.set("youtube:accessToken", accessToken, 3600); } youtubeOAuth2Client.setCredentials({ access_token: accessToken, refresh_token: config.youtube.refreshToken }); return cache.tryGet("youtube:getSubscriptions", () => getSubscriptionsRecusive(part), config.cache.routeExpire, false); }; async function getSubscriptionsRecusive(part, nextPageToken) { const res = await google.youtube("v3").subscriptions.list({ auth: youtubeOAuth2Client, part, mine: true, maxResults: 50, pageToken: nextPageToken ?? void 0 }); if (res.data.nextPageToken) { const next = await getSubscriptionsRecusive(part, res.data.nextPageToken); if (next.data.items) res.data.items = [...res.data.items || [], ...next.data.items]; } return res; } const isYouTubeChannelId = (id) => /^UC[\w-]{21}[AQgw]$/.test(id); const getLive = (id, cache) => cache.tryGet(`youtube:getLive:${id}`, async () => { return await exec((youtube$1) => youtube$1.search.list({ part: "snippet", channelId: id, eventType: "live", type: "video" })); }); const getVideoUrl = (id) => `https://www.youtube-nocookie.com/embed/${id}?controls=1&autoplay=1&mute=0`; const getPlaylistWithShortsFilter = (id, filterShorts = true) => { if (filterShorts) { if (id.startsWith("UC")) return "UULF" + id.slice(2); else if (id.startsWith("UU")) return "UULF" + id.slice(2); } return id; }; const callApi = async ({ googleApi, youtubeiApi, params }) => { if (config.youtube?.key) try { return await googleApi(params); } catch { return await youtubeiApi(params); } return await youtubeiApi(params); }; var utils_default = { getPlaylistItems, getPlaylist, getChannelWithId, getChannelWithUsername, getVideos, getThumbnail, formatDescription, renderDescription, getSubscriptions, getSubscriptionsRecusive, isYouTubeChannelId, getLive, getVideoUrl, getPlaylistWithShortsFilter }; //#endregion //#region lib/routes/youtube/api/subtitles.ts function pad(n, width = 2) { return String(n).padStart(width, "0"); } function toSrtTime(seconds) { const totalMs = Math.floor(seconds * 1e3); const hours = Math.floor(totalMs / 36e5); const minutes = Math.floor(totalMs % 36e5 / 6e4); const secs = Math.floor(totalMs % 6e4 / 1e3); const millis = totalMs % 1e3; return `${pad(hours)}:${pad(minutes)}:${pad(secs)},${pad(millis, 3)}`; } function convertToSrt(segments) { return segments.map((seg, index$1) => { const start = Number.parseFloat(seg.start); const end = start + Number.parseFloat(seg.dur); return `${index$1 + 1} ${toSrtTime(start)} --> ${toSrtTime(end)} ${seg.text} `; }).join("\n"); } const getSubtitlesByVideoId = (videoId) => cache_default.tryGet(`youtube:getSubtitlesByVideoId:${videoId}`, async () => { try { return convertToSrt(await getSubtitles({ videoID: videoId })); } catch { return ""; } }); const createSubtitleDataUrl = (srt) => `data:text/plain;charset=utf-8,${encodeURIComponent(srt)}`; function createSrtAttachmentFromSrt(srt) { if (!srt || srt.trim() === "") return []; return [{ url: createSubtitleDataUrl(srt), mime_type: "text/srt", title: "Subtitles" }]; } const getSrtAttachmentBatch = async (videoIds) => { const results = await pMap(videoIds, async (videoId) => { return { videoId, srt: createSrtAttachmentFromSrt(await getSubtitlesByVideoId(videoId)) }; }, { concurrency: 5 }); return Object.fromEntries(results.map(({ videoId, srt }) => [videoId, srt])); }; //#endregion //#region lib/routes/youtube/api/google.ts const { OAuth2 } = google.auth; dayjs.extend(duration); let count = 0; const youtube = {}; if (config.youtube && config.youtube.key) { const keys = config.youtube.key.split(","); for (const [index$1, key] of keys.entries()) if (key) { youtube[index$1] = google.youtube({ version: "v3", auth: key }); count = index$1 + 1; } } let index = -1; const exec = async (func) => { let result; for (let i = 0; i < count; i++) { index++; try { result = await func(youtube[index % count]); break; } catch {} } 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 }); } const getDataByUsername = async ({ username, embed, filterShorts, isJsonFeed }) => { let userHandleData; if (username.startsWith("@")) userHandleData = await cache_default.tryGet(`youtube:handle:${username}`, async () => { const response = await ofetch_default(`https://www.youtube.com/${username}`); const $ = cheerio.load(response); const metadataRenderer = JSON.parse($("script").text().match(/ytInitialData = ({.*?});/)?.[1] || "{}").metadata.channelMetadataRenderer; const channelId = metadataRenderer.externalId; return { channelName: metadataRenderer.title, image: metadataRenderer.avatar?.thumbnails?.[0]?.url, description: metadataRenderer.description, playlistId: (await utils_default.getChannelWithId(channelId, "contentDetails", cache_default)).data.items[0].contentDetails.relatedPlaylists.uploads }; }); const playlistId = await (async () => { if (userHandleData?.playlistId) { const origPlaylistId = userHandleData.playlistId; return utils_default.getPlaylistWithShortsFilter(origPlaylistId, filterShorts); } else { const items = (await utils_default.getChannelWithUsername(username, "contentDetails", cache_default)).data.items; if (!items) throw new not_found_default(`The channel https://www.youtube.com/user/${username} does not exist.`); const channelId = items[0].id; return filterShorts ? utils_default.getPlaylistWithShortsFilter(channelId, filterShorts) : items[0].contentDetails.relatedPlaylists.uploads; } })(); const playlistItems = await utils_default.getPlaylistItems(playlistId, "snippet", cache_default); if (!playlistItems) throw new not_found_default("This channel doesn't have any content."); const videoIds = playlistItems.data.items.map((item) => item.snippet.resourceId.videoId); const videoDetails = await utils_default.getVideos(videoIds.join(","), "contentDetails", cache_default); 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_default.getThumbnail(snippet.thumbnails); const detail = videoDetails?.data.items.find((d) => d.id === videoId); const srtAttachments = subtitlesMap ? subtitlesMap[videoId] || [] : []; return { title: snippet.title, description: utils_default.renderDescription(embed, videoId, img, utils_default.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() : void 0 }, ...srtAttachments] }; }) }; }; const getDataByChannelId = async ({ channelId, embed, filterShorts, isJsonFeed }) => { const originalPlaylistId = filterShorts ? null : (await utils_default.getChannelWithId(channelId, "contentDetails", cache_default)).data.items[0].contentDetails.relatedPlaylists.uploads; const playlistId = filterShorts ? utils_default.getPlaylistWithShortsFilter(channelId) : originalPlaylistId; const data = (await utils_default.getPlaylistItems(playlistId, "snippet", cache_default)).data.items; const videoIds = data.map((item) => item.snippet.resourceId.videoId); const videoDetails = await utils_default.getVideos(videoIds.join(","), "contentDetails", cache_default); const subtitlesMap = isJsonFeed ? 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_default.getThumbnail(snippet.thumbnails); const detail = videoDetails?.data.items.find((d) => d.id === videoId); const srtAttachments = subtitlesMap ? subtitlesMap[videoId] || [] : []; return { title: snippet.title, description: utils_default.renderDescription(embed, videoId, img, utils_default.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() : void 0 }, ...srtAttachments] }; }) }; }; const getDataByPlaylistId = async ({ playlistId, embed, isJsonFeed }) => { const playlistTitle = (await utils_default.getPlaylist(playlistId, "snippet", cache_default)).data.items[0].snippet.title; const data = (await utils_default.getPlaylistItems(playlistId, "snippet", cache_default)).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_default.getVideos(videoIds.join(","), "contentDetails", cache_default); const subtitlesMap = isJsonFeed ? 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_default.getThumbnail(snippet.thumbnails); const detail = videoDetails?.data.items.find((d) => d.id === videoId); const srtAttachments = subtitlesMap ? subtitlesMap[videoId] || [] : []; return { title: snippet.title, description: utils_default.renderDescription(embed, videoId, img, utils_default.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() : void 0 }, ...srtAttachments] }; }) }; }; //#endregion export { callApi as a, renderDescription as c, getSrtAttachmentBatch as i, utils_default as l, getDataByPlaylistId as n, getVideoUrl as o, getDataByUsername as r, isYouTubeChannelId as s, getDataByChannelId as t };