ffm3u8_open.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. import { _ } from './lib/cat.js';
  2. import * as HLS from './lib/hls.js';
  3. let key = 'ffm3u8';
  4. let url = '';
  5. let categories = [];
  6. let siteKey = '';
  7. let siteType = 0;
  8. async function request(reqUrl, agentSp) {
  9. let res = await req(reqUrl, {
  10. method: 'get',
  11. });
  12. return JSON.parse(res.content);
  13. }
  14. async function init(cfg) {
  15. siteKey = cfg.skey;
  16. siteType = cfg.stype;
  17. url = cfg.ext.url;
  18. categories = cfg.ext.categories;
  19. }
  20. async function home(filter) {
  21. const data = await request(url);
  22. let classes = [];
  23. for (const cls of data.class) {
  24. const n = cls.type_name.toString().trim();
  25. if (categories && categories.length > 0) {
  26. if (categories.indexOf(n) < 0) continue;
  27. }
  28. classes.push({
  29. type_id: cls.type_id.toString(),
  30. type_name: n,
  31. });
  32. }
  33. if (categories && categories.length > 0) {
  34. classes = _.sortBy(classes, (p) => {
  35. return categories.indexOf(p.type_name);
  36. });
  37. }
  38. return {
  39. class: classes,
  40. };
  41. }
  42. async function homeVod() {
  43. return '{}';
  44. }
  45. async function category(tid, pg, filter, extend) {
  46. let page = pg || 1;
  47. if (page == 0) page = 1;
  48. const data = await request(url + `?ac=detail&t=${tid}&pg=${page}`);
  49. let videos = [];
  50. for (const vod of data.list) {
  51. videos.push({
  52. vod_id: vod.vod_id.toString(),
  53. vod_name: vod.vod_name.toString(),
  54. vod_pic: vod.vod_pic,
  55. vod_remarks: vod.vod_remarks,
  56. });
  57. }
  58. return {
  59. page: parseInt(data.page),
  60. pagecount: data.pagecount,
  61. total: data.total,
  62. list: videos,
  63. };
  64. }
  65. async function detail(id) {
  66. const data = (await request(url + `?ac=detail&ids=${id}`)).list[0];
  67. let vod = {
  68. vod_id: data.vod_id,
  69. vod_name: data.vod_name,
  70. vod_pic: data.vod_pic,
  71. type_name: data.type_name,
  72. vod_year: data.vod_year,
  73. vod_area: data.vod_area,
  74. vod_remarks: data.vod_remarks,
  75. vod_actor: data.vod_actor,
  76. vod_director: data.vod_director,
  77. vod_content: data.vod_content.trim(),
  78. vod_play_from: data.vod_play_from,
  79. vod_play_url: data.vod_play_url,
  80. };
  81. return {
  82. list: [vod],
  83. };
  84. }
  85. async function proxy(segments, headers, reqHeaders) {
  86. let what = segments[0];
  87. let segs = decodeURIComponent(segments[1]);
  88. if (what == 'hls') {
  89. function hlsHeader(data, hls) {
  90. let hlsHeaders = {};
  91. if (data.headers['content-length']) {
  92. Object.assign(hlsHeaders, data.headers, { 'content-length': hls.length.toString() });
  93. } else {
  94. Object.assign(hlsHeaders, data.headers);
  95. }
  96. delete hlsHeaders['transfer-encoding'];
  97. if (hlsHeaders['content-encoding'] == 'gzip') {
  98. delete hlsHeaders['content-encoding'];
  99. }
  100. return hlsHeaders;
  101. }
  102. const hlsData = await hlsCache(segs, headers);
  103. if (hlsData.variants) {
  104. // variants -> variants -> .... ignore
  105. const hls = HLS.stringify(hlsData.plist);
  106. return {
  107. code: hlsData.code,
  108. content: hls,
  109. headers: hlsHeader(hlsData, hls),
  110. };
  111. } else {
  112. const hls = HLS.stringify(hlsData.plist, (segment) => {
  113. return js2Proxy(false, siteType, siteKey, 'ts/' + encodeURIComponent(hlsData.key + '/' + segment.mediaSequenceNumber.toString()), headers);
  114. });
  115. return {
  116. code: hlsData.code,
  117. content: hls,
  118. headers: hlsHeader(hlsData, hls),
  119. };
  120. }
  121. } else if (what == 'ts') {
  122. const info = segs.split('/');
  123. const hlsKey = info[0];
  124. const segIdx = parseInt(info[1]);
  125. return await tsCache(hlsKey, segIdx, headers);
  126. }
  127. return '{}';
  128. }
  129. async function play(flag, id, flags) {
  130. try {
  131. const pUrls = await hls2Urls(id, {});
  132. for (let index = 1; index < pUrls.length; index += 2) {
  133. pUrls[index] = js2Proxy(false, siteType, siteKey, 'hls/' + encodeURIComponent(pUrls[index]), {});
  134. }
  135. pUrls.push('original');
  136. pUrls.push(id);
  137. return {
  138. parse: 0,
  139. url: pUrls,
  140. };
  141. } catch (e) {
  142. return {
  143. parse: 0,
  144. url: id,
  145. };
  146. }
  147. }
  148. async function search(wd, quick, pg) {
  149. let page = pg || 1;
  150. if (page == 0) page = 1;
  151. const data = await request(url + `?ac=detail&wd=${wd}`);
  152. let videos = [];
  153. for (const vod of data.list) {
  154. videos.push({
  155. vod_id: vod.vod_id.toString(),
  156. vod_name: vod.vod_name.toString(),
  157. vod_pic: vod.vod_pic,
  158. vod_remarks: vod.vod_remarks,
  159. });
  160. }
  161. return {
  162. page: parseInt(data.page),
  163. pagecount: data.pagecount,
  164. total: data.total,
  165. list: videos,
  166. };
  167. }
  168. const cacheRoot = 'hls_cache';
  169. const hlsKeys = [];
  170. const hlsPlistCaches = {};
  171. const interrupts = {};
  172. const downloadTask = {};
  173. let currentDownloadHlsKey = '';
  174. function hlsCacheInsert(key, data) {
  175. hlsKeys.push(key);
  176. hlsPlistCaches[key] = data;
  177. if (hlsKeys.length > 5) {
  178. const rmKey = hlsKeys.shift();
  179. hlsCacheRemove(rmKey);
  180. }
  181. }
  182. function hlsCacheRemove(key) {
  183. delete hlsPlistCaches[key];
  184. delete hlsKeys[key];
  185. new JSFile(cacheRoot + '/' + key).delete();
  186. }
  187. function plistUriResolve(baseUrl, plist) {
  188. if (plist.variants) {
  189. for (const v of plist.variants) {
  190. if (!v.uri.startsWith('http')) {
  191. v.uri = relative2Absolute(baseUrl, v.uri);
  192. }
  193. }
  194. }
  195. if (plist.segments) {
  196. for (const s of plist.segments) {
  197. if (!s.uri.startsWith('http')) {
  198. s.uri = relative2Absolute(baseUrl, s.uri);
  199. }
  200. if (s.key && s.key.uri && !s.key.uri.startsWith('http')) {
  201. s.key.uri = relative2Absolute(baseUrl, s.key.uri);
  202. }
  203. }
  204. }
  205. return plist;
  206. }
  207. async function hls2Urls(url, headers) {
  208. let urls = [];
  209. let resp = {};
  210. let tmpUrl = url;
  211. while (true) {
  212. resp = await req(tmpUrl, {
  213. headers: headers,
  214. redirect: 0,
  215. });
  216. if (resp.headers['location']) {
  217. tmpUrl = resp.headers['location'];
  218. } else {
  219. break;
  220. }
  221. }
  222. if (resp.code == 200) {
  223. var hls = resp.content;
  224. const plist = plistUriResolve(tmpUrl, HLS.parse(hls));
  225. if (plist.variants) {
  226. for (const vari of _.sortBy(plist.variants, (v) => -1 * v.bandwidth)) {
  227. urls.push(`proxy_${vari.resolution.width}x${vari.resolution.height}`);
  228. urls.push(vari.uri);
  229. }
  230. } else {
  231. urls.push('proxy');
  232. urls.push(url);
  233. const hlsKey = md5X(url);
  234. hlsCacheInsert(hlsKey, {
  235. code: resp.code,
  236. plist: plist,
  237. key: hlsKey,
  238. headers: resp.headers,
  239. });
  240. }
  241. }
  242. return urls;
  243. }
  244. async function hlsCache(url, headers) {
  245. const hlsKey = md5X(url);
  246. if (hlsPlistCaches[hlsKey]) {
  247. return hlsPlistCaches[hlsKey];
  248. }
  249. let resp = {};
  250. let tmpUrl = url;
  251. while (true) {
  252. resp = await req(tmpUrl, {
  253. headers: headers,
  254. redirect: 0,
  255. });
  256. if (resp.headers['location']) {
  257. tmpUrl = resp.headers['location'];
  258. } else {
  259. break;
  260. }
  261. }
  262. if (resp.code == 200) {
  263. var hls = resp.content;
  264. const plist = plistUriResolve(tmpUrl, HLS.parse(hls));
  265. hlsCacheInsert(hlsKey, {
  266. code: resp.code,
  267. plist: plist,
  268. key: hlsKey,
  269. headers: resp.headers,
  270. });
  271. return hlsPlistCaches[hlsKey];
  272. }
  273. return {};
  274. }
  275. async function tsCache(hlsKey, segmentIndex, headers) {
  276. if (!hlsPlistCaches[hlsKey]) {
  277. return {};
  278. }
  279. const plist = hlsPlistCaches[hlsKey].plist;
  280. const segments = plist.segments;
  281. let startFirst = !downloadTask[hlsKey];
  282. if (startFirst) {
  283. downloadTask[hlsKey] = {};
  284. for (const seg of segments) {
  285. const tk = md5X(seg.uri + seg.mediaSequenceNumber.toString());
  286. downloadTask[hlsKey][tk] = {
  287. file: cacheRoot + '/' + hlsKey + '/' + tk,
  288. uri: seg.uri,
  289. key: tk,
  290. index: seg.mediaSequenceNumber,
  291. order: seg.mediaSequenceNumber,
  292. state: -1,
  293. read: false,
  294. };
  295. }
  296. }
  297. // sort task
  298. for (const tk in downloadTask[hlsKey]) {
  299. const task = downloadTask[hlsKey][tk];
  300. if (task.index >= segmentIndex) {
  301. task.order = task.index - segmentIndex;
  302. } else {
  303. task.order = segments.length - segmentIndex + task.index;
  304. }
  305. }
  306. if (startFirst) {
  307. fixedCachePool(hlsKey, 5, headers);
  308. }
  309. const segment = segments[segmentIndex];
  310. const tsKey = md5X(segment.uri + segment.mediaSequenceNumber.toString());
  311. const task = downloadTask[hlsKey][tsKey];
  312. if (task.state == 1 || task.state == -1) {
  313. const file = new JSFile(task.file);
  314. if (await file.exist()) {
  315. task.state = 1;
  316. // download finish
  317. return {
  318. buffer: 3,
  319. code: 200,
  320. headers: {
  321. connection: 'close',
  322. 'content-type': 'video/mp2t',
  323. },
  324. content: file,
  325. };
  326. } else {
  327. // file miss?? retry
  328. task.state = -1;
  329. }
  330. }
  331. if (task.state == -1) {
  332. // start download
  333. startTsTask(hlsKey, task, headers);
  334. }
  335. // wait read dwonload
  336. if (task.state == 0) {
  337. var stream = new JSProxyStream();
  338. stream.head(200, {
  339. connection: 'close',
  340. 'content-type': 'video/mp2t',
  341. });
  342. let downloaded = 0;
  343. task.read = true;
  344. new Promise(async function (resolve, reject) {
  345. const f = new JSFile(task.file + '.dl');
  346. await f.open('r');
  347. (async function waitReadFile() {
  348. const s = await f.size();
  349. if (s > downloaded) {
  350. var downloadBuf = await f.read(s - downloaded, downloaded);
  351. await stream.write(downloadBuf);
  352. downloaded = s;
  353. }
  354. if (task.state == 1 || task.state < 0) {
  355. // finish error or done
  356. stream.done();
  357. await f.close();
  358. await f.delete();
  359. task.read = false;
  360. resolve();
  361. return;
  362. }
  363. setTimeout(waitReadFile, 5);
  364. })();
  365. });
  366. return {
  367. buffer: 3,
  368. content: stream,
  369. };
  370. }
  371. }
  372. async function startTsTask(hlsKey, task, headers) {
  373. if (task.state >= 0) return;
  374. if (!interrupts[hlsKey]) {
  375. return;
  376. }
  377. task.state = 0;
  378. if (await new JSFile(task.file).exist()) {
  379. task.state = 1;
  380. return;
  381. }
  382. const file = new JSFile(task.file + '.dl');
  383. await file.open('w');
  384. const resp = await req(task.uri, {
  385. buffer: 3,
  386. headers: headers,
  387. stream: file,
  388. timeout: [5000, 10000],
  389. });
  390. if (resp.error || resp.code >= 300) {
  391. await file.close();
  392. if (!task.read) {
  393. await file.delete();
  394. }
  395. task.state = -1;
  396. return;
  397. }
  398. await file.close();
  399. if (task.read) {
  400. await file.copy(task.file);
  401. } else {
  402. await file.move(task.file);
  403. }
  404. task.state = 1;
  405. }
  406. async function fixedCachePool(hlsKey, limit, headers) {
  407. // keep last cache task only
  408. if (currentDownloadHlsKey && currentDownloadHlsKey != hlsKey) {
  409. delete interrupts[currentDownloadHlsKey];
  410. }
  411. currentDownloadHlsKey = hlsKey;
  412. interrupts[hlsKey] = true;
  413. for (let index = 0; index < limit; index++) {
  414. if (!interrupts[hlsKey]) break;
  415. new Promise(function (resolve, reject) {
  416. (async function doTask() {
  417. if (!interrupts[hlsKey]) {
  418. resolve();
  419. return;
  420. }
  421. const tasks = _.pickBy(downloadTask[hlsKey], function (o) {
  422. return o.state == -1;
  423. });
  424. const task = _.minBy(Object.values(tasks), function (o) {
  425. return o.order;
  426. });
  427. if (!task) {
  428. resolve();
  429. return;
  430. }
  431. await startTsTask(hlsKey, task, headers);
  432. setTimeout(doTask, 5);
  433. })();
  434. });
  435. }
  436. }
  437. function relative2Absolute(base, relative) {
  438. var stack = base.split('/'),
  439. parts = relative.split('/');
  440. stack.pop();
  441. for (var i = 0; i < parts.length; i++) {
  442. if (parts[i] == '.') continue;
  443. if (parts[i] == '..') stack.pop();
  444. else stack.push(parts[i]);
  445. }
  446. return stack.join('/');
  447. }
  448. export function __jsEvalReturn() {
  449. return {
  450. init: init,
  451. home: home,
  452. homeVod: homeVod,
  453. category: category,
  454. detail: detail,
  455. play: play,
  456. proxy: proxy,
  457. search: search,
  458. };
  459. }