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 d60fabff..a85ff0a1 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,8 @@ 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/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/pkg/errors v0.8.1 github.com/pkg/profile v1.3.0 diff --git a/go.sum b/go.sum index 58b274b5..3f056d39 100644 --- a/go.sum +++ b/go.sum @@ -168,6 +168,7 @@ github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b/go.mod h1:7rIyQ github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4= github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -192,10 +193,14 @@ 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= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2 h1:uqH7bpe+ERSiDa34FDOF7RikN6RzXgduUF8yarlZp94= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= 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..56d2f1df --- /dev/null +++ b/modules/football/widget.go @@ -0,0 +1,197 @@ +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 { + return fmt.Sprintf("Error fetching standings") + } + + 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) + } + } + + 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 { + return fmt.Sprintf("Error fetching matches") + } + + for _, m := range l.Matches { + + widget.markFavorite(&m) + + if m.Status == "SCHEDULED" { + row := []string{m.HomeTeam.Name, "🆚", m.AwayTeam.Name, parseDateString(m.Date)} + tScheduled.Append(row) + } else if m.Status == "FINISHED" { + row := []string{m.HomeTeam.Name, strconv.Itoa(m.Score.FullTime.HomeTeam), "🆚", m.AwayTeam.Name, strconv.Itoa(m.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 +} + +func (widget *Widget) markFavorite(m *Matches) { + + switch { + + case widget.settings.favTeam == "": + return + case strings.Contains(m.AwayTeam.Name, widget.settings.favTeam): + m.AwayTeam.Name = fmt.Sprintf("%s ⭐", m.AwayTeam.Name) + case strings.Contains(m.HomeTeam.Name, widget.settings.favTeam): + m.HomeTeam.Name = fmt.Sprintf("%s ⭐", m.HomeTeam.Name) + } +}