Skip to content

Instantly share code, notes, and snippets.

@lihaohong6
Created December 22, 2024 04:00
Show Gist options
  • Save lihaohong6/b7233bcd26629d54bfee10e2883e1047 to your computer and use it in GitHub Desktop.
Save lihaohong6/b7233bcd26629d54bfee10e2883e1047 to your computer and use it in GitHub Desktop.
千聊下载器(纯js)
/*
* 用法:(1)打开某个课程
* (2)点击课程的第二个视频(如果课程只有一个视频则无法自动下载)
* (3)把这段代码复制粘贴到浏览器控制台(F12),按回车。
* (4)如果一切正常,网页会跳转到第一个视频并开始下载。第一个视频下载完成后自动开始第二个视频的下载,如此重复直到所有视频下载完成。
* 注意:浏览器可能会询问用户是否允许该网站下载多个文件,遇到此情况请点击同意/允许。
* 协议:AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.en.html)
*/
(function() {
const all_classes = Array.from(document.querySelectorAll("p.title.elli-text"));
let cur = -1;
function download_next() {
cur += 1;
if (cur >= all_classes.length) {
return;
}
const cur_node = all_classes[cur];
cur_node.click();
}
function download_func(url, filename) {
console.log("Starting download of " + url + " into " + filename);
// 使用m3u8下载器下载
const m3u8 = new M3U8();
const download = m3u8.start(url, {"filename": filename});
download.on("finished", (f) => {
console.log(f);
download_next();
});
download.on("aborted", (f) => console.log(f));
download.on("error", (f) => console.log(f));
download.on("progress", (f) => console.log(f));
}
// 监听所有xhr请求,遇到带m3u8的url之后下载它
(function (send) {
XMLHttpRequest.prototype.send = function (data) {
const url = this.url.toString();
console.log("First encounter " + url);
if (url.search(".m3u8") !== -1) {
console.log("Initiate download");
const file_name = "文件" + (cur + 1) + ".mp4"
download_func(url, file_name);
}
send.call(this, data);
};
})(XMLHttpRequest.prototype.send);
download_next();
/*
Source: https://github.com/TheUndo/m3u8
License: MIT
*/
function M3U8() {
var _this = this; // access root scope
this.ie = navigator.appVersion.toString().indexOf(".NET") > 0;
this.ios = navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform);
this.start = function(m3u8, options) {
if (!options)
options = {};
var callbacks = {
progress: null,
finished: null,
error: null,
aborted: null
}
var recur; // the recursive download instance to later be initialized. Scoped here so callbakcs can access and manage it.
function handleCb(key, payload) {
if (key && callbacks[key])
callbacks[key](payload);
}
if (_this.ios)
return handleCb("error", "Downloading on IOS is not supported.");
var startObj = {
on: function(str, cb) {
switch (str) {
case "progress": {
callbacks.progress = cb;
break;
}
case "finished": {
callbacks.finished = cb;
break;
}
case "error": {
callbacks.error = cb;
break;
}
case "aborted": {
callbacks.aborted = cb;
break;
}
}
return startObj;
},
abort: function() {
;
recur && (recur.aborted = function() {
handleCb("aborted");
});
}
}
var download = new Promise(function(resolve, reject) {
var url = new URL(m3u8);
var req = fetch(m3u8)
.then(function(d) {
return d.text();
})
.then(function(d) {
var filtered = filter(d.split(/(\r\n|\r|\n)/gi), function(item) {
return item.indexOf(".ts") > -1; // select only ts files
});
var mapped = map(filtered, function(v, i) {
if (v.indexOf("http") === 0 || v.indexOf("ftp") === 0) { // absolute url
return v;
}
return url.protocol + "//" + url.host + url.pathname + "/./../" + v; // map ts files into url
});
if (!mapped.length) {
reject("Invalid m3u8 playlist");
return handleCb("error", "Invalid m3u8 playlist");
}
recur = new RecurseDownload(mapped, function(data) {
var blob = new Blob(data, {
type: "octet/stream"
});
handleCb("progress", {
status: "Processing..."
});
if (!options.returnBlob) {
if (_this.ios) {
// handle ios?
} else if (_this.ie) {
handleCb("progress", {
status: "Sending video to Internet Explorer... this may take a while depending on your device's performance."
});
window.navigator.msSaveBlob(blob, (options && options.filename) || "video.mp4");
} else {
handleCb("progress", {
status: "Sending video to browser..."
});
var a = document.createElementNS("http://www.w3.org/1999/xhtml", "a");
a.href = URL.createObjectURL(blob);
a.download = (options && options.filename) || "video.mp4";
a.style.display = "none";
document.body.appendChild(a); // Firefox fix
a.click();
handleCb("finished", {
status: "Successfully downloaded video",
data: blob
});
resolve(blob);
}
} else {
handleCb("finished", {
status: "Successfully downloaded video",
data: blob
});
resolve(blob)
}
}, 0, []);
recur.onprogress = function(obj) {
handleCb("progress", obj);
}
})
.catch(function(err) {
handleCb("error", "Something went wrong when downloading m3u8 playlist: " + err);
});
});
return startObj;
}
function RecurseDownload(arr, cb, i, data) { // recursively download asynchronously 2 at the time
var _this = this;
this.aborted = false;
recurseDownload(arr, cb, i, data);
function recurseDownload(arr, cb, i, data) {
var req = Promise.all([fetch(arr[i]), arr[i + 1] ? fetch(arr[i + 1]) : Promise.resolve()]) // HTTP protocol dictates only TWO requests can be simultaneously performed
.then(function(d) {
return map(filter(d, function(v) {
return v && v.blob;
}), function(v) {
return v.blob();
});
})
.then(function(d) {
return Promise.all(d);
})
.then(function(d) {
var blobs = map(d, function(v, j) {
return new Promise(function(resolve, reject) {
var reader = new FileReader();
var read = reader.readAsArrayBuffer(new Blob([v], {
type: "octet/stream"
})); // IE can't read Blob.arrayBuffer :(
reader.addEventListener("loadend", function(event) { // event listener, my old friend we meet again... I cenrtainly haven't missed you in place of promise
resolve(reader.result);;
(_this.onprogress && _this.onprogress({
segment: i + j + 1,
total: arr.length,
percentage: ((i + j + 1) / arr.length * 100).toFixed(3),
downloaded: formatNumber(+reduce(map(data, function(v) {
return v.byteLength;
}), function(t, c) {
return t + c;
}, 0)),
status: "Downloading..."
}));
});
});
});
Promise.all(blobs).then(function(d) {
for (var n = 0; n < d.length; n++) { // polymorphism
data.push(d[n]);
}
var increment = arr[i + 2] ? 2 : 1; // look ahead to see if we can perform 2 requests at the same time again
if (_this.aborted) {
data = null; // purge data... client side calling of garbage collector isn't possible. I know about opera and ie's garbage collectors but they're not ideal.
_this.aborted();
return; // exit promise
} else if (arr[i + increment]) {
setTimeout(function() {
recurseDownload(arr, cb, i + increment, data);
}, _this.ie ? 500 : 0);
} else {
cb(data);
}
});
})
.catch(function(err) {
;
_this.onerror && _this.onerror("Something went wrong when downloading ts file, nr. " + i + ": " + err);
});
}
}
function filter(arr, condition) {
var result = [];
for (var i = 0; i < arr.length; i++) {
if (condition(arr[i], i)) {
result.push(arr[i]);
}
}
return result;
}
function map(arr, condition) {
var result = arr.slice(0);
for (var i = 0; i < arr.length; i++) {
result[i] = condition(arr[i], i);
}
return result;
}
function reduce(arr, condition, start) {
var result = start;
arr.forEach(function(v, i) {
var res = +condition(result, v, i);
result = res;
});
return result;
}
function formatNumber(n) {
var ranges = [{
divider: 1e18,
suffix: "EB"
},
{
divider: 1e15,
suffix: "PB"
},
{
divider: 1e12,
suffix: "TB"
},
{
divider: 1e9,
suffix: "GB"
},
{
divider: 1e6,
suffix: "MB"
},
{
divider: 1e3,
suffix: "kB"
}
]
for (var i = 0; i < ranges.length; i++) {
if (n >= ranges[i].divider) {
var res = (n / ranges[i].divider).toString()
return res.toString().split(".")[0] + ranges[i].suffix;
}
}
return n.toString();
}
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment