-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
370 additions
and
279 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string | number, SearchApi>(), | ||
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); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string, M3u8StorConfig['remoteConfig']['data']['apiSites'][0]>(); | ||
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<VideoSearchResult>(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<VideoDetailsResult>( | ||
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)]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.