main.js 23 KB

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