From 588809d78efdbc6ce8a8c5df85e36de2ae0a5152 Mon Sep 17 00:00:00 2001 From: renxia Date: Wed, 28 Jun 2023 18:55:42 +0800 Subject: [PATCH] =?UTF-8?q?wip:=20=E6=96=B0=E5=A2=9E=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E9=87=87=E9=9B=86=E7=AB=99=E8=BF=9C=E7=A8=8B=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E4=BB=8E=E8=BF=9C=E7=A8=8B=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cli.ts | 13 ++++--- src/lib/storage.ts | 11 ++++++ src/lib/video-search.ts | 85 +++++++++++++++++++++++++++++++---------- test/remote-config.json | 19 +++++++++ 4 files changed, 102 insertions(+), 26 deletions(-) create mode 100644 test/remote-config.json diff --git a/src/cli.ts b/src/cli.ts index 1220e28..f8eec53 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -33,7 +33,7 @@ program .option('--debug', `开启调试模式。`) .option('-f, --filename ', `指定下载文件的保存名称。默认取 url md5 值。若指定了多个 url 地址,则会在末尾增加序号`) .option('-n, --thread-num ', `并发下载线程数。默认为 cpu * 2。可设置不同数值观察下载效果`) - .option('-F, --force', `文件已存在时,是否仍继续下载和生成`) + .option('-F, --force', `启用强制执行模式。文件已存在时,是否仍继续下载和生成`) .option('--no-progress', `是否不打印进度信息`) .option('-p, --play', `是否边下边看`) .option('-C, --cache-dir ', `临时文件保存目录。默认为 cache`) @@ -54,6 +54,7 @@ program .command('search [keyword]') .alias('s') .option('-u,--url ', '影视搜索的接口地址(m3u8采集站标准接口)') + .option('-F, --force', `是否强制`) .description('m3u8视频在线搜索与下载') .action(async (keyword, options: { url?: string[] }) => { VideoSerachAndDL(keyword, options, getOptions()); @@ -74,18 +75,18 @@ function getOptions() { async function VideoSerachAndDL(keyword: string, options: { url?: string[] }, baseOpts: POptions): Promise { const vs = new VideoSearch(); - await vs.updateOptions({ api: options.url || [] }); - let apiUrl = vs.api[0]; + 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: vs.api.map(d => ({ name: d, message: d })) as never, - + choices: apis.map(d => ({ name: d.url, message: d.desc })) as never, validate: value => value.length >= 1, - }).then(v => (apiUrl = v.k)); + }).then(v => (apiUrl = vs.apiMap.get(v.k))); } await prompt<{ k: string }>({ diff --git a/src/lib/storage.ts b/src/lib/storage.ts index 09a0ae4..d9f5f81 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -6,6 +6,17 @@ import { homedir } from 'node:os'; export interface M3u8StorConfig extends VSOptions { /** 播放地址缓存 */ api?: string[]; + remoteConfig?: { + updateTime?: number; + data?: { + apiSites: { + url: string; + desc?: string; + enable?: 0 | 1 | boolean; + remote?: boolean; + }[]; + }; + }; } export const stor = LiteStorage.getInstance({ uuid: 'm3u8dl', filepath: resolve(homedir(), '.liteStorage/m3u8dl.json') }); diff --git a/src/lib/video-search.ts b/src/lib/video-search.ts index 3560cd8..ca2604b 100644 --- a/src/lib/video-search.ts +++ b/src/lib/video-search.ts @@ -1,6 +1,7 @@ import { Request } from '@lzwme/fe-utils'; -import { VideoListResult, VideoSearchResult } from '../types'; -import { stor } from './storage'; +import type { VideoListResult, VideoSearchResult } from '../types'; +import { stor, type M3u8StorConfig } from './storage.js'; +import { logger } from './utils.js'; const req = new Request(null, { 'content-type': 'application/json; charset=UTF-8', @@ -9,31 +10,41 @@ const req = new Request(null, { export interface VSOptions { /** 播放地址缓存 */ api?: string[]; + force?: boolean; } export class VideoSearch { + public apiMap = new Map(); public get api() { - return this.options.api; + return [...this.apiMap.values()]; } constructor(protected options: VSOptions = {}) { if (!options.api?.length) options.api = []; if (process.env.VAPI) options.api.push(...process.env.VAPI.split('$$$')); this.updateOptions(options).then(() => { - if (!this.api.length) throw Error('没有可用站点,请添加或指定'); + if (!this.apiMap.size) throw Error('没有可用站点,请添加或指定'); }); } async updateOptions(options: VSOptions) { const cache = stor.get(); - if (Array.isArray(cache.api)) this.options.api.push(...cache.api); + const apis = [...(cache.api || []), ...options.api]; + + await this.formatUrl(apis); + if (options.api?.length) { - this.options.api.unshift(...options.api); - this.options.api = await this.formatUrl(this.options.api); - stor.set({ api: this.options.api }); + stor.set({ api: [...(cache.api || []), ...options.api] }); } + + (cache.api || []).forEach(url => { + this.apiMap.set(url, { url, desc: url }); + }); + + await this.updateApiFromRemote(options.force); + return this; } async search(wd: string, api = this.api[0]) { - let { data } = await req.get(api, { wd }, null, { rejectUnauthorized: false }); + let { data } = await req.get(api.url, { wd }, null, { rejectUnauthorized: false }); if (typeof data == 'string') data = JSON.parse(data) as VideoSearchResult; @@ -41,7 +52,7 @@ export class VideoSearch { } async getVideoList(ids: number | string | (number | string)[], api = this.api[0]) { let { data } = await req.get( - api, + api.url, { ac: 'videolist', ids: Array.isArray(ids) ? ids.join(',') : ids, @@ -54,22 +65,15 @@ export class VideoSearch { return data; } - async formatUrl(url: string | string[]) { + private formatUrl(url: string | string[]) { const urls: string[] = []; if (!url) return urls; if (typeof url === 'string') url = [url]; for (let u of url) { u = String(u || '').trim(); - if (!u) continue; - if (u.endsWith('.json')) { - const { data } = await req.get>(u, null, null, { rejectUnauthorized: false }); - if (Array.isArray(data)) { - urls.push(...(await this.formatUrl(data as string[]))); - } else { - urls.push(...Object.values(data)); - } - } else if (u.startsWith('http')) { + + if (u.startsWith('http')) { if (u.endsWith('provide/')) u += 'vod/'; if (u.endsWith('provide/vod')) u += '/'; urls.push(u.replace('/at/xml/', '/')); @@ -78,6 +82,47 @@ export class VideoSearch { return [...new Set(urls)]; } + private async loadRemoteConfig(force = false) { + const cache = stor.get(); + let needUpdate = true; + + if (!force && cache.remoteConfig?.updateTime) { + needUpdate = Date.now() - cache.remoteConfig.updateTime > 1 * 60 * 60 * 1000; + } + + if (needUpdate) { + const url = 'https://ghproxy.com/raw.githubusercontent.com/lzwme/m3u8-dl/main/test/remote-config.json'; + const { data } = await req.get( + url, + null, + { 'content-type': 'application/json' }, + { rejectUnauthorized: false } + ); + logger.debug('加载远程配置', data); + + if (Array.isArray(data.apiSites)) { + cache.remoteConfig = { + updateTime: Date.now(), + data, + }; + stor.save(cache); + } + } + + return cache.remoteConfig; + } + async updateApiFromRemote(force = false) { + const remoteConfig = await this.loadRemoteConfig(force); + + if (Array.isArray(remoteConfig?.data?.apiSites)) { + remoteConfig.data.apiSites.forEach(item => { + if (item.enable === 0 || item.enable === false) return; + item.url = this.formatUrl(item.url)[0]; + item.remote = true; + this.apiMap.set(item.url, item); + }); + } + } } // const v = new VideoSearch({ api: ['https://api.xinlangapi.com/xinlangapi.php/provide/vod/'] }); diff --git a/test/remote-config.json b/test/remote-config.json new file mode 100644 index 0000000..0bd97e1 --- /dev/null +++ b/test/remote-config.json @@ -0,0 +1,19 @@ +{ + "apiSites": [ + { + "url": "https://jyzyapi.com/provide/vod/", + "desc": "金鹰", + "enable": 1 + }, + { + "url": "https://jyzy1.com/provide/vod/", + "desc": "金鹰备用1", + "enable": 0 + }, + { + "url": "https://api.xinlangapi.com/xinlangapi.php/provide/vod/", + "desc": "新浪API", + "enable": 1 + } + ] +}