mirror of
https://github.com/mxpv/podsync.git
synced 2024-05-11 05:55:04 +00:00
Decouple config package
This commit is contained in:
@@ -3,14 +3,14 @@ package builder
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/feed"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
)
|
||||
|
||||
type Builder interface {
|
||||
Build(ctx context.Context, cfg *config.Feed) (*model.Feed, error)
|
||||
Build(ctx context.Context, cfg *feed.Config) (*model.Feed, error)
|
||||
}
|
||||
|
||||
func New(ctx context.Context, provider model.Provider, key string) (Builder, error) {
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/feed"
|
||||
"github.com/pkg/errors"
|
||||
soundcloudapi "github.com/zackradisic/soundcloud-api"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ type SoundCloudBuilder struct {
|
||||
client *soundcloudapi.API
|
||||
}
|
||||
|
||||
func (s *SoundCloudBuilder) Build(_ctx context.Context, cfg *config.Feed) (*model.Feed, error) {
|
||||
func (s *SoundCloudBuilder) Build(_ctx context.Context, cfg *feed.Config) (*model.Feed, error) {
|
||||
info, err := ParseURL(cfg.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -3,10 +3,9 @@ package builder
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/feed"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
)
|
||||
|
||||
func TestSC_BUILDFEED(t *testing.T) {
|
||||
@@ -20,7 +19,7 @@ func TestSC_BUILDFEED(t *testing.T) {
|
||||
|
||||
for _, addr := range urls {
|
||||
t.Run(addr, func(t *testing.T) {
|
||||
feed, err := builder.Build(testCtx, &config.Feed{URL: addr})
|
||||
feed, err := builder.Build(testCtx, &feed.Config{URL: addr})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEmpty(t, feed.Title)
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/feed"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/silentsokolov/go-vimeo/vimeo"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
)
|
||||
|
||||
@@ -158,7 +158,7 @@ func (v *VimeoBuilder) queryVideos(getVideos getVideosFunc, feed *model.Feed) er
|
||||
}
|
||||
}
|
||||
|
||||
func (v *VimeoBuilder) Build(ctx context.Context, cfg *config.Feed) (*model.Feed, error) {
|
||||
func (v *VimeoBuilder) Build(ctx context.Context, cfg *feed.Config) (*model.Feed, error) {
|
||||
info, err := ParseURL(cfg.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/BrianHicks/finch/duration"
|
||||
"github.com/mxpv/podsync/pkg/feed"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/api/youtube/v3"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
)
|
||||
|
||||
@@ -384,7 +384,7 @@ func (yt *YouTubeBuilder) queryItems(ctx context.Context, feed *model.Feed) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
func (yt *YouTubeBuilder) Build(ctx context.Context, cfg *config.Feed) (*model.Feed, error) {
|
||||
func (yt *YouTubeBuilder) Build(ctx context.Context, cfg *feed.Config) (*model.Feed, error) {
|
||||
info, err := ParseURL(cfg.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/feed"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
)
|
||||
|
||||
@@ -53,7 +53,7 @@ func TestYT_BuildFeed(t *testing.T) {
|
||||
|
||||
for _, addr := range urls {
|
||||
t.Run(addr, func(t *testing.T) {
|
||||
feed, err := builder.Build(testCtx, &config.Feed{URL: addr})
|
||||
feed, err := builder.Build(testCtx, &feed.Config{URL: addr})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEmpty(t, feed.Title)
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/pelletier/go-toml"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
)
|
||||
|
||||
// Feed is a configuration for a feed
|
||||
type Feed struct {
|
||||
ID string `toml:"-"`
|
||||
// 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 time.Duration `toml:"update_period"`
|
||||
// Cron expression format is how often to check update
|
||||
// NOTE: too often update check might drain your API token.
|
||||
CronSchedule string `toml:"cron_schedule"`
|
||||
// Quality to use for this feed
|
||||
Quality model.Quality `toml:"quality"`
|
||||
// Maximum height of video
|
||||
MaxHeight int `toml:"max_height"`
|
||||
// Format to use for this feed
|
||||
Format model.Format `toml:"format"`
|
||||
// Only download episodes that match this regexp (defaults to matching anything)
|
||||
Filters Filters `toml:"filters"`
|
||||
// Clean is a cleanup policy to use for this feed
|
||||
Clean Cleanup `toml:"clean"`
|
||||
// Custom is a list of feed customizations
|
||||
Custom Custom `toml:"custom"`
|
||||
// List of additional youtube-dl arguments passed at download time
|
||||
YouTubeDLArgs []string `toml:"youtube_dl_args"`
|
||||
// Included in OPML file
|
||||
OPML bool `toml:"opml"`
|
||||
// Playlist sort
|
||||
PlaylistSort model.Sorting `toml:"playlist_sort"`
|
||||
}
|
||||
|
||||
type Filters struct {
|
||||
Title string `toml:"title"`
|
||||
NotTitle string `toml:"not_title"`
|
||||
Description string `toml:"description"`
|
||||
NotDescription string `toml:"not_description"`
|
||||
// More filters to be added here
|
||||
}
|
||||
|
||||
type Custom struct {
|
||||
CoverArt string `toml:"cover_art"`
|
||||
CoverArtQuality model.Quality `toml:"cover_art_quality"`
|
||||
Category string `toml:"category"`
|
||||
Subcategories []string `toml:"subcategories"`
|
||||
Explicit bool `toml:"explicit"`
|
||||
Language string `toml:"lang"`
|
||||
Author string `toml:"author"`
|
||||
Title string `toml:"title"`
|
||||
Description string `toml:"description"`
|
||||
OwnerName string `toml:"ownerName"`
|
||||
OwnerEmail string `toml:"ownerEmail"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
// Hostname to use for download links
|
||||
Hostname string `toml:"hostname"`
|
||||
// Port is a server port to listen to
|
||||
Port int `toml:"port"`
|
||||
// Bind a specific IP addresses for server
|
||||
// "*": bind all IP addresses which is default option
|
||||
// localhost or 127.0.0.1 bind a single IPv4 address
|
||||
BindAddress string `toml:"bind_address"`
|
||||
// Specify path for reverse proxy and only [A-Za-z0-9]
|
||||
Path string `toml:"path"`
|
||||
// DataDir is a path to a directory to keep XML feeds and downloaded episodes,
|
||||
// that will be available to user via web server for download.
|
||||
DataDir string `toml:"data_dir"`
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
// Dir is a directory to keep database files
|
||||
Dir string `toml:"dir"`
|
||||
Badger *Badger `toml:"badger"`
|
||||
}
|
||||
|
||||
// Badger represents BadgerDB configuration parameters
|
||||
// See https://github.com/dgraph-io/badger#memory-usage
|
||||
type Badger struct {
|
||||
Truncate bool `toml:"truncate"`
|
||||
FileIO bool `toml:"file_io"`
|
||||
}
|
||||
|
||||
type Cleanup struct {
|
||||
// KeepLast defines how many episodes to keep
|
||||
KeepLast int `toml:"keep_last"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
// Downloader is a youtube-dl related configuration
|
||||
type Downloader struct {
|
||||
// SelfUpdate toggles self update every 24 hour
|
||||
SelfUpdate bool `toml:"self_update"`
|
||||
// Timeout in minutes for youtube-dl process to finish download
|
||||
Timeout int `toml:"timeout"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
// Server is the web server configuration
|
||||
Server Server `toml:"server"`
|
||||
// Log is the optional logging configuration
|
||||
Log Log `toml:"log"`
|
||||
// Database configuration
|
||||
Database Database `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
|
||||
// Tokens is API keys to use to access YouTube/Vimeo APIs.
|
||||
Tokens map[model.Provider][]string `toml:"tokens"`
|
||||
// Downloader (youtube-dl) configuration
|
||||
Downloader Downloader `toml:"downloader"`
|
||||
}
|
||||
|
||||
// LoadConfig loads TOML configuration from a file path
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
data, err := ioutil.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, feed := range config.Feeds {
|
||||
feed.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 == "" {
|
||||
result = multierror.Append(result, errors.New("data directory is required"))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.Feeds) == 0 {
|
||||
result = multierror.Append(result, errors.New("at least one feed must be specified"))
|
||||
}
|
||||
|
||||
for id, feed := range c.Feeds {
|
||||
if feed.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.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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
)
|
||||
|
||||
func TestLoadConfig(t *testing.T) {
|
||||
const file = `
|
||||
[tokens]
|
||||
youtube = ["123"]
|
||||
vimeo = ["321", "456"]
|
||||
|
||||
[server]
|
||||
port = 80
|
||||
data_dir = "test/data/"
|
||||
|
||||
[database]
|
||||
dir = "/home/user/db/"
|
||||
|
||||
[downloader]
|
||||
self_update = true
|
||||
timeout = 15
|
||||
|
||||
[feeds]
|
||||
[feeds.XYZ]
|
||||
url = "https://youtube.com/watch?v=ygIUF678y40"
|
||||
page_size = 48
|
||||
update_period = "5h"
|
||||
format = "audio"
|
||||
quality = "low"
|
||||
filters = { title = "regex for title here" }
|
||||
playlist_sort = "desc"
|
||||
clean = { keep_last = 10 }
|
||||
[feeds.XYZ.custom]
|
||||
cover_art = "http://img"
|
||||
cover_art_quality = "high"
|
||||
category = "TV"
|
||||
subcategories = ["1", "2"]
|
||||
explicit = true
|
||||
lang = "en"
|
||||
author = "Mrs. Smith ([email protected])"
|
||||
ownerName = "Mrs. Smith"
|
||||
ownerEmail = "[email protected]"
|
||||
`
|
||||
path := setup(t, file)
|
||||
defer os.Remove(path)
|
||||
|
||||
config, err := LoadConfig(path)
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
assert.Equal(t, "test/data/", config.Server.DataDir)
|
||||
assert.EqualValues(t, 80, config.Server.Port)
|
||||
|
||||
assert.Equal(t, "/home/user/db/", config.Database.Dir)
|
||||
|
||||
require.Len(t, config.Tokens["youtube"], 1)
|
||||
assert.Equal(t, "123", config.Tokens["youtube"][0])
|
||||
require.Len(t, config.Tokens["vimeo"], 2)
|
||||
assert.Equal(t, "321", config.Tokens["vimeo"][0])
|
||||
assert.Equal(t, "456", config.Tokens["vimeo"][1])
|
||||
|
||||
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, 48, feed.PageSize)
|
||||
assert.EqualValues(t, 5*time.Hour, feed.UpdatePeriod)
|
||||
assert.EqualValues(t, "audio", feed.Format)
|
||||
assert.EqualValues(t, "low", feed.Quality)
|
||||
assert.EqualValues(t, "regex for title here", feed.Filters.Title)
|
||||
assert.EqualValues(t, 10, feed.Clean.KeepLast)
|
||||
assert.EqualValues(t, model.SortingDesc, feed.PlaylistSort)
|
||||
|
||||
assert.EqualValues(t, "http://img", feed.Custom.CoverArt)
|
||||
assert.EqualValues(t, "high", feed.Custom.CoverArtQuality)
|
||||
assert.EqualValues(t, "TV", feed.Custom.Category)
|
||||
assert.True(t, feed.Custom.Explicit)
|
||||
assert.EqualValues(t, "en", feed.Custom.Language)
|
||||
assert.EqualValues(t, "Mrs. Smith ([email protected])", feed.Custom.Author)
|
||||
assert.EqualValues(t, "Mrs. Smith", feed.Custom.OwnerName)
|
||||
assert.EqualValues(t, "[email protected]", feed.Custom.OwnerEmail)
|
||||
|
||||
assert.EqualValues(t, feed.Custom.Subcategories, []string{"1", "2"})
|
||||
|
||||
assert.Nil(t, config.Database.Badger)
|
||||
|
||||
assert.True(t, config.Downloader.SelfUpdate)
|
||||
assert.EqualValues(t, 15, config.Downloader.Timeout)
|
||||
}
|
||||
|
||||
func TestLoadEmptyKeyList(t *testing.T) {
|
||||
const file = `
|
||||
[tokens]
|
||||
vimeo = []
|
||||
|
||||
[server]
|
||||
data_dir = "/data"
|
||||
[feeds]
|
||||
[feeds.A]
|
||||
url = "https://youtube.com/watch?v=ygIUF678y40"
|
||||
`
|
||||
path := setup(t, file)
|
||||
defer os.Remove(path)
|
||||
|
||||
config, err := LoadConfig(path)
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
require.Len(t, config.Tokens, 1)
|
||||
require.Len(t, config.Tokens["vimeo"], 0)
|
||||
}
|
||||
|
||||
func TestApplyDefaults(t *testing.T) {
|
||||
const file = `
|
||||
[server]
|
||||
data_dir = "/data"
|
||||
|
||||
[feeds]
|
||||
[feeds.A]
|
||||
url = "https://youtube.com/watch?v=ygIUF678y40"
|
||||
`
|
||||
path := setup(t, file)
|
||||
defer os.Remove(path)
|
||||
|
||||
config, err := LoadConfig(path)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, config)
|
||||
|
||||
assert.Len(t, config.Feeds, 1)
|
||||
feed, ok := config.Feeds["A"]
|
||||
require.True(t, ok)
|
||||
|
||||
assert.EqualValues(t, feed.UpdatePeriod, model.DefaultUpdatePeriod)
|
||||
assert.EqualValues(t, feed.PageSize, 50)
|
||||
assert.EqualValues(t, feed.Quality, "high")
|
||||
assert.EqualValues(t, feed.Custom.CoverArtQuality, "high")
|
||||
assert.EqualValues(t, feed.Format, "video")
|
||||
}
|
||||
|
||||
func TestHttpServerListenAddress(t *testing.T) {
|
||||
const file = `
|
||||
[server]
|
||||
bind_address = "172.20.10.2"
|
||||
port = 8080
|
||||
path = "test"
|
||||
data_dir = "/data"
|
||||
|
||||
[feeds]
|
||||
[feeds.A]
|
||||
url = "https://youtube.com/watch?v=ygIUF678y40"
|
||||
|
||||
[database]
|
||||
badger = { truncate = true, file_io = true }
|
||||
`
|
||||
path := setup(t, file)
|
||||
defer os.Remove(path)
|
||||
|
||||
config, err := LoadConfig(path)
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.Server.BindAddress)
|
||||
require.NotNil(t, config.Server.Path)
|
||||
}
|
||||
|
||||
func TestDefaultHostname(t *testing.T) {
|
||||
cfg := Config{
|
||||
Server: Server{},
|
||||
}
|
||||
|
||||
t.Run("empty hostname", func(t *testing.T) {
|
||||
cfg.applyDefaults("")
|
||||
assert.Equal(t, "http://localhost", cfg.Server.Hostname)
|
||||
})
|
||||
|
||||
t.Run("empty hostname with port", func(t *testing.T) {
|
||||
cfg.Server.Hostname = ""
|
||||
cfg.Server.Port = 7979
|
||||
cfg.applyDefaults("")
|
||||
assert.Equal(t, "http://localhost:7979", cfg.Server.Hostname)
|
||||
})
|
||||
|
||||
t.Run("skip overwrite", func(t *testing.T) {
|
||||
cfg.Server.Hostname = "https://my.host:4443"
|
||||
cfg.Server.Port = 80
|
||||
cfg.applyDefaults("")
|
||||
assert.Equal(t, "https://my.host:4443", cfg.Server.Hostname)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDefaultDatabasePath(t *testing.T) {
|
||||
cfg := Config{}
|
||||
cfg.applyDefaults("/home/user/podsync/config.toml")
|
||||
assert.Equal(t, "/home/user/podsync/db", cfg.Database.Dir)
|
||||
}
|
||||
|
||||
func TestLoadBadgerConfig(t *testing.T) {
|
||||
const file = `
|
||||
[server]
|
||||
data_dir = "/data"
|
||||
|
||||
[feeds]
|
||||
[feeds.A]
|
||||
url = "https://youtube.com/watch?v=ygIUF678y40"
|
||||
|
||||
[database]
|
||||
badger = { truncate = true, file_io = true }
|
||||
`
|
||||
path := setup(t, file)
|
||||
defer os.Remove(path)
|
||||
|
||||
config, err := LoadConfig(path)
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.Database.Badger)
|
||||
|
||||
assert.True(t, config.Database.Badger.Truncate)
|
||||
assert.True(t, config.Database.Badger.FileIO)
|
||||
}
|
||||
|
||||
func setup(t *testing.T, file string) string {
|
||||
t.Helper()
|
||||
|
||||
f, err := ioutil.TempFile("", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.WriteString(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
return f.Name()
|
||||
}
|
||||
+8
-2
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
)
|
||||
|
||||
@@ -23,13 +22,20 @@ const (
|
||||
episodePath = "episode/%s/%s" // FeedID + EpisodeID
|
||||
)
|
||||
|
||||
// BadgerConfig represents BadgerDB configuration parameters
|
||||
// See https://github.com/dgraph-io/badger#memory-usage
|
||||
type BadgerConfig struct {
|
||||
Truncate bool `toml:"truncate"`
|
||||
FileIO bool `toml:"file_io"`
|
||||
}
|
||||
|
||||
type Badger struct {
|
||||
db *badger.DB
|
||||
}
|
||||
|
||||
var _ Storage = (*Badger)(nil)
|
||||
|
||||
func NewBadger(config *config.Database) (*Badger, error) {
|
||||
func NewBadger(config *Config) (*Badger, error) {
|
||||
var (
|
||||
dir = config.Dir
|
||||
)
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
)
|
||||
|
||||
@@ -20,7 +19,7 @@ func TestNewBadger(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "podsync-badger-")
|
||||
require.NoError(t, err)
|
||||
|
||||
db, err := NewBadger(&config.Database{Dir: dir})
|
||||
db, err := NewBadger(&Config{Dir: dir})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = db.Close()
|
||||
@@ -35,7 +34,7 @@ func TestBadger_Version(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
db, err := NewBadger(&config.Database{Dir: dir})
|
||||
db, err := NewBadger(&Config{Dir: dir})
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
@@ -49,7 +48,7 @@ func TestBadger_AddFeed(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
db, err := NewBadger(&config.Database{Dir: dir})
|
||||
db, err := NewBadger(&Config{Dir: dir})
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
@@ -63,7 +62,7 @@ func TestBadger_GetFeed(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
db, err := NewBadger(&config.Database{Dir: dir})
|
||||
db, err := NewBadger(&Config{Dir: dir})
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
@@ -83,7 +82,7 @@ func TestBadger_WalkFeeds(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
db, err := NewBadger(&config.Database{Dir: dir})
|
||||
db, err := NewBadger(&Config{Dir: dir})
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
@@ -109,7 +108,7 @@ func TestBadger_DeleteFeed(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
db, err := NewBadger(&config.Database{Dir: dir})
|
||||
db, err := NewBadger(&Config{Dir: dir})
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
@@ -134,7 +133,7 @@ func TestBadger_UpdateEpisode(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
db, err := NewBadger(&config.Database{Dir: dir})
|
||||
db, err := NewBadger(&Config{Dir: dir})
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
@@ -164,7 +163,7 @@ func TestBadger_WalkEpisodes(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
db, err := NewBadger(&config.Database{Dir: dir})
|
||||
db, err := NewBadger(&Config{Dir: dir})
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package db
|
||||
|
||||
type Config struct {
|
||||
// Dir is a directory to keep database files
|
||||
Dir string `toml:"dir"`
|
||||
Badger *BadgerConfig `toml:"badger"`
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package feed
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
)
|
||||
|
||||
// Config is a configuration for a feed loaded from TOML
|
||||
type Config struct {
|
||||
ID string `toml:"-"`
|
||||
// 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 time.Duration `toml:"update_period"`
|
||||
// Cron expression format is how often to check update
|
||||
// NOTE: too often update check might drain your API token.
|
||||
CronSchedule string `toml:"cron_schedule"`
|
||||
// Quality to use for this feed
|
||||
Quality model.Quality `toml:"quality"`
|
||||
// Maximum height of video
|
||||
MaxHeight int `toml:"max_height"`
|
||||
// Format to use for this feed
|
||||
Format model.Format `toml:"format"`
|
||||
// Only download episodes that match this regexp (defaults to matching anything)
|
||||
Filters Filters `toml:"filters"`
|
||||
// Clean is a cleanup policy to use for this feed
|
||||
Clean Cleanup `toml:"clean"`
|
||||
// Custom is a list of feed customizations
|
||||
Custom Custom `toml:"custom"`
|
||||
// List of additional youtube-dl arguments passed at download time
|
||||
YouTubeDLArgs []string `toml:"youtube_dl_args"`
|
||||
// Included in OPML file
|
||||
OPML bool `toml:"opml"`
|
||||
// Playlist sort
|
||||
PlaylistSort model.Sorting `toml:"playlist_sort"`
|
||||
}
|
||||
|
||||
type Filters struct {
|
||||
Title string `toml:"title"`
|
||||
NotTitle string `toml:"not_title"`
|
||||
Description string `toml:"description"`
|
||||
NotDescription string `toml:"not_description"`
|
||||
// More filters to be added here
|
||||
}
|
||||
|
||||
type Custom struct {
|
||||
CoverArt string `toml:"cover_art"`
|
||||
CoverArtQuality model.Quality `toml:"cover_art_quality"`
|
||||
Category string `toml:"category"`
|
||||
Subcategories []string `toml:"subcategories"`
|
||||
Explicit bool `toml:"explicit"`
|
||||
Language string `toml:"lang"`
|
||||
Author string `toml:"author"`
|
||||
Title string `toml:"title"`
|
||||
Description string `toml:"description"`
|
||||
OwnerName string `toml:"ownerName"`
|
||||
OwnerEmail string `toml:"ownerEmail"`
|
||||
}
|
||||
|
||||
type Cleanup struct {
|
||||
// KeepLast defines how many episodes to keep
|
||||
KeepLast int `toml:"keep_last"`
|
||||
}
|
||||
+2
-3
@@ -9,16 +9,15 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
)
|
||||
|
||||
func BuildOPML(ctx context.Context, config *config.Config, db feedProvider, hostname string) (string, error) {
|
||||
func BuildOPML(ctx context.Context, feeds map[string]*Config, db feedProvider, hostname string) (string, error) {
|
||||
doc := opml.OPML{Version: "1.0"}
|
||||
doc.Head = opml.Head{Title: "Podsync feeds"}
|
||||
doc.Body = opml.Body{}
|
||||
|
||||
for _, feed := range config.Feeds {
|
||||
for _, feed := range feeds {
|
||||
f, err := db.GetFeed(ctx, feed.ID)
|
||||
if err == model.ErrNotFound {
|
||||
// As we update OPML on per-feed basis, some feeds may not yet be populated in database.
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
)
|
||||
|
||||
@@ -28,13 +27,8 @@ func TestBuildOPML(t *testing.T) {
|
||||
dbMock := NewMockfeedProvider(ctrl)
|
||||
dbMock.EXPECT().GetFeed(gomock.Any(), "1").Return(&model.Feed{Title: "1", Description: "desc"}, nil)
|
||||
|
||||
cfg := config.Config{
|
||||
Feeds: map[string]*config.Feed{
|
||||
"any": {ID: "1", OPML: true},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := BuildOPML(context.Background(), &cfg, dbMock, "https://url/")
|
||||
feeds := map[string]*Config{"any": {ID: "1", OPML: true}}
|
||||
out, err := BuildOPML(context.Background(), feeds, dbMock, "https://url/")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, out)
|
||||
}
|
||||
|
||||
+2
-3
@@ -11,7 +11,6 @@ import (
|
||||
itunes "github.com/eduncan911/podcast"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
)
|
||||
|
||||
@@ -31,7 +30,7 @@ func (p timeSlice) Swap(i, j int) {
|
||||
p[i], p[j] = p[j], p[i]
|
||||
}
|
||||
|
||||
func Build(_ctx context.Context, feed *model.Feed, cfg *config.Feed, hostname string) (*itunes.Podcast, error) {
|
||||
func Build(_ctx context.Context, feed *model.Feed, cfg *Config, hostname string) (*itunes.Podcast, error) {
|
||||
const (
|
||||
podsyncGenerator = "Podsync generator (support us at https://github.com/mxpv/podsync)"
|
||||
defaultCategory = "TV & Film"
|
||||
@@ -152,7 +151,7 @@ func Build(_ctx context.Context, feed *model.Feed, cfg *config.Feed, hostname st
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func EpisodeName(feedConfig *config.Feed, episode *model.Episode) string {
|
||||
func EpisodeName(feedConfig *Config, episode *model.Episode) string {
|
||||
ext := "mp4"
|
||||
if feedConfig.Format == model.FormatAudio {
|
||||
ext = "mp3"
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"testing"
|
||||
|
||||
itunes "github.com/eduncan911/podcast"
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -23,9 +22,9 @@ func TestBuildXML(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
cfg := config.Feed{
|
||||
cfg := Config{
|
||||
ID: "test",
|
||||
Custom: config.Custom{Description: "description", Category: "Technology", Subcategories: []string{"Gadgets", "Podcasting"}},
|
||||
Custom: Custom{Description: "description", Category: "Technology", Subcategories: []string{"Gadgets", "Podcasting"}},
|
||||
}
|
||||
|
||||
out, err := Build(context.Background(), &feed, &cfg, "http://localhost/")
|
||||
|
||||
+12
-4
@@ -13,10 +13,10 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/feed"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
)
|
||||
|
||||
@@ -29,13 +29,21 @@ var (
|
||||
ErrTooManyRequests = errors.New(http.StatusText(http.StatusTooManyRequests))
|
||||
)
|
||||
|
||||
// Config is a youtube-dl related configuration
|
||||
type Config struct {
|
||||
// SelfUpdate toggles self update every 24 hour
|
||||
SelfUpdate bool `toml:"self_update"`
|
||||
// Timeout in minutes for youtube-dl process to finish download
|
||||
Timeout int `toml:"timeout"`
|
||||
}
|
||||
|
||||
type YoutubeDl struct {
|
||||
path string
|
||||
timeout time.Duration
|
||||
updateLock sync.Mutex // Don't call youtube-dl while self updating
|
||||
}
|
||||
|
||||
func New(ctx context.Context, cfg config.Downloader) (*YoutubeDl, error) {
|
||||
func New(ctx context.Context, cfg Config) (*YoutubeDl, error) {
|
||||
path, err := exec.LookPath("youtube-dl")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "youtube-dl binary not found")
|
||||
@@ -134,7 +142,7 @@ func (dl *YoutubeDl) Update(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dl *YoutubeDl) Download(ctx context.Context, feedConfig *config.Feed, episode *model.Episode) (r io.ReadCloser, err error) {
|
||||
func (dl *YoutubeDl) Download(ctx context.Context, feedConfig *feed.Config, episode *model.Episode) (r io.ReadCloser, err error) {
|
||||
tmpDir, err := ioutil.TempDir("", "podsync-")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get temp dir for download")
|
||||
@@ -198,7 +206,7 @@ func (dl *YoutubeDl) exec(ctx context.Context, args ...string) (string, error) {
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
func buildArgs(feedConfig *config.Feed, episode *model.Episode, outputFilePath string) []string {
|
||||
func buildArgs(feedConfig *feed.Config, episode *model.Episode, outputFilePath string) []string {
|
||||
var args []string
|
||||
|
||||
if feedConfig.Format == model.FormatVideo {
|
||||
|
||||
@@ -3,7 +3,7 @@ package ytdl
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/feed"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -105,7 +105,7 @@ func TestBuildArgs(t *testing.T) {
|
||||
|
||||
for _, tst := range tests {
|
||||
t.Run(tst.name, func(t *testing.T) {
|
||||
result := buildArgs(&config.Feed{
|
||||
result := buildArgs(&feed.Config{
|
||||
Format: tst.format,
|
||||
Quality: tst.quality,
|
||||
MaxHeight: tst.maxHeight,
|
||||
|
||||
Reference in New Issue
Block a user