1
0
mirror of https://github.com/mxpv/podsync.git synced 2024-05-11 05:55:04 +00:00

197 lines
5.0 KiB
Go

package main
import (
"fmt"
"os"
"path/filepath"
"regexp"
"github.com/hashicorp/go-multierror"
"github.com/pelletier/go-toml"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/mxpv/podsync/pkg/db"
"github.com/mxpv/podsync/pkg/feed"
"github.com/mxpv/podsync/pkg/fs"
"github.com/mxpv/podsync/pkg/model"
"github.com/mxpv/podsync/pkg/ytdl"
"github.com/mxpv/podsync/services/web"
)
type Config struct {
// Server is the web server configuration
Server web.Config `toml:"server"`
// S3 is the optional configuration for S3-compatible storage provider
Storage fs.Config `toml:"storage"`
// Log is the optional logging configuration
Log Log `toml:"log"`
// Database configuration
Database db.Config `toml:"database"`
// Feeds is a list of feeds to host by this app.
// ID will be used as feed ID in http://podsync.net/{FEED_ID}.xml
Feeds map[string]*feed.Config
// Tokens is API keys to use to access YouTube/Vimeo APIs.
Tokens map[model.Provider]StringSlice `toml:"tokens"`
// Downloader (youtube-dl) configuration
Downloader ytdl.Config `toml:"downloader"`
}
type Log struct {
// Filename to write the log to (instead of stdout)
Filename string `toml:"filename"`
// MaxSize is the maximum size of the log file in MB
MaxSize int `toml:"max_size"`
// MaxBackups is the maximum number of log file backups to keep after rotation
MaxBackups int `toml:"max_backups"`
// MaxAge is the maximum number of days to keep the logs for
MaxAge int `toml:"max_age"`
// Compress old backups
Compress bool `toml:"compress"`
// Debug mode
Debug bool `toml:"debug"`
}
// LoadConfig loads TOML configuration from a file path
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, errors.Wrapf(err, "failed to read config file: %s", path)
}
config := Config{}
if err := toml.Unmarshal(data, &config); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal toml")
}
for id, f := range config.Feeds {
f.ID = id
}
config.applyDefaults(path)
if err := config.validate(); err != nil {
return nil, err
}
return &config, nil
}
func (c *Config) validate() error {
var result *multierror.Error
if c.Server.DataDir != "" {
log.Warnf(`server.data_dir is deprecated, and will be removed in a future release. Use the following config instead:
[storage]
[storage.local]
data_dir = "%s"
`, c.Server.DataDir)
if c.Storage.Local.DataDir == "" {
c.Storage.Local.DataDir = c.Server.DataDir
}
}
if c.Server.Path != "" {
var pathReg = regexp.MustCompile(model.PathRegex)
if !pathReg.MatchString(c.Server.Path) {
result = multierror.Append(result, errors.Errorf("Server handle path must be match %s or empty", model.PathRegex))
}
}
switch c.Storage.Type {
case "local":
if c.Storage.Local.DataDir == "" {
result = multierror.Append(result, errors.New("data directory is required for local storage"))
}
case "s3":
if c.Storage.S3.EndpointURL == "" || c.Storage.S3.Region == "" || c.Storage.S3.Bucket == "" {
result = multierror.Append(result, errors.New("S3 storage requires endpoint_url, region and bucket to be set"))
}
default:
result = multierror.Append(result, errors.Errorf("unknown storage type: %s", c.Storage.Type))
}
if len(c.Feeds) == 0 {
result = multierror.Append(result, errors.New("at least one feed must be specified"))
}
for id, f := range c.Feeds {
if f.URL == "" {
result = multierror.Append(result, errors.Errorf("URL is required for %q", id))
}
}
return result.ErrorOrNil()
}
func (c *Config) applyDefaults(configPath string) {
if c.Server.Hostname == "" {
if c.Server.Port != 0 && c.Server.Port != 80 {
c.Server.Hostname = fmt.Sprintf("http://localhost:%d", c.Server.Port)
} else {
c.Server.Hostname = "http://localhost"
}
}
if c.Storage.Type == "" {
c.Storage.Type = "local"
}
if c.Log.Filename != "" {
if c.Log.MaxSize == 0 {
c.Log.MaxSize = model.DefaultLogMaxSize
}
if c.Log.MaxAge == 0 {
c.Log.MaxAge = model.DefaultLogMaxAge
}
if c.Log.MaxBackups == 0 {
c.Log.MaxBackups = model.DefaultLogMaxBackups
}
}
if c.Database.Dir == "" {
c.Database.Dir = filepath.Join(filepath.Dir(configPath), "db")
}
for _, _feed := range c.Feeds {
if _feed.UpdatePeriod == 0 {
_feed.UpdatePeriod = model.DefaultUpdatePeriod
}
if _feed.Quality == "" {
_feed.Quality = model.DefaultQuality
}
if _feed.Custom.CoverArtQuality == "" {
_feed.Custom.CoverArtQuality = model.DefaultQuality
}
if _feed.Format == "" {
_feed.Format = model.DefaultFormat
}
if _feed.PageSize == 0 {
_feed.PageSize = model.DefaultPageSize
}
if _feed.PlaylistSort == "" {
_feed.PlaylistSort = model.SortingAsc
}
}
}
// StringSlice is a toml extension that lets you to specify either a string
// value (a slice with just one element) or a string slice.
type StringSlice []string
func (s *StringSlice) UnmarshalTOML(v interface{}) error {
if str, ok := v.(string); ok {
*s = []string{str}
return nil
}
return errors.New("failed to decode string slice field")
}