UNPKG

@lzwme/m3u8-dl

Version:

Batch download of m3u8 files and convert to mp4

185 lines (184 loc) 8.74 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.workPoll = void 0; exports.preDownLoad = preDownLoad; exports.m3u8Download = m3u8Download; const node_path_1 = require("node:path"); const node_fs_1 = require("node:fs"); const node_os_1 = require("node:os"); const fe_utils_1 = require("@lzwme/fe-utils"); const helper_1 = require("@lzwme/fe-utils/cjs/common/helper"); const console_log_colors_1 = require("console-log-colors"); const utils_1 = require("./utils"); const worker_pool_1 = require("./worker_pool"); const parseM3u8_1 = require("./parseM3u8"); const m3u8_convert_1 = require("./m3u8-convert"); const local_play_1 = require("./local-play"); const cache = { m3u8Info: {}, downloading: new Set(), }; const tsDlFile = (0, node_path_1.resolve)(__dirname, './ts-download.js'); exports.workPoll = new worker_pool_1.WorkerPool(tsDlFile); function formatOptions(url, opts) { const options = { delCache: !opts.debug, saveDir: process.cwd(), showProgress: true, ...opts, }; if (!url.startsWith('http')) { url = url.replace(/\$+/, '|').replace(/\|\|+/, '|'); if (url.includes('|')) { const r = url.split('|'); url = r[1]; if (!options.filename) options.filename = r[0]; else options.filename = `${options.filename.replace(/\.(ts|mp4)$/, '')}-${r[0]}`; } } const urlMd5 = (0, fe_utils_1.md5)(url, false); if (!options.threadNum || +options.threadNum <= 0) options.threadNum = Math.min((0, node_os_1.cpus)().length * 2, 8); if (!options.filename) options.filename = urlMd5; if (!options.filename.endsWith('.mp4')) options.filename += '.mp4'; if (!options.cacheDir) options.cacheDir = `cache/${urlMd5}`; if (options.headers) utils_1.request.setHeaders(options.headers); if (options.debug) { utils_1.logger.updateOptions({ levelType: 'debug' }); utils_1.logger.debug('[m3u8-DL]options', options, url); } return [url, options]; } async function m3u8InfoParse(url, options = {}) { [url, options] = formatOptions(url, options); const ext = (0, utils_1.isSupportFfmpeg)() ? '.mp4' : '.ts'; let filepath = (0, node_path_1.resolve)(options.saveDir, options.filename); if (!filepath.endsWith(ext)) filepath += ext; const result = { options, m3u8Info: null, filepath }; if (cache.m3u8Info[url]) { Object.assign(result, cache.m3u8Info[url]); return result; } if (!options.force && (0, node_fs_1.existsSync)(filepath)) return result; const m3u8Info = await (0, parseM3u8_1.parseM3U8)('', url, options.cacheDir).catch(e => utils_1.logger.error('[parseM3U8][failed]', e)); if (m3u8Info && m3u8Info?.tsCount > 0) result.m3u8Info = m3u8Info; return result; } async function preDownLoad(url, options) { const result = await m3u8InfoParse(url, options); if (!result.m3u8Info) return; for (const info of result.m3u8Info.data) { if (!exports.workPoll.freeNum) return; if (!cache.downloading.has(info.uri)) { cache.downloading.add(info.uri); exports.workPoll.runTask({ info, options: JSON.parse(JSON.stringify(result.options)), crypto: result.m3u8Info.crypto }, () => { cache.downloading.delete(info.uri); }); } } } async function m3u8Download(url, options = {}) { utils_1.logger.info('Starting download for', (0, console_log_colors_1.cyanBright)(url)); const result = await m3u8InfoParse(url, options); options = result.options; if (!options.force && (0, node_fs_1.existsSync)(result.filepath) && !result.m3u8Info) { utils_1.logger.info('file already exist:', result.filepath); return result; } if (result.m3u8Info?.tsCount > 0) { let n = options.threadNum - exports.workPoll.numThreads; if (n > 0) while (n--) exports.workPoll.addNewWorker(); const { m3u8Info } = result; const startTime = Date.now(); const barrier = new fe_utils_1.Barrier(); const playStart = Math.min(options.threadNum + 2, result.m3u8Info.tsCount); const stats = { /** 下载成功的 ts 数量 */ tsSuccess: 0, /** 下载失败的 ts 数量 */ tsFailed: 0, /** 下载完成的时长 */ duration: 0, }; const runTask = (data) => { for (const info of data) { exports.workPoll.runTask({ info, options: JSON.parse(JSON.stringify(options)), crypto: m3u8Info.crypto }, (err, res) => { if (!res || err) { if (err) { console.log('\n'); utils_1.logger.error('[TS-DL][error]', info.index, err, res || ''); } if (!info.success) info.success = -1; else info.success--; if (info.success > -3) { utils_1.logger.info(`[retry][times: ${info.success}]`, info.index, info.uri); setTimeout(() => runTask([info]), 1000); return; } } if (res?.success) { info.tsSize = res.info.tsSize; info.success = 1; stats.tsSuccess++; stats.duration += info.duration; } else { stats.tsFailed++; } const finished = stats.tsFailed + stats.tsSuccess; if (options.showProgress) { const timeCost = Date.now() - startTime; const downloadedSize = m3u8Info.data.reduce((a, b) => a + (b.tsSize || 0), 0); const downloadedDuration = m3u8Info.data.reduce((a, b) => a + (b.tsSize ? b.duration : 0), 0); const avgSpeed = (0, helper_1.formatByteSize)((downloadedSize / timeCost) * 1000); const restTime = downloadedDuration ? (timeCost * (m3u8Info.durationSecond - stats.duration)) / downloadedDuration : 0; const percent = Math.floor((finished / m3u8Info.tsCount) * 100); const processBar = '='.repeat(Math.floor(percent * 0.2)).padEnd(20, '-'); utils_1.logger.logInline(`${percent}% [${(0, console_log_colors_1.greenBright)(processBar)}] ${(0, console_log_colors_1.cyan)(finished)} ` + `${(0, console_log_colors_1.green)(stats.duration.toFixed(2) + 'sec')} ` + `${(0, console_log_colors_1.blueBright)((0, helper_1.formatByteSize)(downloadedSize))} ${(0, console_log_colors_1.yellowBright)((0, fe_utils_1.formatTimeCost)(startTime))} ${(0, console_log_colors_1.magentaBright)(avgSpeed + '/s')} ` + (finished === m3u8Info.tsCount ? '\n' : restTime ? `${(0, console_log_colors_1.cyan)((0, fe_utils_1.formatTimeCost)(Date.now() - Math.ceil(restTime)))}` : '')); } if (options.onProgress) options.onProgress(finished, m3u8Info.tsCount, info); if (finished === m3u8Info.tsCount) { // pool.close(); barrier.open(); } if (options.play && finished === playStart) { (0, local_play_1.localPlay)(m3u8Info.data, options); } }); } }; if (options.showProgress) { console.info(`\nTotal segments: ${(0, console_log_colors_1.cyan)(m3u8Info.tsCount)}, duration: ${(0, console_log_colors_1.green)(m3u8Info.durationSecond + 'sec')}.`, `Parallel jobs: ${(0, console_log_colors_1.magenta)(options.threadNum)}`); } runTask(m3u8Info.data); await barrier.wait(); if (stats.tsFailed === 0) { result.filepath = await (0, m3u8_convert_1.m3u8Convert)(options, m3u8Info.data); if (result.filepath && (0, node_fs_1.existsSync)(options.cacheDir) && options.delCache) (0, fe_utils_1.rmrfAsync)(options.cacheDir); } else utils_1.logger.warn('Download Failed! Please retry!', stats.tsFailed); } utils_1.logger.debug('Done!', url, result.m3u8Info); return result; }