main.js 21 KB

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