|
@@ -1,771 +0,0 @@
|
|
|
-import path from "path";
|
|
|
|
|
-import { fileURLToPath } from "url";
|
|
|
|
|
-
|
|
|
|
|
-import Logs from "./libs/logs.js";
|
|
|
|
|
-import { getData, setData } from "./libs/cache.js";
|
|
|
|
|
-import getDateInTimezone from "./libs/getDateInTimezone.js";
|
|
|
|
|
-
|
|
|
|
|
-import {
|
|
|
|
|
- pinnacleGet,
|
|
|
|
|
- pinnacleWebLeagues, pinnacleWebGames,
|
|
|
|
|
- platformPost, platformGet,
|
|
|
|
|
- notifyException,
|
|
|
|
|
-} from "./libs/pinnacleClient.js";
|
|
|
|
|
-import parseGame from "./libs/parseGameData.js";
|
|
|
|
|
-
|
|
|
|
|
-const __filename = fileURLToPath(import.meta.url);
|
|
|
|
|
-const __dirname = path.dirname(__filename);
|
|
|
|
|
-
|
|
|
|
|
-const gamesMapCacheFile = path.join(__dirname, './cache/pinnacleGamesCache.json');
|
|
|
|
|
-const globalDataCacheFile = path.join(__dirname, './cache/pinnacleGlobalDataCache.json');
|
|
|
|
|
-
|
|
|
|
|
-const GLOBAL_DATA = {
|
|
|
|
|
- relatedLeagues: [],
|
|
|
|
|
- selectedLeagues: [],
|
|
|
|
|
- relatedGames: [],
|
|
|
|
|
- gamesMap: {},
|
|
|
|
|
- straightFixturesVersion: 0,
|
|
|
|
|
- straightFixturesCount: 0,
|
|
|
|
|
- specialFixturesVersion: 0,
|
|
|
|
|
- specialFixturesCount: 0,
|
|
|
|
|
- straightOddsVersion: 0,
|
|
|
|
|
- // straightOddsCount: 0,
|
|
|
|
|
- specialsOddsVersion: 0,
|
|
|
|
|
- // specialsOddsCount: 0,
|
|
|
|
|
- requestErrorCount: 0,
|
|
|
|
|
- loopActive: false,
|
|
|
|
|
- loopLastResultTime: 0,
|
|
|
|
|
- wsClientData: null,
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-const resetVersionsCount = () => {
|
|
|
|
|
- GLOBAL_DATA.straightFixturesVersion = 0;
|
|
|
|
|
- GLOBAL_DATA.specialFixturesVersion = 0;
|
|
|
|
|
- GLOBAL_DATA.straightOddsVersion = 0;
|
|
|
|
|
- GLOBAL_DATA.specialsOddsVersion = 0;
|
|
|
|
|
-
|
|
|
|
|
- GLOBAL_DATA.straightFixturesCount = 0;
|
|
|
|
|
- GLOBAL_DATA.specialFixturesCount = 0;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const incrementVersionsCount = () => {
|
|
|
|
|
- GLOBAL_DATA.straightFixturesCount += 1;
|
|
|
|
|
- GLOBAL_DATA.specialFixturesCount += 1;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 更新Web联赛列表
|
|
|
|
|
- */
|
|
|
|
|
-const updateWebLeagues = async () => {
|
|
|
|
|
- const getWebLeaguesToday = pinnacleWebLeagues(1);
|
|
|
|
|
- const getWebLeaguesTomorrow = pinnacleWebLeagues(0);
|
|
|
|
|
- return Promise.all([getWebLeaguesToday, getWebLeaguesTomorrow])
|
|
|
|
|
- .then(data => {
|
|
|
|
|
- const [webLeaguesToday, webLeaguesTomorrow] = data;
|
|
|
|
|
- const leaguesMap = new Map(webLeaguesToday.concat(webLeaguesTomorrow));
|
|
|
|
|
- return Array.from(leaguesMap.values()).sort((a, b) => a.id - b.id);
|
|
|
|
|
- })
|
|
|
|
|
- .then(leagues => {
|
|
|
|
|
- Logs.outDev('update leagues list', leagues.length);
|
|
|
|
|
- return platformPost('/api/platforms/update_leagues', { platform: 'pinnacle', leagues });
|
|
|
|
|
- })
|
|
|
|
|
- .then(() => {
|
|
|
|
|
- Logs.outDev('leagues list updated');
|
|
|
|
|
- return Promise.resolve({ delay: 60 });
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 定时更新Web联赛列表
|
|
|
|
|
- */
|
|
|
|
|
-const updateWebLeaguesLoop = () => {
|
|
|
|
|
- updateWebLeagues()
|
|
|
|
|
- .then(data => {
|
|
|
|
|
- const { delay=5 } = data ?? {};
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- updateWebLeaguesLoop();
|
|
|
|
|
- }, 1000 * delay);
|
|
|
|
|
- })
|
|
|
|
|
- .catch(err => {
|
|
|
|
|
- Logs.err('failed to update leagues list', err.message);
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- updateWebLeaguesLoop();
|
|
|
|
|
- }, 1000 * 5);
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 更新Web比赛列表
|
|
|
|
|
- */
|
|
|
|
|
-const updateWebGames = async () => {
|
|
|
|
|
- if (!GLOBAL_DATA.relatedLeagues.length) {
|
|
|
|
|
- return Promise.resolve({ delay: 5 });
|
|
|
|
|
- }
|
|
|
|
|
- const getWebGamesToday = pinnacleWebGames(GLOBAL_DATA.relatedLeagues, 1);
|
|
|
|
|
- const getWebGamesTomorrow = pinnacleWebGames(GLOBAL_DATA.relatedLeagues, 0);
|
|
|
|
|
- return Promise.all([getWebGamesToday, getWebGamesTomorrow])
|
|
|
|
|
- .then(data => {
|
|
|
|
|
- const [webGamesToday, webGamesTomorrow] = data;
|
|
|
|
|
- return webGamesToday.concat(webGamesTomorrow).sort((a, b) => a.timestamp - b.timestamp);
|
|
|
|
|
- })
|
|
|
|
|
- .then(games => {
|
|
|
|
|
- Logs.outDev('update games list', games.length);
|
|
|
|
|
- return platformPost('/api/platforms/update_games', { platform: 'pinnacle', games });
|
|
|
|
|
- })
|
|
|
|
|
- .then(() => {
|
|
|
|
|
- Logs.outDev('games list updated');
|
|
|
|
|
- return Promise.resolve({ delay: 60 });
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 定时更新Web比赛列表
|
|
|
|
|
- */
|
|
|
|
|
-const updateWebGamesLoop = () => {
|
|
|
|
|
- updateWebGames()
|
|
|
|
|
- .then(data => {
|
|
|
|
|
- const { delay=5 } = data ?? {};
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- updateWebGamesLoop();
|
|
|
|
|
- }, 1000 * delay);
|
|
|
|
|
- })
|
|
|
|
|
- .catch(err => {
|
|
|
|
|
- Logs.err('failed to update games list', err.message);
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- updateWebGamesLoop();
|
|
|
|
|
- }, 1000 * 5);
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 获取过滤后的联赛数据
|
|
|
|
|
- * @returns
|
|
|
|
|
- */
|
|
|
|
|
-const updateRelatedLeagues = () => {
|
|
|
|
|
- platformGet('/api/platforms/get_related_leagues', { platform: 'pinnacle' })
|
|
|
|
|
- .then(res => {
|
|
|
|
|
- const { data: relatedLeagues } = res;
|
|
|
|
|
- GLOBAL_DATA.relatedLeagues = relatedLeagues.map(item => item.id);
|
|
|
|
|
- })
|
|
|
|
|
- .catch(error => {
|
|
|
|
|
- Logs.err('failed to update filtered leagues', error.message);
|
|
|
|
|
- })
|
|
|
|
|
- .finally(() => {
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- updateRelatedLeagues();
|
|
|
|
|
- }, 1000 * 10);
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 获取过滤后的比赛数据
|
|
|
|
|
- * @returns
|
|
|
|
|
- */
|
|
|
|
|
-const updateRelatedGames = () => {
|
|
|
|
|
- platformGet('/api/platforms/get_related_games', { platform: 'pinnacle' })
|
|
|
|
|
- .then(res => {
|
|
|
|
|
- const { data: relatedGames } = res;
|
|
|
|
|
- GLOBAL_DATA.relatedGames = relatedGames.map(item => item.id);
|
|
|
|
|
- GLOBAL_DATA.selectedLeagues = [...new Set(relatedGames.map(item => item.leagueId))];
|
|
|
|
|
- })
|
|
|
|
|
- .catch(error => {
|
|
|
|
|
- Logs.err('failed to update related games', error.message);
|
|
|
|
|
- })
|
|
|
|
|
- .finally(() => {
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- updateRelatedGames();
|
|
|
|
|
- }, 1000 * 10);
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 获取直赛数据
|
|
|
|
|
- * @returns
|
|
|
|
|
- */
|
|
|
|
|
-const getStraightFixtures = async () => {
|
|
|
|
|
- if (!GLOBAL_DATA.selectedLeagues.length) {
|
|
|
|
|
- // resetVersionsCount();
|
|
|
|
|
- GLOBAL_DATA.straightFixturesVersion = 0;
|
|
|
|
|
- return Promise.resolve({ games: [], update: 'full' });
|
|
|
|
|
- // return Promise.reject(new Error('no related leagues', { cause: 400 }));
|
|
|
|
|
- }
|
|
|
|
|
- const leagueIds = GLOBAL_DATA.selectedLeagues.join(',');
|
|
|
|
|
- let since = GLOBAL_DATA.straightFixturesVersion;
|
|
|
|
|
- if (GLOBAL_DATA.straightFixturesCount >= 12) {
|
|
|
|
|
- since = 0;
|
|
|
|
|
- GLOBAL_DATA.straightFixturesCount = 0;
|
|
|
|
|
- }
|
|
|
|
|
- if (since == 0) {
|
|
|
|
|
- Logs.outDev('full update straight fixtures');
|
|
|
|
|
- }
|
|
|
|
|
- return pinnacleGet('/v3/fixtures', { sportId: 29, leagueIds, since })
|
|
|
|
|
- .then(data => {
|
|
|
|
|
- const { league, last } = data;
|
|
|
|
|
- if (!last) {
|
|
|
|
|
- return {};
|
|
|
|
|
- }
|
|
|
|
|
- GLOBAL_DATA.straightFixturesVersion = last;
|
|
|
|
|
- const games = league?.map(league => {
|
|
|
|
|
- const { id: leagueId, events } = league;
|
|
|
|
|
- return events?.map(event => {
|
|
|
|
|
- const { starts } = event;
|
|
|
|
|
- const timestamp = new Date(starts).getTime();
|
|
|
|
|
- return { leagueId, ...event, timestamp };
|
|
|
|
|
- });
|
|
|
|
|
- })
|
|
|
|
|
- .flat() ?? [];
|
|
|
|
|
- const update = since == 0 ? 'full' : 'increment';
|
|
|
|
|
- return { games, update };
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 更新直赛数据
|
|
|
|
|
- * @returns
|
|
|
|
|
- */
|
|
|
|
|
-const updateStraightFixtures = async () => {
|
|
|
|
|
- return getStraightFixtures()
|
|
|
|
|
- .then(data => {
|
|
|
|
|
- const { games, update } = data ?? {};
|
|
|
|
|
- const { gamesMap } = GLOBAL_DATA;
|
|
|
|
|
- if (games?.length) {
|
|
|
|
|
- games.forEach(game => {
|
|
|
|
|
- const { id } = game;
|
|
|
|
|
- if (!gamesMap[id]) {
|
|
|
|
|
- gamesMap[id] = game;
|
|
|
|
|
- }
|
|
|
|
|
- else {
|
|
|
|
|
- Object.assign(gamesMap[id], game);
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- if (update && update == 'full') {
|
|
|
|
|
- const gamesSet = new Set(games.map(game => game.id));
|
|
|
|
|
- Object.keys(gamesMap).forEach(key => {
|
|
|
|
|
- if (!gamesSet.has(+key)) {
|
|
|
|
|
- delete gamesMap[key];
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- return Promise.resolve('StraightFixtures');
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 获取直赛赔率数据
|
|
|
|
|
- * @returns
|
|
|
|
|
- */
|
|
|
|
|
-const getStraightOdds = async () => {
|
|
|
|
|
- if (!GLOBAL_DATA.selectedLeagues.length) {
|
|
|
|
|
- // resetVersionsCount();
|
|
|
|
|
- GLOBAL_DATA.straightOddsVersion = 0;
|
|
|
|
|
- return Promise.resolve([]);
|
|
|
|
|
- // return Promise.reject(new Error('no related leagues', { cause: 400 }));
|
|
|
|
|
- }
|
|
|
|
|
- const leagueIds = GLOBAL_DATA.selectedLeagues.join(',');
|
|
|
|
|
- const since = GLOBAL_DATA.straightOddsVersion;
|
|
|
|
|
- // if (GLOBAL_DATA.straightOddsCount >= 27) {
|
|
|
|
|
- // since = 0;
|
|
|
|
|
- // GLOBAL_DATA.straightOddsCount = 3;
|
|
|
|
|
- // }
|
|
|
|
|
- if (since == 0) {
|
|
|
|
|
- Logs.outDev('full update straight odds');
|
|
|
|
|
- }
|
|
|
|
|
- return pinnacleGet('/v3/odds', { sportId: 29, oddsFormat: 'Decimal', leagueIds, since })
|
|
|
|
|
- .then(data => {
|
|
|
|
|
- const { leagues, last } = data;
|
|
|
|
|
- if (!last) {
|
|
|
|
|
- return [];
|
|
|
|
|
- }
|
|
|
|
|
- GLOBAL_DATA.straightOddsVersion = last;
|
|
|
|
|
- const games = leagues?.flatMap(league => league.events);
|
|
|
|
|
- // return games;
|
|
|
|
|
- return games?.map(item => {
|
|
|
|
|
- const { periods, ...rest } = item;
|
|
|
|
|
- const straight = periods?.find(period => period.number == 0);
|
|
|
|
|
- return { ...rest, periods: { straight }};
|
|
|
|
|
- }) ?? [];
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 更新直赛赔率数据
|
|
|
|
|
- * @returns
|
|
|
|
|
- */
|
|
|
|
|
-const updateStraightOdds = async () => {
|
|
|
|
|
- return getStraightOdds()
|
|
|
|
|
- .then(games => {
|
|
|
|
|
- if (games?.length) {
|
|
|
|
|
- const { gamesMap } = GLOBAL_DATA;
|
|
|
|
|
- games.forEach(game => {
|
|
|
|
|
- const { id, periods, ...rest } = game;
|
|
|
|
|
- const localGame = gamesMap[id];
|
|
|
|
|
- if (localGame) {
|
|
|
|
|
- Object.assign(localGame, rest);
|
|
|
|
|
- if (!localGame.periods && periods) {
|
|
|
|
|
- localGame.periods = periods;
|
|
|
|
|
- }
|
|
|
|
|
- else if (periods) {
|
|
|
|
|
- Object.assign(localGame.periods, periods);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- return Promise.resolve('StraightOdds');
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 获取特殊赛数据
|
|
|
|
|
- * @returns
|
|
|
|
|
- */
|
|
|
|
|
-const getSpecialFixtures = async () => {
|
|
|
|
|
- if (!GLOBAL_DATA.selectedLeagues.length) {
|
|
|
|
|
- // resetVersionsCount();
|
|
|
|
|
- GLOBAL_DATA.specialFixturesVersion = 0;
|
|
|
|
|
- return Promise.resolve({ specials: [], update: 'full' });
|
|
|
|
|
- // return Promise.reject(new Error('no related leagues', { cause: 400 }));
|
|
|
|
|
- }
|
|
|
|
|
- const leagueIds = GLOBAL_DATA.selectedLeagues.join(',');
|
|
|
|
|
- let since = GLOBAL_DATA.specialFixturesVersion;
|
|
|
|
|
- if (GLOBAL_DATA.specialFixturesCount >= 18) {
|
|
|
|
|
- since = 0;
|
|
|
|
|
- GLOBAL_DATA.specialFixturesCount = 6;
|
|
|
|
|
- }
|
|
|
|
|
- if (since == 0) {
|
|
|
|
|
- Logs.outDev('full update special fixtures');
|
|
|
|
|
- }
|
|
|
|
|
- return pinnacleGet('/v2/fixtures/special', { sportId: 29, leagueIds, since })
|
|
|
|
|
- .then(data => {
|
|
|
|
|
- const { leagues, last } = data;
|
|
|
|
|
- if (!last) {
|
|
|
|
|
- return [];
|
|
|
|
|
- }
|
|
|
|
|
- GLOBAL_DATA.specialFixturesVersion = last;
|
|
|
|
|
- const specials = leagues?.map(league => {
|
|
|
|
|
- const { specials } = league;
|
|
|
|
|
- return specials?.filter(special => special.event)
|
|
|
|
|
- .map(special => {
|
|
|
|
|
- const { event: { id: eventId, periodNumber }, ...rest } = special ?? { event: {} };
|
|
|
|
|
- return { eventId, periodNumber, ...rest };
|
|
|
|
|
- }) ?? [];
|
|
|
|
|
- })
|
|
|
|
|
- .flat()
|
|
|
|
|
- .filter(special => {
|
|
|
|
|
- if (special.name.includes('Winning Margin') || special.name.includes('Exact Total Goals')) {
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
- return false;
|
|
|
|
|
- }) ?? [];
|
|
|
|
|
- const update = since == 0 ? 'full' : 'increment';
|
|
|
|
|
- return { specials, update };
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 合并特殊赛盘口数据
|
|
|
|
|
- * @param {*} localContestants
|
|
|
|
|
- * @param {*} remoteContestants
|
|
|
|
|
- * @returns
|
|
|
|
|
- */
|
|
|
|
|
-const mergeContestants = (localContestants=[], remoteContestants=[]) => {
|
|
|
|
|
- const localContestantsMap = new Map(localContestants.map(contestant => [contestant.id, contestant]));
|
|
|
|
|
- remoteContestants.forEach(contestant => {
|
|
|
|
|
- const localContestant = localContestantsMap.get(contestant.id);
|
|
|
|
|
- if (localContestant) {
|
|
|
|
|
- Object.assign(localContestant, contestant);
|
|
|
|
|
- }
|
|
|
|
|
- else {
|
|
|
|
|
- localContestants.push(contestant);
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- const remoteContestantsMap = new Map(remoteContestants.map(contestant => [contestant.id, contestant]));
|
|
|
|
|
- for (let i = localContestants.length - 1; i >= 0; i--) {
|
|
|
|
|
- if (!remoteContestantsMap.has(localContestants[i].id)) {
|
|
|
|
|
- localContestants.splice(i, 1);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return localContestants;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 合并特殊赛数据
|
|
|
|
|
- * @param {*} localSpecials
|
|
|
|
|
- * @param {*} remoteSpecials
|
|
|
|
|
- * @param {*} specialName
|
|
|
|
|
- * @returns
|
|
|
|
|
- */
|
|
|
|
|
-const mergeSpecials = (localSpecials, remoteSpecials, specialName) => {
|
|
|
|
|
- if (localSpecials[specialName] && remoteSpecials[specialName]) {
|
|
|
|
|
- const { contestants: specialContestants, ...specialRest } = remoteSpecials[specialName];
|
|
|
|
|
- Object.assign(localSpecials[specialName], specialRest);
|
|
|
|
|
- mergeContestants(localSpecials[specialName].contestants, specialContestants);
|
|
|
|
|
- }
|
|
|
|
|
- else if (remoteSpecials[specialName]) {
|
|
|
|
|
- localSpecials[specialName] = remoteSpecials[specialName];
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 更新特殊赛数据
|
|
|
|
|
- * @returns
|
|
|
|
|
- */
|
|
|
|
|
-const updateSpecialFixtures = async () => {
|
|
|
|
|
- return getSpecialFixtures()
|
|
|
|
|
- .then(data => {
|
|
|
|
|
- const { specials, update } = data ?? {};
|
|
|
|
|
- if (specials?.length) {
|
|
|
|
|
- const { gamesMap } = GLOBAL_DATA;
|
|
|
|
|
- const gamesSpecialsMap = {};
|
|
|
|
|
- specials.forEach(special => {
|
|
|
|
|
- const { eventId } = special;
|
|
|
|
|
- if (!gamesSpecialsMap[eventId]) {
|
|
|
|
|
- gamesSpecialsMap[eventId] = {};
|
|
|
|
|
- }
|
|
|
|
|
- if (special.name == 'Winning Margin') {
|
|
|
|
|
- gamesSpecialsMap[eventId].winningMargin = special;
|
|
|
|
|
- }
|
|
|
|
|
- else if (special.name == 'Exact Total Goals') {
|
|
|
|
|
- gamesSpecialsMap[eventId].exactTotalGoals = special;
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- Object.keys(gamesSpecialsMap).forEach(eventId => {
|
|
|
|
|
- if (!gamesMap[eventId]) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!gamesMap[eventId].specials) {
|
|
|
|
|
- gamesMap[eventId].specials = {};
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const localSpecials = gamesMap[eventId].specials;
|
|
|
|
|
- const remoteSpecials = gamesSpecialsMap[eventId];
|
|
|
|
|
-
|
|
|
|
|
- mergeSpecials(localSpecials, remoteSpecials, 'winningMargin');
|
|
|
|
|
- mergeSpecials(localSpecials, remoteSpecials, 'exactTotalGoals');
|
|
|
|
|
-
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- return Promise.resolve('SpecialFixtures');
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 获取特殊赛赔率数据
|
|
|
|
|
- * @returns
|
|
|
|
|
- */
|
|
|
|
|
-const getSpecialsOdds = async () => {
|
|
|
|
|
- if (!GLOBAL_DATA.selectedLeagues.length) {
|
|
|
|
|
- // resetVersionsCount();
|
|
|
|
|
- GLOBAL_DATA.specialsOddsVersion = 0;
|
|
|
|
|
- return Promise.resolve([]);
|
|
|
|
|
- // return Promise.reject(new Error('no related leagues', { cause: 400 }));
|
|
|
|
|
- }
|
|
|
|
|
- const leagueIds = GLOBAL_DATA.selectedLeagues.join(',');
|
|
|
|
|
- const since = GLOBAL_DATA.specialsOddsVersion;
|
|
|
|
|
- // if (GLOBAL_DATA.specialsOddsCount >= 33) {
|
|
|
|
|
- // since = 0;
|
|
|
|
|
- // GLOBAL_DATA.specialsOddsCount = 9;
|
|
|
|
|
- // }
|
|
|
|
|
- if (since == 0) {
|
|
|
|
|
- Logs.outDev('full update specials odds');
|
|
|
|
|
- }
|
|
|
|
|
- return pinnacleGet('/v2/odds/special', { sportId: 29, oddsFormat: 'Decimal', leagueIds, since })
|
|
|
|
|
- .then(data => {
|
|
|
|
|
- const { leagues, last } = data;
|
|
|
|
|
- if (!last) {
|
|
|
|
|
- return [];
|
|
|
|
|
- }
|
|
|
|
|
- GLOBAL_DATA.specialsOddsVersion = last;
|
|
|
|
|
- return leagues?.flatMap(league => league.specials);
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 更新特殊赛赔率数据
|
|
|
|
|
- * @returns
|
|
|
|
|
- */
|
|
|
|
|
-const updateSpecialsOdds = async () => {
|
|
|
|
|
- return getSpecialsOdds()
|
|
|
|
|
- .then(specials => {
|
|
|
|
|
- if (specials?.length) {
|
|
|
|
|
- const { gamesMap } = GLOBAL_DATA;
|
|
|
|
|
- const contestants = Object.values(gamesMap)
|
|
|
|
|
- .filter(game => game.specials)
|
|
|
|
|
- .map(game => {
|
|
|
|
|
- const { specials } = game;
|
|
|
|
|
- const contestants = Object.values(specials).map(special => {
|
|
|
|
|
- const { contestants } = special;
|
|
|
|
|
- return contestants.map(contestant => [contestant.id, contestant]);
|
|
|
|
|
- });
|
|
|
|
|
- return contestants;
|
|
|
|
|
- }).flat(2);
|
|
|
|
|
- const contestantsMap = new Map(contestants);
|
|
|
|
|
- const lines = specials.flatMap(special => special.contestantLines);
|
|
|
|
|
- lines.forEach(line => {
|
|
|
|
|
- const { id, handicap, lineId, max, price } = line;
|
|
|
|
|
- const contestant = contestantsMap.get(id);
|
|
|
|
|
- if (!contestant) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- contestant.handicap = handicap;
|
|
|
|
|
- contestant.lineId = lineId;
|
|
|
|
|
- contestant.max = max;
|
|
|
|
|
- contestant.price = price;
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- return Promise.resolve('SpecialsOdds');
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 获取滚球数据
|
|
|
|
|
- * @returns
|
|
|
|
|
- */
|
|
|
|
|
-const getInRunning = async () => {
|
|
|
|
|
- return pinnacleGet('/v2/inrunning')
|
|
|
|
|
- .then(data => {
|
|
|
|
|
- const sportId = 29;
|
|
|
|
|
- const leagues = data.sports?.find(sport => sport.id == sportId)?.leagues ?? [];
|
|
|
|
|
- return leagues.filter(league => {
|
|
|
|
|
- if (!GLOBAL_DATA.selectedLeagues.length) {
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
- const { id } = league;
|
|
|
|
|
- const selectedLeaguesSet = new Set(GLOBAL_DATA.selectedLeagues);
|
|
|
|
|
- return selectedLeaguesSet.has(id);
|
|
|
|
|
- }).flatMap(league => league.events);
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 更新滚球数据
|
|
|
|
|
- * @returns
|
|
|
|
|
- */
|
|
|
|
|
-const updateInRunning = async () => {
|
|
|
|
|
- return getInRunning()
|
|
|
|
|
- .then(games => {
|
|
|
|
|
- if (!games.length) {
|
|
|
|
|
- return Promise.resolve('InRunning');
|
|
|
|
|
- }
|
|
|
|
|
- const { gamesMap } = GLOBAL_DATA;
|
|
|
|
|
- games.forEach(game => {
|
|
|
|
|
- const { id, state, elapsed } = game;
|
|
|
|
|
- const localGame = gamesMap[id];
|
|
|
|
|
- if (localGame) {
|
|
|
|
|
- Object.assign(localGame, { state, elapsed });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- return Promise.resolve('InRunning');
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 获取比赛盘口赔率数据
|
|
|
|
|
- * @returns {Object}
|
|
|
|
|
- */
|
|
|
|
|
-const getGamesEvents = () => {
|
|
|
|
|
- const { relatedGames, gamesMap } = GLOBAL_DATA;
|
|
|
|
|
- const relatedGamesSet = new Set(relatedGames);
|
|
|
|
|
- const nowTime = Date.now();
|
|
|
|
|
- const gamesData = {};
|
|
|
|
|
- Object.values(gamesMap).forEach(game => {
|
|
|
|
|
- const { id, liveStatus, parentId, resultingUnit, timestamp } = game;
|
|
|
|
|
-
|
|
|
|
|
- if (resultingUnit !== 'Regular') {
|
|
|
|
|
- return false; // 非常规赛事不处理
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const gmtMinus4Date = getDateInTimezone(-4);
|
|
|
|
|
- const todayEndTime = new Date(`${gmtMinus4Date} 23:59:59 GMT-4`).getTime();
|
|
|
|
|
- const tomorrowEndTime = todayEndTime + 24 * 60 * 60 * 1000;
|
|
|
|
|
- if (liveStatus == 1 && timestamp < nowTime) {
|
|
|
|
|
- game.marketType = 2; // 滚球赛事
|
|
|
|
|
- }
|
|
|
|
|
- else if (liveStatus != 1 && timestamp > nowTime && timestamp <= todayEndTime) {
|
|
|
|
|
- game.marketType = 1; // 今日赛事
|
|
|
|
|
- }
|
|
|
|
|
- else if (liveStatus != 1 && timestamp > todayEndTime && timestamp <= tomorrowEndTime) {
|
|
|
|
|
- game.marketType = 0; // 明日早盘赛事
|
|
|
|
|
- }
|
|
|
|
|
- else {
|
|
|
|
|
- game.marketType = -1; // 非近期赛事
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (game.marketType < 0) {
|
|
|
|
|
- return false; // 非近期赛事不处理
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- let actived = false;
|
|
|
|
|
- if (liveStatus != 1 && (relatedGamesSet.has(id) || !relatedGames.length)) {
|
|
|
|
|
- actived = true; // 在赛前列表中
|
|
|
|
|
- game.id = id;
|
|
|
|
|
- game.originId = 0;
|
|
|
|
|
- }
|
|
|
|
|
- else if (liveStatus == 1 && (relatedGamesSet.has(parentId) || !relatedGames.length)) {
|
|
|
|
|
- actived = true; // 在滚球列表中
|
|
|
|
|
- game.id = parentId;
|
|
|
|
|
- game.originId = id;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (actived) {
|
|
|
|
|
- const { marketType, ...rest } = parseGame(game);
|
|
|
|
|
- if (!gamesData[marketType]) {
|
|
|
|
|
- gamesData[marketType] = [];
|
|
|
|
|
- }
|
|
|
|
|
- gamesData[marketType].push(rest);
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- const games = Object.values(gamesData).flat();
|
|
|
|
|
- const timestamp = nowTime;
|
|
|
|
|
- const data = { games, timestamp };
|
|
|
|
|
- return data;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 更新赔率数据
|
|
|
|
|
- */
|
|
|
|
|
-const updateOdds = async () => {
|
|
|
|
|
- const { games, timestamp } = getGamesEvents();
|
|
|
|
|
- return platformPost('/api/platforms/update_odds', { platform: 'pinnacle', games, timestamp });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-const pinnacleDataLoop = () => {
|
|
|
|
|
- updateStraightFixtures()
|
|
|
|
|
- .then(() => {
|
|
|
|
|
- return Promise.all([
|
|
|
|
|
- updateStraightOdds(),
|
|
|
|
|
- updateSpecialFixtures(),
|
|
|
|
|
- updateInRunning(),
|
|
|
|
|
- ]);
|
|
|
|
|
- })
|
|
|
|
|
- .then(() => {
|
|
|
|
|
- return updateSpecialsOdds();
|
|
|
|
|
- })
|
|
|
|
|
- .then(() => {
|
|
|
|
|
- if (!GLOBAL_DATA.loopActive) {
|
|
|
|
|
- GLOBAL_DATA.loopActive = true;
|
|
|
|
|
- Logs.out('loop active');
|
|
|
|
|
- notifyException('Pinnacle API startup.');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (GLOBAL_DATA.requestErrorCount > 0) {
|
|
|
|
|
- GLOBAL_DATA.requestErrorCount = 0;
|
|
|
|
|
- Logs.out('request error count reset');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const nowTime = Date.now();
|
|
|
|
|
- const loopDuration = nowTime - GLOBAL_DATA.loopLastResultTime;
|
|
|
|
|
- GLOBAL_DATA.loopLastResultTime = nowTime;
|
|
|
|
|
- if (loopDuration > 15000) {
|
|
|
|
|
- Logs.out('loop duration is too long', loopDuration);
|
|
|
|
|
- }
|
|
|
|
|
- else {
|
|
|
|
|
- Logs.outDev('loop duration', loopDuration);
|
|
|
|
|
- }
|
|
|
|
|
- setData(gamesMapCacheFile, GLOBAL_DATA.gamesMap);
|
|
|
|
|
-
|
|
|
|
|
- return updateOdds();
|
|
|
|
|
- })
|
|
|
|
|
- .catch(err => {
|
|
|
|
|
- Logs.err(err.message, err.source);
|
|
|
|
|
- GLOBAL_DATA.requestErrorCount++;
|
|
|
|
|
- if (GLOBAL_DATA.loopActive && GLOBAL_DATA.requestErrorCount > 5) {
|
|
|
|
|
- const exceptionMessage = 'request errors have reached the limit';
|
|
|
|
|
- Logs.out(exceptionMessage);
|
|
|
|
|
- GLOBAL_DATA.loopActive = false;
|
|
|
|
|
-
|
|
|
|
|
- Logs.out('loop inactive');
|
|
|
|
|
- notifyException(`Pinnacle API paused. ${exceptionMessage}. ${err.message}`);
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
- .finally(() => {
|
|
|
|
|
- const { loopActive, selectedLeagues } = GLOBAL_DATA;
|
|
|
|
|
- let loopDelay = 5_000;
|
|
|
|
|
- if (!loopActive) {
|
|
|
|
|
- loopDelay = 60_000;
|
|
|
|
|
- resetVersionsCount();
|
|
|
|
|
- }
|
|
|
|
|
- else if (!selectedLeagues.length) {
|
|
|
|
|
- resetVersionsCount();
|
|
|
|
|
- }
|
|
|
|
|
- else {
|
|
|
|
|
- incrementVersionsCount();
|
|
|
|
|
- }
|
|
|
|
|
- setTimeout(pinnacleDataLoop, loopDelay);
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 缓存GLOBAL_DATA数据到文件
|
|
|
|
|
- */
|
|
|
|
|
-const saveGlobalDataToCache = async () => {
|
|
|
|
|
- return setData(globalDataCacheFile, GLOBAL_DATA);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const loadGlobalDataFromCache = async () => {
|
|
|
|
|
- return getData(globalDataCacheFile)
|
|
|
|
|
- .then(data => {
|
|
|
|
|
- if (!data) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- Object.keys(GLOBAL_DATA).forEach(key => {
|
|
|
|
|
- if (key in data) {
|
|
|
|
|
- GLOBAL_DATA[key] = data[key];
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const main = () => {
|
|
|
|
|
- if (!process.env.PINNACLE_USERNAME || !process.env.PINNACLE_PASSWORD) {
|
|
|
|
|
- Logs.err('USERNAME or PASSWORD is not set');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- updateRelatedLeagues();
|
|
|
|
|
- updateRelatedGames();
|
|
|
|
|
- loadGlobalDataFromCache()
|
|
|
|
|
- .then(() => {
|
|
|
|
|
- Logs.out('global data loaded');
|
|
|
|
|
- })
|
|
|
|
|
- .then(() => {
|
|
|
|
|
- updateWebLeaguesLoop();
|
|
|
|
|
- updateWebGamesLoop();
|
|
|
|
|
- pinnacleDataLoop();
|
|
|
|
|
- })
|
|
|
|
|
- .catch(err => {
|
|
|
|
|
- Logs.err('failed to load global data', err.message);
|
|
|
|
|
- })
|
|
|
|
|
- .finally(() => {
|
|
|
|
|
- GLOBAL_DATA.loopLastResultTime = Date.now();
|
|
|
|
|
- GLOBAL_DATA.loopActive = true;
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 监听进程退出事件,保存GLOBAL_DATA数据
|
|
|
|
|
-const saveExit = (code) => {
|
|
|
|
|
- saveGlobalDataToCache()
|
|
|
|
|
- .then(() => {
|
|
|
|
|
- Logs.out('global data saved');
|
|
|
|
|
- })
|
|
|
|
|
- .catch(err => {
|
|
|
|
|
- Logs.err('failed to save global data', err.message);
|
|
|
|
|
- })
|
|
|
|
|
- .finally(() => {
|
|
|
|
|
- process.exit(code);
|
|
|
|
|
- });
|
|
|
|
|
-}
|
|
|
|
|
-process.on('SIGINT', () => {
|
|
|
|
|
- saveExit(0);
|
|
|
|
|
-});
|
|
|
|
|
-process.on('SIGTERM', () => {
|
|
|
|
|
- saveExit(0);
|
|
|
|
|
-});
|
|
|
|
|
-process.on('SIGUSR2', () => {
|
|
|
|
|
- saveExit(0);
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-main();
|
|
|