| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762 |
- 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 = {
- filteredLeagues: [],
- filteredGames: [],
- 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);
- return platformPost('/api/platforms/update_leagues', { platform: 'pinnacle', leagues });
- })
- .then(() => {
- Logs.outDev('leagues list updated');
- return Promise.resolve(60);
- });
- }
- /**
- * 定时更新Web联赛列表
- */
- const updateWebLeaguesLoop = () => {
- updateWebLeagues()
- .then(delay => {
- 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.filteredLeagues.length) {
- return Promise.resolve(5);
- }
- const getWebGamesToday = pinnacleWebGames(GLOBAL_DATA.filteredLeagues, 1);
- const getWebGamesTomorrow = pinnacleWebGames(GLOBAL_DATA.filteredLeagues, 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);
- return platformPost('/api/platforms/update_games', { platform: 'pinnacle', games });
- })
- .then(() => {
- Logs.outDev('games list updated');
- return Promise.resolve(60);
- });
- }
- /**
- * 定时更新Web比赛列表
- */
- const updateWebGamesLoop = () => {
- updateWebGames()
- .then(delay => {
- setTimeout(() => {
- updateWebGamesLoop();
- }, 1000 * delay);
- })
- .catch(err => {
- Logs.err('failed to update games list', err.message);
- setTimeout(() => {
- updateWebGamesLoop();
- }, 1000 * 5);
- });
- }
- /**
- * 获取过滤后的联赛数据
- * @returns
- */
- const updateFilteredLeagues = () => {
- platformGet('/api/platforms/get_filtered_leagues', { platform: 'pinnacle' })
- .then(res => {
- const { data: filteredLeagues } = res;
- GLOBAL_DATA.filteredLeagues = filteredLeagues.map(item => item.id);
- })
- .catch(error => {
- Logs.err('failed to update filtered leagues', error.message);
- })
- .finally(() => {
- setTimeout(() => {
- updateFilteredLeagues();
- }, 1000 * 10);
- });
- }
- /**
- * 获取过滤后的比赛数据
- * @returns
- */
- const updateFilteredGames = () => {
- platformGet('/api/platforms/get_filtered_games', { platform: 'pinnacle' })
- .then(res => {
- const { data: filteredGames } = res;
- GLOBAL_DATA.filteredGames = filteredGames.map(item => item.id);
- })
- .catch(error => {
- Logs.err('failed to update filtered games', error.message);
- })
- .finally(() => {
- setTimeout(() => {
- updateFilteredGames();
- }, 1000 * 10);
- });
- }
- /**
- * 获取直赛数据
- * @returns
- */
- const getStraightFixtures = async () => {
- if (!GLOBAL_DATA.filteredLeagues.length) {
- resetVersionsCount();
- return Promise.reject(new Error('no filtered leagues', { cause: 400 }));
- }
- const leagueIds = GLOBAL_DATA.filteredLeagues.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.filteredLeagues.length) {
- resetVersionsCount();
- return Promise.reject(new Error('no filtered leagues', { cause: 400 }));
- }
- const leagueIds = GLOBAL_DATA.filteredLeagues.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.filteredLeagues.length) {
- resetVersionsCount();
- return Promise.reject(new Error('no filtered leagues', { cause: 400 }));
- }
- const leagueIds = GLOBAL_DATA.filteredLeagues.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.filteredLeagues.length) {
- resetVersionsCount();
- return Promise.reject(new Error('no filtered leagues', { cause: 400 }));
- }
- const leagueIds = GLOBAL_DATA.filteredLeagues.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.filteredLeagues.length) {
- return true;
- }
- const { id } = league;
- const filteredLeaguesSet = new Set(GLOBAL_DATA.filteredLeagues);
- return filteredLeaguesSet.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 {
- filteredGames, gamesMap,
- straightFixturesVersion: sfv,
- specialFixturesVersion: pfv,
- straightOddsVersion: sov,
- specialsOddsVersion: pov
- } = GLOBAL_DATA;
- const filteredGamesSet = new Set(filteredGames);
- 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 && (filteredGamesSet.has(id) || !filteredGames.length)) {
- actived = true; // 在赛前列表中
- game.id = id;
- game.originId = 0;
- }
- else if (liveStatus == 1 && (filteredGamesSet.has(parentId) || !filteredGames.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 = Math.max(sfv, pfv, sov, pov);
- 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 } = GLOBAL_DATA;
- let loopDelay = 1000 * 5;
- if (!loopActive) {
- loopDelay = 1000 * 60;
- 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;
- }
- updateFilteredLeagues();
- updateFilteredGames();
- 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();
|