live2cms.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. /**
  2. * live2cms.js
  3. * 配置设置 {"key":"Live2CMS","name":"直播转点播V2","type":3,"api":"{{host}}/libs/live2cms.js","searchable":2,"quickSearch":0,"filterable":0,"ext":"{{host}}/txt/json/live2mv_data.json"}
  4. * live2mv_data.json
  5. * 支持m3u类直播,支持线路归并。支持筛选切换显示模式
  6. [
  7. {"name": "甜蜜", "url": "http://zdir.kebedd69.repl.co/public/live.txt"},
  8. {"name": "俊于", "url": "http://home.jundie.top:81/Cat/tv/live.txt"},
  9. {"name": "菜妮丝", "url": "http://xn--ihqu10cn4c.xn--z7x900a.love:63/TV/tvzb.txt"},
  10. {"name": "布里m3u", "url": "http://jiexi.bulisite.top/m3u.php"},
  11. {"name": "吾爱", "url": "http://52bsj.vip:81/api/v3/file/get/763/live.txt?sign=87BTGT1_6AOry7FPwy_uuxFTv2Wcb9aDMj46rDdRTD8%3D%3A0"},
  12. {"name": "饭太硬", "url": "http://ftyyy.tk/live.txt"}
  13. ]
  14. * 提示 ext文件格式为json列表,name,url参数
  15. * 取消加密,减少性能问题
  16. */
  17. String.prototype.rstrip = function (chars) {
  18. let regex = new RegExp(chars + "$");
  19. return this.replace(regex, "");
  20. };
  21. const request_timeout = 5000;
  22. const RKEY = 'live2cms'; // 源的唯一标识
  23. const VERSION = 'live2cms 20230619';
  24. const UA = 'Mozilla/5.0'; //默认请求ua
  25. const __ext = {data_dict:{}};
  26. //const tips = `\n道长直播转点播js-当前版本${VERSION}`;
  27. const tips = `\nDRPY2+-当前版本${VERSION}`;
  28. const def_pic = 'https://notabug.org/xi-ao/KR/raw/master/kr.png';
  29. //const def_pic = 'https://avatars.githubusercontent.com/u/97389433?s=120&v=4';
  30. /**
  31. * 存在数据库配置表里, key字段对应值value,没有就新增,有就更新,调用此方法会清除key对应的内存缓存
  32. * @param k 键
  33. * @param v 值
  34. */
  35. function setItem(k,v){
  36. local.set(RKEY,k,v);
  37. console.log(`规则${RKEY}设置${k} => ${v}`)
  38. }
  39. /**
  40. * 获取数据库配置表对应的key字段的value,没有这个key就返回value默认传参.需要有缓存,第一次获取后会存在内存里
  41. * @param k 键
  42. * @param v 值
  43. * @returns {*}
  44. */
  45. function getItem(k,v){
  46. return local.get(RKEY,k) || v;
  47. }
  48. /**
  49. * 删除数据库key对应的一条数据,并清除此key对应的内存缓存
  50. * @param k
  51. */
  52. function clearItem(k){
  53. local.delete(RKEY,k);
  54. }
  55. var showMode = getItem('showMode','groups'); // groups按组分类显示 all全部一条线路展示
  56. var groupDict = JSON.parse(getItem('groupDict','{}')); // 搜索分组字典
  57. /**
  58. * 打印日志
  59. * @param any 任意变量
  60. */
  61. function print(any){
  62. any = any||'';
  63. if(typeof(any)=='object'&&Object.keys(any).length>0){
  64. try {
  65. any = JSON.stringify(any);
  66. console.log(any);
  67. }catch (e) {
  68. // console.log('print:'+e.message);
  69. console.log(typeof(any)+':'+any.length);
  70. }
  71. }else if(typeof(any)=='object'&&Object.keys(any).length<1){
  72. console.log('null object');
  73. }else{
  74. console.log(any);
  75. }
  76. }
  77. /*** js自封装的方法 ***/
  78. /**
  79. * 获取链接的host(带http协议的完整链接)
  80. * @param url 任意一个正常完整的Url,自动提取根
  81. * @returns {string}
  82. */
  83. function getHome(url){
  84. if(!url){
  85. return ''
  86. }
  87. let tmp = url.split('//');
  88. url = tmp[0] + '//' + tmp[1].split('/')[0];
  89. try {
  90. url = decodeURIComponent(url);
  91. }catch (e) {}
  92. return url
  93. }
  94. /**
  95. * m3u直播格式转一般直播格式
  96. * @param m3u
  97. * @returns {string}
  98. */
  99. function convertM3uToNormal(m3u) {
  100. try {
  101. const lines = m3u.split('\n');
  102. let result = '';
  103. let TV='';
  104. // let flag='#genre#';
  105. let flag='#m3u#';
  106. let currentGroupTitle = '';
  107. lines.forEach((line) => {
  108. if (line.startsWith('#EXTINF:')) {
  109. const groupTitle = line.split('"')[1].trim();
  110. TV= line.split('"')[2].substring(1);
  111. if (currentGroupTitle !== groupTitle) {
  112. currentGroupTitle = groupTitle;
  113. result += `\n${currentGroupTitle},${flag}\n`;
  114. }
  115. } else if (line.startsWith('http')) {
  116. const splitLine = line.split(',');
  117. result += `${TV}\,${splitLine[0]}\n`;
  118. }
  119. });
  120. return result.trim();
  121. }catch (e) {
  122. print(`m3u直播转普通直播发生错误:${e.message}`);
  123. return m3u
  124. }
  125. }
  126. /**
  127. * 线路归类
  128. * @param arr
  129. * @returns {*[][]}
  130. */
  131. function merge(arr) {
  132. var parse = arguments[1] ? arguments[1] : '';
  133. var p = [];
  134. if (parse !== '' && typeof(parse)=="function") {
  135. p = arr.map(parse);
  136. }
  137. const createEmptyArrays = (length) => Array.from({
  138. length
  139. }, () => []);
  140. let lists = createEmptyArrays(arr.length);
  141. let sl = createEmptyArrays(arr.length);
  142. (p.length ? p : arr).forEach((k, index) => {
  143. var i = 0;
  144. while (sl[i].includes(k)) {
  145. i = i + 1
  146. }
  147. sl[i].push(k);
  148. lists[i].push(arr[index]);
  149. })
  150. lists=lists.filter(x=>x.some(k=>k.length));
  151. return lists
  152. }
  153. /**
  154. * 线路归类/小棉袄算法
  155. * @param arr 数组
  156. * @param parse 解析式
  157. * @returns {[[*]]}
  158. */
  159. function splitArray(arr,parse) {
  160. parse = parse&&typeof(parse)=='function'?parse:'';
  161. let result = [[arr[0]]];
  162. for (let i = 1; i < arr.length; i++) {
  163. let index = -1;
  164. for (let j = 0; j < result.length; j++) {
  165. if (parse&&result[j].map(parse).includes(parse(arr[i]))) {
  166. index = j;
  167. }else if((!parse) && result[j].includes(arr[i])){
  168. index = j;
  169. }
  170. }
  171. if (index >= result.length - 1) {
  172. result.push([]);
  173. result[result.length - 1].push(arr[i]);
  174. } else {
  175. result[index + 1].push(arr[i]);
  176. }
  177. }
  178. return result;
  179. }
  180. /**
  181. * 搜索结果生成分组字典
  182. * @param arr
  183. * @param parse x=>x.split(',')[0]
  184. * @returns {{}}
  185. */
  186. function gen_group_dict(arr,parse){
  187. let dict = {};
  188. arr.forEach((it)=>{
  189. let k = it.split(',')[0];
  190. if(parse && typeof(parse)==='function'){
  191. k = parse(k);
  192. }
  193. if(!dict[k]){
  194. dict[k] = [it]
  195. }else{
  196. dict[k].push(it);
  197. }
  198. });
  199. return dict
  200. }
  201. const http = function (url, options = {}) {
  202. if(options.method ==='POST' && options.data){
  203. options.body = JSON.stringify(options.data);
  204. options.headers = Object.assign({'content-type':'application/json'}, options.headers);
  205. }
  206. options.timeout = request_timeout;
  207. if(!options.headers){
  208. options.headers = {};
  209. }
  210. let keys = Object.keys(options.headers).map(it=>it.toLowerCase());
  211. if(!keys.includes('referer')){
  212. options.headers['Referer'] = getHome(url);
  213. }
  214. if(!keys.includes('user-agent')){
  215. options.headers['User-Agent'] = UA;
  216. }
  217. console.log(JSON.stringify(options.headers));
  218. try {
  219. const res = req(url, options);
  220. // if(options.headers['Authorization']){
  221. // console.log(res.content);
  222. // }
  223. res.json = () => res&&res.content ? JSON.parse(res.content) : null;
  224. res.text = () => res&&res.content ? res.content:'';
  225. return res
  226. }catch (e) {
  227. return {
  228. json() {
  229. return null
  230. }, text() {
  231. return ''
  232. }
  233. }
  234. }
  235. };
  236. ["get", "post"].forEach(method => {
  237. http[method] = function (url, options = {}) {
  238. return http(url, Object.assign(options, {method: method.toUpperCase()}));
  239. }
  240. });
  241. function init(ext) {
  242. console.log("当前版本号:"+VERSION);
  243. let data;
  244. if (typeof ext == 'object'){
  245. data = ext;
  246. print('live ext:object');
  247. } else if (typeof ext == 'string') {
  248. if (ext.startsWith('http')) {
  249. let ext_paramas = ext.split(';');
  250. let data_url = ext_paramas[0];
  251. print(data_url);
  252. data = http.get(data_url).json();
  253. }
  254. }
  255. print(data);
  256. __ext.data = data;
  257. print('init执行完毕');
  258. }
  259. function home(filter) {
  260. let classes = __ext.data.map(it => ({
  261. type_id: it.url,
  262. type_name: it.name,
  263. }));
  264. print("----home----");
  265. let filter_dict = {};
  266. let filters = [
  267. {'key': 'show', 'name': '播放展示', 'value': [{'n': '多线路分组', 'v': 'groups'},{'n': '单线路', 'v': 'all'}]}
  268. ];
  269. classes.forEach(it=>{
  270. filter_dict[it.type_id] = filters;
  271. });
  272. print(classes);
  273. return JSON.stringify({ 'class': classes,'filters': filter_dict});
  274. }
  275. function homeVod(params) {
  276. let _get_url = __ext.data[0].url;
  277. let html;
  278. if(__ext.data_dict[_get_url]){
  279. html = __ext.data_dict[_get_url];
  280. }else{
  281. html = http.get(_get_url).text();
  282. if(/#EXTM3U/.test(html)){
  283. html = convertM3uToNormal(html);
  284. }
  285. __ext.data_dict[_get_url] = html;
  286. }
  287. // let arr = html.match(/.*?,#[\s\S].*?#/g);
  288. let arr = html.match(/.*?[,,]#[\s\S].*?#/g); // 可能存在中文逗号
  289. let _list = [];
  290. try {
  291. arr.forEach(it=>{
  292. let vname = it.split(/[,,]/)[0];
  293. let vtab = it.match(/#(.*?)#/)[0];
  294. _list.push({
  295. vod_name:vname,
  296. vod_id:_get_url+'$'+vname,
  297. vod_pic:def_pic,
  298. vod_remarks:vtab,
  299. });
  300. });
  301. }catch (e) {
  302. print('Live2cms获取首页推荐发送错误:'+e.message);
  303. }
  304. return JSON.stringify({ 'list': _list });
  305. }
  306. function category(tid, pg, filter, extend) {
  307. let fl = filter?extend:{};
  308. if(fl.show){
  309. showMode = fl.show;
  310. setItem('showMode',showMode);
  311. }
  312. if(parseInt(pg)>1){
  313. return JSON.stringify({
  314. 'list': [],
  315. });
  316. }
  317. let _get_url = tid;
  318. let html;
  319. if(__ext.data_dict[_get_url]){
  320. html = __ext.data_dict[_get_url];
  321. }else{
  322. html = http.get(_get_url).text();
  323. if(/#EXTM3U/.test(html)){
  324. html = convertM3uToNormal(html);
  325. }
  326. __ext.data_dict[_get_url] = html;
  327. }
  328. // let arr = html.match(/.*?[,,]#[\s\S].*?#/g);
  329. let arr = html.match(/.*?[,,]#[\s\S].*?#/g); // 可能存在中文逗号
  330. let _list = [];
  331. try {
  332. arr.forEach(it=>{
  333. let vname = it.split(/[,,]/)[0];
  334. let vtab = it.match(/#(.*?)#/)[0];
  335. _list.push({
  336. // vod_name:it.split(',')[0],
  337. vod_name:vname,
  338. vod_id:_get_url+'$'+vname,
  339. vod_pic:def_pic,
  340. vod_remarks:vtab,
  341. });
  342. });
  343. }catch (e) {
  344. print('Live2cms获取一级分类页发生错误:'+e.message);
  345. }
  346. return JSON.stringify({
  347. 'page': 1,
  348. 'pagecount': 1,
  349. 'limit': _list.length,
  350. 'total': _list.length,
  351. 'list': _list,
  352. });
  353. }
  354. function detail(tid) { // ⛵ 港•澳•台
  355. let _get_url = tid.split('$')[0];
  356. let _tab = tid.split('$')[1];
  357. if(tid.includes('#search#')){
  358. let vod_name = _tab.replace('#search#','');
  359. let vod_play_from = '来自搜索';
  360. vod_play_from+=`:${_get_url}`;
  361. // let vod_play_url = vod_name+'$'+_get_url;
  362. // print(vod_play_url);
  363. let vod_play_url = groupDict[_get_url].map(x=>x.replace(',','$')).join('#');
  364. return JSON.stringify({
  365. list: [{
  366. vod_id: tid,
  367. vod_name: '搜索:'+vod_name,
  368. type_name: "直播列表",
  369. vod_pic: def_pic,
  370. vod_content: tid,
  371. vod_play_from: vod_play_from,
  372. vod_play_url: vod_play_url,
  373. vod_director: tips,
  374. vod_remarks: `道长直播转点播js-当前版本${VERSION}`,
  375. }]
  376. });
  377. }
  378. let html;
  379. if(__ext.data_dict[_get_url]){
  380. html = __ext.data_dict[_get_url];
  381. }else{
  382. html = http.get(_get_url).text();
  383. if(/#EXTM3U/.test(html)){
  384. html = convertM3uToNormal(html);
  385. }
  386. __ext.data_dict[_get_url] = html;
  387. }
  388. // let a = new RegExp(`.*?${_tab},#[\\s\\S].*?#`);
  389. let a = new RegExp(`.*?${_tab.replace('(','\\(').replace(')','\\)')}[,,]#[\\s\\S].*?#`);
  390. let b = html.match(a)[0];
  391. let c = html.split(b)[1];
  392. if(c.match(/.*?[,,]#[\s\S].*?#/)){
  393. let d = c.match(/.*?[,,]#[\s\S].*?#/)[0];
  394. c = c.split(d)[0];
  395. }
  396. let arr = c.trim().split('\n');
  397. let _list = [];
  398. arr.forEach((it)=>{
  399. if(it.trim()){
  400. let t = it.trim().split(',')[0];
  401. let u = it.trim().split(',')[1];
  402. _list.push(t+'$'+u);
  403. }
  404. });
  405. let vod_name = __ext.data.find(x=>x.url===_get_url).name;
  406. let vod_play_url;
  407. let vod_play_from;
  408. if(showMode==='groups'){
  409. let groups = splitArray(_list,x=>x.split('$')[0]);
  410. let tabs = [];
  411. for(let i=0;i<groups.length;i++){
  412. if(i===0){
  413. tabs.push(vod_name+'1')
  414. }else{
  415. tabs.push(` ${i+1} `)
  416. }
  417. }
  418. vod_play_url = groups.map(it=>it.join('#')).join('$$$');
  419. vod_play_from = tabs.join('$$$');
  420. }else{
  421. vod_play_url = _list.join('#');
  422. vod_play_from = vod_name;
  423. }
  424. let vod = {
  425. vod_id: tid,
  426. vod_name: vod_name+'|'+_tab,
  427. type_name: "直播列表",
  428. vod_pic: def_pic,
  429. vod_content: tid,
  430. vod_play_from: vod_play_from,
  431. vod_play_url: vod_play_url,
  432. vod_director: tips,
  433. vod_remarks: `道长直播转点播js-当前版本${VERSION}`,
  434. };
  435. return JSON.stringify({
  436. list: [vod]
  437. });
  438. }
  439. function play(flag, id, flags) {
  440. let vod = {
  441. 'parse': /m3u8/.test(id)?0:1,
  442. 'playUrl': '',
  443. 'url': id
  444. };
  445. print(vod);
  446. return JSON.stringify(vod);
  447. }
  448. function search(wd, quick) {
  449. let _get_url = __ext.data[0].url;
  450. let html;
  451. if(__ext.data_dict[_get_url]){
  452. html = __ext.data_dict[_get_url];
  453. }else{
  454. html = http.get(_get_url).text();
  455. if(/#EXTM3U/.test(html)){
  456. html = convertM3uToNormal(html);
  457. }
  458. __ext.data_dict[_get_url] = html;
  459. }
  460. let str='';
  461. Object.keys(__ext.data_dict).forEach(()=>{
  462. str+=__ext.data_dict[_get_url];
  463. });
  464. let links = str.split('\n').filter(it=>it.trim() && it.includes(',') && it.split(',')[1].trim().startsWith('http'));
  465. links = links.map(it=>it.trim());
  466. let plays = Array.from(new Set(links));
  467. print('搜索关键词:'+wd);
  468. print('过滤前:'+plays.length);
  469. plays = plays.filter(it=>it.includes(wd));
  470. print('过滤后:'+plays.length);
  471. print(plays);
  472. let new_group = gen_group_dict(plays);
  473. groupDict = Object.assign(groupDict,new_group);
  474. // 搜索分组结果存至本地方便二级调用
  475. setItem('groupDict',JSON.stringify(groupDict));
  476. let _list = [];
  477. // plays.forEach((it)=>{
  478. // _list.push({
  479. // 'vod_name':it.split(',')[0],
  480. // 'vod_id':it.split(',')[1].trim()+'$'+it.split(',')[0].trim()+'#search#',
  481. // 'vod_pic':def_pic,
  482. // })
  483. // });
  484. Object.keys(groupDict).forEach((it)=>{
  485. _list.push({
  486. 'vod_name':it,
  487. 'vod_id':it+'$'+wd+'#search#',
  488. 'vod_pic':def_pic,
  489. });
  490. });
  491. return JSON.stringify({
  492. 'list': _list
  493. });
  494. }
  495. // 导出函数对象
  496. export default {
  497. init: init,
  498. home: home,
  499. homeVod: homeVod,
  500. category: category,
  501. detail: detail,
  502. play: play,
  503. search: search
  504. }