From 7358a533e2a7c02b40d5189c03848e3a507d79e2 Mon Sep 17 00:00:00 2001 From: renxia Date: Fri, 2 Aug 2024 01:04:46 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=BE=AE=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=BC=93=E5=AD=98=E4=B8=8E=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E7=9A=84=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=BB=85=E7=BC=93=E5=AD=98=20ts=20=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=87=B3=E6=9C=AC=E5=9C=B0=E7=9A=84=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cli.ts | 1 + src/lib/local-play.ts | 21 ++++++++++++++------- src/lib/m3u8-download.ts | 14 +++++++++----- src/lib/parseM3u8.ts | 27 +++++++++++++++++---------- src/types/m3u8.ts | 2 ++ 5 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 659f60f..6a475a7 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -33,6 +33,7 @@ program .option('-C, --cache-dir ', `临时文件保存目录。默认为 cache`) .option('-S, --save-dir ', `下载文件保存的路径。默认为当前目录`) .option('--no-del-cache', `下载成功后是否删除临时文件。默认为 true。保存临时文件可以在重复下载时识别缓存`) + .option('--no-convert', '下载成功后,是否不合并转换为 mp4 文件。默认为 true。') .action(async (urls: string[]) => { const options = getOptions(); logger.debug(urls, options); diff --git a/src/lib/local-play.ts b/src/lib/local-play.ts index b1993e2..57a2f67 100644 --- a/src/lib/local-play.ts +++ b/src/lib/local-play.ts @@ -1,6 +1,6 @@ -import { execSync, findFreePort } from '@lzwme/fe-utils'; +import { execSync, findFreePort, mkdirp } from '@lzwme/fe-utils'; import { color } from 'console-log-colors'; -import { createReadStream, existsSync, promises, readdirSync, statSync } from 'node:fs'; +import { createReadStream, existsSync, readdirSync, statSync, writeFileSync } from 'node:fs'; import { createServer } from 'node:http'; import { basename, dirname, extname, join, resolve } from 'node:path'; import { logger } from './utils'; @@ -17,7 +17,8 @@ export async function localPlay(m3u8Info: TsItemInfo[], options: M3u8DLOptions) const info = await createLocalServer(dirname(cacheDir)); const filename = basename(options.filename).slice(0, options.filename.lastIndexOf('.')) + `.m3u8`; - await toLocalM3u8(m3u8Info, resolve(cacheDir, filename), `/${cacheDirname}`); + const cacheFilepath = resolve(cacheDir, filename); + if (!existsSync(cacheFilepath)) toLocalM3u8(m3u8Info, cacheFilepath); const playUrl = `https://lzw.me/x/m3u8-player?url=${encodeURIComponent(`${info.origin}/${cacheDirname}/${filename}`)}`; const cmd = `${process.platform === 'win32' ? 'start' : 'open'} ${playUrl}`; @@ -26,7 +27,7 @@ export async function localPlay(m3u8Info: TsItemInfo[], options: M3u8DLOptions) return info; } -export async function toLocalM3u8(m3u8Info: TsItemInfo[], filepath: string, host = '') { +export function toLocalM3u8(m3u8Info: TsItemInfo[], m3u8FilePath: string, host = '.') { const m3u8ContentList = [ `#EXTM3U`, `#EXT-X-VERSION:3`, @@ -43,7 +44,13 @@ export async function toLocalM3u8(m3u8Info: TsItemInfo[], filepath: string, host m3u8ContentList.push(`#EXT-X-ENDLIST`); const m3u8Content = m3u8ContentList.join('\n'); - await promises.writeFile(filepath, m3u8Content, 'utf8'); + const ext = extname(m3u8FilePath); + if (ext !== '.m3u8') m3u8FilePath = m3u8FilePath.replace(ext, '') + '.m3u8'; + m3u8FilePath = resolve(dirname(m3u8Info[0].tsOut), m3u8FilePath); + mkdirp(dirname(m3u8FilePath)); + writeFileSync(m3u8FilePath, m3u8Content, 'utf8'); + + return m3u8FilePath; } async function createLocalServer(baseDir: string) { @@ -74,8 +81,8 @@ async function createLocalServer(baseDir: string) { createReadStream(filename).pipe(res); return; } else if (stats.isDirectory()) { - const html = readdirSync(filename).map(filepath => { - const rpath = resolve(filename, filepath).replace(baseDir, ''); + const html = readdirSync(filename).map(fname => { + const rpath = resolve(filename, fname).replace(baseDir, ''); return `
  • ${rpath}
  • `; }); diff --git a/src/lib/m3u8-download.ts b/src/lib/m3u8-download.ts index a3c7395..0408a49 100644 --- a/src/lib/m3u8-download.ts +++ b/src/lib/m3u8-download.ts @@ -9,7 +9,7 @@ import { WorkerPool } from './worker_pool'; import { parseM3U8 } from './parseM3u8'; import { m3u8Convert } from './m3u8-convert'; import type { M3u8DLOptions, TsItemInfo, WorkerTaskInfo } from '../types/m3u8'; -import { localPlay } from './local-play'; +import { localPlay, toLocalM3u8 } from './local-play'; const cache = { m3u8Info: {} as Record, @@ -55,6 +55,7 @@ async function m3u8InfoParse(url: string, options: M3u8DLOptions = {}) { [url, options] = formatOptions(url, options); const ext = isSupportFfmpeg() ? '.mp4' : '.ts'; + /** 最终合并转换后的文件路径 */ let filepath = resolve(options.saveDir, options.filename); if (!filepath.endsWith(ext)) filepath += ext; @@ -67,7 +68,7 @@ async function m3u8InfoParse(url: string, options: M3u8DLOptions = {}) { if (!options.force && existsSync(filepath)) return result; - const m3u8Info = await parseM3U8('', url, options.cacheDir).catch(e => logger.error('[parseM3U8][failed]', e)); + const m3u8Info = await parseM3U8(url, options.cacheDir).catch(e => logger.error('[parseM3U8][failed]', e)); if (m3u8Info && m3u8Info?.tsCount > 0) result.m3u8Info = m3u8Info; return result; @@ -184,13 +185,16 @@ export async function m3u8Download(url: string, options: M3u8DLOptions = {}) { `Parallel jobs: ${magenta(options.threadNum)}` ); } + runTask(m3u8Info.data); + toLocalM3u8(m3u8Info.data, options.filename); await barrier.wait(); if (stats.tsFailed === 0) { - result.filepath = await m3u8Convert(options, m3u8Info.data); - - if (result.filepath && existsSync(options.cacheDir) && options.delCache) rmrfAsync(options.cacheDir); + if (options.convert !== false) { + result.filepath = await m3u8Convert(options, m3u8Info.data); + if (result.filepath && existsSync(options.cacheDir) && options.delCache) rmrfAsync(options.cacheDir); + } } else logger.warn('Download Failed! Please retry!', stats.tsFailed); } logger.debug('Done!', url, result.m3u8Info); diff --git a/src/lib/parseM3u8.ts b/src/lib/parseM3u8.ts index 3ac2183..a477898 100644 --- a/src/lib/parseM3u8.ts +++ b/src/lib/parseM3u8.ts @@ -1,17 +1,24 @@ import { existsSync, promises } from 'node:fs'; -import { basename, resolve } from 'node:path'; +import { resolve } from 'node:path'; +import { md5 } from '@lzwme/fe-utils'; import { Parser } from 'm3u8-parser'; import type { M3u8Crypto, TsItemInfo } from '../types/m3u8'; import { logger, getRetry } from './utils'; -export async function parseM3U8(content: string, url = process.cwd(), cacheDir = './cache') { - if (!content && url) { - if (!url.startsWith('http') && existsSync(url)) { - url = resolve(process.cwd(), url); - content = await promises.readFile(url, 'utf8'); - } else { - content = (await getRetry(url)).data; - } +/** + * 解析 m3u8 文件 + * @param content m3u8 文件的内容,可为 http 远程地址、本地文件路径 + * @param cacheDir 缓存文件保存目录 + */ +export async function parseM3U8(content: string, cacheDir = './cache') { + let url = process.cwd(); + + if (content.startsWith('http')) { + url = content; + content = (await getRetry(url)).data; + } else if (!content.includes('\n') && existsSync(content)) { + url = resolve(process.cwd(), content); + content = await promises.readFile(url, 'utf8'); } if (!content) { @@ -84,7 +91,7 @@ export async function parseM3U8(content: string, url = process.cwd(), cacheDir = duration: tsList[i].duration, timeline: tsList[i].timeline, uri: tsList[i].uri, - tsOut: `${cacheDir}/${i}-${basename(tsList[i].uri).replace(/\.ts\?.+/, '.ts')}`, + tsOut: `${cacheDir}/${md5(tsList[i].uri)}.ts`, }); result.durationSecond += tsList[i].duration; } diff --git a/src/types/m3u8.ts b/src/types/m3u8.ts index cab3199..b9827e8 100644 --- a/src/types/m3u8.ts +++ b/src/types/m3u8.ts @@ -51,6 +51,8 @@ export interface M3u8DLOptions { headers?: IncomingHttpHeaders; /** 下载时是否启动本地资源播放(边下边看) */ play?: boolean; + /** 下载完毕后,是否合并转换为 mp4 或 ts 文件。默认为 true */ + convert?: boolean; } export interface WorkerTaskInfo {