From 73e67368b07e3315d78538096ff81aa080830c0b Mon Sep 17 00:00:00 2001 From: renxia Date: Thu, 1 Aug 2024 17:22:14 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E6=90=9C=E7=B4=A2=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=8F=92=E4=BB=B6=E5=8C=96=E5=8A=A0=E8=BD=BD=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E6=90=9C=E7=B4=A2=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cli.ts | 3 +- src/lib/local-play.ts | 6 +- src/lib/search-api/ApiManage.ts | 95 +++++++++++ src/lib/search-api/CommSearchApi.ts | 90 +++++++++++ src/lib/storage.ts | 10 +- src/lib/utils.ts | 22 +++ src/lib/video-search.ts | 182 +++------------------- src/types/m3u8.ts | 7 + src/types/video-search.ts | 234 +++++++++++++++------------- 9 files changed, 370 insertions(+), 279 deletions(-) create mode 100644 src/lib/search-api/ApiManage.ts create mode 100644 src/lib/search-api/CommSearchApi.ts diff --git a/src/cli.ts b/src/cli.ts index 6a475a7..4e47bb2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -49,7 +49,8 @@ program .command('search [keyword]') .alias('s') .option('-u,--url ', '影视搜索的接口地址(m3u8采集站标准接口)') - .option('-R,--remote-config-url ', '自定义远程配置加载地址。默认从主仓库配置读取') + .option('-d, --apidir ', '指定自定义视频搜索 api 所在的目录或具体路径') + // .option('-R,--remote-config-url ', '自定义远程配置加载地址。默认从主仓库配置读取') .description('m3u8视频在线搜索与下载') .action(async (keyword, options: { url?: string[]; remoteConfigUrl?: string }) => { await VideoSerachAndDL(keyword, options, getOptions()); diff --git a/src/lib/local-play.ts b/src/lib/local-play.ts index 57a2f67..e9f537f 100644 --- a/src/lib/local-play.ts +++ b/src/lib/local-play.ts @@ -27,7 +27,7 @@ export async function localPlay(m3u8Info: TsItemInfo[], options: M3u8DLOptions) return info; } -export function toLocalM3u8(m3u8Info: TsItemInfo[], m3u8FilePath: string, host = '.') { +export function toLocalM3u8(m3u8Info: TsItemInfo[], m3u8FilePath: string, host = '') { const m3u8ContentList = [ `#EXTM3U`, `#EXT-X-VERSION:3`, @@ -37,8 +37,10 @@ export function toLocalM3u8(m3u8Info: TsItemInfo[], m3u8FilePath: string, host = // `#EXT-X-KEY:METHOD=AES-128,URI="/api/aes/enc.key"`, ]; + if (host && !host.endsWith('/')) host += '/'; + m3u8Info.forEach(d => { - if (d.tsOut) m3u8ContentList.push(`#EXTINF:${Number(d.duration).toFixed(6)},`, `${host}/${basename(d.tsOut)}`); + if (d.tsOut) m3u8ContentList.push(`#EXTINF:${Number(d.duration).toFixed(6)},`, `${host}${basename(d.tsOut)}`); }); m3u8ContentList.push(`#EXT-X-ENDLIST`); diff --git a/src/lib/search-api/ApiManage.ts b/src/lib/search-api/ApiManage.ts new file mode 100644 index 0000000..996af8e --- /dev/null +++ b/src/lib/search-api/ApiManage.ts @@ -0,0 +1,95 @@ +import { prompt } from 'enquirer'; +import type { SearchApi, VideoSearchResult } from '../../types'; +import { findFiles, logger } from '../utils'; +import { CommSearchApi } from './CommSearchApi'; + +export const apiManage = { + api: new Map(), + current: null as SearchApi, + load(apidir?: string, force = false) { + const files: string[] = findFiles(apidir, (filepath, s) => !s.isFile() || /\.c?js/.test(filepath)); + + for (const filepath of files) { + /* eslint-disable @typescript-eslint/no-var-requires */ + const sApi = require(filepath); + this.add(sApi.default || sApi, force); + } + }, + /** 添加 API 到列表中 */ + add(sApi: SearchApi | { api: string; desc?: string; enable?: boolean; key?: string | number }, force = false) { + if (Array.isArray(sApi)) return sApi.forEach(d => this.add(d)); + + if (sApi.api?.startsWith('http') && !('search' in sApi)) sApi = new CommSearchApi(sApi) as SearchApi; + + if (this.validate(sApi as SearchApi) && (force || !this.api.has(sApi.key))) { + this.api.set(sApi.key, sApi as SearchApi); + logger.debug('添加Api:', sApi.desc || sApi.key); + } + }, + /** API 有效性校验 */ + validate(sApi: SearchApi, desc?: string): sApi is SearchApi { + if (!sApi) return false; + + const requiredKeys = ['enable', 'key', 'search', 'detail']; + + if (!sApi.key) sApi.key = sApi.desc; + + for (const key of requiredKeys) { + if (!(key in sApi)) { + logger.warn(`【API校验不通过】${desc} 缺少关键属性 ${key}`); + return false; + } + + if ((key === 'search' || key === 'detail') && typeof sApi[key] !== 'function') return false; + } + + return sApi.enable !== false; + }, + /** 选择一个 API */ + async select() { + if (!this.api.size) { + logger.error('没有可用的 API,请配置或指定 url、apidir 参数'); + process.exit(-1); + } + + if (this.api.size === 1) { + this.current = [...this.api.values()][0]; + return; + } + + const apis = [...this.api.values()]; + const v = await prompt<{ k: string }>({ + type: 'select', + name: 'k', + message: '请选择 API 站点', + choices: apis.map(d => ({ name: String(d.key), message: d.desc })), + validate: value => value.length >= 1, + }); + + this.current = apis.find(d => String(d.key) === v.k); + }, + async search(wd: string, api?: SearchApi) { + const result: VideoSearchResult['list'] = []; + try { + if (api) return (await api.search(wd)).list; + + for (api of this.api.values()) { + const r = await api.search(wd); + if (Array.isArray(r.list)) { + r.list.forEach(d => { + d.api_key = api.key; + result.push(d); + }); + } + } + } catch (error) { + logger.error('搜索失败!', (error as Error).message); + } + + return result; + }, + detail(info: VideoSearchResult['list'][0]) { + const api = this.api.get(info.api_key) || this.current; + return api.detail(info.vod_id); + }, +}; diff --git a/src/lib/search-api/CommSearchApi.ts b/src/lib/search-api/CommSearchApi.ts new file mode 100644 index 0000000..48fc617 --- /dev/null +++ b/src/lib/search-api/CommSearchApi.ts @@ -0,0 +1,90 @@ +import { Request } from '@lzwme/fe-utils'; +import type { SearchApi, VideoDetailsResult, VideoSearchResult } from '../../types'; +import { type M3u8StorConfig } from '../storage.js'; + +const req = new Request(null, { + 'content-type': 'application/json; charset=UTF-8', +}); + +export interface VSOptions { + /** 采集站地址 */ + api?: string; + /** 站点描述 */ + desc?: string; + /** 是否启用 */ + enable?: 0 | 1 | boolean; +} + +/** + * 基于采集站点 API 的通用搜索 + * @example + * ```ts + * const v = new CommSearchApi({ api: 'https://api.xinlangapi.com/xinlangapi.php/provide/vod/' }); + * v.search('三体') + * .then(d => { + * console.log(d.total, d.list); + * return v.getVideoList(d.list[0].vod_id); + * }) + * .then(d => { + * console.log('detail:', d.total, d.list[0]); + * }); + * ``` + */ +export class CommSearchApi implements SearchApi { + protected currentUrl: M3u8StorConfig['remoteConfig']['data']['apiSites'][0]; + public apiMap = new Map(); + public get desc() { + return this.options.desc || this.options.api; + } + public get key() { + return this.options.api; + } + public get enable() { + return this.options.api && this.options.enable !== false; + } + constructor(protected options: VSOptions = {}) { + if (options.api) options.api = this.formatUrl(options.api)[0]; + this.options = options; + } + async search(wd: string, api = this.options.api) { + let { data } = await req.get(api, { wd }, null, { rejectUnauthorized: false }); + if (typeof data == 'string') data = JSON.parse(data) as VideoSearchResult; + return data; + } + async detail(id: string | number, api = this.options.api) { + return this.getVideoList(id, api); + } + /** 按 id 取列表(每一项中包含了更为详细的内容) */ + async getVideoList(ids: number | string | (number | string)[], api = this.options.api) { + let { data } = await req.get( + api, + { + ac: 'videolist', + ids: Array.isArray(ids) ? ids.join(',') : ids, + }, + null, + { rejectUnauthorized: false } + ); + + if (typeof data == 'string') data = JSON.parse(data) as VideoDetailsResult; + + return data; + } + 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.startsWith('http')) { + if (u.endsWith('provide/')) u += 'vod/'; + if (u.endsWith('provide/vod')) u += '/'; + urls.push(u.replace('/at/xml/', '/')); + } + } + + return [...new Set(urls)]; + } +} diff --git a/src/lib/storage.ts b/src/lib/storage.ts index da04f3c..fb922fc 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -1,13 +1,15 @@ import { LiteStorage } from '@lzwme/fe-utils'; import { resolve } from 'node:path'; import { homedir } from 'node:os'; -import { type VSOptions } from './video-search'; import type { M3u8DLOptions, VideoDetails } from '../types'; -export interface M3u8StorConfig extends VSOptions { +export interface M3u8StorConfig { /** 播放地址缓存 */ api?: string[]; - /** 远程加载的配置信息 */ + /** + * 远程加载的配置信息 + * @deprecated + */ remoteConfig?: { /** 最近一次更新的时间。默认缓存1小时 */ updateTime?: number; @@ -25,7 +27,7 @@ export interface M3u8StorConfig extends VSOptions { latestSearchDL?: { keyword: string; urls: string[]; - info: VideoDetails; + info: Partial; dlOptions: M3u8DLOptions; }; } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 0d038f1..dfd9cd4 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,4 +1,6 @@ import { execSync, NLogger, color, Request, retry } from '@lzwme/fe-utils'; +import { existsSync, readdirSync, Stats, statSync } from 'node:fs'; +import { resolve } from 'node:path'; export const request = new Request('', { 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', @@ -28,3 +30,23 @@ export function isSupportFfmpeg() { if (null == _isSupportFfmpeg) _isSupportFfmpeg = execSync('ffmpeg -version').stderr === ''; return _isSupportFfmpeg; } + +export function findFiles(apidir?: string, validate?: (filepath: string, stat: Stats) => boolean) { + const files: string[] = []; + + if (apidir && existsSync(apidir)) { + const stat = statSync(apidir); + + if (!validate || validate(apidir, stat)) { + if (stat.isFile()) { + files.push(resolve(apidir)); + } else if (stat.isDirectory()) { + readdirSync(apidir).forEach(filename => { + findFiles(resolve(apidir, filename)).forEach(f => files.push(f)); + }); + } + } + } + + return files; +} diff --git a/src/lib/video-search.ts b/src/lib/video-search.ts index d76ad11..72a3387 100644 --- a/src/lib/video-search.ts +++ b/src/lib/video-search.ts @@ -1,156 +1,19 @@ -import { Request } from '@lzwme/fe-utils'; -import type { VideoListResult, VideoSearchResult, CliOptions, VideoDetails } from '../types'; -import { stor, type M3u8StorConfig } from './storage.js'; +import type { CliOptions, VideoDetails } from '../types'; +import { stor } from './storage.js'; import { logger } from './utils.js'; import { m3u8BatchDownload } from '../m3u8-batch-download.js'; import { prompt } from 'enquirer'; import { cyanBright, color, greenBright, gray, green } from 'console-log-colors'; - -const req = new Request(null, { - 'content-type': 'application/json; charset=UTF-8', -}); - -export interface VSOptions { - /** 播放地址缓存 */ - api?: string[]; - force?: boolean; - /** 远程配置的请求地址 */ - remoteConfigUrl?: string; -} - -/** - * @example - * ```ts - * const v = new VideoSearch({ api: ['https://api.xinlangapi.com/xinlangapi.php/provide/vod/'] }); - * v.search('三体') - * .then(d => { - * console.log(d.total, d.list); - * return v.getVideoList(d.list[0].vod_id); - * }) - * .then(d => { - * console.log('detail:', d.total, d.list[0]); - * }); - * ``` - */ -export class VideoSearch { - public apiMap = new Map(); - public get api() { - return [...this.apiMap.values()].reverse(); - } - constructor(protected options: VSOptions = {}) { - if (!options.api?.length) options.api = []; - if (process.env.VAPI) options.api.push(...process.env.VAPI.split('$$$')); - this.updateOptions(options); - } - async updateOptions(options: VSOptions) { - const cache = stor.get(); - const apis = [...(cache.api || []), ...options.api]; - - this.formatUrl(apis); - - if (options.api?.length) stor.set({ api: apis }); - - (cache.api || []).forEach(url => { - this.apiMap.set(url, { url, desc: url }); - }); - - await this.updateApiFromRemote(options.force); - - if (!this.apiMap.size) throw Error('没有可用的 API 站点,请添加或指定'); - - return this; - } - async search(wd: string, api = this.api[0]) { - let { data } = await req.get(api.url, { wd }, null, { rejectUnauthorized: false }); - - if (typeof data == 'string') data = JSON.parse(data) as VideoSearchResult; - - return data; - } - async getVideoList(ids: number | string | (number | string)[], api = this.api[0]) { - let { data } = await req.get( - api.url, - { - ac: 'videolist', - ids: Array.isArray(ids) ? ids.join(',') : ids, - }, - null, - { rejectUnauthorized: false } - ); - - if (typeof data == 'string') data = JSON.parse(data) as VideoListResult; - - return data; - } - 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.startsWith('http')) { - if (u.endsWith('provide/')) u += 'vod/'; - if (u.endsWith('provide/vod')) u += '/'; - urls.push(u.replace('/at/xml/', '/')); - } - } - - 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 = - this.options.remoteConfigUrl || 'https://mirror.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)) { - stor.set({ - remoteConfig: { - updateTime: Date.now(), - data, - }, - }); - } - } - - 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); - }); - } - } -} +import { apiManage } from './search-api/ApiManage'; export async function VideoSerachAndDL( keyword: string, - options: { url?: string[]; remoteConfigUrl?: string }, + options: { url?: string[]; apidir?: string; remoteConfigUrl?: string }, baseOpts: CliOptions ): Promise { + logger.debug(options, baseOpts); const cache = stor.get(); - const doDownload = async (info: VideoDetails, urls: string[]) => { + const doDownload = async (info: Partial, urls: string[]) => { const p = await prompt<{ play: boolean }>({ type: 'confirm', name: 'play', @@ -188,21 +51,13 @@ export async function VideoSerachAndDL( } } - const vs = new VideoSearch(); - await vs.updateOptions({ api: options.url || [], force: baseOpts.force, remoteConfigUrl: options.remoteConfigUrl }); - const apis = vs.api; - let apiUrl = options.url?.length ? { url: options.url[0] } : apis[0]; - - if (!options.url && apis.length > 0) { - await prompt<{ k: string }>({ - type: 'select', - name: 'k', - message: '请选择 API 站点', - choices: apis.map(d => ({ name: d.url, message: d.desc })) as never, - validate: value => value.length >= 1, - }).then(v => (apiUrl = apis.find(d => d.url === v.k))); + if (options.apidir && !apiManage.current) apiManage.load(options.apidir); + if (options.url) { + options.url.forEach(api => apiManage.add({ api, desc: api })); } + await apiManage.select(); + await prompt<{ k: string }>({ type: 'input', name: 'k', @@ -211,14 +66,15 @@ export async function VideoSerachAndDL( initial: keyword, }).then(v => (keyword = v.k)); - const sRes = await vs.search(keyword, apiUrl); + const sRes = await apiManage.search(keyword, apiManage.current); logger.debug(sRes); - if (!sRes.total) { + + if (!sRes.length) { console.log(color.green(`[${keyword}]`), `没有搜到结果`); return VideoSerachAndDL(keyword, options, baseOpts); } - const choices = sRes.list.map((d, idx) => ({ + const choices = sRes.map((d, idx) => ({ name: d.vod_id, message: `${idx + 1}. [${d.type_name}] ${d.vod_name}`, hint: `${d.vod_remarks}(${d.vod_time})`, @@ -227,15 +83,15 @@ export async function VideoSerachAndDL( type: 'select', name: 'vid', pointer: '👉', - message: `查找到了 ${color.greenBright(sRes.list.length)} 条结果,请选择:`, + message: `查找到了 ${color.greenBright(sRes.length)} 条结果,请选择:`, choices: choices.concat({ name: -1, message: greenBright('重新搜索'), hint: '' }) as never, } as never); if (answer1.vid === -1) return VideoSerachAndDL(keyword, options, baseOpts); - const vResult = await vs.getVideoList(answer1.vid, apiUrl); - if (!vResult.list?.length) { - logger.error('获取视频信息失败!', vResult.msg); + const vResult = await apiManage.detail(sRes.find(d => d.vod_id == answer1.vid)); + if (!vResult) { + logger.error('获取视频信息失败!'); return VideoSerachAndDL(keyword, options, baseOpts); } else { const info = vResult.list[0]; diff --git a/src/types/m3u8.ts b/src/types/m3u8.ts index b9827e8..942d4e3 100644 --- a/src/types/m3u8.ts +++ b/src/types/m3u8.ts @@ -1,3 +1,10 @@ +/* + * @Author: renxia + * @Date: 2024-08-02 09:58:56 + * @LastEditors: renxia + * @LastEditTime: 2024-08-02 17:15:07 + * @Description: + */ /* eslint-disable @typescript-eslint/triple-slash-reference */ /// diff --git a/src/types/video-search.ts b/src/types/video-search.ts index 106df87..e6569e0 100644 --- a/src/types/video-search.ts +++ b/src/types/video-search.ts @@ -1,124 +1,140 @@ -/** 模糊搜索返回的结果(?wd=) */ -export interface VideoSearchResult { - code: number; - msg: string; - page: number; - pagecount: number; - limit: string; +import { AnyObject } from '@lzwme/fe-utils'; + +export interface SearchApiResult { + /** api 来源 */ + api_key?: number | string; + code?: number; + msg?: string; + page?: number; + pagecount?: number; + limit?: string; total: number; - list: VodList[]; - class: { + list: T; +} + +/** 模糊搜索返回的结果(?wd=) */ +export interface VideoSearchResult extends SearchApiResult { + class?: { type_id: number; type_pid: number; type_name: string; }[]; } -interface VodList { - vod_id: number; - vod_name: string; - type_id: number; - type_name: string; - vod_en: string; - vod_time: string; - vod_remarks: string; - vod_play_from: string; - vod_play_url: string; -} +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' +>; /** 按 id 搜素返回的详情列表 */ -export interface VideoListResult { - code: number; - msg: string; - page: number; - pagecount: number; - limit: string; - total: number; - list: VideoDetails[]; -} +export type VideoDetailsResult = SearchApiResult; export interface VideoDetails { + /** api 来源 */ + api_key: number | string; + /** 视频 id,可用于查询详情 */ vod_id: number; - type_id: number; - type_id_1: number; - group_id: number; + /** 视频名称 */ vod_name: string; - vod_sub: string; - vod_en: string; - vod_status: number; - vod_letter: string; - vod_color: string; - vod_tag: string; - vod_class: string; - vod_pic: string; - vod_pic_thumb: string; - vod_pic_slide: string; - vod_pic_screenshot: null; - vod_actor: string; - vod_director: string; - vod_writer: string; - vod_behind: string; - vod_blurb: string; - vod_remarks: string; - vod_pubdate: string; - vod_total: number; - vod_serial: string; - vod_tv: string; - vod_weekday: string; - vod_area: string; - vod_lang: string; - vod_year: string; - vod_version: string; - vod_state: string; - vod_author: string; - vod_jumpurl: string; - vod_tpl: string; - vod_tpl_play: string; - vod_tpl_down: string; - vod_isend: number; - vod_lock: number; - vod_level: number; - vod_copyright: number; - vod_points: number; - vod_points_play: number; - vod_points_down: number; - vod_hits: number; - vod_hits_day: number; - vod_hits_week: number; - vod_hits_month: number; - vod_duration: string; - vod_up: number; - vod_down: number; - vod_score: string; - vod_score_all: number; - vod_score_num: number; - vod_time: string; - vod_time_add: number; - vod_time_hits: number; - vod_time_make: number; - vod_trysee: number; - vod_douban_id: number; - vod_douban_score: string; - vod_reurl: string; - vod_rel_vod: string; - vod_rel_art: string; - vod_pwd: string; - vod_pwd_url: string; - vod_pwd_play: string; - vod_pwd_play_url: string; - vod_pwd_down: string; - vod_pwd_down_url: string; - vod_content: string; - vod_play_from: string; - vod_play_server: string; - vod_play_note: string; + /** 播放地址 */ vod_play_url: string; - vod_down_from: string; - vod_down_server: string; - vod_down_note: string; - vod_down_url: string; - vod_plot: number; - vod_plot_name: string; - vod_plot_detail: string; - type_name: string; + /** 分类 id */ + type_id?: number; + /** 分类名称 */ + type_name?: string; + vod_en?: string; + vod_time?: string; + vod_remarks?: string; + vod_play_from?: string; + type_id_1?: number; + group_id?: number; + vod_sub?: string; + vod_status?: number; + vod_letter?: string; + vod_color?: string; + vod_tag?: string; + vod_class?: string; + vod_pic?: string; + vod_pic_thumb?: string; + vod_pic_slide?: string; + vod_pic_screenshot: null; + vod_actor?: string; + vod_director?: string; + vod_writer?: string; + vod_behind?: string; + vod_blurb?: string; + vod_pubdate?: string; + vod_total?: number; + vod_serial?: string; + vod_tv?: string; + vod_weekday?: string; + vod_area?: string; + vod_lang?: string; + vod_year?: string; + vod_version?: string; + vod_state?: string; + vod_author?: string; + vod_jumpurl?: string; + vod_tpl?: string; + vod_tpl_play?: string; + vod_tpl_down?: string; + vod_isend?: number; + vod_lock?: number; + vod_level?: number; + vod_copyright?: number; + vod_points?: number; + vod_points_play?: number; + vod_points_down?: number; + vod_hits?: number; + vod_hits_day?: number; + vod_hits_week?: number; + vod_hits_month?: number; + vod_duration?: string; + vod_up?: number; + vod_down?: number; + /** 评分 */ + vod_score?: string; + vod_score_all?: number; + vod_score_num?: number; + vod_time_add?: number; + vod_time_hits?: number; + vod_time_make?: number; + vod_trysee?: number; + /** 在豆瓣的 id */ + vod_douban_id?: number; + /** 豆瓣评分 */ + vod_douban_score?: string; + vod_reurl?: string; + vod_rel_vod?: string; + vod_rel_art?: string; + vod_pwd?: string; + vod_pwd_url?: string; + vod_pwd_play?: string; + vod_pwd_play_url?: string; + vod_pwd_down?: string; + vod_pwd_down_url?: string; + vod_content?: string; + vod_play_server?: string; + vod_play_note?: string; + vod_down_from?: string; + vod_down_server?: string; + vod_down_note?: string; + vod_down_url?: string; + vod_plot?: number; + vod_plot_name?: string; + vod_plot_detail?: string; +} + +/** 搜索Api的格式 */ +export interface SearchApi extends AnyObject { + /** API 唯一标记 */ + key: string | number; + /** API 描述 */ + desc?: string; + /** 是否启用 */ + enable?: boolean; + /** 按关键字搜索列表 */ + search(wd: string, ...args: unknown[]): Promise; + /** 按 id 获取某个视频的详情 */ + detail(id: string | number, ...args: unknown[]): Promise; }