live2cms.js 13 KB

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