@lzwme/m3u8-dl 0.0.9 → 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/bin/m3u8dl.js CHANGED
File without changes
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());
@@ -7,4 +7,4 @@ export declare function localPlay(m3u8Info: TsItemInfo[], options: M3u8DLOptions
7
7
  origin: string;
8
8
  server: import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
9
9
  }>;
10
- export declare function toLocalM3u8(m3u8Info: TsItemInfo[], filepath: string, host?: string): Promise<void>;
10
+ export declare function toLocalM3u8(m3u8Info: TsItemInfo[], m3u8FilePath: string, host?: string): string;
@@ -18,13 +18,15 @@ async function localPlay(m3u8Info, options) {
18
18
  const cacheDirname = (0, node_path_1.basename)(cacheDir);
19
19
  const info = await createLocalServer((0, node_path_1.dirname)(cacheDir));
20
20
  const filename = (0, node_path_1.basename)(options.filename).slice(0, options.filename.lastIndexOf('.')) + `.m3u8`;
21
- await toLocalM3u8(m3u8Info, (0, node_path_1.resolve)(cacheDir, filename), `/${cacheDirname}`);
21
+ const cacheFilepath = (0, node_path_1.resolve)(cacheDir, filename);
22
+ if (!(0, node_fs_1.existsSync)(cacheFilepath))
23
+ toLocalM3u8(m3u8Info, cacheFilepath);
22
24
  const playUrl = `https://lzw.me/x/m3u8-player?url=${encodeURIComponent(`${info.origin}/${cacheDirname}/${filename}`)}`;
23
25
  const cmd = `${process.platform === 'win32' ? 'start' : 'open'} ${playUrl}`;
24
26
  (0, fe_utils_1.execSync)(cmd);
25
27
  return info;
26
28
  }
27
- async function toLocalM3u8(m3u8Info, filepath, host = '') {
29
+ function toLocalM3u8(m3u8Info, m3u8FilePath, host = '') {
28
30
  const m3u8ContentList = [
29
31
  `#EXTM3U`,
30
32
  `#EXT-X-VERSION:3`,
@@ -33,13 +35,21 @@ async function toLocalM3u8(m3u8Info, filepath, host = '') {
33
35
  `#EXT-X-MEDIA-SEQUENCE:0`,
34
36
  // `#EXT-X-KEY:METHOD=AES-128,URI="/api/aes/enc.key"`,
35
37
  ];
38
+ if (host && !host.endsWith('/'))
39
+ host += '/';
36
40
  m3u8Info.forEach(d => {
37
41
  if (d.tsOut)
38
- 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)}`);
39
43
  });
40
44
  m3u8ContentList.push(`#EXT-X-ENDLIST`);
41
45
  const m3u8Content = m3u8ContentList.join('\n');
42
- 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;
43
53
  }
44
54
  async function createLocalServer(baseDir) {
45
55
  baseDir = (0, node_path_1.resolve)(baseDir);
@@ -66,8 +76,8 @@ async function createLocalServer(baseDir) {
66
76
  return;
67
77
  }
68
78
  else if (stats.isDirectory()) {
69
- const html = (0, node_fs_1.readdirSync)(filename).map(filepath => {
70
- const rpath = (0, node_path_1.resolve)(filename, filepath).replace(baseDir, '');
79
+ const html = (0, node_fs_1.readdirSync)(filename).map(fname => {
80
+ const rpath = (0, node_path_1.resolve)(filename, fname).replace(baseDir, '');
71
81
  return `<li><a href="${rpath}">${rpath}</a></li>`;
72
82
  });
73
83
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
@@ -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,7 +35,14 @@ 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 '';
@@ -58,6 +58,7 @@ function formatOptions(url, opts) {
58
58
  async function m3u8InfoParse(url, options = {}) {
59
59
  [url, options] = formatOptions(url, options);
60
60
  const ext = (0, utils_1.isSupportFfmpeg)() ? '.mp4' : '.ts';
61
+ /** 最终合并转换后的文件路径 */
61
62
  let filepath = (0, node_path_1.resolve)(options.saveDir, options.filename);
62
63
  if (!filepath.endsWith(ext))
63
64
  filepath += ext;
@@ -68,7 +69,7 @@ async function m3u8InfoParse(url, options = {}) {
68
69
  }
69
70
  if (!options.force && (0, node_fs_1.existsSync)(filepath))
70
71
  return result;
71
- 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));
72
73
  if (m3u8Info && m3u8Info?.tsCount > 0)
73
74
  result.m3u8Info = m3u8Info;
74
75
  return result;
@@ -170,11 +171,14 @@ async function m3u8Download(url, options = {}) {
170
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)}`);
171
172
  }
172
173
  runTask(m3u8Info.data);
174
+ (0, local_play_1.toLocalM3u8)(m3u8Info.data, options.filename);
173
175
  await barrier.wait();
174
176
  if (stats.tsFailed === 0) {
175
- result.filepath = await (0, m3u8_convert_1.m3u8Convert)(options, m3u8Info.data);
176
- if (result.filepath && (0, node_fs_1.existsSync)(options.cacheDir) && options.delCache)
177
- (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
+ }
178
182
  }
179
183
  else
180
184
  utils_1.logger.warn('Download Failed! Please retry!', stats.tsFailed);
@@ -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;
@@ -3,17 +3,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
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);
@@ -71,7 +77,7 @@ async function parseM3U8(content, url = process.cwd(), cacheDir = './cache') {
71
77
  duration: tsList[i].duration,
72
78
  timeline: tsList[i].timeline,
73
79
  uri: tsList[i].uri,
74
- 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`,
75
81
  });
76
82
  result.durationSecond += tsList[i].duration;
77
83
  }
@@ -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,4 +1,5 @@
1
1
  import { NLogger, Request } from '@lzwme/fe-utils';
2
+ import { Stats } from 'node:fs';
2
3
  export declare const request: Request;
3
4
  export declare const getRetry: <T = string>(url: string, retries?: number) => Promise<{
4
5
  data: T;
@@ -8,3 +9,4 @@ export declare const getRetry: <T = string>(url: string, retries?: number) => Pr
8
9
  }>;
9
10
  export declare const logger: NLogger;
10
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
@@ -2,7 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.logger = exports.getRetry = exports.request = void 0;
4
4
  exports.isSupportFfmpeg = isSupportFfmpeg;
5
+ exports.findFiles = findFiles;
5
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");
6
9
  exports.request = new fe_utils_1.Request('', {
7
10
  'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
8
11
  });
@@ -23,3 +26,20 @@ function isSupportFfmpeg() {
23
26
  _isSupportFfmpeg = (0, fe_utils_1.execSync)('ffmpeg -version').stderr === '';
24
27
  return _isSupportFfmpeg;
25
28
  }
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?: 0 | 1 | boolean;
29
- remote?: boolean;
30
- }>;
31
- get api(): {
32
- url: string;
33
- desc?: string;
34
- enable?: 0 | 1 | boolean;
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?: 0 | 1 | boolean;
43
- remote?: boolean;
44
- }): Promise<VideoSearchResult>;
45
- getVideoList(ids: number | string | (number | string)[], api?: {
46
- url: string;
47
- desc?: string;
48
- enable?: 0 | 1 | boolean;
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,127 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VideoSearch = void 0;
4
3
  exports.VideoSerachAndDL = VideoSerachAndDL;
5
- const fe_utils_1 = require("@lzwme/fe-utils");
6
4
  const storage_js_1 = require("./storage.js");
7
5
  const utils_js_1 = require("./utils.js");
8
6
  const m3u8_batch_download_js_1 = require("../m3u8-batch-download.js");
9
7
  const enquirer_1 = require("enquirer");
10
8
  const console_log_colors_1 = require("console-log-colors");
11
- const req = new fe_utils_1.Request(null, {
12
- 'content-type': 'application/json; charset=UTF-8',
13
- });
14
- /**
15
- * @example
16
- * ```ts
17
- * const v = new VideoSearch({ api: ['https://api.xinlangapi.com/xinlangapi.php/provide/vod/'] });
18
- * v.search('三体')
19
- * .then(d => {
20
- * console.log(d.total, d.list);
21
- * return v.getVideoList(d.list[0].vod_id);
22
- * })
23
- * .then(d => {
24
- * console.log('detail:', d.total, d.list[0]);
25
- * });
26
- * ```
27
- */
28
- class VideoSearch {
29
- options;
30
- apiMap = new Map();
31
- get api() {
32
- return [...this.apiMap.values()].reverse();
33
- }
34
- constructor(options = {}) {
35
- this.options = options;
36
- if (!options.api?.length)
37
- options.api = [];
38
- if (process.env.VAPI)
39
- options.api.push(...process.env.VAPI.split('$$$'));
40
- this.updateOptions(options);
41
- }
42
- async updateOptions(options) {
43
- const cache = storage_js_1.stor.get();
44
- const apis = [...(cache.api || []), ...options.api];
45
- this.formatUrl(apis);
46
- if (options.api?.length)
47
- storage_js_1.stor.set({ api: apis });
48
- (cache.api || []).forEach(url => {
49
- this.apiMap.set(url, { url, desc: url });
50
- });
51
- await this.updateApiFromRemote(options.force);
52
- if (!this.apiMap.size)
53
- throw Error('没有可用的 API 站点,请添加或指定');
54
- return this;
55
- }
56
- async search(wd, api = this.api[0]) {
57
- let { data } = await req.get(api.url, { wd }, null, { rejectUnauthorized: false });
58
- if (typeof data == 'string')
59
- data = JSON.parse(data);
60
- return data;
61
- }
62
- async getVideoList(ids, api = this.api[0]) {
63
- let { data } = await req.get(api.url, {
64
- ac: 'videolist',
65
- ids: Array.isArray(ids) ? ids.join(',') : ids,
66
- }, null, { rejectUnauthorized: false });
67
- if (typeof data == 'string')
68
- data = JSON.parse(data);
69
- return data;
70
- }
71
- formatUrl(url) {
72
- const urls = [];
73
- if (!url)
74
- return urls;
75
- if (typeof url === 'string')
76
- url = [url];
77
- for (let u of url) {
78
- u = String(u || '').trim();
79
- if (u.startsWith('http')) {
80
- if (u.endsWith('provide/'))
81
- u += 'vod/';
82
- if (u.endsWith('provide/vod'))
83
- u += '/';
84
- urls.push(u.replace('/at/xml/', '/'));
85
- }
86
- }
87
- return [...new Set(urls)];
88
- }
89
- async loadRemoteConfig(force = false) {
90
- const cache = storage_js_1.stor.get();
91
- let needUpdate = true;
92
- if (!force && cache.remoteConfig?.updateTime) {
93
- needUpdate = Date.now() - cache.remoteConfig.updateTime > 1 * 60 * 60 * 1000;
94
- }
95
- if (needUpdate) {
96
- const url = this.options.remoteConfigUrl || 'https://mirror.ghproxy.com/raw.githubusercontent.com/lzwme/m3u8-dl/main/test/remote-config.json';
97
- const { data } = await req.get(url, null, { 'content-type': 'application/json' }, { rejectUnauthorized: false });
98
- utils_js_1.logger.debug('加载远程配置', data);
99
- if (Array.isArray(data.apiSites)) {
100
- storage_js_1.stor.set({
101
- remoteConfig: {
102
- updateTime: Date.now(),
103
- data,
104
- },
105
- });
106
- }
107
- }
108
- return cache.remoteConfig;
109
- }
110
- async updateApiFromRemote(force = false) {
111
- const remoteConfig = await this.loadRemoteConfig(force);
112
- if (Array.isArray(remoteConfig?.data?.apiSites)) {
113
- remoteConfig.data.apiSites.forEach(item => {
114
- if (item.enable === 0 || item.enable === false)
115
- return;
116
- item.url = this.formatUrl(item.url)[0];
117
- item.remote = true;
118
- this.apiMap.set(item.url, item);
119
- });
120
- }
121
- }
122
- }
123
- exports.VideoSearch = VideoSearch;
9
+ const ApiManage_1 = require("./search-api/ApiManage");
124
10
  async function VideoSerachAndDL(keyword, options, baseOpts) {
11
+ utils_js_1.logger.debug(options, baseOpts);
125
12
  const cache = storage_js_1.stor.get();
126
13
  const doDownload = async (info, urls) => {
127
14
  const p = await (0, enquirer_1.prompt)({
@@ -144,7 +31,7 @@ async function VideoSerachAndDL(keyword, options, baseOpts) {
144
31
  storage_js_1.stor.set({ latestSearchDL: null });
145
32
  }
146
33
  catch (error) {
147
- utils_js_1.logger.info('cachel download');
34
+ utils_js_1.logger.info('cancel download:', error.message);
148
35
  }
149
36
  };
150
37
  if (cache.latestSearchDL?.urls) {
@@ -161,19 +48,12 @@ async function VideoSerachAndDL(keyword, options, baseOpts) {
161
48
  storage_js_1.stor.set({ latestSearchDL: null });
162
49
  }
163
50
  }
164
- const vs = new VideoSearch();
165
- await vs.updateOptions({ api: options.url || [], force: baseOpts.force, remoteConfigUrl: options.remoteConfigUrl });
166
- const apis = vs.api;
167
- let apiUrl = options.url?.length ? { url: options.url[0] } : apis[0];
168
- if (!options.url && apis.length > 0) {
169
- await (0, enquirer_1.prompt)({
170
- type: 'select',
171
- name: 'k',
172
- message: '请选择 API 站点',
173
- choices: apis.map(d => ({ name: d.url, message: d.desc })),
174
- validate: value => value.length >= 1,
175
- }).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 }));
176
55
  }
56
+ await ApiManage_1.apiManage.select();
177
57
  await (0, enquirer_1.prompt)({
178
58
  type: 'input',
179
59
  name: 'k',
@@ -181,13 +61,13 @@ async function VideoSerachAndDL(keyword, options, baseOpts) {
181
61
  validate: value => value.length > 1,
182
62
  initial: keyword,
183
63
  }).then(v => (keyword = v.k));
184
- const sRes = await vs.search(keyword, apiUrl);
64
+ const sRes = await ApiManage_1.apiManage.search(keyword, ApiManage_1.apiManage.current);
185
65
  utils_js_1.logger.debug(sRes);
186
- if (!sRes.total) {
66
+ if (!sRes.length) {
187
67
  console.log(console_log_colors_1.color.green(`[${keyword}]`), `没有搜到结果`);
188
68
  return VideoSerachAndDL(keyword, options, baseOpts);
189
69
  }
190
- const choices = sRes.list.map((d, idx) => ({
70
+ const choices = sRes.map((d, idx) => ({
191
71
  name: d.vod_id,
192
72
  message: `${idx + 1}. [${d.type_name}] ${d.vod_name}`,
193
73
  hint: `${d.vod_remarks}(${d.vod_time})`,
@@ -196,14 +76,14 @@ async function VideoSerachAndDL(keyword, options, baseOpts) {
196
76
  type: 'select',
197
77
  name: 'vid',
198
78
  pointer: '👉',
199
- message: `查找到了 ${console_log_colors_1.color.greenBright(sRes.list.length)} 条结果,请选择:`,
79
+ message: `查找到了 ${console_log_colors_1.color.greenBright(sRes.length)} 条结果,请选择:`,
200
80
  choices: choices.concat({ name: -1, message: (0, console_log_colors_1.greenBright)('重新搜索'), hint: '' }),
201
81
  });
202
82
  if (answer1.vid === -1)
203
83
  return VideoSerachAndDL(keyword, options, baseOpts);
204
- const vResult = await vs.getVideoList(answer1.vid, apiUrl);
205
- if (!vResult.list?.length) {
206
- 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('获取视频信息失败!');
207
87
  return VideoSerachAndDL(keyword, options, baseOpts);
208
88
  }
209
89
  else {
@@ -45,6 +45,8 @@ export interface M3u8DLOptions {
45
45
  headers?: IncomingHttpHeaders;
46
46
  /** 下载时是否启动本地资源播放(边下边看) */
47
47
  play?: boolean;
48
+ /** 下载完毕后,是否合并转换为 mp4 或 ts 文件。默认为 true */
49
+ convert?: boolean;
48
50
  }
49
51
  export interface WorkerTaskInfo {
50
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.9",
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",
@@ -43,26 +43,26 @@
43
43
  "registry": "https://registry.npmjs.com"
44
44
  },
45
45
  "devDependencies": {
46
- "@eslint/js": "^9.8.0",
47
- "@lzwme/fed-lint-helper": "^2.6.2",
48
- "@types/node": "^22.0.0",
49
- "@typescript-eslint/eslint-plugin": "^7.18.0",
50
- "@typescript-eslint/parser": "^7.18.0",
51
- "eslint": "^9.8.0",
52
- "eslint-config-prettier": "^9.1.0",
53
- "eslint-plugin-prettier": "^5.2.1",
54
- "husky": "^9.1.4",
55
- "prettier": "^3.3.3",
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",
56
56
  "standard-version": "^9.5.0",
57
- "typescript": "^5.5.4",
58
- "typescript-eslint": "^7.18.0"
57
+ "typescript": "^5.8.3",
58
+ "typescript-eslint": "^8.32.0"
59
59
  },
60
60
  "dependencies": {
61
- "@lzwme/fe-utils": "^1.7.5",
62
- "commander": "^12.1.0",
61
+ "@lzwme/fe-utils": "^1.8.3",
62
+ "commander": "^13.1.0",
63
63
  "console-log-colors": "^0.5.0",
64
64
  "enquirer": "^2.4.1",
65
- "m3u8-parser": "^7.1.0"
65
+ "m3u8-parser": "^7.2.0"
66
66
  },
67
67
  "files": [
68
68
  "cjs",