main.js 24 KB

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