Din data, dina verktyg
Rakt in i Excel eller Google Kalkylark
Ett kalkylblad kräver ingen egen kod. Excel 365 läser API:et via Power Query, Google Kalkylark behöver ett kort Apps Script. Båda håller din token borta från cellerna.
Excel 365
- Välj Data → Hämta data → Från andra källor → Tom fråga i Excel och öppna sedan Avancerad redigerare.
- Klistra in frågan nedan med din token i stället för platshållaren och välj Stäng och läs in.
- Uppdatera med Data → Uppdatera alla när du vill ha nya ronder. Vill du hålla din token borta från arbetsboken laddar du ner din export från profilsidan och använder Hämta data → Från fil → Från JSON i stället.
let
Token = "<token>",
Source = Json.Document(Web.Contents("https://backend.goffy.app/api/me/rounds", [Headers = [Authorization = "Bearer " & Token]])),
Rounds = Table.FromRecords(Source),
Typed = Table.TransformColumnTypes(Rounds, {{"playDate", type date}, {"holesScored", Int64.Type}, {"grossStrokes", Int64.Type}, {"toPar", Int64.Type}, {"meritPoints", type number}})
in
TypedGoogle Kalkylark
- Öppna kalkylarket du vill ha ronderna i och välj Tillägg → Apps Script. Starta det från arket, inte från script.google.com: ett fristående skript har inget kalkylark att skriva till. Ersätt det som står i redigeraren med skriptet nedan och spara.
- Lägg till en skriptegenskap med namnet GOFFY_TOKEN och din token under Projektinställningar. Klistra aldrig in din token i en cell.
- Bocka för Visa manifestfilen appsscript.json, fortfarande under Projektinställningar, och ersätt manifestet med det nedan. Det begränsar skriptet till det här kalkylarket och Goffys API, vilket är precis vad Googles behörighetsskärm sedan visar.
- Kör importGoffyRounds en gång. Google varnar för att skriptet inte är verifierat, som det gör för alla skript du skrivit själv: välj Avancerat, sedan Gå till ditt projekt och därefter Tillåt.
- Gå tillbaka till kalkylarket. Ronderna ligger i en ny flik som heter Rounds — skriptet skapar den, så det är inte fliken du tittade på. Körningsloggen säger hur många rader som skrevs; bara en rubrikrad betyder att API:et inte hittade några ronder för ditt konto.
- Lägg till en tidsstyrd utlösare för importGoffyRounds under Utlösare för att hålla arket aktuellt. Varje körning skriver om fliken Rounds.
{
"timeZone": "Europe/Stockholm",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets.currentonly",
"https://www.googleapis.com/auth/script.external_request"
]
}const GOFFY_API = 'https://backend.goffy.app';
const SHEET_NAME = 'Rounds';
function importGoffyRounds() {
const book = SpreadsheetApp.getActiveSpreadsheet();
if (!book) throw new Error('No spreadsheet attached. Create the script from the sheet: Extensions → Apps Script.');
const token = PropertiesService.getScriptProperties().getProperty('GOFFY_TOKEN');
if (!token) throw new Error('Add GOFFY_TOKEN under Project Settings → Script properties.');
const response = UrlFetchApp.fetch(GOFFY_API + '/api/me/rounds', {
headers: { Authorization: 'Bearer ' + token },
muteHttpExceptions: true,
});
if (response.getResponseCode() !== 200) {
throw new Error('Goffy answered ' + response.getResponseCode() + ': ' + response.getContentText().slice(0, 300));
}
const rounds = JSON.parse(response.getContentText());
const columns = ['playDate', 'courseName', 'tourName', 'status', 'holesScored', 'grossStrokes', 'toPar', 'meritPoints', 'position', 'fieldSize'];
const sheet = book.getSheetByName(SHEET_NAME) || book.insertSheet(SHEET_NAME);
sheet.clear();
sheet.getRange(1, 1, 1, columns.length).setValues([columns]).setFontWeight('bold');
if (rounds.length) {
const rows = rounds.map((round) => columns.map((column) => (round[column] == null ? '' : round[column])));
sheet.getRange(2, 1, rows.length, columns.length).setValues(rows);
}
sheet.setFrozenRows(1);
sheet.autoResizeColumns(1, columns.length);
book.setActiveSheet(sheet);
book.toast(rounds.length + ' rounds written to "' + SHEET_NAME + '"', 'Goffy');
Logger.log('Wrote %s rounds to "%s"', rounds.length, SHEET_NAME);
}Varje hål, med par, längd och slagdetaljer
Rondlistan har en rad per rond. Den här har en rad per spelare och hål — par, slagindex, hålets längd, brutto-, handicap- och nettoslag, upplockade bollar, greenträffar och en kolumn för varje sorts slagdetalj ronden registrerat.
- Sätt upp det precis som rondskriptet ovan: Tillägg → Apps Script från själva kalkylarket, samma skriptegenskap GOFFY_TOKEN, samma manifest.
- Klistra in det här skriptet i stället, spara och kör importGoffyHoles. Det läser dina tjugo senaste ronder — ett anrop per rond, så det tar några sekunder — och skriver dem till en flik som heter Holes.
- En tom cell i lengthMeters betyder att Goffy inte har mätt hålet själv. Bara vår egen mätning hamnar i den kolumnen: ett tal från ett importerat scorekort skulle läsas som vårt och vara fel oftare än rätt.
const GOFFY_API = 'https://backend.goffy.app';
const SHEET_NAME = 'Holes';
const MAX_ROUNDS = 20; // Newest first. Each round is one more request, so raise it deliberately.
function importGoffyHoles() {
const book = SpreadsheetApp.getActiveSpreadsheet();
if (!book) throw new Error('No spreadsheet attached. Create the script from the sheet: Extensions -> Apps Script.');
const token = PropertiesService.getScriptProperties().getProperty('GOFFY_TOKEN');
if (!token) throw new Error('Add GOFFY_TOKEN under Project Settings -> Script properties.');
const rounds = fetchGoffy('/api/me/rounds', token).slice(0, MAX_ROUNDS);
const rows = [];
const shotTypes = {};
rounds.forEach(function (summary) {
const round = fetchGoffy('/api/me/rounds/' + summary.roundId, token);
const names = {};
round.players.forEach(function (player) { names[player.playerId] = player.name || player.playerId; });
round.holeEntries.forEach(function (entry) {
const events = round.holeEvents.filter(function (event) {
return event.playerId === entry.playerId && event.holeNumber === entry.hole;
});
events.forEach(function (event) { shotTypes[event.eventType] = true; });
rows.push({
playDate: summary.playDate,
courseName: summary.courseName,
player: names[entry.playerId] || entry.playerId,
hole: entry.hole,
par: entry.par,
strokeIndex: entry.holeIndex,
// Blank on a hole Goffy has not measured itself. It is never filled in from an
// imported scorecard, so an empty cell means unknown, not "no length".
lengthMeters: entry.lengthMeters,
grossStrokes: entry.grossStroke,
handicapStrokes: entry.handicapStroke,
netStrokes: entry.netStroke,
pickup: entry.pickup,
greenInRegulation: entry.greenInRegulation,
events: events,
});
});
});
const fixed = ['playDate', 'courseName', 'player', 'hole', 'par', 'strokeIndex', 'lengthMeters',
'grossStrokes', 'handicapStrokes', 'netStrokes', 'pickup', 'greenInRegulation'];
const shotColumns = Object.keys(shotTypes).sort();
const columns = fixed.concat(shotColumns);
const sheet = book.getSheetByName(SHEET_NAME) || book.insertSheet(SHEET_NAME);
sheet.clear();
sheet.getRange(1, 1, 1, columns.length).setValues([columns]).setFontWeight('bold');
if (rows.length) {
const values = rows.map(function (row) {
return columns.map(function (column) {
if (shotColumns.indexOf(column) === -1) return row[column] == null ? '' : row[column];
const hit = row.events.filter(function (event) { return event.eventType === column; })[0];
if (!hit) return '';
// An event with no value happened at all: fairway hit, ball in water.
return hit.eventValue == null ? 'yes' : hit.eventValue;
});
});
sheet.getRange(2, 1, values.length, columns.length).setValues(values);
}
sheet.setFrozenRows(1);
sheet.autoResizeColumns(1, columns.length);
book.setActiveSheet(sheet);
book.toast(rows.length + ' hole rows from ' + rounds.length + ' rounds', 'Goffy');
Logger.log('Wrote %s hole rows from %s rounds', rows.length, rounds.length);
}
function fetchGoffy(path, token) {
const response = UrlFetchApp.fetch(GOFFY_API + path, {
headers: { Authorization: 'Bearer ' + token },
muteHttpExceptions: true,
});
if (response.getResponseCode() !== 200) {
throw new Error('Goffy answered ' + response.getResponseCode() + ' for ' + path + ': ' + response.getContentText().slice(0, 300));
}
return JSON.parse(response.getContentText());
}Power BI
Power BI kör samma Power Query-motor som Excel, så frågan ovan fungerar där som den är — Hämta data, Tom fråga, Avancerad redigerare. Ingen gateway behövs: API:et är en publik endpoint. Den här andra frågan är den per hål, och den är skriven som Power BI-tjänsten kräver.
- Använd en personlig åtkomsttoken, inte din apptoken. En apptoken gäller i 24 timmar, så en schemalagd uppdatering skulle sluta fungera dagen efter.
- Lägg märke till var rond-id:t hamnar. Ett anrop per rond betyder en URL byggd av data, och Power BI-tjänsten vägrar uppdatera sådana på schema. Att låta värdnamnet ligga kvar i Web.Contents och resten i RelativePath är det som håller den uppdateringsbar.
- Token ligger i frågan och följer alltså med i .pbix-filen — alla som kan redigera den publicerade datamängden kan läsa den. Power Querys autentiseringstyp Webb-API lägger den i autentiseringslagret i stället. Oavsett vilket slutar token gälla efter ett år och uppdateringen stannar då, så lägg en påminnelse någonstans.
- Rikta inte en schemalagd uppdatering mot exportvägen. Den tillåter två anrop i minuten, vilket passar för en engångshämtning av allt och inte för ett schema.
let
Token = "<token>",
Base = "https://backend.goffy.app",
Get = (path as text) =>
Json.Document(Web.Contents(Base, [
RelativePath = path,
Headers = [Authorization = "Bearer " & Token]
])),
Rounds = Table.FirstN(Table.FromRecords(Get("api/me/rounds")), 20),
Detail = Table.AddColumn(Rounds, "detail", each Get("api/me/rounds/" & [roundId])),
Lines = Table.ExpandListColumn(
Table.AddColumn(Detail, "entry", each [detail][holeEntries]), "entry"),
Holes = Table.ExpandRecordColumn(Lines, "entry",
{"playerId", "hole", "par", "holeIndex", "lengthMeters",
"grossStroke", "handicapStroke", "netStroke", "pickup", "greenInRegulation"})
in
HolesAllt appen visar om dig
Varje anrop knyts till din token. Det finns inget id någonstans i API:et, så ett anrop kan aldrig nå någon annans data.
- ProfilNamn, e-post, golf-id, hemmaklubbar och ditt handicapindex.
- TourerVarje tour du är medlem i, även avslutade.
- RonderVarje rond du spelat, och för var och en dina egna hålnoteringar, hålhändelser och scorer.
- StatistikRonder, tourer, spel, troféer och scoringtrender, per kalenderår eller sammanlagt.
- ExportAlltihop som en JSON-fil, från profilsidan eller från API:et.
Två vägar in
Skicka en bearer-token i Authorization-headern. Båda sorterna fungerar på alla rutter.
Din appsession
Samma token som appen använder. Den gäller i 24 timmar, så den passar interaktiv användning och engångsskript, inte något obevakat.
En personlig åtkomsttoken
Skapas under Profil → API-åtkomst i appen. Den börjar med gpat_, visas exakt en gång, går ut efter ett år och kan återkallas när som helst. Skrivskyddad per konstruktion: den fungerar på användardatarutterna och ingen annanstans.
Behandla en token som ett lösenord. Den som har den kan läsa allt det här API:et returnerar, inklusive din e-post.
Två curl-anrop
Klistra in din token i stället för platshållaren. Det första anropet returnerar din profil, det andra sparar din export.
curl -H "Authorization: Bearer <token>" https://backend.goffy.app/api/me
curl -H "Authorization: Bearer <token>" -o goffy-export.json https://backend.goffy.app/api/me/exportEller installera en klient
Genererade från samma kontrakt för TypeScript, .NET och Python, och publicerade vid varje version. Var och en tar din token och ger dig typade anrop.
TypeScript · @goffy/user-data-client 1.2.0
npm config set @goffy:registry https://git.zyberze.ro/api/packages/goffy/npm/
npm install @goffy/user-data-clientimport { createGoffyClient } from '@goffy/user-data-client';
const goffy = createGoffyClient(process.env.GOFFY_TOKEN);
const me = await goffy.api.me.get();
const rounds = await goffy.api.me.rounds.get();.NET · Goffy.UserData.Client 1.2.0
dotnet nuget add source https://git.zyberze.ro/api/packages/goffy/nuget/index.json --name goffy
dotnet add package Goffy.UserData.Clientvar goffy = GoffyUserDataClientFactory.Create(token);
var me = await goffy.Api.Me.GetAsync();
var rounds = await goffy.Api.Me.Rounds.GetAsync();Python · goffy-user-data-client 1.2.0
pip install goffy-user-data-client --index-url https://git.zyberze.ro/api/packages/goffy/pypi/simple/from goffy_user_data_client import create_client
goffy = create_client(token)
me = await goffy.api.me.get()
rounds = await goffy.api.me.rounds.get()Rutterna har en egen sida
Varje rutt, dess parametrar och exakt vad den svarar med — genererat från kontraktet, så det är precis vad servern gör. Det är långt, så det bor i rummet bredvid.
Spelreglerna
- Ronderna du spelat, i sin helhetEn rond kommer tillbaka så som appen visar den för dig: varje spelares scorekort, spelpoängen och leaderboarden.
- De du spelat medTourer och ronder namnger de andra spelarna och innehåller deras scorer och pinnar. Deras kontouppgifter — e-post, golf-id — följer aldrig med.
- BegränsningarExporten är begränsad till två nedladdningar per minut. Tio okända tokens från samma adress låser ute den i en kvart.
- Tokens agerar som duÅterkalla en token så fort du inte längre behöver den. Raderar du ditt konto återkallas alla.
OpenAPI, om du vill ha allt
Allt på den här sidan genereras från OpenAPI-dokumentet. Ladda ner det, eller läs det live från API:et.