Initial commit, pt. 12

This commit is contained in:
Dmitrii Okunev
2024-06-09 00:29:23 +01:00
parent f942ba3aeb
commit a5ee9dfe10
8 changed files with 423 additions and 52 deletions

3
go.mod
View File

@@ -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
View File

@@ -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=

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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
}