main.js 21 KB

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