1
0
mirror of https://github.com/taigrr/wtf synced 2025-01-18 04:03:14 -08:00

Adding twitch module to display top streams (#805)

This commit is contained in:
Bjoern Weidlich 2019-12-30 19:20:54 -08:00 committed by Chris Cummer
parent fa0d8761ae
commit 86b32b3f9f
9 changed files with 256 additions and 0 deletions

View File

@ -57,6 +57,7 @@ import (
"github.com/wtfutil/wtf/modules/transmission"
"github.com/wtfutil/wtf/modules/travisci"
"github.com/wtfutil/wtf/modules/trello"
"github.com/wtfutil/wtf/modules/twitch"
"github.com/wtfutil/wtf/modules/twitter"
"github.com/wtfutil/wtf/modules/twitterstats"
"github.com/wtfutil/wtf/modules/unknown"
@ -256,6 +257,9 @@ func MakeWidget(
case "trello":
settings := trello.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = trello.NewWidget(app, settings)
case "twitch":
settings := twitch.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = twitch.NewWidget(app, pages, settings)
case "twitter":
settings := twitter.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = twitter.NewWidget(app, pages, settings)

2
go.mod
View File

@ -36,6 +36,7 @@ require (
github.com/microsoft/azure-devops-go-api/azuredevops v0.0.0-20191014190507-26902c1d4325
github.com/mmcdole/gofeed v1.0.0-beta2
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect
github.com/nicklaw5/helix v0.5.4
github.com/olebedev/config v0.0.0-20190528211619-364964f3a8e4
github.com/olekukonko/tablewriter v0.0.4
github.com/onsi/ginkgo v1.10.3 // indirect
@ -56,6 +57,7 @@ require (
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 // indirect
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69 // indirect
golang.org/x/text v0.3.2
google.golang.org/api v0.15.0
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/jarcoal/httpmock.v1 v1.0.0-20181110093347-3be5f16b70eb // indirect

2
go.sum
View File

@ -200,6 +200,8 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nicklaw5/helix v0.5.4 h1:OAyIHMdmzsNwYJOZscvxAS6eU63YFjtE++CEKvAgujY=
github.com/nicklaw5/helix v0.5.4/go.mod h1:nRcok4VLg8ONQYW/iXBZ24wcfiJjTlDbhgk0ZatOrUY=
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=

27
modules/twitch/client.go Normal file
View File

@ -0,0 +1,27 @@
package twitch
import (
"fmt"
"github.com/nicklaw5/helix"
)
type Twitch struct {
client *helix.Client
}
func NewClient(clientId string) *Twitch {
client, err := helix.NewClient(&helix.Options{
ClientID: clientId,
})
if err != nil {
fmt.Println(err)
}
return &Twitch{client: client}
}
func (t *Twitch) TopStreams(params *helix.StreamsParams) (*helix.StreamsResponse, error) {
if params == nil {
params = &helix.StreamsParams{}
}
return t.client.GetStreams(params)
}

View File

@ -0,0 +1,16 @@
package twitch
import "github.com/gdamore/tcell"
func (widget *Widget) initializeKeyboardControls() {
widget.InitializeCommonControls(widget.Refresh)
widget.SetKeyboardChar("j", widget.Next, "Select next item")
widget.SetKeyboardChar("k", widget.Prev, "Select previous item")
widget.SetKeyboardChar("o", widget.openTwitch, "Open target URL in browser")
widget.SetKeyboardKey(tcell.KeyDown, widget.Next, "Select next item")
widget.SetKeyboardKey(tcell.KeyUp, widget.Prev, "Select previous item")
widget.SetKeyboardKey(tcell.KeyEnter, widget.openTwitch, "Open stream in browser")
widget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, "Clear selection")
}

View File

@ -0,0 +1,45 @@
package twitch
import (
"github.com/olebedev/config"
"github.com/wtfutil/wtf/cfg"
"github.com/wtfutil/wtf/utils"
"os"
)
const (
defaultFocusable = true
)
type Settings struct {
common *cfg.Common
numberOfResults int `help:"Number of results to show. Default is 10." optional:"true"`
clientId string `help:"Client Id (default is env var TWITCH_CLIENT_ID)"`
languages []string `help:"Stream languages" optional:"true"`
gameIds []string `help:"Twitch Game IDs" optional:"true"`
streamType string `help:"Type of stream 'live' (default), 'all', 'vodcast'" optional:"true"`
userIds []string `help:"Twitch user ids" optional:"true"`
userLogins []string `help:"Twitch user names" optional:"true"`
}
func defaultLanguage() []interface{} {
var defaults []interface{}
defaults = append(defaults, "en")
return defaults
}
func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {
twitch := ymlConfig.UString("twitch")
settings := Settings{
common: cfg.NewCommonSettingsFromModule(name, twitch, defaultFocusable, ymlConfig, globalConfig),
numberOfResults: ymlConfig.UInt("numberOfResults", 10),
clientId: ymlConfig.UString("clientId", os.Getenv("TWITCH_CLIENT_ID")),
languages: utils.ToStrs(ymlConfig.UList("languages", defaultLanguage())),
streamType: ymlConfig.UString("streamType", "live"),
gameIds: utils.ToStrs(ymlConfig.UList("gameIds", make([]interface{}, 0))),
userIds: utils.ToStrs(ymlConfig.UList("userIds", make([]interface{}, 0))),
userLogins: utils.ToStrs(ymlConfig.UList("userLogins", make([]interface{}, 0))),
}
return &settings
}

133
modules/twitch/widget.go Normal file
View File

@ -0,0 +1,133 @@
package twitch
import (
"errors"
"fmt"
"github.com/nicklaw5/helix"
"github.com/rivo/tview"
"github.com/wtfutil/wtf/utils"
"github.com/wtfutil/wtf/view"
)
type Widget struct {
view.KeyboardWidget
view.ScrollableWidget
settings *Settings
err error
twitch *Twitch
topStreams []*Stream
}
type Stream struct {
Streamer string
ViewerCount int
Language string
GameID string
Title string
}
func NewWidget(app *tview.Application, pages *tview.Pages, settings *Settings) *Widget {
widget := &Widget{
KeyboardWidget: view.NewKeyboardWidget(app, pages, settings.common),
ScrollableWidget: view.NewScrollableWidget(app, settings.common),
settings: settings,
twitch: NewClient(settings.clientId),
}
widget.SetRenderFunction(widget.Render)
widget.initializeKeyboardControls()
widget.View.SetInputCapture(widget.InputCapture)
widget.KeyboardWidget.SetView(widget.View)
return widget
}
func (widget *Widget) Refresh() {
response, err := widget.twitch.TopStreams(&helix.StreamsParams{
First: widget.settings.numberOfResults,
GameIDs: widget.settings.gameIds,
Language: widget.settings.languages,
Type: widget.settings.streamType,
UserIDs: widget.settings.gameIds,
UserLogins: widget.settings.userLogins,
})
if err != nil {
handleError(widget, err)
} else if response.ErrorMessage != "" {
handleError(widget, errors.New(response.ErrorMessage))
} else {
streams := makeStreams(response)
widget.topStreams = streams
widget.err = nil
if len(streams) <= widget.settings.numberOfResults {
widget.SetItemCount(len(widget.topStreams))
} else {
widget.topStreams = streams[:widget.settings.numberOfResults]
widget.SetItemCount(len(widget.topStreams))
}
}
widget.Render()
}
func (widget *Widget) Render() {
widget.Redraw(widget.content)
}
func makeStreams(response *helix.StreamsResponse) []*Stream {
streams := make([]*Stream, 0)
for _, b := range response.Data.Streams {
streams = append(streams, &Stream{
b.UserName,
b.ViewerCount,
b.Language,
b.GameID,
b.Title,
})
}
return streams
}
func handleError(widget *Widget, err error) {
widget.err = err
widget.topStreams = nil
widget.SetItemCount(0)
}
func (widget *Widget) content() (string, string, bool) {
var title = "Top Streams"
if widget.CommonSettings().Title != "" {
title = widget.CommonSettings().Title
}
if widget.err != nil {
return title, widget.err.Error(), true
}
if len(widget.topStreams) == 0 {
return title, "No data", false
}
var str string
for idx, stream := range widget.topStreams {
row := fmt.Sprintf(
"[%s]%2d. [red]%s [white]%s",
widget.RowColor(idx),
idx+1,
utils.PrettyNumber(float64(stream.ViewerCount)),
stream.Streamer,
)
str += utils.HighlightableHelper(widget.View, row, idx, len(stream.Streamer))
}
return title, str, false
}
// Opens stream in the browser
func (widget *Widget) openTwitch() {
sel := widget.GetSelected()
if sel >= 0 && widget.topStreams != nil && sel < len(widget.topStreams) {
stream := widget.topStreams[sel]
fullLink := "https://twitch.com/" + stream.Streamer
utils.OpenFile(fullLink)
}
}

View File

@ -2,6 +2,9 @@ package utils
import (
"fmt"
"golang.org/x/text/language"
"golang.org/x/text/message"
"math"
"strings"
"github.com/rivo/tview"
@ -72,3 +75,13 @@ func Truncate(src string, maxLen int, withEllipse bool) string {
}
return src
}
// Formats number as string with 1000 delimiters and, if necessary, rounds it to 2 decimals
func PrettyNumber(number float64) string {
p := message.NewPrinter(language.English)
if number == math.Trunc(number) {
return p.Sprintf("%.0f", number)
} else {
return p.Sprintf("%.2f", number)
}
}

View File

@ -43,3 +43,17 @@ func Test_Truncate(t *testing.T) {
// Only supports non-ellipsed emoji
assert.Equal(t, "🌮🚙", Truncate("🌮🚙💥👾", 2, false))
}
func Test_PrettyNumber(t *testing.T) {
assert.Equal(t, "1,000,000", PrettyNumber(1000000))
assert.Equal(t, "1,000,000.99", PrettyNumber(1000000.99))
assert.Equal(t, "1,000,000", PrettyNumber(1000000.00))
assert.Equal(t, "100,000", PrettyNumber(100000))
assert.Equal(t, "100,000.01", PrettyNumber(100000.009))
assert.Equal(t, "10,000", PrettyNumber(10000))
assert.Equal(t, "1,000", PrettyNumber(1000))
assert.Equal(t, "1,000", PrettyNumber(1000))
assert.Equal(t, "100", PrettyNumber(100))
assert.Equal(t, "0", PrettyNumber(0))
assert.Equal(t, "0.10", PrettyNumber(0.1))
}