main.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842
  1. import { pinnacleGet, getPsteryRelations, updateBaseEvents, notifyException } from "./libs/pinnacleClient.js";
  2. import { Logs } from "./libs/logs.js";
  3. import { getData, setData } from "./libs/cache.js";
  4. const gamesMapCacheFile = 'data/gamesCache.json';
  5. const globalDataCacheFile = 'data/globalDataCache.json';
  6. const TP = 'ps_9_9_1';
  7. const IS_DEV = process.env.NODE_ENV == 'development';
  8. const GLOBAL_DATA = {
  9. filtedLeagues: [],
  10. filtedGames: [],
  11. gamesMap: {},
  12. straightFixturesVersion: 0,
  13. straightFixturesCount: 0,
  14. specialFixturesVersion: 0,
  15. specialFixturesCount: 0,
  16. straightOddsVersion: 0,
  17. // straightOddsCount: 0,
  18. specialsOddsVersion: 0,
  19. // specialsOddsCount: 0,
  20. requestErrorCount: 0,
  21. loopActive: false,
  22. loopResultTime: 0,
  23. };
  24. const resetVersionsCount = () => {
  25. GLOBAL_DATA.straightFixturesVersion = 0;
  26. GLOBAL_DATA.specialFixturesVersion = 0;
  27. GLOBAL_DATA.straightOddsVersion = 0;
  28. GLOBAL_DATA.specialsOddsVersion = 0;
  29. GLOBAL_DATA.straightFixturesCount = 0;
  30. GLOBAL_DATA.specialFixturesCount = 0;
  31. }
  32. const incrementVersionsCount = () => {
  33. GLOBAL_DATA.straightFixturesCount += 1;
  34. GLOBAL_DATA.specialFixturesCount += 1;
  35. }
  36. /**
  37. * 获取指定时区当前日期或时间
  38. * @param {number} offsetHours - 时区相对 UTC 的偏移(例如:-4 表示 GMT-4)
  39. * @param {boolean} [withTime=false] - 是否返回完整时间(默认只返回日期)
  40. * @returns {string} 格式化的日期或时间字符串
  41. */
  42. const getDateInTimezone = (offsetHours, withTime = false) => {
  43. const nowUTC = new Date();
  44. const targetTime = new Date(nowUTC.getTime() + offsetHours * 60 * 60 * 1000);
  45. const year = targetTime.getUTCFullYear();
  46. const month = String(targetTime.getUTCMonth() + 1).padStart(2, '0');
  47. const day = String(targetTime.getUTCDate()).padStart(2, '0');
  48. if (!withTime) {
  49. return `${year}-${month}-${day}`;
  50. }
  51. const hours = String(targetTime.getUTCHours()).padStart(2, '0');
  52. const minutes = String(targetTime.getUTCMinutes()).padStart(2, '0');
  53. const seconds = String(targetTime.getUTCSeconds()).padStart(2, '0');
  54. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  55. }
  56. const isArrayEqualUnordered = (arr1, arr2) => {
  57. if (arr1.length !== arr2.length) return false;
  58. const s1 = [...arr1].sort();
  59. const s2 = [...arr2].sort();
  60. return s1.every((v, i) => v === s2[i]);
  61. }
  62. const getFiltedGames = () => {
  63. return new Promise(resolve => {
  64. const updateFiltedGames = () => {
  65. getPsteryRelations()
  66. .then(res => {
  67. if (res.statusCode !== 200) {
  68. throw new Error(res.message);
  69. }
  70. // Logs.outDev('update filted games', res.data);
  71. const games = res.data.map(game => {
  72. const { eventId, leagueId } = game?.rel?.ps ?? {};
  73. return {
  74. eventId,
  75. leagueId,
  76. };
  77. });
  78. const newFiltedLeagues = [...new Set(games.map(game => game.leagueId).filter(leagueId => leagueId))];
  79. if (!isArrayEqualUnordered(newFiltedLeagues, GLOBAL_DATA.filtedLeagues)) {
  80. Logs.outDev('filted leagues changed', newFiltedLeagues);
  81. resetVersionsCount();
  82. GLOBAL_DATA.filtedLeagues = newFiltedLeagues;
  83. }
  84. GLOBAL_DATA.filtedGames = games.map(game => game.eventId).filter(eventId => eventId);
  85. resolve();
  86. setTimeout(updateFiltedGames, 1000 * 60);
  87. })
  88. .catch(err => {
  89. Logs.err('failed to update filted games:', err.message);
  90. setTimeout(updateFiltedGames, 1000 * 5);
  91. });
  92. }
  93. updateFiltedGames();
  94. });
  95. }
  96. const getStraightFixtures = async () => {
  97. if (!GLOBAL_DATA.filtedLeagues.length) {
  98. return Promise.reject(new Error('no filted leagues'));
  99. }
  100. const leagueIds = GLOBAL_DATA.filtedLeagues.join(',');
  101. let since = GLOBAL_DATA.straightFixturesVersion;
  102. if (GLOBAL_DATA.straightFixturesCount >= 12) {
  103. since = 0;
  104. GLOBAL_DATA.straightFixturesCount = 0;
  105. }
  106. if (since == 0) {
  107. Logs.outDev('full update straight fixtures');
  108. }
  109. return pinnacleGet('/v3/fixtures', { sportId: 29, leagueIds, since })
  110. .then(data => {
  111. const { league, last } = data;
  112. if (!last) {
  113. return {};
  114. }
  115. GLOBAL_DATA.straightFixturesVersion = last;
  116. const games = league?.map(league => {
  117. const { id: leagueId, events } = league;
  118. return events?.map(event => {
  119. const { starts } = event;
  120. const timestamp = new Date(starts).getTime();
  121. return { leagueId, ...event, timestamp };
  122. });
  123. })
  124. .flat() ?? [];
  125. const update = since == 0 ? 'full' : 'increment';
  126. return { games, update };
  127. });
  128. }
  129. const updateStraightFixtures = async () => {
  130. return getStraightFixtures()
  131. .then(data => {
  132. const { games, update } = data;
  133. const { gamesMap } = GLOBAL_DATA;
  134. if (games?.length) {
  135. games.forEach(game => {
  136. const { id } = game;
  137. if (!gamesMap[id]) {
  138. gamesMap[id] = game;
  139. }
  140. else {
  141. Object.assign(gamesMap[id], game);
  142. }
  143. });
  144. }
  145. if (update && update == 'full') {
  146. const gamesSet = new Set(games.map(game => game.id));
  147. Object.keys(gamesMap)?.forEach(key => {
  148. if (!gamesSet.has(+key)) {
  149. delete gamesMap[key];
  150. }
  151. });
  152. }
  153. });
  154. }
  155. const getStraightOdds = async () => {
  156. if (!GLOBAL_DATA.filtedLeagues.length) {
  157. return Promise.reject(new Error('no filted leagues'));
  158. }
  159. const leagueIds = GLOBAL_DATA.filtedLeagues.join(',');
  160. const since = GLOBAL_DATA.straightOddsVersion;
  161. // if (GLOBAL_DATA.straightOddsCount >= 27) {
  162. // since = 0;
  163. // GLOBAL_DATA.straightOddsCount = 3;
  164. // }
  165. if (since == 0) {
  166. Logs.outDev('full update straight odds');
  167. }
  168. return pinnacleGet('/v3/odds', { sportId: 29, oddsFormat: 'Decimal', leagueIds, since })
  169. .then(data => {
  170. const { leagues, last } = data;
  171. if (!last) {
  172. return [];
  173. }
  174. GLOBAL_DATA.straightOddsVersion = last;
  175. const games = leagues?.flatMap(league => league.events);
  176. // return games;
  177. return games?.map(item => {
  178. const { periods, ...rest } = item;
  179. const straight = periods?.find(period => period.number == 0);
  180. const straight1st = periods?.find(period => period.number == 1);
  181. const gameInfo = rest;
  182. if (straight || straight1st) {
  183. gameInfo.periods = {};
  184. }
  185. if (straight) {
  186. gameInfo.periods.straight = straight;
  187. }
  188. if (straight1st) {
  189. gameInfo.periods.straight1st = straight1st;
  190. }
  191. return gameInfo;
  192. }) ?? [];
  193. });
  194. }
  195. const updateStraightOdds = async () => {
  196. return getStraightOdds()
  197. .then(games => {
  198. if (games?.length) {
  199. const { gamesMap={} } = GLOBAL_DATA;
  200. games.forEach(game => {
  201. const { id, periods, ...rest } = game;
  202. const localGame = gamesMap[id];
  203. if (localGame) {
  204. Object.assign(localGame, rest);
  205. if (!localGame.periods && periods) {
  206. localGame.periods = periods;
  207. }
  208. else if (periods) {
  209. Object.assign(localGame.periods, periods);
  210. }
  211. }
  212. });
  213. }
  214. });
  215. }
  216. const getSpecialFixtures = async () => {
  217. if (!GLOBAL_DATA.filtedLeagues.length) {
  218. return Promise.reject(new Error('no filted leagues'));
  219. }
  220. const leagueIds = GLOBAL_DATA.filtedLeagues.join(',');
  221. let since = GLOBAL_DATA.specialFixturesVersion;
  222. if (GLOBAL_DATA.specialFixturesCount >= 18) {
  223. since = 0;
  224. GLOBAL_DATA.specialFixturesCount = 6;
  225. }
  226. if (since == 0) {
  227. Logs.outDev('full update special fixtures');
  228. }
  229. return pinnacleGet('/v2/fixtures/special', { sportId: 29, leagueIds, since })
  230. .then(data => {
  231. const { leagues, last } = data;
  232. if (!last) {
  233. return { specials: [], update: 'increment' };
  234. }
  235. GLOBAL_DATA.specialFixturesVersion = last;
  236. const specials = leagues?.map(league => {
  237. const { specials } = league;
  238. return specials?.filter(special => special.event)
  239. .map(special => {
  240. const { event: { id: eventId, periodNumber }, ...rest } = special ?? { event: {} };
  241. return { eventId, periodNumber, ...rest };
  242. }) ?? [];
  243. })
  244. .flat()
  245. .filter(special => {
  246. if (special.name.includes('Winning Margin') || special.name.includes('Exact Total Goals')) {
  247. return true;
  248. }
  249. return false;
  250. }) ?? [];
  251. const update = since == 0 ? 'full' : 'increment';
  252. return { specials, update };
  253. });
  254. }
  255. const mergeContestants = (localContestants=[], remoteContestants=[]) => {
  256. const localContestantsMap = new Map(localContestants.map(contestant => [contestant.id, contestant]));
  257. remoteContestants.forEach(contestant => {
  258. const localContestant = localContestantsMap.get(contestant.id);
  259. if (localContestant) {
  260. Object.assign(localContestant, contestant);
  261. }
  262. else {
  263. localContestants.push(contestant);
  264. }
  265. });
  266. const remoteContestantsMap = new Map(remoteContestants.map(contestant => [contestant.id, contestant]));
  267. for (let i = localContestants.length - 1; i >= 0; i--) {
  268. if (!remoteContestantsMap.has(localContestants[i].id)) {
  269. localContestants.splice(i, 1);
  270. }
  271. }
  272. return localContestants;
  273. }
  274. const mergeSpecials = (localSpecials, remoteSpecials, specialName) => {
  275. if (localSpecials[specialName] && remoteSpecials[specialName]) {
  276. const { contestants: specialContestants, ...specialRest } = remoteSpecials[specialName];
  277. Object.assign(localSpecials[specialName], specialRest);
  278. mergeContestants(localSpecials[specialName].contestants, specialContestants);
  279. }
  280. else if (remoteSpecials[specialName]) {
  281. localSpecials[specialName] = remoteSpecials[specialName];
  282. }
  283. }
  284. const updateSpecialFixtures = async () => {
  285. return getSpecialFixtures()
  286. .then(data => {
  287. const { specials, update } = data;
  288. if (specials?.length) {
  289. const { gamesMap={} } = GLOBAL_DATA;
  290. const gamesSpecialsMap = {};
  291. specials.forEach(special => {
  292. const { eventId } = special;
  293. if (!gamesSpecialsMap[eventId]) {
  294. gamesSpecialsMap[eventId] = {};
  295. }
  296. if (special.name == 'Winning Margin') {
  297. gamesSpecialsMap[eventId].winningMargin = special;
  298. }
  299. else if (special.name == 'Exact Total Goals') {
  300. gamesSpecialsMap[eventId].exactTotalGoals = special;
  301. }
  302. else if (special.name == 'Winning Margin 1st Half') {
  303. gamesSpecialsMap[eventId].winningMargin1st = special;
  304. }
  305. else if (special.name == 'Exact Total Goals 1st Half') {
  306. gamesSpecialsMap[eventId].exactTotalGoals1st = special;
  307. }
  308. });
  309. Object.keys(gamesSpecialsMap).forEach(eventId => {
  310. if (!gamesMap[eventId]) {
  311. return;
  312. }
  313. if (!gamesMap[eventId].specials) {
  314. gamesMap[eventId].specials = {};
  315. }
  316. const localSpecials = gamesMap[eventId].specials;
  317. const remoteSpecials = gamesSpecialsMap[eventId];
  318. mergeSpecials(localSpecials, remoteSpecials, 'winningMargin');
  319. mergeSpecials(localSpecials, remoteSpecials, 'exactTotalGoals');
  320. mergeSpecials(localSpecials, remoteSpecials, 'winningMargin1st');
  321. mergeSpecials(localSpecials, remoteSpecials, 'exactTotalGoals1st');
  322. });
  323. }
  324. });
  325. }
  326. const getSpecialsOdds = async () => {
  327. if (!GLOBAL_DATA.filtedLeagues.length) {
  328. return Promise.reject(new Error('no filted leagues'));
  329. }
  330. const leagueIds = GLOBAL_DATA.filtedLeagues.join(',');
  331. const since = GLOBAL_DATA.specialsOddsVersion;
  332. // if (GLOBAL_DATA.specialsOddsCount >= 33) {
  333. // since = 0;
  334. // GLOBAL_DATA.specialsOddsCount = 9;
  335. // }
  336. if (since == 0) {
  337. Logs.outDev('full update specials odds');
  338. }
  339. return pinnacleGet('/v2/odds/special', { sportId: 29, oddsFormat: 'Decimal', leagueIds, since })
  340. .then(data => {
  341. const { leagues, last } = data;
  342. if (!last) {
  343. return [];
  344. }
  345. GLOBAL_DATA.specialsOddsVersion = last;
  346. return leagues?.flatMap(league => league.specials);
  347. });
  348. }
  349. const updateSpecialsOdds = async () => {
  350. return getSpecialsOdds()
  351. .then(specials => {
  352. if (specials.length) {
  353. const { gamesMap={} } = GLOBAL_DATA;
  354. const contestants = Object.values(gamesMap)
  355. .filter(game => game.specials)
  356. .map(game => {
  357. const { specials } = game;
  358. const contestants = Object.values(specials).map(special => {
  359. const { contestants } = special;
  360. return contestants.map(contestant => [contestant.id, contestant]);
  361. });
  362. return contestants;
  363. }).flat(2);
  364. const contestantsMap = new Map(contestants);
  365. const lines = specials?.flatMap(special => special.contestantLines) ?? [];
  366. lines.forEach(line => {
  367. const { id, handicap, lineId, max, price } = line;
  368. const contestant = contestantsMap.get(id);
  369. if (!contestant) {
  370. return;
  371. }
  372. contestant.handicap = handicap;
  373. contestant.lineId = lineId;
  374. contestant.max = max;
  375. contestant.price = price;
  376. });
  377. }
  378. });
  379. }
  380. const getInRunning = async () => {
  381. return pinnacleGet('/v2/inrunning')
  382. .then(data => {
  383. const sportId = 29;
  384. const leagues = data.sports?.find(sport => sport.id == sportId)?.leagues ?? [];
  385. return leagues.filter(league => {
  386. const { id } = league;
  387. const filtedLeaguesSet = new Set(GLOBAL_DATA.filtedLeagues);
  388. return filtedLeaguesSet.has(id);
  389. }).flatMap(league => league.events);
  390. });
  391. }
  392. const updateInRunning = async () => {
  393. return getInRunning()
  394. .then(games => {
  395. if (!games.length) {
  396. return;
  397. }
  398. const { gamesMap={} } = GLOBAL_DATA;
  399. games.forEach(game => {
  400. const { id, state, elapsed } = game;
  401. const localGame = gamesMap[id];
  402. if (localGame) {
  403. Object.assign(localGame, { state, elapsed });
  404. }
  405. });
  406. });
  407. }
  408. const ratioAccept = (ratio) => {
  409. if (ratio > 0) {
  410. return 'a';
  411. }
  412. return '';
  413. }
  414. const ratioString = (ratio) => {
  415. ratio = Math.abs(ratio);
  416. ratio = ratio.toString();
  417. ratio = ratio.replace(/\./, '');
  418. return ratio;
  419. }
  420. const parseSpreads = (spreads, wm) => {
  421. // 让分盘
  422. if (!spreads?.length) {
  423. return null;
  424. }
  425. const events = {};
  426. spreads.forEach(spread => {
  427. const { hdp, home, away } = spread;
  428. // if (!(hdp % 1) || !!(hdp % 0.5)) {
  429. // // 整数或不能被0.5整除的让分盘不处理
  430. // return;
  431. // }
  432. const ratio_ro = hdp;
  433. const ratio_r = ratio_ro - wm;
  434. events[`ior_r${ratioAccept(ratio_r)}h_${ratioString(ratio_r)}`] = {
  435. v: home,
  436. r: wm != 0 ? `ior_r${ratioAccept(ratio_ro)}h_${ratioString(ratio_ro)}` : undefined
  437. };
  438. events[`ior_r${ratioAccept(-ratio_r)}c_${ratioString(ratio_r)}`] = {
  439. v: away,
  440. r: wm != 0 ? `ior_r${ratioAccept(-ratio_ro)}c_${ratioString(ratio_ro)}` : undefined
  441. };
  442. });
  443. return events;
  444. }
  445. const parseMoneyline = (moneyline) => {
  446. // 胜平负
  447. if (!moneyline) {
  448. return null;
  449. }
  450. const { home, away, draw } = moneyline;
  451. return {
  452. 'ior_mh': { v: home },
  453. 'ior_mc': { v: away },
  454. 'ior_mn': { v: draw },
  455. }
  456. }
  457. const parseTotals = (totals) => {
  458. // 大小球盘
  459. if (!totals?.length) {
  460. return null;
  461. }
  462. const events = {};
  463. totals.forEach(total => {
  464. const { points, over, under } = total;
  465. events[`ior_ouc_${ratioString(points)}`] = { v: over };
  466. events[`ior_ouh_${ratioString(points)}`] = { v: under };
  467. });
  468. return events;
  469. }
  470. const parseStraight = (straight, wm) => {
  471. if (!straight) {
  472. return null;
  473. }
  474. const { cutoff='', status=0, spreads=[], moneyline={}, totals=[] } = straight;
  475. const cutoffTime = new Date(cutoff).getTime();
  476. const nowTime = Date.now();
  477. if (status != 1 || cutoffTime < nowTime) {
  478. return null;
  479. }
  480. const events = {};
  481. Object.assign(events, parseSpreads(spreads, wm));
  482. Object.assign(events, parseMoneyline(moneyline));
  483. Object.assign(events, parseTotals(totals));
  484. return events;
  485. }
  486. const parseWinningMargin = (winningMargin, home, away) => {
  487. if (!winningMargin) {
  488. return null;
  489. }
  490. const { cutoff='', status='', contestants=[] } = winningMargin;
  491. const cutoffTime = new Date(cutoff).getTime();
  492. const nowTime = Date.now();
  493. if (status != 'O' || cutoffTime < nowTime || !contestants?.length) {
  494. return null;
  495. }
  496. const events = {};
  497. contestants.forEach(contestant => {
  498. const { name, price } = contestant;
  499. const nr = name.match(/\d+$/)?.[0];
  500. if (!nr) {
  501. return;
  502. }
  503. let side;
  504. if (name.startsWith(home)) {
  505. side = 'h';
  506. }
  507. else if (name.startsWith(away)) {
  508. side = 'c';
  509. }
  510. else {
  511. return;
  512. }
  513. events[`ior_wm${side}_${nr}`] = { v: price };
  514. });
  515. return events;
  516. }
  517. const parseExactTotalGoals = (exactTotalGoals) => {
  518. if (!exactTotalGoals) {
  519. return null;
  520. }
  521. const { cutoff='', status='', contestants=[] } = exactTotalGoals;
  522. const cutoffTime = new Date(cutoff).getTime();
  523. const nowTime = Date.now();
  524. if (status != 'O' || cutoffTime < nowTime || !contestants?.length) {
  525. return null;
  526. }
  527. const events = {};
  528. contestants.forEach(contestant => {
  529. const { name, price } = contestant;
  530. if (+name >= 1 && +name <= 7) {
  531. events[`ior_ot_${name}`] = { v: price };
  532. }
  533. });
  534. return events;
  535. }
  536. const parseRbState = (state) => {
  537. let stage = null;
  538. if (state == 1) {
  539. stage = '1H';
  540. }
  541. else if (state == 2) {
  542. stage = 'HT';
  543. }
  544. else if (state == 3) {
  545. stage = '2H';
  546. }
  547. else if (state >= 4) {
  548. stage = 'ET';
  549. }
  550. return stage;
  551. }
  552. const parseGame = (game) => {
  553. const { eventId=0, originId=0, periods={}, specials={}, home, away, marketType, state, elapsed, homeScore=0, awayScore=0 } = game;
  554. const { straight, straight1st } = periods;
  555. const { winningMargin={}, exactTotalGoals={}, winningMargin1st={}, exactTotalGoals1st={} } = specials;
  556. const filtedGamesSet = new Set(GLOBAL_DATA.filtedGames);
  557. const wm = homeScore - awayScore;
  558. const score = `${homeScore}-${awayScore}`;
  559. const events = parseStraight(straight, wm) ?? {};
  560. const stage = parseRbState(state);
  561. const retime = elapsed ? `${elapsed}'` : '';
  562. const evtime = Date.now();
  563. Object.assign(events, parseWinningMargin(winningMargin, home, away));
  564. Object.assign(events, parseExactTotalGoals(exactTotalGoals));
  565. const gameInfos = [];
  566. gameInfos.push({ eventId, originId, events, evtime, stage, retime, score, wm, marketType });
  567. const halfEventId = eventId * -1;
  568. if (filtedGamesSet.has(halfEventId)) {
  569. const events = parseStraight(straight1st, wm) ?? {};
  570. Object.assign(events, parseWinningMargin(winningMargin1st, home, away));
  571. Object.assign(events, parseExactTotalGoals(exactTotalGoals1st));
  572. gameInfos.push({ eventId: halfEventId, originId, events, evtime, stage, retime, score, wm, marketType });
  573. }
  574. return gameInfos;
  575. }
  576. const getGames = () => {
  577. const { filtedGames, gamesMap={} } = GLOBAL_DATA;
  578. const filtedGamesSet = new Set(filtedGames);
  579. const nowTime = Date.now();
  580. const gamesData = {};
  581. Object.values(gamesMap).forEach(game => {
  582. const { id, liveStatus, parentId, resultingUnit, timestamp } = game;
  583. if (resultingUnit !== 'Regular') {
  584. return false; // 非常规赛事不处理
  585. }
  586. const gmtMinus4Date = getDateInTimezone(-4);
  587. const todayEndTime = new Date(`${gmtMinus4Date} 23:59:59 GMT-4`).getTime();
  588. const tomorrowEndTime = todayEndTime + 24 * 60 * 60 * 1000;
  589. if (liveStatus == 1 && timestamp < nowTime) {
  590. game.marketType = 2; // 滚球赛事
  591. }
  592. else if (liveStatus != 1 && timestamp > nowTime && timestamp <= todayEndTime) {
  593. game.marketType = 1; // 今日赛事
  594. }
  595. else if (liveStatus != 1 && timestamp > todayEndTime && timestamp <= tomorrowEndTime) {
  596. game.marketType = 0; // 明日早盘赛事
  597. }
  598. else {
  599. game.marketType = -1; // 非近期赛事
  600. }
  601. if (game.marketType < 0) {
  602. return false; // 非近期赛事不处理
  603. }
  604. let actived = false;
  605. if (liveStatus != 1 && filtedGamesSet.has(id)) {
  606. actived = true; // 在赛前列表中
  607. game.eventId = id;
  608. game.originId = 0;
  609. }
  610. else if (liveStatus == 1 && filtedGamesSet.has(parentId)) {
  611. actived = true; // 在滚球列表中
  612. game.eventId = parentId;
  613. game.originId = id;
  614. }
  615. if (actived) {
  616. parseGame(game)?.forEach(gameInfo => {
  617. const { marketType, ...rest } = gameInfo;
  618. if (!gamesData[marketType]) {
  619. gamesData[marketType] = [];
  620. }
  621. gamesData[marketType].push(rest);
  622. });
  623. }
  624. });
  625. return gamesData;
  626. }
  627. const pinnacleDataLoop = () => {
  628. updateStraightFixtures()
  629. .then(() => {
  630. return Promise.all([
  631. updateStraightOdds(),
  632. updateSpecialFixtures(),
  633. updateInRunning(),
  634. ]);
  635. })
  636. .then(() => {
  637. return updateSpecialsOdds();
  638. })
  639. .then(() => {
  640. if (!GLOBAL_DATA.loopActive) {
  641. GLOBAL_DATA.loopActive = true;
  642. Logs.out('loop active');
  643. notifyException('Pinnacle API startup.');
  644. }
  645. if (GLOBAL_DATA.requestErrorCount > 0) {
  646. GLOBAL_DATA.requestErrorCount = 0;
  647. Logs.out('request error count reset');
  648. }
  649. const nowTime = Date.now();
  650. const loopDuration = nowTime - GLOBAL_DATA.loopResultTime;
  651. GLOBAL_DATA.loopResultTime = nowTime;
  652. if (loopDuration > 15000) {
  653. Logs.out('loop duration is too long', loopDuration);
  654. }
  655. else {
  656. Logs.outDev('loop duration', loopDuration);
  657. }
  658. const timestamp = Date.now();
  659. const games = getGames();
  660. const data = { games, timestamp, tp: TP };
  661. updateBaseEvents(data);
  662. if (IS_DEV) {
  663. setData(gamesMapCacheFile, GLOBAL_DATA.gamesMap)
  664. .then(() => {
  665. Logs.outDev('games map saved');
  666. })
  667. .catch(err => {
  668. Logs.err('failed to save games map', err.message);
  669. });
  670. }
  671. })
  672. .catch(err => {
  673. Logs.err(err.message, err.source);
  674. GLOBAL_DATA.requestErrorCount++;
  675. if (GLOBAL_DATA.loopActive && GLOBAL_DATA.requestErrorCount > 5) {
  676. const exceptionMessage = 'request errors have reached the limit';
  677. Logs.out(exceptionMessage);
  678. GLOBAL_DATA.loopActive = false;
  679. Logs.out('loop inactive');
  680. notifyException(`Pinnacle API paused. ${exceptionMessage}. ${err.message}`);
  681. }
  682. })
  683. .finally(() => {
  684. const { loopActive } = GLOBAL_DATA;
  685. let loopDelay = 1000 * 5;
  686. if (!loopActive) {
  687. loopDelay = 1000 * 60;
  688. resetVersionsCount();
  689. }
  690. else {
  691. incrementVersionsCount();
  692. }
  693. setTimeout(pinnacleDataLoop, loopDelay);
  694. });
  695. }
  696. /**
  697. * 缓存GLOBAL_DATA数据到文件
  698. */
  699. const saveGlobalDataToCache = async () => {
  700. return setData(globalDataCacheFile, GLOBAL_DATA);
  701. }
  702. const loadGlobalDataFromCache = async () => {
  703. return getData(globalDataCacheFile)
  704. .then(data => {
  705. if (!data) {
  706. return;
  707. }
  708. Object.keys(GLOBAL_DATA)?.forEach(key => {
  709. if (key in data) {
  710. GLOBAL_DATA[key] = data[key];
  711. }
  712. });
  713. });
  714. }
  715. // 监听进程退出事件,保存GLOBAL_DATA数据
  716. const saveExit = (code) => {
  717. saveGlobalDataToCache()
  718. .then(() => {
  719. Logs.out('global data saved');
  720. })
  721. .catch(err => {
  722. Logs.err('failed to save global data', err.message);
  723. })
  724. .finally(() => {
  725. process.exit(code);
  726. });
  727. }
  728. process.on('SIGINT', () => {
  729. saveExit(0);
  730. });
  731. process.on('SIGTERM', () => {
  732. saveExit(0);
  733. });
  734. process.on('SIGUSR2', () => {
  735. saveExit(0);
  736. });
  737. (() => {
  738. loadGlobalDataFromCache()
  739. .then(() => {
  740. Logs.out('global data loaded');
  741. })
  742. .catch(err => {
  743. Logs.err('failed to load global data', err.message);
  744. })
  745. .finally(() => {
  746. GLOBAL_DATA.loopResultTime = Date.now();
  747. GLOBAL_DATA.loopActive = true;
  748. return getFiltedGames();
  749. })
  750. .then(pinnacleDataLoop);
  751. })();