From d9d725d776c83fb7449e5469161c7d36e43a0e6b Mon Sep 17 00:00:00 2001 From: Dung Nguyen Dang Date: Thu, 28 Jul 2022 12:34:54 +0700 Subject: [PATCH] Support music video. Fix #14 --- package.json | 2 +- src/index.js | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++-- test/test.js | 2 +- 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 14eea83..0dcec68 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "youtube-stream-url", - "version": "2.1.0", + "version": "2.2.0", "description": "Get stream url from youtube link", "main": "src/index.js", "scripts": { diff --git a/src/index.js b/src/index.js index c60e6e9..68ffecc 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,63 @@ const resolvePlayerResponse = (watchHtml) => { return matches ? matches[1] + '}}}' : '' } +const getJSFile = async(url) => { + try { + let { data } = await axios.get(url); + return data; + } catch (e) { + return null; + } +} + +const buildDecoder = async(watchHtml) => { + if (!watchHtml) { + return null; + } + + let jsFileUrlMatches = watchHtml.match(/\/s\/player\/[A-Za-z0-9]+\/[A-Za-z0-9_.]+\/[A-Za-z0-9_]+\/base\.js/); + + if (!jsFileUrlMatches) { + return null; + } + + + let jsFileContent = await getJSFile(`https://www.youtube.com${jsFileUrlMatches[0]}`); + + let decodeFunctionMatches = jsFileContent.match(/function.*\.split\(\"\"\).*\.join\(\"\"\)}/); + + if (!decodeFunctionMatches) { + return null; + } + + let decodeFunction = decodeFunctionMatches[0]; + + let varNameMatches = decodeFunction.match(/\.split\(\"\"\);([a-zA-Z0-9]+)\./); + + if (!varNameMatches) { + return null; + } + + let varDeclaresMatches = jsFileContent.match(new RegExp(`(var ${varNameMatches[1]}={[\\s\\S]+}};)[a-zA-Z0-9]+\\.[a-zA-Z0-9]+\\.prototype`)); + + if (!varDeclaresMatches) { + return null; + } + + return function(signatureCipher) { + let params = new URLSearchParams(signatureCipher); + let { s: signature, sp: signatureParam, url } = Object.fromEntries(params); + let decodedSignature = eval(` + "use strict"; + ${varDeclaresMatches[1]} + (${decodeFunction})("${signature}") + `); + + return `${url}&${signatureParam}=${encodeURIComponent(decodedSignature)}`; + } + +} + const getInfo = async({ url }) => { let videoId = getVideoId({ url }); @@ -26,13 +83,36 @@ const getInfo = async({ url }) => { try { let ytInitialPlayerResponse = resolvePlayerResponse(response.data); let parsedResponse = JSON.parse(ytInitialPlayerResponse); - let streamingData = parsedResponse.streamingData || {} + let streamingData = parsedResponse.streamingData || {}; + + let formats = (streamingData.formats || []) + .concat(streamingData.adaptiveFormats || []); + + let isEncryptedVideo = !!formats.find(it => !!it.signatureCipher); + + if (isEncryptedVideo) { + let decoder = await buildDecoder(response.data); + + if (decoder) { + formats = formats.map(it => { + if (it.url || !it.signatureCipher) { + return it; + } + + it.url = decoder(it.signatureCipher); + delete it.signatureCipher; + return it; + }); + } + } return { videoDetails: parsedResponse.videoDetails || {}, - formats: (streamingData.formats || []).concat(streamingData.adaptiveFormats || []).filter(format => format.url) + formats: formats + .filter(format => format.url) } - } catch { + } catch (e) { + console.log(e); //Do nothing here return false } diff --git a/test/test.js b/test/test.js index 16be0fe..83c3d83 100644 --- a/test/test.js +++ b/test/test.js @@ -1,5 +1,5 @@ const Youtube = require('../src'); (async() => { - console.log(await Youtube.getInfo({ url: 'https://www.youtube.com/watch?v=pJ7WN3yome4' })) + console.log(await Youtube.getInfo({ url: 'https://www.youtube.com/watch?v=weRHyjj34ZE' })) })(); \ No newline at end of file