UNPKG

w-dwload-dlp

Version:
509 lines (411 loc) 15.4 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>WDwloadDlp.mjs - Documentation</title> <script src="scripts/prettify/prettify.js"></script> <script src="scripts/prettify/lang-css.js"></script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc.css"> <script src="scripts/nav.js" defer></script> <meta name="viewport" content="width=device-width, initial-scale=1.0"> </head> <body> <input type="checkbox" id="nav-trigger" class="nav-trigger" /> <label for="nav-trigger" class="navicon-button x"> <div class="navicon"></div> </label> <label for="nav-trigger" class="overlay"></label> <nav > <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#WDwloadDlp">WDwloadDlp</a></li></ul> </nav> <div id="main"> <h1 class="page-title">WDwloadDlp.mjs</h1> <section> <article> <pre class="prettyprint source linenums"><code>import path from 'path' import process from 'process' import { createRequire } from 'module' import get from 'lodash-es/get.js' import size from 'lodash-es/size.js' import each from 'lodash-es/each.js' import genID from 'wsemi/src/genID.mjs' import now2strp from 'wsemi/src/now2strp.mjs' import sep from 'wsemi/src/sep.mjs' import strright from 'wsemi/src/strright.mjs' import strdelright from 'wsemi/src/strdelright.mjs' import isestr from 'wsemi/src/isestr.mjs' import isbol from 'wsemi/src/isbol.mjs' import isnum from 'wsemi/src/isnum.mjs' import isfun from 'wsemi/src/isfun.mjs' import cint from 'wsemi/src/cint.mjs' import cdbl from 'wsemi/src/cdbl.mjs' import execProcess from 'wsemi/src/execProcess.mjs' import fsIsFile from 'wsemi/src/fsIsFile.mjs' import fsCopyFile from 'wsemi/src/fsCopyFile.mjs' import fsCleanFolder from 'wsemi/src/fsCleanFolder.mjs' import fsDeleteFile from 'wsemi/src/fsDeleteFile.mjs' let fdSrv = path.resolve() function isWindows() { return process.platform === 'win32' } /** * 下載video檔案,預設轉mp4,核心調用yt-dlp,只能用於Windows作業系統 * * yt-dlp: https://github.com/yt-dlp/yt-dlp * * @param {String} url 輸入網址字串,支援網站種類詳見yt-dlp * @param {String} fp 輸入儲存video(*.mp4)檔案路徑字串 * @param {Object} [opt={}] 輸入設定物件,預設{} * @param {Boolean} [opt.clean=false] 輸入預先清除暫存檔布林值,預設false * @param {Function} [opt.funProg=null] 輸入回傳進度函數,傳入參數為prog代表進度百分比、nn代表當前已下載ts檔案數量、na代表全部須下載ts檔案數量,預設null * @returns {Promise} 回傳Promise,resolve回傳成功訊息,reject回傳錯誤訊息 * @example * import fs from 'fs' * import WDwloadDlp from './src/WDwloadDlp.mjs' * * async function test() { * * //url * let url = `https://www.youtube.com/watch?v=jfXg5ZslKg4` //youtube mp4 * // let url = `https://www.youtube.com/watch?v=fTk0mc946dk` //youtube webm * // let url = `https://www.youtube.com/watch?v=lKoCiBVKQaQ` //youtube webm * // let url = `https://www.bilibili.com/video/BV1JZ421x7q8/?spm_id_from=333.1073.channel.secondary_floor_video.click` //bilibili * // let url = `https://ooo.mp4` //直接提供mp4檔 * * //fp * let fp = './abc.mp4' * * //funProg * let funProg = (prog, nn, na) => { * console.log('prog', `${prog.toFixed(2)}%`, nn, na) * } * * //WDwloadDlp * await WDwloadDlp(url, fp, { * clean: true, //單一程序執行時, 事先清除之前暫存檔, 減少浪費硬碟空間 * funProg, * }) * * //len * let len = fs.statSync(fp).size * console.log('len', len) * * console.log('done:', fp) * } * test() * .catch((err) => { * console.log('catch', err) * }) * // prog 0.49% 1 99 * // prog 5.05% 4 99 * // ... * // prog 99.00% 98 99 * // prog 100.00% 99 99 * // len 22394508 * // done: ./abc.mp4 * */ async function WDwloadDlp(url, fp, opt = {}) { let errTemp = null let rc //clean let clean = get(opt, 'clean') if (!isbol(clean)) { clean = false } //funProg let funProg = get(opt, 'funProg') //isWindows if (!isWindows()) { return Promise.reject('operating system is not windows') } //check if (!isestr(url)) { return Promise.reject('url is not a file') } //fnExeDlp let fnExeDlp = 'yt-dlp.exe' //fdExe let fdExe = '' if (true) { let fdExeSrc = `${fdSrv}/yt-dlp/` let fdExeNM = `${fdSrv}/node_modules/w-dwload-dlp/yt-dlp/` if (fsIsFile(`${fdExeSrc}${fnExeDlp}`)) { fdExe = fdExeSrc } else if (fsIsFile(`${fdExeNM}${fnExeDlp}`)) { fdExe = fdExeNM } else { return Promise.reject('can not find folder for yt-dlp') } } // console.log('fdExe', fdExe) //fpExeDlp let fpExeDlp = path.resolve(fdExe, fnExeDlp) // fpExeDlp = `"${fpExeDlp}"` //用雙引號包住避免路徑有空格 //execProcess預設使用spawn不須雙引號括住 // console.log('fpExeDlp', fpExeDlp) //fnExeFfmpeg let fnExeFfmpeg = 'ffmpeg.exe' //fdFfmpeg, 取用已安裝的w-ffmpeg套件內src/ (與fdExe同風格) let fdFfmpeg = '' if (true) { let fdFfmpegHoist = `${fdSrv}/node_modules/w-ffmpeg/src/` //一般hoisted(含開發w-dwload-dlp本身時) if (!isestr(fdFfmpeg) &amp;&amp; fsIsFile(`${fdFfmpegHoist}${fnExeFfmpeg}`)) { fdFfmpeg = fdFfmpegHoist } //createRequire, 未hoist(版本衝突巢狀)等情形, 由node自身解析w-ffmpeg實際位置(不受巢狀深度影響) if (!isestr(fdFfmpeg)) { try { let nodeRequire = createRequire(import.meta.url) let fdFfmpegReq = `${path.dirname(nodeRequire.resolve('w-ffmpeg/package.json'))}/src/` if (fsIsFile(`${fdFfmpegReq}${fnExeFfmpeg}`)) { fdFfmpeg = fdFfmpegReq } } catch (err) { //resolve失敗代表找不到w-ffmpeg, 留待下方統一reject } } if (!isestr(fdFfmpeg)) { return Promise.reject('can not find folder for ffmpeg (w-ffmpeg)') } } // console.log('fdFfmpeg', fdFfmpeg) //fpExeFfmpeg let fpExeFfmpeg = path.resolve(fdFfmpeg, fnExeFfmpeg) // console.log('fpExeFfmpeg', fpExeFfmpeg) //cwdOri, cwdTar let cwdOri = process.cwd() let cwdTar = fdExe // console.log('process.cwd1', process.cwd()) //chdir, 若不切換mseed2ascii預設輸出檔案是在工作路徑, 輸出檔變成會出現在專案下 process.chdir(cwdTar) // console.log('process.cwd2', process.cwd()) //id let id = `${now2strp()}-${genID(6)}` //fnAny let fnAny = `${id}` //無副檔名 //fdDownloads let fdDownloads = path.resolve(fdExe, 'Downloads') // console.log('fdDownloads', fdDownloads) //clean if (clean) { fsCleanFolder(fdDownloads) } //fpInAny let fpInAny = path.resolve(fdDownloads, fnAny) // console.log('fpInAny', fpInAny) //fpInMp4 let fpInMp4 = path.resolve(fdDownloads, `${id}.mp4`) // console.log('fpInMp4', fpInMp4) let fmts = [] // let mode = 'mp4' //default let dn = 0 //default let da = 1 //default // let bdl = false let nnPre = 0 let nn = 0 let nnTotal = 100 //default let naf = 0 let prog = 0 let bFunProg = isfun(funProg) let cbStdout = (msg) => { // console.log('cbStdout', msg) msg = msg.replaceAll('\n', ' ') //可能多訊息合併觸發, 去除換行符號不分列處理, 並只偵測處理前面(第1條) // console.log('cbStdout', `*${msg}*`) //s let s = sep(msg, ' ') // console.log('s', s) //stdout分不同frag之進度: // stdout [download] 4.0% of ~ 6.17MiB at 99.52KiB/s ETA Unknown (frag 1/99) // stdout [download] 2.4% of ~ 12.36MiB at 262.95KiB/s ETA Unknown (frag 1/99) // stdout [download] 1.0% of ~ 29.18MiB at 262.95KiB/s ETA Unknown (frag 2/99) // stdout [download] 3.0% of ~ 9.74MiB at 329.58KiB/s ETA Unknown (frag 2/99) // stdout [download] 3.1% of ~ 9.77MiB at 329.58KiB/s ETA Unknown (frag 2/99) //stdout無frag之進度: // stdout [download] 0.0% of 153.09MiB at 1.43MiB/s ETA 01:49 // stdout [download] 0.1% of 153.09MiB at 1.41MiB/s ETA 01:48 // stdout [download] 0.2% of 153.09MiB at 1.75MiB/s ETA 01:27 // stdout [download] 0.3% of 153.09MiB at 2.43MiB/s ETA 01:02 // stdout [download] 0.7% of 153.09MiB at 2.75MiB/s ETA 00:55 //fmts if (msg.indexOf('format(s):') >= 0) { let ss = sep(msg, 'format(s):') let ss1 = get(ss, 1, '') fmts = sep(ss1, '+') // console.log('fmts', fmts) da = size(fmts) // console.log('da', da, fmts, msg) } //自動更新fmts, 並將size(fmts)視為階段數 if (size(fmts) > 0) { let _kfmt = -1 // let _fmt = '' each(fmts, (fmt, kfmt) => { if (msg.indexOf(`.f${fmt}.`) >= 0) { _kfmt = kfmt // _fmt = fmt } }) let _dn = _kfmt + 1 if (dn &lt; _dn) { dn = _dn // console.log('_kfmt', _kfmt, '_fmt', _fmt) // console.log('dn', dn, 'da', da) } } //自動更新naf if (naf === 0) { if (msg.indexOf(' ETA ') >= 0) { if (msg.indexOf('(frag') >= 0) { //(frag 82/99) let ss = sep(msg, '(frag') let ss1 = get(ss, 1, '') ss1 = ss1.replaceAll(')', '') let nnna = sep(ss1, '/') let _naf = get(nnna, 1, '') if (isnum(_naf)) { _naf = cint(_naf) if (_naf > 0) { naf = _naf // console.log('naf', naf) } } } else { //沒有frag, 例如直接下載mp4檔案 naf = 100 } } } //更新dn if (dn === 0) { if (msg.indexOf(' ETA ') >= 0 &amp;&amp; msg.indexOf('(frag') &lt; 0) { //若dn=0, 若有下載訊息ETA出現但沒有看到階段frag, 則視為1階 dn = 1 } } //s1 let s1 = get(s, 1, '') //bp let bp1 = strright(s1, 1) === '%' let bp2 = msg.indexOf('(frag 0/') >= 0 //第1個可能是下載清單瞬間會100%, 直接忽略不考慮 let bp = bp1 &amp;&amp; !bp2 //prog let _prog = 0 if (bp) { // console.log(s, s1) _prog = strdelright(s1, 1) _prog = cdbl(_prog) // console.log('_prog(ori)', _prog) } //依照階段重算_prog if (_prog > 0) { //rDif, rPre let rDif = 1 / da let pPre = ((dn - 1) / da) * 100 // console.log('dn', dn) // console.log('da', da) // console.log('pPre', pPre) // console.log('rDif', rDif) //計算分階段值 _prog = rDif * _prog + pPre // console.log('_prog(stage)', _prog) } //最高99%, 因可能還有轉檔(例如webm轉mp4), 故最後100%改由最後完成階段觸發 _prog *= 0.99 // console.log('_prog(99%)', _prog) //update if (prog &lt; _prog) { prog = _prog } else { return } // console.log('prog(now)', prog) //update nn if (naf > 0) { nnTotal = naf } nn = cint(prog / 100 * nnTotal) // console.log('nn', nn, 'nat', nat) //check, 若nn沒有>nnPre則視為不需要觸發funProg, 減少切太細導致高頻觸發 if (nn &lt;= nnPre) { return } //funProg if (bFunProg) { // console.log('prog', nn, nat, prog) funProg(prog, nn, nnTotal) } //update nnPre = nn } //cmdDl // let cmdDl = `"${url}" -o "${fpInAny}" --newline --merge-output-format "mp4"` let cmdDl = [url, '-o', fpInAny, '--newline', '--no-playlist', '--merge-output-format', 'mp4', '--ffmpeg-location', fpExeFfmpeg] //execProcess預設使用spawn不須雙引號括住 // console.log('cmdDl', cmdDl) //execProcess await execProcess(fpExeDlp, cmdDl, { cbStdout }) // .then(function(res) { // console.log('execProcess then', res) // }) .catch((err) => { console.log('execProcess catch', err) errTemp = err.toString() }) //chdir, 不論正常或錯誤皆需還原工作路徑 process.chdir(cwdOri) //check if (errTemp) { return Promise.reject(errTemp) } //check if (!fsIsFile(fpInMp4)) { //_fpInMp4, dlp針對某些mp4下載後會無法自動提供副檔名, 得再偵測無副檔名之檔案是否存在 let _fpInMp4 = path.resolve(fdDownloads, id) //check if (!fsIsFile(_fpInMp4)) { console.log(`can not find the merged file[${fpInMp4}]`) return Promise.reject(`invalid url[${url}] or can not download`) } //modify fpInMp4 = _fpInMp4 } //fpOut let fpOut = fp // console.log('fpOut', fpOut) //fsCopyFile rc = fsCopyFile(fpInMp4, fpOut) errTemp = get(rc, 'error') if (errTemp) { return Promise.reject(errTemp.toString()) } //fsDeleteFile rc = fsDeleteFile(fpInMp4) //可能無檔案無法刪, 故不檢查錯誤 //funProg if (bFunProg) { funProg(100, nnTotal, nnTotal) } return 'ok' } export default WDwloadDlp </code></pre> </article> </section> </div> <br class="clear"> <footer> Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu May 21 2026 23:13:01 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme. </footer> <script>prettyPrint();</script> <script src="scripts/polyfill.js"></script> <script src="scripts/linenumber.js"></script> </body> </html>