From 269c635f988396878fcf816c55b585cc3e7392d7 Mon Sep 17 00:00:00 2001 From: gari Date: Sat, 29 Jan 2022 19:25:03 +0100 Subject: [PATCH] initial commit --- .envrc | 1 + .gitignore | 95 ++++++++++++++++ Readme.md | 3 + berkutschi/http.go | 59 ++++++++++ berkutschi/pollType.go | 246 ++++++++++++++++++++++++++++++++++++++++ berkutschi/types.go | 35 ++++++ berkutschi/websocket.go | 129 +++++++++++++++++++++ default.nix | 7 ++ flake.lock | 41 +++++++ flake.nix | 15 +++ go.mod | 18 +++ go.sum | 98 ++++++++++++++++ main.go | 165 +++++++++++++++++++++++++++ package.nix | 30 +++++ types.go | 125 ++++++++++++++++++++ 15 files changed, 1067 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 Readme.md create mode 100644 berkutschi/http.go create mode 100644 berkutschi/pollType.go create mode 100644 berkutschi/types.go create mode 100644 berkutschi/websocket.go create mode 100644 default.nix create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 package.nix create mode 100644 types.go diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..8392d15 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd9abdf --- /dev/null +++ b/.gitignore @@ -0,0 +1,95 @@ +#nix flake build output +result/ + +*.log + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Go template +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..8956959 --- /dev/null +++ b/Readme.md @@ -0,0 +1,3 @@ +# Berkutschi Ski Jumping Data Scraper + +This is very much WIP \ No newline at end of file diff --git a/berkutschi/http.go b/berkutschi/http.go new file mode 100644 index 0000000..9bb4ad0 --- /dev/null +++ b/berkutschi/http.go @@ -0,0 +1,59 @@ +package berkutschi + +import ( + "context" + "fmt" + "time" + + "github.com/monaco-io/request" + "github.com/rs/zerolog/log" +) + +var pollURL = "https://live.berkutschi.com/events/" + +func Poll(event int) (PollData, error) { + ctx, _ := context.WithTimeout(context.Background(), 15*time.Second) + c := request.Client{ + Context: ctx, + URL: fmt.Sprintf("%s%d.json", pollURL, event), + Method: "GET", + } + data := new(PollData) + resp := c.Send().Scan(data) + if !resp.OK() { + return PollData{}, resp.Error() + } + return *data, nil +} + +func (b *Berkutschi) registerClient() error { + var body = struct { + Channel string `json:"channel"` + ID string `json:"id"` + SupportedConnectionTypes []string `json:"supportedConnectionTypes"` + Version string `json:"version"` + }{ + Channel: "/meta/handshake", + Version: "1.0", + ID: "1", + SupportedConnectionTypes: []string{"websocket"}, + } + var result []BerkutschiClientRegisterResponse + + c := request.Client{ + URL: "https://live.berkutschi.com/faye", + Method: "POST", + JSON: body, + } + resp := c.Send().Scan(&result) + if !resp.OK() { + // handle error + log.Error().Err(fmt.Errorf("%v", resp.Error())).Fields(struct{ Event int }{Event: b.event}).Send() + return resp.Error() + } + + b.log.Debug().Msgf("%+v", result) + b.clientID = result[0].ClientID + //TODO check length of result + return nil +} diff --git a/berkutschi/pollType.go b/berkutschi/pollType.go new file mode 100644 index 0000000..2f93588 --- /dev/null +++ b/berkutschi/pollType.go @@ -0,0 +1,246 @@ +package berkutschi + +type PollData struct { + Data struct { + AvailableStates []string `json:"available_states"` + Current struct { + Bib string `json:"bib"` + Club string `json:"club"` + Cumul struct { + Points float64 `json:"points"` + Rank int64 `json:"rank"` + } `json:"cumul"` + DateOfBirth string `json:"date_of_birth"` + Dnf bool `json:"dnf"` + DNS bool `json:"dns"` + Dq bool `json:"dq"` + Dqp bool `json:"dqp"` + Dtb1 interface{} `json:"dtb1"` + Dtb2 interface{} `json:"dtb2"` + Dtb3 interface{} `json:"dtb3"` + Firstname string `json:"firstname"` + Fiscode string `json:"fiscode"` + Gatecomp float64 `json:"gatecomp"` + Gatesnumber interface{} `json:"gatesnumber"` + Image string `json:"image"` + Image2 string `json:"image2"` + Image3 string `json:"image3"` + Judge struct { + One struct { + Discard bool `json:"discard"` + ID string `json:"id"` + Order int64 `json:"order"` + Rate float64 `json:"rate"` + } `json:"1"` + Two struct { + Discard bool `json:"discard"` + ID string `json:"id"` + Order int64 `json:"order"` + Rate float64 `json:"rate"` + } `json:"2"` + Three struct { + ID string `json:"id"` + Order int64 `json:"order"` + Rate float64 `json:"rate"` + } `json:"3"` + Four struct { + ID string `json:"id"` + Order int64 `json:"order"` + Rate float64 `json:"rate"` + } `json:"4"` + Five struct { + ID string `json:"id"` + Order int64 `json:"order"` + Rate float64 `json:"rate"` + } `json:"5"` + } `json:"judge"` + Judgetotal struct { + Points float64 `json:"points"` + Rank int64 `json:"rank"` + } `json:"judgetotal"` + Lastname string `json:"lastname"` + Length struct { + Length float64 `json:"length"` + Points float64 `json:"points"` + } `json:"length"` + Nat string `json:"nat"` + Nps bool `json:"nps"` + Points struct { + Points float64 `json:"points"` + Rank int64 `json:"rank"` + } `json:"points"` + Speed struct { + Speed string `json:"speed"` + } `json:"speed"` + Wind struct { + Compensation float64 `json:"compensation"` + Wind float64 `json:"wind"` + } `json:"wind"` + } `json:"current"` + Messages []struct { + Text string `json:"text"` + Timestamp string `json:"timestamp"` + } `json:"messages"` + Next struct { + Bib string `json:"bib"` + Club string `json:"club"` + DateOfBirth string `json:"date_of_birth"` + Firstname string `json:"firstname"` + Fiscode string `json:"fiscode"` + Image string `json:"image"` + Image2 string `json:"image2"` + Image3 string `json:"image3"` + Lastname string `json:"lastname"` + Nat string `json:"nat"` + } `json:"next"` + Raceinfo struct { + Discipline interface{} `json:"discipline"` + Event string `json:"event"` + Gender string `json:"gender"` + Judges struct { + One struct { + Nation string `json:"nation"` + } `json:"1"` + Two struct { + Nation string `json:"nation"` + } `json:"2"` + Three struct { + Nation string `json:"nation"` + } `json:"3"` + Four struct { + Nation string `json:"nation"` + } `json:"4"` + Five struct { + Nation string `json:"nation"` + } `json:"5"` + } `json:"judges"` + Mvalue string `json:"mvalue"` + No string `json:"no"` + Team string `json:"team"` + } `json:"raceinfo"` + Results []struct { + Bib int64 `json:"bib"` + DNS bool `json:"dns"` + Dq bool `json:"dq"` + FinalRank int64 `json:"final_rank"` + Gatecomp float64 `json:"gatecomp"` + Gatesnumber interface{} `json:"gatesnumber"` + Length1 float64 `json:"length1"` + LengthPoints1 float64 `json:"length_points1"` + Name string `json:"name"` + Nation string `json:"nation"` + Nps bool `json:"nps"` + Points1 float64 `json:"points1"` + Q bool `json:"q"` + Qualified string `json:"qualified"` + Speed string `json:"speed"` + Total float64 `json:"total"` + Wind struct { + Compensation float64 `json:"compensation"` + Wind float64 `json:"wind"` + } `json:"wind"` + } `json:"results"` + Startlist struct { + Jumpers []struct { + Bib string `json:"bib"` + Club string `json:"club"` + DateOfBirth string `json:"date_of_birth"` + Firstname string `json:"firstname"` + Fiscode string `json:"fiscode"` + Image string `json:"image"` + Image2 string `json:"image2"` + Image3 string `json:"image3"` + Lastname string `json:"lastname"` + Nat string `json:"nat"` + } `json:"jumpers"` + Runno string `json:"runno"` + } `json:"startlist"` + Status string `json:"status"` + Team bool `json:"team"` + } `json:"data"` + Event struct { + Canceled bool `json:"canceled"` + Cancelled bool `json:"cancelled"` + CompetitionActs []struct { + Date string `json:"date"` + Sort string `json:"sort"` + Time string `json:"time"` + } `json:"competition_acts"` + CreatedAt string `json:"created_at"` + Date string `json:"date"` + EndOfPeriod bool `json:"end_of_period"` + Fiscodex string `json:"fiscodex"` + Gender string `json:"gender"` + Hill struct { + BackwindFactor string `json:"backwind_factor"` + BuildingYear int64 `json:"building_year"` + Certificate string `json:"certificate"` + Contact string `json:"contact"` + CreatedAt interface{} `json:"created_at"` + Description string `json:"description"` + GateFactor string `json:"gate_factor"` + HeadwindFactor string `json:"headwind_factor"` + Height interface{} `json:"height"` + HillImages []struct { + HillThumb string `json:"hill_thumb"` + HillThumb150 string `json:"hill_thumb150"` + HillThumb200 string `json:"hill_thumb200"` + HillThumb480 string `json:"hill_thumb480"` + PhotoFileName string `json:"photo_file_name"` + } `json:"hill_images"` + HillJumpers []struct { + Distance string `json:"distance"` + HillID int64 `json:"hill_id"` + ID int64 `json:"id"` + Jumper struct { + Name string `json:"name"` + Nation struct { + FlagPath string `json:"flag_path"` + ID int64 `json:"id"` + Name string `json:"name"` + Shortname string `json:"shortname"` + Slug string `json:"slug"` + Visible bool `json:"visible"` + } `json:"nation"` + } `json:"jumper"` + JumperID int64 `json:"jumper_id"` + RecordDate string `json:"record_date"` + } `json:"hill_jumpers"` + HillRecord string `json:"hill_record"` + HillSize string `json:"hill_size"` + ID int64 `json:"id"` + InrunLength string `json:"inrun_length"` + Location struct { + Lat string `json:"lat"` + Lng string `json:"lng"` + Name string `json:"name"` + Nation struct { + FlagPath string `json:"flag_path"` + Shortname string `json:"shortname"` + } `json:"nation"` + } `json:"location"` + LocationID int64 `json:"location_id"` + Name string `json:"name"` + OutrunGradient string `json:"outrun_gradient"` + PointK string `json:"point_k"` + Size string `json:"size"` + Slug string `json:"slug"` + Speed string `json:"speed"` + StandCapacity int64 `json:"stand_capacity"` + TableGradient string `json:"table_gradient"` + TableHeight string `json:"table_height"` + TowerHeight interface{} `json:"tower_height"` + UpdatedAt string `json:"updated_at"` + Visible bool `json:"visible"` + Windfinder string `json:"windfinder"` + } `json:"hill"` + HillID int64 `json:"hill_id"` + ID int64 `json:"id"` + Remarks string `json:"remarks"` + Season string `json:"season"` + Sorts []string `json:"sorts"` + Start string `json:"start"` + Team int64 `json:"team"` + UpdatedAt string `json:"updated_at"` + } `json:"event"` +} diff --git a/berkutschi/types.go b/berkutschi/types.go new file mode 100644 index 0000000..99008e2 --- /dev/null +++ b/berkutschi/types.go @@ -0,0 +1,35 @@ +package berkutschi + +type BerkutschiClientRegisterResponse struct { + Advice struct { + Interval int64 `json:"interval"` + Reconnect string `json:"reconnect"` + Timeout int64 `json:"timeout"` + } `json:"advice"` + Channel string `json:"channel"` + ClientID string `json:"clientId"` + ID string `json:"id"` + Successful bool `json:"successful"` + SupportedConnectionTypes []string `json:"supportedConnectionTypes"` + Version string `json:"version"` +} + +type BerkutschiClientMessages interface { + Marshal() ([]byte, error) +} + +type BerkutschiConnectMessages []BerkutschiConnectMessage +type BerkutschiConnectMessage struct { + Channel string `json:"channel"` + ClientID string `json:"clientId"` + ConnectionType string `json:"connectionType"` + ID string `json:"id"` +} + +type BerkutschiSubscribeMessages []BerkutschiSubscribeMessage +type BerkutschiSubscribeMessage struct { + Channel string `json:"channel"` + ClientID string `json:"clientId"` + ID string `json:"id"` + Subscription string `json:"subscription"` +} diff --git a/berkutschi/websocket.go b/berkutschi/websocket.go new file mode 100644 index 0000000..74c8bd2 --- /dev/null +++ b/berkutschi/websocket.go @@ -0,0 +1,129 @@ +package berkutschi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "time" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "nhooyr.io/websocket" +) + +var u = url.URL{Scheme: "wss", Host: "live.berkutschi.com", Path: "/faye"} + +type Berkutschi struct { + conn *websocket.Conn + event int + clientID string + log zerolog.Logger + ctx context.Context + TX chan BerkutschiClientMessages + RX chan []byte +} + +func Init(event int) *Berkutschi { + l := log.With().Int("event", event).Logger() + b := &Berkutschi{ + event: event, + TX: make(chan BerkutschiClientMessages), + RX: make(chan []byte), + log: l, + } + b.registerClient() + b.connect() + b.connectAndSubscribe() + + return b +} + +func (b *Berkutschi) connectAndSubscribe() { + connectMessage := BerkutschiConnectMessage{ + Channel: "/meta/connect", + ClientID: b.clientID, + ConnectionType: "websocket", + ID: "2", + } + subscribeMessage := BerkutschiSubscribeMessage{ + Channel: "/meta/subscribe", + ClientID: b.clientID, + Subscription: fmt.Sprintf("/messages/%d", b.event), + ID: "3", + } + b.TX <- BerkutschiConnectMessages{connectMessage} + b.log.Debug().Msgf("connected to berkutschi") + b.TX <- BerkutschiSubscribeMessages{subscribeMessage} + b.log.Debug().Msgf("subscribed to event %d", b.event) +} + +func (b *Berkutschi) connect() error { + ctx, _ := context.WithTimeout(context.Background(), time.Second*10) + c, _, err := websocket.Dial(ctx, u.String(), nil) + if err != nil { + b.log.Err(fmt.Errorf("Error connecting to websocket: %s", err)).Send() + } + b.log.Debug().Msgf("Connected to websocket") + ctx, cancel := context.WithCancel(context.Background()) + go func() { + defer cancel() + for { + select { + case <-ctx.Done(): + return + default: + _, message, err := c.Read(ctx) + if err != nil { + b.log.Error().Err(fmt.Errorf("Error reading from websocket, reconnecting: %s", err)).Send() + return + } + b.RX <- message + } + } + }() + + go func() { + defer cancel() + for { + select { + case <-ctx.Done(): + return + case message := <-b.TX: + byteMessage, _ := message.Marshal() + err := c.Write(ctx, websocket.MessageText, byteMessage) + if err != nil { + b.log.Error().Err(fmt.Errorf("Error writing to websocket, reconnecting: %s", err)).Send() + return + } + b.log.Debug().Msgf("Sent message: %v", message) + } + } + }() + + go func() { + <-ctx.Done() + go b.closeAndReconnect() + }() + + b.conn = c + + return err +} + +func (b *Berkutschi) closeAndReconnect() error { + b.registerClient() + b.log.Debug().Msgf("Reconnecting websocket connection") + b.conn.Close(websocket.StatusInternalError, "") + b.connect() + b.connectAndSubscribe() + return nil +} + +func (m BerkutschiConnectMessages) Marshal() ([]byte, error) { + return json.Marshal(m) +} + +func (m BerkutschiSubscribeMessages) Marshal() ([]byte, error) { + return json.Marshal(m) +} diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..8978c53 --- /dev/null +++ b/default.nix @@ -0,0 +1,7 @@ +{ pkgs ? import { } }: +with pkgs; +mkShell { + nativeBuildInputs = [ + go_1_17 + ]; +} \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6908856 --- /dev/null +++ b/flake.lock @@ -0,0 +1,41 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1642700792, + "narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1643119265, + "narHash": "sha256-mmDEctIkHSWcC/HRpeaw6QOe+DbNOSzc0wsXAHOZWwo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b05d2077ebe219f6a47825767f8bab5c6211d200", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..fcadcca --- /dev/null +++ b/flake.nix @@ -0,0 +1,15 @@ +{ + description = "berkutschi"; + + inputs.flake-utils.url = "github:numtide/flake-utils"; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem + (system: + let pkgs = nixpkgs.legacyPackages.${system}; in + { + devShell = import ./default.nix { inherit pkgs; }; + defaultPackage = import ./package.nix { inherit pkgs;}; + } + ); +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a28e441 --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module berkutschi + +go 1.17 + +require ( + github.com/gorilla/websocket v1.4.2 + github.com/maurodelazeri/gorilla-reconnect v0.0.0-20180328170005-42501a5438b9 + github.com/mehdioa/nlog v0.0.0-20210327090009-d60bf476a16a + github.com/monaco-io/request v1.0.15 +) + +require ( + github.com/jpillora/backoff v1.0.0 // indirect + github.com/klauspost/compress v1.10.3 // indirect + github.com/rs/zerolog v1.26.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + nhooyr.io/websocket v1.8.7 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ee8f00e --- /dev/null +++ b/go.sum @@ -0,0 +1,98 @@ +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/maurodelazeri/gorilla-reconnect v0.0.0-20180328170005-42501a5438b9 h1:ZuBaZYBKi9zJFaqXyw0hN46nvBcU315To+SN+Qh0wjU= +github.com/maurodelazeri/gorilla-reconnect v0.0.0-20180328170005-42501a5438b9/go.mod h1:jawYJmNk6FVmenPRjYlbV+OcQ9fBGqv3dZwZnUSoR48= +github.com/mehdioa/nlog v0.0.0-20210327090009-d60bf476a16a h1:p/otnVjZSfec48nsELzyGDwoAMtD+xoG8a6c5TW8Fuw= +github.com/mehdioa/nlog v0.0.0-20210327090009-d60bf476a16a/go.mod h1:Co+vI+QcF2CG+gvNdG+kvzh2f9EE+YiF0OSdWoAmlOU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/monaco-io/request v1.0.15 h1:krhHE0yL6yL+xP0YwX5udyd+xnfQfHccop3Nsixec54= +github.com/monaco-io/request v1.0.15/go.mod h1:voq81GC2YDynQM3W/4D6oqGhqb+u7zULvbLzFkTmDfo= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc= +github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= diff --git a/main.go b/main.go new file mode 100644 index 0000000..c90948e --- /dev/null +++ b/main.go @@ -0,0 +1,165 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + + "berkutschi/berkutschi" +) + +const event = 3860 +const logFile = "my.log" + +var Judges = [5]JugdeRaceInfo{} + +func main() { + zerolog.TimeFieldFormat = time.RFC1123Z + zerolog.SetGlobalLevel(zerolog.InfoLevel) + f, err := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0664) + consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC1123Z} + multi := zerolog.MultiLevelWriter(consoleWriter, f) + log.Logger = zerolog.New(multi).With().Timestamp().Caller().Logger() + + pollData, err := berkutschi.Poll(event) + if err != nil { + log.Panic().Err(fmt.Errorf("Error while polling: %s", err)).Send() + } + fillJudgeraceinfo(pollData) + + b := berkutschi.Init(event) + + go func() { + jumperInfo := JumperInfo{} + jumperscore := JumperScore{} + for { + message := <-b.RX + var jumpMessage BerkutschiJumpUpdateMessages + err := json.Unmarshal(message, &jumpMessage) + //log.Debug().Msgf("Received message: %v", string(message)) + if err == nil && jumpMessage[0].Data.Current.Lastname != "" { + jumper, err := fillJumperInfo(jumpMessage) + if err != nil { + continue + } + if newJumperInfoData(jumperInfo, jumper) { + jumperInfo = jumper + log.Info().Msgf("New jumper info: %+v", jumperInfo) + } + score, _ := fillJumperScore(jumpMessage) + if score.Rank != 0 && newJumperScoreData(jumperscore, score) { + jumperscore = score + log.Info().Msgf("New jumper score: %+v", jumperscore) + } + } else if err != nil { + log.Error().RawJSON("receivedMessage", message).Err(fmt.Errorf("%v", err)) + } else { + if jumpMessage[0].Data.Next.Lastname == "" { + log.Debug().RawJSON("receivedMessage", message).Msg("Received message i cant decode") + } + } + } + }() + + ctx, _ := context.WithCancel(context.Background()) + <-ctx.Done() +} + +func fillJumperInfo(m BerkutschiJumpUpdateMessages) (JumperInfo, error) { + var err error + JumperInfo := JumperInfo{} + if m[0].Data.Current.Firstname == "" || m[0].Data.Current.Lastname == "" { + err = fmt.Errorf("Name is empty") + return JumperInfo, err + } + JumperInfo.Name = fmt.Sprint(m[0].Data.Current.Firstname, " ", strings.ToUpper(m[0].Data.Current.Lastname)) + JumperInfo.Bib = m[0].Data.Current.Bib + JumperInfo.Nation = strings.ToUpper(m[0].Data.Current.Nat) + JumperInfo.Image = m[0].Data.Current.Image2 + + return JumperInfo, err +} + +func fillJumperScore(m BerkutschiJumpUpdateMessages) (JumperScore, error) { + var err error + JumperScore := JumperScore{} + if m[0].Data.Current.Firstname == "" || m[0].Data.Current.Lastname == "" { + err = fmt.Errorf("Name is empty") + return JumperScore, err + } + JumperScore.Name = fmt.Sprint(m[0].Data.Current.Firstname, " ", strings.ToUpper(m[0].Data.Current.Lastname)) + JumperScore.Bib = m[0].Data.Current.Bib + JumperScore.Nation = strings.ToUpper(m[0].Data.Current.Nat) + JumperScore.Points = m[0].Data.Current.Cumul.Points + JumperScore.Rank = m[0].Data.Current.Cumul.Rank + JumperScore.Wind = m[0].Data.Current.Wind.Wind + JumperScore.Length = fmt.Sprintf("%vm", m[0].Data.Current.Length.Length) + for i, _ := range JumperScore.Judges { + switch i { + case 0: + JumperScore.Judges[i].Score = m[0].Data.Current.Judge.One.Rate + JumperScore.Judges[i].Nation = Judges[i].Nation + JumperScore.Judges[i].Discard = m[0].Data.Current.Judge.One.Discard + case 1: + JumperScore.Judges[i].Score = m[0].Data.Current.Judge.Two.Rate + JumperScore.Judges[i].Nation = Judges[i].Nation + JumperScore.Judges[i].Discard = m[0].Data.Current.Judge.Two.Discard + case 2: + JumperScore.Judges[i].Score = m[0].Data.Current.Judge.Three.Rate + JumperScore.Judges[i].Nation = Judges[i].Nation + JumperScore.Judges[i].Discard = m[0].Data.Current.Judge.Three.Discard + case 3: + JumperScore.Judges[i].Score = m[0].Data.Current.Judge.Four.Rate + JumperScore.Judges[i].Nation = Judges[i].Nation + JumperScore.Judges[i].Discard = m[0].Data.Current.Judge.Four.Discard + case 4: + JumperScore.Judges[i].Score = m[0].Data.Current.Judge.Five.Rate + JumperScore.Judges[i].Nation = Judges[i].Nation + JumperScore.Judges[i].Discard = m[0].Data.Current.Judge.Five.Discard + } + } + + return JumperScore, err +} + +func fillJudgeraceinfo(data berkutschi.PollData) { + for i, _ := range Judges { + switch i { + case 0: + Judges[i].Nation = strings.ToUpper(data.Data.Raceinfo.Judges.One.Nation) + case 1: + Judges[i].Nation = strings.ToUpper(data.Data.Raceinfo.Judges.Two.Nation) + case 2: + Judges[i].Nation = strings.ToUpper(data.Data.Raceinfo.Judges.Three.Nation) + case 3: + Judges[i].Nation = strings.ToUpper(data.Data.Raceinfo.Judges.Four.Nation) + case 4: + Judges[i].Nation = strings.ToUpper(data.Data.Raceinfo.Judges.Five.Nation) + } + } +} + +func newJumperInfoData(oldData, newData JumperInfo) bool { + if oldData.Name != newData.Name || oldData.Bib != newData.Bib || oldData.Nation != newData.Nation || oldData.Image != newData.Image { + return true + } + return false +} +func newJumperScoreData(oldData, newData JumperScore) bool { + if oldData.Name != newData.Name || oldData.Bib != newData.Bib || oldData.Nation != newData.Nation || + oldData.Points != newData.Points || oldData.Rank != newData.Rank || oldData.Wind != newData.Wind || oldData.Length != newData.Length { + return true + } + for i, oldJudge := range oldData.Judges { + if oldJudge.Score != newData.Judges[i].Score || oldJudge.Nation != newData.Judges[i].Nation || oldJudge.Discard != newData.Judges[i].Discard { + return true + } + } + return false +} diff --git a/package.nix b/package.nix new file mode 100644 index 0000000..fcb7204 --- /dev/null +++ b/package.nix @@ -0,0 +1,30 @@ +{ pkgs ? import { } }: +with pkgs; +let + version = "0.0.1"; + deps = []; + nativeDeps = [ + ]; +in +pkgs.buildGo117Module { + pname = "berkutschi"; + inherit version; + + src = ./.; + + buildInputs = [ + ] ++deps; + nativeBuildInputs = [ + ] ++nativeDeps; + + tags = [ ]; + + allowGoReference = true; + + vendorSha256 = "sha256-qXi32q5PjnvYj6LUUTT9vF3VICj6mFnMUdVA1FJMSD0="; + + meta = { + description = "A Client for the Berkutschi WS Api"; + homepage = "https://git.entr0py.de/garionion/berkutschi"; + }; +} \ No newline at end of file diff --git a/types.go b/types.go new file mode 100644 index 0000000..3624084 --- /dev/null +++ b/types.go @@ -0,0 +1,125 @@ +package main + +type JugdeRaceInfo struct { + Nation string +} + +type JumperInfo struct { + Name string + Bib string + Nation string + Image string +} + +type JumperScore struct { + Name string + Bib string + Nation string + Points float64 + Rank int + Wind float64 + Length string + Judges [5]Judge +} + +type Judge struct { + Nation string + Score float64 + Discard bool +} + +type BerkutschiJumpUpdateMessages []BerkutschiJumpUpdateMessage +type BerkutschiJumpUpdateMessage struct { + Channel string `json:"channel"` + Data struct { + Current struct { + Bib string `json:"bib"` + Club string `json:"club"` + Cumul struct { + Points float64 `json:"points"` + Rank int `json:"rank"` + } `json:"cumul"` + DateOfBirth string `json:"date_of_birth"` + Dnf bool `json:"dnf"` + DNS bool `json:"dns"` + Dq bool `json:"dq"` + Dqp bool `json:"dqp"` + Dtb1 interface{} `json:"dtb1"` + Dtb2 interface{} `json:"dtb2"` + Dtb3 interface{} `json:"dtb3"` + Firstname string `json:"firstname"` + Fiscode string `json:"fiscode"` + Gatecomp float64 `json:"gatecomp"` + Gatesnumber interface{} `json:"gatesnumber"` + Image string `json:"image"` + Image2 string `json:"image2"` + Image3 string `json:"image3"` + Judge struct { + One struct { + Discard bool `json:"discard"` + ID string `json:"id"` + Order int64 `json:"order"` + Rate float64 `json:"rate"` + } `json:"1"` + Two struct { + Discard bool `json:"discard"` + ID string `json:"id"` + Order int64 `json:"order"` + Rate float64 `json:"rate"` + } `json:"2"` + Three struct { + Discard bool `json:"discard"` + ID string `json:"id"` + Order int64 `json:"order"` + Rate float64 `json:"rate"` + } `json:"3"` + Four struct { + Discard bool `json:"discard"` + ID string `json:"id"` + Order int64 `json:"order"` + Rate float64 `json:"rate"` + } `json:"4"` + Five struct { + Discard bool `json:"discard"` + ID string `json:"id"` + Order int64 `json:"order"` + Rate float64 `json:"rate"` + } `json:"5"` + } `json:"judge"` + Judgetotal struct { + Points float64 `json:"points"` + Rank int64 `json:"rank"` + } `json:"judgetotal"` + Lastname string `json:"lastname"` + Length struct { + Length float64 `json:"length"` + Points float64 `json:"points"` + } `json:"length"` + Nat string `json:"nat"` + Nps bool `json:"nps"` + Points struct { + Points float64 `json:"points"` + Rank int `json:"rank"` + } `json:"points"` + Speed struct { + Speed string `json:"speed"` + } `json:"speed"` + Wind struct { + Compensation float64 `json:"compensation"` + Wind float64 `json:"wind"` + } `json:"wind"` + } `json:"current"` + Next struct { + Bib string `json:"bib"` + Club string `json:"club"` + DateOfBirth string `json:"date_of_birth"` + Firstname string `json:"firstname"` + Fiscode string `json:"fiscode"` + Image string `json:"image"` + Image2 string `json:"image2"` + Image3 string `json:"image3"` + Lastname string `json:"lastname"` + Nat string `json:"nat"` + } `json:"next"` + } `json:"data"` +}