diff --git a/cmd/podsync/main.go b/cmd/podsync/main.go new file mode 100644 index 0000000..46da245 --- /dev/null +++ b/cmd/podsync/main.go @@ -0,0 +1,76 @@ +package main + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + + "github.com/jessevdk/go-flags" + log "github.com/sirupsen/logrus" + + "github.com/mxpv/podsync/pkg/config" +) + +type Opts struct { + Config string `long:"config" short:"c" default:"config.toml"` + Debug bool `long:"debug" short:"d"` +} + +func main() { + log.SetFormatter(&log.TextFormatter{}) + + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Parse args + opts := Opts{} + _, err := flags.Parse(&opts) + if err != nil { + log.WithError(err).Fatal("failed to parse command line arguments") + } + + if opts.Debug { + log.SetLevel(log.DebugLevel) + } + + // Load TOML file + log.Debugf("loading configuration %q", opts.Config) + cfg, err := config.LoadConfig(opts.Config) + if err != nil { + log.WithError(err).Fatal("failed to load configuration file") + } + + // Create web server + if cfg.Port == 0 { + log.Debug("using default port 8080") + cfg.Port = 8080 + } + + srv := http.Server{ + Addr: fmt.Sprintf(":%d", cfg.Port), + } + + // Run listener + go func() { + log.Infof("running listener at %s", srv.Addr) + if err := srv.ListenAndServe(); err != nil { + log.WithError(err).Error("failed to listen") + } + }() + + <-stop + + log.Info("shutting down") + + if err := srv.Shutdown(ctx); err != nil { + log.WithError(err).Error("server shutdown failed") + } + + log.Info("gracefully stopped") +} diff --git a/go.mod b/go.mod index 4d5307f..d1ca576 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/mxpv/podsync require ( cloud.google.com/go v0.25.0 // indirect github.com/BrianHicks/finch v0.0.0-20140409222414-419bd73c29ec + github.com/BurntSushi/toml v0.3.1 github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170929212804-61590edac4c7 github.com/aws/aws-sdk-go v1.15.81 github.com/boj/redistore v0.0.0-20160128113310-fc113767cd6b // indirect @@ -13,7 +14,6 @@ require ( github.com/gin-contrib/gzip v0.0.1 github.com/gin-contrib/sessions v0.0.0-20170731012558-a71ea9167c61 github.com/gin-gonic/gin v1.3.0 - github.com/go-chi/chi v4.0.2+incompatible github.com/go-pg/pg v6.14.2+incompatible github.com/golang/mock v1.2.0 github.com/gorilla/sessions v1.1.1 // indirect @@ -38,3 +38,5 @@ require ( google.golang.org/appengine v1.1.0 // indirect gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce // indirect ) + +go 1.13 diff --git a/go.sum b/go.sum index 0c20171..5130f56 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ cloud.google.com/go v0.25.0 h1:6vD6xZTc8Jo6To8gHxFDRVsMvWFDgY3rugNszcDalN8= cloud.google.com/go v0.25.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BrianHicks/finch v0.0.0-20140409222414-419bd73c29ec h1:1VPruZMM1WQC7POhjxbZOWK564cuFz1hlpwYW6ocM4E= github.com/BrianHicks/finch v0.0.0-20140409222414-419bd73c29ec/go.mod h1:+hWo/MWgY8VtjZvdrYM2nPRMaK40zX2iPsH/qD0+Xs0= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170929212804-61590edac4c7 h1:Clo7QBZv+fHzjCgVp4ELlbIsY5rScCmj+4VCfoMfqtQ= github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170929212804-61590edac4c7/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo= github.com/aws/aws-sdk-go v1.15.81 h1:va7uoFaV9uKAtZ6BTmp1u7paoMsizYRRLvRuoC07nQ8= @@ -30,8 +32,6 @@ github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cou github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs= github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= -github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= -github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-pg/pg v6.14.2+incompatible h1:FrOgsHDUhC3V3wkBGAIN5LVj4nJczFPyy1YNFnetfIQ= github.com/go-pg/pg v6.14.2+incompatible/go.mod h1:a2oXow+aFOrvwcKs3eIA0lNFmMilrxK2sOkB5NWe0vA= github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..1e98c34 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,64 @@ +package config + +import ( + "time" + + "github.com/BurntSushi/toml" + "github.com/pkg/errors" +) + +// Feed is a configuration for a feed +type Feed struct { + // URL is a full URL of the field + URL string `toml:"url"` + // PageSize is the number of pages to query from YouTube API. + // NOTE: larger page sizes/often requests might drain your API token. + PageSize int `toml:"page_size"` + // UpdatePeriod is how often to check for updates. + // Format is "300ms", "1.5h" or "2h45m". + // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + // NOTE: too often update check might drain your API token. + UpdatePeriod Duration `toml:"update_period"` +} + +type Tokens struct { + // YouTube API key. + // See https://developers.google.com/youtube/registering_an_application + YouTube string `toml:"youtube"` + // Vimeo developer key. + // See https://developer.vimeo.com/api/guides/start#generate-access-token + Vimeo string `toml:"vimeo"` +} + +type Config struct { + // DataDir is a path to a directory to keep XML feeds and downloaded episodes + DataDir string `toml:"data_dir"` + // Port is a server port to listen to + Port int `toml:"port"` + // 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 + // Tokens is API keys to use to access YouTube/Vimeo APIs. + Tokens Tokens `toml:"tokens"` +} + +// LoadConfig loads TOML configuration from a file path +func LoadConfig(path string) (*Config, error) { + config := Config{} + _, err := toml.DecodeFile(path, &config) + if err != nil { + return nil, errors.Wrapf(err, "failed to load config file %q", path) + } + + return &config, nil +} + +type Duration struct { + time.Duration +} + +func (d *Duration) UnmarshalText(text []byte) error { + var err error + d.Duration, err = time.ParseDuration(string(text)) + return err +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..62a6258 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,53 @@ +package config + +import ( + "io/ioutil" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoadConfig(t *testing.T) { + const file = ` +data_dir = "test/data/" +port = 80 + +[tokens] +youtube = "123" +vimeo = "321" + +[feeds] + [feeds.XYZ] + url = "https://youtube.com/watch?v=ygIUF678y40" + page_size = 50 + update_period = "5h" +` + + f, err := ioutil.TempFile("", "") + require.NoError(t, err) + + defer os.Remove(f.Name()) + + _, err = f.WriteString(file) + require.NoError(t, err) + + config, err := LoadConfig(f.Name()) + assert.NoError(t, err) + assert.NotNil(t, config) + + assert.Equal(t, "test/data/", config.DataDir) + assert.EqualValues(t, 80, config.Port) + + assert.Equal(t, "123", config.Tokens.YouTube) + assert.Equal(t, "321", config.Tokens.Vimeo) + + assert.Len(t, config.Feeds, 1) + feed, ok := config.Feeds["XYZ"] + assert.True(t, ok) + assert.Equal(t, "https://youtube.com/watch?v=ygIUF678y40", feed.URL) + assert.EqualValues(t, 50, feed.PageSize) + assert.EqualValues(t, Duration{5 * time.Hour}, feed.UpdatePeriod) +}