mirror of
https://github.com/xaionaro-go/streamctl.git
synced 2025-10-19 13:54:48 +08:00
Initial commit, pt. 6
This commit is contained in:
@@ -3,10 +3,26 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"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"
|
"github.com/xaionaro-go/streamctl/pkg/streampanel"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
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)
|
logger.Debugf(ctx, "config '%s' was not found in cfg: %#+v", id, cfg)
|
||||||
return nil
|
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)
|
platCfgCfg, ok := platCfg.Config.(*T)
|
||||||
if !ok {
|
if !ok {
|
||||||
var zeroValue T
|
var zeroValue T
|
||||||
logger.Errorf(ctx, "unable to get the config: expected type '%T', but received type '%T'", zeroValue, platCfg.Config)
|
logger.Errorf(ctx, "unable to get the config: expected type '%T', but received type '%T'", zeroValue, platCfg.Config)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &PlatformConfig[T, S]{
|
return &PlatformConfig[T, S]{
|
||||||
Config: *platCfgCfg,
|
Config: *platCfgCfg,
|
||||||
StreamProfiles: GetStreamProfiles[S](platCfg.StreamProfiles),
|
StreamProfiles: GetStreamProfiles[S](platCfg.StreamProfiles),
|
||||||
|
@@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -33,10 +35,15 @@ type Profile struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Panel struct {
|
type Panel struct {
|
||||||
|
saveConfigLock sync.Mutex
|
||||||
startStopMutex sync.Mutex
|
startStopMutex sync.Mutex
|
||||||
updateTimerHandler *updateTimerHandler
|
updateTimerHandler *updateTimerHandler
|
||||||
panelConfig config
|
panelConfig config
|
||||||
streamConfig streamcontrol.Config
|
streamConfig streamConfig
|
||||||
|
streamControllers struct {
|
||||||
|
Twitch *twitch.Twitch
|
||||||
|
YouTube *youtube.YouTube
|
||||||
|
}
|
||||||
profilesOrder []streamcontrol.ProfileName
|
profilesOrder []streamcontrol.ProfileName
|
||||||
profilesOrderFiltered []streamcontrol.ProfileName
|
profilesOrderFiltered []streamcontrol.ProfileName
|
||||||
selectedProfileName *streamcontrol.ProfileName
|
selectedProfileName *streamcontrol.ProfileName
|
||||||
@@ -68,6 +75,14 @@ func (p *Panel) Loop(ctx context.Context) error {
|
|||||||
p.defaultContext = ctx
|
p.defaultContext = ctx
|
||||||
logger.Debug(ctx, "config", p.panelConfig)
|
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()
|
a := fyneapp.New()
|
||||||
p.initMainWindow(ctx, a)
|
p.initMainWindow(ctx, a)
|
||||||
|
|
||||||
@@ -75,32 +90,82 @@ func (p *Panel) Loop(ctx context.Context) error {
|
|||||||
return nil
|
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) {
|
func (p *Panel) onProfileCreatedOrUpdated(profile Profile) {
|
||||||
logger.Trace(p.defaultContext, "onProfileCreatedOrUpdated(%s)", profile.Name)
|
logger.Trace(p.defaultContext, "onProfileCreatedOrUpdated(%s)", profile.Name)
|
||||||
for platformName, platformProfile := range profile.PerPlatform {
|
for platformName, platformProfile := range profile.PerPlatform {
|
||||||
p.streamConfig[platformName].StreamProfiles[profile.Name] = platformProfile
|
p.streamConfig.ControllersConfig[platformName].StreamProfiles[profile.Name] = platformProfile
|
||||||
}
|
}
|
||||||
p.rearrangeProfiles()
|
p.rearrangeProfiles()
|
||||||
p.saveConfig()
|
p.saveConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Panel) onProfileDeleted(profileName streamcontrol.ProfileName) {
|
func (p *Panel) onProfileDeleted(profileName streamcontrol.ProfileName) {
|
||||||
for platformName := range p.streamConfig {
|
for platformName := range p.streamConfig.ControllersConfig {
|
||||||
delete(p.streamConfig[platformName].StreamProfiles, profileName)
|
delete(p.streamConfig.ControllersConfig[platformName].StreamProfiles, profileName)
|
||||||
}
|
}
|
||||||
p.rearrangeProfiles()
|
p.rearrangeProfiles()
|
||||||
p.saveConfig()
|
p.saveConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Panel) saveConfig() {
|
func (p *Panel) saveConfig() error {
|
||||||
panic("not implemented")
|
return writeConfigToPath(p.defaultContext, p.configPath, p.streamConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Panel) getProfile(profileName streamcontrol.ProfileName) Profile {
|
func (p *Panel) getProfile(profileName streamcontrol.ProfileName) Profile {
|
||||||
prof := Profile{
|
prof := Profile{
|
||||||
Name: profileName,
|
Name: profileName,
|
||||||
}
|
}
|
||||||
for platName, platCfg := range p.streamConfig {
|
for platName, platCfg := range p.streamConfig.ControllersConfig {
|
||||||
platProf, ok := platCfg.GetStreamProfile(profileName)
|
platProf, ok := platCfg.GetStreamProfile(profileName)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
@@ -111,8 +176,8 @@ func (p *Panel) getProfile(profileName streamcontrol.ProfileName) Profile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Panel) rearrangeProfiles() {
|
func (p *Panel) rearrangeProfiles() {
|
||||||
var curProfilesMap map[streamcontrol.ProfileName]*Profile
|
curProfilesMap := map[streamcontrol.ProfileName]*Profile{}
|
||||||
for platName, platCfg := range p.streamConfig {
|
for platName, platCfg := range p.streamConfig.ControllersConfig {
|
||||||
for profName, platProf := range platCfg.StreamProfiles {
|
for profName, platProf := range platCfg.StreamProfiles {
|
||||||
prof := curProfilesMap[profName]
|
prof := curProfilesMap[profName]
|
||||||
if prof == nil {
|
if prof == nil {
|
||||||
@@ -165,7 +230,7 @@ func (p *Panel) refilterProfiles() {
|
|||||||
for _, profileName := range p.profilesOrder {
|
for _, profileName := range p.profilesOrder {
|
||||||
titleMatch := strings.Contains(strings.ToLower(string(profileName)), filterValue)
|
titleMatch := strings.Contains(strings.ToLower(string(profileName)), filterValue)
|
||||||
subValueMatch := false
|
subValueMatch := false
|
||||||
for _, platCfg := range p.streamConfig {
|
for _, platCfg := range p.streamConfig.ControllersConfig {
|
||||||
prof, ok := platCfg.GetStreamProfile(profileName)
|
prof, ok := platCfg.GetStreamProfile(profileName)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
@@ -230,7 +295,7 @@ func (p *Panel) profilesListItemUpdate(
|
|||||||
profileName := streamcontrol.ProfileName(p.profilesOrderFiltered[itemID])
|
profileName := streamcontrol.ProfileName(p.profilesOrderFiltered[itemID])
|
||||||
profile := p.getProfile(profileName)
|
profile := p.getProfile(profileName)
|
||||||
|
|
||||||
w.SetText(fmt.Sprintf("%s", profile.Name))
|
w.SetText(string(profile.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
func ptrCopy[T any](v T) *T {
|
func ptrCopy[T any](v T) *T {
|
||||||
@@ -330,13 +395,14 @@ func (p *Panel) onStartStopButton() {
|
|||||||
p.startStopButton.Refresh()
|
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 := a.NewWindow("Create a profile")
|
||||||
w.Resize(fyne.NewSize(400, 300))
|
w.Resize(fyne.NewSize(400, 300))
|
||||||
activityTitle := widget.NewEntry()
|
activityTitle := widget.NewEntry()
|
||||||
activityTitle.SetPlaceHolder("title")
|
activityTitle.SetPlaceHolder("title")
|
||||||
activityDescription := widget.NewMultiLineEntry()
|
activityDescription := widget.NewMultiLineEntry()
|
||||||
activityDescription.SetPlaceHolder("description")
|
activityDescription.SetPlaceHolder("description")
|
||||||
|
twitchCategory := widget.NewSelectEntry()
|
||||||
tagsEntryField := widget.NewEntry()
|
tagsEntryField := widget.NewEntry()
|
||||||
tagsEntryField.SetPlaceHolder("add a tag")
|
tagsEntryField.SetPlaceHolder("add a tag")
|
||||||
s := tagsEntryField.Size()
|
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