From e68174a7a05ae9608eceb81ada438c5e4cd2bc9a Mon Sep 17 00:00:00 2001 From: Dmitrii Okunev Date: Sat, 20 Apr 2024 21:49:00 +0100 Subject: [PATCH] Initial commit, pt. 6 --- cmd/streampanel/main.go | 18 +++++- pkg/streamcontrol/config.go | 10 +++ pkg/streampanel/panel.go | 96 +++++++++++++++++++++++----- pkg/streampanel/stream_config.go | 86 +++++++++++++++++++++++++ pkg/streampanel/stream_controller.go | 63 ++++++++++++++++++ 5 files changed, 257 insertions(+), 16 deletions(-) create mode 100644 pkg/streampanel/stream_config.go create mode 100644 pkg/streampanel/stream_controller.go diff --git a/cmd/streampanel/main.go b/cmd/streampanel/main.go index 0421b48..c3048c9 100644 --- a/cmd/streampanel/main.go +++ b/cmd/streampanel/main.go @@ -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)) } diff --git a/pkg/streamcontrol/config.go b/pkg/streamcontrol/config.go index 9e8ce38..99f6e1f 100644 --- a/pkg/streamcontrol/config.go +++ b/pkg/streamcontrol/config.go @@ -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), diff --git a/pkg/streampanel/panel.go b/pkg/streampanel/panel.go index 72ef4d8..13ab7bf 100644 --- a/pkg/streampanel/panel.go +++ b/pkg/streampanel/panel.go @@ -4,6 +4,8 @@ import ( "context" _ "embed" "fmt" + "os" + "path" "runtime/debug" "sort" "strings" @@ -33,10 +35,15 @@ type Profile struct { } type Panel struct { - startStopMutex sync.Mutex - updateTimerHandler *updateTimerHandler - panelConfig config - streamConfig streamcontrol.Config + saveConfigLock sync.Mutex + startStopMutex sync.Mutex + updateTimerHandler *updateTimerHandler + panelConfig 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() diff --git a/pkg/streampanel/stream_config.go b/pkg/streampanel/stream_config.go new file mode 100644 index 0000000..5fc4b26 --- /dev/null +++ b/pkg/streampanel/stream_config.go @@ -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 +} diff --git a/pkg/streampanel/stream_controller.go b/pkg/streampanel/stream_controller.go new file mode 100644 index 0000000..d21bfa9 --- /dev/null +++ b/pkg/streampanel/stream_controller.go @@ -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), + }) + }, + ) +}