main.js 23 KB

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