@lzwme/m3u8-dl 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cjs/cli.js CHANGED
@@ -30,6 +30,7 @@ commander_1.program
30
30
  .option('-C, --cache-dir <dirpath>', `临时文件保存目录。默认为 cache`)
31
31
  .option('-S, --save-dir <dirpath>', `下载文件保存的路径。默认为当前目录`)
32
32
  .option('--no-del-cache', `下载成功后是否删除临时文件。默认为 true。保存临时文件可以在重复下载时识别缓存`)
33
+ .option('--no-convert', '下载成功后,是否不合并转换为 mp4 文件。默认为 true。')
33
34
  .action(async (urls) => {
34
35
  const options = getOptions();
35
36
  utils_js_1.logger.debug(urls, options);
@@ -45,7 +46,8 @@ commander_1.program
45
46
  .command('search [keyword]')
46
47
  .alias('s')
47
48
  .option('-u,--url <api...>', '影视搜索的接口地址(m3u8采集站标准接口)')
48
- .option('-R,--remote-config-url <url>', '自定义远程配置加载地址。默认从主仓库配置读取')
49
+ .option('-d, --apidir <dirpath>', '指定自定义视频搜索 api 所在的目录或具体路径')
50
+ // .option('-R,--remote-config-url <url>', '自定义远程配置加载地址。默认从主仓库配置读取')
49
51
  .description('m3u8视频在线搜索与下载')
50
52
  .action(async (keyword, options) => {
51
53
  await (0, video_search_js_1.VideoSerachAndDL)(keyword, options, getOptions());
@@ -1,4 +1,3 @@
1
- /// <reference types="node" />
2
1
  import { M3u8DLOptions, TsItemInfo } from '../types/m3u8';
3
2
  /**
4
3
  * 边下边看
@@ -8,4 +7,4 @@ export declare function localPlay(m3u8Info: TsItemInfo[], options: M3u8DLOptions
8
7
  origin: string;
9
8
  server: import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
10
9
  }>;
11
- export declare function toLocalM3u8(m3u8Info: TsItemInfo[], filepath: string, host?: string): Promise<void>;
10
+ export declare function toLocalM3u8(m3u8Info: TsItemInfo[], m3u8FilePath: string, host?: string): string;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.toLocalM3u8 = exports.localPlay = void 0;
3
+ exports.localPlay = localPlay;
4
+ exports.toLocalM3u8 = toLocalM3u8;
4
5
  const fe_utils_1 = require("@lzwme/fe-utils");
5
6
  const console_log_colors_1 = require("console-log-colors");
6
7
  const node_fs_1 = require("node:fs");
@@ -14,16 +15,18 @@ async function localPlay(m3u8Info, options) {
14
15
  if (!m3u8Info?.length)
15
16
  return null;
16
17
  const cacheDir = (0, node_path_1.dirname)(m3u8Info[0].tsOut);
17
- const info = await createLocalServer(cacheDir);
18
+ const cacheDirname = (0, node_path_1.basename)(cacheDir);
19
+ const info = await createLocalServer((0, node_path_1.dirname)(cacheDir));
18
20
  const filename = (0, node_path_1.basename)(options.filename).slice(0, options.filename.lastIndexOf('.')) + `.m3u8`;
19
- await toLocalM3u8(m3u8Info, (0, node_path_1.resolve)(cacheDir, filename), info.origin);
20
- const playUrl = `https://lzw.me/x/m3u8-player?url=${encodeURIComponent(`${info.origin}/${filename}`)}`;
21
+ const cacheFilepath = (0, node_path_1.resolve)(cacheDir, filename);
22
+ if (!(0, node_fs_1.existsSync)(cacheFilepath))
23
+ toLocalM3u8(m3u8Info, cacheFilepath);
24
+ const playUrl = `https://lzw.me/x/m3u8-player?url=${encodeURIComponent(`${info.origin}/${cacheDirname}/${filename}`)}`;
21
25
  const cmd = `${process.platform === 'win32' ? 'start' : 'open'} ${playUrl}`;
22
26
  (0, fe_utils_1.execSync)(cmd);
23
27
  return info;
24
28
  }
25
- exports.localPlay = localPlay;
26
- async function toLocalM3u8(m3u8Info, filepath, host = '') {
29
+ function toLocalM3u8(m3u8Info, m3u8FilePath, host = '') {
27
30
  const m3u8ContentList = [
28
31
  `#EXTM3U`,
29
32
  `#EXT-X-VERSION:3`,
@@ -32,16 +35,24 @@ async function toLocalM3u8(m3u8Info, filepath, host = '') {
32
35
  `#EXT-X-MEDIA-SEQUENCE:0`,
33
36
  // `#EXT-X-KEY:METHOD=AES-128,URI="/api/aes/enc.key"`,
34
37
  ];
38
+ if (host && !host.endsWith('/'))
39
+ host += '/';
35
40
  m3u8Info.forEach(d => {
36
41
  if (d.tsOut)
37
- m3u8ContentList.push(`#EXTINF:${Number(d.duration).toFixed(6)},`, `${host}/${(0, node_path_1.basename)(d.tsOut)}`);
42
+ m3u8ContentList.push(`#EXTINF:${Number(d.duration).toFixed(6)},`, `${host}${(0, node_path_1.basename)(d.tsOut)}`);
38
43
  });
39
44
  m3u8ContentList.push(`#EXT-X-ENDLIST`);
40
45
  const m3u8Content = m3u8ContentList.join('\n');
41
- await node_fs_1.promises.writeFile(filepath, m3u8Content, 'utf8');
46
+ const ext = (0, node_path_1.extname)(m3u8FilePath);
47
+ if (ext !== '.m3u8')
48
+ m3u8FilePath = m3u8FilePath.replace(ext, '') + '.m3u8';
49
+ m3u8FilePath = (0, node_path_1.resolve)((0, node_path_1.dirname)(m3u8Info[0].tsOut), m3u8FilePath);
50
+ (0, fe_utils_1.mkdirp)((0, node_path_1.dirname)(m3u8FilePath));
51
+ (0, node_fs_1.writeFileSync)(m3u8FilePath, m3u8Content, 'utf8');
52
+ return m3u8FilePath;
42
53
  }
43
- exports.toLocalM3u8 = toLocalM3u8;
44
54
  async function createLocalServer(baseDir) {
55
+ baseDir = (0, node_path_1.resolve)(baseDir);
45
56
  const port = await (0, fe_utils_1.findFreePort)();
46
57
  const origin = `http://localhost:${port}`;
47
58
  const server = (0, node_http_1.createServer)((req, res) => {
@@ -64,6 +75,15 @@ async function createLocalServer(baseDir) {
64
75
  (0, node_fs_1.createReadStream)(filename).pipe(res);
65
76
  return;
66
77
  }
78
+ else if (stats.isDirectory()) {
79
+ const html = (0, node_fs_1.readdirSync)(filename).map(fname => {
80
+ const rpath = (0, node_path_1.resolve)(filename, fname).replace(baseDir, '');
81
+ return `<li><a href="${rpath}">${rpath}</a></li>`;
82
+ });
83
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
84
+ res.end(`<ol>${html.join('')}</ol>`);
85
+ return;
86
+ }
67
87
  }
68
88
  res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
69
89
  res.end('Not found');
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.m3u8Convert = void 0;
3
+ exports.m3u8Convert = m3u8Convert;
4
4
  const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
6
  const fe_utils_1 = require("@lzwme/fe-utils");
@@ -19,7 +19,7 @@ async function m3u8Convert(options, data) {
19
19
  let filesAllArr = data.map(d => (0, node_path_1.resolve)(d.tsOut)).filter(d => (0, node_fs_1.existsSync)(d));
20
20
  if (process.platform === 'win32')
21
21
  filesAllArr = filesAllArr.map(d => d.replaceAll('\\', '/'));
22
- await node_fs_1.promises.writeFile(inputFilePath, 'ffconcat version 1.0\nfile ' + filesAllArr.join('\nfile '));
22
+ (0, node_fs_1.writeFileSync)(inputFilePath, 'ffconcat version 1.0\nfile ' + filesAllArr.join('\nfile '));
23
23
  let headersString = '';
24
24
  if (options.headers) {
25
25
  for (const [key, value] of Object.entries(options.headers)) {
@@ -35,11 +35,17 @@ async function m3u8Convert(options, data) {
35
35
  }
36
36
  if (!ffmpegSupport) {
37
37
  filepath = filepath.replace(/\.mp4$/, '.ts');
38
- await node_fs_1.promises.writeFile(filepath, Buffer.concat(data.map(d => (0, node_fs_1.readFileSync)(d.tsOut))));
38
+ const filteWriteStream = (0, node_fs_1.createWriteStream)(filepath);
39
+ for (const d of data) {
40
+ const err = await new Promise(rs => {
41
+ filteWriteStream.write((0, node_fs_1.readFileSync)(d.tsOut), e => rs(e));
42
+ });
43
+ if (err)
44
+ utils_1.logger.error(`Write file failed: ${d.tsOut}`, err);
45
+ }
39
46
  }
40
47
  if (!(0, node_fs_1.existsSync)(filepath))
41
48
  return '';
42
49
  utils_1.logger.info(`File saved[${(0, console_log_colors_1.magentaBright)((0, fe_utils_1.formatByteSize)((0, node_fs_1.statSync)(filepath).size))}]:`, (0, console_log_colors_1.greenBright)(filepath));
43
50
  return filepath;
44
51
  }
45
- exports.m3u8Convert = m3u8Convert;
@@ -1,4 +1,5 @@
1
1
  import { WorkerPool } from './worker_pool';
2
+ import { parseM3U8 } from './parseM3u8';
2
3
  import type { M3u8DLOptions, TsItemInfo, WorkerTaskInfo } from '../types/m3u8';
3
4
  export declare const workPoll: WorkerPool<WorkerTaskInfo, {
4
5
  success: boolean;
@@ -7,12 +8,6 @@ export declare const workPoll: WorkerPool<WorkerTaskInfo, {
7
8
  export declare function preDownLoad(url: string, options: M3u8DLOptions): Promise<void>;
8
9
  export declare function m3u8Download(url: string, options?: M3u8DLOptions): Promise<{
9
10
  options: M3u8DLOptions;
10
- m3u8Info: {
11
- manifest: any;
12
- tsCount: number;
13
- durationSecond: number;
14
- data: TsItemInfo[];
15
- crypto: import("../types/m3u8").M3u8Crypto;
16
- };
11
+ m3u8Info: Awaited<ReturnType<typeof parseM3U8>> | null;
17
12
  filepath: string;
18
13
  }>;
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.m3u8Download = exports.preDownLoad = exports.workPoll = void 0;
3
+ exports.workPoll = void 0;
4
+ exports.preDownLoad = preDownLoad;
5
+ exports.m3u8Download = m3u8Download;
4
6
  const node_path_1 = require("node:path");
5
7
  const node_fs_1 = require("node:fs");
6
8
  const node_os_1 = require("node:os");
@@ -56,6 +58,7 @@ function formatOptions(url, opts) {
56
58
  async function m3u8InfoParse(url, options = {}) {
57
59
  [url, options] = formatOptions(url, options);
58
60
  const ext = (0, utils_1.isSupportFfmpeg)() ? '.mp4' : '.ts';
61
+ /** 最终合并转换后的文件路径 */
59
62
  let filepath = (0, node_path_1.resolve)(options.saveDir, options.filename);
60
63
  if (!filepath.endsWith(ext))
61
64
  filepath += ext;
@@ -66,7 +69,7 @@ async function m3u8InfoParse(url, options = {}) {
66
69
  }
67
70
  if (!options.force && (0, node_fs_1.existsSync)(filepath))
68
71
  return result;
69
- const m3u8Info = await (0, parseM3u8_1.parseM3U8)('', url, options.cacheDir).catch(e => utils_1.logger.error('[parseM3U8][failed]', e));
72
+ const m3u8Info = await (0, parseM3u8_1.parseM3U8)(url, options.cacheDir).catch(e => utils_1.logger.error('[parseM3U8][failed]', e));
70
73
  if (m3u8Info && m3u8Info?.tsCount > 0)
71
74
  result.m3u8Info = m3u8Info;
72
75
  return result;
@@ -86,7 +89,6 @@ async function preDownLoad(url, options) {
86
89
  }
87
90
  }
88
91
  }
89
- exports.preDownLoad = preDownLoad;
90
92
  async function m3u8Download(url, options = {}) {
91
93
  utils_1.logger.info('Starting download for', (0, console_log_colors_1.cyanBright)(url));
92
94
  const result = await m3u8InfoParse(url, options);
@@ -169,11 +171,14 @@ async function m3u8Download(url, options = {}) {
169
171
  console.info(`\nTotal segments: ${(0, console_log_colors_1.cyan)(m3u8Info.tsCount)}, duration: ${(0, console_log_colors_1.green)(m3u8Info.durationSecond + 'sec')}.`, `Parallel jobs: ${(0, console_log_colors_1.magenta)(options.threadNum)}`);
170
172
  }
171
173
  runTask(m3u8Info.data);
174
+ (0, local_play_1.toLocalM3u8)(m3u8Info.data, options.filename);
172
175
  await barrier.wait();
173
176
  if (stats.tsFailed === 0) {
174
- result.filepath = await (0, m3u8_convert_1.m3u8Convert)(options, m3u8Info.data);
175
- if (result.filepath && (0, node_fs_1.existsSync)(options.cacheDir) && options.delCache)
176
- (0, fe_utils_1.rmrfAsync)(options.cacheDir);
177
+ if (options.convert !== false) {
178
+ result.filepath = await (0, m3u8_convert_1.m3u8Convert)(options, m3u8Info.data);
179
+ if (result.filepath && (0, node_fs_1.existsSync)(options.cacheDir) && options.delCache)
180
+ (0, fe_utils_1.rmrfAsync)(options.cacheDir);
181
+ }
177
182
  }
178
183
  else
179
184
  utils_1.logger.warn('Download Failed! Please retry!', stats.tsFailed);
@@ -181,4 +186,3 @@ async function m3u8Download(url, options = {}) {
181
186
  utils_1.logger.debug('Done!', url, result.m3u8Info);
182
187
  return result;
183
188
  }
184
- exports.m3u8Download = m3u8Download;
@@ -1,5 +1,10 @@
1
1
  import type { M3u8Crypto, TsItemInfo } from '../types/m3u8';
2
- export declare function parseM3U8(content: string, url?: string, cacheDir?: string): Promise<{
2
+ /**
3
+ * 解析 m3u8 文件
4
+ * @param content m3u8 文件的内容,可为 http 远程地址、本地文件路径
5
+ * @param cacheDir 缓存文件保存目录
6
+ */
7
+ export declare function parseM3U8(content: string, cacheDir?: string): Promise<{
3
8
  manifest: any;
4
9
  /** ts 文件数量 */
5
10
  tsCount: number;
@@ -1,19 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parseM3U8 = void 0;
3
+ exports.parseM3U8 = parseM3U8;
4
4
  const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
+ const fe_utils_1 = require("@lzwme/fe-utils");
6
7
  const m3u8_parser_1 = require("m3u8-parser");
7
8
  const utils_1 = require("./utils");
8
- async function parseM3U8(content, url = process.cwd(), cacheDir = './cache') {
9
- if (!content && url) {
10
- if (!url.startsWith('http') && (0, node_fs_1.existsSync)(url)) {
11
- url = (0, node_path_1.resolve)(process.cwd(), url);
12
- content = await node_fs_1.promises.readFile(url, 'utf8');
13
- }
14
- else {
15
- content = (await (0, utils_1.getRetry)(url)).data;
16
- }
9
+ /**
10
+ * 解析 m3u8 文件
11
+ * @param content m3u8 文件的内容,可为 http 远程地址、本地文件路径
12
+ * @param cacheDir 缓存文件保存目录
13
+ */
14
+ async function parseM3U8(content, cacheDir = './cache') {
15
+ let url = process.cwd();
16
+ if (content.startsWith('http')) {
17
+ url = content;
18
+ content = (await (0, utils_1.getRetry)(url)).data;
19
+ }
20
+ else if (!content.includes('\n') && (0, node_fs_1.existsSync)(content)) {
21
+ url = (0, node_path_1.resolve)(process.cwd(), content);
22
+ content = await node_fs_1.promises.readFile(url, 'utf8');
17
23
  }
18
24
  if (!content) {
19
25
  utils_1.logger.error('获取播放列表为空!', url);
@@ -54,8 +60,9 @@ async function parseM3U8(content, url = process.cwd(), cacheDir = './cache') {
54
60
  if (tsKeyInfo?.uri) {
55
61
  if (tsKeyInfo.method)
56
62
  result.crypto.method = tsKeyInfo.method.toUpperCase();
57
- if (tsKeyInfo.iv)
58
- result.crypto.iv = new Uint8Array(Buffer.from(tsKeyInfo.iv));
63
+ if (tsKeyInfo.iv) {
64
+ result.crypto.iv = typeof tsKeyInfo.iv === 'string' ? new Uint8Array(Buffer.from(tsKeyInfo.iv)) : tsKeyInfo.iv;
65
+ }
59
66
  result.crypto.uri = tsKeyInfo.uri.includes('://') ? tsKeyInfo.uri : new URL(tsKeyInfo.uri, url).toString();
60
67
  }
61
68
  if (result.crypto.uri !== '') {
@@ -70,12 +77,11 @@ async function parseM3U8(content, url = process.cwd(), cacheDir = './cache') {
70
77
  duration: tsList[i].duration,
71
78
  timeline: tsList[i].timeline,
72
79
  uri: tsList[i].uri,
73
- tsOut: `${cacheDir}/${i}-${(0, node_path_1.basename)(tsList[i].uri).replace(/\.ts\?.+/, '.ts')}`,
80
+ tsOut: `${cacheDir}/${(0, fe_utils_1.md5)(tsList[i].uri)}.ts`,
74
81
  });
75
82
  result.durationSecond += tsList[i].duration;
76
83
  }
77
84
  result.durationSecond = +Number(result.durationSecond).toFixed(2);
78
85
  return result;
79
86
  }
80
- exports.parseM3U8 = parseM3U8;
81
87
  // parseM3U8('', 't.m3u8').then(d => console.log(d));
@@ -0,0 +1,30 @@
1
+ import type { SearchApi, VideoSearchResult } from '../../types';
2
+ export declare const apiManage: {
3
+ api: Map<string | number, SearchApi>;
4
+ current: SearchApi;
5
+ load(apidir?: string, force?: boolean): void;
6
+ /** 添加 API 到列表中 */
7
+ add(sApi: SearchApi | {
8
+ api: string;
9
+ desc?: string;
10
+ enable?: boolean;
11
+ key?: string | number;
12
+ }, force?: boolean): void;
13
+ /** API 有效性校验 */
14
+ validate(sApi: SearchApi, desc?: string): sApi is SearchApi;
15
+ /** 选择一个 API */
16
+ select(): Promise<void>;
17
+ search(wd: string, api?: SearchApi): Promise<{
18
+ api_key: number | string;
19
+ vod_id: number;
20
+ vod_name: string;
21
+ vod_en?: string;
22
+ vod_time?: string;
23
+ vod_remarks?: string;
24
+ vod_play_from?: string;
25
+ vod_play_url: string;
26
+ type_name?: string;
27
+ type_id?: number;
28
+ }[]>;
29
+ detail(info: VideoSearchResult["list"][0]): Promise<import("../../types").VideoDetailsResult>;
30
+ };
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.apiManage = void 0;
4
+ const enquirer_1 = require("enquirer");
5
+ const utils_1 = require("../utils");
6
+ const CommSearchApi_1 = require("./CommSearchApi");
7
+ exports.apiManage = {
8
+ api: new Map(),
9
+ current: null,
10
+ load(apidir, force = false) {
11
+ const files = (0, utils_1.findFiles)(apidir, (filepath, s) => !s.isFile() || /\.c?js/.test(filepath));
12
+ for (const filepath of files) {
13
+ /* eslint-disable @typescript-eslint/no-var-requires */
14
+ const sApi = require(filepath);
15
+ this.add(sApi.default || sApi, force);
16
+ }
17
+ },
18
+ /** 添加 API 到列表中 */
19
+ add(sApi, force = false) {
20
+ if (Array.isArray(sApi))
21
+ return sApi.forEach(d => this.add(d));
22
+ if (sApi.api?.startsWith('http') && !('search' in sApi))
23
+ sApi = new CommSearchApi_1.CommSearchApi(sApi);
24
+ if (this.validate(sApi) && (force || !this.api.has(sApi.key))) {
25
+ this.api.set(sApi.key, sApi);
26
+ utils_1.logger.debug('添加Api:', sApi.desc || sApi.key);
27
+ }
28
+ },
29
+ /** API 有效性校验 */
30
+ validate(sApi, desc) {
31
+ if (!sApi)
32
+ return false;
33
+ const requiredKeys = ['enable', 'key', 'search', 'detail'];
34
+ if (!sApi.key)
35
+ sApi.key = sApi.desc;
36
+ for (const key of requiredKeys) {
37
+ if (!(key in sApi)) {
38
+ utils_1.logger.warn(`【API校验不通过】${desc} 缺少关键属性 ${key}`);
39
+ return false;
40
+ }
41
+ if ((key === 'search' || key === 'detail') && typeof sApi[key] !== 'function')
42
+ return false;
43
+ }
44
+ return sApi.enable !== false;
45
+ },
46
+ /** 选择一个 API */
47
+ async select() {
48
+ if (!this.api.size) {
49
+ utils_1.logger.error('没有可用的 API,请配置或指定 url、apidir 参数');
50
+ process.exit(-1);
51
+ }
52
+ if (this.api.size === 1) {
53
+ this.current = [...this.api.values()][0];
54
+ return;
55
+ }
56
+ const apis = [...this.api.values()];
57
+ const v = await (0, enquirer_1.prompt)({
58
+ type: 'select',
59
+ name: 'k',
60
+ message: '请选择 API 站点',
61
+ choices: apis.map(d => ({ name: String(d.key), message: d.desc })),
62
+ validate: value => value.length >= 1,
63
+ });
64
+ this.current = apis.find(d => String(d.key) === v.k);
65
+ },
66
+ async search(wd, api) {
67
+ const result = [];
68
+ try {
69
+ if (api)
70
+ return (await api.search(wd)).list;
71
+ for (api of this.api.values()) {
72
+ const r = await api.search(wd);
73
+ if (Array.isArray(r.list)) {
74
+ r.list.forEach(d => {
75
+ d.api_key = api.key;
76
+ result.push(d);
77
+ });
78
+ }
79
+ }
80
+ }
81
+ catch (error) {
82
+ utils_1.logger.error('搜索失败!', error.message);
83
+ }
84
+ return result;
85
+ },
86
+ detail(info) {
87
+ const api = this.api.get(info.api_key) || this.current;
88
+ return api.detail(info.vod_id);
89
+ },
90
+ };
@@ -0,0 +1,44 @@
1
+ import type { SearchApi, VideoDetailsResult, VideoSearchResult } from '../../types';
2
+ import { type M3u8StorConfig } from '../storage.js';
3
+ export interface VSOptions {
4
+ /** 采集站地址 */
5
+ api?: string;
6
+ /** 站点描述 */
7
+ desc?: string;
8
+ /** 是否启用 */
9
+ enable?: 0 | 1 | boolean;
10
+ }
11
+ /**
12
+ * 基于采集站点 API 的通用搜索
13
+ * @example
14
+ * ```ts
15
+ * const v = new CommSearchApi({ api: 'https://api.xinlangapi.com/xinlangapi.php/provide/vod/' });
16
+ * v.search('三体')
17
+ * .then(d => {
18
+ * console.log(d.total, d.list);
19
+ * return v.getVideoList(d.list[0].vod_id);
20
+ * })
21
+ * .then(d => {
22
+ * console.log('detail:', d.total, d.list[0]);
23
+ * });
24
+ * ```
25
+ */
26
+ export declare class CommSearchApi implements SearchApi {
27
+ protected options: VSOptions;
28
+ protected currentUrl: M3u8StorConfig['remoteConfig']['data']['apiSites'][0];
29
+ apiMap: Map<string, {
30
+ url: string;
31
+ desc?: string;
32
+ enable?: 0 | 1 | boolean;
33
+ remote?: boolean;
34
+ }>;
35
+ get desc(): string;
36
+ get key(): string;
37
+ get enable(): boolean;
38
+ constructor(options?: VSOptions);
39
+ search(wd: string, api?: string): Promise<VideoSearchResult>;
40
+ detail(id: string | number, api?: string): Promise<VideoDetailsResult>;
41
+ /** 按 id 取列表(每一项中包含了更为详细的内容) */
42
+ getVideoList(ids: number | string | (number | string)[], api?: string): Promise<VideoDetailsResult>;
43
+ private formatUrl;
44
+ }
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CommSearchApi = void 0;
4
+ const fe_utils_1 = require("@lzwme/fe-utils");
5
+ const req = new fe_utils_1.Request(null, {
6
+ 'content-type': 'application/json; charset=UTF-8',
7
+ });
8
+ /**
9
+ * 基于采集站点 API 的通用搜索
10
+ * @example
11
+ * ```ts
12
+ * const v = new CommSearchApi({ api: 'https://api.xinlangapi.com/xinlangapi.php/provide/vod/' });
13
+ * v.search('三体')
14
+ * .then(d => {
15
+ * console.log(d.total, d.list);
16
+ * return v.getVideoList(d.list[0].vod_id);
17
+ * })
18
+ * .then(d => {
19
+ * console.log('detail:', d.total, d.list[0]);
20
+ * });
21
+ * ```
22
+ */
23
+ class CommSearchApi {
24
+ options;
25
+ currentUrl;
26
+ apiMap = new Map();
27
+ get desc() {
28
+ return this.options.desc || this.options.api;
29
+ }
30
+ get key() {
31
+ return this.options.api;
32
+ }
33
+ get enable() {
34
+ return this.options.api && this.options.enable !== false;
35
+ }
36
+ constructor(options = {}) {
37
+ this.options = options;
38
+ if (options.api)
39
+ options.api = this.formatUrl(options.api)[0];
40
+ this.options = options;
41
+ }
42
+ async search(wd, api = this.options.api) {
43
+ let { data } = await req.get(api, { wd }, null, { rejectUnauthorized: false });
44
+ if (typeof data == 'string')
45
+ data = JSON.parse(data);
46
+ return data;
47
+ }
48
+ async detail(id, api = this.options.api) {
49
+ return this.getVideoList(id, api);
50
+ }
51
+ /** 按 id 取列表(每一项中包含了更为详细的内容) */
52
+ async getVideoList(ids, api = this.options.api) {
53
+ let { data } = await req.get(api, {
54
+ ac: 'videolist',
55
+ ids: Array.isArray(ids) ? ids.join(',') : ids,
56
+ }, null, { rejectUnauthorized: false });
57
+ if (typeof data == 'string')
58
+ data = JSON.parse(data);
59
+ return data;
60
+ }
61
+ formatUrl(url) {
62
+ const urls = [];
63
+ if (!url)
64
+ return urls;
65
+ if (typeof url === 'string')
66
+ url = [url];
67
+ for (let u of url) {
68
+ u = String(u || '').trim();
69
+ if (u.startsWith('http')) {
70
+ if (u.endsWith('provide/'))
71
+ u += 'vod/';
72
+ if (u.endsWith('provide/vod'))
73
+ u += '/';
74
+ urls.push(u.replace('/at/xml/', '/'));
75
+ }
76
+ }
77
+ return [...new Set(urls)];
78
+ }
79
+ }
80
+ exports.CommSearchApi = CommSearchApi;
@@ -1,10 +1,12 @@
1
1
  import { LiteStorage } from '@lzwme/fe-utils';
2
- import { type VSOptions } from './video-search';
3
2
  import type { M3u8DLOptions, VideoDetails } from '../types';
4
- export interface M3u8StorConfig extends VSOptions {
3
+ export interface M3u8StorConfig {
5
4
  /** 播放地址缓存 */
6
5
  api?: string[];
7
- /** 远程加载的配置信息 */
6
+ /**
7
+ * 远程加载的配置信息
8
+ * @deprecated
9
+ */
8
10
  remoteConfig?: {
9
11
  /** 最近一次更新的时间。默认缓存1小时 */
10
12
  updateTime?: number;
@@ -22,7 +24,7 @@ export interface M3u8StorConfig extends VSOptions {
22
24
  latestSearchDL?: {
23
25
  keyword: string;
24
26
  urls: string[];
25
- info: VideoDetails;
27
+ info: Partial<VideoDetails>;
26
28
  dlOptions: M3u8DLOptions;
27
29
  };
28
30
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.tsDownload = void 0;
3
+ exports.tsDownload = tsDownload;
4
4
  const node_crypto_1 = require("node:crypto");
5
5
  const node_fs_1 = require("node:fs");
6
6
  const node_path_1 = require("node:path");
@@ -27,10 +27,15 @@ async function tsDownload(info, cryptoInfo) {
27
27
  }
28
28
  return false;
29
29
  }
30
- exports.tsDownload = tsDownload;
31
30
  function aesDecrypt(data, cryptoInfo) {
32
- const decipher = (0, node_crypto_1.createDecipheriv)((cryptoInfo.method + '-cbc').toLocaleLowerCase(), cryptoInfo.key, cryptoInfo.iv);
33
- return Buffer.concat([decipher.update(Buffer.isBuffer(data) ? data : Buffer.from(data)), decipher.final()]);
31
+ try {
32
+ const decipher = (0, node_crypto_1.createDecipheriv)((cryptoInfo.method + '-cbc').toLocaleLowerCase(), cryptoInfo.key, cryptoInfo.iv);
33
+ return Buffer.concat([decipher.update(Buffer.isBuffer(data) ? data : Buffer.from(data)), decipher.final()]);
34
+ }
35
+ catch (err) {
36
+ console.log('aesDecrypt err:', err);
37
+ throw err;
38
+ }
34
39
  }
35
40
  if (!node_worker_threads_1.isMainThread && node_worker_threads_1.parentPort) {
36
41
  node_worker_threads_1.parentPort.on('message', (data) => {
@@ -1,6 +1,5 @@
1
- /// <reference types="node" />
2
- /// <reference types="node" />
3
1
  import { NLogger, Request } from '@lzwme/fe-utils';
2
+ import { Stats } from 'node:fs';
4
3
  export declare const request: Request;
5
4
  export declare const getRetry: <T = string>(url: string, retries?: number) => Promise<{
6
5
  data: T;
@@ -10,3 +9,4 @@ export declare const getRetry: <T = string>(url: string, retries?: number) => Pr
10
9
  }>;
11
10
  export declare const logger: NLogger;
12
11
  export declare function isSupportFfmpeg(): boolean;
12
+ export declare function findFiles(apidir?: string, validate?: (filepath: string, stat: Stats) => boolean): string[];
package/cjs/lib/utils.js CHANGED
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isSupportFfmpeg = exports.logger = exports.getRetry = exports.request = void 0;
3
+ exports.logger = exports.getRetry = exports.request = void 0;
4
+ exports.isSupportFfmpeg = isSupportFfmpeg;
5
+ exports.findFiles = findFiles;
4
6
  const fe_utils_1 = require("@lzwme/fe-utils");
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
5
9
  exports.request = new fe_utils_1.Request('', {
6
10
  'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
7
11
  });
@@ -22,4 +26,20 @@ function isSupportFfmpeg() {
22
26
  _isSupportFfmpeg = (0, fe_utils_1.execSync)('ffmpeg -version').stderr === '';
23
27
  return _isSupportFfmpeg;
24
28
  }
25
- exports.isSupportFfmpeg = isSupportFfmpeg;
29
+ function findFiles(apidir, validate) {
30
+ const files = [];
31
+ if (apidir && (0, node_fs_1.existsSync)(apidir)) {
32
+ const stat = (0, node_fs_1.statSync)(apidir);
33
+ if (!validate || validate(apidir, stat)) {
34
+ if (stat.isFile()) {
35
+ files.push((0, node_path_1.resolve)(apidir));
36
+ }
37
+ else if (stat.isDirectory()) {
38
+ (0, node_fs_1.readdirSync)(apidir).forEach(filename => {
39
+ findFiles((0, node_path_1.resolve)(apidir, filename)).forEach(f => files.push(f));
40
+ });
41
+ }
42
+ }
43
+ }
44
+ return files;
45
+ }
@@ -1,58 +1,6 @@
1
- import type { VideoListResult, VideoSearchResult, CliOptions } from '../types';
2
- export interface VSOptions {
3
- /** 播放地址缓存 */
4
- api?: string[];
5
- force?: boolean;
6
- /** 远程配置的请求地址 */
7
- remoteConfigUrl?: string;
8
- }
9
- /**
10
- * @example
11
- * ```ts
12
- * const v = new VideoSearch({ api: ['https://api.xinlangapi.com/xinlangapi.php/provide/vod/'] });
13
- * v.search('三体')
14
- * .then(d => {
15
- * console.log(d.total, d.list);
16
- * return v.getVideoList(d.list[0].vod_id);
17
- * })
18
- * .then(d => {
19
- * console.log('detail:', d.total, d.list[0]);
20
- * });
21
- * ```
22
- */
23
- export declare class VideoSearch {
24
- protected options: VSOptions;
25
- apiMap: Map<string, {
26
- url: string;
27
- desc?: string;
28
- enable?: boolean | 0 | 1; /** 播放地址缓存 */
29
- remote?: boolean;
30
- }>;
31
- get api(): {
32
- url: string;
33
- desc?: string;
34
- enable?: boolean | 0 | 1; /** 播放地址缓存 */
35
- remote?: boolean;
36
- }[];
37
- constructor(options?: VSOptions);
38
- updateOptions(options: VSOptions): Promise<this>;
39
- search(wd: string, api?: {
40
- url: string;
41
- desc?: string;
42
- enable?: boolean | 0 | 1; /** 播放地址缓存 */
43
- remote?: boolean;
44
- }): Promise<VideoSearchResult>;
45
- getVideoList(ids: number | string | (number | string)[], api?: {
46
- url: string;
47
- desc?: string;
48
- enable?: boolean | 0 | 1; /** 播放地址缓存 */
49
- remote?: boolean;
50
- }): Promise<VideoListResult>;
51
- private formatUrl;
52
- private loadRemoteConfig;
53
- updateApiFromRemote(force?: boolean): Promise<void>;
54
- }
1
+ import type { CliOptions } from '../types';
55
2
  export declare function VideoSerachAndDL(keyword: string, options: {
56
3
  url?: string[];
4
+ apidir?: string;
57
5
  remoteConfigUrl?: string;
58
6
  }, baseOpts: CliOptions): Promise<void>;
@@ -1,126 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VideoSerachAndDL = exports.VideoSearch = void 0;
4
- const fe_utils_1 = require("@lzwme/fe-utils");
3
+ exports.VideoSerachAndDL = VideoSerachAndDL;
5
4
  const storage_js_1 = require("./storage.js");
6
5
  const utils_js_1 = require("./utils.js");
7
6
  const m3u8_batch_download_js_1 = require("../m3u8-batch-download.js");
8
7
  const enquirer_1 = require("enquirer");
9
8
  const console_log_colors_1 = require("console-log-colors");
10
- const req = new fe_utils_1.Request(null, {
11
- 'content-type': 'application/json; charset=UTF-8',
12
- });
13
- /**
14
- * @example
15
- * ```ts
16
- * const v = new VideoSearch({ api: ['https://api.xinlangapi.com/xinlangapi.php/provide/vod/'] });
17
- * v.search('三体')
18
- * .then(d => {
19
- * console.log(d.total, d.list);
20
- * return v.getVideoList(d.list[0].vod_id);
21
- * })
22
- * .then(d => {
23
- * console.log('detail:', d.total, d.list[0]);
24
- * });
25
- * ```
26
- */
27
- class VideoSearch {
28
- options;
29
- apiMap = new Map();
30
- get api() {
31
- return [...this.apiMap.values()].reverse();
32
- }
33
- constructor(options = {}) {
34
- this.options = options;
35
- if (!options.api?.length)
36
- options.api = [];
37
- if (process.env.VAPI)
38
- options.api.push(...process.env.VAPI.split('$$$'));
39
- this.updateOptions(options);
40
- }
41
- async updateOptions(options) {
42
- const cache = storage_js_1.stor.get();
43
- const apis = [...(cache.api || []), ...options.api];
44
- this.formatUrl(apis);
45
- if (options.api?.length)
46
- storage_js_1.stor.set({ api: apis });
47
- (cache.api || []).forEach(url => {
48
- this.apiMap.set(url, { url, desc: url });
49
- });
50
- await this.updateApiFromRemote(options.force);
51
- if (!this.apiMap.size)
52
- throw Error('没有可用的 API 站点,请添加或指定');
53
- return this;
54
- }
55
- async search(wd, api = this.api[0]) {
56
- let { data } = await req.get(api.url, { wd }, null, { rejectUnauthorized: false });
57
- if (typeof data == 'string')
58
- data = JSON.parse(data);
59
- return data;
60
- }
61
- async getVideoList(ids, api = this.api[0]) {
62
- let { data } = await req.get(api.url, {
63
- ac: 'videolist',
64
- ids: Array.isArray(ids) ? ids.join(',') : ids,
65
- }, null, { rejectUnauthorized: false });
66
- if (typeof data == 'string')
67
- data = JSON.parse(data);
68
- return data;
69
- }
70
- formatUrl(url) {
71
- const urls = [];
72
- if (!url)
73
- return urls;
74
- if (typeof url === 'string')
75
- url = [url];
76
- for (let u of url) {
77
- u = String(u || '').trim();
78
- if (u.startsWith('http')) {
79
- if (u.endsWith('provide/'))
80
- u += 'vod/';
81
- if (u.endsWith('provide/vod'))
82
- u += '/';
83
- urls.push(u.replace('/at/xml/', '/'));
84
- }
85
- }
86
- return [...new Set(urls)];
87
- }
88
- async loadRemoteConfig(force = false) {
89
- const cache = storage_js_1.stor.get();
90
- let needUpdate = true;
91
- if (!force && cache.remoteConfig?.updateTime) {
92
- needUpdate = Date.now() - cache.remoteConfig.updateTime > 1 * 60 * 60 * 1000;
93
- }
94
- if (needUpdate) {
95
- const url = this.options.remoteConfigUrl || 'https://mirror.ghproxy.com/raw.githubusercontent.com/lzwme/m3u8-dl/main/test/remote-config.json';
96
- const { data } = await req.get(url, null, { 'content-type': 'application/json' }, { rejectUnauthorized: false });
97
- utils_js_1.logger.debug('加载远程配置', data);
98
- if (Array.isArray(data.apiSites)) {
99
- storage_js_1.stor.set({
100
- remoteConfig: {
101
- updateTime: Date.now(),
102
- data,
103
- },
104
- });
105
- }
106
- }
107
- return cache.remoteConfig;
108
- }
109
- async updateApiFromRemote(force = false) {
110
- const remoteConfig = await this.loadRemoteConfig(force);
111
- if (Array.isArray(remoteConfig?.data?.apiSites)) {
112
- remoteConfig.data.apiSites.forEach(item => {
113
- if (item.enable === 0 || item.enable === false)
114
- return;
115
- item.url = this.formatUrl(item.url)[0];
116
- item.remote = true;
117
- this.apiMap.set(item.url, item);
118
- });
119
- }
120
- }
121
- }
122
- exports.VideoSearch = VideoSearch;
9
+ const ApiManage_1 = require("./search-api/ApiManage");
123
10
  async function VideoSerachAndDL(keyword, options, baseOpts) {
11
+ utils_js_1.logger.debug(options, baseOpts);
124
12
  const cache = storage_js_1.stor.get();
125
13
  const doDownload = async (info, urls) => {
126
14
  const p = await (0, enquirer_1.prompt)({
@@ -143,7 +31,7 @@ async function VideoSerachAndDL(keyword, options, baseOpts) {
143
31
  storage_js_1.stor.set({ latestSearchDL: null });
144
32
  }
145
33
  catch (error) {
146
- utils_js_1.logger.info('cachel download');
34
+ utils_js_1.logger.info('cancel download:', error.message);
147
35
  }
148
36
  };
149
37
  if (cache.latestSearchDL?.urls) {
@@ -160,19 +48,12 @@ async function VideoSerachAndDL(keyword, options, baseOpts) {
160
48
  storage_js_1.stor.set({ latestSearchDL: null });
161
49
  }
162
50
  }
163
- const vs = new VideoSearch();
164
- await vs.updateOptions({ api: options.url || [], force: baseOpts.force, remoteConfigUrl: options.remoteConfigUrl });
165
- const apis = vs.api;
166
- let apiUrl = options.url?.length ? { url: options.url[0] } : apis[0];
167
- if (!options.url && apis.length > 0) {
168
- await (0, enquirer_1.prompt)({
169
- type: 'select',
170
- name: 'k',
171
- message: '请选择 API 站点',
172
- choices: apis.map(d => ({ name: d.url, message: d.desc })),
173
- validate: value => value.length >= 1,
174
- }).then(v => (apiUrl = apis.find(d => d.url === v.k)));
51
+ if (options.apidir && !ApiManage_1.apiManage.current)
52
+ ApiManage_1.apiManage.load(options.apidir);
53
+ if (options.url) {
54
+ options.url.forEach(api => ApiManage_1.apiManage.add({ api, desc: api }));
175
55
  }
56
+ await ApiManage_1.apiManage.select();
176
57
  await (0, enquirer_1.prompt)({
177
58
  type: 'input',
178
59
  name: 'k',
@@ -180,13 +61,13 @@ async function VideoSerachAndDL(keyword, options, baseOpts) {
180
61
  validate: value => value.length > 1,
181
62
  initial: keyword,
182
63
  }).then(v => (keyword = v.k));
183
- const sRes = await vs.search(keyword, apiUrl);
64
+ const sRes = await ApiManage_1.apiManage.search(keyword, ApiManage_1.apiManage.current);
184
65
  utils_js_1.logger.debug(sRes);
185
- if (!sRes.total) {
66
+ if (!sRes.length) {
186
67
  console.log(console_log_colors_1.color.green(`[${keyword}]`), `没有搜到结果`);
187
68
  return VideoSerachAndDL(keyword, options, baseOpts);
188
69
  }
189
- const choices = sRes.list.map((d, idx) => ({
70
+ const choices = sRes.map((d, idx) => ({
190
71
  name: d.vod_id,
191
72
  message: `${idx + 1}. [${d.type_name}] ${d.vod_name}`,
192
73
  hint: `${d.vod_remarks}(${d.vod_time})`,
@@ -195,14 +76,14 @@ async function VideoSerachAndDL(keyword, options, baseOpts) {
195
76
  type: 'select',
196
77
  name: 'vid',
197
78
  pointer: '👉',
198
- message: `查找到了 ${console_log_colors_1.color.greenBright(sRes.list.length)} 条结果,请选择:`,
79
+ message: `查找到了 ${console_log_colors_1.color.greenBright(sRes.length)} 条结果,请选择:`,
199
80
  choices: choices.concat({ name: -1, message: (0, console_log_colors_1.greenBright)('重新搜索'), hint: '' }),
200
81
  });
201
82
  if (answer1.vid === -1)
202
83
  return VideoSerachAndDL(keyword, options, baseOpts);
203
- const vResult = await vs.getVideoList(answer1.vid, apiUrl);
204
- if (!vResult.list?.length) {
205
- utils_js_1.logger.error('获取视频信息失败!', vResult.msg);
84
+ const vResult = await ApiManage_1.apiManage.detail(sRes.find(d => d.vod_id == answer1.vid));
85
+ if (!vResult) {
86
+ utils_js_1.logger.error('获取视频信息失败!');
206
87
  return VideoSerachAndDL(keyword, options, baseOpts);
207
88
  }
208
89
  else {
@@ -258,4 +139,3 @@ async function VideoSerachAndDL(keyword, options, baseOpts) {
258
139
  return VideoSerachAndDL(keyword, options, baseOpts);
259
140
  }
260
141
  }
261
- exports.VideoSerachAndDL = VideoSerachAndDL;
@@ -1,4 +1,3 @@
1
- /// <reference types="node" />
2
1
  import { EventEmitter } from 'node:events';
3
2
  export declare class WorkerPool<T = unknown, R = unknown> extends EventEmitter {
4
3
  private processorFile;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.m3u8BatchDownload = void 0;
3
+ exports.m3u8BatchDownload = m3u8BatchDownload;
4
4
  const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
6
  const m3u8_download_1 = require("./lib/m3u8-download");
@@ -63,4 +63,3 @@ async function m3u8BatchDownload(urls, options) {
63
63
  return d;
64
64
  });
65
65
  }
66
- exports.m3u8BatchDownload = m3u8BatchDownload;
@@ -1,6 +1,3 @@
1
- /// <reference path="../../global.d.ts" />
2
- /// <reference types="node" />
3
- /// <reference types="node" />
4
1
  import type { IncomingHttpHeaders } from 'node:http';
5
2
  export interface TsItemInfo {
6
3
  /** ts 文件次序 */
@@ -48,6 +45,8 @@ export interface M3u8DLOptions {
48
45
  headers?: IncomingHttpHeaders;
49
46
  /** 下载时是否启动本地资源播放(边下边看) */
50
47
  play?: boolean;
48
+ /** 下载完毕后,是否合并转换为 mp4 或 ts 文件。默认为 true */
49
+ convert?: boolean;
51
50
  }
52
51
  export interface WorkerTaskInfo {
53
52
  info: TsItemInfo;
package/cjs/types/m3u8.js CHANGED
@@ -1,4 +1,11 @@
1
1
  "use strict";
2
+ /*
3
+ * @Author: renxia
4
+ * @Date: 2024-08-02 09:58:56
5
+ * @LastEditors: renxia
6
+ * @LastEditTime: 2024-08-02 17:15:07
7
+ * @Description:
8
+ */
2
9
  /* eslint-disable @typescript-eslint/triple-slash-reference */
3
10
  /// <reference path="../../global.d.ts"/>
4
11
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,122 +1,132 @@
1
- /** 模糊搜索返回的结果(?wd=<wd>) */
2
- export interface VideoSearchResult {
3
- code: number;
4
- msg: string;
5
- page: number;
6
- pagecount: number;
7
- limit: string;
1
+ import { AnyObject } from '@lzwme/fe-utils';
2
+ export interface SearchApiResult<T> {
3
+ /** api 来源 */
4
+ api_key?: number | string;
5
+ code?: number;
6
+ msg?: string;
7
+ page?: number;
8
+ pagecount?: number;
9
+ limit?: string;
8
10
  total: number;
9
- list: VodList[];
10
- class: {
11
+ list: T;
12
+ }
13
+ /** 模糊搜索返回的结果(?wd=<wd>) */
14
+ export interface VideoSearchResult extends SearchApiResult<VodList[]> {
15
+ class?: {
11
16
  type_id: number;
12
17
  type_pid: number;
13
18
  type_name: string;
14
19
  }[];
15
20
  }
16
- interface VodList {
17
- vod_id: number;
18
- vod_name: string;
19
- type_id: number;
20
- type_name: string;
21
- vod_en: string;
22
- vod_time: string;
23
- vod_remarks: string;
24
- vod_play_from: string;
25
- vod_play_url: string;
26
- }
21
+ type VodList = Pick<VideoDetails, 'api_key' | 'vod_id' | 'vod_name' | 'vod_en' | 'vod_time' | 'vod_remarks' | 'vod_play_from' | 'vod_play_url' | 'type_name' | 'type_id'>;
27
22
  /** 按 id 搜素返回的详情列表 */
28
- export interface VideoListResult {
29
- code: number;
30
- msg: string;
31
- page: number;
32
- pagecount: number;
33
- limit: string;
34
- total: number;
35
- list: VideoDetails[];
36
- }
23
+ export type VideoDetailsResult = SearchApiResult<VideoDetails[]>;
37
24
  export interface VideoDetails {
25
+ /** api 来源 */
26
+ api_key: number | string;
27
+ /** 视频 id,可用于查询详情 */
38
28
  vod_id: number;
39
- type_id: number;
40
- type_id_1: number;
41
- group_id: number;
29
+ /** 视频名称 */
42
30
  vod_name: string;
43
- vod_sub: string;
44
- vod_en: string;
45
- vod_status: number;
46
- vod_letter: string;
47
- vod_color: string;
48
- vod_tag: string;
49
- vod_class: string;
50
- vod_pic: string;
51
- vod_pic_thumb: string;
52
- vod_pic_slide: string;
53
- vod_pic_screenshot: null;
54
- vod_actor: string;
55
- vod_director: string;
56
- vod_writer: string;
57
- vod_behind: string;
58
- vod_blurb: string;
59
- vod_remarks: string;
60
- vod_pubdate: string;
61
- vod_total: number;
62
- vod_serial: string;
63
- vod_tv: string;
64
- vod_weekday: string;
65
- vod_area: string;
66
- vod_lang: string;
67
- vod_year: string;
68
- vod_version: string;
69
- vod_state: string;
70
- vod_author: string;
71
- vod_jumpurl: string;
72
- vod_tpl: string;
73
- vod_tpl_play: string;
74
- vod_tpl_down: string;
75
- vod_isend: number;
76
- vod_lock: number;
77
- vod_level: number;
78
- vod_copyright: number;
79
- vod_points: number;
80
- vod_points_play: number;
81
- vod_points_down: number;
82
- vod_hits: number;
83
- vod_hits_day: number;
84
- vod_hits_week: number;
85
- vod_hits_month: number;
86
- vod_duration: string;
87
- vod_up: number;
88
- vod_down: number;
89
- vod_score: string;
90
- vod_score_all: number;
91
- vod_score_num: number;
92
- vod_time: string;
93
- vod_time_add: number;
94
- vod_time_hits: number;
95
- vod_time_make: number;
96
- vod_trysee: number;
97
- vod_douban_id: number;
98
- vod_douban_score: string;
99
- vod_reurl: string;
100
- vod_rel_vod: string;
101
- vod_rel_art: string;
102
- vod_pwd: string;
103
- vod_pwd_url: string;
104
- vod_pwd_play: string;
105
- vod_pwd_play_url: string;
106
- vod_pwd_down: string;
107
- vod_pwd_down_url: string;
108
- vod_content: string;
109
- vod_play_from: string;
110
- vod_play_server: string;
111
- vod_play_note: string;
31
+ /** 播放地址 */
112
32
  vod_play_url: string;
113
- vod_down_from: string;
114
- vod_down_server: string;
115
- vod_down_note: string;
116
- vod_down_url: string;
117
- vod_plot: number;
118
- vod_plot_name: string;
119
- vod_plot_detail: string;
120
- type_name: string;
33
+ /** 分类 id */
34
+ type_id?: number;
35
+ /** 分类名称 */
36
+ type_name?: string;
37
+ vod_en?: string;
38
+ vod_time?: string;
39
+ vod_remarks?: string;
40
+ vod_play_from?: string;
41
+ type_id_1?: number;
42
+ group_id?: number;
43
+ vod_sub?: string;
44
+ vod_status?: number;
45
+ vod_letter?: string;
46
+ vod_color?: string;
47
+ vod_tag?: string;
48
+ vod_class?: string;
49
+ vod_pic?: string;
50
+ vod_pic_thumb?: string;
51
+ vod_pic_slide?: string;
52
+ vod_pic_screenshot: null;
53
+ vod_actor?: string;
54
+ vod_director?: string;
55
+ vod_writer?: string;
56
+ vod_behind?: string;
57
+ vod_blurb?: string;
58
+ vod_pubdate?: string;
59
+ vod_total?: number;
60
+ vod_serial?: string;
61
+ vod_tv?: string;
62
+ vod_weekday?: string;
63
+ vod_area?: string;
64
+ vod_lang?: string;
65
+ vod_year?: string;
66
+ vod_version?: string;
67
+ vod_state?: string;
68
+ vod_author?: string;
69
+ vod_jumpurl?: string;
70
+ vod_tpl?: string;
71
+ vod_tpl_play?: string;
72
+ vod_tpl_down?: string;
73
+ vod_isend?: number;
74
+ vod_lock?: number;
75
+ vod_level?: number;
76
+ vod_copyright?: number;
77
+ vod_points?: number;
78
+ vod_points_play?: number;
79
+ vod_points_down?: number;
80
+ vod_hits?: number;
81
+ vod_hits_day?: number;
82
+ vod_hits_week?: number;
83
+ vod_hits_month?: number;
84
+ vod_duration?: string;
85
+ vod_up?: number;
86
+ vod_down?: number;
87
+ /** 评分 */
88
+ vod_score?: string;
89
+ vod_score_all?: number;
90
+ vod_score_num?: number;
91
+ vod_time_add?: number;
92
+ vod_time_hits?: number;
93
+ vod_time_make?: number;
94
+ vod_trysee?: number;
95
+ /** 在豆瓣的 id */
96
+ vod_douban_id?: number;
97
+ /** 豆瓣评分 */
98
+ vod_douban_score?: string;
99
+ vod_reurl?: string;
100
+ vod_rel_vod?: string;
101
+ vod_rel_art?: string;
102
+ vod_pwd?: string;
103
+ vod_pwd_url?: string;
104
+ vod_pwd_play?: string;
105
+ vod_pwd_play_url?: string;
106
+ vod_pwd_down?: string;
107
+ vod_pwd_down_url?: string;
108
+ vod_content?: string;
109
+ vod_play_server?: string;
110
+ vod_play_note?: string;
111
+ vod_down_from?: string;
112
+ vod_down_server?: string;
113
+ vod_down_note?: string;
114
+ vod_down_url?: string;
115
+ vod_plot?: number;
116
+ vod_plot_name?: string;
117
+ vod_plot_detail?: string;
118
+ }
119
+ /** 搜索Api的格式 */
120
+ export interface SearchApi extends AnyObject {
121
+ /** API 唯一标记 */
122
+ key: string | number;
123
+ /** API 描述 */
124
+ desc?: string;
125
+ /** 是否启用 */
126
+ enable?: boolean;
127
+ /** 按关键字搜索列表 */
128
+ search(wd: string, ...args: unknown[]): Promise<VideoSearchResult>;
129
+ /** 按 id 获取某个视频的详情 */
130
+ detail(id: string | number, ...args: unknown[]): Promise<VideoDetailsResult>;
121
131
  }
122
132
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzwme/m3u8-dl",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "Batch download of m3u8 files and convert to mp4",
5
5
  "main": "cjs/index.js",
6
6
  "types": "cjs/index.d.ts",
@@ -35,7 +35,6 @@
35
35
  "download",
36
36
  "ffmpeg"
37
37
  ],
38
- "packageManager": "pnpm@8.0.0",
39
38
  "engines": {
40
39
  "node": ">=14.18"
41
40
  },
@@ -44,26 +43,26 @@
44
43
  "registry": "https://registry.npmjs.com"
45
44
  },
46
45
  "devDependencies": {
47
- "@eslint/js": "^9.0.0",
48
- "@lzwme/fed-lint-helper": "^2.6.0",
49
- "@types/node": "^20.12.7",
50
- "@typescript-eslint/eslint-plugin": "^7.7.0",
51
- "@typescript-eslint/parser": "^7.7.0",
52
- "eslint": "^9.1.0",
53
- "eslint-config-prettier": "^9.1.0",
54
- "eslint-plugin-prettier": "^5.1.3",
55
- "husky": "^9.0.11",
56
- "prettier": "^3.2.5",
46
+ "@eslint/js": "^9.26.0",
47
+ "@lzwme/fed-lint-helper": "^2.6.6",
48
+ "@types/node": "^22.15.12",
49
+ "@typescript-eslint/eslint-plugin": "^8.32.0",
50
+ "@typescript-eslint/parser": "^8.32.0",
51
+ "eslint": "^9.26.0",
52
+ "eslint-config-prettier": "^10.1.2",
53
+ "eslint-plugin-prettier": "^5.4.0",
54
+ "husky": "^9.1.7",
55
+ "prettier": "^3.5.3",
57
56
  "standard-version": "^9.5.0",
58
- "typescript": "^5.4.5",
59
- "typescript-eslint": "^7.6.0"
57
+ "typescript": "^5.8.3",
58
+ "typescript-eslint": "^8.32.0"
60
59
  },
61
60
  "dependencies": {
62
- "@lzwme/fe-utils": "^1.7.2",
63
- "commander": "^12.0.0",
64
- "console-log-colors": "^0.4.0",
61
+ "@lzwme/fe-utils": "^1.8.3",
62
+ "commander": "^13.1.0",
63
+ "console-log-colors": "^0.5.0",
65
64
  "enquirer": "^2.4.1",
66
- "m3u8-parser": "^7.1.0"
65
+ "m3u8-parser": "^7.2.0"
67
66
  },
68
67
  "files": [
69
68
  "cjs",