46
cmd/nabarr/config.go
Normal file
46
cmd/nabarr/config.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/kirsle/configdir"
|
||||
"golang.org/x/sys/unix"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func defaultConfigPath() string {
|
||||
// get binary path
|
||||
bp := getBinaryPath()
|
||||
if dirIsWriteable(bp) == nil {
|
||||
return bp
|
||||
}
|
||||
|
||||
// binary path is not write-able, use alternative path
|
||||
cp := configdir.LocalConfig("nabarr")
|
||||
if _, err := os.Stat(cp); os.IsNotExist(err) {
|
||||
if e := os.MkdirAll(cp, os.ModePerm); e != nil {
|
||||
panic("failed to create nabarr config directory")
|
||||
}
|
||||
}
|
||||
|
||||
return cp
|
||||
}
|
||||
|
||||
func getBinaryPath() string {
|
||||
// get current binary path
|
||||
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
||||
if err != nil {
|
||||
// get current working dir
|
||||
if dir, err = os.Getwd(); err != nil {
|
||||
panic("failed to determine current binary location")
|
||||
}
|
||||
}
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
func dirIsWriteable(dir string) error {
|
||||
// credits: https://stackoverflow.com/questions/20026320/how-to-tell-if-folder-exists-and-is-writable
|
||||
return unix.Access(dir, unix.W_OK)
|
||||
}
|
||||
60
cmd/nabarr/config_windows.go
Normal file
60
cmd/nabarr/config_windows.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/kirsle/configdir"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func defaultConfigPath() string {
|
||||
// get binary path
|
||||
bp := getBinaryPath()
|
||||
if dirIsWriteable(bp) == nil {
|
||||
return bp
|
||||
}
|
||||
|
||||
// binary path is not write-able, use alternative path
|
||||
cp := configdir.LocalConfig("nabarr")
|
||||
if _, err := os.Stat(cp); os.IsNotExist(err) {
|
||||
if e := os.MkdirAll(cp, os.ModePerm); e != nil {
|
||||
panic("failed to create nabarr config directory")
|
||||
}
|
||||
}
|
||||
|
||||
return cp
|
||||
}
|
||||
|
||||
func getBinaryPath() string {
|
||||
// get current binary path
|
||||
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
||||
if err != nil {
|
||||
// get current working dir
|
||||
if dir, err = os.Getwd(); err != nil {
|
||||
panic("failed to determine current binary location")
|
||||
}
|
||||
}
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
func dirIsWriteable(dir string) error {
|
||||
// credits: https://stackoverflow.com/questions/20026320/how-to-tell-if-folder-exists-and-is-writable
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
return errors.New("path does not exist")
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
return errors.New("path is not a directory")
|
||||
}
|
||||
|
||||
// Check if the user bit is enabled in file permission
|
||||
if info.Mode().Perm()&(1<<(uint(7))) == 0 {
|
||||
fmt.Println("Write permission bit is not set on this file for user")
|
||||
return errors.New("write permission not set for user")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
244
cmd/nabarr/main.go
Normal file
244
cmd/nabarr/main.go
Normal file
@@ -0,0 +1,244 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/l3uddz/nabarr"
|
||||
"github.com/l3uddz/nabarr/cache"
|
||||
"github.com/l3uddz/nabarr/cmd/nabarr/pvr"
|
||||
"github.com/l3uddz/nabarr/media"
|
||||
"github.com/l3uddz/nabarr/rss"
|
||||
"github.com/l3uddz/nabarr/util"
|
||||
"github.com/lefelys/state"
|
||||
"github.com/natefinch/lumberjack"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Media media.Config `yaml:"media"`
|
||||
Pvrs []nabarr.PvrConfig `yaml:"pvrs"`
|
||||
Rss rss.Config `yaml:"rss"`
|
||||
}
|
||||
|
||||
var (
|
||||
Version string
|
||||
Timestamp string
|
||||
GitCommit string
|
||||
|
||||
// CLI
|
||||
cli struct {
|
||||
globals
|
||||
|
||||
// flags
|
||||
Config string `type:"path" default:"${config_file}" env:"APP_CONFIG" help:"Config file path"`
|
||||
Cache string `type:"path" default:"${cache_file}" env:"APP_CACHE" help:"Cache file path"`
|
||||
Log string `type:"path" default:"${log_file}" env:"APP_LOG" help:"Log file path"`
|
||||
Verbosity int `type:"counter" default:"0" short:"v" env:"APP_VERBOSITY" help:"Log level verbosity"`
|
||||
|
||||
// commands
|
||||
Run struct{} `cmd help:"Run"`
|
||||
Test struct {
|
||||
Pvr string `type:"string" required:"1" help:"PVR to test item against" placeholder:"sonarr"`
|
||||
Id string `type:"string" required:"1" help:"Metadata ID of item to test" placeholder:"tvdb:121361"`
|
||||
} `cmd help:"Test your filters and stop"`
|
||||
}
|
||||
)
|
||||
|
||||
type globals struct {
|
||||
Version versionFlag `name:"version" help:"Print version information and quit"`
|
||||
Update updateFlag `name:"update" help:"Update if newer version is available and quit"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// cli
|
||||
ctx := kong.Parse(&cli,
|
||||
kong.Name("nabarr"),
|
||||
kong.Description("Monitor newznab/torznab rss and add new media to sonarr/radarr"),
|
||||
kong.UsageOnError(),
|
||||
kong.ConfigureHelp(kong.HelpOptions{
|
||||
Summary: true,
|
||||
Compact: true,
|
||||
}),
|
||||
kong.Vars{
|
||||
"version": fmt.Sprintf("%s (%s@%s)", Version, GitCommit, Timestamp),
|
||||
"config_file": filepath.Join(defaultConfigPath(), "config.yml"),
|
||||
"cache_file": filepath.Join(defaultConfigPath(), "cache"),
|
||||
"log_file": filepath.Join(defaultConfigPath(), "activity.log"),
|
||||
},
|
||||
)
|
||||
|
||||
if err := ctx.Validate(); err != nil {
|
||||
fmt.Println("Failed parsing cli:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Command() == "test" && cli.Verbosity == 0 {
|
||||
// default to debug verbosity in test mode
|
||||
cli.Verbosity = 1
|
||||
}
|
||||
|
||||
// logger
|
||||
logger := log.Output(io.MultiWriter(zerolog.ConsoleWriter{
|
||||
TimeFormat: time.Stamp,
|
||||
Out: os.Stderr,
|
||||
}, zerolog.ConsoleWriter{
|
||||
TimeFormat: time.Stamp,
|
||||
Out: &lumberjack.Logger{
|
||||
Filename: cli.Log,
|
||||
MaxSize: 5,
|
||||
MaxAge: 14,
|
||||
MaxBackups: 5,
|
||||
},
|
||||
NoColor: true,
|
||||
}))
|
||||
|
||||
switch {
|
||||
case cli.Verbosity == 1:
|
||||
log.Logger = logger.Level(zerolog.DebugLevel)
|
||||
case cli.Verbosity > 1:
|
||||
log.Logger = logger.Level(zerolog.TraceLevel)
|
||||
default:
|
||||
log.Logger = logger.Level(zerolog.InfoLevel)
|
||||
}
|
||||
|
||||
// config
|
||||
log.Trace().Msg("Initialising config")
|
||||
file, err := os.Open(cli.Config)
|
||||
if err != nil {
|
||||
log.Fatal().
|
||||
Err(err).
|
||||
Msg("Failed opening config")
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
cfg := config{}
|
||||
decoder := yaml.NewDecoder(file, yaml.Strict())
|
||||
err = decoder.Decode(&cfg)
|
||||
if err != nil {
|
||||
log.Error().Msg("Failed decoding configuration")
|
||||
log.Fatal().
|
||||
Msg(err.Error())
|
||||
}
|
||||
|
||||
// cache
|
||||
c, err := cache.New(cli.Cache)
|
||||
if err != nil {
|
||||
log.Fatal().
|
||||
Err(err).
|
||||
Msg("Failed initialising cache")
|
||||
}
|
||||
defer func() {
|
||||
if err := c.Close(); err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("Failed closing cache gracefully")
|
||||
}
|
||||
}()
|
||||
|
||||
// media
|
||||
log.Trace().Msg("Initialising media")
|
||||
m, err := media.New(&cfg.Media)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed initialising media")
|
||||
}
|
||||
|
||||
// states
|
||||
pvrStates := make([]state.State, 0)
|
||||
rssState := state.Empty()
|
||||
|
||||
// pvrs
|
||||
log.Trace().Msg("Initialising pvrs")
|
||||
cacheFiltersHash := ""
|
||||
pvrs := make(map[string]pvr.PVR, 0)
|
||||
|
||||
for _, p := range cfg.Pvrs {
|
||||
if ctx.Command() == "run" || (ctx.Command() == "test" && strings.EqualFold(cli.Test.Pvr, p.Name)) {
|
||||
// init pvr
|
||||
po, err := pvr.NewPVR(p, ctx.Command(), m, c)
|
||||
if err != nil {
|
||||
log.Fatal().
|
||||
Err(err).
|
||||
Str("pvr", p.Name).
|
||||
Msg("Failed initialising pvr")
|
||||
}
|
||||
|
||||
// start pvr processor
|
||||
pvrStates = append(pvrStates, po.Start())
|
||||
|
||||
// add pvr to map
|
||||
pvrs[p.Name] = po
|
||||
|
||||
// add cacheFiltersHash
|
||||
cacheFiltersHash += util.AsSHA256(p.Filters)
|
||||
}
|
||||
}
|
||||
|
||||
// run mode (start rss scheduler and wait for shutdown signal)
|
||||
if ctx.Command() == "run" {
|
||||
// rss
|
||||
log.Trace().Msg("Initialising rss")
|
||||
r := rss.New(cfg.Rss, c, cacheFiltersHash, pvrs)
|
||||
for _, feed := range cfg.Rss.Feeds {
|
||||
if err := r.AddJob(feed); err != nil {
|
||||
log.Fatal().
|
||||
Err(err).
|
||||
Msg("Failed initialising rss")
|
||||
}
|
||||
}
|
||||
rssState = r.Start()
|
||||
|
||||
// wait for shutdown signal
|
||||
waitShutdown()
|
||||
} else {
|
||||
// test mode
|
||||
idParts := strings.Split(cli.Test.Id, ":")
|
||||
if len(idParts) < 2 {
|
||||
log.Fatal().
|
||||
Str("id", cli.Test.Id).
|
||||
Msg("An invalid id was provided")
|
||||
}
|
||||
|
||||
// prepare test item
|
||||
testItem := new(media.FeedItem)
|
||||
switch strings.ToLower(idParts[0]) {
|
||||
case "imdb":
|
||||
testItem.Title = "Test.Mode.2021.BluRay.1080p.TrueHD.Atmos.7.1.AVC.HYBRID.REMUX-FraMeSToR"
|
||||
testItem.ImdbId = idParts[1]
|
||||
case "tvdb":
|
||||
testItem.Title = "Test.Mode.S01E01.1080p.DTS-HD.MA.5.1.AVC.REMUX-FraMeSToR"
|
||||
testItem.TvdbId = idParts[1]
|
||||
default:
|
||||
log.Fatal().
|
||||
Str("agent", idParts[0]).
|
||||
Str("id", idParts[1]).
|
||||
Msg("Unsupported agent was provided")
|
||||
}
|
||||
|
||||
// queue test item
|
||||
for _, p := range pvrs {
|
||||
p.QueueFeedItem(testItem)
|
||||
}
|
||||
|
||||
// sleep for a moment
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
// shutdown
|
||||
appCtx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
appState := state.Merge(pvrStates...).DependsOn(rssState)
|
||||
if err := appState.Shutdown(appCtx); err != nil {
|
||||
log.Fatal().
|
||||
Err(err).
|
||||
Msg("Failed shutting down gracefully")
|
||||
}
|
||||
}
|
||||
32
cmd/nabarr/pvr/pvr.go
Normal file
32
cmd/nabarr/pvr/pvr.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package pvr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/l3uddz/nabarr"
|
||||
"github.com/l3uddz/nabarr/cache"
|
||||
"github.com/l3uddz/nabarr/media"
|
||||
"github.com/l3uddz/nabarr/radarr"
|
||||
"github.com/l3uddz/nabarr/sonarr"
|
||||
"github.com/lefelys/state"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PVR interface {
|
||||
Type() string
|
||||
AddMediaItem(*media.Item) error
|
||||
ShouldIgnore(*media.Item) (bool, string, error)
|
||||
Start() state.State
|
||||
QueueFeedItem(*media.FeedItem)
|
||||
}
|
||||
|
||||
func NewPVR(c nabarr.PvrConfig, mode string, m *media.Client, cc *cache.Client) (PVR, error) {
|
||||
// return pvr object
|
||||
switch strings.ToLower(c.Type) {
|
||||
case "sonarr":
|
||||
return sonarr.New(c, mode, m, cc)
|
||||
case "radarr":
|
||||
return radarr.New(c, mode, m, cc)
|
||||
}
|
||||
|
||||
return nil, errors.New("unknown pvr")
|
||||
}
|
||||
17
cmd/nabarr/shutdown.go
Normal file
17
cmd/nabarr/shutdown.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func waitShutdown() {
|
||||
/* wait for shutdown signal */
|
||||
quit := make(chan os.Signal, 1)
|
||||
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
log.Warn().Msg("Shutting down...")
|
||||
}
|
||||
71
cmd/nabarr/update.go
Normal file
71
cmd/nabarr/update.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/blang/semver"
|
||||
"github.com/rhysd/go-github-selfupdate/selfupdate"
|
||||
"os"
|
||||
)
|
||||
|
||||
type updateFlag string
|
||||
|
||||
func (u updateFlag) Decode(ctx *kong.DecodeContext) error { return nil }
|
||||
func (u updateFlag) IsBool() bool { return true }
|
||||
func (u updateFlag) BeforeApply(app *kong.Kong, vars kong.Vars) error {
|
||||
// parse current version
|
||||
v, err := semver.Parse(Version)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed parsing current build version: %v\n", err)
|
||||
app.Exit(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// detect latest version
|
||||
fmt.Println("Checking for the latest version...")
|
||||
latest, found, err := selfupdate.DetectLatest("l3uddz/nabarr")
|
||||
if err != nil {
|
||||
fmt.Printf("Failed determining latest available version: %v\n", err)
|
||||
app.Exit(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// check version
|
||||
if !found || latest.Version.LTE(v) {
|
||||
fmt.Printf("Already using the latest version: %v\n", Version)
|
||||
app.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ask update
|
||||
fmt.Printf("Do you want to update to the latest version: %v? (y/n): ", latest.Version)
|
||||
input, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if err != nil || (input != "y\n" && input != "n\n") {
|
||||
fmt.Println("Failed validating input...")
|
||||
app.Exit(1)
|
||||
return nil
|
||||
} else if input == "n\n" {
|
||||
app.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// get existing executable path
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed locating current executable path: %v\n", err)
|
||||
app.Exit(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := selfupdate.UpdateTo(latest.AssetURL, exe); err != nil {
|
||||
fmt.Printf("Failed updating existing binary to latest release: %v\n", err)
|
||||
app.Exit(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully updated to the latest version: %v\n", latest.Version)
|
||||
|
||||
app.Exit(0)
|
||||
return nil
|
||||
}
|
||||
16
cmd/nabarr/version.go
Normal file
16
cmd/nabarr/version.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/alecthomas/kong"
|
||||
)
|
||||
|
||||
type versionFlag string
|
||||
|
||||
func (v versionFlag) Decode(ctx *kong.DecodeContext) error { return nil }
|
||||
func (v versionFlag) IsBool() bool { return true }
|
||||
func (v versionFlag) BeforeApply(app *kong.Kong, vars kong.Vars) error {
|
||||
fmt.Println(vars["version"])
|
||||
app.Exit(0)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user