diff --git a/README.md b/README.md index 5a7d6cc..6ff85e4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # M3U8-Downloader HLS Downloader,一个Electron App,可以下载、播放HLS视频流。 -![HLSDownload Show](https://github.com/HeiSir2014/M3U8-Downloader/blob/master/resource/HLSDownloadShow.gif?raw=true) +![HLSDownload Show](https://github.com/HeiSir2014/M3U8-Downloader/blob/master/resource/HLSDownloadShow-2.gif?raw=true) # 官网 [M3U8-Downloader 官网](https://tools.heisir.cn/HLSDownload) @@ -18,3 +18,31 @@ QQ交流群:341972319 目前仅编译了Windows Release. 下载地址:[Release](https://github.com/HeiSir2014/M3U8-Downloader/releases) + +# 运行源码 +### 1.NodeJS开发环境搭建 + +安装NodeJs最新版,[NodeJs Download](http://nodejs.cn/download/) + +### 2.Clone 代码 + +在任意文件夹下新建一个文件夹存放代码,并执行以下命令 +``` +cd newdir + +git clone https://github.com/HeiSir2014/M3U8-Downloader.git . +``` + +### 3.环境初始化 + +``` +npm install +``` + +### 4.运行M3U8-Downloader + +``` +npm run start2 +``` + +### 5.Enjoy it \ No newline at end of file diff --git a/css/style.css b/css/style.css index 71c1e1b..f0f38a3 100644 --- a/css/style.css +++ b/css/style.css @@ -147,7 +147,8 @@ html{ .heisir .main .TaskList{ width: 85%; height: calc(100% - 160px); - margin: 40px auto; + margin: 15px auto; + padding: 10px; overflow-y: auto; padding: 0px; border-radius: 28px; diff --git a/ffmpegTest.js b/ffmpegTest.js new file mode 100644 index 0000000..c15d503 --- /dev/null +++ b/ffmpegTest.js @@ -0,0 +1,40 @@ +"use strict"; +const ffmpeg = require('fluent-ffmpeg'); +const fs = require('fs'); +const { Readable} = require('stream'); +const path = require('path'); + + +const dir = 'E:\\Project\\my_project\\M3U8-Downloader\\source\\download\\1592447619950\\'; +let inputStream = new Readable(); +let _ffmpeg = ffmpeg(inputStream) +.setFfmpegPath('E:\\Project\\my_project\\M3U8-Downloader\\source\\ffmpeg.exe') +.videoCodec('copy') +.audioCodec('copy') +.save("E:\\Project\\my_project\\M3U8-Downloader\\source\\download\\1592447619950\\output.mp4") +.on('progress', function(info) { + console.log(info); +}) +.on('end', function() { + console.log('done processing input stream'); +}) +.on('error', function(err) { + console.log('an error happened: ' + err.message); +}); + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +fs.readdir(dir,function(err,files){ + for (let index = 0; index < files.length; index++) { + const file = files[index]; + if(file.endsWith(".ts")) + { + inputStream.push( fs.readFileSync(path.join(dir,file)) ); + //console.log(`file : ${index}`); + } + } + inputStream.push(null); +}); + diff --git a/index.html b/index.html index 1d429f7..afe5e12 100644 --- a/index.html +++ b/index.html @@ -8,7 +8,7 @@ -
M3U8-Downloader v1.0.4 +
M3U8-Downloader v1.0.5
官网 点击加群 @@ -35,6 +35,7 @@
+
diff --git a/main.js b/main.js index 9274c43..0f405fe 100644 --- a/main.js +++ b/main.js @@ -1,4 +1,17 @@ "use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } @@ -39,12 +52,14 @@ var _a = require('electron'), app = _a.app, BrowserWindow = _a.BrowserWindow, Tr var spawn = require('child_process').spawn; var path = require('path'); var Parser = require('m3u8-parser').Parser; -var HTTPPlus = require('./HTTPPlus'); var fs = require('fs'); var async = require('async'); var dateFormat = require('dateformat'); var download = require('download'); var crypto = require('crypto'); +var got = require('got'); +var Readable = require('stream').Readable; +var ffmpeg = require('fluent-ffmpeg'); var isdelts = true; var mainWindow = null; var playerWindow = null; @@ -52,6 +67,7 @@ var tray = null; var AppTitle = 'HLS Downloader'; var firstHide = true; var configVideos = []; +var globalCond = {}; function createWindow() { // 创建浏览器窗口 mainWindow = new BrowserWindow({ @@ -171,36 +187,48 @@ ipcMain.on('get-all-videos', function (event, arg) { event.sender.send('get-all-videos-reply', configVideos); }); ipcMain.on('task-add', function (event, arg) { - console.log(arg); - var hlsSrc = arg; - HTTPPlus.httpSend(hlsSrc, 'GET', null, null, function (data, error) { - var info = ''; - var code = 0; - code = -1; - info = '解析资源失败!'; - if (data != null - && data != '') { - var parser = new Parser(); - parser.push(data); - parser.end(); - var count_seg = parser.manifest.segments.length; - if (count_seg > 0) { - code = 0; - if (parser.manifest.endList) { - var duration_1 = 0; - parser.manifest.segments.forEach(function (segment) { - duration_1 += segment.duration; - }); - info = "\u70B9\u64AD\u8D44\u6E90\u89E3\u6790\u6210\u529F\uFF0C\u6709 " + count_seg + " \u4E2A\u7247\u6BB5\uFF0C\u65F6\u957F\uFF1A" + formatTime(duration_1) + "\uFF0C\u5373\u5C06\u5F00\u59CB\u7F13\u5B58..."; - } - else { - info = "\u76F4\u64AD\u8D44\u6E90\u89E3\u6790\u6210\u529F\uFF0C\u5373\u5C06\u5F00\u59CB\u7F13\u5B58..."; - } - startDownload(hlsSrc, parser); + return __awaiter(this, void 0, void 0, function () { + var hlsSrc, response, info, code, parser, count_seg, duration_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + console.log(arg); + hlsSrc = arg; + return [4 /*yield*/, got(hlsSrc)["catch"](console.log)]; + case 1: + response = _a.sent(); + { + info = ''; + code = 0; + code = -1; + info = '解析资源失败!'; + if (response && response.body != null + && response.body != '') { + parser = new Parser(); + parser.push(response.body); + parser.end(); + count_seg = parser.manifest.segments.length; + if (count_seg > 0) { + code = 0; + if (parser.manifest.endList) { + duration_1 = 0; + parser.manifest.segments.forEach(function (segment) { + duration_1 += segment.duration; + }); + info = "\u70B9\u64AD\u8D44\u6E90\u89E3\u6790\u6210\u529F\uFF0C\u6709 " + count_seg + " \u4E2A\u7247\u6BB5\uFF0C\u65F6\u957F\uFF1A" + formatTime(duration_1) + "\uFF0C\u5373\u5C06\u5F00\u59CB\u7F13\u5B58..."; + startDownload(hlsSrc); + } + else { + info = "\u76F4\u64AD\u8D44\u6E90\u89E3\u6790\u6210\u529F\uFF0C\u5373\u5C06\u5F00\u59CB\u7F13\u5B58..."; + startDownloadLive(hlsSrc); + } + } + } + event.sender.send('task-add-reply', { code: code, message: info }); + } + return [2 /*return*/]; } - } - event.sender.send('task-add-reply', { code: code, message: info }); - //fs.writeFileSync('out.json', JSON.stringify(parser)); + }); }); }); var QueueObject = /** @class */ (function () { @@ -213,6 +241,10 @@ var QueueObject = /** @class */ (function () { return __generator(this, function (_a) { switch (_a.label) { case 0: + if (!globalCond[this.id]) { + _callback(); + return [2 /*return*/]; + } partent_uri = this.url.replace(/([^\/]*\?.*$)|([^\/]*$)/g, ''); segment = this.segment; uri_ts = ''; @@ -317,92 +349,362 @@ var QueueObject = /** @class */ (function () { function queue_callback(that, callback) { that.callback(callback); } -function startDownload(url, parser) { - var id = new Date().getTime(); - var dir = path.join(app.getAppPath().replace(/resources\\app.asar$/g, ""), 'download/' + id); - console.log(dir); - var filesegments = []; - fs.mkdirSync(dir, { recursive: true }); - //并发 6 个线程下载 - var tsQueues = async.queue(queue_callback, 6); - var count_seg = parser.manifest.segments.length; - var count_downloaded = 0; - var video = { - id: id, - url: url, - dir: dir, - segment_total: count_seg, - segment_downloaded: count_downloaded, - time: dateFormat(new Date(), "yyyy-mm-dd HH:MM:ss"), - status: '初始化...', - videopath: '' - }; - mainWindow.webContents.send('task-notify-create', video); - var segments = parser.manifest.segments; - for (var iSeg = 0; iSeg < segments.length; iSeg++) { - var qo = new QueueObject(); - qo.dir = dir; - qo.idx = iSeg; - qo.url = url; - qo.segment = segments[iSeg]; - qo.then = function () { - count_downloaded = count_downloaded + 1; - video.segment_downloaded = count_downloaded; - video.status = "\u4E0B\u8F7D\u4E2D..." + count_downloaded + "/" + count_seg; - mainWindow.webContents.send('task-notify-update', video); - }; - tsQueues.push(qo); - } - tsQueues.drain(function () { - console.log('download success'); - video.status = "已完成,合并中..."; - mainWindow.webContents.send('task-notify-end', video); - var indexData = ''; - for (var iSeg = 0; iSeg < segments.length; iSeg++) { - var filpath = path.join(dir, ((iSeg + 1) + '').padStart(6, '0') + ".ts"); - indexData += "file '" + filpath + "'\r\n"; - filesegments.push(filpath); - } - fs.writeFileSync(path.join(dir, 'index.txt'), indexData); - var outPathMP4 = path.join(dir, id + '.mp4'); - var ffmpegBin = path.join(app.getAppPath().replace(/resources\\app.asar$/g, ""), "ffmpeg.exe"); - if (!fs.existsSync(ffmpegBin)) { - ffmpegBin = path.join(app.getAppPath().replace(/resources\\app.asar$/g, ""), "ffmpeg"); - } - if (fs.existsSync(ffmpegBin)) { - var p = spawn(ffmpegBin, ["-f", "concat", "-safe", "0", "-i", "" + path.join(dir, 'index.txt'), "-c", "copy", "-f", "mp4", "" + outPathMP4]); - p.on("close", function () { - if (fs.existsSync(outPathMP4)) { - video.videopath = outPathMP4; - video.status = "已完成"; - mainWindow.webContents.send('task-notify-end', video); - if (isdelts) { - var index_path = path.join(dir, 'index.txt'); - if (fs.existsSync(index_path)) { - fs.unlinkSync(index_path); +function startDownload(url, nId) { + if (nId === void 0) { nId = null; } + return __awaiter(this, void 0, void 0, function () { + var id, dir, filesegments, response, parser, tsQueues, count_seg, count_downloaded, video, segments, iSeg, qo; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + id = nId == null ? new Date().getTime() : nId; + dir = path.join(app.getAppPath().replace(/resources\\app.asar$/g, ""), 'download/' + id); + console.log(dir); + filesegments = []; + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + return [4 /*yield*/, got(url)["catch"](console.log)]; + case 1: + response = _a.sent(); + if (response == null || response.body == null || response.body == '') { + return [2 /*return*/]; + } + parser = new Parser(); + parser.push(response.body); + parser.end(); + tsQueues = async.queue(queue_callback, 6); + count_seg = parser.manifest.segments.length; + count_downloaded = 0; + video = { + id: id, + url: url, + dir: dir, + segment_total: count_seg, + segment_downloaded: count_downloaded, + time: dateFormat(new Date(), "yyyy-mm-dd HH:MM:ss"), + status: '初始化...', + isLiving: false, + videopath: '' + }; + if (nId == null) { + mainWindow.webContents.send('task-notify-create', video); + } + segments = parser.manifest.segments; + for (iSeg = 0; iSeg < segments.length; iSeg++) { + qo = new QueueObject(); + qo.dir = dir; + qo.idx = iSeg; + qo.id = id; + qo.url = url; + qo.segment = segments[iSeg]; + qo.then = function () { + count_downloaded = count_downloaded + 1; + video.segment_downloaded = count_downloaded; + video.status = "\u4E0B\u8F7D\u4E2D..." + count_downloaded + "/" + count_seg; + mainWindow.webContents.send('task-notify-update', video); + }; + tsQueues.push(qo); + } + tsQueues.drain(function () { + console.log('download success'); + video.status = "已完成,合并中..."; + mainWindow.webContents.send('task-notify-end', video); + var indexData = ''; + for (var iSeg = 0; iSeg < segments.length; iSeg++) { + var filpath = path.join(dir, ((iSeg + 1) + '').padStart(6, '0') + ".ts"); + indexData += "file '" + filpath + "'\r\n"; + filesegments.push(filpath); + } + fs.writeFileSync(path.join(dir, 'index.txt'), indexData); + var outPathMP4 = path.join(dir, id + '.mp4'); + var ffmpegBin = path.join(app.getAppPath().replace(/resources\\app.asar$/g, ""), "ffmpeg.exe"); + if (!fs.existsSync(ffmpegBin)) { + ffmpegBin = path.join(app.getAppPath().replace(/resources\\app.asar$/g, ""), "ffmpeg"); } - filesegments.forEach(function (fileseg) { - if (fs.existsSync(fileseg)) { - fs.unlinkSync(fileseg); + if (fs.existsSync(ffmpegBin)) { + var p = spawn(ffmpegBin, ["-f", "concat", "-safe", "0", "-i", "" + path.join(dir, 'index.txt'), "-c", "copy", "-f", "mp4", "" + outPathMP4]); + p.on("close", function () { + if (fs.existsSync(outPathMP4)) { + video.videopath = outPathMP4; + video.status = "已完成"; + mainWindow.webContents.send('task-notify-end', video); + if (isdelts) { + var index_path = path.join(dir, 'index.txt'); + if (fs.existsSync(index_path)) { + fs.unlinkSync(index_path); + } + filesegments.forEach(function (fileseg) { + if (fs.existsSync(fileseg)) { + fs.unlinkSync(fileseg); + } + }); + } + } + else { + video.videopath = outPathMP4; + video.status = "合成失败,可能是非标准加密视频源,暂不支持。"; + mainWindow.webContents.send('task-notify-end', video); + } + configVideos.push(video); + fs.writeFileSync("config.data", JSON.stringify(configVideos)); + }); + p.on("data", console.log); + } + else { + video.videopath = outPathMP4; + video.status = "已完成,未发现本地FFMPEG,不进行合成。"; + mainWindow.webContents.send('task-notify-end', video); + } + }); + console.log("drain over"); + return [2 /*return*/]; + } + }); + }); +} +function sleep(ms) { + return new Promise(function (resolve) { return setTimeout(resolve, ms); }); +} +var FFmpegStreamReadable = /** @class */ (function (_super) { + __extends(FFmpegStreamReadable, _super); + function FFmpegStreamReadable(opt) { + return _super.call(this, opt) || this; + } + FFmpegStreamReadable.prototype._read = function () { }; + return FFmpegStreamReadable; +}(Readable)); +function startDownloadLive(url, nId) { + if (nId === void 0) { nId = null; } + return __awaiter(this, void 0, void 0, function () { + var id, dir, count_downloaded, count_seg, video, partent_uri, segmentSet, ffmpegInputStream, ffmpegObj, response, parser, count_seg_1, segments, find, _startTime, _videoDuration, _loop_1, iSeg, state_1, _downloadTime, error_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + id = nId == null ? new Date().getTime() : nId; + dir = path.join(app.getAppPath().replace(/resources\\app.asar$/g, ""), 'download/' + id); + console.log(dir); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + count_downloaded = 0; + count_seg = 100; + video = { + id: id, + url: url, + dir: dir, + segment_total: count_seg, + segment_downloaded: count_downloaded, + time: dateFormat(new Date(), "yyyy-mm-dd HH:MM:ss"), + status: '初始化...', + isLiving: true, + videopath: '' + }; + configVideos.push(video); + fs.writeFileSync("config.data", JSON.stringify(configVideos)); + if (nId == null) { + mainWindow.webContents.send('task-notify-create', video); + } + partent_uri = url.replace(/([^\/]*\?.*$)|([^\/]*$)/g, ''); + segmentSet = new Set(); + ffmpegInputStream = null; + ffmpegObj = null; + globalCond[id] = true; + _a.label = 1; + case 1: + if (!globalCond[id]) return [3 /*break*/, 14]; + _a.label = 2; + case 2: + _a.trys.push([2, 12, , 13]); + return [4 /*yield*/, got(url)["catch"](console.log)]; + case 3: + response = _a.sent(); + if (response == null || response.body == null || response.body == '') { + return [3 /*break*/, 14]; + } + parser = new Parser(); + parser.push(response.body); + parser.end(); + count_seg_1 = parser.manifest.segments.length; + segments = parser.manifest.segments; + console.log("\u89E3\u6790\u5230 " + count_seg_1 + " \u7247\u6BB5"); + if (!(count_seg_1 > 0)) return [3 /*break*/, 10]; + find = false; + _startTime = new Date(); + _videoDuration = 0; + _loop_1 = function (iSeg) { + var segment, uri_ts, mes, filename, filpath, filpath_dl, _loop_2, index, state_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + segment = segments[iSeg]; + if (find == false && segmentSet.has(segment.uri)) { + return [2 /*return*/, "continue"]; + } + else if (find == false) { + find = true; + } + if (!globalCond[id]) { + return [2 /*return*/, "break"]; + } + segmentSet.add(segment.uri); + _videoDuration = _videoDuration + segment.duration * 1000; + uri_ts = ''; + if (/^http.*/.test(segment.uri)) { + uri_ts = segment.uri; + } + else if (/^\/.*/.test(segment.uri)) { + mes = url.match(/^https?:\/\/[^/]*/); + if (mes && mes.length >= 1) { + uri_ts = mes[0] + segment.uri; + } + else { + uri_ts = partent_uri + segment.uri; + } + } + else { + uri_ts = partent_uri + segment.uri; + } + filename = ((count_downloaded + 1) + '').padStart(6, '0') + ".ts"; + filpath = path.join(dir, filename); + filpath_dl = path.join(dir, filename + ".dl"); + _loop_2 = function (index) { + var stat, outPathMP4_1, newid, ffmpegBin; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!globalCond[id]) { + return [2 /*return*/, "break"]; + } + return [4 /*yield*/, download(uri_ts, dir, { filename: filename + ".dl", timeout: 30000 })["catch"](function (err) { + console.log(err); + if (fs.existsSync(filpath_dl)) { + fs.unlinkSync(filpath_dl); + } + })]; + case 1: + _a.sent(); + if (fs.existsSync(filpath_dl)) { + stat = fs.statSync(filpath_dl); + if (stat.size > 0) { + fs.renameSync(filpath_dl, filpath); + } + else { + fs.unlinkSync(filpath_dl); + } + } + if (fs.existsSync(filpath)) { + if (ffmpegObj == null) { + outPathMP4_1 = path.join(dir, id + '.mp4'); + newid = id; + //不要覆盖之前下载的直播内容 + while (fs.existsSync(outPathMP4_1)) { + outPathMP4_1 = path.join(dir, newid + '.mp4'); + newid = newid + 1; + } + ffmpegBin = path.join(app.getAppPath().replace(/resources\\app.asar$/g, ""), "ffmpeg.exe"); + if (!fs.existsSync(ffmpegBin)) { + ffmpegBin = path.join(app.getAppPath().replace(/resources\\app.asar$/g, ""), "ffmpeg"); + } + if (fs.existsSync(ffmpegBin)) { + ffmpegInputStream = new FFmpegStreamReadable(null); + ffmpegObj = new ffmpeg(ffmpegInputStream) + .setFfmpegPath(ffmpegBin) + .videoCodec('copy') + .audioCodec('copy') + .save(outPathMP4_1) + .on('error', console.log) + .on('end', function () { + video.videopath = outPathMP4_1; + video.status = "已完成"; + mainWindow.webContents.send('task-notify-end', video); + fs.writeFileSync("config.data", JSON.stringify(configVideos)); + }) + .on('progress', console.log); + } + else { + video.videopath = outPathMP4_1; + video.status = "已完成,未发现本地FFMPEG,不进行合成。"; + mainWindow.webContents.send('task-notify-update', video); + } + } + if (ffmpegInputStream) { + ffmpegInputStream.push(fs.readFileSync(filpath)); + fs.unlinkSync(filpath); + } + //fs.appendFileSync(path.join(dir,'index.txt'),`file '${filpath}'\r\n`); + count_downloaded = count_downloaded + 1; + video.segment_downloaded = count_downloaded; + video.status = "\u76F4\u64AD\u4E2D... [" + count_downloaded + "]"; + mainWindow.webContents.send('task-notify-update', video); + return [2 /*return*/, "break"]; + } + return [2 /*return*/]; + } + }); + }; + index = 0; + _a.label = 1; + case 1: + if (!(index < 3)) return [3 /*break*/, 4]; + return [5 /*yield**/, _loop_2(index)]; + case 2: + state_2 = _a.sent(); + if (state_2 === "break") + return [3 /*break*/, 4]; + _a.label = 3; + case 3: + index++; + return [3 /*break*/, 1]; + case 4: return [2 /*return*/]; } }); + }; + iSeg = 0; + _a.label = 4; + case 4: + if (!(iSeg < segments.length)) return [3 /*break*/, 7]; + return [5 /*yield**/, _loop_1(iSeg)]; + case 5: + state_1 = _a.sent(); + if (state_1 === "break") + return [3 /*break*/, 7]; + _a.label = 6; + case 6: + iSeg++; + return [3 /*break*/, 4]; + case 7: + if (!globalCond[id]) return [3 /*break*/, 9]; + //使下次下载M3U8时间提前1秒钟。 + _videoDuration = _videoDuration - 1000; + _downloadTime = (new Date().getTime() - _startTime.getTime()); + if (!(_downloadTime < _videoDuration)) return [3 /*break*/, 9]; + return [4 /*yield*/, sleep(_videoDuration - _downloadTime)]; + case 8: + _a.sent(); + _a.label = 9; + case 9: return [3 /*break*/, 11]; + case 10: return [3 /*break*/, 14]; + case 11: + parser = null; + return [3 /*break*/, 13]; + case 12: + error_1 = _a.sent(); + console.log(error_1.response.body); + return [3 /*break*/, 13]; + case 13: return [3 /*break*/, 1]; + case 14: + if (ffmpegInputStream) { + ffmpegInputStream.push(null); } - } - else { - video.videopath = outPathMP4; - video.status = "合成失败,可能是非标准加密视频源,暂不支持。"; - mainWindow.webContents.send('task-notify-end', video); - } - configVideos.push(video); - fs.writeFileSync("config.data", JSON.stringify(configVideos)); - }); - p.on("data", console.log); - } - else { - video.videopath = outPathMP4; - video.status = "已完成,未发现本地FFMPEG,不进行合成。"; - mainWindow.webContents.send('task-notify-end', video); - } + if (count_downloaded <= 0) { + video.videopath = ''; + video.status = "已完成,下载失败"; + mainWindow.webContents.send('task-notify-end', video); + return [2 /*return*/]; + } + return [2 /*return*/]; + } + }); }); } function formatTime(duration) { @@ -447,6 +749,27 @@ ipcMain.on('opendir', function (event, arg) { ipcMain.on('playvideo', function (event, arg) { createPlayerWindow(arg); }); +ipcMain.on('StartOrStop', function (event, arg) { + console.log(arg); + var id = Number.parseInt(arg); + if (globalCond[id] == null) { + console.log("不存在此任务"); + return; + } + globalCond[id] = !globalCond[id]; + if (globalCond[id] == true) { + configVideos.forEach(function (Element) { + if (Element.id == id) { + if (Element.isLiving == true) { + startDownloadLive(Element.url, id); + } + else { + startDownload(Element.url, id); + } + } + }); + } +}); ipcMain.on('setting_isdelts', function (event, arg) { isdelts = arg; }); diff --git a/main.ts b/main.ts index 5bba2f8..2bef9b5 100644 --- a/main.ts +++ b/main.ts @@ -1,17 +1,18 @@ import { resolve } from "dns"; import { rejects } from "assert"; -import { httpSend } from "./HTTPPlus"; const { app, BrowserWindow, Tray, ipcMain, shell,Menu } = require('electron'); const { spawn } = require('child_process'); const path = require('path'); const { Parser } = require('m3u8-parser'); -const HTTPPlus = require('./HTTPPlus'); const fs = require('fs'); var async = require('async'); const dateFormat = require('dateformat'); const download = require('download'); const crypto = require('crypto'); +const got = require('got'); +const { Readable} = require('stream'); +const ffmpeg = require('fluent-ffmpeg'); let isdelts = true; let mainWindow = null; @@ -21,6 +22,7 @@ let AppTitle = 'HLS Downloader' let firstHide = true; var configVideos = []; +let globalCond ={}; function createWindow() { // 创建浏览器窗口 @@ -156,19 +158,21 @@ ipcMain.on('get-all-videos', function (event, arg) { event.sender.send('get-all-videos-reply', configVideos); }); -ipcMain.on('task-add', function (event, arg:string) { +ipcMain.on('task-add', async function (event, arg:string) { console.log(arg); let hlsSrc = arg; - HTTPPlus.httpSend(hlsSrc, 'GET', null, null, function (data, error) { + const response = await got(hlsSrc).catch(console.log); + { let info = ''; let code = 0; code = -1; info = '解析资源失败!'; - if (data != null - && data != '') { + if (response && response.body != null + && response.body != '') + { let parser = new Parser(); - parser.push(data); + parser.push(response.body); parser.end(); let count_seg = parser.manifest.segments.length; if (count_seg > 0) { @@ -179,18 +183,16 @@ ipcMain.on('task-add', function (event, arg:string) { duration += segment.duration; }); info = `点播资源解析成功,有 ${count_seg} 个片段,时长:${formatTime(duration)},即将开始缓存...`; + startDownload(hlsSrc); } else { info = `直播资源解析成功,即将开始缓存...`; + startDownloadLive(hlsSrc); } - startDownload(hlsSrc, parser); } } event.sender.send('task-add-reply', { code: code, message: info }); - - //fs.writeFileSync('out.json', JSON.stringify(parser)); - }); - + } }); class QueueObject { @@ -199,11 +201,19 @@ class QueueObject { } public segment: any; public url: string; + public id: number; public idx: number; public dir: string; public then: Function; public catch: Function; public async callback( _callback: Function ) { + + if(!globalCond[this.id]) + { + _callback(); + return; + } + let partent_uri = this.url.replace(/([^\/]*\?.*$)|([^\/]*$)/g, ''); let segment = this.segment; let uri_ts = ''; @@ -314,13 +324,26 @@ function queue_callback(that:QueueObject,callback:Function) that.callback(callback); } -function startDownload(url:string, parser:any) { - let id = new Date().getTime(); +async function startDownload(url:string, nId:number = null) { + let id = nId == null ? new Date().getTime():nId; let dir = path.join(app.getAppPath().replace(/resources\\app.asar$/g,""), 'download/'+id); console.log(dir); let filesegments = []; - fs.mkdirSync(dir, { recursive: true }); + + if(!fs.existsSync(dir)) + { + fs.mkdirSync(dir, { recursive: true }); + } + + const response = await got(url).catch(console.log); + if(response == null || response.body == null || response.body == '') + { + return; + } + let parser = new Parser(); + parser.push(response.body); + parser.end(); //并发 6 个线程下载 var tsQueues = async.queue(queue_callback, 6 ); @@ -335,15 +358,20 @@ function startDownload(url:string, parser:any) { segment_downloaded:count_downloaded, time: dateFormat(new Date(),"yyyy-mm-dd HH:MM:ss"), status:'初始化...', + isLiving:false, videopath:'' }; - mainWindow.webContents.send('task-notify-create',video); + if(nId == null) + { + mainWindow.webContents.send('task-notify-create',video); + } let segments = parser.manifest.segments; for (let iSeg = 0; iSeg < segments.length; iSeg++) { let qo = new QueueObject(); qo.dir = dir; qo.idx = iSeg; + qo.id = id; qo.url = url; qo.segment = segments[iSeg]; qo.then = function(){ @@ -414,8 +442,236 @@ function startDownload(url:string, parser:any) { mainWindow.webContents.send('task-notify-end',video); } }); + console.log("drain over"); +} + +function sleep(ms:number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +class FFmpegStreamReadable extends Readable { + constructor(opt:any) { + super(opt); + } + _read() {} +} + +async function startDownloadLive(url:string,nId:number = null) { + let id = nId == null ? new Date().getTime() : nId; + + let dir = path.join(app.getAppPath().replace(/resources\\app.asar$/g,""), 'download/'+id); + console.log(dir); + if(!fs.existsSync(dir)) + { + fs.mkdirSync(dir, { recursive: true }); + } + + let count_downloaded = 0; + let count_seg = 100; + var video = { + id:id, + url:url, + dir:dir, + segment_total:count_seg, + segment_downloaded:count_downloaded, + time: dateFormat(new Date(),"yyyy-mm-dd HH:MM:ss"), + status:'初始化...', + isLiving:true, + videopath:'' + }; + + configVideos.push(video); + fs.writeFileSync("config.data",JSON.stringify(configVideos)); + + if(nId == null) + { + mainWindow.webContents.send('task-notify-create',video); + } + + let partent_uri = url.replace(/([^\/]*\?.*$)|([^\/]*$)/g, ''); + let segmentSet = new Set(); + let ffmpegInputStream = null; + let ffmpegObj = null; + globalCond[id] = true; + while (globalCond[id]) { + + try { + const response = await got(url).catch(console.log); + if(response == null || response.body == null || response.body == '') + { + break; + } + let parser = new Parser(); + parser.push(response.body); + parser.end(); + + let count_seg = parser.manifest.segments.length; + let segments = parser.manifest.segments; + console.log(`解析到 ${count_seg} 片段`) + if (count_seg > 0) { + let find = false; + //开始下载片段的时间,下载完毕后,需要计算下次请求的时间 + let _startTime = new Date(); + let _videoDuration = 0; + for (let iSeg = 0; iSeg < segments.length; iSeg++) { + let segment = segments[iSeg]; + if(find == false && segmentSet.has(segment.uri)) + { + continue; + } + else if(find == false) + { + find = true; + } + if(!globalCond[id]) + { + break; + } + segmentSet.add(segment.uri); + _videoDuration = _videoDuration + segment.duration*1000; + let uri_ts = ''; + if (/^http.*/.test(segment.uri)) { + uri_ts = segment.uri; + } + else if(/^\/.*/.test(segment.uri)) + { + let mes = url.match(/^https?:\/\/[^/]*/); + if(mes && mes.length >= 1) + { + uri_ts = mes[0] + segment.uri; + } + else + { + uri_ts = partent_uri + segment.uri; + } + } + else + { + uri_ts = partent_uri + segment.uri; + } + + let filename = `${ ((count_downloaded + 1) +'').padStart(6,'0') }.ts`; + let filpath = path.join(dir, filename); + let filpath_dl = path.join(dir, filename+".dl"); + + for (let index = 0; index < 3; index++) { + if(!globalCond[id]) + { + break; + } + await download (uri_ts, dir, { filename: filename + ".dl", timeout:30000 }).catch((err:any)=>{ + console.log(err); + if(fs.existsSync( filpath_dl )) + { + fs.unlinkSync( filpath_dl ); + } + }); + if( fs.existsSync(filpath_dl) ) + { + let stat = fs.statSync(filpath_dl); + if(stat.size > 0) + { + fs.renameSync(filpath_dl,filpath); + } + else + { + fs.unlinkSync( filpath_dl); + } + } + if( fs.existsSync(filpath) ) + { + if(ffmpegObj == null) + { + let outPathMP4 = path.join(dir,id+'.mp4'); + let newid = id; + //不要覆盖之前下载的直播内容 + while(fs.existsSync(outPathMP4)) + { + outPathMP4 = path.join(dir,newid+'.mp4'); + newid = newid + 1; + } + let ffmpegBin = path.join(app.getAppPath().replace(/resources\\app.asar$/g,""),"ffmpeg.exe"); + if(!fs.existsSync(ffmpegBin)) + { + ffmpegBin = path.join(app.getAppPath().replace(/resources\\app.asar$/g,""),"ffmpeg"); + } + if(fs.existsSync(ffmpegBin)) + { + ffmpegInputStream = new FFmpegStreamReadable(null); + + ffmpegObj = new ffmpeg(ffmpegInputStream) + .setFfmpegPath(ffmpegBin) + .videoCodec('copy') + .audioCodec('copy') + .save(outPathMP4) + .on('error', console.log) + .on('end', function(){ + video.videopath = outPathMP4; + video.status = "已完成"; + mainWindow.webContents.send('task-notify-end',video); + fs.writeFileSync("config.data",JSON.stringify(configVideos)); + }) + .on('progress', console.log); + } + else{ + video.videopath = outPathMP4; + video.status = "已完成,未发现本地FFMPEG,不进行合成。" + mainWindow.webContents.send('task-notify-update',video); + } + } + + if(ffmpegInputStream) + { + ffmpegInputStream.push(fs.readFileSync(filpath)); + fs.unlinkSync( filpath ); + } + + //fs.appendFileSync(path.join(dir,'index.txt'),`file '${filpath}'\r\n`); + count_downloaded = count_downloaded + 1; + video.segment_downloaded = count_downloaded; + video.status = `直播中... [${count_downloaded}]`; + mainWindow.webContents.send('task-notify-update',video); + break; + } + } + } + if(globalCond[id]) + { + //使下次下载M3U8时间提前1秒钟。 + _videoDuration = _videoDuration - 1000; + let _downloadTime = (new Date().getTime() - _startTime.getTime()); + if(_downloadTime < _videoDuration) + { + await sleep(_videoDuration - _downloadTime); + } + } + } + else + { + break; + } + parser = null; + } + catch (error) + { + console.log(error.response.body); + } + } + if(ffmpegInputStream) + { + ffmpegInputStream.push(null); + } + + if(count_downloaded <= 0) + { + video.videopath = ''; + video.status = "已完成,下载失败" + mainWindow.webContents.send('task-notify-end',video); + return; + } } + function formatTime(duration: number) { let sec = Math.floor(duration % 60).toLocaleString(); let min = Math.floor(duration / 60 % 60).toLocaleString(); @@ -460,6 +716,33 @@ ipcMain.on('opendir', function (event, arg:string) { ipcMain.on('playvideo', function (event, arg:string) { createPlayerWindow(arg); }); +ipcMain.on('StartOrStop', function (event, arg:string) { + console.log(arg); + + let id = Number.parseInt(arg); + if(globalCond[id] == null) + { + console.log("不存在此任务") + return; + } + globalCond[id] = !globalCond[ id]; + if(globalCond[ id] == true) + { + configVideos.forEach(Element=>{ + if(Element.id==id) + { + if(Element.isLiving == true) + { + startDownloadLive(Element.url, id); + } + else + { + startDownload(Element.url, id); + } + } + }); + } +}); ipcMain.on('setting_isdelts', function (event, arg:boolean) { isdelts = arg; diff --git a/package-lock.json b/package-lock.json index 3e2a0e7..59bd0ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "m3u8-downloader", - "version": "1.0.2", + "version": "1.0.4", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -99,9 +99,9 @@ } }, "@sindresorhus/is": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", - "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-2.1.1.tgz", + "integrity": "sha512-/aPsuoj/1Dw/kzhkgz+ES6TxG0zfTMGLwuK2ZG00k/iJzYHTLCE8mVU8EPqEOp/lmxPoq1C1C9RYToRKb2KEfg==" }, "@szmarczak/http-timer": { "version": "1.1.2", @@ -112,11 +112,42 @@ "defer-to-connect": "^1.0.1" } }, + "@types/cacheable-request": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz", + "integrity": "sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==", + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } + }, + "@types/http-cache-semantics": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", + "integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==" + }, + "@types/keyv": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz", + "integrity": "sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==", + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "12.12.47", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.47.tgz", - "integrity": "sha512-yzBInQFhdY8kaZmqoL2+3U5dSTMrKaYcb561VU+lDzAYvqt+2lojvBEy+hmpSNuXnPTx7m9+04CzWYOUqWME2A==", - "dev": true + "integrity": "sha512-yzBInQFhdY8kaZmqoL2+3U5dSTMrKaYcb561VU+lDzAYvqt+2lojvBEy+hmpSNuXnPTx7m9+04CzWYOUqWME2A==" + }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "requires": { + "@types/node": "*" + } }, "archive-type": { "version": "4.0.0", @@ -227,29 +258,58 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, + "cacheable-lookup": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz", + "integrity": "sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w==" + }, "cacheable-request": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", - "integrity": "sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=", - "requires": { - "clone-response": "1.0.2", - "get-stream": "3.0.0", - "http-cache-semantics": "3.8.1", - "keyv": "3.0.0", - "lowercase-keys": "1.0.0", - "normalize-url": "2.0.1", - "responselike": "1.0.2" + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.1.tgz", + "integrity": "sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^2.0.0" }, "dependencies": { "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "requires": { + "pump": "^3.0.0" + } + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "keyv": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.1.tgz", + "integrity": "sha512-xz6Jv6oNkbhrFCvCP7HQa8AaII8y8LRpoSm661NOKLr4uHuBwhX4epXrPQgF3+xdJnN4Esm5X0xwY4bOlALOtw==", + "requires": { + "json-buffer": "3.0.1" + } }, "lowercase-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", - "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, + "responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "requires": { + "lowercase-keys": "^2.0.0" + } } } }, @@ -519,10 +579,105 @@ "pify": "^4.0.1" }, "dependencies": { + "@sindresorhus/is": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", + "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==" + }, + "cacheable-request": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", + "integrity": "sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=", + "requires": { + "clone-response": "1.0.2", + "get-stream": "3.0.0", + "http-cache-semantics": "3.8.1", + "keyv": "3.0.0", + "lowercase-keys": "1.0.0", + "normalize-url": "2.0.1", + "responselike": "1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=" + } + } + }, + "got": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", + "integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==", + "requires": { + "@sindresorhus/is": "^0.7.0", + "cacheable-request": "^2.1.1", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "into-stream": "^3.1.0", + "is-retry-allowed": "^1.1.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "mimic-response": "^1.0.0", + "p-cancelable": "^0.4.0", + "p-timeout": "^2.0.1", + "pify": "^3.0.0", + "safe-buffer": "^5.1.1", + "timed-out": "^4.0.1", + "url-parse-lax": "^3.0.0", + "url-to-options": "^1.0.1" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + } + } + }, + "http-cache-semantics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==" + }, + "normalize-url": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", + "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", + "requires": { + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" + } + }, + "p-cancelable": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", + "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==" + }, "pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", + "requires": { + "is-plain-obj": "^1.0.0" + } } } }, @@ -671,35 +826,6 @@ "requires": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, "fs-constants": { @@ -792,38 +918,70 @@ } }, "got": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", - "integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==", - "requires": { - "@sindresorhus/is": "^0.7.0", - "cacheable-request": "^2.1.1", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "into-stream": "^3.1.0", - "is-retry-allowed": "^1.1.0", - "isurl": "^1.0.0-alpha5", - "lowercase-keys": "^1.0.0", - "mimic-response": "^1.0.0", - "p-cancelable": "^0.4.0", - "p-timeout": "^2.0.1", - "pify": "^3.0.0", - "safe-buffer": "^5.1.1", - "timed-out": "^4.0.1", - "url-parse-lax": "^3.0.0", - "url-to-options": "^1.0.1" + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/got/-/got-11.3.0.tgz", + "integrity": "sha512-yi/kiZY2tNMtt5IfbfX8UL3hAZWb2gZruxYZ72AY28pU5p0TZjZdl0uRsuaFbnC0JopdUi3I+Mh1F3dPQ9Dh0Q==", + "requires": { + "@sindresorhus/is": "^2.1.1", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.1", + "decompress-response": "^6.0.0", + "get-stream": "^5.1.0", + "http2-wrapper": "^1.0.0-beta.4.5", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" }, "dependencies": { + "@szmarczak/http-timer": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz", + "integrity": "sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==", + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, + "defer-to-connect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz", + "integrity": "sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg==" + }, "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "requires": { + "pump": "^3.0.0" + } }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + }, + "responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "requires": { + "lowercase-keys": "^2.0.0" + } } } }, @@ -856,9 +1014,18 @@ "integrity": "sha1-huYybSnF0Dnen6xYSkVon5KfT3I=" }, "http-cache-semantics": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", - "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, + "http2-wrapper": { + "version": "1.0.0-beta.4.6", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.0-beta.4.6.tgz", + "integrity": "sha512-9oB4BiGDTI1FmIBlOF9OJ5hwJvcBEmPCqk/hy314Uhy2uq5TjekUZM8w8SPLLlUEM+mxNhXdPAXfrJN2Zbb/GQ==", + "requires": { + "quick-lru": "^5.0.0", + "resolve-alpn": "^1.0.0" + } }, "https": { "version": "1.0.0", @@ -924,8 +1091,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -1064,24 +1230,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "normalize-url": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", - "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", - "requires": { - "prepend-http": "^2.0.0", - "query-string": "^5.0.1", - "sort-keys": "^2.0.0" - }, - "dependencies": { - "sort-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", - "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", - "requires": { - "is-plain-obj": "^1.0.0" - } - } - } + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" }, "npm-conf": { "version": "1.1.3", @@ -1124,9 +1275,9 @@ } }, "p-cancelable": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", - "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz", + "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==" }, "p-event": { "version": "2.3.1", @@ -1237,11 +1388,15 @@ "inherits": "~2.0.3" } }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -1252,6 +1407,11 @@ "util-deprecate": "~1.0.1" } }, + "resolve-alpn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.0.0.tgz", + "integrity": "sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA==" + }, "responselike": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", @@ -1351,7 +1511,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } diff --git a/package.json b/package.json index 2f8a5ab..d6faa3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "m3u8-downloader", - "version": "1.0.4", + "version": "1.0.5", "description": "hls 下载器,用于下载主流网站的视频和直播。", "main": "main.js", "dependencies": { @@ -9,6 +9,7 @@ "dateformat": "^3.0.3", "download": "^8.0.0", "fluent-ffmpeg": "^2.1.2", + "got": "^11.3.0", "http": "0.0.0", "https": "^1.0.0", "m3u8-parser": "^4.4.0", diff --git a/render.js b/render.js index 45e43e9..9b94ef2 100644 --- a/render.js +++ b/render.js @@ -53,19 +53,29 @@ function addVideo(data) newItem.querySelector('.link input').value = data.url; newItem.querySelector('.time .value').innerHTML = data.time; newItem.querySelector('.status .value').innerHTML = data.status; - newItem.querySelector('.opendir').setAttribute('dir',data.dir); - newItem.querySelector('.opendir').onclick = opendir; + newItem.querySelector('.opendir').setAttribute('opt', "opendir"); + newItem.querySelector('.opendir').setAttribute('data',data.dir); + newItem.querySelector('.opendir').onclick = click_callback; + newItem.querySelector('.del').setAttribute('opt','delvideo'); newItem.querySelector('.del').setAttribute('data',data.id); - newItem.querySelector('.del').onclick = delvideo; + newItem.querySelector('.del').onclick = click_callback; + + newItem.querySelector('.StartStop').setAttribute('opt','StartOrStop'); + newItem.querySelector('.StartStop').setAttribute('data',data.id); + newItem.querySelector('.StartStop').onclick = click_callback; + + + newItem.querySelector('.play').setAttribute('opt','playvideo'); + newItem.querySelector('.play').onclick = click_callback; + if(data.status != "已完成") { newItem.querySelector('.del').style.display='none'; newItem.querySelector('.play').style.display='none'; } else - { - newItem.querySelector('.play').setAttribute('videopath',data.videopath); - newItem.querySelector('.play').onclick = playvideo; + { + newItem.querySelector('.play').setAttribute('data',data.videopath); } taskList.insertBefore(newItem,null); @@ -82,10 +92,9 @@ ipcRenderer.on('task-notify-update',function(event,data){ ipcRenderer.on('task-notify-end',function(event,data){ var newItem = document.querySelector('#_'+data.id); - - newItem.querySelector('.play').setAttribute('videopath',data.videopath); + + newItem.querySelector('.play').setAttribute('data',data.videopath); newItem.querySelector('.status .value').innerHTML = data.status; - newItem.querySelector('.play').onclick = playvideo; newItem.querySelector('.del').style.display=''; newItem.querySelector('.play').style.display=''; }); @@ -106,27 +115,16 @@ ipcRenderer.on('delvideo-reply',function(event,data){ document.body.onload = function(){ ipcRenderer.send('get-all-videos'); - return; - var TaskList = document.querySelector('.TaskList'); - var TaskItem = document.querySelector('.TaskList .item'); - for (let index = 0; index < 10; index++) { - - TaskList.innerHTML = TaskList.innerHTML +TaskItem.outerHTML; - } }; -function opendir(dir) -{ - ipcRenderer.send('opendir',this.getAttribute('dir')); -} - -function delvideo() +function click_callback() { - ipcRenderer.send('delvideo',this.getAttribute('data')); -} -function playvideo() -{ - ipcRenderer.send('playvideo',this.getAttribute('videopath')); + var opt = this.getAttribute('opt'); + if(opt == "StartOrStop") + { + this.value = this.value == "停止"?"重新开始":"停止"; + } + ipcRenderer.send(this.getAttribute('opt'),this.getAttribute('data')); } function setting_isdelts(){ diff --git a/resource/HLSDownloadShow-2.gif b/resource/HLSDownloadShow-2.gif new file mode 100644 index 0000000..50d0277 Binary files /dev/null and b/resource/HLSDownloadShow-2.gif differ