commit 055b2a4227d16de49751e27660ba4b172d334987 Author: Carl Kittelberger Date: Mon Jun 13 00:39:00 2016 +0200 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..daf913b --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7ff98cd --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: go +go: + - 1.3.3 + - 1.4.3 + - 1.5.4 + - 1.6.2 + - release + - tip + +install: + - export GOPATH="$HOME/gopath" + - mkdir -p "$GOPATH/src/github.com/icedream" + - mv "$TRAVIS_BUILD_DIR" "$GOPATH/src/github.com/icedream" + - go get -v -t -d github.com/icedream/go-footballdata/... + +script: + - go test -v github.com/icedream/go-footballdata/... \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba5cb89 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Football-Data API for Golang + +[![Build Status](https://travis-ci.org/icedream/go-footballdata.svg?branch=master)](https://travis-ci.org/icedream/go-footballdata) +[![GoDoc](https://godoc.org/github.com/icedream/go-footballdata?status.svg)](https://godoc.org/github.com/icedream/go-footballdata) + +This library provides functionality to communicate with the API provided by http://football-api.org. This way programs can use data provided by the API in order to show information about football/soccer games from various seasons for free. + +## How to use this library? + +Before you use this library please [register for a free API key](http://api.football-data.org/register) in order to increase your usage limits. The library also works without an API key. + +You can install this library by running: + + go get github.com/icedream/go-footballdata + +Afterwards you can use this library like this: + +```go +package main + +import ( + "fmt" + "net/http" + + "github.com/icedream/go-footballdata" +) + +func main() { + // Create client + client := footballdata.NewClient(http.DefaultClient) + + // Tell it to use our API token + client.AuthToken = "" + + // Get list of seasons... + seasons, err := client.SoccerSeasons().Do() + if err != nil { + panic(err) + } + + // ...and print them + for _, season := range seasons { + fmt.Println(season.Id, season.Caption) + } +} + +``` \ No newline at end of file diff --git a/api_methods.go b/api_methods.go new file mode 100644 index 0000000..f10ab2a --- /dev/null +++ b/api_methods.go @@ -0,0 +1,16 @@ +package footballdata + +import ( + "encoding/json" + "net/url" +) + +type request struct { + c *Client + p string + v url.Values +} + +func (r request) doJson(method string) (*json.Decoder, ResponseMeta, error) { + return r.c.doJson(method, r.p, r.v) +} diff --git a/api_types.go b/api_types.go new file mode 100644 index 0000000..470559a --- /dev/null +++ b/api_types.go @@ -0,0 +1,124 @@ +package footballdata + +import "time" + +type FixtureStatus string + +const ( + FixtureStatus_Timed FixtureStatus = "TIMED" + FixtureStatus_InPlay FixtureStatus = "IN_PLAY" + FixtureStatus_Finished FixtureStatus = "FINISHED" +) + +// Describes the venue. +type Venue string + +const ( + // The home venue. + Venue_Home Venue = "home" + // The away venue. + Venue_Away Venue = "away" +) + +// Contains the list of soccer seasons returned by the API. +type SoccerSeasonList []SoccerSeason + +// Contains information about a soccer season. +type SoccerSeason struct { + Id uint64 + Caption string + League string + Year string + CurrentMatchday uint16 + NumberOfMatchdays uint16 + NumberOfTeams uint16 + NumberOfGames uint16 + LastUpdates time.Time +} + +// Contains the fixture and the head to head information delivered by the API +// for a wanted fixture. +type FixtureResponse struct { + Fixture Fixture + Head2Head Head2Head +} + +// Contains head to head information. +type Head2Head struct { + Count uint64 + TimeFrameStart time.Time + TimeFrameEnd time.Time + HomeTeamWins uint64 + AwayTeamWins uint64 + Draws uint64 + LastHomeWinHomeTeam *Fixture + LastWinHomeTeam *Fixture + LastAwayWinAwayTeam *Fixture + Fixtures []Fixture +} + +// Contains information about a fixture. +type Fixture struct { + Date time.Time + Status FixtureStatus + Matchday uint16 + HomeTeamName string + AwayTeamName string + Result FixtureResult + + //HomeTeamId uint64 + //AwayTeamId uint64 +} + +// Contains a list of fixtures. +type FixtureList struct { + Count uint64 + Fixtures []Fixture +} + +// Contains information about a list of fixtures as returned by the API. +type FixturesResponse struct { + FixtureList + + TimeFrameStart time.Time + TimeFrameEnd time.Time +} + +// Contains information about a fixture's results. +type FixtureResult struct { + GoalsHomeTeam uint16 + GoalsAwayTeam uint16 +} + +// Contains a list of teams. +type TeamList struct { + Count uint64 + Teams []Team +} + +// Contains information about a team. +type Team struct { + Id uint64 + Name string + ShortName string + SquadMarketValue string + CrestUrl string +} + +// Contains a list of players. +type PlayerList struct { + Count uint64 + Players []Player +} + +// Contains information about a player. +type Player struct { + Id uint64 + Name string + Position string + JerseyNumber uint8 + DateOfBirth time.Time + Nationality string + ContractUntil time.Time + MarketValue string +} diff --git a/api_url.go b/api_url.go new file mode 100644 index 0000000..f2f3bbc --- /dev/null +++ b/api_url.go @@ -0,0 +1,22 @@ +package footballdata + +import "net/url" + +const ( + baseUrl = "http://api.football-data.org/v1/" +) + +func resolveRelativeUrl(path string, values url.Values) *url.URL { + if values == nil { + values = url.Values{} + } + + u, err := url.Parse(baseUrl) + if err != nil { + panic(err) + } + + ru := &url.URL{Path: path, RawQuery: values.Encode()} + + return u.ResolveReference(ru) +} diff --git a/client.go b/client.go new file mode 100644 index 0000000..3db2c2f --- /dev/null +++ b/client.go @@ -0,0 +1,65 @@ +package footballdata + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +/* +Provides a high-level client to talk to the API that football-data.org offers. + +To create an instance please use NewClient(h). +*/ +type Client struct { + h *http.Client + + // Insert an API token here if you have one. It will be sent across with all requests. + AuthToken string +} + +// Creates a new Client instance that wraps around the given HTTP client. +func NewClient(h *http.Client) *Client { + return &Client{h: h} +} + +func (c *Client) req(path string, pathValues ...interface{}) request { + return request{c, fmt.Sprintf(path, pathValues...), url.Values{}} +} + +// Executes an HTTP request with given parameters and on success returns the response wrapped in a JSON decoder. +func (c *Client) doJson(method string, path string, values url.Values) (j *json.Decoder, meta ResponseMeta, err error) { + // Create request + req := &http.Request{ + Method: method, + URL: resolveRelativeUrl(path, values), + } + + // Set request headers + if len(c.AuthToken) > 0 { + req.Header.Set("X-Auth-Token", c.AuthToken) + } + req.Header.Set("X-Response-Control", "minified") + req.Header.Set("User-Agent", "go-footballdata/0.0") + + // Execute request + resp, err := c.h.Do(req) + if err != nil { + return + } + + // Save metadata from headers + meta = responseMetaFromHeaders(resp.Header) + + // Download to buffer to allow passing back a fully prepared decoder + defer resp.Body.Close() + buf := new(bytes.Buffer) + io.Copy(buf, resp.Body) + + // Wrap JSON decoder around buffered data + j = json.NewDecoder(buf) + return +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..ff662a6 --- /dev/null +++ b/example_test.go @@ -0,0 +1,27 @@ +package footballdata_test + +import ( + "fmt" + "net/http" + + "github.com/icedream/go-footballdata" +) + +func Example() { + // Create client + client := footballdata.NewClient(http.DefaultClient) + + // Tell it to use our API token + client.AuthToken = "" + + // Get list of seasons... + seasons, err := client.SoccerSeasons().Do() + if err != nil { + panic(err) + } + + // ...and print them + for _, season := range seasons { + fmt.Println(season.Id, season.Caption) + } +} diff --git a/req_fixture.go b/req_fixture.go new file mode 100644 index 0000000..f3211f9 --- /dev/null +++ b/req_fixture.go @@ -0,0 +1,27 @@ +package footballdata + +import "fmt" + +type FixtureRequest struct{ request } + +// Modifies the request to specify the number of former games to be analyzed (normally 10). +func (r FixtureRequest) Head2Head(num uint16) FixtureRequest { + r.v.Set("head2head", fmt.Sprintf("%i", num)) + return r +} + +// Executes the request. +func (r FixtureRequest) Do() (s Fixture, err error) { + d, _, err := r.doJson("GET") + if err != nil { + return + } + + err = d.Decode(&s) + return +} + +// Prepares a request to fetch the fixtures of a soccer season. +func (c *Client) Fixture(id uint64) FixtureRequest { + return FixtureRequest{c.req("fixture/%i", id)} +} diff --git a/req_fixtures.go b/req_fixtures.go new file mode 100644 index 0000000..8ba685a --- /dev/null +++ b/req_fixtures.go @@ -0,0 +1,36 @@ +package footballdata + +import ( + "strings" + "time" +) + +type FixturesRequest struct{ request } + +// Modifies the request to specify a specific time frame. +func (r FixturesRequest) TimeFrame(timeframe time.Duration) FixturesRequest { + r.v.Set("timeFrame", durationToTimeFrame(timeframe)) + return r +} + +// Modifies the request to specify a list of leagues by their code. +func (r FixturesRequest) League(leagueCodes ...string) FixturesRequest { + r.v.Set("league", strings.Join(leagueCodes, ",")) + return r +} + +// Executes the request. +func (r FixturesRequest) Do() (s FixturesResponse, err error) { + d, _, err := r.doJson("GET") + if err != nil { + return + } + + err = d.Decode(&s) + return +} + +// Prepares a request to fetch the fixtures of a soccer season. +func (c *Client) Fixtures() FixturesRequest { + return FixturesRequest{c.req("fixtures")} +} diff --git a/req_soccerseason.go b/req_soccerseason.go new file mode 100644 index 0000000..82ab96d --- /dev/null +++ b/req_soccerseason.go @@ -0,0 +1,19 @@ +package footballdata + +type SoccerSeasonRequest struct{ request } + +// Executes the request. +func (r SoccerSeasonRequest) Do() (s SoccerSeason, err error) { + d, _, err := r.doJson("GET") + if err != nil { + return + } + + err = d.Decode(&s) + return +} + +// Prepares a request to fetch the complete list of soccer seasons. +func (c *Client) SoccerSeason(id uint64) SoccerSeasonRequest { + return SoccerSeasonRequest{c.req("soccerseasons/%i", id)} +} diff --git a/req_soccerseason_fixtures.go b/req_soccerseason_fixtures.go new file mode 100644 index 0000000..6470b1b --- /dev/null +++ b/req_soccerseason_fixtures.go @@ -0,0 +1,36 @@ +package footballdata + +import ( + "fmt" + "time" +) + +type SoccerSeasonFixturesRequest struct{ request } + +// Modifies the request to specify a match day. +func (r SoccerSeasonFixturesRequest) Matchday(matchday uint16) SoccerSeasonFixturesRequest { + r.v.Set("matchday", fmt.Sprintf("%i", matchday)) + return r +} + +// Modifies the request to specify a specific time frame. +func (r SoccerSeasonFixturesRequest) TimeFrame(timeframe time.Duration) SoccerSeasonFixturesRequest { + r.v.Set("timeFrame", durationToTimeFrame(timeframe)) + return r +} + +// Executes the request. +func (r SoccerSeasonFixturesRequest) Do() (s SoccerSeason, err error) { + d, _, err := r.doJson("GET") + if err != nil { + return + } + + err = d.Decode(&s) + return +} + +// Prepares a request to fetch the fixtures of a soccer season. +func (c *Client) FixturesOfSoccerSeason(soccerSeasonId uint64) SoccerSeasonFixturesRequest { + return SoccerSeasonFixturesRequest{c.req("soccerseasons/%i/fixtures", soccerSeasonId)} +} diff --git a/req_soccerseason_leaguetable.go b/req_soccerseason_leaguetable.go new file mode 100644 index 0000000..6bd6411 --- /dev/null +++ b/req_soccerseason_leaguetable.go @@ -0,0 +1,27 @@ +package footballdata + +import "fmt" + +type SoccerSeasonLeagueTableRequest struct{ request } + +// Modifies the request to specify a match day. +func (r SoccerSeasonLeagueTableRequest) Matchday(matchday uint16) SoccerSeasonLeagueTableRequest { + r.v.Set("matchday", fmt.Sprintf("%i", matchday)) + return r +} + +// Executes the request. +func (r SoccerSeasonLeagueTableRequest) Do() (s SoccerSeason, err error) { + d, _, err := r.doJson("GET") + if err != nil { + return + } + + err = d.Decode(&s) + return +} + +// Prepares a new request to fetch the league table of a given soccer season. +func (c *Client) LeagueTableOfSoccerSeason(soccerSeasonId uint64) SoccerSeasonLeagueTableRequest { + return SoccerSeasonLeagueTableRequest{c.req("soccerseasons/%i/leagueTable", soccerSeasonId)} +} diff --git a/req_soccerseason_teams.go b/req_soccerseason_teams.go new file mode 100644 index 0000000..f208750 --- /dev/null +++ b/req_soccerseason_teams.go @@ -0,0 +1,19 @@ +package footballdata + +type SoccerSeasonTeamsRequest struct{ request } + +// Executes the request. +func (r SoccerSeasonTeamsRequest) Do() (s TeamList, err error) { + d, _, err := r.doJson("GET") + if err != nil { + return + } + + err = d.Decode(&s) + return +} + +// Prepares a new request to fetch the league table of a given soccer season. +func (c *Client) TeamsOfSoccerSeason(soccerSeasonId uint64) SoccerSeasonTeamsRequest { + return SoccerSeasonTeamsRequest{c.req("soccerseasons/%i/leagueTable", soccerSeasonId)} +} diff --git a/req_soccerseasons.go b/req_soccerseasons.go new file mode 100644 index 0000000..f641c9f --- /dev/null +++ b/req_soccerseasons.go @@ -0,0 +1,27 @@ +package footballdata + +import "fmt" + +type SoccerSeasonsRequest struct{ request } + +// Modifies the request to specify a season. +func (r SoccerSeasonsRequest) Season(num uint32) SoccerSeasonsRequest { + r.v.Set("season", fmt.Sprintf("%i", num)) + return r +} + +// Executes the request. +func (r SoccerSeasonsRequest) Do() (s SoccerSeasonList, err error) { + d, _, err := r.doJson("GET") + if err != nil { + return + } + + err = d.Decode(&s) + return +} + +// Prepares a request to fetch the complete list of soccer seasons. +func (c *Client) SoccerSeasons() SoccerSeasonsRequest { + return SoccerSeasonsRequest{c.req("soccerseasons")} +} diff --git a/req_team.go b/req_team.go new file mode 100644 index 0000000..ef6a2c9 --- /dev/null +++ b/req_team.go @@ -0,0 +1,26 @@ +package footballdata + +type TeamRequest struct { + request + id uint64 +} + +// Executes the request. +func (r TeamRequest) Do() (s Team, err error) { + d, _, err := r.doJson("GET") + if err != nil { + return + } + + err = d.Decode(&s) + if err != nil { + // Fill up data not returned by the server + s.Id = r.id + } + return +} + +// Prepares a request to fetch a team's information. +func (c *Client) Team(id uint64) TeamRequest { + return TeamRequest{c.req("teams/%i", id), id} +} diff --git a/req_team_fixtures.go b/req_team_fixtures.go new file mode 100644 index 0000000..dea8732 --- /dev/null +++ b/req_team_fixtures.go @@ -0,0 +1,42 @@ +package footballdata + +import ( + "fmt" + "time" +) + +type TeamFixturesRequest struct{ request } + +// Modifies the request to specify a specific time frame. +func (r TeamFixturesRequest) TimeFrame(timeframe time.Duration) TeamFixturesRequest { + r.v.Set("timeFrame", durationToTimeFrame(timeframe)) + return r +} + +// Modifies the request to specify a list of leagues by their code. +func (r TeamFixturesRequest) Season(season uint64) TeamFixturesRequest { + r.v.Set("season", fmt.Sprintf("%i", season)) + return r +} + +// Modifies the request to specify a venue. +func (r TeamFixturesRequest) Venue(venue Venue) TeamFixturesRequest { + r.v.Set("venue", string(venue)) + return r +} + +// Executes the request. +func (r TeamFixturesRequest) Do() (s FixturesResponse, err error) { + d, _, err := r.doJson("GET") + if err != nil { + return + } + + err = d.Decode(&s) + return +} + +// Prepares a request to fetch the fixtures of a soccer season. +func (c *Client) FixturesOfTeam(id uint64) TeamFixturesRequest { + return TeamFixturesRequest{c.req("teams/%i/fixtures", id)} +} diff --git a/req_team_players.go b/req_team_players.go new file mode 100644 index 0000000..656e525 --- /dev/null +++ b/req_team_players.go @@ -0,0 +1,19 @@ +package footballdata + +type TeamPlayersRequest struct{ request } + +// Executes the request. +func (r TeamPlayersRequest) Do() (s PlayerList, err error) { + d, _, err := r.doJson("GET") + if err != nil { + return + } + + err = d.Decode(&s) + return +} + +// Prepares a request to fetch a team's players. +func (c *Client) PlayersOfTeam(id uint64) TeamPlayersRequest { + return TeamPlayersRequest{c.req("teams/%i/players", id)} +} diff --git a/response_meta.go b/response_meta.go new file mode 100644 index 0000000..0dcb062 --- /dev/null +++ b/response_meta.go @@ -0,0 +1,38 @@ +package footballdata + +import ( + "net/http" + "strconv" +) + +// Contains additional information returned by the Football-Data API in the HTTP headers. +// This includes the currently authenticated user and information about the rate limitation. +type ResponseMeta struct { + // Indicates the recognized user or returns "anonymous" if not authenticated. + AuthenticatedClient string + + // Defines the seconds left to reset your request counter. + RequestCounterReset uint64 + + // Indicates the requests left. + RequestsAvailable uint64 +} + +func responseMetaFromHeaders(h http.Header) (r ResponseMeta) { + if v := h.Get("X-Authenticated-Client"); v != "" { + r.AuthenticatedClient = v + } + if v := h.Get("X-RequestCounter-Reset"); v != "" { + i, err := strconv.ParseUint(v, 10, 64) + if err != nil { + r.RequestCounterReset = i + } + } + if v := h.Get("X-Requests-Available"); v != "" { + i, err := strconv.ParseUint(v, 10, 64) + if err != nil { + r.RequestsAvailable = i + } + } + return +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..4b42cae --- /dev/null +++ b/util.go @@ -0,0 +1,22 @@ +package footballdata + +import ( + "fmt" + "time" +) + +const ( + day = 24 * time.Hour + week = 7 * day +) + +func durationToTimeFrame(d time.Duration) (r string) { + if d < 0 { + r = "p" + d = -d + } else if d > 0 { + r = "n" + } + r += fmt.Sprint(d.Hours() / 24) + return +} diff --git a/util_test.go b/util_test.go new file mode 100644 index 0000000..5e6aa02 --- /dev/null +++ b/util_test.go @@ -0,0 +1,24 @@ +package footballdata + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func Test_durationToTimeFrame_Zero(t *testing.T) { + assert.Equal(t, "0", durationToTimeFrame(0)) +} + +func Test_durationToTimeFrame_Negative7Days(t *testing.T) { + assert.Equal(t, "p7", durationToTimeFrame(-7*day)) +} + +func Test_durationToTimeFrame_Positive7Days(t *testing.T) { + assert.Equal(t, "n7", durationToTimeFrame(7*day)) +} + +func Test_durationToTimeFrame_PositiveHalfDay(t *testing.T) { + assert.Equal(t, "n0.5", durationToTimeFrame(12*time.Hour)) +}