mirror of
https://github.com/xaionaro-go/streamctl.git
synced 2025-10-19 05:44:39 +08:00
Initial commit, pt. 6
This commit is contained in:
@@ -3,10 +3,26 @@ package main
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/facebookincubator/go-belt"
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
"github.com/facebookincubator/go-belt/tool/logger/implementation/zap"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streampanel"
|
||||
)
|
||||
|
||||
func main() {
|
||||
streampanel.New("/tmp/test.yaml").Loop(context.Background())
|
||||
loggerLevel := logger.LevelWarning
|
||||
pflag.Var(&loggerLevel, "log-level", "Log level")
|
||||
configPath := pflag.String("config-path", "~/.streampanel.yaml", "the path to the config file")
|
||||
pflag.Parse()
|
||||
|
||||
l := zap.Default().WithLevel(loggerLevel)
|
||||
ctx := context.Background()
|
||||
ctx = logger.CtxWithLogger(ctx, l)
|
||||
logger.Default = func() logger.Logger {
|
||||
return l
|
||||
}
|
||||
defer belt.Flush(ctx)
|
||||
|
||||
l.Fatal(streampanel.New(*configPath).Loop(ctx))
|
||||
}
|
||||
|
@@ -126,12 +126,22 @@ func GetPlatformConfig[T any, S StreamProfile](
|
||||
logger.Debugf(ctx, "config '%s' was not found in cfg: %#+v", id, cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
return ConvertPlatformConfig[T, S](ctx, platCfg, id)
|
||||
}
|
||||
|
||||
func ConvertPlatformConfig[T any, S StreamProfile](
|
||||
ctx context.Context,
|
||||
platCfg *AbstractPlatformConfig,
|
||||
id PlatformName,
|
||||
) *PlatformConfig[T, S] {
|
||||
platCfgCfg, ok := platCfg.Config.(*T)
|
||||
if !ok {
|
||||
var zeroValue T
|
||||
logger.Errorf(ctx, "unable to get the config: expected type '%T', but received type '%T'", zeroValue, platCfg.Config)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &PlatformConfig[T, S]{
|
||||
Config: *platCfgCfg,
|
||||
StreamProfiles: GetStreamProfiles[S](platCfg.StreamProfiles),
|
||||
|
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -33,10 +35,15 @@ type Profile struct {
|
||||
}
|
||||
|
||||
type Panel struct {
|
||||
saveConfigLock sync.Mutex
|
||||
startStopMutex sync.Mutex
|
||||
updateTimerHandler *updateTimerHandler
|
||||
panelConfig config
|
||||
streamConfig streamcontrol.Config
|
||||
streamConfig streamConfig
|
||||
streamControllers struct {
|
||||
Twitch *twitch.Twitch
|
||||
YouTube *youtube.YouTube
|
||||
}
|
||||
profilesOrder []streamcontrol.ProfileName
|
||||
profilesOrderFiltered []streamcontrol.ProfileName
|
||||
selectedProfileName *streamcontrol.ProfileName
|
||||
@@ -68,6 +75,14 @@ func (p *Panel) Loop(ctx context.Context) error {
|
||||
p.defaultContext = ctx
|
||||
logger.Debug(ctx, "config", p.panelConfig)
|
||||
|
||||
if err := p.initStreamControllers(); err != nil {
|
||||
return fmt.Errorf("unable to initialize stream controllers: %w", err)
|
||||
}
|
||||
|
||||
if err := p.loadConfig(); err != nil {
|
||||
return fmt.Errorf("unable to load the config '%s': %w", p.configPath, err)
|
||||
}
|
||||
|
||||
a := fyneapp.New()
|
||||
p.initMainWindow(ctx, a)
|
||||
|
||||
@@ -75,32 +90,82 @@ func (p *Panel) Loop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func expandPath(rawPath string) (string, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(rawPath, "~/"):
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to get user home dir: %w", err)
|
||||
}
|
||||
return path.Join(homeDir, rawPath[2:]), nil
|
||||
}
|
||||
return rawPath, nil
|
||||
}
|
||||
|
||||
func (p *Panel) getExpandedConfigPath() (string, error) {
|
||||
return expandPath(p.configPath)
|
||||
}
|
||||
|
||||
func (p *Panel) loadConfig() error {
|
||||
return readConfigFromPath(p.defaultContext, p.configPath, &p.streamConfig)
|
||||
}
|
||||
|
||||
func (p *Panel) savePlatformConfig(
|
||||
platID streamcontrol.PlatformName,
|
||||
platCfg *streamcontrol.AbstractPlatformConfig,
|
||||
) error {
|
||||
p.saveConfigLock.Lock()
|
||||
defer p.saveConfigLock.Unlock()
|
||||
p.streamConfig.ControllersConfig[platID] = platCfg
|
||||
return p.saveConfig()
|
||||
}
|
||||
|
||||
func (p *Panel) initStreamControllers() error {
|
||||
for platName, cfg := range p.streamConfig.ControllersConfig {
|
||||
var err error
|
||||
switch strings.ToLower(string(platName)) {
|
||||
case strings.ToLower(string(twitch.ID)):
|
||||
p.streamControllers.Twitch, err = newTwitch(p.defaultContext, cfg, func(cfg *streamcontrol.AbstractPlatformConfig) error {
|
||||
return p.savePlatformConfig(twitch.ID, cfg)
|
||||
})
|
||||
case strings.ToLower(string(youtube.ID)):
|
||||
p.streamControllers.YouTube, err = newYouTube(p.defaultContext, cfg, func(cfg *streamcontrol.AbstractPlatformConfig) error {
|
||||
return p.savePlatformConfig(youtube.ID, cfg)
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to initialize '%s': %w", platName, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Panel) onProfileCreatedOrUpdated(profile Profile) {
|
||||
logger.Trace(p.defaultContext, "onProfileCreatedOrUpdated(%s)", profile.Name)
|
||||
for platformName, platformProfile := range profile.PerPlatform {
|
||||
p.streamConfig[platformName].StreamProfiles[profile.Name] = platformProfile
|
||||
p.streamConfig.ControllersConfig[platformName].StreamProfiles[profile.Name] = platformProfile
|
||||
}
|
||||
p.rearrangeProfiles()
|
||||
p.saveConfig()
|
||||
}
|
||||
|
||||
func (p *Panel) onProfileDeleted(profileName streamcontrol.ProfileName) {
|
||||
for platformName := range p.streamConfig {
|
||||
delete(p.streamConfig[platformName].StreamProfiles, profileName)
|
||||
for platformName := range p.streamConfig.ControllersConfig {
|
||||
delete(p.streamConfig.ControllersConfig[platformName].StreamProfiles, profileName)
|
||||
}
|
||||
p.rearrangeProfiles()
|
||||
p.saveConfig()
|
||||
}
|
||||
|
||||
func (p *Panel) saveConfig() {
|
||||
panic("not implemented")
|
||||
func (p *Panel) saveConfig() error {
|
||||
return writeConfigToPath(p.defaultContext, p.configPath, p.streamConfig)
|
||||
}
|
||||
|
||||
func (p *Panel) getProfile(profileName streamcontrol.ProfileName) Profile {
|
||||
prof := Profile{
|
||||
Name: profileName,
|
||||
}
|
||||
for platName, platCfg := range p.streamConfig {
|
||||
for platName, platCfg := range p.streamConfig.ControllersConfig {
|
||||
platProf, ok := platCfg.GetStreamProfile(profileName)
|
||||
if !ok {
|
||||
continue
|
||||
@@ -111,8 +176,8 @@ func (p *Panel) getProfile(profileName streamcontrol.ProfileName) Profile {
|
||||
}
|
||||
|
||||
func (p *Panel) rearrangeProfiles() {
|
||||
var curProfilesMap map[streamcontrol.ProfileName]*Profile
|
||||
for platName, platCfg := range p.streamConfig {
|
||||
curProfilesMap := map[streamcontrol.ProfileName]*Profile{}
|
||||
for platName, platCfg := range p.streamConfig.ControllersConfig {
|
||||
for profName, platProf := range platCfg.StreamProfiles {
|
||||
prof := curProfilesMap[profName]
|
||||
if prof == nil {
|
||||
@@ -165,7 +230,7 @@ func (p *Panel) refilterProfiles() {
|
||||
for _, profileName := range p.profilesOrder {
|
||||
titleMatch := strings.Contains(strings.ToLower(string(profileName)), filterValue)
|
||||
subValueMatch := false
|
||||
for _, platCfg := range p.streamConfig {
|
||||
for _, platCfg := range p.streamConfig.ControllersConfig {
|
||||
prof, ok := platCfg.GetStreamProfile(profileName)
|
||||
if !ok {
|
||||
continue
|
||||
@@ -230,7 +295,7 @@ func (p *Panel) profilesListItemUpdate(
|
||||
profileName := streamcontrol.ProfileName(p.profilesOrderFiltered[itemID])
|
||||
profile := p.getProfile(profileName)
|
||||
|
||||
w.SetText(fmt.Sprintf("%s", profile.Name))
|
||||
w.SetText(string(profile.Name))
|
||||
}
|
||||
|
||||
func ptrCopy[T any](v T) *T {
|
||||
@@ -330,13 +395,14 @@ func (p *Panel) onStartStopButton() {
|
||||
p.startStopButton.Refresh()
|
||||
}
|
||||
|
||||
func (p *Panel) newProfileWindow(ctx context.Context, a fyne.App) fyne.Window {
|
||||
func (p *Panel) newProfileWindow(_ context.Context, a fyne.App) fyne.Window {
|
||||
w := a.NewWindow("Create a profile")
|
||||
w.Resize(fyne.NewSize(400, 300))
|
||||
activityTitle := widget.NewEntry()
|
||||
activityTitle.SetPlaceHolder("title")
|
||||
activityDescription := widget.NewMultiLineEntry()
|
||||
activityDescription.SetPlaceHolder("description")
|
||||
twitchCategory := widget.NewSelectEntry()
|
||||
tagsEntryField := widget.NewEntry()
|
||||
tagsEntryField.SetPlaceHolder("add a tag")
|
||||
s := tagsEntryField.Size()
|
||||
|
86
pkg/streampanel/stream_config.go
Normal file
86
pkg/streampanel/stream_config.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package streampanel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/twitch"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube"
|
||||
)
|
||||
|
||||
type streamConfig struct {
|
||||
ControllersConfig streamcontrol.Config
|
||||
}
|
||||
|
||||
func newStreamConfig() streamConfig {
|
||||
cfg := streamcontrol.Config{}
|
||||
twitch.InitConfig(cfg)
|
||||
youtube.InitConfig(cfg)
|
||||
return streamConfig{
|
||||
ControllersConfig: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func newSampleStreamConfig(cmd *cobra.Command, args []string) streamConfig {
|
||||
cfg := newStreamConfig()
|
||||
cfg.ControllersConfig[twitch.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": twitch.StreamProfile{}}
|
||||
cfg.ControllersConfig[youtube.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": youtube.StreamProfile{}}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func readConfigFromPath(
|
||||
ctx context.Context,
|
||||
cfgPath string,
|
||||
cfg *streamConfig,
|
||||
) error {
|
||||
b, err := os.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read file '%s': %w", cfgPath, err)
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(b, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to unserialize config: %w: <%s>", err, b)
|
||||
}
|
||||
|
||||
err = streamcontrol.ConvertStreamProfiles[twitch.StreamProfile](ctx, cfg.ControllersConfig[twitch.ID].StreamProfiles)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to convert stream profiles of twitch: %w: <%s>", err, b)
|
||||
}
|
||||
logger.Debugf(ctx, "final stream profiles of twitch: %#+v", cfg.ControllersConfig[twitch.ID].StreamProfiles)
|
||||
|
||||
err = streamcontrol.ConvertStreamProfiles[youtube.StreamProfile](ctx, cfg.ControllersConfig[youtube.ID].StreamProfiles)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to convert stream profiles of twitch: %w: <%s>", err, b)
|
||||
}
|
||||
logger.Debugf(ctx, "final stream profiles of youtube: %#+v", cfg.ControllersConfig[youtube.ID].StreamProfiles)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeConfigToPath(
|
||||
ctx context.Context,
|
||||
cfgPath string,
|
||||
cfg streamConfig,
|
||||
) error {
|
||||
b, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to serialize config %#+v: %w", cfg, err)
|
||||
}
|
||||
pathNew := cfgPath + ".new"
|
||||
err = os.WriteFile(pathNew, b, 0750)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write config to file '%s': %w", pathNew, err)
|
||||
}
|
||||
err = os.Rename(pathNew, cfgPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot move '%s' to '%s': %w", pathNew, cfgPath, err)
|
||||
}
|
||||
logger.Infof(ctx, "wrote to '%s' config <%s>", cfgPath, b)
|
||||
return nil
|
||||
}
|
63
pkg/streampanel/stream_controller.go
Normal file
63
pkg/streampanel/stream_controller.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package streampanel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/facebookincubator/go-belt/tool/logger"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/twitch"
|
||||
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube"
|
||||
)
|
||||
|
||||
func newTwitch(
|
||||
ctx context.Context,
|
||||
cfg *streamcontrol.AbstractPlatformConfig,
|
||||
saveCfgFunc func(*streamcontrol.AbstractPlatformConfig) error,
|
||||
) (
|
||||
*twitch.Twitch,
|
||||
error,
|
||||
) {
|
||||
platCfg := streamcontrol.ConvertPlatformConfig[twitch.PlatformSpecificConfig, twitch.StreamProfile](
|
||||
ctx, cfg, twitch.ID,
|
||||
)
|
||||
if platCfg == nil {
|
||||
return nil, fmt.Errorf("twitch config was not found")
|
||||
}
|
||||
|
||||
logger.Debugf(ctx, "twitch config: %#+v", platCfg)
|
||||
return twitch.New(ctx, *platCfg,
|
||||
func(c twitch.Config) error {
|
||||
return saveCfgFunc(&streamcontrol.AbstractPlatformConfig{
|
||||
Config: c.Config,
|
||||
StreamProfiles: streamcontrol.ToAbstractStreamProfiles(c.StreamProfiles),
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func newYouTube(
|
||||
ctx context.Context,
|
||||
cfg *streamcontrol.AbstractPlatformConfig,
|
||||
saveCfgFunc func(*streamcontrol.AbstractPlatformConfig) error,
|
||||
) (
|
||||
*youtube.YouTube,
|
||||
error,
|
||||
) {
|
||||
platCfg := streamcontrol.ConvertPlatformConfig[youtube.PlatformSpecificConfig, youtube.StreamProfile](
|
||||
ctx, cfg, twitch.ID,
|
||||
)
|
||||
if platCfg == nil {
|
||||
return nil, fmt.Errorf("youtube config was not found")
|
||||
}
|
||||
|
||||
logger.Debugf(ctx, "youtube config: %#+v", platCfg)
|
||||
return youtube.New(ctx, *platCfg,
|
||||
func(c youtube.Config) error {
|
||||
return saveCfgFunc(&streamcontrol.AbstractPlatformConfig{
|
||||
Config: c.Config,
|
||||
StreamProfiles: streamcontrol.ToAbstractStreamProfiles(c.StreamProfiles),
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user