import path from 'path'
import fs from 'fs'
import process from 'process'
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 isearr from 'wsemi/src/isearr.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 genPm from 'wsemi/src/genPm.mjs'
import execProcess from 'wsemi/src/execProcess.mjs'
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
import fsCopyFile from 'wsemi/src/fsCopyFile.mjs'
import fsRenameFile from 'wsemi/src/fsRenameFile.mjs'
import fsCleanFolder from 'wsemi/src/fsCleanFolder.mjs'
import fsDeleteFile from 'wsemi/src/fsDeleteFile.mjs'
import fsDeleteFolder from 'wsemi/src/fsDeleteFolder.mjs'
import mZip from 'w-zip/src/mZip.mjs'
let fdSrv = path.resolve()
function isWindows() {
return process.platform === 'win32'
}
function fsMergeFiles(files, target) {
//check
if (!isearr(files) && !isestr(files)) {
throw new Error(`files[${files}] is not an effective string or array`)
}
if (!isearr(files)) {
files = [files]
}
//pm
let pm = genPm()
//writeable
let writeable = fs.createWriteStream(target)
//core
let core = () => {
if (size(files) === 0) {
writeable.end()
pm.resolve()
}
else {
let readable = fs.createReadStream(files.shift())
readable.pipe(writeable, { end: false })
readable.on('end', () => {
core()
})
readable.on('error', (err) => {
pm.reject(err)
})
}
}
//core
core()
return pm
}
/**
* 下載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 WDwloadDlp from 'w-dwload-dlp'
* import WDwloadDlp from './src/WDwloadDlp.mjs'
*
* async function test() {
*
* //url
* let url = `https://www.youtube.com/watch?v=uj8hjLyEBmU&ab_channel=%E7%A0%81%E5%86%9C%E9%AB%98%E5%A4%A9` //youtube
* // let url = `https://www.bilibili.com/video/BV1JZ421x7q8/?spm_id_from=333.1073.channel.secondary_floor_video.click` //bilibili
*
* //fp
* let fp = './test.mp4'
*
* //funProg
* let funProg = (prog, nn, nat) => {
* console.log('prog', `${prog.toFixed(2)}%`, nn, nat)
* }
*
* //WDwloadDlp
* await WDwloadDlp(url, fp, {
* clean: true, //單一程序執行時, 事先清除之前暫存檔, 減少浪費硬碟空間
* funProg,
* })
*
* 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
* // done: ./test.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')
}
//fdExe
let fdms = 'yt-dlp'
let fdExeSelf = `${fdSrv}/${fdms}/`
let fdExeDist = `${fdSrv}/node_modules/w-dwload-dlp/${fdms}/`
let fdExe = fdExeSelf
if (fsIsFolder(fdExeDist)) {
fdExe = fdExeDist
}
//exeDlp
let exeDlp = path.resolve(fdExe, 'yt-dlp.exe')
exeDlp = `"${exeDlp}"` //用雙引號包住避免路徑有空格
// console.log('exeDlp', exeDlp)
//exeFfmpeg
let exeFfmpeg = path.resolve(fdExe, 'ffmpeg.exe')
// console.log('exeFfmpeg', exeFfmpeg)
//check ffmpeg, 若ffmpeg不存在則由分拆zip檔解壓縮出來用
if (!fsIsFile(exeFfmpeg)) {
//zipFfmpeg
let zipFfmpeg = path.resolve(fdExe, 'ffmpeg.zip')
//fsMergeFiles to ffmpeg.zip
let fp1 = path.resolve(fdExe, 'ffmpeg.zip.001')
let fp2 = path.resolve(fdExe, 'ffmpeg.zip.002')
let fp3 = path.resolve(fdExe, 'ffmpeg.zip.003')
let fp4 = path.resolve(fdExe, 'ffmpeg.zip.004')
let fp5 = path.resolve(fdExe, 'ffmpeg.zip.005')
await fsMergeFiles([fp1, fp2, fp3, fp4, fp5], zipFfmpeg)
//unzip
let fdFfmpeg = path.resolve(fdExe, 'temp')
if (true) {
await mZip.unzip(zipFfmpeg, fdFfmpeg)
// console.log('mZip.unzip', r)
}
//fsRenameFile ffmpeg.exe
if (true) {
let exeFfmpegTemp = path.resolve(`${fdFfmpeg}/`, 'ffmpeg.exe')
// console.log('exeFfmpegTemp', exeFfmpegTemp)
fsRenameFile(exeFfmpegTemp, exeFfmpeg)
// console.log('fsRenameFile', r)
}
//fsDeleteFile ffmpeg.zip
fsDeleteFile(zipFfmpeg)
//fsDeleteFolder temp
fsDeleteFolder(fdFfmpeg)
}
//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 < _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 && msg.indexOf('(frag') < 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 && !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 < _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 <= 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"`
// console.log('cmdDl', cmdDl)
//execProcess
await execProcess(exeDlp, cmdDl, { cbStdout })
// .then(function(res) {
// console.log('execProcess then', res)
// })
.catch((err) => {
console.log('execProcess catch', err)
errTemp = 'execProcess error'
})
//chdir, 不論正常或錯誤皆需還原工作路徑
process.chdir(cwdOri)
//check
if (isestr(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}]`)
errTemp = `invalid url[${url}] or can not download`
return Promise.reject(errTemp)
}
//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