Skip to content

Commit

Permalink
feat: 视频搜索增强缓存能力,支持缓存并继续最近一次未完成的下载
Browse files Browse the repository at this point in the history
  • Loading branch information
renxia committed Jun 29, 2023
1 parent 588809d commit 13a496a
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 142 deletions.
2 changes: 1 addition & 1 deletion README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
- 支持指定多个 m3u8 地址批量下载。
- 支持缓存续传。下载失败会保留缓存,重试时只下载失败的片段。
- 支持常见的 AES 加密视频流解密。
- 自动转换为 mp4。**需全局安装 ffmpeg**
- 自动转换为 mp4。**需全局安装 [ffmpeg](https://ffmpeg.org/download.html)**
- `[NEW!]`支持指定采集站标准 API,以命令行交互的方式搜索和下载。

## 安装(Install)
Expand Down
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,23 +45,23 @@
},
"devDependencies": {
"@lzwme/fed-lint-helper": "^2.3.2",
"@types/node": "^20.2.0",
"@typescript-eslint/eslint-plugin": "^5.59.6",
"@typescript-eslint/parser": "^5.59.6",
"eslint": "^8.40.0",
"@types/node": "^20.3.2",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.3",
"prettier": "^2.8.8",
"standard-version": "^9.5.0",
"typescript": "^5.0.4"
"typescript": "^5.1.6"
},
"dependencies": {
"@lzwme/fe-utils": "^1.5.1",
"commander": "^10.0.1",
"commander": "^11.0.0",
"console-log-colors": "^0.4.0",
"enquirer": "^2.3.6",
"m3u8-parser": "^6.1.0"
"m3u8-parser": "^6.2.0"
},
"files": [
"cjs",
Expand Down
123 changes: 6 additions & 117 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
import { resolve } from 'node:path';
import { program } from 'commander';
import { cyanBright, color, greenBright, gray, green } from 'console-log-colors';
import { cyanBright } from 'console-log-colors';
import { PackageJson, readJsonFileSync } from '@lzwme/fe-utils';
import { prompt } from 'enquirer';
import { logger } from './lib/utils.js';
import { m3u8BatchDownload } from './m3u8-batch-download';
import type { M3u8DLOptions } from './types/m3u8';
import { VideoSearch } from './lib/video-search.js';

interface POptions extends M3u8DLOptions {
silent?: boolean;
progress?: boolean;
}
import type { CliOptions } from './types/m3u8';
import { VideoSerachAndDL } from './lib/video-search.js';

const pkg = readJsonFileSync<PackageJson>(resolve(__dirname, '../package.json'));

process.on('unhandledRejection', (r, p) => {
console.log('[退出]UnhandledPromiseRejection', r, p);
console.log('[退出][unhandledRejection]', r, p);
process.exit();
});

process.on('SIGINT', signal => {
logger.info('强制退出', signal);
logger.info('[SIGINT]强制退出', signal);
process.exit();
});

Expand Down Expand Up @@ -63,7 +57,7 @@ program
program.parse(process.argv);

function getOptions() {
const options = program.opts<POptions>();
const options = program.opts<CliOptions>();
if (options.debug) {
logger.updateOptions({ levelType: 'debug' });
} else if (options.silent) {
Expand All @@ -72,108 +66,3 @@ function getOptions() {
}
return options;
}

async function VideoSerachAndDL(keyword: string, options: { url?: string[] }, baseOpts: POptions): Promise<void> {
const vs = new VideoSearch();
await vs.updateOptions({ api: options.url || [], force: baseOpts.force });
const apis = vs.api;
let apiUrl = apis[0];

if (!options.url && vs.api.length > 0) {
await prompt<{ k: string }>({
type: 'select',
name: 'k',
message: '请选择 API 站点',
choices: apis.map(d => ({ name: d.url, message: d.desc })) as never,
validate: value => value.length >= 1,
}).then(v => (apiUrl = vs.apiMap.get(v.k)));
}

await prompt<{ k: string }>({
type: 'input',
name: 'k',
message: '请输入关键字',
validate: value => value.length > 1,
initial: keyword,
}).then(v => (keyword = v.k));

const sRes = await vs.search(keyword, apiUrl);
logger.debug(sRes);
if (!sRes.total) {
console.log(color.green(`[${keyword}]`), `没有搜到结果`);
return VideoSerachAndDL(keyword, options, baseOpts);
}

const choices = sRes.list.map((d, idx) => ({
name: d.vod_id,
message: `${idx + 1}. [${d.type_name}] ${d.vod_name}`,
hint: `${d.vod_remarks}(${d.vod_time})`,
}));
const answer1 = await prompt<{ vid: number }>({
type: 'select',
name: 'vid',
pointer: '👉',
message: `查找到了 ${color.greenBright(sRes.list.length)} 条结果,请选择:`,
choices: choices.concat({ name: -1, message: greenBright('重新搜索'), hint: '' }) as never,
} as never);

if (answer1.vid === -1) return VideoSerachAndDL(keyword, options, baseOpts);

const vResult = await vs.getVideoList(answer1.vid, apiUrl);
if (!vResult.list?.length) {
logger.error('获取视频信息失败!', vResult.msg);
return VideoSerachAndDL(keyword, options, baseOpts);
} else {
const info = vResult.list[0];
const urls = info.vod_play_url
.split(info.vod_play_note)
.find(d => d.includes('.m3u8'))
.split('#');

logger.debug(info, urls);
const r = (key: keyof typeof info, desc: string) => (info[key] ? ` [${desc}] ${greenBright(info[key])}` : '');
console.log(
[
`\n [名称] ${cyanBright(info.vod_name)}`,
r('vod_sub', '别名'),
` [更新] ${greenBright(info.vod_remarks)}(${gray(info.vod_time)})`,
r('vod_total', '总集数'),
r('type_name', '分类'),
r('vod_class', '类别'),
r('vod_writer', '作者'),
r('vod_area', '地区'),
r('vod_lang', '语言'),
r('vod_year', '年份'),
r('vod_douban_score', '评分'),
r('vod_pubdate', '上映日期'),
`\n${green((info.vod_content || info.vod_blurb).replace(/<\/?.+?>/g, ''))}\n`, // 描述
]
.filter(Boolean)
.join('\n'),
'\n'
);

const answer = await prompt<{ url: string }>({
type: 'select',
name: 'url',
choices: [
{ name: '1', message: green('全部下载') },
{ name: '-1', message: cyanBright('重新搜索') },
].concat(urls.map((d, i) => ({ name: d, message: `${i + 1}. ${d}` }))),
message: `获取到了 ${color.magentaBright(urls.length)} 条视频下载地址,请选择:`,
});

if (answer.url !== '-1') {
const p = await prompt<{ play: boolean }>({
type: 'confirm',
name: 'play',
initial: baseOpts.play,
message: `【${greenBright(info.vod_name)}】是否边下边播?`,
});
baseOpts.play = p.play;
await m3u8BatchDownload(answer.url === '1' ? urls : [answer.url], { filename: info.vod_name.replaceAll(' ', '_'), ...baseOpts });
}

return VideoSerachAndDL(keyword, options, baseOpts);
}
}
13 changes: 12 additions & 1 deletion src/lib/storage.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { LiteStorage } from '@lzwme/fe-utils';
import { type VSOptions } from './video-search';
import { resolve } from 'node:path';
import { homedir } from 'node:os';
import { type VSOptions } from './video-search';
import type { M3u8DLOptions } from '../types';

export interface M3u8StorConfig extends VSOptions {
/** 播放地址缓存 */
api?: string[];
/** 远程加载的配置信息 */
remoteConfig?: {
/** 最近一次更新的时间。默认缓存1小时 */
updateTime?: number;
/** 远程配置缓存 */
data?: {
apiSites: {
url: string;
Expand All @@ -17,6 +21,13 @@ export interface M3u8StorConfig extends VSOptions {
}[];
};
};
/** 最近一次搜索下载的信息缓存 */
latestSearchDL?: {
keyword: string;
name: string;
urls: string[];
dlOptions: M3u8DLOptions;
};
}

export const stor = LiteStorage.getInstance<M3u8StorConfig>({ uuid: 'm3u8dl', filepath: resolve(homedir(), '.liteStorage/m3u8dl.json') });
Loading

0 comments on commit 13a496a

Please sign in to comment.