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/transmission"
|
||||||
"github.com/wtfutil/wtf/modules/travisci"
|
"github.com/wtfutil/wtf/modules/travisci"
|
||||||
"github.com/wtfutil/wtf/modules/trello"
|
"github.com/wtfutil/wtf/modules/trello"
|
||||||
|
"github.com/wtfutil/wtf/modules/twitch"
|
||||||
"github.com/wtfutil/wtf/modules/twitter"
|
"github.com/wtfutil/wtf/modules/twitter"
|
||||||
"github.com/wtfutil/wtf/modules/twitterstats"
|
"github.com/wtfutil/wtf/modules/twitterstats"
|
||||||
"github.com/wtfutil/wtf/modules/unknown"
|
"github.com/wtfutil/wtf/modules/unknown"
|
||||||
@ -256,6 +257,9 @@ func MakeWidget(
|
|||||||
case "trello":
|
case "trello":
|
||||||
settings := trello.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
settings := trello.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
||||||
widget = trello.NewWidget(app, settings)
|
widget = trello.NewWidget(app, settings)
|
||||||
|
case "twitch":
|
||||||
|
settings := twitch.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
||||||
|
widget = twitch.NewWidget(app, pages, settings)
|
||||||
case "twitter":
|
case "twitter":
|
||||||
settings := twitter.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
settings := twitter.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
||||||
widget = twitter.NewWidget(app, pages, settings)
|
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/microsoft/azure-devops-go-api/azuredevops v0.0.0-20191014190507-26902c1d4325
|
||||||
github.com/mmcdole/gofeed v1.0.0-beta2
|
github.com/mmcdole/gofeed v1.0.0-beta2
|
||||||
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect
|
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/olebedev/config v0.0.0-20190528211619-364964f3a8e4
|
||||||
github.com/olekukonko/tablewriter v0.0.4
|
github.com/olekukonko/tablewriter v0.0.4
|
||||||
github.com/onsi/ginkgo v1.10.3 // indirect
|
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/net v0.0.0-20190923162816-aa69164e4478 // indirect
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69 // indirect
|
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69 // indirect
|
||||||
|
golang.org/x/text v0.3.2
|
||||||
google.golang.org/api v0.15.0
|
google.golang.org/api v0.15.0
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||||
gopkg.in/jarcoal/httpmock.v1 v1.0.0-20181110093347-3be5f16b70eb // 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/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/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/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/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 h1:JnVsYEQzhEcOspy6ngIYNF2u0h2mjkXZptzX0IzZQ4g=
|
||||||
github.com/olebedev/config v0.0.0-20190528211619-364964f3a8e4/go.mod h1:RL5+WRxWTAXqqCi9i+eZlHrUtO7AQujUqWi+xMohmc4=
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
"golang.org/x/text/message"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/rivo/tview"
|
"github.com/rivo/tview"
|
||||||
@ -72,3 +75,13 @@ func Truncate(src string, maxLen int, withEllipse bool) string {
|
|||||||
}
|
}
|
||||||
return src
|
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
|
// Only supports non-ellipsed emoji
|
||||||
assert.Equal(t, "๐ฎ๐", Truncate("๐ฎ๐๐ฅ๐พ", 2, false))
|
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