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:
parent
fa0d8761ae
commit
86b32b3f9f
@ -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
2
go.mod
@ -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
2
go.sum
@ -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
27
modules/twitch/client.go
Normal 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)
|
||||
}
|
16
modules/twitch/keyboard.go
Normal file
16
modules/twitch/keyboard.go
Normal 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")
|
||||
}
|
45
modules/twitch/settings.go
Normal file
45
modules/twitch/settings.go
Normal 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
133
modules/twitch/widget.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user