config.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. import {readdirSync, readFileSync, writeFileSync, existsSync} from 'fs';
  2. import path from 'path';
  3. import * as drpy from '../libs/drpyS.js';
  4. import '../libs_drpy/jinja.js'
  5. import {naturalSort, urljoin, updateQueryString} from '../utils/utils.js'
  6. import {md5} from "../libs_drpy/crypto-util.js";
  7. import {ENV} from "../utils/env.js";
  8. import {validateBasicAuth, validatePwd} from "../utils/api_validate.js";
  9. import {getSitesMap} from "../utils/sites-map.js";
  10. import {getParsesDict} from "../utils/file.js";
  11. import batchExecute from '../libs_drpy/batchExecute.js';
  12. const {jsEncoder} = drpy;
  13. // 工具函数:生成 JSON 数据
  14. async function generateSiteJSON(options, requestHost, sub, pwd) {
  15. const jsDir = options.jsDir;
  16. const dr2Dir = options.dr2Dir;
  17. const configDir = options.configDir;
  18. const subFilePath = options.subFilePath;
  19. const rootDir = options.rootDir;
  20. const files = readdirSync(jsDir);
  21. let valid_files = files.filter((file) => file.endsWith('.js') && !file.startsWith('_')); // 筛选出不是 "_" 开头的 .js 文件
  22. let sort_list = [];
  23. if (sub) {
  24. if (sub.mode === 0) {
  25. valid_files = valid_files.filter(it => (new RegExp(sub.reg || '.*')).test(it));
  26. } else if (sub.mode === 1) {
  27. valid_files = valid_files.filter(it => !(new RegExp(sub.reg || '.*')).test(it));
  28. }
  29. let sort_file = path.join(path.dirname(subFilePath), `./order_common.html`);
  30. if (!existsSync(sort_file)) {
  31. sort_file = path.join(path.dirname(subFilePath), `./order_common.example.html`);
  32. }
  33. if (sub.sort) {
  34. sort_file = path.join(path.dirname(subFilePath), `./${sub.sort}.html`);
  35. if (!existsSync(sort_file)) {
  36. sort_file = path.join(path.dirname(subFilePath), `./${sub.sort}.example.html`);
  37. }
  38. }
  39. if (existsSync(sort_file)) {
  40. console.log('sort_file:', sort_file);
  41. let sort_file_content = readFileSync(sort_file, 'utf-8');
  42. // console.log(sort_file_content)
  43. sort_list = sort_file_content.split('\n').filter(it => it.trim()).map(it => it.trim());
  44. // console.log(sort_list);
  45. }
  46. }
  47. let sites = [];
  48. let link_jar = '';
  49. // console.log('hide_adult:', ENV.get('hide_adult'));
  50. if (ENV.get('hide_adult') === '1') {
  51. valid_files = valid_files.filter(it => !(new RegExp('\\[[密]\\]|密+')).test(it));
  52. }
  53. let SitesMap = getSitesMap(configDir);
  54. // console.log(SitesMap);
  55. const tasks = valid_files.map((file) => {
  56. return {
  57. func: async ({file, jsDir, requestHost, pwd, drpy, SitesMap, jsEncoder}) => {
  58. const baseName = path.basename(file, '.js'); // 去掉文件扩展名
  59. let api = `${requestHost}/api/${baseName}`; // 使用请求的 host 地址,避免硬编码端口
  60. if (pwd) {
  61. api += `?pwd=${pwd}`;
  62. }
  63. let ruleObject = {
  64. searchable: 0, // 固定值
  65. filterable: 0, // 固定值
  66. quickSearch: 0, // 固定值
  67. };
  68. try {
  69. ruleObject = await drpy.getRuleObject(path.join(jsDir, file));
  70. } catch (e) {
  71. throw new Error(`Error parsing rule object for file: ${file}, ${e.message}`);
  72. }
  73. let fileSites = [];
  74. if (baseName === 'push_agent') {
  75. let key = 'push_agent';
  76. let name = `${ruleObject.title}(DS)`;
  77. fileSites.push({key, name});
  78. } else if (SitesMap.hasOwnProperty(baseName) && Array.isArray(SitesMap[baseName])) {
  79. SitesMap[baseName].forEach((it) => {
  80. let key = `drpyS_${it.alias}`;
  81. let name = `${it.alias}(DS)`;
  82. let ext = it.queryObject.type === 'url' ? it.queryObject.params : it.queryStr;
  83. if (ext) {
  84. ext = jsEncoder.gzip(ext);
  85. }
  86. fileSites.push({key, name, ext});
  87. });
  88. } else {
  89. let key = `drpyS_${baseName}`;
  90. let name = `${baseName}(DS)`;
  91. fileSites.push({key, name});
  92. }
  93. fileSites.forEach((fileSite) => {
  94. const site = {
  95. key: fileSite.key,
  96. name: fileSite.name,
  97. type: 4, // 固定值
  98. api,
  99. searchable: ruleObject.searchable,
  100. filterable: ruleObject.filterable,
  101. quickSearch: ruleObject.quickSearch,
  102. more: ruleObject.more,
  103. logo: ruleObject.logo,
  104. ext: fileSite.ext || "", // 固定为空字符串
  105. };
  106. sites.push(site);
  107. });
  108. },
  109. param: {file, jsDir, requestHost, pwd, drpy, SitesMap, jsEncoder},
  110. id: file,
  111. };
  112. });
  113. const listener = {
  114. func: (param, id, error, result) => {
  115. if (error) {
  116. console.error(`Error processing file ${id}:`, error.message);
  117. } else {
  118. // console.log(`Successfully processed file ${id}:`, result);
  119. }
  120. },
  121. param: {}, // 外部参数可以在这里传入
  122. };
  123. await batchExecute(tasks, listener);
  124. // 根据用户是否启用dr2源去生成对应配置
  125. if (ENV.get('enable_dr2', '1') === '1') {
  126. const dr2_files = readdirSync(dr2Dir);
  127. let dr2_valid_files = dr2_files.filter((file) => file.endsWith('.js') && !file.startsWith('_')); // 筛选出不是 "_" 开头的 .js 文件
  128. // log(dr2_valid_files);
  129. log(`开始生成dr2的t3配置,dr2Dir:${dr2Dir},源数量: ${dr2_valid_files.length}`);
  130. const dr2_tasks = dr2_valid_files.map((file) => {
  131. return {
  132. func: async ({file, dr2Dir, requestHost, pwd, drpy, SitesMap}) => {
  133. const baseName = path.basename(file, '.js'); // 去掉文件扩展名
  134. let api = `assets://js/lib/drpy2.js`; // 使用内置drpy2
  135. let ext = `${requestHost}/js/${file}`;
  136. if (pwd) {
  137. ext += `?pwd=${pwd}`;
  138. }
  139. let ruleObject = {
  140. searchable: 0, // 固定值
  141. filterable: 0, // 固定值
  142. quickSearch: 0, // 固定值
  143. };
  144. try {
  145. // console.log('file:', path.join(dr2Dir, file));
  146. ruleObject = await drpy.getRuleObject(path.join(dr2Dir, file));
  147. } catch (e) {
  148. throw new Error(`Error parsing rule object for file: ${file}, ${e.message}`);
  149. }
  150. let fileSites = [];
  151. if (baseName === 'push_agent') {
  152. let key = 'push_agent';
  153. let name = `${ruleObject.title}(DR2)`;
  154. fileSites.push({key, name, ext});
  155. } else if (SitesMap.hasOwnProperty(baseName) && Array.isArray(SitesMap[baseName])) {
  156. SitesMap[baseName].forEach((it) => {
  157. let key = `drpy2_${it.alias}`;
  158. let name = `${it.alias}(DR2)`;
  159. let _ext = updateQueryString(ext, it.queryStr);
  160. fileSites.push({key, name, ext: _ext});
  161. });
  162. } else {
  163. let key = `drpy2_${baseName}`;
  164. let name = `${baseName}(DR2)`;
  165. fileSites.push({key, name, ext});
  166. }
  167. fileSites.forEach((fileSite) => {
  168. const site = {
  169. key: fileSite.key,
  170. name: fileSite.name,
  171. type: 3, // 固定值
  172. api,
  173. searchable: ruleObject.searchable,
  174. filterable: ruleObject.filterable,
  175. quickSearch: ruleObject.quickSearch,
  176. more: ruleObject.more,
  177. logo: ruleObject.logo,
  178. ext: fileSite.ext || "", // 固定为空字符串
  179. };
  180. sites.push(site);
  181. });
  182. },
  183. param: {file, dr2Dir, requestHost, pwd, drpy, SitesMap},
  184. id: file,
  185. };
  186. });
  187. await batchExecute(dr2_tasks, listener);
  188. }
  189. // 根据用户是否启用挂载数据源去生成对应配置
  190. if (ENV.get('enable_link_data', '0') === '1') {
  191. log(`开始挂载外部T4数据`);
  192. let link_sites = [];
  193. let link_url = ENV.get('link_url');
  194. let enable_link_push = ENV.get('enable_link_push', '0');
  195. let enable_link_jar = ENV.get('enable_link_jar', '0');
  196. try {
  197. let link_data = readFileSync(path.join(rootDir, './data/settings/link_data.json'), 'utf-8');
  198. let link_config = JSON.parse(link_data);
  199. link_sites = link_config.sites.filter(site => site.type = 4);
  200. if (link_config.spider && enable_link_jar === '1') {
  201. let link_spider_arr = link_config.spider.split(';');
  202. link_jar = urljoin(link_url, link_spider_arr[0]);
  203. if (link_spider_arr.length > 1) {
  204. link_jar = [link_jar].concat(link_spider_arr.slice(1)).join(';')
  205. }
  206. log(`开始挂载外部T4 Jar: ${link_jar}`);
  207. }
  208. link_sites.forEach((site) => {
  209. if (site.key === 'push_agent' && enable_link_push !== '1') {
  210. return
  211. }
  212. if (site.api && !site.api.startsWith('http')) {
  213. site.api = urljoin(link_url, site.api)
  214. }
  215. if (site.ext && site.ext.startsWith('.')) {
  216. site.ext = urljoin(link_url, site.ext)
  217. }
  218. if (site.key === 'push_agent' && enable_link_push === '1') { // 推送覆盖
  219. let pushIndex = sites.findIndex(s => s.key === 'push_agent');
  220. if (pushIndex > -1) {
  221. sites[pushIndex] = site;
  222. } else {
  223. sites.push(site);
  224. }
  225. } else {
  226. sites.push(site);
  227. }
  228. });
  229. } catch (e) {
  230. }
  231. }
  232. // 订阅再次处理别名的情况
  233. if (sub) {
  234. if (sub.mode === 0) {
  235. sites = sites.filter(it => (new RegExp(sub.reg || '.*')).test(it.name));
  236. } else if (sub.mode === 1) {
  237. sites = sites.filter(it => !(new RegExp(sub.reg || '.*')).test(it.name));
  238. }
  239. }
  240. // 青少年模式再次处理自定义别名的情况
  241. if (ENV.get('hide_adult') === '1') {
  242. sites = sites.filter(it => !(new RegExp('\\[[密]\\]|密+')).test(it.name));
  243. }
  244. sites = naturalSort(sites, 'name', sort_list);
  245. return {sites, spider: link_jar};
  246. }
  247. async function generateParseJSON(jxDir, requestHost) {
  248. const files = readdirSync(jxDir);
  249. const jx_files = files.filter((file) => file.endsWith('.js') && !file.startsWith('_')) // 筛选出不是 "_" 开头的 .js 文件
  250. const jx_dict = getParsesDict(requestHost);
  251. let parses = [];
  252. const tasks = jx_files.map((file) => {
  253. return {
  254. func: async ({file, jxDir, requestHost, drpy}) => {
  255. const baseName = path.basename(file, '.js'); // 去掉文件扩展名
  256. const api = `${requestHost}/parse/${baseName}?url=`; // 使用请求的 host 地址,避免硬编码端口
  257. let jxObject = {
  258. type: 1, // 固定值
  259. ext: {
  260. flag: [
  261. "qiyi",
  262. "imgo",
  263. "爱奇艺",
  264. "奇艺",
  265. "qq",
  266. "qq 预告及花絮",
  267. "腾讯",
  268. "youku",
  269. "优酷",
  270. "pptv",
  271. "PPTV",
  272. "letv",
  273. "乐视",
  274. "leshi",
  275. "mgtv",
  276. "芒果",
  277. "sohu",
  278. "xigua",
  279. "fun",
  280. "风行"
  281. ]
  282. },
  283. header: {
  284. "User-Agent": "Mozilla/5.0"
  285. }
  286. };
  287. try {
  288. let _jxObject = await drpy.getJx(path.join(jxDir, file));
  289. jxObject = {...jxObject, ..._jxObject};
  290. } catch (e) {
  291. throw new Error(`Error parsing jx object for file: ${file}, ${e.message}`);
  292. }
  293. parses.push({
  294. name: baseName,
  295. url: jxObject.url || api,
  296. type: jxObject.type,
  297. ext: jxObject.ext,
  298. header: jxObject.header
  299. });
  300. },
  301. param: {file, jxDir, requestHost, drpy},
  302. id: file,
  303. };
  304. });
  305. const listener = {
  306. func: (param, id, error, result) => {
  307. if (error) {
  308. console.error(`Error processing file ${id}:`, error.message);
  309. } else {
  310. // console.log(`Successfully processed file ${id}:`, result);
  311. }
  312. },
  313. param: {}, // 外部参数可以在这里传入
  314. };
  315. await batchExecute(tasks, listener);
  316. let sorted_parses = naturalSort(parses, 'name', ['JSON并发', 'JSON合集', '虾米', '奇奇']);
  317. let sorted_jx_dict = naturalSort(jx_dict, 'name', ['J', 'W']);
  318. parses = sorted_parses.concat(sorted_jx_dict);
  319. return {parses};
  320. }
  321. function generateLivesJSON(requestHost) {
  322. let lives = [];
  323. let live_url = process.env.LIVE_URL || '';
  324. let epg_url = process.env.EPG_URL || ''; // 从.env文件读取
  325. let logo_url = process.env.LOGO_URL || ''; // 从.env文件读取
  326. if (live_url && !live_url.startsWith('http')) {
  327. let public_url = urljoin(requestHost, 'public/');
  328. live_url = urljoin(public_url, live_url);
  329. }
  330. // console.log('live_url:', live_url);
  331. if (live_url) {
  332. lives.push(
  333. {
  334. "name": "直播",
  335. "type": 0,
  336. "url": live_url,
  337. "playerType": 1,
  338. "ua": "okhttp/3.12.13",
  339. "epg": epg_url,
  340. "logo": logo_url
  341. }
  342. )
  343. }
  344. return {lives}
  345. }
  346. function generatePlayerJSON(configDir, requestHost) {
  347. let playerConfig = {};
  348. let playerConfigPath = path.join(configDir, './player.json');
  349. if (existsSync(playerConfigPath)) {
  350. try {
  351. playerConfig = JSON.parse(readFileSync(playerConfigPath, 'utf-8'))
  352. } catch (e) {
  353. }
  354. }
  355. return playerConfig
  356. }
  357. function getSubs(subFilePath) {
  358. let subs = [];
  359. try {
  360. const subContent = readFileSync(subFilePath, 'utf-8');
  361. subs = JSON.parse(subContent)
  362. } catch (e) {
  363. console.log(`读取订阅失败:${e.message}`);
  364. }
  365. return subs
  366. }
  367. export default (fastify, options, done) => {
  368. fastify.get('/index', {preHandler: validatePwd}, async (request, reply) => {
  369. if (!existsSync(options.indexFilePath)) {
  370. reply.code(404).send({error: 'index.json not found'});
  371. return;
  372. }
  373. const content = readFileSync(options.indexFilePath, 'utf-8');
  374. reply.send(JSON.parse(content));
  375. });
  376. // 接口:返回配置 JSON,同时写入 index.json
  377. fastify.get('/config*', {preHandler: [validatePwd, validateBasicAuth]}, async (request, reply) => {
  378. let t1 = (new Date()).getTime();
  379. const query = request.query; // 获取 query 参数
  380. const pwd = query.pwd || '';
  381. const sub_code = query.sub || '';
  382. const cfg_path = request.params['*']; // 捕获整个路径
  383. try {
  384. // 获取主机名,协议及端口
  385. const protocol = request.headers['x-forwarded-proto'] || (request.socket.encrypted ? 'https' : 'http'); // http 或 https
  386. const hostname = request.hostname; // 主机名,不包含端口
  387. const port = request.socket.localPort; // 获取当前服务的端口
  388. console.log(`cfg_path:${cfg_path},port:${port}`);
  389. let not_local = cfg_path.startsWith('/1') || cfg_path.startsWith('/index');
  390. let requestHost = not_local ? `${protocol}://${hostname}` : `http://127.0.0.1:${options.PORT}`; // 动态生成根地址
  391. let requestUrl = not_local ? `${protocol}://${hostname}${request.url}` : `http://127.0.0.1:${options.PORT}${request.url}`; // 动态生成请求链接
  392. // console.log('requestUrl:', requestUrl);
  393. // if (cfg_path.endsWith('.js')) {
  394. // if (cfg_path.includes('index.js')) {
  395. // // return reply.sendFile('index.js', path.join(options.rootDir, 'data/cat'));
  396. // let content = readFileSync(path.join(options.rootDir, 'data/cat/index.js'), 'utf-8');
  397. // // content = jinja.render(content, {config_url: requestUrl.replace(cfg_path, `/1?sub=all&pwd=${process.env.API_PWD || ''}`)});
  398. // content = content.replace('$config_url', requestUrl.replace(cfg_path, `/1?sub=all&pwd=${process.env.API_PWD || ''}`));
  399. // return reply.type('application/javascript;charset=utf-8').send(content);
  400. // } else if (cfg_path.includes('index.config.js')) {
  401. // let content = readFileSync(path.join(options.rootDir, 'data/cat/index.config.js'), 'utf-8');
  402. // // content = jinja.render(content, {config_url: requestUrl.replace(cfg_path, `/1?sub=all&pwd=${process.env.API_PWD || ''}`)});
  403. // content = content.replace('$config_url', requestUrl.replace(cfg_path, `/1?sub=all&pwd=${process.env.API_PWD || ''}`));
  404. // return reply.type('application/javascript;charset=utf-8').send(content);
  405. // }
  406. // }
  407. // if (cfg_path.endsWith('.js.md5')) {
  408. // if (cfg_path.includes('index.js')) {
  409. // let content = readFileSync(path.join(options.rootDir, 'data/cat/index.js'), 'utf-8');
  410. // // content = jinja.render(content, {config_url: requestUrl.replace(cfg_path, `/1?sub=all&pwd=${process.env.API_PWD || ''}`)});
  411. // content = content.replace('$config_url', requestUrl.replace(cfg_path, `/1?sub=all&pwd=${process.env.API_PWD || ''}`));
  412. // let contentHash = md5(content);
  413. // console.log('index.js contentHash:', contentHash);
  414. // return reply.type('text/plain;charset=utf-8').send(contentHash);
  415. // } else if (cfg_path.includes('index.config.js')) {
  416. // let content = readFileSync(path.join(options.rootDir, 'data/cat/index.config.js'), 'utf-8');
  417. // // content = jinja.render(content, {config_url: requestUrl.replace(cfg_path, `/1?sub=all&pwd=${process.env.API_PWD || ''}`)});
  418. // content = content.replace('$config_url', requestUrl.replace(cfg_path, `/1?sub=all&pwd=${process.env.API_PWD || ''}`));
  419. // let contentHash = md5(content);
  420. // console.log('index.config.js contentHash:', contentHash);
  421. // return reply.type('text/plain;charset=utf-8').send(contentHash);
  422. // }
  423. // }
  424. const getFilePath = (cfgPath, rootDir, fileName) => path.join(rootDir, `data/cat/${fileName}`);
  425. const processContent = (content, cfgPath, requestUrl) =>
  426. content.replace('$config_url', requestUrl.replace(cfgPath, `/1?sub=all&pwd=${process.env.API_PWD || ''}`));
  427. const handleJavaScript = (cfgPath, requestUrl, options, reply) => {
  428. const fileMap = {
  429. 'index.js': 'index.js',
  430. 'index.config.js': 'index.config.js'
  431. };
  432. for (const [key, fileName] of Object.entries(fileMap)) {
  433. if (cfgPath.includes(key)) {
  434. const filePath = getFilePath(cfgPath, options.rootDir, fileName);
  435. let content = readFileSync(filePath, 'utf-8');
  436. content = processContent(content, cfgPath, requestUrl);
  437. return reply.type('application/javascript;charset=utf-8').send(content);
  438. }
  439. }
  440. };
  441. const handleJsMd5 = (cfgPath, requestUrl, options, reply) => {
  442. const fileMap = {
  443. 'index.js': 'index.js',
  444. 'index.config.js': 'index.config.js'
  445. };
  446. for (const [key, fileName] of Object.entries(fileMap)) {
  447. if (cfgPath.includes(key)) {
  448. const filePath = getFilePath(cfgPath, options.rootDir, fileName);
  449. let content = readFileSync(filePath, 'utf-8');
  450. content = processContent(content, cfgPath, requestUrl);
  451. const contentHash = md5(content);
  452. console.log(`${fileName} contentHash:`, contentHash);
  453. return reply.type('text/plain;charset=utf-8').send(contentHash);
  454. }
  455. }
  456. };
  457. if (cfg_path.endsWith('.js')) {
  458. return handleJavaScript(cfg_path, requestUrl, options, reply);
  459. }
  460. if (cfg_path.endsWith('.js.md5')) {
  461. return handleJsMd5(cfg_path, requestUrl, options, reply);
  462. }
  463. let sub = null;
  464. if (sub_code) {
  465. let subs = getSubs(options.subFilePath);
  466. sub = subs.find(it => it.code === sub_code);
  467. // console.log('sub:', sub);
  468. if (sub && sub.status === 0) {
  469. return reply.status(500).send({error: `此订阅码:【${sub_code}】已禁用`});
  470. }
  471. }
  472. const siteJSON = await generateSiteJSON(options, requestHost, sub, pwd);
  473. const parseJSON = await generateParseJSON(options.jxDir, requestHost);
  474. const livesJSON = generateLivesJSON(requestHost);
  475. const playerJSON = generatePlayerJSON(options.configDir, requestHost);
  476. const configObj = {sites_count: siteJSON.sites.length, ...playerJSON, ...siteJSON, ...parseJSON, ...livesJSON};
  477. if (!configObj.spider) {
  478. configObj.spider = playerJSON.spider
  479. }
  480. // console.log(configObj);
  481. const configStr = JSON.stringify(configObj, null, 2);
  482. if (!process.env.VERCEL) { // Vercel 环境不支持写文件,关闭此功能
  483. writeFileSync(options.indexFilePath, configStr, 'utf8'); // 写入 index.json
  484. if (cfg_path === '/1') {
  485. writeFileSync(options.customFilePath, configStr, 'utf8'); // 写入 index.json
  486. }
  487. }
  488. let t2 = (new Date()).getTime();
  489. let cost = t2 - t1;
  490. // configObj.cost = cost;
  491. // reply.send(configObj);
  492. reply.send(Object.assign({cost}, configObj));
  493. } catch (error) {
  494. reply.status(500).send({error: 'Failed to generate site JSON', details: error.message});
  495. }
  496. });
  497. done();
  498. };