mirror of
https://github.com/xaionaro-go/streamctl.git
synced 2025-10-30 10:27:03 +08:00
Initial commit, pt. 12
This commit is contained in:
3
go.mod
3
go.mod
@@ -29,12 +29,14 @@ require (
|
||||
github.com/go-text/typesetting v0.1.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
github.com/huandu/go-tls v0.0.0-20200109070953-6f75fb441850 // indirect
|
||||
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
github.com/tevino/abool v1.2.0 // indirect
|
||||
github.com/xaionaro-go/spinlock v0.0.0-20200518175509-30e6d1ce68a1 // indirect
|
||||
github.com/yuin/goldmark v1.5.5 // indirect
|
||||
golang.org/x/image v0.11.0 // indirect
|
||||
golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda // indirect
|
||||
@@ -68,6 +70,7 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/xaionaro-go/gorex v0.0.0-20200314172213-23bed04bc3e3
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
|
||||
15
go.sum
15
go.sum
@@ -49,6 +49,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DataDog/gostackparse v0.6.0 h1:egCGQviIabPwsyoWpGvIBGrEnNWez35aEO7OJ1vBI4o=
|
||||
github.com/DataDog/gostackparse v0.6.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
@@ -243,6 +244,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/huandu/go-tls v0.0.0-20200109070953-6f75fb441850 h1:e6Xuec7psx1wWcYffIzWzhXBBFOJ526073fie9Cc79c=
|
||||
github.com/huandu/go-tls v0.0.0-20200109070953-6f75fb441850/go.mod h1:WeItecBdaIdUBRb7cSMMk+rq41iFKhf6Q9mDRDpbdec=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
@@ -338,6 +341,13 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
|
||||
github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
|
||||
github.com/valyala/fastrand v1.0.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
|
||||
github.com/xaionaro-go/gorex v0.0.0-20200314172213-23bed04bc3e3 h1:V9vb07lOoiuOXwF2XviWrLM+VXD7+0YAnlWAW7e1BXs=
|
||||
github.com/xaionaro-go/gorex v0.0.0-20200314172213-23bed04bc3e3/go.mod h1:azYJIFDMpJyMON8WqQL7nz7ZnoM8XDdGyr5kZ0fQxBI=
|
||||
github.com/xaionaro-go/rand v0.0.0-20191005105903-aba1befc54a5/go.mod h1:7GUXx8dYHRMAYntgs4o1Vm1IPZ7EIpGbev68nKIKGMQ=
|
||||
github.com/xaionaro-go/spinlock v0.0.0-20190309154744-55278e21e817/go.mod h1:Nb/15eS0BMty6TMuWgRQM8WCDIUlyPZagcpchHT6c9Y=
|
||||
github.com/xaionaro-go/spinlock v0.0.0-20200518175509-30e6d1ce68a1 h1:1Kqw9dv2LnznIhJoMt3dNzc/ctSj6VHjyGh4YZHjpE4=
|
||||
github.com/xaionaro-go/spinlock v0.0.0-20200518175509-30e6d1ce68a1/go.mod h1:UwmTXX+EpoEYHuy0rSys1Rp5PW+eVTgZSjgMVLJENKg=
|
||||
github.com/yoelsusanto/go-yaml v0.0.0-20240324162521-2018c1ab915b h1:eoc4aMdU40lOUIlpKTRXfSMcaE8Z6EFJuFw5ptB83lg=
|
||||
github.com/yoelsusanto/go-yaml v0.0.0-20240324162521-2018c1ab915b/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -349,6 +359,7 @@ github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.5.5 h1:IJznPe8wOzfIKETmMkd06F8nXkmlhaHqFRM9l1hAGsU=
|
||||
github.com/yuin/goldmark v1.5.5/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40/go.mod h1:rOnSnoRyxMI3fe/7KIbVcsHRGxe30OONv8dEgo+vCfA=
|
||||
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
|
||||
@@ -386,6 +397,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
@@ -516,11 +528,13 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -774,6 +788,7 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
lukechampine.com/frand v1.1.0/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
||||
@@ -82,7 +82,11 @@ func NewCodeReceiver(redirectURL string) (codeCh chan string, err error) {
|
||||
codeCh <- code
|
||||
listener.Close()
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprintf(w, "Received code: %v\r\nYou can now safely close this browser window.", code)
|
||||
if code == "" {
|
||||
fmt.Fprintf(w, "No code received :(\r\n\r\nYou can close this browser window.")
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "Received code: %v\r\n\r\nYou can now safely close this browser window.", code)
|
||||
}))
|
||||
|
||||
return codeCh, nil
|
||||
|
||||
@@ -200,6 +200,16 @@ func GetPlatformConfig[T any, S StreamProfile](
|
||||
return ConvertPlatformConfig[T, S](ctx, platCfg)
|
||||
}
|
||||
|
||||
func ToAbstractPlatformConfig[T any, S StreamProfile](
|
||||
ctx context.Context,
|
||||
platCfg *PlatformConfig[T, S],
|
||||
) *AbstractPlatformConfig {
|
||||
return &AbstractPlatformConfig{
|
||||
Config: platCfg.Config,
|
||||
StreamProfiles: ToAbstractStreamProfiles[S](platCfg.StreamProfiles),
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertPlatformConfig[T any, S StreamProfile](
|
||||
ctx context.Context,
|
||||
platCfg *AbstractPlatformConfig,
|
||||
|
||||
@@ -41,21 +41,33 @@ type StreamProfile interface {
|
||||
AbstractStreamProfile
|
||||
}
|
||||
|
||||
func GetStreamProfile[T StreamProfile](
|
||||
ctx context.Context,
|
||||
v AbstractStreamProfile,
|
||||
) (*T, error) {
|
||||
var profile T
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to serialize: %w: %#+v", err, v)
|
||||
}
|
||||
err = json.Unmarshal(b, &profile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to deserialize: %w: <%s>", err, b)
|
||||
}
|
||||
logger.Debugf(ctx, "converted %#+v to %#+v", v, profile)
|
||||
return &profile, nil
|
||||
}
|
||||
|
||||
func ConvertStreamProfiles[T StreamProfile](
|
||||
ctx context.Context,
|
||||
m map[ProfileName]AbstractStreamProfile,
|
||||
) error {
|
||||
for k, v := range m {
|
||||
var profile T
|
||||
b, err := json.Marshal(v)
|
||||
profile, err := GetStreamProfile[T](ctx, v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to serialize: %w: %#+v", err, v)
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(b, &profile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to deserialize: %w: <%s>", err, b)
|
||||
}
|
||||
m[k] = profile
|
||||
m[k] = *profile
|
||||
logger.Debugf(ctx, "converted %#+v to %#+v", v, profile)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -3,6 +3,7 @@ package twitch
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/facebookincubator/go-belt/tool/experimental/errmon"
|
||||
@@ -105,8 +106,19 @@ func (t *Twitch) ApplyProfile(
|
||||
logger.Errorf(ctx, "unable to get the category ID: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
tags := make([]string, 0, len(profile.Tags))
|
||||
for _, tag := range profile.Tags {
|
||||
tag = strings.ReplaceAll(tag, " ", "")
|
||||
tag = strings.ReplaceAll(tag, "-", "")
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
|
||||
params := &helix.EditChannelInformationParams{
|
||||
Tags: profile.Tags,
|
||||
Tags: tags,
|
||||
}
|
||||
if profile.Language != nil {
|
||||
params.BroadcasterLanguage = *profile.Language
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"github.com/facebookincubator/go-belt/tool/experimental/errmon"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
"github.com/go-ng/xmath"
|
||||
"github.com/xaionaro-go/gorex"
|
||||
"github.com/xaionaro-go/streamctl/pkg/oauthhandler"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/twitch"
|
||||
@@ -40,7 +42,7 @@ type Panel struct {
|
||||
|
||||
app fyne.App
|
||||
config config
|
||||
startStopMutex sync.Mutex
|
||||
startStopMutex gorex.Mutex
|
||||
updateTimerHandler *updateTimerHandler
|
||||
streamControllers struct {
|
||||
Twitch *twitch.Twitch
|
||||
@@ -59,6 +61,9 @@ type Panel struct {
|
||||
|
||||
dataPath string
|
||||
filterValue string
|
||||
|
||||
youtubeCheck *widget.Check
|
||||
twitchCheck *widget.Check
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -75,6 +80,10 @@ func (p *Panel) Loop(ctx context.Context) error {
|
||||
if p.defaultContext != nil {
|
||||
return fmt.Errorf("Loop was already used, and cannot be used the second time")
|
||||
}
|
||||
p.startStopMutex = gorex.Mutex{
|
||||
InfiniteContext: ctx,
|
||||
}
|
||||
|
||||
p.defaultContext = ctx
|
||||
logger.Debug(ctx, "config", p.config)
|
||||
|
||||
@@ -84,19 +93,36 @@ func (p *Panel) Loop(ctx context.Context) error {
|
||||
|
||||
p.app = fyneapp.New()
|
||||
|
||||
go func() {
|
||||
if err := p.initStreamControllers(ctx); err != nil {
|
||||
return fmt.Errorf("unable to initialize stream controllers: %w", err)
|
||||
err = fmt.Errorf("unable to initialize stream controllers: %w", err)
|
||||
p.displayError(err)
|
||||
return
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
p.initTwitchData()
|
||||
p.normalizeTwitchData()
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
p.initYoutubeData()
|
||||
p.normalizeYoutubeData()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
p.initMainWindow(ctx)
|
||||
p.rearrangeProfiles(ctx)
|
||||
}()
|
||||
|
||||
p.mainWindow.ShowAndRun()
|
||||
p.app.Run()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -267,7 +293,11 @@ func (p *Panel) openBrowser(authURL string) error {
|
||||
case "darwin":
|
||||
browserCmd = "open"
|
||||
case "linux":
|
||||
if envBrowser := os.Getenv("BROWSER"); envBrowser != "" {
|
||||
browserCmd = envBrowser
|
||||
} else {
|
||||
browserCmd = "xdg-open"
|
||||
}
|
||||
default:
|
||||
return oauthhandler.LaunchBrowser(authURL)
|
||||
}
|
||||
@@ -275,6 +305,8 @@ func (p *Panel) openBrowser(authURL string) error {
|
||||
waitCh := make(chan struct{})
|
||||
|
||||
w := p.app.NewWindow("Browser selection window")
|
||||
promptText := widget.NewRichTextWithText("It is required to confirm access in Twitch/YouTube using browser. Select a browser for that (or leave the field empty for auto-selection):")
|
||||
promptText.Wrapping = fyne.TextWrapWord
|
||||
browserField := widget.NewEntry()
|
||||
browserField.PlaceHolder = "command to execute the browser"
|
||||
browserField.OnSubmitted = func(s string) {
|
||||
@@ -284,16 +316,19 @@ func (p *Panel) openBrowser(authURL string) error {
|
||||
close(waitCh)
|
||||
})
|
||||
w.SetContent(container.NewBorder(
|
||||
container.NewVBox(
|
||||
promptText,
|
||||
browserField,
|
||||
),
|
||||
okButton,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
))
|
||||
|
||||
go w.ShowAndRun()
|
||||
w.Show()
|
||||
<-waitCh
|
||||
w.Close()
|
||||
w.Hide()
|
||||
|
||||
if browserField.Text != "" {
|
||||
browserCmd = browserField.Text
|
||||
@@ -302,18 +337,136 @@ func (p *Panel) openBrowser(authURL string) error {
|
||||
return exec.Command(browserCmd, authURL).Start()
|
||||
}
|
||||
|
||||
var twitchAppsCreateLink, _ = url.Parse("https://dev.twitch.tv/console/apps/create")
|
||||
|
||||
func (p *Panel) inputTwitchUserInfo(
|
||||
ctx context.Context,
|
||||
cfg *streamcontrol.PlatformConfig[twitch.PlatformSpecificConfig, twitch.StreamProfile],
|
||||
) error {
|
||||
w := p.app.NewWindow("Input Twitch user info")
|
||||
w.Resize(fyne.NewSize(600, 200))
|
||||
|
||||
channelField := widget.NewEntry()
|
||||
channelField.SetPlaceHolder("channel ID (copy&paste it from the browser: https://www.twitch.tv/<the channel ID is here>)")
|
||||
clientIDField := widget.NewEntry()
|
||||
clientIDField.SetPlaceHolder("client ID")
|
||||
clientSecretField := widget.NewEntry()
|
||||
clientSecretField.SetPlaceHolder("client secret")
|
||||
instructionText := widget.NewRichText(
|
||||
&widget.TextSegment{Text: "Go to\n", Style: widget.RichTextStyle{Inline: true}},
|
||||
&widget.HyperlinkSegment{Text: twitchAppsCreateLink.String(), URL: twitchAppsCreateLink},
|
||||
&widget.TextSegment{Text: `,` + "\n" + `create an application (enter "http://localhost:8091/" as the "OAuth Redirect URLs" value), then click "Manage" then "New Secret", and copy&paste client ID and client secret.`, Style: widget.RichTextStyle{Inline: true}},
|
||||
)
|
||||
instructionText.Wrapping = fyne.TextWrapWord
|
||||
|
||||
waitCh := make(chan struct{})
|
||||
okButton := widget.NewButtonWithIcon("OK", theme.ConfirmIcon(), func() {
|
||||
close(waitCh)
|
||||
})
|
||||
|
||||
w.SetContent(container.NewBorder(
|
||||
widget.NewRichTextWithText("Enter Twitch user info:"),
|
||||
okButton,
|
||||
nil,
|
||||
nil,
|
||||
container.NewVBox(
|
||||
channelField,
|
||||
clientIDField,
|
||||
clientSecretField,
|
||||
instructionText,
|
||||
),
|
||||
))
|
||||
w.Show()
|
||||
<-waitCh
|
||||
w.Hide()
|
||||
|
||||
cfg.Config.AuthType = "user"
|
||||
cfg.Config.Channel = channelField.Text
|
||||
cfg.Config.ClientID = clientIDField.Text
|
||||
cfg.Config.ClientSecret = clientSecretField.Text
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var youtubeCredentialsCreateLink, _ = url.Parse("https://console.cloud.google.com/apis/credentials/oauthclient")
|
||||
|
||||
func (p *Panel) inputYouTubeUserInfo(
|
||||
ctx context.Context,
|
||||
cfg *streamcontrol.PlatformConfig[youtube.PlatformSpecificConfig, youtube.StreamProfile],
|
||||
) error {
|
||||
w := p.app.NewWindow("Input YouTube user info")
|
||||
w.Resize(fyne.NewSize(600, 200))
|
||||
|
||||
clientIDField := widget.NewEntry()
|
||||
clientIDField.SetPlaceHolder("client ID")
|
||||
clientSecretField := widget.NewEntry()
|
||||
clientSecretField.SetPlaceHolder("client secret")
|
||||
instructionText := widget.NewRichText(
|
||||
&widget.TextSegment{Text: "Go to\n", Style: widget.RichTextStyle{Inline: true}},
|
||||
&widget.HyperlinkSegment{Text: youtubeCredentialsCreateLink.String(), URL: youtubeCredentialsCreateLink},
|
||||
&widget.TextSegment{Text: `,` + "\n" + `configure "consent screen" (note: you may add yourself into Test Users to avoid problems further on, and don't forget to add "YouTube Data API v3" scopes) and go back to` + "\n", Style: widget.RichTextStyle{Inline: true}},
|
||||
&widget.HyperlinkSegment{Text: youtubeCredentialsCreateLink.String(), URL: youtubeCredentialsCreateLink},
|
||||
&widget.TextSegment{Text: `,` + "\n" + `choose "Desktop app", confirm and copy&paste client ID and client secret.`, Style: widget.RichTextStyle{Inline: true}},
|
||||
)
|
||||
instructionText.Wrapping = fyne.TextWrapWord
|
||||
|
||||
waitCh := make(chan struct{})
|
||||
okButton := widget.NewButtonWithIcon("OK", theme.ConfirmIcon(), func() {
|
||||
close(waitCh)
|
||||
})
|
||||
|
||||
w.SetContent(container.NewBorder(
|
||||
widget.NewRichTextWithText("Enter YouTube user info:"),
|
||||
okButton,
|
||||
nil,
|
||||
nil,
|
||||
container.NewVBox(
|
||||
clientIDField,
|
||||
clientSecretField,
|
||||
instructionText,
|
||||
),
|
||||
))
|
||||
w.Show()
|
||||
<-waitCh
|
||||
w.Hide()
|
||||
|
||||
cfg.Config.ClientID = clientIDField.Text
|
||||
cfg.Config.ClientSecret = clientSecretField.Text
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Panel) initStreamControllers(ctx context.Context) error {
|
||||
for platName, cfg := range p.data.Backends {
|
||||
platNames := make([]streamcontrol.PlatformName, 0, len(p.data.Backends))
|
||||
for platName := range p.data.Backends {
|
||||
platNames = append(platNames, platName)
|
||||
}
|
||||
sort.Slice(platNames, func(i, j int) bool {
|
||||
return platNames[i] < platNames[j]
|
||||
})
|
||||
for _, platName := range platNames {
|
||||
cfg := p.data.Backends[platName]
|
||||
var err error
|
||||
switch strings.ToLower(string(platName)) {
|
||||
case strings.ToLower(string(twitch.ID)):
|
||||
p.streamControllers.Twitch, err = newTwitch(ctx, cfg, func(cfg *streamcontrol.AbstractPlatformConfig) error {
|
||||
p.streamControllers.Twitch, err = newTwitch(
|
||||
ctx,
|
||||
cfg,
|
||||
p.inputTwitchUserInfo,
|
||||
func(cfg *streamcontrol.AbstractPlatformConfig) error {
|
||||
return p.savePlatformConfig(ctx, twitch.ID, cfg)
|
||||
}, p.oauthHandlerTwitch)
|
||||
},
|
||||
p.oauthHandlerTwitch)
|
||||
case strings.ToLower(string(youtube.ID)):
|
||||
p.streamControllers.YouTube, err = newYouTube(ctx, cfg, func(cfg *streamcontrol.AbstractPlatformConfig) error {
|
||||
p.streamControllers.YouTube, err = newYouTube(
|
||||
ctx,
|
||||
cfg,
|
||||
p.inputYouTubeUserInfo,
|
||||
func(cfg *streamcontrol.AbstractPlatformConfig) error {
|
||||
return p.savePlatformConfig(ctx, youtube.ID, cfg)
|
||||
}, p.oauthHandlerYouTube)
|
||||
},
|
||||
p.oauthHandlerYouTube,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to initialize '%s': %w", platName, err)
|
||||
@@ -550,6 +703,7 @@ func (p *Panel) setFilter(ctx context.Context, filter string) {
|
||||
|
||||
func (p *Panel) initMainWindow(ctx context.Context) {
|
||||
w := p.app.NewWindow("StreamPanel")
|
||||
w.Resize(fyne.NewSize(400, 600))
|
||||
|
||||
profileFilter := widget.NewEntry()
|
||||
profileFilter.SetPlaceHolder("filter")
|
||||
@@ -625,11 +779,21 @@ func (p *Panel) initMainWindow(ctx context.Context) {
|
||||
p.startStopButton.OnTapped()
|
||||
}
|
||||
|
||||
bottomPanel := container.NewAdaptiveGrid(
|
||||
1,
|
||||
p.twitchCheck = widget.NewCheck("Twitch", nil)
|
||||
p.twitchCheck.SetChecked(true)
|
||||
p.youtubeCheck = widget.NewCheck("YouTube", nil)
|
||||
p.youtubeCheck.SetChecked(true)
|
||||
|
||||
bottomPanel := container.NewVBox(
|
||||
p.streamTitleField,
|
||||
p.streamDescriptionField,
|
||||
container.NewBorder(
|
||||
nil,
|
||||
nil,
|
||||
container.NewHBox(p.twitchCheck, p.youtubeCheck),
|
||||
nil,
|
||||
p.startStopButton,
|
||||
),
|
||||
)
|
||||
w.SetContent(container.NewBorder(
|
||||
topPanel,
|
||||
@@ -644,24 +808,118 @@ func (p *Panel) initMainWindow(ctx context.Context) {
|
||||
p.profilesListWidget = profilesList
|
||||
}
|
||||
|
||||
func (p *Panel) getSelectedProfile() Profile {
|
||||
return p.getProfile(*p.selectedProfileName)
|
||||
}
|
||||
|
||||
func (p *Panel) startStream() {
|
||||
p.startStopMutex.Lock()
|
||||
defer p.startStopMutex.Unlock()
|
||||
|
||||
if p.startStopButton.Disabled() {
|
||||
return
|
||||
}
|
||||
p.startStopButton.Disable()
|
||||
defer p.startStopButton.Enable()
|
||||
|
||||
p.twitchCheck.Disable()
|
||||
p.youtubeCheck.Disable()
|
||||
|
||||
p.startStopButton.SetText("Stop stream")
|
||||
p.startStopButton.Icon = theme.MediaStopIcon()
|
||||
p.startStopButton.Importance = widget.DangerImportance
|
||||
if p.updateTimerHandler != nil {
|
||||
p.updateTimerHandler.Stop()
|
||||
}
|
||||
p.updateTimerHandler = newUpdateTimerHandler(p.startStopButton)
|
||||
profile := p.getSelectedProfile()
|
||||
|
||||
var twitchProfile *twitch.StreamProfile
|
||||
if p.twitchCheck.Checked && p.streamControllers.Twitch != nil {
|
||||
var err error
|
||||
twitchProfile, err = streamcontrol.GetStreamProfile[twitch.StreamProfile](p.defaultContext, profile.PerPlatform[twitch.ID])
|
||||
if err != nil {
|
||||
p.displayError(fmt.Errorf("unable to get the streaming profile for Twitch: %w", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var youtubeProfile *youtube.StreamProfile
|
||||
if p.youtubeCheck.Checked && p.streamControllers.YouTube != nil {
|
||||
var err error
|
||||
youtubeProfile, err = streamcontrol.GetStreamProfile[youtube.StreamProfile](p.defaultContext, profile.PerPlatform[youtube.ID])
|
||||
if err != nil {
|
||||
p.displayError(fmt.Errorf("unable to get the streaming profile for YouTube: %w", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if p.twitchCheck.Checked && p.streamControllers.Twitch != nil {
|
||||
err := p.streamControllers.Twitch.StartStream(
|
||||
p.defaultContext,
|
||||
p.streamTitleField.Text,
|
||||
p.streamDescriptionField.Text,
|
||||
*twitchProfile,
|
||||
)
|
||||
if err != nil {
|
||||
p.displayError(fmt.Errorf("unable to setup the stream on Twitch: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
if p.youtubeCheck.Checked && p.streamControllers.YouTube != nil {
|
||||
err := p.streamControllers.YouTube.StartStream(
|
||||
p.defaultContext,
|
||||
p.streamTitleField.Text,
|
||||
p.streamDescriptionField.Text,
|
||||
*youtubeProfile,
|
||||
)
|
||||
if err != nil {
|
||||
p.displayError(fmt.Errorf("unable to start the stream on YouTube: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Panel) stopStream() {
|
||||
p.startStopMutex.Lock()
|
||||
defer p.startStopMutex.Unlock()
|
||||
|
||||
p.twitchCheck.Enable()
|
||||
p.youtubeCheck.Enable()
|
||||
|
||||
p.startStopButton.SetText("Start stream")
|
||||
p.startStopButton.Icon = theme.MediaRecordIcon()
|
||||
p.startStopButton.Importance = widget.SuccessImportance
|
||||
|
||||
p.updateTimerHandler.Stop()
|
||||
if p.updateTimerHandler != nil {
|
||||
p.updateTimerHandler.Stop()
|
||||
}
|
||||
p.updateTimerHandler = nil
|
||||
p.startStopButton.SetText("")
|
||||
|
||||
if p.streamControllers.Twitch != nil {
|
||||
err := p.streamControllers.Twitch.EndStream(p.defaultContext)
|
||||
if err != nil {
|
||||
p.displayError(fmt.Errorf("unable to stop the stream on Twitch: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
if p.streamControllers.YouTube != nil {
|
||||
err := p.streamControllers.YouTube.EndStream(p.defaultContext)
|
||||
if err != nil {
|
||||
p.displayError(fmt.Errorf("unable to stop the stream on YouTube: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Panel) onStartStopButton() {
|
||||
p.startStopMutex.Lock()
|
||||
defer p.startStopMutex.Unlock()
|
||||
|
||||
if p.updateTimerHandler != nil {
|
||||
p.startStopButton.SetText("Start stream")
|
||||
p.startStopButton.Icon = theme.MediaRecordIcon()
|
||||
p.startStopButton.Importance = widget.SuccessImportance
|
||||
p.updateTimerHandler.Stop()
|
||||
panic("stream stopping is not implemented")
|
||||
p.updateTimerHandler = nil
|
||||
p.startStopButton.SetText("")
|
||||
p.stopStream()
|
||||
} else {
|
||||
p.startStopButton.SetText("Stop stream")
|
||||
p.startStopButton.Icon = theme.MediaStopIcon()
|
||||
p.startStopButton.Importance = widget.DangerImportance
|
||||
p.updateTimerHandler = newUpdateTimerHandler(p.startStopButton)
|
||||
panic("stream starting is not implemented")
|
||||
p.startStream()
|
||||
}
|
||||
|
||||
p.startStopButton.Refresh()
|
||||
@@ -750,8 +1008,15 @@ func (p *Panel) newProfileWindow(ctx context.Context) fyne.Window {
|
||||
"Create a profile",
|
||||
Profile{},
|
||||
func(ctx context.Context, profile Profile) error {
|
||||
oldProfile := p.getProfile(profile.Name)
|
||||
if oldProfile.Name == profile.Name {
|
||||
found := false
|
||||
for _, platCfg := range p.data.Backends {
|
||||
_, ok := platCfg.GetStreamProfile(profile.Name)
|
||||
if ok {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
return fmt.Errorf("profile with name '%s' already exists", profile.Name)
|
||||
}
|
||||
if err := p.profileCreateOrUpdate(ctx, profile); err != nil {
|
||||
@@ -1002,6 +1267,9 @@ func (p *Panel) profileWindow(
|
||||
}
|
||||
_tags := make([]string, len(tags))
|
||||
for k := range tags {
|
||||
if k == "" {
|
||||
continue
|
||||
}
|
||||
_tags = append(_tags, k)
|
||||
}
|
||||
profile := Profile{
|
||||
@@ -1050,6 +1318,13 @@ func (p *Panel) displayError(err error) {
|
||||
w := p.app.NewWindow("Got an error: " + err.Error())
|
||||
errorMessage := fmt.Sprintf("Error: %v\n\nstack trace:\n%s", err, debug.Stack())
|
||||
w.Resize(fyne.NewSize(400, 300))
|
||||
w.SetContent(widget.NewLabelWithStyle(errorMessage, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}))
|
||||
textWidget := widget.NewMultiLineEntry()
|
||||
textWidget.SetText(errorMessage)
|
||||
textWidget.Wrapping = fyne.TextWrapWord
|
||||
textWidget.TextStyle = fyne.TextStyle{
|
||||
Bold: true,
|
||||
Monospace: true,
|
||||
}
|
||||
w.SetContent(textWidget)
|
||||
w.Show()
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
func newTwitch(
|
||||
ctx context.Context,
|
||||
cfg *streamcontrol.AbstractPlatformConfig,
|
||||
setUserData func(context.Context, *streamcontrol.PlatformConfig[twitch.PlatformSpecificConfig, twitch.StreamProfile]) error,
|
||||
saveCfgFunc func(*streamcontrol.AbstractPlatformConfig) error,
|
||||
customOAuthHandler twitch.OAuthHandler,
|
||||
) (
|
||||
@@ -26,9 +27,18 @@ func newTwitch(
|
||||
return nil, fmt.Errorf("twitch config was not found")
|
||||
}
|
||||
|
||||
hadSetNewUserData := false
|
||||
if platCfg.Config.Channel == "" || platCfg.Config.ClientID == "" || platCfg.Config.ClientSecret == "" {
|
||||
if err := setUserData(ctx, platCfg); err != nil {
|
||||
return nil, fmt.Errorf("unable to set user info: %w", err)
|
||||
}
|
||||
hadSetNewUserData = true
|
||||
}
|
||||
|
||||
logger.Debugf(ctx, "twitch config: %#+v", platCfg)
|
||||
platCfg.Config.CustomOAuthHandler = customOAuthHandler
|
||||
return twitch.New(ctx, *platCfg,
|
||||
cfg = streamcontrol.ToAbstractPlatformConfig(ctx, platCfg)
|
||||
twitch, err := twitch.New(ctx, *platCfg,
|
||||
func(c twitch.Config) error {
|
||||
return saveCfgFunc(&streamcontrol.AbstractPlatformConfig{
|
||||
Config: c.Config,
|
||||
@@ -36,11 +46,22 @@ func newTwitch(
|
||||
})
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to initialize Twitch client: %w", err)
|
||||
}
|
||||
if hadSetNewUserData {
|
||||
logger.Debugf(ctx, "confirmed new twitch user data, saving it")
|
||||
if err := saveCfgFunc(cfg); err != nil {
|
||||
return nil, fmt.Errorf("unable to save the configuration: %w", err)
|
||||
}
|
||||
}
|
||||
return twitch, nil
|
||||
}
|
||||
|
||||
func newYouTube(
|
||||
ctx context.Context,
|
||||
cfg *streamcontrol.AbstractPlatformConfig,
|
||||
setUserData func(context.Context, *streamcontrol.PlatformConfig[youtube.PlatformSpecificConfig, youtube.StreamProfile]) error,
|
||||
saveCfgFunc func(*streamcontrol.AbstractPlatformConfig) error,
|
||||
customOAuthHandler youtube.OAuthHandler,
|
||||
) (
|
||||
@@ -54,9 +75,18 @@ func newYouTube(
|
||||
return nil, fmt.Errorf("youtube config was not found")
|
||||
}
|
||||
|
||||
hadSetNewUserData := false
|
||||
if platCfg.Config.ClientID == "" || platCfg.Config.ClientSecret == "" {
|
||||
if err := setUserData(ctx, platCfg); err != nil {
|
||||
return nil, fmt.Errorf("unable to set user info: %w", err)
|
||||
}
|
||||
hadSetNewUserData = true
|
||||
}
|
||||
|
||||
logger.Debugf(ctx, "youtube config: %#+v", platCfg)
|
||||
platCfg.Config.CustomOAuthHandler = customOAuthHandler
|
||||
return youtube.New(ctx, *platCfg,
|
||||
cfg = streamcontrol.ToAbstractPlatformConfig(ctx, platCfg)
|
||||
yt, err := youtube.New(ctx, *platCfg,
|
||||
func(c youtube.Config) error {
|
||||
return saveCfgFunc(&streamcontrol.AbstractPlatformConfig{
|
||||
Config: c.Config,
|
||||
@@ -64,4 +94,14 @@ func newYouTube(
|
||||
})
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to initialize YouTube client: %w", err)
|
||||
}
|
||||
if hadSetNewUserData {
|
||||
logger.Debugf(ctx, "confirmed new youtube user data, saving it")
|
||||
if err := saveCfgFunc(cfg); err != nil {
|
||||
return nil, fmt.Errorf("unable to save the configuration: %w", err)
|
||||
}
|
||||
}
|
||||
return yt, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user