initial code (#6)

* initial code commit
This commit is contained in:
l3uddz
2021-02-14 16:18:26 +00:00
committed by GitHub
parent 3f55336fbd
commit ce3807b819
53 changed files with 3694 additions and 0 deletions

46
cmd/nabarr/config.go Normal file
View 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)
}

View 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
View 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
View 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
View 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
View 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
View 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
}