Skip to content

Commit

Permalink
refactor: 微重构文件缓存与本地播放的逻辑,支持仅缓存 ts 文件至本地的能力
Browse files Browse the repository at this point in the history
  • Loading branch information
renxia committed Aug 1, 2024
1 parent b88d9a5 commit 7358a53
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 22 deletions.
1 change: 1 addition & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ program
.option('-C, --cache-dir <dirpath>', `临时文件保存目录。默认为 cache`)
.option('-S, --save-dir <dirpath>', `下载文件保存的路径。默认为当前目录`)
.option('--no-del-cache', `下载成功后是否删除临时文件。默认为 true。保存临时文件可以在重复下载时识别缓存`)
.option('--no-convert', '下载成功后,是否不合并转换为 mp4 文件。默认为 true。')
.action(async (urls: string[]) => {
const options = getOptions();
logger.debug(urls, options);
Expand Down
21 changes: 14 additions & 7 deletions src/lib/local-play.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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}`;
Expand All @@ -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`,
Expand All @@ -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) {
Expand Down Expand Up @@ -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 `<li><a href="${rpath}">${rpath}</a></li>`;
});

Expand Down
14 changes: 9 additions & 5 deletions src/lib/m3u8-download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>,
Expand Down Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
27 changes: 17 additions & 10 deletions src/lib/parseM3u8.ts
Original file line number Diff line number Diff line change
@@ -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<string>(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<string>(url)).data;
} else if (!content.includes('\n') && existsSync(content)) {
url = resolve(process.cwd(), content);
content = await promises.readFile(url, 'utf8');
}

if (!content) {
Expand Down Expand Up @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions src/types/m3u8.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export interface M3u8DLOptions {
headers?: IncomingHttpHeaders;
/** 下载时是否启动本地资源播放(边下边看) */
play?: boolean;
/** 下载完毕后,是否合并转换为 mp4 或 ts 文件。默认为 true */
convert?: boolean;
}

export interface WorkerTaskInfo {
Expand Down

0 comments on commit 7358a53

Please sign in to comment.