mirror of https://github.com/FongMi/CatVodOpen.git
Compare commits
10 Commits
0be85b3b2a
...
4742130b20
| Author | SHA1 | Date |
|---|---|---|
|
|
4742130b20 | 2 years ago |
|
|
0239319813 | 2 years ago |
|
|
e5e20a98ac | 2 years ago |
|
|
1c305bbcc5 | 2 years ago |
|
|
c8fb263dde | 2 years ago |
|
|
549a1e0232 | 2 years ago |
|
|
684c4f395f | 2 years ago |
|
|
12d06291c0 | 2 years ago |
|
|
1f391686a5 | 2 years ago |
|
|
df369d4a03 | 2 years ago |
@ -0,0 +1,484 @@ |
|||||||
|
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') { |
||||||
|
function hlsHeader(data, hls) { |
||||||
|
let hlsHeaders = {}; |
||||||
|
if (data.headers['content-length']) { |
||||||
|
Object.assign(hlsHeaders, data.headers, { 'content-length': hls.length.toString() }); |
||||||
|
} else { |
||||||
|
Object.assign(hlsHeaders, data.headers); |
||||||
|
} |
||||||
|
delete hlsHeaders['transfer-encoding']; |
||||||
|
if (hlsHeaders['content-encoding'] == 'gzip') { |
||||||
|
delete hlsHeaders['content-encoding']; |
||||||
|
} |
||||||
|
return hlsHeaders; |
||||||
|
} |
||||||
|
const hlsData = await hlsCache(segs, headers); |
||||||
|
if (hlsData.variants) { |
||||||
|
// variants -> variants -> .... ignore
|
||||||
|
const hls = HLS.stringify(hlsData.plist); |
||||||
|
return { |
||||||
|
code: hlsData.code, |
||||||
|
content: hls, |
||||||
|
headers: hlsHeader(hlsData, hls), |
||||||
|
}; |
||||||
|
} else { |
||||||
|
const hls = HLS.stringify(hlsData.plist, (segment) => { |
||||||
|
return js2Proxy(false, siteType, siteKey, 'ts/' + encodeURIComponent(hlsData.key + '/' + segment.mediaSequenceNumber.toString()), headers); |
||||||
|
}); |
||||||
|
return { |
||||||
|
code: hlsData.code, |
||||||
|
content: hls, |
||||||
|
headers: hlsHeader(hlsData, hls), |
||||||
|
}; |
||||||
|
} |
||||||
|
} 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 || resp.code >= 300) { |
||||||
|
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, |
||||||
|
}; |
||||||
|
} |
||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue