480 lines
13 KiB
JavaScript
480 lines
13 KiB
JavaScript
/*
|
|
* @File : ffm3u8_open.js.js
|
|
* @Author : jade
|
|
* @Date : 2024/2/5 16:06
|
|
* @Email : jadehh@1ive.com
|
|
* @Software : Samples
|
|
* @Desc :
|
|
*/
|
|
import { _ } from './cat.js';
|
|
import * as HLS from './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 {hls2Urls,hlsCache,tsCache} |