Initial commit
This commit is contained in:
parent
93509aaa5e
commit
9add3b5149
3 changed files with 130 additions and 0 deletions
23
docker-compose.yml
Normal file
23
docker-compose.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
services:
|
||||||
|
biggame-xml-proxy:
|
||||||
|
image: node:20-alpine
|
||||||
|
container_name: biggame-xml-proxy
|
||||||
|
working_dir: /app
|
||||||
|
volumes:
|
||||||
|
- ./:/app
|
||||||
|
command: sh -c "npm install --silent && node server.js"
|
||||||
|
ports:
|
||||||
|
- "3737:3737"
|
||||||
|
environment:
|
||||||
|
BG_BEARER_TOKEN: "bg_live_k_K1zO7Jh-fMkRttp8.uoD4OzE3L_SrLXu0tTsS_4nDdL8DkhzRV6eOSXSkivY"
|
||||||
|
BG_EVENT_ID: "1"
|
||||||
|
BG_RANKING: "stableford_gross"
|
||||||
|
POLL_INTERVAL_MS: "10000"
|
||||||
|
STALE_THRESHOLD_MS: "60000"
|
||||||
|
PORT: "3737"
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:3737/ping"]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
11
package.json
Normal file
11
package.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"name": "biggame-xml-proxy",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "JSON-to-XML proxy bridging Big Game Golf API to Ross XPression DataLinq",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": { "start": "node server.js" },
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"fast-xml-parser": "^4.4.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
96
server.js
Normal file
96
server.js
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
'use strict';
|
||||||
|
const express = require('express');
|
||||||
|
const https = require('https');
|
||||||
|
const { XMLBuilder } = require('fast-xml-parser');
|
||||||
|
const CONFIG = {
|
||||||
|
BG_BASE_URL: 'https://api.biggamegolf.com/api/integrations/v1',
|
||||||
|
BG_BEARER_TOKEN: process.env.BG_BEARER_TOKEN || 'MISSING',
|
||||||
|
BG_EVENT_ID: process.env.BG_EVENT_ID || '1',
|
||||||
|
BG_RANKING: process.env.BG_RANKING || 'stableford_gross',
|
||||||
|
POLL_INTERVAL_MS: parseInt(process.env.POLL_INTERVAL_MS || '10000'),
|
||||||
|
STALE_THRESHOLD_MS: parseInt(process.env.STALE_THRESHOLD_MS || '60000'),
|
||||||
|
PORT: parseInt(process.env.PORT || '3737'),
|
||||||
|
};
|
||||||
|
const state = { summaryXml:null, playerCache:{}, lastUpdated:null, lastError:null, fetchCount:0, errorCount:0 };
|
||||||
|
function bgFetch(path) {
|
||||||
|
return new Promise((resolve,reject) => {
|
||||||
|
const url=`${CONFIG.BG_BASE_URL}${path}`;
|
||||||
|
https.get(url,{headers:{Authorization:`Bearer ${CONFIG.BG_BEARER_TOKEN}`,Accept:'application/json'}},(res)=>{
|
||||||
|
let body='';
|
||||||
|
res.on('data',(c)=>body+=c);
|
||||||
|
res.on('end',()=>{
|
||||||
|
if(res.statusCode===200){try{resolve(JSON.parse(body));}catch(e){reject(new Error(e.message));}}
|
||||||
|
else{reject(new Error(`HTTP ${res.statusCode}: ${body}`));}
|
||||||
|
});
|
||||||
|
}).on('error',reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const builder=new XMLBuilder({ignoreAttributes:false,attributeNamePrefix:'@_',format:true,suppressEmptyNode:true});
|
||||||
|
function fmtPar(v){if(v===null||v===undefined)return '';if(v===0)return 'E';return v>0?`+${v}`:String(v);}
|
||||||
|
function isStale(){if(!state.lastUpdated)return true;return(Date.now()-state.lastUpdated.getTime())>CONFIG.STALE_THRESHOLD_MS;}
|
||||||
|
function buildSummaryXml(data){
|
||||||
|
const{leaderboard,players}=data;
|
||||||
|
const now=new Date().toISOString();
|
||||||
|
const xmlPlayers=players.map((p)=>{
|
||||||
|
const r1=(p.rounds||[]).find(r=>r.round===1)||{};
|
||||||
|
const r2=(p.rounds||[]).find(r=>r.round===2)||{};
|
||||||
|
return{'@_rank':p.standing.position,'@_tied':p.standing.isTied?1:0,'@_sortOrder':p.standing.sortOrder,
|
||||||
|
'@_playerId':p.playerId,'@_firstName':p.firstName,'@_lastName':p.lastName,'@_displayName':p.displayName,'@_profilePicUrl':p.profilePicUrl||'',
|
||||||
|
'@_points':p.summary.points,'@_strokes':p.summary.strokes,'@_toPar':p.summary.toPar,'@_toParDisplay':fmtPar(p.summary.toPar),
|
||||||
|
'@_holesPlayed':p.summary.holesPlayed,'@_thru':p.summary.thru,
|
||||||
|
'@_r1Points':r1.points??'','@_r1Strokes':r1.strokes??'','@_r1ToPar':r1.toPar??'','@_r1ToParDisplay':fmtPar(r1.toPar),'@_r1Status':r1.status||'','@_r1HolesPlayed':r1.holesPlayed??'',
|
||||||
|
'@_r2Points':r2.points??'','@_r2Strokes':r2.strokes??'','@_r2ToPar':r2.toPar??'','@_r2ToParDisplay':fmtPar(r2.toPar),'@_r2Status':r2.status||'','@_r2HolesPlayed':r2.holesPlayed??''};
|
||||||
|
});
|
||||||
|
const rds=(leaderboard.eventRounds||[]).map(r=>({'@_round':r.round,'@_label':r.label,'@_date':r.date,'@_playedHoles':r.playedHoles,'@_facility':r.course?.facility||'','@_course':r.course?.name||'','@_location':r.course?.location||''}));
|
||||||
|
const doc={leaderboard:{'@_eventId':leaderboard.eventId,'@_eventName':leaderboard.name,'@_eventStart':leaderboard.eventDates?.start||'','@_eventEnd':leaderboard.eventDates?.end||'','@_ranking':`${leaderboard.ranking?.pointSystem}_${leaderboard.ranking?.handicapMode}`,'@_updated':now,'@_stale':isStale()?1:0,rounds:{round:rds},players:{player:xmlPlayers}}};
|
||||||
|
return '<?xml version="1.0" encoding="UTF-8"?>\n'+builder.build(doc);
|
||||||
|
}
|
||||||
|
function buildPlayerXml(data){
|
||||||
|
const{leaderboard,player,standing,summary,detail}=data;
|
||||||
|
const now=new Date().toISOString();
|
||||||
|
const rds=(detail.rounds||[]).map(r=>({'@_round':r.round,'@_label':r.label,'@_date':r.date,'@_status':r.status,'@_points':r.points??'','@_strokes':r.strokes??'','@_toPar':r.toPar??'','@_toParDisplay':fmtPar(r.toPar),'@_holesPlayed':r.holesPlayed??''}));
|
||||||
|
const scs=(detail.scorecards||[]).map(sc=>{const holes=(sc.holes||[]).map(h=>({'@_hole':h.hole,'@_par':h.par??'','@_strokes':h.strokes??'','@_points':h.points??'','@_scoreX':h.scoreX?1:0}));return{'@_round':sc.round,'@_date':sc.date,'@_pointsOut':sc.points?.out??'','@_pointsIn':sc.points?.in??'','@_pointsTotal':sc.points?.total??'','@_strokesOut':sc.strokes?.out??'','@_strokesIn':sc.strokes?.in??'','@_strokesTotal':sc.strokes?.total??'',hole:holes};});
|
||||||
|
const doc={playerDetail:{'@_eventId':leaderboard.eventId,'@_eventName':leaderboard.name,'@_updated':now,'@_stale':isStale()?1:0,player:{'@_playerId':player.playerId,'@_firstName':player.firstName,'@_lastName':player.lastName,'@_displayName':player.displayName,'@_profilePicUrl':player.profilePicUrl||'','@_rank':standing.position,'@_tied':standing.isTied?1:0,'@_points':summary.points,'@_strokes':summary.strokes,'@_toPar':summary.toPar,'@_toParDisplay':fmtPar(summary.toPar),'@_holesPlayed':summary.holesPlayed,'@_thru':summary.thru},rounds:{round:rds},scorecards:{scorecard:scs}}};
|
||||||
|
return '<?xml version="1.0" encoding="UTF-8"?>\n'+builder.build(doc);
|
||||||
|
}
|
||||||
|
async function pollSummary(){
|
||||||
|
try{
|
||||||
|
console.log(`[poll] Fetching event ${CONFIG.BG_EVENT_ID}...`);
|
||||||
|
const data=await bgFetch(`/leaderboards/${CONFIG.BG_EVENT_ID}/summary/?ranking=${CONFIG.BG_RANKING}`);
|
||||||
|
state.summaryXml=buildSummaryXml(data);
|
||||||
|
state.lastUpdated=new Date();
|
||||||
|
state.lastError=null;
|
||||||
|
state.fetchCount++;
|
||||||
|
console.log(`[poll] OK - ${data.players?.length??0} players`);
|
||||||
|
}catch(err){
|
||||||
|
state.lastError=err.message;
|
||||||
|
state.errorCount++;
|
||||||
|
console.error(`[poll] ERROR: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const app=express();
|
||||||
|
app.get('/leaderboard',(req,res)=>{
|
||||||
|
if(!state.summaryXml)return res.status(503).send('<?xml version="1.0"?><error>not_ready</error>');
|
||||||
|
res.set('Content-Type','application/xml').send(state.summaryXml);
|
||||||
|
});
|
||||||
|
app.get('/player/:id',async(req,res)=>{
|
||||||
|
const id=req.params.id;
|
||||||
|
const c=state.playerCache[id];
|
||||||
|
if(c&&(Date.now()-c.ts)<CONFIG.POLL_INTERVAL_MS)return res.set('Content-Type','application/xml').send(c.xml);
|
||||||
|
try{
|
||||||
|
const data=await bgFetch(`/leaderboards/${CONFIG.BG_EVENT_ID}/players/${id}/?ranking=${CONFIG.BG_RANKING}`);
|
||||||
|
const xml=buildPlayerXml(data);
|
||||||
|
state.playerCache[id]={xml,ts:Date.now()};
|
||||||
|
res.set('Content-Type','application/xml').send(xml);
|
||||||
|
}catch(err){
|
||||||
|
if(c)return res.set('Content-Type','application/xml').send(c.xml);
|
||||||
|
res.status(502).send(`<?xml version="1.0"?><error>${err.message}</error>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.get('/status',(req,res)=>res.json({ok:!!state.summaryXml&&!isStale(),stale:isStale(),lastUpdated:state.lastUpdated,lastError:state.lastError,fetchCount:state.fetchCount,errorCount:state.errorCount,eventId:CONFIG.BG_EVENT_ID,cachedPlayers:Object.keys(state.playerCache)}));
|
||||||
|
app.get('/ping',(req,res)=>res.send('pong'));
|
||||||
|
app.listen(CONFIG.PORT,()=>{
|
||||||
|
console.log(`[proxy] Big Game Golf XML Proxy on :${CONFIG.PORT}`);
|
||||||
|
pollSummary();
|
||||||
|
setInterval(pollSummary,CONFIG.POLL_INTERVAL_MS);
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue