From 549a1e0232d51be705ee00d5895abee64897352b Mon Sep 17 00:00:00 2001 From: catvod <88956744+catvod@users.noreply.github.com> Date: Wed, 27 Dec 2023 10:35:08 +0800 Subject: [PATCH] Demo of req, JSFile, JSProxyStream... --- open/ffm3u8_open.js | 485 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 485 insertions(+) create mode 100644 open/ffm3u8_open.js diff --git a/open/ffm3u8_open.js b/open/ffm3u8_open.js new file mode 100644 index 0000000..876b2c2 --- /dev/null +++ b/open/ffm3u8_open.js @@ -0,0 +1,485 @@ +import { _ } from './lib/cat.js'; +import * as HLS from './lib/hls.js'; + +let key = 'ffm3u8'; +let url = ''; +let categories = []; +let siteKey = ''; +let siteType = 0; + +async function request(reqUrl, agentSp) { + let res = await req(reqUrl, { + method: 'get', + }); + return JSON.parse(res.content); +} + +async function init(cfg) { + siteKey = cfg.skey; + siteType = cfg.stype; + url = cfg.ext.url; + categories = cfg.ext.categories; +} + +async function home(filter) { + const data = await request(url); + let classes = []; + for (const cls of data.class) { + const n = cls.type_name.toString().trim(); + if (categories && categories.length > 0) { + if (categories.indexOf(n) < 0) continue; + } + classes.push({ + type_id: cls.type_id.toString(), + type_name: n, + }); + } + if (categories && categories.length > 0) { + classes = _.sortBy(classes, (p) => { + return categories.indexOf(p.type_name); + }); + } + return { + class: classes, + }; +} + +async function homeVod() { + return '{}'; +} + +async function category(tid, pg, filter, extend) { + let page = pg || 1; + if (page == 0) page = 1; + const data = await request(url + `?ac=detail&t=${tid}&pg=${page}`); + let videos = []; + for (const vod of data.list) { + videos.push({ + vod_id: vod.vod_id.toString(), + vod_name: vod.vod_name.toString(), + vod_pic: vod.vod_pic, + vod_remarks: vod.vod_remarks, + }); + } + return { + page: parseInt(data.page), + pagecount: data.pagecount, + total: data.total, + list: videos, + }; +} + +async function detail(id) { + const data = (await request(url + `?ac=detail&ids=${id}`)).list[0]; + let vod = { + vod_id: data.vod_id, + vod_name: data.vod_name, + vod_pic: data.vod_pic, + type_name: data.type_name, + vod_year: data.vod_year, + vod_area: data.vod_area, + vod_remarks: data.vod_remarks, + vod_actor: data.vod_actor, + vod_director: data.vod_director, + vod_content: data.vod_content.trim(), + vod_play_from: data.vod_play_from, + vod_play_url: data.vod_play_url, + }; + return { + list: [vod], + }; +} + +async function proxy(segments, headers, reqHeaders) { + let what = segments[0]; + let segs = decodeURIComponent(segments[1]); + if (what == 'hls') { + const hlsData = await hlsCache(segs, headers); + if (hlsData.variants) { + // variants -> variants -> .... ignore + const hls = HLS.stringify(hlsData.plist); + let hlsHeaders = {}; + if (hlsData.headers['content-length']) { + Object.assign(hlsHeaders, hlsData.headers, { 'content-length': hls.length.toString() }); + } else { + Object.assign(hlsHeaders, hlsData.headers); + } + const result = { + code: hlsData.code, + content: hls, + headers: hlsHeaders, + }; + return result; + } else { + const hls = HLS.stringify(hlsData.plist, (segment) => { + return js2Proxy(false, siteType, siteKey, 'ts/' + encodeURIComponent(hlsData.key + '/' + segment.mediaSequenceNumber.toString()), headers); + }); + let hlsHeaders = {}; + if (hlsData.headers['content-length']) { + Object.assign(hlsHeaders, hlsData.headers, { 'content-length': hls.length.toString() }); + } else { + Object.assign(hlsHeaders, hlsData.headers); + } + const result = { + code: hlsData.code, + content: hls, + headers: hlsHeaders, + }; + return result; + } + } else if (what == 'ts') { + const info = segs.split('/'); + const hlsKey = info[0]; + const segIdx = parseInt(info[1]); + return await tsCache(hlsKey, segIdx, headers); + } + return '{}'; +} + +async function play(flag, id, flags) { + try { + const pUrls = await hls2Urls(id, {}); + for (let index = 1; index < pUrls.length; index += 2) { + pUrls[index] = js2Proxy(false, siteType, siteKey, 'hls/' + encodeURIComponent(pUrls[index]), {}); + } + pUrls.push('original'); + pUrls.push(id); + return { + parse: 0, + url: pUrls, + }; + } catch (e) { + return { + parse: 0, + url: id, + }; + } +} + +async function search(wd, quick, pg) { + let page = pg || 1; + if (page == 0) page = 1; + const data = await request(url + `?ac=detail&wd=${wd}`); + let videos = []; + for (const vod of data.list) { + videos.push({ + vod_id: vod.vod_id.toString(), + vod_name: vod.vod_name.toString(), + vod_pic: vod.vod_pic, + vod_remarks: vod.vod_remarks, + }); + } + return { + page: parseInt(data.page), + pagecount: data.pagecount, + total: data.total, + list: videos, + }; +} + +const cacheRoot = 'hls_cache'; +const hlsKeys = []; +const hlsPlistCaches = {}; +const interrupts = {}; +const downloadTask = {}; +let currentDownloadHlsKey = ''; + +function hlsCacheInsert(key, data) { + hlsKeys.push(key); + hlsPlistCaches[key] = data; + if (hlsKeys.length > 5) { + const rmKey = hlsKeys.shift(); + hlsCacheRemove(rmKey); + } +} + +function hlsCacheRemove(key) { + delete hlsPlistCaches[key]; + delete hlsKeys[key]; + new JSFile(cacheRoot + '/' + key).delete(); +} + +function plistUriResolve(baseUrl, plist) { + if (plist.variants) { + for (const v of plist.variants) { + if (!v.uri.startsWith('http')) { + v.uri = relative2Absolute(baseUrl, v.uri); + } + } + } + if (plist.segments) { + for (const s of plist.segments) { + if (!s.uri.startsWith('http')) { + s.uri = relative2Absolute(baseUrl, s.uri); + } + if (s.key && s.key.uri && !s.key.uri.startsWith('http')) { + s.key.uri = relative2Absolute(baseUrl, s.key.uri); + } + } + } + return plist; +} + +async function hls2Urls(url, headers) { + let urls = []; + let resp = {}; + let tmpUrl = url; + while (true) { + resp = await req(tmpUrl, { + headers: headers, + redirect: 0, + }); + if (resp.headers['location']) { + tmpUrl = resp.headers['location']; + } else { + break; + } + } + if (resp.code == 200) { + var hls = resp.content; + const plist = plistUriResolve(tmpUrl, HLS.parse(hls)); + if (plist.variants) { + for (const vari of _.sortBy(plist.variants, (v) => -1 * v.bandwidth)) { + urls.push(`proxy_${vari.resolution.width}x${vari.resolution.height}`); + urls.push(vari.uri); + } + } else { + urls.push('proxy'); + urls.push(url); + const hlsKey = md5X(url); + hlsCacheInsert(hlsKey, { + code: resp.code, + plist: plist, + key: hlsKey, + headers: resp.headers, + }); + } + } + return urls; +} + +async function hlsCache(url, headers) { + const hlsKey = md5X(url); + if (hlsPlistCaches[hlsKey]) { + return hlsPlistCaches[hlsKey]; + } + let resp = {}; + let tmpUrl = url; + while (true) { + resp = await req(tmpUrl, { + headers: headers, + redirect: 0, + }); + if (resp.headers['location']) { + tmpUrl = resp.headers['location']; + } else { + break; + } + } + if (resp.code == 200) { + var hls = resp.content; + const plist = plistUriResolve(tmpUrl, HLS.parse(hls)); + hlsCacheInsert(hlsKey, { + code: resp.code, + plist: plist, + key: hlsKey, + headers: resp.headers, + }); + return hlsPlistCaches[hlsKey]; + } + return {}; +} + +async function tsCache(hlsKey, segmentIndex, headers) { + if (!hlsPlistCaches[hlsKey]) { + return {}; + } + const plist = hlsPlistCaches[hlsKey].plist; + const segments = plist.segments; + + let startFirst = !downloadTask[hlsKey]; + if (startFirst) { + downloadTask[hlsKey] = {}; + for (const seg of segments) { + const tk = md5X(seg.uri + seg.mediaSequenceNumber.toString()); + downloadTask[hlsKey][tk] = { + file: cacheRoot + '/' + hlsKey + '/' + tk, + uri: seg.uri, + key: tk, + index: seg.mediaSequenceNumber, + order: seg.mediaSequenceNumber, + state: -1, + read: false, + }; + } + } + + // sort task + for (const tk in downloadTask[hlsKey]) { + const task = downloadTask[hlsKey][tk]; + if (task.index >= segmentIndex) { + task.order = task.index - segmentIndex; + } else { + task.order = segments.length - segmentIndex + task.index; + } + } + + if (startFirst) { + fixedCachePool(hlsKey, 5, headers); + } + + const segment = segments[segmentIndex]; + const tsKey = md5X(segment.uri + segment.mediaSequenceNumber.toString()); + const task = downloadTask[hlsKey][tsKey]; + if (task.state == 1 || task.state == -1) { + const file = new JSFile(task.file); + if (await file.exist()) { + task.state = 1; + // download finish + return { + buffer: 3, + code: 200, + headers: { + connection: 'close', + 'content-type': 'video/mp2t', + }, + content: file, + }; + } else { + // file miss?? retry + task.state = -1; + } + } + if (task.state == -1) { + // start download + startTsTask(hlsKey, task, headers); + } + // wait read dwonload + if (task.state == 0) { + var stream = new JSProxyStream(); + stream.head(200, { + connection: 'close', + 'content-type': 'video/mp2t', + }); + let downloaded = 0; + task.read = true; + new Promise(async function (resolve, reject) { + const f = new JSFile(task.file + '.dl'); + await f.open('r'); + (async function waitReadFile() { + const s = await f.size(); + if (s > downloaded) { + var downloadBuf = await f.read(s - downloaded, downloaded); + await stream.write(downloadBuf); + downloaded = s; + } + if (task.state == 1 || task.state < 0) { + // finish error or done + stream.done(); + await f.close(); + await f.delete(); + task.read = false; + resolve(); + return; + } + setTimeout(waitReadFile, 5); + })(); + }); + return { + buffer: 3, + content: stream, + }; + } +} + +async function startTsTask(hlsKey, task, headers) { + if (task.state >= 0) return; + if (!interrupts[hlsKey]) { + return; + } + task.state = 0; + if (await new JSFile(task.file).exist()) { + task.state = 1; + return; + } + const file = new JSFile(task.file + '.dl'); + await file.open('w'); + const resp = await req(task.uri, { + buffer: 3, + headers: headers, + stream: file, + timeout: [5000, 10000], + }); + if (resp.error) { + await file.close(); + if (!task.read) { + await file.delete(); + } + task.state = -1; + return; + } + await file.close(); + if (task.read) { + await file.copy(task.file); + } else { + await file.move(task.file); + } + task.state = 1; +} + +async function fixedCachePool(hlsKey, limit, headers) { + // keep last cache task only + if (currentDownloadHlsKey && currentDownloadHlsKey != hlsKey) { + delete interrupts[currentDownloadHlsKey]; + } + currentDownloadHlsKey = hlsKey; + interrupts[hlsKey] = true; + for (let index = 0; index < limit; index++) { + if (!interrupts[hlsKey]) break; + new Promise(function (resolve, reject) { + (async function doTask() { + if (!interrupts[hlsKey]) { + resolve(); + return; + } + const tasks = _.pickBy(downloadTask[hlsKey], function (o) { + return o.state == -1; + }); + const task = _.minBy(Object.values(tasks), function (o) { + return o.order; + }); + if (!task) { + resolve(); + return; + } + await startTsTask(hlsKey, task, headers); + setTimeout(doTask, 5); + })(); + }); + } +} + +function relative2Absolute(base, relative) { + var stack = base.split('/'), + parts = relative.split('/'); + stack.pop(); + for (var i = 0; i < parts.length; i++) { + if (parts[i] == '.') continue; + if (parts[i] == '..') stack.pop(); + else stack.push(parts[i]); + } + return stack.join('/'); +} + +export function __jsEvalReturn() { + return { + init: init, + home: home, + homeVod: homeVod, + category: category, + detail: detail, + play: play, + proxy: proxy, + search: search, + }; +}