Skip to content

Commit 998161c

Browse files
committed
youtube local download - WIP
1 parent 0a2b1ce commit 998161c

File tree

4 files changed

+169
-2
lines changed

4 files changed

+169
-2
lines changed

popup/tabs.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ const tabs = [
181181
{
182182
...CATEGORY.youtube,
183183
scripts: [
184+
s.youtube_localDownloader,
184185
s.youtube_downloadVideo,
185186
s.pictureInPicture,
186187
s.youtube_toggleLight,

scripts/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ import vuiz_createLogo from "./vuiz_createLogo.js";
183183
import vuiz_getLink from "./vuiz_getLink.js";
184184
import ggdrive_downloadPdf from "./ggdrive_downloadPdf.js";
185185
import ggdrive_downloadPresentation from "./ggdrive_downloadPresentation.js";
186+
import youtube_localDownloader from "./youtube_localDownloader.js";
186187

187188
// inject badges
188189
const allScripts = {
@@ -390,6 +391,7 @@ const allScripts = {
390391
ggdrive_downloadPresentation,
391392
BADGES.new
392393
),
394+
youtube_localDownloader: addBadge(youtube_localDownloader, BADGES.new),
393395
};
394396

395397
// alert(Object.keys(allScripts).length);

scripts/youtube_localDownloader.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
export default {
2+
icon: "https://www.youtube.com/s/desktop/accca349/img/favicon_48x48.png",
3+
name: {
4+
en: "Youtube local downloader",
5+
vi: "Youtube tải video",
6+
},
7+
description: {
8+
en: "",
9+
vi: "",
10+
},
11+
12+
whiteList: ["https://*.youtube.com/*"],
13+
14+
onDocumentStart: () => {
15+
const app = {};
16+
17+
const $ = (s, x = document) => x.querySelector(s);
18+
const $el = (tag, opts) => {
19+
const el = document.createElement(tag);
20+
Object.assign(el, opts);
21+
return el;
22+
};
23+
24+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25+
const parseDecsig = (data) => {
26+
try {
27+
if (data.startsWith("var script")) {
28+
// they inject the script via script tag
29+
const obj = {};
30+
const document = {
31+
createElement: () => obj,
32+
head: { appendChild: () => {} },
33+
};
34+
eval(data);
35+
data = obj.innerHTML;
36+
}
37+
const fnnameresult = /=([a-zA-Z0-9\$_]+?)\(decodeURIComponent/.exec(
38+
data
39+
);
40+
const fnname = fnnameresult[1];
41+
const _argnamefnbodyresult = new RegExp(
42+
escapeRegExp(fnname) + "=function\\((.+?)\\){((.+)=\\2.+?)}"
43+
).exec(data);
44+
const [_, argname, fnbody] = _argnamefnbodyresult;
45+
const helpernameresult = /;([a-zA-Z0-9$_]+?)\..+?\(/.exec(fnbody);
46+
const helpername = helpernameresult[1];
47+
const helperresult = new RegExp(
48+
"var " + escapeRegExp(helpername) + "={[\\s\\S]+?};"
49+
).exec(data);
50+
const helper = helperresult[0];
51+
console.log(
52+
`parsedecsig result: %s=>{%s\n%s}`,
53+
argname,
54+
helper,
55+
fnbody
56+
);
57+
return new Function([argname], helper + "\n" + fnbody);
58+
} catch (e) {
59+
console.error("parsedecsig error: %o", e);
60+
console.info("script content: %s", data);
61+
console.info(
62+
'If you encounter this error, please copy the full "script content" to https://pastebin.com/ for me.'
63+
);
64+
}
65+
};
66+
const parseQuery = (s) =>
67+
[...new URLSearchParams(s).entries()].reduce(
68+
(acc, [k, v]) => ((acc[k] = v), acc),
69+
{}
70+
);
71+
const parseResponse = (id, playerResponse, decsig) => {
72+
console.log(`video %s playerResponse: %o`, id, playerResponse);
73+
let stream = [];
74+
if (playerResponse.streamingData.formats) {
75+
stream = playerResponse.streamingData.formats.map((x) =>
76+
Object.assign({}, x, parseQuery(x.cipher || x.signatureCipher))
77+
);
78+
console.log(`video %s stream: %o`, id, stream);
79+
for (const obj of stream) {
80+
if (obj.s) {
81+
obj.s = decsig(obj.s);
82+
obj.url += `&${obj.sp}=${encodeURIComponent(obj.s)}`;
83+
}
84+
}
85+
}
86+
87+
let adaptive = [];
88+
if (playerResponse.streamingData.adaptiveFormats) {
89+
adaptive = playerResponse.streamingData.adaptiveFormats.map((x) =>
90+
Object.assign({}, x, parseQuery(x.cipher || x.signatureCipher))
91+
);
92+
console.log(`video %s adaptive: %o`, id, adaptive);
93+
for (const obj of adaptive) {
94+
if (obj.s) {
95+
obj.s = decsig(obj.s);
96+
obj.url += `&${obj.sp}=${encodeURIComponent(obj.s)}`;
97+
}
98+
}
99+
}
100+
console.log(`video %s result: %o`, id, { stream, adaptive });
101+
return {
102+
stream,
103+
adaptive,
104+
details: playerResponse.videoDetails,
105+
playerResponse,
106+
};
107+
};
108+
109+
const load = async (playerResponse) => {
110+
try {
111+
const basejs =
112+
(typeof ytplayer !== "undefined" &&
113+
"config" in ytplayer &&
114+
ytplayer.config.assets
115+
? "https://" + location.host + ytplayer.config.assets.js
116+
: "web_player_context_config" in ytplayer
117+
? "https://" +
118+
location.host +
119+
ytplayer.web_player_context_config.jsUrl
120+
: null) || $('script[src$="base.js"]').src;
121+
debugger;
122+
const res = await fetch(basejs);
123+
const text = await res.text();
124+
const decsig = parseDecsig(text);
125+
const id = parseQuery(location.search).v;
126+
const data = parseResponse(id, playerResponse, decsig);
127+
console.log("video loaded: %s", id);
128+
app.isLiveStream =
129+
data.playerResponse.playabilityStatus.liveStreamability != null;
130+
app.id = id;
131+
app.stream = data.stream;
132+
app.adaptive = data.adaptive;
133+
app.details = data.details;
134+
} catch (err) {
135+
alert(app.strings.get_video_failed);
136+
console.error("load", err);
137+
}
138+
};
139+
140+
// hook fetch response
141+
const ff = fetch;
142+
window.fetch = (...args) => {
143+
if (args[0] instanceof Request) {
144+
return ff(...args).then((resp) => {
145+
if (resp.url.includes("player")) {
146+
resp.clone().json().then(load);
147+
}
148+
return resp;
149+
});
150+
}
151+
return ff(...args);
152+
};
153+
154+
window.addEventListener("load", () => {
155+
const firstResp = window?.ytplayer?.config?.args?.raw_player_response;
156+
if (firstResp) {
157+
load(firstResp);
158+
}
159+
});
160+
},
161+
};
162+
163+
// functions/attributes that other scripts can import and use
164+
export const shared = {};

scripts/youtube_nonstop.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export default {
2-
icon: "https://lh3.googleusercontent.com/OS9P4SJOFAg8lhCyaRTJ7y4ADF0TGpqFF904BcpCtdBjJIDBbNb_J8PpgoJ9QvariiG_RzgH8fCSSY_kQu-chQQ0Aw=w128-h128-e365-rj-sc0x00ffffff",
2+
icon: "https://www.youtube.com/s/desktop/accca349/img/favicon_48x48.png",
33
name: {
44
en: "Youtube nonstop",
55
vi: "Youtube nonstop",
@@ -8,7 +8,7 @@ export default {
88
en: 'Kiss the annoying "Video paused. Continue watching?" confirmation goodbye!',
99
vi: "Phát youtube không còn bị làm phiền bởi popup 'Video đã tạm dừng. Bạn có muốn xem tiếp?' của youtube.",
1010
},
11-
whiteList: ["*://music.youtube.com/*", "*://www.youtube.com/*"],
11+
whiteList: ["*://music.youtube.com/*", "*://www.youtube.com/*"],
1212

1313
onClick: function () {
1414
// source code from: https://chrome.google.com/webstore/detail/youtube-nonstop/nlkaejimjacpillmajjnopmpbkbnocid

0 commit comments

Comments
 (0)