UNPKG

@lzwme/m3u8-dl

Version:

Batch download of m3u8 files and convert to mp4

262 lines (261 loc) 10.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.VideoSearch = void 0; exports.VideoSerachAndDL = VideoSerachAndDL; const fe_utils_1 = require("@lzwme/fe-utils"); const storage_js_1 = require("./storage.js"); const utils_js_1 = require("./utils.js"); const m3u8_batch_download_js_1 = require("../m3u8-batch-download.js"); const enquirer_1 = require("enquirer"); const console_log_colors_1 = require("console-log-colors"); const req = new fe_utils_1.Request(null, { 'content-type': 'application/json; charset=UTF-8', }); /** * @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]); * }); * ``` */ class VideoSearch { options; apiMap = new Map(); get api() { return [...this.apiMap.values()].reverse(); } constructor(options = {}) { this.options = options; if (!options.api?.length) options.api = []; if (process.env.VAPI) options.api.push(...process.env.VAPI.split('$$$')); this.updateOptions(options); } async updateOptions(options) { const cache = storage_js_1.stor.get(); const apis = [...(cache.api || []), ...options.api]; this.formatUrl(apis); if (options.api?.length) storage_js_1.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, api = this.api[0]) { let { data } = await req.get(api.url, { wd }, null, { rejectUnauthorized: false }); if (typeof data == 'string') data = JSON.parse(data); return data; } async getVideoList(ids, 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); return data; } formatUrl(url) { const urls = []; 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)]; } async loadRemoteConfig(force = false) { const cache = storage_js_1.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 }); utils_js_1.logger.debug('加载远程配置', data); if (Array.isArray(data.apiSites)) { storage_js_1.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); }); } } } exports.VideoSearch = VideoSearch; async function VideoSerachAndDL(keyword, options, baseOpts) { const cache = storage_js_1.stor.get(); const doDownload = async (info, urls) => { const p = await (0, enquirer_1.prompt)({ type: 'confirm', name: 'play', initial: baseOpts.play, message: `【${(0, console_log_colors_1.greenBright)(info.vod_name)}】是否边下边播?`, }); baseOpts.play = p.play; try { cache.latestSearchDL = { ...cache.latestSearchDL, info, urls, dlOptions: { filename: info.vod_name.replaceAll(' ', '_'), ...baseOpts }, }; storage_js_1.stor.save({ latestSearchDL: cache.latestSearchDL }); const r = await (0, m3u8_batch_download_js_1.m3u8BatchDownload)(cache.latestSearchDL.urls, cache.latestSearchDL.dlOptions); if (r) storage_js_1.stor.set({ latestSearchDL: null }); } catch (error) { utils_js_1.logger.info('cachel download'); } }; if (cache.latestSearchDL?.urls) { const p = await (0, enquirer_1.prompt)({ type: 'confirm', name: 'k', initial: true, message: `存在上次未完成的下载【${(0, console_log_colors_1.greenBright)(cache.latestSearchDL.info.vod_name)}】,是否继续?`, }); if (p.k) { await doDownload(cache.latestSearchDL.info, cache.latestSearchDL.urls); } else { storage_js_1.stor.set({ latestSearchDL: null }); } } 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 (0, enquirer_1.prompt)({ type: 'select', name: 'k', message: '请选择 API 站点', choices: apis.map(d => ({ name: d.url, message: d.desc })), validate: value => value.length >= 1, }).then(v => (apiUrl = apis.find(d => d.url === v.k))); } await (0, enquirer_1.prompt)({ type: 'input', name: 'k', message: '请输入关键字', validate: value => value.length > 1, initial: keyword, }).then(v => (keyword = v.k)); const sRes = await vs.search(keyword, apiUrl); utils_js_1.logger.debug(sRes); if (!sRes.total) { console.log(console_log_colors_1.color.green(`[${keyword}]`), `没有搜到结果`); return VideoSerachAndDL(keyword, options, baseOpts); } const choices = sRes.list.map((d, idx) => ({ name: d.vod_id, message: `${idx + 1}. [${d.type_name}] ${d.vod_name}`, hint: `${d.vod_remarks}(${d.vod_time})`, })); const answer1 = await (0, enquirer_1.prompt)({ type: 'select', name: 'vid', pointer: '👉', message: `查找到了 ${console_log_colors_1.color.greenBright(sRes.list.length)} 条结果,请选择:`, choices: choices.concat({ name: -1, message: (0, console_log_colors_1.greenBright)('重新搜索'), hint: '' }), }); if (answer1.vid === -1) return VideoSerachAndDL(keyword, options, baseOpts); const vResult = await vs.getVideoList(answer1.vid, apiUrl); if (!vResult.list?.length) { utils_js_1.logger.error('获取视频信息失败!', vResult.msg); return VideoSerachAndDL(keyword, options, baseOpts); } else { const info = vResult.list[0]; if (!info.vod_play_url) { utils_js_1.logger.error('未获取到播放地址信息', info); return VideoSerachAndDL(keyword, options, baseOpts); } if (!info.vod_play_note || !String(info.vod_play_url).includes(info.vod_play_note)) { ['#', '$'].some(d => { if (info.vod_play_url.includes(d)) { info.vod_play_note = d; return true; } return true; }); } const urls = info.vod_play_url .split(info.vod_play_note || '$') .find(d => d.includes('.m3u8')) .split('#'); utils_js_1.logger.debug(info, urls); const r = (key, desc) => (info[key] ? ` [${desc}] ${(0, console_log_colors_1.greenBright)(info[key])}` : ''); console.log([ `\n [名称] ${(0, console_log_colors_1.cyanBright)(info.vod_name)}`, r('vod_sub', '别名'), ` [更新] ${(0, console_log_colors_1.greenBright)(info.vod_remarks)}(${(0, console_log_colors_1.gray)(info.vod_time)})`, r('vod_total', '总集数'), r('type_name', '分类'), r('vod_class', '类别'), r('vod_writer', '作者'), r('vod_area', '地区'), r('vod_lang', '语言'), r('vod_year', '年份'), r('vod_douban_score', '评分'), r('vod_pubdate', '上映日期'), `\n${(0, console_log_colors_1.green)((info.vod_content || info.vod_blurb).replace(/<\/?.+?>/g, ''))}\n`, // 描述 ] .filter(Boolean) .join('\n'), '\n'); const answer = await (0, enquirer_1.prompt)({ type: 'select', name: 'url', choices: [ { name: '1', message: (0, console_log_colors_1.green)('全部下载') }, { name: '-1', message: (0, console_log_colors_1.cyanBright)('重新搜索') }, ].concat(urls.map((d, i) => ({ name: d, message: `${i + 1}. ${d}` }))), message: `获取到了 ${console_log_colors_1.color.magentaBright(urls.length)} 条视频下载地址,请选择:`, }); if (answer.url !== '-1') { await doDownload(info, answer.url === '1' ? urls : [answer.url]); } return VideoSerachAndDL(keyword, options, baseOpts); } }