Merge remote-tracking branch 'upstream/main'

main
FongMi 2 years ago
commit 4742130b20
  1. 10
      open/config_open.json
  2. 484
      open/ffm3u8_open.js
  3. 1
      open/lib/hls.js
  4. 19
      open/testVideo.js
  5. 71
      open/wrapper/index.js

@ -6,6 +6,16 @@
"name": "琨娱七七",
"type": 3,
"api": "assets://js/kunyu77_open.js"
},
{
"key": "ffm3u8",
"name": "非凡",
"type": 3,
"api": "assets://js/ffm3u8_open.js",
"ext": {
"url": "https://cj.ffzyapi.com/api.php/provide/vod/from/ffm3u8/",
"categories": ["国产剧", "香港剧", "韩国剧", "欧美剧", "台湾剧", "日本剧", "海外剧", "泰国剧", "短剧", "动作片", "喜剧片", "爱情片", "科幻片", "恐怖片", "剧情片", "战争片", "动漫片", "大陆综艺", "港台综艺", "日韩综艺", "欧美综艺", "国产动漫", "日韩动漫", "欧美动漫", "港台动漫", "海外动漫", "记录片"]
}
}
]
},

@ -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

@ -2,6 +2,13 @@ import { __jsEvalReturn } from './kunyu77_open.js';
var spider = __jsEvalReturn();
function jsonParse(obj) {
if (typeof obj === 'string') {
return JSON.parse(obj);
}
return obj;
}
async function test() {
var spType = null;
var spVid = null;
@ -9,17 +16,17 @@ async function test() {
// spVid = '95873';
await spider.init({ skey: 'siteKey', ext: '' });
var classes = JSON.parse(await spider.home(true));
var classes = jsonParse(await spider.home(true));
console.log(classes);
var homeVod = JSON.parse(await spider.homeVod());
var homeVod = jsonParse(await spider.homeVod());
console.log(homeVod);
if (classes.class && classes.class.length > 0) {
var page = JSON.parse(await spider.category(spType || classes.class[0].type_id, 0, undefined, {}));
var page = jsonParse(await spider.category(spType || classes.class[0].type_id, 0, undefined, {}));
console.log(page);
if (page.list && page.list.length > 0) {
for (const k in page.list) {
if (k >= 5) break;
var detail = JSON.parse(await spider.detail(spVid || page.list[k].vod_id));
var detail = jsonParse(await spider.detail(spVid || page.list[k].vod_id));
console.log(detail);
if (detail.list && detail.list.length > 0) {
var pFlag = detail.list[0].vod_play_from.split('$$$');
@ -41,10 +48,10 @@ async function test() {
}
}
}
var search = JSON.parse(await spider.search('奥特曼'));
var search = jsonParse(await spider.search('奥特曼'));
console.log(search);
search = JSON.parse(await spider.search('喜欢'));
search = jsonParse(await spider.search('喜欢'));
console.log(search);
}

@ -301,6 +301,7 @@ globalThis.JSProxyStream = function () {
this.error = async function (err) {};
};
/**
* Creates a new JSFile object with the specified path.
*
@ -327,6 +328,12 @@ globalThis.JSFile = function (path) {
this.open = async function (mode) {
const file = this;
return await new Promise((resolve, reject) => {
if (mode == 'w' || mode == 'a') {
const directoryPath = dirname(file._path);
if (!fs.existsSync(directoryPath)) {
fs.mkdirSync(directoryPath, { recursive: true });
}
}
fs.open(file._path, mode, null, (e, f) => {
if (!e) file.fd = f;
if (file.fd) resolve(true);
@ -373,6 +380,13 @@ globalThis.JSFile = function (path) {
});
};
/**
* Flush buffers to disk.
*/
this.flush = async function () {
return;
};
/**
* Closes the file descriptor.
*
@ -391,7 +405,7 @@ globalThis.JSFile = function (path) {
* Moves the file to a new path.
*
* @param {string} newPath - The new path where the file will be moved.
* @return {boolean} Returns true if the file was successfully moved, otherwise returns false.
* @return {Promise<boolean>} A promise that resolves with `true` if the file was successfully moved, otherwise returns false.
*/
this.move = async function (newPath) {
const file = this;
@ -403,6 +417,22 @@ globalThis.JSFile = function (path) {
});
};
/**
* Copies the file to a new path.
*
* @param {string} newPath - The path of the new location where the file will be copied.
* @return {Promise<boolean>} A promise that resolves with `true` if the file is successfully copied, and `false` otherwise.
*/
this.copy = async function (newPath) {
const file = this;
return await new Promise((resolve, reject) => {
fs.copyFile(file._path, newPath, (err) => {
if (!err) resolve(true);
else resolve(false);
});
});
};
/**
* Deletes the file associated with this object.
*
@ -415,18 +445,39 @@ globalThis.JSFile = function (path) {
});
});
};
};
globalThis.url2Proxy = async function (type, url, headers) {
let hd = Object.keys(headers).length == 0 ? '_' : encodeURIComponent(JSON.stringify(headers));
let uri = new Uri(url);
let path = uri.path();
path = path.substring(path.lastIndexOf('/'));
let ext = path.indexOf('.') >= 0 ? path.substring(path.indexOf('.')) : '.bin';
return 'http://127.0.0.1:13333/up/' + randStr(6) + '/' + type + '/' + hd + '/' + encodeURIComponent(url) + '/' + ext;
/**
* Checks if the file exists.
*
* @return {Promise<boolean>} A promise that resolves to a boolean value indicating whether the file exists or not.
*/
this.exist = async function () {
const file = this;
return await new Promise((resolve, reject) => {
fs.exists(file._path, (stat) => {
resolve(stat);
});
});
};
/**
* @returns the file length
*/
this.size = async function () {
const file = this;
return await new Promise((resolve, reject) => {
fs.stat(file._path, (err, stat) => {
if (err) {
resolve(0);
} else {
resolve(stat.size);
}
});
});
};
};
globalThis.js2Proxy = async function (dynamic, siteType, site, url, headers) {
globalThis.js2Proxy = function (dynamic, siteType, site, url, headers) {
let hd = Object.keys(headers).length == 0 ? '_' : encodeURIComponent(JSON.stringify(headers));
return (dynamic ? 'js2p://_WEB_/' : 'http://127.0.0.1:13333/jp/') + randStr(6) + '/' + siteType + '/' + site + '/' + hd + '/' + encodeURIComponent(url);
};

Loading…
Cancel
Save