main.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762
  1. import path from "path";
  2. import { fileURLToPath } from "url";
  3. import Logs from "./libs/logs.js";
  4. import { getData, setData } from "./libs/cache.js";
  5. import getDateInTimezone from "./libs/getDateInTimezone.js";
  6. import {
  7. pinnacleGet,
  8. pinnacleWebLeagues, pinnacleWebGames,
  9. platformPost, platformGet,
  10. notifyException,
  11. } from "./libs/pinnacleClient.js";
  12. import parseGame from "./libs/parseGameData.js";
  13. const __filename = fileURLToPath(import.meta.url);
  14. const __dirname = path.dirname(__filename);
  15. const gamesMapCacheFile = path.join(__dirname, './cache/pinnacleGamesCache.json');
  16. const globalDataCacheFile = path.join(__dirname, './cache/pinnacleGlobalDataCache.json');
  17. const GLOBAL_DATA = {
  18. filteredLeagues: [],
  19. filteredGames: [],
  20. gamesMap: {},
  21. straightFixturesVersion: 0,
  22. straightFixturesCount: 0,
  23. specialFixturesVersion: 0,
  24. specialFixturesCount: 0,
  25. straightOddsVersion: 0,
  26. // straightOddsCount: 0,
  27. specialsOddsVersion: 0,
  28. // specialsOddsCount: 0,
  29. requestErrorCount: 0,
  30. loopActive: false,
  31. loopLastResultTime: 0,
  32. wsClientData: null,
  33. };
  34. const resetVersionsCount = () => {
  35. GLOBAL_DATA.straightFixturesVersion = 0;
  36. GLOBAL_DATA.specialFixturesVersion = 0;
  37. GLOBAL_DATA.straightOddsVersion = 0;
  38. GLOBAL_DATA.specialsOddsVersion = 0;
  39. GLOBAL_DATA.straightFixturesCount = 0;
  40. GLOBAL_DATA.specialFixturesCount = 0;
  41. }
  42. const incrementVersionsCount = () => {
  43. GLOBAL_DATA.straightFixturesCount += 1;
  44. GLOBAL_DATA.specialFixturesCount += 1;
  45. }
  46. /**
  47. * 更新Web联赛列表
  48. */
  49. const updateWebLeagues = async () => {
  50. const getWebLeaguesToday = pinnacleWebLeagues(1);
  51. const getWebLeaguesTomorrow = pinnacleWebLeagues(0);
  52. return Promise.all([getWebLeaguesToday, getWebLeaguesTomorrow])
  53. .then(data => {
  54. const [webLeaguesToday, webLeaguesTomorrow] = data;
  55. const leaguesMap = new Map(webLeaguesToday.concat(webLeaguesTomorrow));
  56. return Array.from(leaguesMap.values()).sort((a, b) => a.id - b.id);
  57. })
  58. .then(leagues => {
  59. Logs.outDev('update leagues list', leagues);
  60. return platformPost('/api/platforms/update_leagues', { platform: 'pinnacle', leagues });
  61. })
  62. .then(() => {
  63. Logs.outDev('leagues list updated');
  64. return Promise.resolve(60);
  65. });
  66. }
  67. /**
  68. * 定时更新Web联赛列表
  69. */
  70. const updateWebLeaguesLoop = () => {
  71. updateWebLeagues()
  72. .then(delay => {
  73. setTimeout(() => {
  74. updateWebLeaguesLoop();
  75. }, 1000 * delay);
  76. })
  77. .catch(err => {
  78. Logs.err('failed to update leagues list', err.message);
  79. setTimeout(() => {
  80. updateWebLeaguesLoop();
  81. }, 1000 * 5);
  82. });
  83. }
  84. /**
  85. * 更新Web比赛列表
  86. */
  87. const updateWebGames = async () => {
  88. if (!GLOBAL_DATA.filteredLeagues.length) {
  89. return Promise.resolve(5);
  90. }
  91. const getWebGamesToday = pinnacleWebGames(GLOBAL_DATA.filteredLeagues, 1);
  92. const getWebGamesTomorrow = pinnacleWebGames(GLOBAL_DATA.filteredLeagues, 0);
  93. return Promise.all([getWebGamesToday, getWebGamesTomorrow])
  94. .then(data => {
  95. const [webGamesToday, webGamesTomorrow] = data;
  96. return webGamesToday.concat(webGamesTomorrow).sort((a, b) => a.timestamp - b.timestamp);
  97. })
  98. .then(games => {
  99. Logs.outDev('update games list', games);
  100. return platformPost('/api/platforms/update_games', { platform: 'pinnacle', games });
  101. })
  102. .then(() => {
  103. Logs.outDev('games list updated');
  104. return Promise.resolve(60);
  105. });
  106. }
  107. /**
  108. * 定时更新Web比赛列表
  109. */
  110. const updateWebGamesLoop = () => {
  111. updateWebGames()
  112. .then(delay => {
  113. setTimeout(() => {
  114. updateWebGamesLoop();
  115. }, 1000 * delay);
  116. })
  117. .catch(err => {
  118. Logs.err('failed to update games list', err.message);
  119. setTimeout(() => {
  120. updateWebGamesLoop();
  121. }, 1000 * 5);
  122. });
  123. }
  124. /**
  125. * 获取过滤后的联赛数据
  126. * @returns
  127. */
  128. const updateFilteredLeagues = () => {
  129. platformGet('/api/platforms/get_filtered_leagues', { platform: 'pinnacle' })
  130. .then(res => {
  131. const { data: filteredLeagues } = res;
  132. GLOBAL_DATA.filteredLeagues = filteredLeagues.map(item => item.id);
  133. })
  134. .catch(error => {
  135. Logs.err('failed to update filtered leagues', error.message);
  136. })
  137. .finally(() => {
  138. setTimeout(() => {
  139. updateFilteredLeagues();
  140. }, 1000 * 10);
  141. });
  142. }
  143. /**
  144. * 获取过滤后的比赛数据
  145. * @returns
  146. */
  147. const updateFilteredGames = () => {
  148. platformGet('/api/platforms/get_filtered_games', { platform: 'pinnacle' })
  149. .then(res => {
  150. const { data: filteredGames } = res;
  151. GLOBAL_DATA.filteredGames = filteredGames.map(item => item.id);
  152. })
  153. .catch(error => {
  154. Logs.err('failed to update filtered games', error.message);
  155. })
  156. .finally(() => {
  157. setTimeout(() => {
  158. updateFilteredGames();
  159. }, 1000 * 10);
  160. });
  161. }
  162. /**
  163. * 获取直赛数据
  164. * @returns
  165. */
  166. const getStraightFixtures = async () => {
  167. if (!GLOBAL_DATA.filteredLeagues.length) {
  168. resetVersionsCount();
  169. return Promise.reject(new Error('no filtered leagues', { cause: 400 }));
  170. }
  171. const leagueIds = GLOBAL_DATA.filteredLeagues.join(',');
  172. let since = GLOBAL_DATA.straightFixturesVersion;
  173. if (GLOBAL_DATA.straightFixturesCount >= 12) {
  174. since = 0;
  175. GLOBAL_DATA.straightFixturesCount = 0;
  176. }
  177. if (since == 0) {
  178. Logs.outDev('full update straight fixtures');
  179. }
  180. return pinnacleGet('/v3/fixtures', { sportId: 29, leagueIds, since })
  181. .then(data => {
  182. const { league, last } = data;
  183. if (!last) {
  184. return {};
  185. }
  186. GLOBAL_DATA.straightFixturesVersion = last;
  187. const games = league?.map(league => {
  188. const { id: leagueId, events } = league;
  189. return events?.map(event => {
  190. const { starts } = event;
  191. const timestamp = new Date(starts).getTime();
  192. return { leagueId, ...event, timestamp };
  193. });
  194. })
  195. .flat() ?? [];
  196. const update = since == 0 ? 'full' : 'increment';
  197. return { games, update };
  198. });
  199. }
  200. /**
  201. * 更新直赛数据
  202. * @returns
  203. */
  204. const updateStraightFixtures = async () => {
  205. return getStraightFixtures()
  206. .then(data => {
  207. const { games, update } = data ?? {};
  208. const { gamesMap } = GLOBAL_DATA;
  209. if (games?.length) {
  210. games.forEach(game => {
  211. const { id } = game;
  212. if (!gamesMap[id]) {
  213. gamesMap[id] = game;
  214. }
  215. else {
  216. Object.assign(gamesMap[id], game);
  217. }
  218. });
  219. }
  220. if (update && update == 'full') {
  221. const gamesSet = new Set(games.map(game => game.id));
  222. Object.keys(gamesMap).forEach(key => {
  223. if (!gamesSet.has(+key)) {
  224. delete gamesMap[key];
  225. }
  226. });
  227. }
  228. return Promise.resolve('StraightFixtures');
  229. });
  230. }
  231. /**
  232. * 获取直赛赔率数据
  233. * @returns
  234. */
  235. const getStraightOdds = async () => {
  236. if (!GLOBAL_DATA.filteredLeagues.length) {
  237. resetVersionsCount();
  238. return Promise.reject(new Error('no filtered leagues', { cause: 400 }));
  239. }
  240. const leagueIds = GLOBAL_DATA.filteredLeagues.join(',');
  241. const since = GLOBAL_DATA.straightOddsVersion;
  242. // if (GLOBAL_DATA.straightOddsCount >= 27) {
  243. // since = 0;
  244. // GLOBAL_DATA.straightOddsCount = 3;
  245. // }
  246. if (since == 0) {
  247. Logs.outDev('full update straight odds');
  248. }
  249. return pinnacleGet('/v3/odds', { sportId: 29, oddsFormat: 'Decimal', leagueIds, since })
  250. .then(data => {
  251. const { leagues, last } = data;
  252. if (!last) {
  253. return [];
  254. }
  255. GLOBAL_DATA.straightOddsVersion = last;
  256. const games = leagues?.flatMap(league => league.events);
  257. // return games;
  258. return games?.map(item => {
  259. const { periods, ...rest } = item;
  260. const straight = periods?.find(period => period.number == 0);
  261. return { ...rest, periods: { straight }};
  262. }) ?? [];
  263. });
  264. }
  265. /**
  266. * 更新直赛赔率数据
  267. * @returns
  268. */
  269. const updateStraightOdds = async () => {
  270. return getStraightOdds()
  271. .then(games => {
  272. if (games?.length) {
  273. const { gamesMap } = GLOBAL_DATA;
  274. games.forEach(game => {
  275. const { id, periods, ...rest } = game;
  276. const localGame = gamesMap[id];
  277. if (localGame) {
  278. Object.assign(localGame, rest);
  279. if (!localGame.periods && periods) {
  280. localGame.periods = periods;
  281. }
  282. else if (periods) {
  283. Object.assign(localGame.periods, periods);
  284. }
  285. }
  286. });
  287. }
  288. return Promise.resolve('StraightOdds');
  289. });
  290. }
  291. /**
  292. * 获取特殊赛数据
  293. * @returns
  294. */
  295. const getSpecialFixtures = async () => {
  296. if (!GLOBAL_DATA.filteredLeagues.length) {
  297. resetVersionsCount();
  298. return Promise.reject(new Error('no filtered leagues', { cause: 400 }));
  299. }
  300. const leagueIds = GLOBAL_DATA.filteredLeagues.join(',');
  301. let since = GLOBAL_DATA.specialFixturesVersion;
  302. if (GLOBAL_DATA.specialFixturesCount >= 18) {
  303. since = 0;
  304. GLOBAL_DATA.specialFixturesCount = 6;
  305. }
  306. if (since == 0) {
  307. Logs.outDev('full update special fixtures');
  308. }
  309. return pinnacleGet('/v2/fixtures/special', { sportId: 29, leagueIds, since })
  310. .then(data => {
  311. const { leagues, last } = data;
  312. if (!last) {
  313. return [];
  314. }
  315. GLOBAL_DATA.specialFixturesVersion = last;
  316. const specials = leagues?.map(league => {
  317. const { specials } = league;
  318. return specials?.filter(special => special.event)
  319. .map(special => {
  320. const { event: { id: eventId, periodNumber }, ...rest } = special ?? { event: {} };
  321. return { eventId, periodNumber, ...rest };
  322. }) ?? [];
  323. })
  324. .flat()
  325. .filter(special => {
  326. if (special.name.includes('Winning Margin') || special.name.includes('Exact Total Goals')) {
  327. return true;
  328. }
  329. return false;
  330. }) ?? [];
  331. const update = since == 0 ? 'full' : 'increment';
  332. return { specials, update };
  333. });
  334. }
  335. /**
  336. * 合并特殊赛盘口数据
  337. * @param {*} localContestants
  338. * @param {*} remoteContestants
  339. * @returns
  340. */
  341. const mergeContestants = (localContestants=[], remoteContestants=[]) => {
  342. const localContestantsMap = new Map(localContestants.map(contestant => [contestant.id, contestant]));
  343. remoteContestants.forEach(contestant => {
  344. const localContestant = localContestantsMap.get(contestant.id);
  345. if (localContestant) {
  346. Object.assign(localContestant, contestant);
  347. }
  348. else {
  349. localContestants.push(contestant);
  350. }
  351. });
  352. const remoteContestantsMap = new Map(remoteContestants.map(contestant => [contestant.id, contestant]));
  353. for (let i = localContestants.length - 1; i >= 0; i--) {
  354. if (!remoteContestantsMap.has(localContestants[i].id)) {
  355. localContestants.splice(i, 1);
  356. }
  357. }
  358. return localContestants;
  359. }
  360. /**
  361. * 合并特殊赛数据
  362. * @param {*} localSpecials
  363. * @param {*} remoteSpecials
  364. * @param {*} specialName
  365. * @returns
  366. */
  367. const mergeSpecials = (localSpecials, remoteSpecials, specialName) => {
  368. if (localSpecials[specialName] && remoteSpecials[specialName]) {
  369. const { contestants: specialContestants, ...specialRest } = remoteSpecials[specialName];
  370. Object.assign(localSpecials[specialName], specialRest);
  371. mergeContestants(localSpecials[specialName].contestants, specialContestants);
  372. }
  373. else if (remoteSpecials[specialName]) {
  374. localSpecials[specialName] = remoteSpecials[specialName];
  375. }
  376. }
  377. /**
  378. * 更新特殊赛数据
  379. * @returns
  380. */
  381. const updateSpecialFixtures = async () => {
  382. return getSpecialFixtures()
  383. .then(data => {
  384. const { specials, update } = data ?? {};
  385. if (specials?.length) {
  386. const { gamesMap } = GLOBAL_DATA;
  387. const gamesSpecialsMap = {};
  388. specials.forEach(special => {
  389. const { eventId } = special;
  390. if (!gamesSpecialsMap[eventId]) {
  391. gamesSpecialsMap[eventId] = {};
  392. }
  393. if (special.name == 'Winning Margin') {
  394. gamesSpecialsMap[eventId].winningMargin = special;
  395. }
  396. else if (special.name == 'Exact Total Goals') {
  397. gamesSpecialsMap[eventId].exactTotalGoals = special;
  398. }
  399. });
  400. Object.keys(gamesSpecialsMap).forEach(eventId => {
  401. if (!gamesMap[eventId]) {
  402. return;
  403. }
  404. if (!gamesMap[eventId].specials) {
  405. gamesMap[eventId].specials = {};
  406. }
  407. const localSpecials = gamesMap[eventId].specials;
  408. const remoteSpecials = gamesSpecialsMap[eventId];
  409. mergeSpecials(localSpecials, remoteSpecials, 'winningMargin');
  410. mergeSpecials(localSpecials, remoteSpecials, 'exactTotalGoals');
  411. });
  412. }
  413. return Promise.resolve('SpecialFixtures');
  414. });
  415. }
  416. /**
  417. * 获取特殊赛赔率数据
  418. * @returns
  419. */
  420. const getSpecialsOdds = async () => {
  421. if (!GLOBAL_DATA.filteredLeagues.length) {
  422. resetVersionsCount();
  423. return Promise.reject(new Error('no filtered leagues', { cause: 400 }));
  424. }
  425. const leagueIds = GLOBAL_DATA.filteredLeagues.join(',');
  426. const since = GLOBAL_DATA.specialsOddsVersion;
  427. // if (GLOBAL_DATA.specialsOddsCount >= 33) {
  428. // since = 0;
  429. // GLOBAL_DATA.specialsOddsCount = 9;
  430. // }
  431. if (since == 0) {
  432. Logs.outDev('full update specials odds');
  433. }
  434. return pinnacleGet('/v2/odds/special', { sportId: 29, oddsFormat: 'Decimal', leagueIds, since })
  435. .then(data => {
  436. const { leagues, last } = data;
  437. if (!last) {
  438. return [];
  439. }
  440. GLOBAL_DATA.specialsOddsVersion = last;
  441. return leagues?.flatMap(league => league.specials);
  442. });
  443. }
  444. /**
  445. * 更新特殊赛赔率数据
  446. * @returns
  447. */
  448. const updateSpecialsOdds = async () => {
  449. return getSpecialsOdds()
  450. .then(specials => {
  451. if (specials?.length) {
  452. const { gamesMap } = GLOBAL_DATA;
  453. const contestants = Object.values(gamesMap)
  454. .filter(game => game.specials)
  455. .map(game => {
  456. const { specials } = game;
  457. const contestants = Object.values(specials).map(special => {
  458. const { contestants } = special;
  459. return contestants.map(contestant => [contestant.id, contestant]);
  460. });
  461. return contestants;
  462. }).flat(2);
  463. const contestantsMap = new Map(contestants);
  464. const lines = specials.flatMap(special => special.contestantLines);
  465. lines.forEach(line => {
  466. const { id, handicap, lineId, max, price } = line;
  467. const contestant = contestantsMap.get(id);
  468. if (!contestant) {
  469. return;
  470. }
  471. contestant.handicap = handicap;
  472. contestant.lineId = lineId;
  473. contestant.max = max;
  474. contestant.price = price;
  475. });
  476. }
  477. return Promise.resolve('SpecialsOdds');
  478. });
  479. }
  480. /**
  481. * 获取滚球数据
  482. * @returns
  483. */
  484. const getInRunning = async () => {
  485. return pinnacleGet('/v2/inrunning')
  486. .then(data => {
  487. const sportId = 29;
  488. const leagues = data.sports?.find(sport => sport.id == sportId)?.leagues ?? [];
  489. return leagues.filter(league => {
  490. if (!GLOBAL_DATA.filteredLeagues.length) {
  491. return true;
  492. }
  493. const { id } = league;
  494. const filteredLeaguesSet = new Set(GLOBAL_DATA.filteredLeagues);
  495. return filteredLeaguesSet.has(id);
  496. }).flatMap(league => league.events);
  497. });
  498. }
  499. /**
  500. * 更新滚球数据
  501. * @returns
  502. */
  503. const updateInRunning = async () => {
  504. return getInRunning()
  505. .then(games => {
  506. if (!games.length) {
  507. return Promise.resolve('InRunning');
  508. }
  509. const { gamesMap } = GLOBAL_DATA;
  510. games.forEach(game => {
  511. const { id, state, elapsed } = game;
  512. const localGame = gamesMap[id];
  513. if (localGame) {
  514. Object.assign(localGame, { state, elapsed });
  515. }
  516. });
  517. return Promise.resolve('InRunning');
  518. });
  519. }
  520. /**
  521. * 获取比赛盘口赔率数据
  522. * @returns {Object}
  523. */
  524. const getGamesEvents = () => {
  525. const {
  526. filteredGames, gamesMap,
  527. straightFixturesVersion: sfv,
  528. specialFixturesVersion: pfv,
  529. straightOddsVersion: sov,
  530. specialsOddsVersion: pov
  531. } = GLOBAL_DATA;
  532. const filteredGamesSet = new Set(filteredGames);
  533. const nowTime = Date.now();
  534. const gamesData = {};
  535. Object.values(gamesMap).forEach(game => {
  536. const { id, liveStatus, parentId, resultingUnit, timestamp } = game;
  537. if (resultingUnit !== 'Regular') {
  538. return false; // 非常规赛事不处理
  539. }
  540. const gmtMinus4Date = getDateInTimezone(-4);
  541. const todayEndTime = new Date(`${gmtMinus4Date} 23:59:59 GMT-4`).getTime();
  542. const tomorrowEndTime = todayEndTime + 24 * 60 * 60 * 1000;
  543. if (liveStatus == 1 && timestamp < nowTime) {
  544. game.marketType = 2; // 滚球赛事
  545. }
  546. else if (liveStatus != 1 && timestamp > nowTime && timestamp <= todayEndTime) {
  547. game.marketType = 1; // 今日赛事
  548. }
  549. else if (liveStatus != 1 && timestamp > todayEndTime && timestamp <= tomorrowEndTime) {
  550. game.marketType = 0; // 明日早盘赛事
  551. }
  552. else {
  553. game.marketType = -1; // 非近期赛事
  554. }
  555. if (game.marketType < 0) {
  556. return false; // 非近期赛事不处理
  557. }
  558. let actived = false;
  559. if (liveStatus != 1 && (filteredGamesSet.has(id) || !filteredGames.length)) {
  560. actived = true; // 在赛前列表中
  561. game.id = id;
  562. game.originId = 0;
  563. }
  564. else if (liveStatus == 1 && (filteredGamesSet.has(parentId) || !filteredGames.length)) {
  565. actived = true; // 在滚球列表中
  566. game.id = parentId;
  567. game.originId = id;
  568. }
  569. if (actived) {
  570. const { marketType, ...rest } = parseGame(game);
  571. if (!gamesData[marketType]) {
  572. gamesData[marketType] = [];
  573. }
  574. gamesData[marketType].push(rest);
  575. }
  576. });
  577. const games = Object.values(gamesData).flat();
  578. const timestamp = Math.max(sfv, pfv, sov, pov);
  579. const data = { games, timestamp };
  580. return data;
  581. }
  582. /**
  583. * 更新赔率数据
  584. */
  585. const updateOdds = async () => {
  586. const { games, timestamp } = getGamesEvents();
  587. return platformPost('/api/platforms/update_odds', { platform: 'pinnacle', games, timestamp });
  588. }
  589. const pinnacleDataLoop = () => {
  590. updateStraightFixtures()
  591. .then(() => {
  592. return Promise.all([
  593. updateStraightOdds(),
  594. updateSpecialFixtures(),
  595. updateInRunning(),
  596. ]);
  597. })
  598. .then(() => {
  599. return updateSpecialsOdds();
  600. })
  601. .then(() => {
  602. if (!GLOBAL_DATA.loopActive) {
  603. GLOBAL_DATA.loopActive = true;
  604. Logs.out('loop active');
  605. notifyException('Pinnacle API startup.');
  606. }
  607. if (GLOBAL_DATA.requestErrorCount > 0) {
  608. GLOBAL_DATA.requestErrorCount = 0;
  609. Logs.out('request error count reset');
  610. }
  611. const nowTime = Date.now();
  612. const loopDuration = nowTime - GLOBAL_DATA.loopLastResultTime;
  613. GLOBAL_DATA.loopLastResultTime = nowTime;
  614. if (loopDuration > 15000) {
  615. Logs.out('loop duration is too long', loopDuration);
  616. }
  617. else {
  618. Logs.outDev('loop duration', loopDuration);
  619. }
  620. setData(gamesMapCacheFile, GLOBAL_DATA.gamesMap);
  621. return updateOdds();
  622. })
  623. .catch(err => {
  624. Logs.err(err.message, err.source);
  625. GLOBAL_DATA.requestErrorCount++;
  626. if (GLOBAL_DATA.loopActive && GLOBAL_DATA.requestErrorCount > 5) {
  627. const exceptionMessage = 'request errors have reached the limit';
  628. Logs.out(exceptionMessage);
  629. GLOBAL_DATA.loopActive = false;
  630. Logs.out('loop inactive');
  631. notifyException(`Pinnacle API paused. ${exceptionMessage}. ${err.message}`);
  632. }
  633. })
  634. .finally(() => {
  635. const { loopActive } = GLOBAL_DATA;
  636. let loopDelay = 1000 * 5;
  637. if (!loopActive) {
  638. loopDelay = 1000 * 60;
  639. resetVersionsCount();
  640. }
  641. else {
  642. incrementVersionsCount();
  643. }
  644. setTimeout(pinnacleDataLoop, loopDelay);
  645. });
  646. }
  647. /**
  648. * 缓存GLOBAL_DATA数据到文件
  649. */
  650. const saveGlobalDataToCache = async () => {
  651. return setData(globalDataCacheFile, GLOBAL_DATA);
  652. }
  653. const loadGlobalDataFromCache = async () => {
  654. return getData(globalDataCacheFile)
  655. .then(data => {
  656. if (!data) {
  657. return;
  658. }
  659. Object.keys(GLOBAL_DATA).forEach(key => {
  660. if (key in data) {
  661. GLOBAL_DATA[key] = data[key];
  662. }
  663. });
  664. });
  665. }
  666. const main = () => {
  667. if (!process.env.PINNACLE_USERNAME || !process.env.PINNACLE_PASSWORD) {
  668. Logs.err('USERNAME or PASSWORD is not set');
  669. return;
  670. }
  671. updateFilteredLeagues();
  672. updateFilteredGames();
  673. loadGlobalDataFromCache()
  674. .then(() => {
  675. Logs.out('global data loaded');
  676. })
  677. .then(() => {
  678. updateWebLeaguesLoop();
  679. updateWebGamesLoop();
  680. pinnacleDataLoop();
  681. })
  682. .catch(err => {
  683. Logs.err('failed to load global data', err.message);
  684. })
  685. .finally(() => {
  686. GLOBAL_DATA.loopLastResultTime = Date.now();
  687. GLOBAL_DATA.loopActive = true;
  688. });
  689. }
  690. // 监听进程退出事件,保存GLOBAL_DATA数据
  691. const saveExit = (code) => {
  692. saveGlobalDataToCache()
  693. .then(() => {
  694. Logs.out('global data saved');
  695. })
  696. .catch(err => {
  697. Logs.err('failed to save global data', err.message);
  698. })
  699. .finally(() => {
  700. process.exit(code);
  701. });
  702. }
  703. process.on('SIGINT', () => {
  704. saveExit(0);
  705. });
  706. process.on('SIGTERM', () => {
  707. saveExit(0);
  708. });
  709. process.on('SIGUSR2', () => {
  710. saveExit(0);
  711. });
  712. main();