Add auto shoutouts
Some checks failed
rolling-release / build (push) Has been cancelled
rolling-release / rolling-release (push) Has been cancelled

This commit is contained in:
Dmitrii Okunev
2025-07-19 22:45:35 +01:00
parent 9a1e8fffd5
commit bc97d5d6b8
13 changed files with 248 additions and 48 deletions

View File

@@ -134,7 +134,7 @@ DOCKER_CONTAINER_NAME?=streampanel-android-builder
dockerbuilder-android-arm64: dockerbuilder-android-arm64:
docker pull $(DOCKER_IMAGE) docker pull $(DOCKER_IMAGE)
docker start $(DOCKER_IMAGE) >/dev/null 2>&1 || \ docker start $(DOCKER_CONTAINER_NAME) >/dev/null 2>&1 || \
docker run \ docker run \
--detach \ --detach \
--init \ --init \

View File

@@ -5,4 +5,4 @@ Website = "https://github.com/xaionaro/streamctl"
Name = "streampanel" Name = "streampanel"
ID = "center.dx.streampanel" ID = "center.dx.streampanel"
Version = "0.1.0" Version = "0.1.0"
Build = 433 Build = 437

1
go.mod
View File

@@ -332,6 +332,7 @@ require (
github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250424061409-ccd60fbc7c1c github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250424061409-ccd60fbc7c1c
github.com/coder/websocket v1.8.13 github.com/coder/websocket v1.8.13
github.com/joeyak/go-twitch-eventsub/v3 v3.0.0 github.com/joeyak/go-twitch-eventsub/v3 v3.0.0
github.com/jweslley/localtunnel v0.1.0
github.com/phuslu/goid v1.0.2 // indirect github.com/phuslu/goid v1.0.2 // indirect
github.com/pion/datachannel v1.5.10 // indirect github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect

2
go.sum
View File

@@ -632,6 +632,8 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/jweslley/localtunnel v0.1.0 h1:9VChFBu1lfIq9s6mVF4u4roXFakWArRVF2WhGFVWBLA=
github.com/jweslley/localtunnel v0.1.0/go.mod h1:gf1VMi7Ii8y7PewgFNl1LFxGrJy5uIrSDWxZzTNfddA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12/go.mod h1:u9MdXq/QageOOSGp7qG4XAQsYUMP+V5zEel/Vrl6OOc= github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12/go.mod h1:u9MdXq/QageOOSGp7qG4XAQsYUMP+V5zEel/Vrl6OOc=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=

View File

@@ -28,16 +28,17 @@ type ReverseEngClient interface {
} }
type Kick struct { type Kick struct {
CloseCtx context.Context CloseCtx context.Context
CloseFn context.CancelFunc CloseFn context.CancelFunc
Channel *kickcom.ChannelV1 Channel *kickcom.ChannelV1
Client *gokick.Client Client *gokick.Client
ClientOBSOLETE *kickcom.Kick ClientOBSOLETE *kickcom.Kick
ChatHandler *ChatHandlerOBSOLETE ChatHandler *ChatHandlerOBSOLETE
ChatHandlerLocker xsync.CtxLocker ChatHandlerLocker xsync.CtxLocker
CurrentConfig Config CurrentConfig Config
SaveCfgFn func(Config) error CurrentConfigLocker xsync.Mutex
PrepareLocker xsync.Mutex SaveCfgFn func(Config) error
PrepareLocker xsync.Mutex
lazyInitOnce sync.Once lazyInitOnce sync.Once
getAccessTokenLocker xsync.Mutex getAccessTokenLocker xsync.Mutex
@@ -57,9 +58,10 @@ func New(
} }
options := &gokick.ClientOptions{ options := &gokick.ClientOptions{
UserAccessToken: cfg.Config.UserAccessToken.Get(), UserAccessToken: cfg.Config.UserAccessToken.Get(),
ClientID: cfg.Config.ClientID, UserRefreshToken: cfg.Config.RefreshToken.Get(),
ClientSecret: cfg.Config.ClientSecret.Get(), ClientID: cfg.Config.ClientID,
ClientSecret: cfg.Config.ClientSecret.Get(),
} }
client, err := gokick.NewClient(options) client, err := gokick.NewClient(options)
if err != nil { if err != nil {
@@ -81,9 +83,25 @@ func New(
ClientOBSOLETE: clientOld, ClientOBSOLETE: clientOld,
SaveCfgFn: saveCfgFn, SaveCfgFn: saveCfgFn,
} }
client.OnUserAccessTokenRefreshed(k.onUserAccessTokenRefreshed)
return k, nil return k, nil
} }
func (k *Kick) onUserAccessTokenRefreshed(
userAccessToken string,
refreshToken string,
) {
ctx := context.TODO()
k.CurrentConfigLocker.Do(ctx, func() {
k.CurrentConfig.Config.UserAccessToken.Set(userAccessToken)
k.CurrentConfig.Config.RefreshToken.Set(refreshToken)
err := k.SaveCfgFn(k.CurrentConfig)
if err != nil {
logger.Errorf(ctx, "unable to save the config: %v", err)
}
})
}
func (k *Kick) initChatHandler( func (k *Kick) initChatHandler(
ctx context.Context, ctx context.Context,
) error { ) error {
@@ -666,7 +684,7 @@ func (k *Kick) IsCapable(
case streamcontrol.CapabilityBanUser: case streamcontrol.CapabilityBanUser:
return true return true
case streamcontrol.CapabilityShoutout: case streamcontrol.CapabilityShoutout:
return false return true
case streamcontrol.CapabilityIsChannelStreaming: case streamcontrol.CapabilityIsChannelStreaming:
return false return false
case streamcontrol.CapabilityRaid: case streamcontrol.CapabilityRaid:
@@ -693,5 +711,40 @@ func (k *Kick) Shoutout(
ctx context.Context, ctx context.Context,
chanID streamcontrol.ChatUserID, chanID streamcontrol.ChatUserID,
) error { ) error {
return fmt.Errorf("not implemented") reply, err := k.ClientOBSOLETE.GetChannelV1(ctx, string(chanID))
if err != nil {
logger.Errorf(ctx, "unable to get channel info ('%s'): %w", chanID, err)
return k.sendShoutoutMessageWithoutChanInfo(ctx, chanID)
}
if len(reply.PreviousLivestreams) == 0 {
return k.sendShoutoutMessageWithoutChanInfo(ctx, chanID)
}
return k.sendShoutoutMessage(ctx, chanID, reply.PreviousLivestreams[0])
}
func (k *Kick) sendShoutoutMessageWithoutChanInfo(
ctx context.Context,
chanID streamcontrol.ChatUserID,
) (_err error) {
logger.Debugf(ctx, "sendShoutoutMessageWithoutChanInfo(ctx, '%s')", chanID)
defer func() { logger.Debugf(ctx, "/sendShoutoutMessageWithoutChanInfo(ctx, '%s'): %v", chanID, _err) }()
err := k.SendChatMessage(ctx, fmt.Sprintf("Shoutout to %s! Great creator! Take a look at their channel and click that follow button! https://www.twitch.tv/%s", chanID, chanID))
if err != nil {
return fmt.Errorf("unable to send the message (case #0): %w", err)
}
return nil
}
func (k *Kick) sendShoutoutMessage(
ctx context.Context,
chanID streamcontrol.ChatUserID,
stream kickcom.LivestreamV1,
) (_err error) {
logger.Debugf(ctx, "sendShoutoutMessage(ctx, '%s')", chanID)
defer func() { logger.Debugf(ctx, "/sendShoutoutMessage(ctx, '%s'): %v", chanID, _err) }()
err := k.SendChatMessage(ctx, fmt.Sprintf("Shoutout to %s! Great creator! Their last stream: '%s'. Take a look at their channel and click that follow button! https://kick.com/%s", chanID, stream.SessionTitle, chanID))
if err != nil {
return fmt.Errorf("unable to send the message (case #1): %w", err)
}
return nil
} }

View File

@@ -939,5 +939,43 @@ func (t *Twitch) Shoutout(
if err != nil { if err != nil {
return fmt.Errorf("unable to send the shoutout (%#+v): %w", params, err) return fmt.Errorf("unable to send the shoutout (%#+v): %w", params, err)
} }
reply, err := t.client.GetStreams(&helix.StreamsParams{
UserIDs: []string{string(chanID)},
})
if err != nil {
logger.Errorf(ctx, "unable to get channel info ('%s'): %w", chanID, err)
return t.sendShoutoutMessageWithoutChanInfo(ctx, chanID)
}
if len(reply.Data.Streams) == 0 {
return t.sendShoutoutMessageWithoutChanInfo(ctx, chanID)
}
return t.sendShoutoutMessage(ctx, chanID, reply.Data.Streams[0])
}
func (t *Twitch) sendShoutoutMessageWithoutChanInfo(
ctx context.Context,
chanID streamcontrol.ChatUserID,
) (_err error) {
logger.Debugf(ctx, "sendShoutoutMessageWithoutChanInfo(ctx, '%s')", chanID)
defer func() { logger.Debugf(ctx, "/sendShoutoutMessageWithoutChanInfo(ctx, '%s'): %v", chanID, _err) }()
err := t.SendChatMessage(ctx, fmt.Sprintf("Shoutout to %s! Great creator! Take a look at their channel and click that follow button! https://www.twitch.tv/%s", chanID, chanID))
if err != nil {
return fmt.Errorf("unable to send the message (case #0): %w", err)
}
return nil
}
func (t *Twitch) sendShoutoutMessage(
ctx context.Context,
chanID streamcontrol.ChatUserID,
stream helix.Stream,
) (_err error) {
logger.Debugf(ctx, "sendShoutoutMessage(ctx, '%s')", chanID)
defer func() { logger.Debugf(ctx, "/sendShoutoutMessage(ctx, '%s'): %v", chanID, _err) }()
err := t.SendChatMessage(ctx, fmt.Sprintf("Shoutout to %s! Great creator! Their last stream: '%s'. Take a look at their channel and click that follow button! https://www.twitch.tv/%s", chanID, stream.Title, chanID))
if err != nil {
return fmt.Errorf("unable to send the message (case #1): %w", err)
}
return nil return nil
} }

View File

@@ -235,6 +235,8 @@ func getAuthCfgBase(cfg Config) *oauth2.Config {
Endpoint: google.Endpoint, Endpoint: google.Endpoint,
Scopes: []string{ Scopes: []string{
"https://www.googleapis.com/auth/youtube", "https://www.googleapis.com/auth/youtube",
"https://www.googleapis.com/auth/youtube.force-ssl",
"https://www.googleapis.com/auth/youtube.upload",
}, },
} }
} }
@@ -1012,8 +1014,10 @@ func (yt *YouTube) startChatListener(
yt.chatListeners[broadcast.Id] = _chatListener yt.chatListeners[broadcast.Id] = _chatListener
return oldListener return oldListener
}) })
if err := oldListener.Close(ctx); err != nil { if oldListener != nil {
logger.Debugf(ctx, "unable to close the old chat listener: %v", err) if err := oldListener.Close(ctx); err != nil {
logger.Debugf(ctx, "unable to close the old chat listener: %v", err)
}
} }
observability.Go(ctx, func(ctx context.Context) { observability.Go(ctx, func(ctx context.Context) {
@@ -1509,9 +1513,9 @@ func (yt *YouTube) IsCapable(
case streamcontrol.CapabilityBanUser: case streamcontrol.CapabilityBanUser:
return false return false
case streamcontrol.CapabilityShoutout: case streamcontrol.CapabilityShoutout:
return false return true
case streamcontrol.CapabilityIsChannelStreaming: case streamcontrol.CapabilityIsChannelStreaming:
return false return true
case streamcontrol.CapabilityRaid: case streamcontrol.CapabilityRaid:
return false return false
} }
@@ -1522,13 +1526,19 @@ func (yt *YouTube) IsChannelStreaming(
ctx context.Context, ctx context.Context,
chanID streamcontrol.ChatUserID, chanID streamcontrol.ChatUserID,
) (bool, error) { ) (bool, error) {
return false, fmt.Errorf("not implemented") resp, err := yt.YouTubeClient.Search(ctx, string(chanID), EventTypeLive, []string{"snippet"})
if err != nil {
return false, fmt.Errorf("unable to search: %w", err)
}
return len(resp.Items) > 0, nil
} }
func (yt *YouTube) RaidTo( func (yt *YouTube) RaidTo(
ctx context.Context, ctx context.Context,
chanID streamcontrol.ChatUserID, chanID streamcontrol.ChatUserID,
) error { ) error {
// https://issuetracker.google.com/issues/408498307?pli=1
return fmt.Errorf("not implemented") return fmt.Errorf("not implemented")
} }
@@ -1536,5 +1546,30 @@ func (yt *YouTube) Shoutout(
ctx context.Context, ctx context.Context,
chanID streamcontrol.ChatUserID, chanID streamcontrol.ChatUserID,
) error { ) error {
return fmt.Errorf("not implemented") resp, err := yt.YouTubeClient.Search(ctx, string(chanID), "", []string{"snippet"})
if err != nil {
logger.Errorf(ctx, "unable to get channel info ('%s'): %w", chanID, err)
return yt.shoutoutWithoutSearch(ctx, chanID)
}
if len(resp.Items) == 0 {
return yt.shoutoutWithoutSearch(ctx, chanID)
}
lastStream := resp.Items[0]
err = yt.SendChatMessage(ctx, fmt.Sprintf("Shoutout to %s! Great creator! Their last stream: '%s'. Take a look at their channel and click that subscribe button! https://www.youtube.com/channel/%s", lastStream.Snippet.ChannelTitle, lastStream.Snippet.Title, chanID))
if err != nil {
return fmt.Errorf("unable to send the message (case #0): %w", err)
}
return nil
}
func (yt *YouTube) shoutoutWithoutSearch(
ctx context.Context,
chanID streamcontrol.ChatUserID,
) error {
err := yt.SendChatMessage(ctx, fmt.Sprintf("Shoutout to a great creator! Take a look at their channel and click that subscribe button! https://www.youtube.com/channel/%s", chanID))
if err != nil {
return fmt.Errorf("unable to send the message (case #1): %w", err)
}
return nil
} }

View File

@@ -54,8 +54,17 @@ type YouTubeClient interface {
InsertCommentThread(ctx context.Context, t *youtube.CommentThread, parts []string) error InsertCommentThread(ctx context.Context, t *youtube.CommentThread, parts []string) error
ListChatMessages(ctx context.Context, chatID string, parts []string) (*youtube.LiveChatMessageListResponse, error) ListChatMessages(ctx context.Context, chatID string, parts []string) (*youtube.LiveChatMessageListResponse, error)
DeleteChatMessage(ctx context.Context, messageID string) error DeleteChatMessage(ctx context.Context, messageID string) error
Search(ctx context.Context, chanID string, eventType EventType, parts []string) (*youtube.SearchListResponse, error)
} }
type EventType string
const (
EventTypeCompleted = EventType("completed")
EventTypeLive = EventType("live")
EventTypeUpcoming = EventType("upcoming")
)
type YouTubeClientV3 struct { type YouTubeClientV3 struct {
*youtube.Service *youtube.Service
RequestWrapper func(context.Context, func(context.Context) error) error RequestWrapper func(context.Context, func(context.Context) error) error
@@ -325,3 +334,21 @@ func (c *YouTubeClientV3) GetLiveChatMessages(
} }
return q.Do() return q.Do()
} }
func (c *YouTubeClientV3) Search(
ctx context.Context,
chanID string,
eventType EventType,
parts []string,
) (_ret *youtube.SearchListResponse, _err error) {
logger.Tracef(ctx, "Search")
defer func() { logger.Tracef(ctx, "/Search: %v", _err) }()
q := c.Service.Search.List(parts).Context(ctx).Order("date").MaxResults(50)
if chanID != "" {
q = q.ChannelId(chanID)
}
if eventType != "" {
q = q.EventType(string(eventType))
}
return q.Do()
}

View File

@@ -218,3 +218,13 @@ func (c *YouTubeClientCalcPoints) GetLiveChatMessages(
defer func() { c.addUsedPointsIfNoError(ctx, 1, _err) }() defer func() { c.addUsedPointsIfNoError(ctx, 1, _err) }()
return c.Client.GetLiveChatMessages(ctx, chatID, pageToken, parts) return c.Client.GetLiveChatMessages(ctx, chatID, pageToken, parts)
} }
func (c *YouTubeClientCalcPoints) Search(
ctx context.Context,
chanID string,
eventType EventType,
parts []string,
) (_ret *youtube.SearchListResponse, _err error) {
defer func() { c.addUsedPointsIfNoError(ctx, 1, _err) }()
return c.Client.Search(ctx, chanID, eventType, parts)
}

View File

@@ -188,3 +188,12 @@ func (c *YouTubeClientMock) GetLiveChatMessages(
defer func() { logger.Tracef(ctx, "/GetLiveChatMessages: %v", _err) }() defer func() { logger.Tracef(ctx, "/GetLiveChatMessages: %v", _err) }()
return nil, fmt.Errorf("not implemented") return nil, fmt.Errorf("not implemented")
} }
func (c *YouTubeClientMock) Search(
ctx context.Context,
chanID string,
eventType EventType,
parts []string,
) (_ret *youtube.SearchListResponse, _err error) {
return nil, fmt.Errorf("not implemented")
}

View File

@@ -49,16 +49,20 @@ func (d *StreamD) startListeningForChatMessages(
if !ok { if !ok {
return return
} }
msg := api.ChatMessage{ func() {
ChatMessage: ev, msg := api.ChatMessage{
IsLive: true, ChatMessage: ev,
Platform: platName, IsLive: true,
} Platform: platName,
if err := d.ChatMessagesStorage.AddMessage(ctx, msg); err != nil { }
logger.Errorf(ctx, "unable to add the message %#+v to the chat messages storage: %v", msg, err) logger.Tracef(ctx, "received chat message: %#+v", msg)
} defer logger.Tracef(ctx, "finished processing the chat message")
publishEvent(ctx, d.EventBus, msg) if err := d.ChatMessagesStorage.AddMessage(ctx, msg); err != nil {
d.shoutoutIfNeeded(ctx, msg) logger.Errorf(ctx, "unable to add the message %#+v to the chat messages storage: %v", msg, err)
}
publishEvent(ctx, d.EventBus, msg)
d.shoutoutIfNeeded(ctx, msg)
}()
} }
} }
}) })
@@ -69,6 +73,8 @@ func (d *StreamD) shoutoutIfNeeded(
ctx context.Context, ctx context.Context,
msg api.ChatMessage, msg api.ChatMessage,
) { ) {
logger.Tracef(ctx, "shoutoutIfNeeded(ctx, %#+v)", msg)
defer logger.Tracef(ctx, "/shoutoutIfNeeded(ctx, %#+v)", msg)
if !msg.IsLive { if !msg.IsLive {
logger.Tracef(ctx, "is not a live message") logger.Tracef(ctx, "is not a live message")
return return
@@ -82,6 +88,7 @@ func (d *StreamD) shoutoutIfNeeded(
User: streamcontrol.ChatUserID(strings.ToLower(string(msg.UserID))), User: streamcontrol.ChatUserID(strings.ToLower(string(msg.UserID))),
} }
lastShoutoutAt := d.lastShoutoutAt[userID] lastShoutoutAt := d.lastShoutoutAt[userID]
logger.Tracef(ctx, "lastShoutoutAt(%#+v): %v", userID, lastShoutoutAt)
if v := time.Since(lastShoutoutAt); v < time.Hour { if v := time.Since(lastShoutoutAt); v < time.Hour {
logger.Tracef(ctx, "the previous shoutout was too soon: %v < %v", v, time.Hour) logger.Tracef(ctx, "the previous shoutout was too soon: %v < %v", v, time.Hour)
return return
@@ -121,6 +128,9 @@ func (d *StreamD) shoutoutIfCan(
platID streamcontrol.PlatformName, platID streamcontrol.PlatformName,
userID streamcontrol.ChatUserID, userID streamcontrol.ChatUserID,
) { ) {
logger.Tracef(ctx, "shoutoutIfCan('%s', '%s')", platID, userID)
defer logger.Tracef(ctx, "/shoutoutIfCan('%s', '%s')", platID, userID)
ctrl, err := d.streamController(ctx, platID) ctrl, err := d.streamController(ctx, platID)
if err != nil { if err != nil {
logger.Errorf(ctx, "unable to get a stream controller '%s': %v", platID, err) logger.Errorf(ctx, "unable to get a stream controller '%s': %v", platID, err)
@@ -137,6 +147,11 @@ func (d *StreamD) shoutoutIfCan(
logger.Errorf(ctx, "unable to shoutout '%s' at '%s': %v", userID, platID, err) logger.Errorf(ctx, "unable to shoutout '%s' at '%s': %v", userID, platID, err)
return return
} }
userFullID := config.ChatUserID{
Platform: platID,
User: userID,
}
d.lastShoutoutAt[userFullID] = time.Now()
} }
func (d *StreamD) RemoveChatMessage( func (d *StreamD) RemoveChatMessage(

View File

@@ -2657,7 +2657,7 @@ func (c *Client) SubmitEvent(
) )
}) })
if err != nil { if err != nil {
return fmt.Errorf("unable to submit the event: %w", err) return fmt.Errorf("unable to query: %w", err)
} }
return nil return nil
} }
@@ -2714,7 +2714,7 @@ func (c *Client) RemoveChatMessage(
) )
}) })
if err != nil { if err != nil {
return fmt.Errorf("unable to submit the event: %w", err) return fmt.Errorf("unable to query: %w", err)
} }
return nil return nil
} }
@@ -2747,7 +2747,7 @@ func (c *Client) BanUser(
) )
}) })
if err != nil { if err != nil {
return fmt.Errorf("unable to submit the event: %w", err) return fmt.Errorf("unable to query: %w", err)
} }
return nil return nil
} }
@@ -2773,7 +2773,7 @@ func (c *Client) SendChatMessage(
) )
}) })
if err != nil { if err != nil {
return fmt.Errorf("unable to submit the event: %w", err) return fmt.Errorf("unable to query: %w", err)
} }
return nil return nil
} }
@@ -2799,7 +2799,7 @@ func (c *Client) Shoutout(
) )
}) })
if err != nil { if err != nil {
return fmt.Errorf("unable to submit the event: %w", err) return fmt.Errorf("unable to query: %w", err)
} }
return nil return nil
} }
@@ -2825,7 +2825,7 @@ func (c *Client) RaidTo(
) )
}) })
if err != nil { if err != nil {
return fmt.Errorf("unable to submit the event: %w", err) return fmt.Errorf("unable to query: %w", err)
} }
return nil return nil
} }
@@ -2879,7 +2879,7 @@ func (c *Client) GetPeerIDs(ctx context.Context) ([]p2ptypes.PeerID, error) {
) )
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to submit the event: %w", err) return nil, fmt.Errorf("unable to query: %w", err)
} }
r := make([]p2ptypes.PeerID, 0, len(resp.GetPeerIDs())) r := make([]p2ptypes.PeerID, 0, len(resp.GetPeerIDs()))
@@ -2915,7 +2915,7 @@ func (c *Client) LLMGenerate(
) )
}) })
if err != nil { if err != nil {
return "", fmt.Errorf("unable to submit the event: %w", err) return "", fmt.Errorf("unable to query: %w", err)
} }
return resp.GetResponse(), nil return resp.GetResponse(), nil

View File

@@ -1133,6 +1133,8 @@ func (p *Panel) getUpdatedStatus_backends(ctx context.Context) {
}) })
} }
func (p *Panel) getUpdatedStatus_backends_noLock(ctx context.Context) { func (p *Panel) getUpdatedStatus_backends_noLock(ctx context.Context) {
logger.Tracef(ctx, "getUpdatedStatus_backends_noLock")
defer logger.Tracef(ctx, "/getUpdatedStatus_backends_noLock")
backendEnabled := map[streamcontrol.PlatformName]bool{} backendEnabled := map[streamcontrol.PlatformName]bool{}
for _, backendID := range []streamcontrol.PlatformName{ for _, backendID := range []streamcontrol.PlatformName{
obs.ID, obs.ID,
@@ -1206,6 +1208,9 @@ func (p *Panel) getUpdatedStatus_startStopStreamButton(ctx context.Context) {
} }
func (p *Panel) getUpdatedStatus_startStopStreamButton_noLock(ctx context.Context) { func (p *Panel) getUpdatedStatus_startStopStreamButton_noLock(ctx context.Context) {
logger.Debugf(ctx, "getUpdatedStatus_startStopStreamButton_noLock")
defer logger.Debugf(ctx, "/getUpdatedStatus_startStopStreamButton_noLock")
obsIsEnabled, _ := p.StreamD.IsBackendEnabled(ctx, obs.ID) obsIsEnabled, _ := p.StreamD.IsBackendEnabled(ctx, obs.ID)
if obsIsEnabled { if obsIsEnabled {
obsStreamStatus, err := p.StreamD.GetStreamStatus(ctx, obs.ID) obsStreamStatus, err := p.StreamD.GetStreamStatus(ctx, obs.ID)
@@ -1432,8 +1437,7 @@ func (p *Panel) initMainWindow(
return return
} }
p.startStopButton.OnTapped() p.setupStreamButton.OnTapped()
p.startStopButton.OnTapped()
} }
p.streamTitleLabel = widget.NewLabel("") p.streamTitleLabel = widget.NewLabel("")
p.streamTitleLabel.Wrapping = fyne.TextWrapWord p.streamTitleLabel.Wrapping = fyne.TextWrapWord
@@ -1472,8 +1476,7 @@ func (p *Panel) initMainWindow(
return return
} }
p.startStopButton.OnTapped() p.setupStreamButton.OnTapped()
p.startStopButton.OnTapped()
} }
p.streamDescriptionLabel = widget.NewLabel("") p.streamDescriptionLabel = widget.NewLabel("")
streamDescriptionButton := widget.NewButtonWithIcon("", theme.SettingsIcon(), func() { streamDescriptionButton := widget.NewButtonWithIcon("", theme.SettingsIcon(), func() {
@@ -1516,7 +1519,7 @@ func (p *Panel) initMainWindow(
p.twitchCheck.Disable() p.twitchCheck.Disable()
p.kickCheck = widget.NewCheck("Kick", nil) p.kickCheck = widget.NewCheck("Kick", nil)
p.kickCheck.SetChecked(false) p.kickCheck.SetChecked(true)
p.kickCheck.Disable() p.kickCheck.Disable()
p.youtubeCheck = widget.NewCheck("YouTube", nil) p.youtubeCheck = widget.NewCheck("YouTube", nil)
@@ -1939,9 +1942,10 @@ func (p *Panel) subscribeUpdateControlPage(ctx context.Context) {
t := time.NewTicker(time.Second * 5) t := time.NewTicker(time.Second * 5)
defer t.Stop() defer t.Stop()
for { for {
var ok bool ok := true
select { select {
case <-ctx.Done(): case <-ctx.Done():
logger.Debugf(ctx, "subscribeUpdateControlPage: context closed")
return return
case _, ok = <-chStreams: case _, ok = <-chStreams:
case _, ok = <-restartChStreams: case _, ok = <-restartChStreams:
@@ -1950,7 +1954,7 @@ func (p *Panel) subscribeUpdateControlPage(ctx context.Context) {
case <-t.C: case <-t.C:
} }
if !ok { if !ok {
return logger.Debugf(ctx, "subscribeUpdateControlPage: channel closed")
} }
p.getUpdatedStatus(ctx) p.getUpdatedStatus(ctx)
} }
@@ -2117,6 +2121,12 @@ func (p *Panel) setupStreamNoLock(ctx context.Context) {
deadline := time.Now().Add(waitFor) deadline := time.Now().Add(waitFor)
p.streamMutex.Do(ctx, func() { p.streamMutex.Do(ctx, func() {
defer func(){
p.startStopButton.SetText(startStreamString())
p.startStopButton.Icon = theme.MediaRecordIcon()
p.startStopButton.Importance = widget.SuccessImportance
p.startStopButton.Enable()
}()
p.startStopButton.Disable() p.startStopButton.Disable()
p.startStopButton.Icon = theme.ViewRefreshIcon() p.startStopButton.Icon = theme.ViewRefreshIcon()
p.startStopButton.Importance = widget.DangerImportance p.startStopButton.Importance = widget.DangerImportance