main.js 23 KB

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