You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
schnutibox/cmd/root.go

182 lines
5.4 KiB
Go

//nolint:exhaustivestruct,gochecknoglobals,gochecknoinits,gomnd
package cmd
import (
"fmt"
"os"
"strings"
"time"
"github.com/fsnotify/fsnotify"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"go.xsfx.dev/schnutibox/internal/config"
"go.xsfx.dev/schnutibox/pkg/prepare"
)
var cfgFile string
var rootCmd = &cobra.Command{
Use: "schnutibox",
Run: func(cmd *cobra.Command, args []string) {
if err := cmd.Usage(); err != nil {
log.Error().Msg(err.Error())
}
},
}
// init initializes the command line interface.
//
// nolint:funlen
func init() {
// Root.
rootCmd.PersistentFlags().Bool("pprof", false, "Enables pprof for debugging")
// Run.
rootCmd.AddCommand(runCmd)
runCmd.Flags().StringVarP(&cfgFile, "config", "c", "", "config file")
if err := runCmd.MarkFlagRequired("config"); err != nil {
log.Fatal().Err(err).Msg("missing flag")
}
runCmd.Flags().Bool("ignore-reader", false, "Ignoring that the reader is missing")
// Prepare.
rootCmd.AddCommand(prepareCmd)
prepareCmd.Flags().BoolVar(&prepare.Cfg.ReadOnly, "read-only", false, "Setup read-only system")
prepareCmd.Flags().StringVarP(&prepare.Cfg.System, "system", "s", "raspbian", "Which kind of system to prepare")
prepareCmd.Flags().StringVar(&prepare.Cfg.SpotifyUsername, "spotify-username", "", "Spotify username")
prepareCmd.Flags().StringVar(&prepare.Cfg.SpotifyPassword, "spotify-password", "", "Spotify password")
prepareCmd.Flags().StringVar(&prepare.Cfg.SpotifyClientID, "spotify-client-id", "", "Spotify client ID")
prepareCmd.Flags().StringVar(&prepare.Cfg.SpotifyClientSecret, "spotify-client-secret", "", "Spotify client secret")
prepareCmd.Flags().StringVar(&prepare.Cfg.RFIDReader, "rfid-reader", "/dev/hidraw0", "dev path of rfid reader")
prepareCmd.Flags().StringVar(&prepare.Cfg.StopID, "stop-id", "", "ID of stop tag")
// Version.
rootCmd.AddCommand(versionCmd)
// Web.
rootCmd.AddCommand(webCmd)
webCmd.Flags().StringVarP(&cfgFile, "config", "c", "", "config file")
if err := webCmd.MarkFlagRequired("config"); err != nil {
log.Fatal().Err(err).Msg("missing flag")
}
// Timer.
rootCmd.AddCommand(timerCmd)
timerCmd.Flags().String("hostname", "localhost", "Hostname of schnutibox")
timerCmd.Flags().Int("port", 6600, "Port of schnutibox")
timerCmd.Flags().DurationP("duration", "d", time.Minute, "Duration until the timer stops the playback")
if err := timerCmd.MarkFlagRequired("duration"); err != nil {
log.Fatal().Err(err).Msg("missing flag")
}
// Defaults.
viper.SetDefault("web.hostname", "localhost")
viper.SetDefault("web.port", 9999)
viper.SetDefault("mpd.hostname", "localhost")
viper.SetDefault("mpd.port", 6600)
viper.SetDefault("reader.dev", "/dev/hidraw0")
viper.SetDefault("reader.ignore", false)
viper.SetDefault("debug.pprof", false)
viper.SetDefault("timer.duration", time.Minute)
// Environment handling.
viper.SetEnvPrefix("SCHNUTIBOX")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
// Flags.
for k, v := range map[string]*pflag.Flag{
"debug.pprof": rootCmd.PersistentFlags().Lookup("pprof"),
"reader.dev": prepareCmd.Flags().Lookup("rfid-reader"),
"reader.ignore": runCmd.Flags().Lookup("ignore-reader"),
"web.hostname": timerCmd.Flags().Lookup("hostname"),
"web.port": timerCmd.Flags().Lookup("port"),
"timer.duration": timerCmd.Flags().Lookup("duration"),
} {
if err := viper.BindPFlag(k, v); err != nil {
log.Fatal().Err(err).Msg("could not bind flag")
}
}
}
// initConfig loads the config file.
// fatal defines if config parsing should end in a fatal error or not.
func initConfig(fatal bool) {
logger := log.With().Str("config", cfgFile).Logger()
// Parse config file.
if cfgFile == "" && fatal {
logger.Fatal().Msg("missing config file")
} else {
logger.Warn().Msg("missing config file")
}
// Dont mind if there is no config file... viper also should populate
// flags and environment variables.
viper.SetConfigFile(cfgFile)
parseConfig(logger, fatal)
// Configfile changes watch only enabled if there is a config file.
if cfgFile != "" {
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
logger.Info().Msg("config file changed")
parseConfig(logger, false)
})
}
}
// parseConfig parses the config and does some tests if required fields are there.
// Its also possible to decide if parsing should end up in a fatal or just an error.
func parseConfig(logger zerolog.Logger, fatal bool) {
if err := viper.ReadInConfig(); err != nil {
if fatal {
logger.Fatal().Err(err).Msg("error loading config file")
}
logger.Error().Err(err).Msg("error loading config file")
}
if err := viper.Unmarshal(&config.Cfg); err != nil {
if fatal {
logger.Fatal().Err(err).Msg("could not unmarshal config")
}
logger.Error().Err(err).Msg("could not unmarshal config")
}
// Disabling require check if no config is set.
// Not sure about this!
if cfgFile != "" {
if err := config.Cfg.Require(); err != nil {
if fatal {
logger.Fatal().Err(err).Msg("missing config parts")
}
logger.Error().Err(err).Msg("missing config parts")
return
}
} else {
logger.Warn().Msg("doesnt do a config requirement check")
return
}
}
// Execute executes the commandline interface.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}