From e7e1463181f51a63c3bd1d1d7e2e2dfffa0b56d2 Mon Sep 17 00:00:00 2001 From: Cizer Pereira Date: Sat, 19 Oct 2019 16:11:53 +0200 Subject: [PATCH] feat: Add new widget for football scores and standings --- app/widget_maker.go | 4 + go.mod | 7 ++ go.sum | 4 +- modules/football/client.go | 45 +++++++++ modules/football/settings.go | 37 +++++++ modules/football/types.go | 46 +++++++++ modules/football/util.go | 37 +++++++ modules/football/widget.go | 181 +++++++++++++++++++++++++++++++++++ 8 files changed, 359 insertions(+), 2 deletions(-) create mode 100644 modules/football/client.go create mode 100644 modules/football/settings.go create mode 100644 modules/football/types.go create mode 100644 modules/football/util.go create mode 100644 modules/football/widget.go diff --git a/app/widget_maker.go b/app/widget_maker.go index cab1821a..ac07076c 100644 --- a/app/widget_maker.go +++ b/app/widget_maker.go @@ -18,6 +18,7 @@ import ( "github.com/wtfutil/wtf/modules/digitalclock" "github.com/wtfutil/wtf/modules/docker" "github.com/wtfutil/wtf/modules/feedreader" + "github.com/wtfutil/wtf/modules/football" "github.com/wtfutil/wtf/modules/gcal" "github.com/wtfutil/wtf/modules/gerrit" "github.com/wtfutil/wtf/modules/git" @@ -128,6 +129,9 @@ func MakeWidget( case "feedreader": settings := feedreader.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = feedreader.NewWidget(app, pages, settings) + case "football": + settings := football.NewSettingsFromYAML(moduleName, moduleConfig, config) + widget = football.NewWidget(app, pages, settings) case "gcal": settings := gcal.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = gcal.NewWidget(app, settings) diff --git a/go.mod b/go.mod index eb187024..a664dbd7 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,9 @@ require ( github.com/mmcdole/gofeed v1.0.0-beta2 github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect github.com/olebedev/config v0.0.0-20190528211619-364964f3a8e4 + github.com/olekukonko/tablewriter v0.0.1 + github.com/onsi/ginkgo v1.10.2 // indirect + github.com/onsi/gomega v1.7.0 // indirect github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/pkg/errors v0.8.1 github.com/pkg/profile v1.3.0 @@ -42,7 +45,11 @@ require ( github.com/shirou/gopsutil v2.19.9+incompatible github.com/sticreations/spotigopher v0.0.0-20181009182052-98632f6f94b0 github.com/stretchr/testify v1.4.0 +<<<<<<< HEAD github.com/wtfutil/todoist v0.0.1 +======= + github.com/wtfutil/todoist v0.0.0-20190913231042-97395e581a76 +>>>>>>> 59b8877b... feat: Add new widget for football scores and standings github.com/xanzy/go-gitlab v0.20.1 github.com/yfronto/newrelic v0.0.0-00010101000000-000000000000 github.com/zmb3/spotify v0.0.0-20191010212056-e12fb981aacb diff --git a/go.sum b/go.sum index 60032bf6..e248b6d0 100644 --- a/go.sum +++ b/go.sum @@ -192,6 +192,8 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+ github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/olebedev/config v0.0.0-20190528211619-364964f3a8e4 h1:JnVsYEQzhEcOspy6ngIYNF2u0h2mjkXZptzX0IzZQ4g= github.com/olebedev/config v0.0.0-20190528211619-364964f3a8e4/go.mod h1:RL5+WRxWTAXqqCi9i+eZlHrUtO7AQujUqWi+xMohmc4= +github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= @@ -225,7 +227,6 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/sticreations/spotigopher v0.0.0-20181009182052-98632f6f94b0 h1:WWyBZL7bRdl7Do39EvkJmBFXT11uXLACy0cJHHOZ7IE= github.com/sticreations/spotigopher v0.0.0-20181009182052-98632f6f94b0/go.mod h1:DjuRbAVIoxD4Lv7aE12Km2XYYYKrtXXNbpivYwXv2HE= @@ -334,7 +335,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o= gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/jarcoal/httpmock.v1 v1.0.0-20181110093347-3be5f16b70eb h1:ggw12VRqlkVtHkyK+zh3QP+V6PIGAuKQG/u0Mnkn6TQ= gopkg.in/jarcoal/httpmock.v1 v1.0.0-20181110093347-3be5f16b70eb/go.mod h1:d3R+NllX3X5e0zlG1Rful3uLvsGC/Q3OHut5464DEQw= diff --git a/modules/football/client.go b/modules/football/client.go new file mode 100644 index 00000000..aaf48548 --- /dev/null +++ b/modules/football/client.go @@ -0,0 +1,45 @@ +package football + +import ( + "fmt" + "net/http" +) + +var ( + footballAPIUrl = "http://api.football-data.org/v2" +) + +type leagueInfo struct { + id int + caption string +} + +type Client struct { + apiKey string +} + +func NewClient(apiKey string) *Client { + client := Client{ + apiKey: apiKey, + } + + return &client +} + +func (client *Client) footballRequest(path string, id int) (*http.Response, error) { + + url := fmt.Sprintf("%s/competitions/%d/%s", footballAPIUrl, id, path) + req, err := http.NewRequest("GET", url, nil) + req.Header.Add("Accept", "application/json") + req.Header.Add("Content-Type", "application/json") + req.Header.Add("X-Auth-Token", client.apiKey) + if err != nil { + return nil, err + } + httpClient := &http.Client{} + resp, err := httpClient.Do(req) + if err != nil { + return nil, err + } + return resp, nil +} diff --git a/modules/football/settings.go b/modules/football/settings.go new file mode 100644 index 00000000..dda75759 --- /dev/null +++ b/modules/football/settings.go @@ -0,0 +1,37 @@ +package football + +import ( + "os" + + "github.com/olebedev/config" + "github.com/wtfutil/wtf/cfg" +) + +const ( + defaultFocusable = true + defaultTitle = "football" +) + +type Settings struct { + common *cfg.Common + apiKey string `help:"Your Football-data API token."` + league string `help:"Name of the competition. For example PL"` + favTeam string `help:"Teams to follow in mentioned league"` + matchesFrom int `help:"Matches till Today (Today - Number of days), Default: 2"` + matchesTo int `help:"Matches from Today (Today + Number of days), Default: 5"` + standingCount int `help:"Top N number of teams in standings, Default: 5"` +} + +func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings { + + settings := Settings{ + common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig), + apiKey: ymlConfig.UString("apiKey", ymlConfig.UString("apikey", os.Getenv("WTF_FOOTBALL_API_KEY"))), + league: ymlConfig.UString("league", ymlConfig.UString("league", os.Getenv("WTF_FOOTBALL_LEAGUE"))), + favTeam: ymlConfig.UString("favTeam", ymlConfig.UString("favTeam", os.Getenv("WTF_FOOTBALL_TEAM"))), + matchesFrom: ymlConfig.UInt("matchesFrom", 5), + matchesTo: ymlConfig.UInt("matchesTo", 5), + standingCount: ymlConfig.UInt("standingCount", 5), + } + return &settings +} diff --git a/modules/football/types.go b/modules/football/types.go new file mode 100644 index 00000000..38e9df14 --- /dev/null +++ b/modules/football/types.go @@ -0,0 +1,46 @@ +package football + +type Team struct { + Name string `json:"name"` +} + +type LeagueStandings struct { + Standings []struct { + Table []Table `json:"table"` + } `json:"standings"` +} + +type Table struct { + Draw int `json:"draw"` + GoalDifference int `json:"goalDifference"` + Lost int `json:"lost"` + Won int `json:"won"` + PlayedGames int `json:"playedGames"` + Points int `json:"points"` + Position int `json:"position"` + Team Team `json:"team"` +} + +type LeagueFixtuers struct { + Matches []Matches `json:"matches"` +} + +type Matches struct { + AwayTeam Team `json:"awayTeam"` + HomeTeam Team `json:"homeTeam"` + Score Score `json:"score"` + Stage string `json:"stage"` + Status string `json:"status"` + Date string `json:"utcDate"` +} + +type Score struct { + FullTime ScoreByTime `json:"fullTime"` + HalfTime ScoreByTime `json:"halfTime"` + Winner string `json:"winner"` +} + +type ScoreByTime struct { + AwayTeam int `json:"awayTeam"` + HomeTeam int `json:"homeTeam"` +} diff --git a/modules/football/util.go b/modules/football/util.go new file mode 100644 index 00000000..ee2c0572 --- /dev/null +++ b/modules/football/util.go @@ -0,0 +1,37 @@ +package football + +import ( + "bytes" + "fmt" + "strings" + "time" + + "github.com/olekukonko/tablewriter" +) + +func createTable(header []string, buf *bytes.Buffer) *tablewriter.Table { + + table := tablewriter.NewWriter(buf) + if len(header) != 0 { + table.SetHeader(header) + } + table.SetBorder(false) + table.SetCenterSeparator(" ") + table.SetColumnSeparator(" ") + table.SetRowSeparator(" ") + table.SetAlignment(tablewriter.ALIGN_LEFT) + + return table +} + +func parseDateString(d string) string { + + return fmt.Sprintf("🕙 %s", strings.Replace(d, "T", " ", 1)) +} + +func getDateString(offset int) string { + + today := time.Now() + return today.AddDate(0, 0, offset).Format("2006-01-02") + +} diff --git a/modules/football/widget.go b/modules/football/widget.go new file mode 100644 index 00000000..b559a480 --- /dev/null +++ b/modules/football/widget.go @@ -0,0 +1,181 @@ +package football + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "strconv" + "strings" + + "github.com/rivo/tview" + "github.com/wtfutil/wtf/view" +) + +var leagueID = map[string]leagueInfo{ + "BSA": {2013, "Brazil Série A"}, + "PL": {2021, "English Premier League"}, + "EC": {2016, "English Championship"}, + "EUC": {2018, "European Championship"}, + "EL2": {444, "Campeonato Brasileiro da Série A"}, + "CL": {2001, "UEFA Champions League"}, + "FL1": {2015, "French Ligue 1"}, + "GB": {2002, "German Bundesliga"}, + "ISA": {2019, "Italy Serie A"}, + "NE": {2003, "Netherlands Eredivisie"}, + "PPL": {2017, "Portugal Primeira Liga"}, + "SPD": {2014, "Spain Primera Division"}, + "WC": {2000, "FIFA World Cup"}, +} + +type Widget struct { + view.TextWidget + *Client + settings *Settings + League leagueInfo + err error +} + +func NewWidget(app *tview.Application, pages *tview.Pages, settings *Settings) *Widget { + var widget Widget + leagueId, err := getLeague(settings.league) + if err != nil { + widget = Widget{ + err: fmt.Errorf("Unable to get the league id for provided league '%s'", settings.league), + Client: NewClient(settings.apiKey), + settings: settings, + } + return &widget + } + widget = Widget{ + TextWidget: view.NewTextWidget(app, settings.common), + Client: NewClient(settings.apiKey), + League: leagueId, + settings: settings, + } + return &widget +} + +func (widget *Widget) Refresh() { + widget.Redraw(widget.content) +} + +func (widget *Widget) content() (string, string, bool) { + + var content string + title := fmt.Sprintf("%s %s", widget.CommonSettings().Title, widget.League.caption) + wrap := false + if widget.err != nil { + return title, widget.err.Error(), true + } + content += widget.GetStandings(widget.League.id) + content += widget.GetMatches(widget.League.id) + + return title, content, wrap +} + +func getLeague(league string) (leagueInfo, error) { + + var l leagueInfo + if val, ok := leagueID[league]; ok { + return val, nil + } + return l, fmt.Errorf("No such league") +} + +// GetStandings of particular league +func (widget *Widget) GetStandings(leagueId int) string { + + var l LeagueStandings + var content string + content += fmt.Sprintf("Standings:\n\n") + buf := new(bytes.Buffer) + tStandings := createTable([]string{"No.", "Team", "MP", "Won", "Draw", "Lost", "GD", "Points"}, buf) + resp, err := widget.Client.footballRequest("standings", leagueId) + if err != nil { + return fmt.Sprintf("Error fetching standings: %s", err.Error()) + } + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Sprintf("Error fetching standings: %s", err.Error()) + } + err = json.Unmarshal(data, &l) + if err != nil { + return fmt.Sprintf("Error fetching standings") + } + + if len(l.Standings) > 0 { + for _, i := range l.Standings[0].Table { + if i.Position <= widget.settings.standingCount { + row := []string{strconv.Itoa(i.Position), i.Team.Name, strconv.Itoa(i.PlayedGames), strconv.Itoa(i.Won), strconv.Itoa(i.Draw), strconv.Itoa(i.Lost), strconv.Itoa(i.GoalDifference), strconv.Itoa(i.Points)} + tStandings.Append(row) + } + } + } else { + return fmt.Sprintf("Error fetching standings") + } + + tStandings.Render() + content += buf.String() + + return content +} + +// GetMatches of particular league +func (widget *Widget) GetMatches(leagueId int) string { + + var l LeagueFixtuers + var content string + scheduledBuf := new(bytes.Buffer) + playedBuf := new(bytes.Buffer) + + tScheduled := createTable([]string{}, scheduledBuf) + tPlayed := createTable([]string{}, playedBuf) + + from := getDateString(-widget.settings.matchesFrom) + to := getDateString(widget.settings.matchesTo) + + requestPath := fmt.Sprintf("matches?dateFrom=%s&dateTo=%s", from, to) + resp, err := widget.Client.footballRequest(requestPath, leagueId) + if err != nil { + return fmt.Sprintf("Error fetching matches: %s", err.Error()) + } + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Sprintf("Error fetching matches: %s", err.Error()) + } + err = json.Unmarshal(data, &l) + if err != nil { + return fmt.Sprintf("Error fetching matches: %s", err.Error()) + } + + if len(l.Matches) != 0 { + + for _, val := range l.Matches { + if strings.Contains(val.AwayTeam.Name, widget.settings.favTeam) || strings.Contains(val.HomeTeam.Name, widget.settings.favTeam) || widget.settings.favTeam == "" { + if val.Status == "SCHEDULED" { + row := []string{"⚽", val.HomeTeam.Name, "🆚", val.AwayTeam.Name, parseDateString(val.Date)} + tScheduled.Append(row) + } else if val.Status == "FINISHED" { + row := []string{"⚽", val.HomeTeam.Name, strconv.Itoa(val.Score.FullTime.HomeTeam), "🆚", val.AwayTeam.Name, strconv.Itoa(val.Score.FullTime.AwayTeam)} + tPlayed.Append(row) + } + } + } + tScheduled.Render() + tPlayed.Render() + if playedBuf.String() != "" { + content += "\nMatches Played:\n\n" + content += playedBuf.String() + + } + if scheduledBuf.String() != "" { + content += "\nUpcoming Matches:\n\n" + content += scheduledBuf.String() + } + } + + return content +}