|
@@ -1,4 +1,56 @@
|
|
|
-const request = async (method, url, data = null, headers = {}) => {
|
|
|
+function parseCodecs(format) {
|
|
|
+ const mimeType = format["mimeType"]
|
|
|
+ if (!mimeType) {
|
|
|
+ return {};
|
|
|
+ }
|
|
|
+ const regex = /(?<mimetype>[^/]+\/[^;]+)(?:;\s*codecs="?(?<codecs>[^"]+))?/;
|
|
|
+ const match = mimeType.match(regex);
|
|
|
+ if (!match) {
|
|
|
+ return {};
|
|
|
+ }
|
|
|
+ const codecs = match.groups.codecs;
|
|
|
+ if (!codecs) {
|
|
|
+ return {};
|
|
|
+ }
|
|
|
+
|
|
|
+ const splitCodecs = codecs.trim().replace(/,$/, '').split(',').map(str => str.trim()).filter(Boolean);
|
|
|
+ let vcodec = null;
|
|
|
+ let acodec = null;
|
|
|
+
|
|
|
+ for (const fullCodec of splitCodecs) {
|
|
|
+ const codec = fullCodec.split('.')[0];
|
|
|
+
|
|
|
+ if (['avc1', 'avc2', 'avc3', 'avc4', 'vp9', 'vp8', 'hev1', 'hev2', 'h263', 'h264', 'mp4v', 'hvc1', 'av01', 'theora'].includes(codec)) {
|
|
|
+ if (!vcodec) {
|
|
|
+ vcodec = fullCodec;
|
|
|
+ }
|
|
|
+ } else if (['mp4a', 'opus', 'vorbis', 'mp3', 'aac', 'ac-3', 'ec-3', 'eac3', 'dtsc', 'dtse', 'dtsh', 'dtsl'].includes(codec)) {
|
|
|
+ if (!acodec) {
|
|
|
+ acodec = fullCodec;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ console.warn(`WARNING: Unknown codec ${fullCodec}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!vcodec && !acodec) {
|
|
|
+ if (splitCodecs.length === 2) {
|
|
|
+ return {
|
|
|
+ vcodec: splitCodecs[0],
|
|
|
+ acodec: splitCodecs[1]
|
|
|
+ };
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ return {
|
|
|
+ vcodec: vcodec,
|
|
|
+ acodec: acodec
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ return {};
|
|
|
+}
|
|
|
+
|
|
|
+request = async (method, url, data = null, headers = {}) => {
|
|
|
return new Promise(function (resolve, reject) {
|
|
|
const xhr = new XMLHttpRequest();
|
|
|
xhr.open(method, url);
|
|
@@ -24,14 +76,48 @@ const request = async (method, url, data = null, headers = {}) => {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-getVideoDetail = async (url) => {
|
|
|
+getStringBetween = (string, needleStart, needleEnd, offsetStart = 0, offsetEnd = 0) => {
|
|
|
+ const x = string.indexOf(needleStart);
|
|
|
+ const y = needleEnd ? string.indexOf(needleEnd, x) : string.length;
|
|
|
+ return string.substring(x + needleStart.length + offsetEnd, y + offsetStart);
|
|
|
+}
|
|
|
+
|
|
|
+getUrlFromSignature = (signatureCipher, baseContent) => {
|
|
|
+ const decipher = getDecipherFunction(baseContent);
|
|
|
+ const searchParams = new URLSearchParams(signatureCipher);
|
|
|
+ const [url, signature, sp] = [searchParams.get("url"), searchParams.get("s"), searchParams.get("sp")];
|
|
|
+ return `${url}&${sp}=${decipher(signature)}`;
|
|
|
+}
|
|
|
+
|
|
|
+getDecipherFunction = (string) => {
|
|
|
+ const js = string.replace("var _yt_player={}", "");
|
|
|
+ const top = getStringBetween(js, `a=a.split("")`, "};", 1, -28);
|
|
|
+ const beginningOfFunction =
|
|
|
+ "var " + getStringBetween(top, `a=a.split("")`, "(", 10, 1).split(".")[0] + "=";
|
|
|
+ const side = getStringBetween(js, beginningOfFunction, "};", 2, -beginningOfFunction.length);
|
|
|
+ return eval(side + top);
|
|
|
+};
|
|
|
+
|
|
|
+detail = async (url, local) => {
|
|
|
const headers = {
|
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Safari/537.36',
|
|
|
}
|
|
|
- const res = await request('GET', url, null, headers)
|
|
|
+ if (local) {
|
|
|
+ url = url.replace("https://www.youtube.com", "http://127.0.0.1");
|
|
|
+ }
|
|
|
+ const html = await request('GET', url, null, headers);
|
|
|
+
|
|
|
+ let baseJsUrl = `https://www.youtube.com${JSON.parse(html.match(/set\(({.+?})\);/)[1])["PLAYER_JS_URL"]}`
|
|
|
+ if (local) {
|
|
|
+ baseJsUrl = baseJsUrl.replace("https://www.youtube.com", "http://127.0.0.1");
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(baseJsUrl);
|
|
|
+
|
|
|
+ const baseContent = await request('GET', baseJsUrl, null, headers);
|
|
|
|
|
|
let regex = /var ytInitialPlayerResponse\s*=\s*({.*?});/;
|
|
|
- let match = res.match(regex);
|
|
|
+ let match = html.match(regex);
|
|
|
if (!match || !match.length) {
|
|
|
throw new Error('JSON not found.');
|
|
|
}
|
|
@@ -48,28 +134,23 @@ getVideoDetail = async (url) => {
|
|
|
}
|
|
|
|
|
|
const formats = []
|
|
|
- for (const item of ytInitialPlayerResponse["streamingData"]["formats"].concat(ytInitialPlayerResponse["streamingData"]["adaptiveFormats"])) {
|
|
|
- if (item && item["signatureCipher"] && item["mimeType"]) {
|
|
|
- let urlRegex = /url=([^&]+)/;
|
|
|
- let match = item["signatureCipher"].match(urlRegex);
|
|
|
- if (!match) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- const encodedUrl = match[1];
|
|
|
- const decodedUrl = decodeURIComponent(encodedUrl);
|
|
|
-
|
|
|
+ for (let format of [].concat(ytInitialPlayerResponse["streamingData"]["formats"]).concat(ytInitialPlayerResponse["streamingData"]["adaptiveFormats"])) {
|
|
|
+ console.log(format);
|
|
|
+ format.url = getUrlFromSignature(format["signatureCipher"], baseContent);
|
|
|
+ const {vcodec, acodec} = parseCodecs(format)
|
|
|
+ if (vcodec && acodec) {
|
|
|
formats.push({
|
|
|
- "width": item["width"] + "",
|
|
|
- "height": item["height"] + "",
|
|
|
- "type": item["mimeType"],
|
|
|
- "quality": item["quality"],
|
|
|
- "itag": item["itag"],
|
|
|
- "fps": item["fps"] + "",
|
|
|
- "bitrate": item["bitrate"] + "",
|
|
|
- "url": decodedUrl,
|
|
|
+ "width": format["width"] + "",
|
|
|
+ "height": format["height"] + "",
|
|
|
+ "type": format["mimeType"],
|
|
|
+ "quality": format["quality"],
|
|
|
+ "itag": format["itag"],
|
|
|
+ "fps": format["fps"] + "",
|
|
|
+ "bitrate": format["bitrate"] + "",
|
|
|
+ "url": format["url"],
|
|
|
"ext": "mp4",
|
|
|
- "vcodec": item["mimeType"],
|
|
|
- "acodec": item["mimeType"],
|
|
|
+ "vcodec": vcodec,
|
|
|
+ "acodec": acodec,
|
|
|
"vbr": "0",
|
|
|
"abr": "0",
|
|
|
"container": "mp4_dash"
|
|
@@ -78,7 +159,7 @@ getVideoDetail = async (url) => {
|
|
|
}
|
|
|
|
|
|
regex = /var ytInitialData\s*=\s*({.*?});/;
|
|
|
- match = res.match(regex);
|
|
|
+ match = html.match(regex);
|
|
|
if (!match || !match.length) {
|
|
|
throw new Error('JSON not found.');
|
|
|
}
|
|
@@ -91,17 +172,20 @@ getVideoDetail = async (url) => {
|
|
|
for (const item of ytInitialData["contents"]["twoColumnWatchNextResults"]["secondaryResults"]["secondaryResults"]["results"]) {
|
|
|
if (item["compactVideoRenderer"]) {
|
|
|
const recommendVideo = item["compactVideoRenderer"];
|
|
|
- recommendInfo.push({
|
|
|
- "type": "gridVideoRenderer",
|
|
|
- "videoId": recommendVideo["videoId"],
|
|
|
- "title": recommendVideo["title"]["simpleText"],
|
|
|
- "thumbnails": recommendVideo["thumbnail"]["thumbnails"],
|
|
|
- "channelName": recommendVideo["longBylineText"]["runs"][0]["text"],
|
|
|
- "publishedTimeText": recommendVideo["publishedTimeText"]["simpleText"],
|
|
|
- "viewCountText": recommendVideo["viewCountText"]["simpleText"],
|
|
|
- "shortViewCountText": recommendVideo["shortViewCountText"]["simpleText"],
|
|
|
- "lengthText": recommendVideo["lengthText"]["simpleText"]
|
|
|
- })
|
|
|
+ console.log(recommendVideo);
|
|
|
+ if (recommendVideo["videoId"]) {
|
|
|
+ recommendInfo.push({
|
|
|
+ "type": "gridVideoRenderer",
|
|
|
+ "videoId": recommendVideo["videoId"],
|
|
|
+ "title": recommendVideo["title"]?.["simpleText"],
|
|
|
+ "thumbnails": recommendVideo["thumbnail"]?.["thumbnails"],
|
|
|
+ "channelName": recommendVideo["longBylineText"]?.["runs"]?.[0]?.["text"],
|
|
|
+ "publishedTimeText": recommendVideo["publishedTimeText"]?.["simpleText"],
|
|
|
+ "viewCountText": recommendVideo["viewCountText"]?.["simpleText"],
|
|
|
+ "shortViewCountText": recommendVideo["shortViewCountText"]?.["simpleText"],
|
|
|
+ "lengthText": recommendVideo["lengthText"]?.["simpleText"]
|
|
|
+ })
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|