import path from 'path'
import fs from 'fs'
import process from 'process'
import get from 'lodash-es/get.js'
import filter from 'lodash-es/filter.js'
import size from 'lodash-es/size.js'
import genID from 'wsemi/src/genID.mjs'
import now2strp from 'wsemi/src/now2strp.mjs'
import getFileNameExt from 'wsemi/src/getFileNameExt.mjs'
import j2o from 'wsemi/src/j2o.mjs'
import isbol from 'wsemi/src/isbol.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import isfun from 'wsemi/src/isfun.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 fsCleanFolder from 'wsemi/src/fsCleanFolder.mjs'
import fsDeleteFile from 'wsemi/src/fsDeleteFile.mjs'
import fsDeleteFolder from 'wsemi/src/fsDeleteFolder.mjs'
import fsTreeFolder from 'wsemi/src/fsTreeFolder.mjs'
let fdSrv = path.resolve()
function isWindows() {
return process.platform === 'win32'
}
/**
* 下載m3u8檔案,核心調用N_m3u8DL-CLI,只能用於Windows作業系統
*
* N_m3u8DL-CLI: https://github.com/nilaoda/N_m3u8DL-CLI
*
* @param {String} url 輸入m3u8網址字串
* @param {String} fp 輸入儲存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 WDwloadM3u8 from './src/WDwloadM3u8.mjs'
*
* async function test() {
*
* //url
* let url = `https://ikcdn01.ikzybf.com/20221214/IEiv7MwN/index.m3u8`
*
* //fp
* let fp = './moon01.mp4'
*
* //funProg
* let funProg = (prog, nn, na) => {
* console.log('prog', `${prog.toFixed(2)}%`, nn, na)
* }
*
* //WDwloadM3u8
* await WDwloadM3u8(url, fp, {
* clean: true, //單一程序執行時, 事先清除之前暫存檔, 減少浪費硬碟空間
* funProg,
* })
*
* console.log('done:', fp)
* }
* test()
* .catch((err) => {
* console.log('catch', err)
* })
* // prog 0.14% 1 708
* // prog 1.41% 10 708
* // ...
* // prog 99.86% 707 708
* // prog 100.00% 708 708
* // done: ./moon01.mp4
*
*/
async function WDwloadM3u8(url, fp, opt = {}) {
let errTemp = null
//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 = 'N_m3u8DL-CLI-3.0.2'
let fdExeSelf = `${fdSrv}/${fdms}/`
let fdExeDist = `${fdSrv}/node_modules/w-dwload-m3u8/${fdms}/`
let fdExe = fdExeSelf
if (fsIsFolder(fdExeDist)) {
fdExe = fdExeDist
}
//exeDl
let exeDl = path.resolve(fdExe, 'N_m3u8DL-CLI.exe')
exeDl = `"${exeDl}"` //用雙引號包住避免路徑有空格
// console.log('exeDl', exeDl)
//exeFfmpeg
let exeFfmpeg = path.resolve(fdExe, 'ffmpeg.exe')
exeFfmpeg = `"${exeFfmpeg}"` //用雙引號包住避免路徑有空格
// console.log('exeFfmpeg', exeFfmpeg)
//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)}`
//fnMp4
let fnMp4 = `${id}.mp4`
//fnTs
let fnTs = `${id}.ts`
//fdDownloads
let fdDownloads = path.resolve(fdExe, 'Downloads')
// console.log('fdDownloads', fdDownloads)
//fdLogs
let fdLogs = path.resolve(fdExe, 'Logs')
// console.log('fdLogs', fdLogs)
//clean
if (clean) {
fsCleanFolder(fdDownloads)
fsCleanFolder(fdLogs)
}
//fdDownloadsId
let fdDownloadsId = path.resolve(fdDownloads, id)
// console.log('fdDownloadsId', fdDownloadsId)
// //fdDownloadsIdPart
// let fdDownloadsIdPart = path.resolve(fdDownloadsId, 'Part_0') //可能會分拆資料夾儲存ts, 故不使用
// // console.log('fdDownloadsIdPart', fdDownloadsIdPart)
//fpInMp4
let fpInMp4 = path.resolve(fdDownloads, fnMp4)
// console.log('fpInMp4', fpInMp4)
//fpInTs
let fpInTs = path.resolve(fdDownloads, fnTs)
// console.log('fpInTs', fpInTs)
//fpDownloadsIdMeta
let fpDownloadsIdMeta = path.resolve(fdDownloadsId, 'meta.json')
// console.log('fpDownloadsIdMeta', fpDownloadsIdMeta)
//監測進度
let om = null
let bFunProg = isfun(funProg)
let nnPre = -1
let nnMax = -1
let t = setInterval(() => {
//na
let na = get(om, 'm3u8Info.count', 0)
if (na === 0) {
if (fsIsFile(fpDownloadsIdMeta)) {
let j = fs.readFileSync(fpDownloadsIdMeta, 'utf8')
om = j2o(j)
}
}
// console.log('om', om)
// console.log('na', na)
//nn
let vps = []
try {
vps = fsTreeFolder(fdDownloadsId, null)
}
catch (err) {
// console.log(err)
// errTemp = err.message
// clearInterval(t)
}
// console.log('vps(ori)', vps)
vps = filter(vps, (v) => {
let ext = getFileNameExt(v.name)
return ext === 'ts'
})
// console.log('vps(filter)', vps)
let nn = size(vps)
// console.log('nn', nn)
//max
nnMax = Math.max(nnMax, nn) //最後階段會依照各part資料夾各自產生合併ts, 會導致ts數量大減, 故須取最大值
//limit
nn = Math.max(nn, nnMax) //合併ts階段會導致ts數量大減, 故須取最大值
nn = Math.min(nn, na) //因最後階段若為產生合併ts, 此時又剛好觸發偵測而nn會大於na(也就是nn-na=1), 故須限制nn最高為na
//prog
if (na > 0 && nnPre !== nn) {
let prog = nn / na * 100
// console.log('prog', prog)
if (bFunProg) {
// console.log('prog', nn, na, prog)
funProg(prog, nn, na)
}
}
//update
nnPre = nn
}, 1000)
//cmdDl
let cmdDl = `"${url}" --saveName "${id}"`
// console.log('cmdDl', cmdDl)
//execProcess
await execProcess(exeDl, cmdDl)
// .then(function(res) {
// console.log('execProcess then', res)
// })
.catch((err) => {
console.log('execProcess catch', err)
errTemp = 'execProcess error'
})
//clearInterval
clearInterval(t)
//chdir, 不論正常或錯誤皆需還原工作路徑
process.chdir(cwdOri)
//check
if (isestr(errTemp)) {
return Promise.reject(errTemp)
}
//check, 若因m3u8內method給NONE無法被N_m3u8DL-CLI識別處理, 故會採取僅合併ts不轉檔方式產生ts
if (!fsIsFile(fpInMp4) && fsIsFile(fpInTs)) {
//cmdFfmpeg
let cmdFfmpeg = `-i "${fpInTs}" -vcodec copy -acodec copy "${fpInMp4}"`
// console.log('cmdFfmpeg', cmdFfmpeg)
//execProcess
await execProcess(exeFfmpeg, cmdFfmpeg)
// .then(function(res) {
// console.log('execProcess then', res)
// })
.catch(() => {
//console.log('execProcess catch', err)
//特殊偵測處理, 不再提供報錯訊尋
})
}
//check
if (!fsIsFile(fpInMp4)) {
console.log(`can not find the merged file[${fnMp4}]`)
errTemp = `invalid url[${url}] or can not download`
return Promise.reject(errTemp)
}
//fpOut
let fpOut = fp
// console.log('fpOut', fpOut)
let rc
//fsCopyFile
rc = fsCopyFile(fpInMp4, fpOut)
errTemp = get(rc, 'error')
if (errTemp) {
return Promise.reject(errTemp.toString())
}
//fsDeleteFile
rc = fsDeleteFile(fpInMp4)
errTemp = get(rc, 'error')
if (errTemp) {
return Promise.reject(errTemp.toString())
}
//fsDeleteFile
rc = fsDeleteFile(fpInTs)
//可能無檔案無法刪, 故不檢查錯誤
//fsDeleteFolder
rc = fsDeleteFolder(fdDownloadsId)
errTemp = get(rc, 'error')
if (errTemp) {
return Promise.reject(errTemp.toString())
}
//fsCleanFolder
rc = fsCleanFolder(fdLogs)
errTemp = get(rc, 'error')
if (errTemp) {
return Promise.reject(errTemp.toString())
}
return 'ok'
}
export default WDwloadM3u8