youtube-search-api
Version:
Node.js and TypeScript library to get YouTube search results with playlist by provided keywords, no Login or API key required!
519 lines (518 loc) • 23.5 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GetShortVideo = exports.GetVideoDetails = exports.GetChannelById = exports.GetSuggestData = exports.GetPlaylistData = exports.NextPage = exports.GetListByKeyword = exports.VideoRender = exports.ErrorHandler = exports.ErrorCodes = exports.YouTubeAPIError = void 0;
const axios_1 = __importDefault(require("axios"));
// 自定義錯誤類別
class YouTubeAPIError extends Error {
constructor(message, code, statusCode, originalError) {
super(message);
this.name = 'YouTubeAPIError';
this.code = code;
this.statusCode = statusCode;
this.originalError = originalError;
}
}
exports.YouTubeAPIError = YouTubeAPIError;
// 錯誤代碼枚舉
var ErrorCodes;
(function (ErrorCodes) {
ErrorCodes["NETWORK_ERROR"] = "NETWORK_ERROR";
ErrorCodes["INIT_DATA_ERROR"] = "INIT_DATA_ERROR";
ErrorCodes["PLAYER_DATA_ERROR"] = "PLAYER_DATA_ERROR";
ErrorCodes["INVALID_PLAYLIST"] = "INVALID_PLAYLIST";
ErrorCodes["INVALID_VIDEO_ID"] = "INVALID_VIDEO_ID";
ErrorCodes["INVALID_CHANNEL_ID"] = "INVALID_CHANNEL_ID";
ErrorCodes["RATE_LIMIT_ERROR"] = "RATE_LIMIT_ERROR";
ErrorCodes["PARSE_ERROR"] = "PARSE_ERROR";
ErrorCodes["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
})(ErrorCodes || (exports.ErrorCodes = ErrorCodes = {}));
// 錯誤處理器
class ErrorHandler {
constructor() { }
static getInstance() {
if (!ErrorHandler.instance) {
ErrorHandler.instance = new ErrorHandler();
}
return ErrorHandler.instance;
}
setErrorLogger(logger) {
this.errorLogger = logger;
}
handleError(error, context, code = ErrorCodes.UNKNOWN_ERROR) {
var _a;
let youtubeError;
if (error instanceof YouTubeAPIError) {
youtubeError = error;
}
else if (axios_1.default.isAxiosError(error)) {
const statusCode = (_a = error.response) === null || _a === void 0 ? void 0 : _a.status;
const message = this.getErrorMessage(code, context, error.message);
youtubeError = new YouTubeAPIError(message, code, statusCode, error);
}
else {
const message = this.getErrorMessage(code, context, (error === null || error === void 0 ? void 0 : error.message) || (error === null || error === void 0 ? void 0 : error.toString()) || '未知錯誤');
youtubeError = new YouTubeAPIError(message, code, undefined, error);
}
// 記錄錯誤
if (this.errorLogger) {
this.errorLogger(youtubeError);
}
else {
console.error(`[YouTubeAPI Error] ${youtubeError.code}: ${youtubeError.message}`);
}
throw youtubeError;
}
getErrorMessage(code, context, originalMessage) {
const errorMessages = {
[ErrorCodes.NETWORK_ERROR]: `Network connection error: ${context}`,
[ErrorCodes.INIT_DATA_ERROR]: `Cannot get initialization data: ${context}`,
[ErrorCodes.PLAYER_DATA_ERROR]: `Cannot get player data: ${context}`,
[ErrorCodes.INVALID_PLAYLIST]: `Invalid playlist ID: ${context}`,
[ErrorCodes.INVALID_VIDEO_ID]: `Invalid video ID: ${context}`,
[ErrorCodes.INVALID_CHANNEL_ID]: `Invalid channel ID: ${context}`,
[ErrorCodes.RATE_LIMIT_ERROR]: `Rate limit exceeded, please try again later: ${context}`,
[ErrorCodes.PARSE_ERROR]: `Data parsing error: ${context}`,
[ErrorCodes.UNKNOWN_ERROR]: `Unknown error: ${context}`
};
const baseMessage = errorMessages[code];
return originalMessage ? `${baseMessage} (${originalMessage})` : baseMessage;
}
createError(message, code, statusCode, originalError) {
return new YouTubeAPIError(message, code, statusCode, originalError);
}
}
exports.ErrorHandler = ErrorHandler;
// 全域錯誤處理器實例
const errorHandler = ErrorHandler.getInstance();
const youtubeEndpoint = `https://www.youtube.com`;
const GetYoutubeInitData = (url) => __awaiter(void 0, void 0, void 0, function* () {
let initdata = {};
let apiToken = null;
let context = null;
try {
const page = yield axios_1.default.get(encodeURI(url));
const ytInitData = page.data.split("var ytInitialData =");
if (ytInitData && ytInitData.length > 1) {
const data = ytInitData[1].split("</script>")[0].slice(0, -1);
if (page.data.split("innertubeApiKey").length > 1) {
const apiKeyPart = page.data.split("innertubeApiKey")[1];
if (apiKeyPart) {
apiToken = apiKeyPart
.trim()
.split(",")[0]
.split('"')[2];
}
}
if (page.data.split("INNERTUBE_CONTEXT").length > 1) {
const contextPart = page.data.split("INNERTUBE_CONTEXT")[1];
if (contextPart) {
context = JSON.parse(contextPart.trim().slice(2, -2));
}
}
initdata = JSON.parse(data);
return { initdata, apiToken, context };
}
else {
errorHandler.handleError(new Error("Cannot parse YouTube initialization data"), `URL: ${url}`, ErrorCodes.INIT_DATA_ERROR);
// 這行永遠不會執行,因為 handleError 會拋出錯誤
throw new Error("Unreachable code");
}
}
catch (ex) {
if (ex instanceof YouTubeAPIError) {
throw ex;
}
errorHandler.handleError(ex, `Failed to get initialization data - URL: ${url}`, ErrorCodes.INIT_DATA_ERROR);
// 這行永遠不會執行,因為 handleError 會拋出錯誤
throw new Error("Unreachable code");
}
});
const GetYoutubePlayerDetail = (url) => __awaiter(void 0, void 0, void 0, function* () {
let initdata = {};
try {
const page = yield axios_1.default.get(encodeURI(url));
const ytInitData = page.data.split("var ytInitialPlayerResponse =");
if (ytInitData && ytInitData.length > 1) {
const data = ytInitData[1].split("</script>")[0].slice(0, -1);
initdata = JSON.parse(data);
return Object.assign({}, initdata.videoDetails);
}
else {
errorHandler.handleError(new Error("Cannot parse YouTube player data"), `URL: ${url}`, ErrorCodes.PLAYER_DATA_ERROR);
// 這行永遠不會執行,因為 handleError 會拋出錯誤
throw new Error("Unreachable code");
}
}
catch (ex) {
if (ex instanceof YouTubeAPIError) {
throw ex;
}
errorHandler.handleError(ex, `Failed to get player data - URL: ${url}`, ErrorCodes.PLAYER_DATA_ERROR);
// 這行永遠不會執行,因為 handleError 會拋出錯誤
throw new Error("Unreachable code");
}
});
const GetData = (keyword_1, ...args_1) => __awaiter(void 0, [keyword_1, ...args_1], void 0, function* (keyword, withPlaylist = false, limit = 0, options = []) {
let endpoint = `${youtubeEndpoint}/results?search_query=${keyword}`;
try {
if (Array.isArray(options) && options.length > 0) {
const type = options.find((z) => z.type);
if (type && typeof type.type === "string") {
switch (type.type.toLowerCase()) {
case "video":
endpoint = `${endpoint}&sp=EgIQAQ%3D%3D`;
break;
case "channel":
endpoint = `${endpoint}&sp=EgIQAg%3D%3D`;
break;
case "playlist":
endpoint = `${endpoint}&sp=EgIQAw%3D%3D`;
break;
case "movie":
endpoint = `${endpoint}&sp=EgIQBA%3D%3D`;
break;
}
}
}
const page = yield GetYoutubeInitData(endpoint);
const sectionListRenderer = page.initdata.contents.twoColumnSearchResultsRenderer.primaryContents
.sectionListRenderer;
let contToken = {};
let items = [];
sectionListRenderer.contents.forEach((content) => {
if (content.continuationItemRenderer) {
contToken =
content.continuationItemRenderer.continuationEndpoint
.continuationCommand.token;
}
else if (content.itemSectionRenderer) {
content.itemSectionRenderer.contents.forEach((item) => {
if (item.channelRenderer) {
let channelRenderer = item.channelRenderer;
items.push({
id: channelRenderer.channelId,
type: "channel",
thumbnail: channelRenderer.thumbnail,
title: channelRenderer.title.simpleText
});
}
else {
let videoRender = item.videoRenderer;
let playListRender = item.playlistRenderer;
if (videoRender && videoRender.videoId) {
items.push((0, exports.VideoRender)(item));
}
if (withPlaylist) {
if (playListRender && playListRender.playlistId) {
items.push({
id: playListRender.playlistId,
type: "playlist",
thumbnail: playListRender.thumbnails,
title: playListRender.title.simpleText,
length: playListRender.videoCount,
videos: playListRender.videos,
videoCount: playListRender.videoCount,
isLive: false
});
}
}
}
});
}
});
const apiToken = page.apiToken;
const context = page.context;
const nextPageContext = { context, continuation: contToken };
const itemsResult = limit !== 0 ? items.slice(0, limit) : items;
return {
items: itemsResult,
nextPage: { nextPageToken: apiToken, nextPageContext }
};
}
catch (ex) {
if (ex instanceof YouTubeAPIError) {
throw ex;
}
errorHandler.handleError(ex, `Search failed - keyword: ${keyword}`, ErrorCodes.UNKNOWN_ERROR);
// 這行永遠不會執行,因為 handleError 會拋出錯誤
throw new Error("Unreachable code");
}
});
exports.GetListByKeyword = GetData;
const nextPage = (nextPage_1, ...args_1) => __awaiter(void 0, [nextPage_1, ...args_1], void 0, function* (nextPage, withPlaylist = false, limit = 0) {
const endpoint = `${youtubeEndpoint}/youtubei/v1/search?key=${nextPage.nextPageToken}`;
try {
const page = yield axios_1.default.post(encodeURI(endpoint), nextPage.nextPageContext);
const item1 = page.data.onResponseReceivedCommands[0].appendContinuationItemsAction;
let items = [];
item1.continuationItems.forEach((conitem) => {
if (conitem.itemSectionRenderer) {
conitem.itemSectionRenderer.contents.forEach((item) => __awaiter(void 0, void 0, void 0, function* () {
let videoRender = item.videoRenderer;
let playListRender = item.playlistRenderer;
if (videoRender && videoRender.videoId) {
items.push((0, exports.VideoRender)(item));
}
if (withPlaylist) {
if (playListRender && playListRender.playlistId) {
items.push({
id: playListRender.playlistId,
type: "playlist",
thumbnail: playListRender.thumbnails,
title: playListRender.title.simpleText,
length: playListRender.videoCount,
videos: (yield GetPlaylistData(playListRender.playlistId))
.items,
videoCount: playListRender.videoCount,
isLive: false
});
}
}
}));
}
else if (conitem.continuationItemRenderer) {
nextPage.nextPageContext.continuation =
conitem.continuationItemRenderer.continuationEndpoint.continuationCommand.token;
}
});
const itemsResult = limit !== 0 ? items.slice(0, limit) : items;
return { items: itemsResult, nextPage };
}
catch (ex) {
if (ex instanceof YouTubeAPIError) {
throw ex;
}
errorHandler.handleError(ex, `Failed to get next page`, ErrorCodes.UNKNOWN_ERROR);
// 這行永遠不會執行,因為 handleError 會拋出錯誤
throw new Error("Unreachable code");
}
});
exports.NextPage = nextPage;
const GetPlaylistData = (playlistId_1, ...args_1) => __awaiter(void 0, [playlistId_1, ...args_1], void 0, function* (playlistId, limit = 0) {
const endpoint = `${youtubeEndpoint}/playlist?list=${playlistId}`;
try {
const initData = yield GetYoutubeInitData(endpoint);
const sectionListRenderer = initData.initdata;
const metadata = sectionListRenderer.metadata;
if (sectionListRenderer && sectionListRenderer.contents) {
const videoItems = sectionListRenderer.contents.twoColumnBrowseResultsRenderer.tabs[0]
.tabRenderer.content.sectionListRenderer.contents[0]
.itemSectionRenderer.contents[0].playlistVideoListRenderer.contents;
let items = [];
videoItems.forEach((item) => {
let videoRender = item.playlistVideoRenderer;
if (videoRender && videoRender.videoId) {
items.push((0, exports.VideoRender)(item));
}
});
const itemsResult = limit !== 0 ? items.slice(0, limit) : items;
return { items: itemsResult, metadata };
}
else {
errorHandler.handleError(new Error("Invalid playlist"), `播放清單 ID: ${playlistId}`, ErrorCodes.INVALID_PLAYLIST);
// 這行永遠不會執行,因為 handleError 會拋出錯誤
throw new Error("Unreachable code");
}
}
catch (ex) {
if (ex instanceof YouTubeAPIError) {
throw ex;
}
errorHandler.handleError(ex, `Failed to get playlist - ID: ${playlistId}`, ErrorCodes.UNKNOWN_ERROR);
// 這行永遠不會執行,因為 handleError 會拋出錯誤
throw new Error("Unreachable code");
}
});
exports.GetPlaylistData = GetPlaylistData;
const GetSuggestData = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (limit = 0) {
const endpoint = youtubeEndpoint;
try {
const page = yield GetYoutubeInitData(endpoint);
const sectionListRenderer = page.initdata.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer
.content.richGridRenderer.contents;
let items = [];
let otherItems = [];
sectionListRenderer.forEach((item) => {
if (item.richItemRenderer && item.richItemRenderer.content) {
let videoRender = item.richItemRenderer.content.videoRenderer;
if (videoRender && videoRender.videoId) {
items.push((0, exports.VideoRender)(item.richItemRenderer.content));
}
else {
otherItems.push(videoRender);
}
}
});
const itemsResult = limit !== 0 ? items.slice(0, limit) : items;
return { items: itemsResult };
}
catch (ex) {
console.error(ex);
throw ex;
}
});
exports.GetSuggestData = GetSuggestData;
const GetChannelById = (channelId) => __awaiter(void 0, void 0, void 0, function* () {
const endpoint = `${youtubeEndpoint}/channel/${channelId}`;
try {
const page = yield GetYoutubeInitData(endpoint);
const tabs = page.initdata.contents.twoColumnBrowseResultsRenderer.tabs;
const items = tabs
.map((json) => {
if (json && json.tabRenderer) {
const tabRenderer = json.tabRenderer;
const title = tabRenderer.title;
const content = tabRenderer.content;
return { title, content };
}
})
.filter((y) => typeof y !== "undefined");
return items;
}
catch (ex) {
console.error(ex);
throw ex;
}
});
exports.GetChannelById = GetChannelById;
const GetVideoDetails = (videoId) => __awaiter(void 0, void 0, void 0, function* () {
const endpoint = `${youtubeEndpoint}/watch?v=${videoId}`;
try {
const page = yield GetYoutubeInitData(endpoint);
const playerData = yield GetYoutubePlayerDetail(endpoint);
const result = page.initdata.contents.twoColumnWatchNextResults;
const firstContent = result.results.results.contents[0].videoPrimaryInfoRenderer;
const secondContent = result.results.results.contents[1].videoSecondaryInfoRenderer;
const res = {
id: playerData.videoId,
title: firstContent.title.runs[0].text,
thumbnail: playerData.thumbnail,
isLive: firstContent.viewCount.videoViewCountRenderer.hasOwnProperty("isLive")
? firstContent.viewCount.videoViewCountRenderer.isLive
: false,
channel: playerData.author ||
secondContent.owner.videoOwnerRenderer.title.runs[0].text,
channelId: playerData.channelId,
description: playerData.shortDescription,
keywords: playerData.keywords,
suggestion: result.secondaryResults.secondaryResults.results
.filter((y) => y.hasOwnProperty("compactVideoRenderer"))
.map((x) => compactVideoRenderer(x))
};
return res;
}
catch (ex) {
console.error(ex);
throw ex;
}
});
exports.GetVideoDetails = GetVideoDetails;
const VideoRender = (json) => {
try {
if (json && (json.videoRenderer || json.playlistVideoRenderer)) {
let videoRenderer = json.videoRenderer || json.playlistVideoRenderer;
let isLive = false;
if (videoRenderer.badges &&
videoRenderer.badges.length > 0 &&
videoRenderer.badges[0].metadataBadgeRenderer &&
videoRenderer.badges[0].metadataBadgeRenderer.style ===
"BADGE_STYLE_TYPE_LIVE_NOW") {
isLive = true;
}
if (videoRenderer.thumbnailOverlays) {
videoRenderer.thumbnailOverlays.forEach((item) => {
if (item.thumbnailOverlayTimeStatusRenderer &&
item.thumbnailOverlayTimeStatusRenderer.style &&
item.thumbnailOverlayTimeStatusRenderer.style === "LIVE") {
isLive = true;
}
});
}
const id = videoRenderer.videoId;
const thumbnail = videoRenderer.thumbnail;
const title = videoRenderer.title.runs[0].text;
const shortBylineText = videoRenderer.shortBylineText
? videoRenderer.shortBylineText
: "";
const lengthText = videoRenderer.lengthText
? videoRenderer.lengthText
: "";
const channelTitle = videoRenderer.ownerText && videoRenderer.ownerText.runs
? videoRenderer.ownerText.runs[0].text
: "";
return {
id,
type: "video",
thumbnail,
title,
channelTitle,
shortBylineText,
length: lengthText,
isLive
};
}
return {
id: "",
type: "",
thumbnail: undefined,
title: ""
};
}
catch (ex) {
throw ex;
}
};
exports.VideoRender = VideoRender;
const compactVideoRenderer = (json) => {
const compactVideoRendererJson = json.compactVideoRenderer;
let isLive = false;
if (compactVideoRendererJson.badges &&
compactVideoRendererJson.badges.length > 0 &&
compactVideoRendererJson.badges[0].metadataBadgeRenderer &&
compactVideoRendererJson.badges[0].metadataBadgeRenderer.style ===
"BADGE_STYLE_TYPE_LIVE_NOW") {
isLive = true;
}
return {
id: compactVideoRendererJson.videoId,
type: "video",
thumbnail: compactVideoRendererJson.thumbnail.thumbnails,
title: compactVideoRendererJson.title.simpleText,
channelTitle: compactVideoRendererJson.shortBylineText.runs[0].text,
shortBylineText: compactVideoRendererJson.shortBylineText.runs[0].text,
length: compactVideoRendererJson.lengthText,
isLive
};
};
const GetShortVideo = () => __awaiter(void 0, void 0, void 0, function* () {
const page = yield GetYoutubeInitData(youtubeEndpoint);
const shortResult = page.initdata.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.richGridRenderer.contents
.filter((x) => x.richSectionRenderer)
.map((z) => z.richSectionRenderer.content)
.filter((y) => y.richShelfRenderer)
.map((u) => u.richShelfRenderer)
.find((i) => i.title.runs[0].text === "Shorts");
const res = shortResult.contents
.map((z) => z.richItemRenderer)
.map((y) => y.content.reelItemRenderer);
return res.map((json) => ({
id: json.videoId,
type: "reel",
thumbnail: json.thumbnail.thumbnails[0],
title: json.headline.simpleText,
inlinePlaybackEndpoint: json.inlinePlaybackEndpoint || {}
}));
});
exports.GetShortVideo = GetShortVideo;