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

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: l3uddz

134
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,134 @@
name: Build
on:
push:
branches:
- '*'
tags:
- 'v*'
pull_request:
types:
- opened
- reopened
- edited
jobs:
build:
runs-on: ubuntu-latest
steps:
# dependencies
- name: dependencies
run: |
curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sudo sh -s -- -b /usr/local/bin
# checkout
- name: checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
# setup go
- name: go
uses: actions/setup-go@v1
with:
go-version: 1.15
- run: go version
- run: go env
# cache
- name: cache
uses: actions/cache@v1
with:
path: vendor
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
# vendor
- name: vendor
run: |
make vendor
# build
- name: build
if: startsWith(github.ref, 'refs/tags/') == false
run: |
make snapshot
# get tag name
- name: tag_name
if: startsWith(github.ref, 'refs/tags/')
uses: little-core-labs/get-git-tag@v3.0.2
with:
tagRegex: "v?(.+)"
# publish
- name: publish
if: startsWith(github.ref, 'refs/tags/')
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REF: ${{ github.ref }}
run: |
make publish
# artifacts
- name: artifact_linux
uses: actions/upload-artifact@v2-preview
with:
name: build_linux
path: dist/*linux*
- name: artifact_darwin
uses: actions/upload-artifact@v2-preview
with:
name: build_darwin
path: dist/*darwin*
- name: artifact_windows
uses: actions/upload-artifact@v2-preview
with:
name: build_windows
path: dist/*windows*
# docker build (latest & tag)
- name: docker - build latest
if: startsWith(github.ref, 'refs/tags/') == true
uses: docker/build-push-action@v1
with:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
registry: docker.pkg.github.com
repository: l3uddz/nabarr/nabarr
dockerfile: docker/Dockerfile
tags: latest
tag_with_ref: true
tag_with_sha: true
always_pull: true
# docker build (master)
- name: docker - build master
if: github.ref == 'refs/heads/master'
uses: docker/build-push-action@v1
with:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
registry: docker.pkg.github.com
repository: l3uddz/nabarr/nabarr
dockerfile: docker/Dockerfile
tags: master
tag_with_sha: true
always_pull: true
# docker build (branch)
- name: docker - build other
if: startsWith(github.ref, 'refs/heads/master') == false
uses: docker/build-push-action@v1
with:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
registry: docker.pkg.github.com
repository: l3uddz/nabarr/nabarr
dockerfile: docker/Dockerfile
tag_with_ref: true
tag_with_sha: false
always_pull: true

54
.github/workflows/cleanup.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
name: Docker Cleanup
on: delete
jobs:
cleanup_branch:
if: startsWith(github.event.ref_type, 'branch') == true
runs-on: ubuntu-latest
steps:
- name: Sanitize branch docker tag
uses: frabert/replace-string-action@master
id: dockertag
with:
pattern: '[:\.\/]+'
string: "${{ github.event.ref }}"
replace-with: '-'
flags: 'g'
- name: query for package version id
uses: octokit/graphql-action@v2.x
id: query_package_version
with:
query: |
query package($owner:String!,$repo:String!,$tag:String!) {
repository(owner: $owner, name: $repo) {
packages(names:[$repo], first:1) {
edges {
node {
id,
name,
version(version: $tag) {
id, version
}
}
}
}
}
}
owner: ${{ github.event.repository.owner.name }}
repo: ${{ github.event.repository.name }}
tag: ${{ steps.dockertag.outputs.replaced }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: parse package version id
id: package_version
run: echo "VERSION_ID=$(echo $json | jq -r $jsonpath)" >> $GITHUB_ENV
env:
json: ${{ steps.query_package_version.outputs.data }}
jsonpath: ".repository.packages.edges[].node.version.id"
- uses: actions/delete-package-versions@v1
with:
package-version-ids: '${{ env.VERSION_ID }}'

45
.goreleaser.yml Normal file
View File

@@ -0,0 +1,45 @@
# https://goreleaser.com
project_name: nabarr
# Build
builds:
-
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
main: ./cmd/nabarr
goarch:
- amd64
ldflags:
- -s -w
- -X "main.Version={{ .Version }}"
- -X "main.GitCommit={{ .ShortCommit }}"
- -X "main.Timestamp={{ .Timestamp }}"
flags:
- -trimpath
# Archive
archives:
-
name_template: "{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}"
format: "binary"
# Checksum
checksum:
name_template: "checksums.txt"
algorithm: sha512
# Snapshot
snapshot:
name_template: "{{ .Major }}.{{ .Minor }}.{{ .Patch }}-dev+{{ .ShortCommit }}"
# Changelog
changelog:
filters:
exclude:
- "^docs:"
- "^test:"
- "^Merge branch"

55
Makefile Normal file
View File

@@ -0,0 +1,55 @@
.DEFAULT_GOAL := build
CMD := nabarr
TARGET := $(shell go env GOOS)_$(shell go env GOARCH)
DIST_PATH := dist
BUILD_PATH := ${DIST_PATH}/${CMD}_${TARGET}
GO_FILES := $(shell find . -path ./vendor -prune -or -type f -name '*.go' -print)
GIT_COMMIT := $(shell git rev-parse --short HEAD)
TIMESTAMP := $(shell date +%s)
VERSION ?= 0.0.0-dev
CGO := 0
# Deps
.PHONY: check_goreleaser
check_goreleaser:
@command -v goreleaser >/dev/null || (echo "goreleaser is required."; exit 1)
.PHONY: vendor
vendor: ## Vendor files and tidy go.mod
go mod vendor
go mod tidy
.PHONY: vendor_update
vendor_update: ## Update vendor dependencies
go get -u ./...
${MAKE} vendor
.PHONY: build
build: vendor ${BUILD_PATH}/${CMD} ## Build application
# Binary
${BUILD_PATH}/${CMD}: ${GO_FILES} go.sum
@echo "Building for ${TARGET}..." && \
mkdir -p ${BUILD_PATH} && \
CGO_ENABLED=${CGO} go build \
-mod vendor \
-trimpath \
-ldflags "-s -w -X main.Version=${VERSION} -X main.GitCommit=${GIT_COMMIT} -X main.Timestamp=${TIMESTAMP}" \
-o ${BUILD_PATH}/${CMD} \
./cmd/nabarr
.PHONY: release
release: check_goreleaser ## Generate a release, but don't publish
goreleaser --skip-validate --skip-publish --rm-dist
.PHONY: publish
publish: check_goreleaser ## Generate a release, and publish
goreleaser --rm-dist
.PHONY: snapshot
snapshot: check_goreleaser ## Generate a snapshot release
goreleaser --snapshot --skip-validate --skip-publish --rm-dist
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

86
cache/cache.go vendored Normal file
View File

@@ -0,0 +1,86 @@
package cache
import (
"context"
"fmt"
"github.com/l3uddz/nabarr/logger"
"github.com/lefelys/state"
"github.com/rs/zerolog"
"github.com/xujiajun/nutsdb"
"time"
)
type Client struct {
log zerolog.Logger
st state.State
db *nutsdb.DB
}
func New(path string) (*Client, error) {
db, err := nutsdb.Open(nutsdb.Options{
Dir: path,
EntryIdxMode: nutsdb.HintKeyValAndRAMIdxMode,
SegmentSize: 8 * 1024 * 1024,
NodeNum: 1,
RWMode: nutsdb.FileIO,
SyncEnable: true,
StartFileLoadingMode: nutsdb.MMap,
})
if err != nil {
return nil, fmt.Errorf("open: %w", err)
}
log := logger.New("trace").With().Logger()
// start cleaner
st, tail := state.WithShutdown()
ticker := time.NewTicker(24 * time.Hour)
go func() {
for {
select {
case <-tail.End():
ticker.Stop()
tail.Done()
return
case <-ticker.C:
// clean cache
err := db.Update(func(tx *nutsdb.Tx) error {
return db.Merge()
})
switch {
case err == nil:
log.Info().Msg("Cleaned cache")
case err.Error() == "the number of files waiting to be merged is at least 2":
// there were no data files to be merged
default:
// unexpected error
log.Error().
Err(err).
Msg("Failed cleaning cache")
}
}
}
}()
return &Client{
log: log,
st: st,
db: db,
}, nil
}
func (c *Client) Close() error {
// shutdown cleaner
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
if err := c.st.Shutdown(ctx); err != nil {
c.log.Error().
Err(err).
Msg("Failed shutting down cache cleaner gracefully")
}
// close cache
return c.db.Close()
}

15
cache/delete.go vendored Normal file
View File

@@ -0,0 +1,15 @@
package cache
import (
"fmt"
"github.com/xujiajun/nutsdb"
)
func (c *Client) Delete(bucket string, key string) error {
if err := c.db.Update(func(tx *nutsdb.Tx) error {
return tx.Delete(bucket, []byte(key))
}); err != nil {
return fmt.Errorf("%v: %v; delete: %w", bucket, key, err)
}
return nil
}

22
cache/get.go vendored Normal file
View File

@@ -0,0 +1,22 @@
package cache
import (
"fmt"
"github.com/xujiajun/nutsdb"
)
func (c *Client) Get(bucket string, key string) ([]byte, error) {
var value []byte
if err := c.db.View(func(tx *nutsdb.Tx) error {
e, err := tx.Get(bucket, []byte(key))
if err != nil {
return fmt.Errorf("%v: %v; get: %w", bucket, key, err)
}
value = e.Value
return nil
}); err != nil {
return nil, err
}
return value, nil
}

16
cache/put.go vendored Normal file
View File

@@ -0,0 +1,16 @@
package cache
import (
"fmt"
"github.com/xujiajun/nutsdb"
"time"
)
func (c *Client) Put(bucket string, key string, val []byte, ttl time.Duration) error {
if err := c.db.Update(func(tx *nutsdb.Tx) error {
return tx.Put(bucket, []byte(key), val, uint32(ttl.Seconds()))
}); err != nil {
return fmt.Errorf("%v: %v; put: %w", bucket, key, err)
}
return nil
}

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
}

17
docker/Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
FROM sc4h/alpine-s6overlay:3.12
ENV \
PATH="/app/nabarr:${PATH}" \
APP_CONFIG="/config/config.yml" \
APP_CACHE="/config/cache" \
APP_LOG="/config/activity.log" \
APP_VERBOSITY="0"
# Binary
COPY ["dist/nabarr_linux_amd64/nabarr", "/app/nabarr/nabarr"]
# Add root files
COPY ["docker/run", "/etc/services.d/nabarr/run"]
# Volume
VOLUME ["/config"]

3
docker/run Normal file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/with-contenv sh
exec s6-setuidgid abc /app/nabarr/nabarr run

30
go.mod Normal file
View File

@@ -0,0 +1,30 @@
module github.com/l3uddz/nabarr
go 1.14
require (
github.com/alecthomas/kong v0.2.12
github.com/antonmedv/expr v1.8.9
github.com/blang/semver v3.5.1+incompatible
github.com/goccy/go-yaml v1.8.8
github.com/golang/protobuf v1.4.3 // indirect
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
github.com/lefelys/state v1.1.0
github.com/lucperkins/rek v0.1.3
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/pkg/errors v0.9.1
github.com/rhysd/go-github-selfupdate v1.2.3
github.com/robfig/cron/v3 v3.0.1
github.com/rs/zerolog v1.20.0
github.com/stretchr/testify v1.7.0 // indirect
github.com/ulikunitz/xz v0.5.10 // indirect
github.com/xujiajun/nutsdb v0.5.0
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/ratelimit v0.1.0
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c // indirect
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

509
go.sum Normal file
View File

@@ -0,0 +1,509 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/alecthomas/kong v0.2.12 h1:X3kkCOXGUNzLmiu+nQtoxWqj4U2a39MpSJR3QdQXOwI=
github.com/alecthomas/kong v0.2.12/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
github.com/antonmedv/expr v1.8.9 h1:O9stiHmHHww9b4ozhPx7T6BK7fXfOCHJ8ybxf0833zw=
github.com/antonmedv/expr v1.8.9/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/goccy/go-yaml v1.8.8 h1:MGfRB1GeSn/hWXYWS2Pt67iC2GJNnebdIro01ddyucA=
github.com/goccy/go-yaml v1.8.8/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU=
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lefelys/state v1.1.0 h1:c/+YvgXOsE0wEN/Au5FV0NDgZBagg11nlSSCwwsUWvc=
github.com/lefelys/state v1.1.0/go.mod h1:CpobVJsY3dSRkpfjlWzX0iVVlvQgRz85eQFF1rHQsEE=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucperkins/rek v0.1.3 h1:X+sBDsB6XD33Y6eu/whJBULJVf/paEYtHSp1CY+dOIU=
github.com/lucperkins/rek v0.1.3/go.mod h1:OkhOWrb/TTqRMfdHkrtlDCtwRIo2wmTE0jIzyQwhXHU=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I=
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag=
github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzxazpPAODuqarmPDe2Rg=
github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs=
github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo=
github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xujiajun/gorouter v1.2.0/go.mod h1:yJrIta+bTNpBM/2UT8hLOaEAFckO+m/qmR3luMIQygM=
github.com/xujiajun/mmap-go v1.0.1 h1:7Se7ss1fLPPRW+ePgqGpCkfGIZzJV6JPq9Wq9iv/WHc=
github.com/xujiajun/mmap-go v1.0.1/go.mod h1:CNN6Sw4SL69Sui00p0zEzcZKbt+5HtEnYUsc6BKKRMg=
github.com/xujiajun/nutsdb v0.5.0 h1:j/jM3Zw7Chg8WK7bAcKR0Xr7Mal47U1oJAMgySfDn9E=
github.com/xujiajun/nutsdb v0.5.0/go.mod h1:owdwN0tW084RxEodABLbO7h4Z2s9WiAjZGZFhRh0/1Q=
github.com/xujiajun/utils v0.0.0-20190123093513-8bf096c4f53b h1:jKG9OiL4T4xQN3IUrhUpc1tG+HfDXppkgVcrAiiaI/0=
github.com/xujiajun/utils v0.0.0-20190123093513-8bf096c4f53b/go.mod h1:AZd87GYJlUzl82Yab2kTjx1EyXSQCAfZDhpTo1SQC4k=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/ratelimit v0.1.0 h1:U2AruXqeTb4Eh9sYQSTrMhH8Cb7M0Ian2ibBOnBcnAw=
go.uber.org/ratelimit v0.1.0/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 h1:JIqe8uIcRBHXDQVvZtHwp80ai3Lw3IJAeJEs55Dc1W0=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c h1:HiAZXo96zOhVhtFHchj/ojzoxCFiPrp9/j0GtS38V3g=
golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

19
logger/logger.go Normal file
View File

@@ -0,0 +1,19 @@
package logger
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func New(verbosity string) zerolog.Logger {
if verbosity == "" {
return log.Logger
}
level, err := zerolog.ParseLevel(verbosity)
if err != nil {
return log.Logger
}
return log.Level(level)
}

34
media/client.go Normal file
View File

@@ -0,0 +1,34 @@
package media
import (
"fmt"
"github.com/l3uddz/nabarr/logger"
"github.com/l3uddz/nabarr/media/omdb"
"github.com/l3uddz/nabarr/media/trakt"
"github.com/rs/zerolog"
)
type Client struct {
trakt *trakt.Client
omdb *omdb.Client
log zerolog.Logger
}
func New(cfg *Config) (*Client, error) {
// trakt
if cfg.Trakt.ClientId == "" {
return nil, fmt.Errorf("trakt: no client_id specified")
}
t := trakt.New(&cfg.Trakt)
// omdb
o := omdb.New(&cfg.Omdb)
return &Client{
trakt: t,
omdb: o,
log: logger.New(cfg.Verbosity).With().Logger(),
}, nil
}

13
media/config.go Normal file
View File

@@ -0,0 +1,13 @@
package media
import (
"github.com/l3uddz/nabarr/media/omdb"
"github.com/l3uddz/nabarr/media/trakt"
)
type Config struct {
Trakt trakt.Config `yaml:"trakt"`
Omdb omdb.Config `yaml:"omdb"`
Verbosity string `yaml:"verbosity,omitempty"`
}

7
media/error.go Normal file
View File

@@ -0,0 +1,7 @@
package media
import "errors"
var (
ErrItemNotFound = errors.New("not found")
)

58
media/movie.go Normal file
View File

@@ -0,0 +1,58 @@
package media
import (
"errors"
"fmt"
"github.com/l3uddz/nabarr/media/trakt"
"strconv"
"time"
)
func (c *Client) GetMovieInfo(item *FeedItem) (*Item, error) {
// lookup on trakt
t, err := c.trakt.GetMovie(item.ImdbId)
if err != nil {
if errors.Is(err, trakt.ErrItemNotFound) {
return nil, fmt.Errorf("trakt: get movie: movie with imdbId %q: %w", item.ImdbId, ErrItemNotFound)
}
return nil, fmt.Errorf("trakt: get movie: movie with imdbId %q: %w", item.ImdbId, err)
}
// transform trakt info
date, err := time.Parse("2006-01-02", t.Released)
if err != nil {
date = time.Time{}
}
mi := &Item{
TvdbId: "",
TmdbId: strconv.Itoa(t.Ids.Tmdb),
ImdbId: t.Ids.Imdb,
Slug: t.Ids.Slug,
Title: t.Title,
FeedTitle: item.Title,
Summary: t.Overview,
Country: []string{t.Country},
Network: "",
Date: date,
Year: date.Year(),
Runtime: t.Runtime,
Rating: t.Rating,
Votes: t.Votes,
Status: t.Status,
Genres: t.Genres,
Languages: []string{t.Language},
}
// omdb
if oi, err := c.omdb.GetItem(t.Ids.Imdb); err != nil {
c.log.Debug().
Err(err).
Str("imdb_id", t.Ids.Imdb).
Msg("Item was not found on omdb")
} else if oi != nil {
mi.Omdb = *oi
}
return mi, nil
}

7
media/omdb/config.go Normal file
View File

@@ -0,0 +1,7 @@
package omdb
type Config struct {
ApiKey string `yaml:"api_key"`
Verbosity string `yaml:"verbosity,omitempty"`
}

72
media/omdb/media.go Normal file
View File

@@ -0,0 +1,72 @@
package omdb
import (
"encoding/json"
"errors"
"fmt"
"github.com/l3uddz/nabarr/util"
"github.com/lucperkins/rek"
"net/url"
"strings"
)
var (
ErrItemNotFound = errors.New("not found")
)
func (c *Client) GetItem(imdbId string) (*Item, error) {
// empty item when appropriate
if c.apiKey == "" || imdbId == "" {
return nil, nil
}
// prepare request
reqUrl, err := util.URLWithQuery(c.apiURL, url.Values{
"apikey": []string{c.apiKey},
"i": []string{imdbId}})
if err != nil {
return nil, fmt.Errorf("generate lookup request url: %w", err)
}
c.log.Trace().
Str("url", reqUrl).
Msg("Searching omdb")
// send request
c.rl.Take()
resp, err := rek.Get(reqUrl, rek.Timeout(c.apiTimeout))
if err != nil {
return nil, fmt.Errorf("request lookup: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 {
return nil, fmt.Errorf("validate lookup response: %s", resp.Status())
}
// decode response
b := new(lookupResponse)
if err := json.NewDecoder(resp.Body()).Decode(b); err != nil {
return nil, fmt.Errorf("decode lookup response: %w", err)
}
if b.Title == "" {
return nil, fmt.Errorf("item with imdbId: %v: %w", imdbId, ErrItemNotFound)
}
// transform response
rt := 0
for _, rating := range b.Ratings {
if strings.EqualFold(rating.Source, "Rotten Tomatoes") {
rt = util.Atoi(strings.TrimSuffix(rating.Value, "%"), 0)
break
}
}
return &Item{
Metascore: util.Atoi(b.Metascore, 0),
RottenTomatoes: rt,
ImdbRating: util.Atof64(b.ImdbRating, 0.0),
}, nil
}

28
media/omdb/omdb.go Normal file
View File

@@ -0,0 +1,28 @@
package omdb
import (
"github.com/l3uddz/nabarr/logger"
"github.com/rs/zerolog"
"go.uber.org/ratelimit"
"time"
)
type Client struct {
apiKey string
log zerolog.Logger
rl ratelimit.Limiter
apiURL string
apiTimeout time.Duration
}
func New(cfg *Config) *Client {
return &Client{
apiKey: cfg.ApiKey,
log: logger.New(cfg.Verbosity).With().Logger(),
rl: ratelimit.New(1, ratelimit.WithoutSlack),
apiURL: "https://www.omdbapi.com",
apiTimeout: 30 * time.Second,
}
}

40
media/omdb/struct.go Normal file
View File

@@ -0,0 +1,40 @@
package omdb
type rating struct {
Source string `json:"Source,omitempty"`
Value string `json:"Value,omitempty"`
}
type lookupResponse struct {
Title string `json:"Title,omitempty"`
Year string `json:"Year,omitempty"`
Rated string `json:"Rated,omitempty"`
Released string `json:"Released,omitempty"`
Runtime string `json:"Runtime,omitempty"`
Genre string `json:"Genre,omitempty"`
Director string `json:"Director,omitempty"`
Writer string `json:"Writer,omitempty"`
Actors string `json:"Actors,omitempty"`
Plot string `json:"Plot,omitempty"`
Language string `json:"Language,omitempty"`
Country string `json:"Country,omitempty"`
Awards string `json:"Awards,omitempty"`
Poster string `json:"Poster,omitempty"`
Ratings []rating `json:"Ratings,omitempty"`
Metascore string `json:"Metascore,omitempty"`
ImdbRating string `json:"imdbRating,omitempty"`
ImdbVotes string `json:"imdbVotes,omitempty"`
ImdbID string `json:"imdbID,omitempty"`
Type string `json:"Type,omitempty"`
DVD string `json:"DVD,omitempty"`
BoxOffice string `json:"BoxOffice,omitempty"`
Production string `json:"Production,omitempty"`
Website string `json:"Website,omitempty"`
Response string `json:"Response,omitempty"`
}
type Item struct {
Metascore int `json:"Metascore,omitempty"`
RottenTomatoes int `json:"RottenTomatoes,omitempty"`
ImdbRating float64 `json:"ImdbRating,omitempty"`
}

53
media/show.go Normal file
View File

@@ -0,0 +1,53 @@
package media
import (
"errors"
"fmt"
"github.com/l3uddz/nabarr/media/trakt"
"strconv"
)
func (c *Client) GetShowInfo(item *FeedItem) (*Item, error) {
// lookup on trakt
t, err := c.trakt.GetShow(item.TvdbId)
if err != nil {
if errors.Is(err, trakt.ErrItemNotFound) {
return nil, fmt.Errorf("trakt: get show: show with tvdbId %q: %w", item.TvdbId, ErrItemNotFound)
}
return nil, fmt.Errorf("trakt: get show: show with tvdbId %q: %w", item.TvdbId, err)
}
// transform trakt info to MediaItem
mi := &Item{
TvdbId: strconv.Itoa(t.Ids.Tvdb),
TmdbId: strconv.Itoa(t.Ids.Tmdb),
ImdbId: t.Ids.Imdb,
Slug: t.Ids.Slug,
Title: t.Title,
FeedTitle: item.Title,
Summary: t.Overview,
Country: []string{t.Country},
Network: t.Network,
Date: t.FirstAired,
Year: t.FirstAired.Year(),
Runtime: t.Runtime,
Rating: t.Rating,
Votes: t.Votes,
Status: t.Status,
Genres: t.Genres,
Languages: []string{t.Language},
AiredEpisodes: t.AiredEpisodes,
}
// omdb
if oi, err := c.omdb.GetItem(t.Ids.Imdb); err != nil {
c.log.Debug().
Err(err).
Str("imdb_id", t.Ids.Imdb).
Msg("Item was not found on omdb")
} else if oi != nil {
mi.Omdb = *oi
}
return mi, nil
}

95
media/struct.go Normal file
View File

@@ -0,0 +1,95 @@
package media
import (
"encoding/xml"
"github.com/l3uddz/nabarr/media/omdb"
"github.com/pkg/errors"
"time"
)
type Item struct {
TvdbId string `json:"TvdbId,omitempty"`
TmdbId string `json:"TmdbId,omitempty"`
ImdbId string `json:"ImdbId,omitempty"`
Slug string `json:"Slug,omitempty"`
FeedTitle string `json:"FeedTitle,omitempty"`
Title string `json:"Title,omitempty"`
Summary string `json:"Summary,omitempty"`
Country []string `json:"Country,omitempty"`
Network string `json:"Network,omitempty"`
Date time.Time `json:"Date"`
Year int `json:"Year,omitempty"`
Runtime int `json:"Runtime,omitempty"`
Rating float64 `json:"Rating,omitempty"`
Votes int `json:"Votes,omitempty"`
Status string `json:"Status,omitempty"`
Genres []string `json:"Genres,omitempty"`
Languages []string `json:"Languages,omitempty"`
AiredEpisodes int `json:"AiredEpisodes,omitempty"`
// additional media provider data
Omdb omdb.Item `json:"Omdb,omitempty"`
}
type Rss struct {
Channel struct {
Items []FeedItem `xml:"item"`
} `xml:"channel"`
}
type FeedItem struct {
Title string `xml:"title,omitempty"`
Category string `xml:"category,omitempty"`
GUID string `xml:"guid,omitempty"`
PubDate Time `xml:"pubDate,omitempty"`
// set by processor
Feed string
// attributes
Language string
TvdbId string `xml:"tvdb,omitempty"`
TvMazeId string
ImdbId string `xml:"imdb,omitempty"`
Attributes []struct {
XMLName xml.Name
Name string `xml:"name,attr"`
Value string `xml:"value,attr"`
} `xml:"attr"`
}
// Time credits: https://github.com/mrobinsn/go-newznab/blob/cd89d9c56447859fa1298dc9a0053c92c45ac7ef/newznab/structs.go#L150
type Time struct {
time.Time
}
func (t *Time) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if err := e.EncodeToken(start); err != nil {
return errors.Wrap(err, "failed to encode xml token")
}
if err := e.EncodeToken(xml.CharData([]byte(t.UTC().Format(time.RFC1123Z)))); err != nil {
return errors.Wrap(err, "failed to encode xml token")
}
if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
return errors.Wrap(err, "failed to encode xml token")
}
return nil
}
func (t *Time) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var raw string
err := d.DecodeElement(&raw, &start)
if err != nil {
return err
}
date, err := time.Parse(time.RFC1123Z, raw)
if err != nil {
return err
}
*t = Time{date}
return nil
}

7
media/trakt/config.go Normal file
View File

@@ -0,0 +1,7 @@
package trakt
type Config struct {
ClientId string `yaml:"client_id"`
Verbosity string `yaml:"verbosity,omitempty"`
}

96
media/trakt/media.go Normal file
View File

@@ -0,0 +1,96 @@
package trakt
import (
"encoding/json"
"errors"
"fmt"
"github.com/l3uddz/nabarr/util"
"github.com/lucperkins/rek"
"net/url"
)
var (
ErrItemNotFound = errors.New("not found")
)
func (c *Client) GetShow(tvdbId string) (*Show, error) {
// prepare request
reqUrl, err := util.URLWithQuery(util.JoinURL(c.apiURL, fmt.Sprintf("/search/tvdb/%s", tvdbId)),
url.Values{
"type": []string{"show"},
"extended": []string{"full"}})
if err != nil {
return nil, fmt.Errorf("generate lookup show request url: %w", err)
}
c.log.Trace().
Str("url", reqUrl).
Msg("Searching trakt")
// send request
c.rl.Take()
resp, err := rek.Get(reqUrl, rek.Headers(c.getAuthHeaders()), rek.Timeout(c.apiTimeout))
if err != nil {
return nil, fmt.Errorf("request show: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 {
return nil, fmt.Errorf("validate show response: %s", resp.Status())
}
// decode response
b := new([]struct{ Show Show })
if err := json.NewDecoder(resp.Body()).Decode(b); err != nil {
return nil, fmt.Errorf("decode show response: %w", err)
}
if len(*b) < 1 {
return nil, ErrItemNotFound
}
// translate response
return &(*b)[0].Show, nil
}
func (c *Client) GetMovie(imdbId string) (*Movie, error) {
// prepare request
reqUrl, err := util.URLWithQuery(util.JoinURL(c.apiURL, fmt.Sprintf("/search/imdb/%s", imdbId)),
url.Values{
"type": []string{"movie"},
"extended": []string{"full"}})
if err != nil {
return nil, fmt.Errorf("generate lookup movie request url: %w", err)
}
c.log.Trace().
Str("url", reqUrl).
Msg("Searching trakt")
// send request
c.rl.Take()
resp, err := rek.Get(reqUrl, rek.Headers(c.getAuthHeaders()), rek.Timeout(c.apiTimeout))
if err != nil {
return nil, fmt.Errorf("request movie: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 {
return nil, fmt.Errorf("validate movie response: %s", resp.Status())
}
// decode response
b := new([]struct{ Movie Movie })
if err := json.NewDecoder(resp.Body()).Decode(b); err != nil {
return nil, fmt.Errorf("decode movie response: %w", err)
}
if len(*b) < 1 {
return nil, ErrItemNotFound
}
// translate response
return &(*b)[0].Movie, nil
}

67
media/trakt/struct.go Normal file
View File

@@ -0,0 +1,67 @@
package trakt
import (
"time"
)
type ShowIds struct {
Trakt int `json:"trakt"`
Slug string `json:"slug"`
Tvdb int `json:"tvdb"`
Imdb string `json:"imdb"`
Tmdb int `json:"tmdb"`
}
type MovieIds struct {
Trakt int `json:"trakt"`
Slug string `json:"slug"`
Imdb string `json:"imdb"`
Tmdb int `json:"tmdb"`
}
type Show struct {
Type string `json:"type"`
Title string `json:"title"`
Year int `json:"year"`
Ids ShowIds `json:"ids"`
Overview string `json:"overview"`
FirstAired time.Time `json:"first_aired"`
Runtime int `json:"runtime"`
Certification string `json:"certification"`
Network string `json:"network"`
Country string `json:"country"`
Trailer string `json:"trailer"`
Homepage string `json:"homepage"`
Status string `json:"status"`
Rating float64 `json:"rating"`
Votes int `json:"votes"`
CommentCount int `json:"comment_count"`
Language string `json:"language"`
AvailableTranslations []string `json:"available_translations"`
Genres []string `json:"genres"`
AiredEpisodes int `json:"aired_episodes"`
Character string `json:"character"`
}
type Movie struct {
Type string `json:"type"`
Title string `json:"title"`
Year int `json:"year"`
Ids MovieIds `json:"ids"`
Tagline string `json:"tagline"`
Overview string `json:"overview"`
Released string `json:"released"`
Runtime int `json:"runtime"`
Country string `json:"country"`
Trailer string `json:"trailer"`
Homepage string `json:"homepage"`
Status string `json:"status"`
Rating float64 `json:"rating"`
Votes int `json:"votes"`
CommentCount int `json:"comment_count"`
Language string `json:"language"`
AvailableTranslations []string `json:"available_translations"`
Genres []string `json:"genres"`
Certification string `json:"certification"`
Character string `json:"character"`
}

35
media/trakt/trakt.go Normal file
View File

@@ -0,0 +1,35 @@
package trakt
import (
"github.com/l3uddz/nabarr/logger"
"github.com/rs/zerolog"
"go.uber.org/ratelimit"
"time"
)
type Client struct {
clientId string
log zerolog.Logger
rl ratelimit.Limiter
apiURL string
apiTimeout time.Duration
}
func New(cfg *Config) *Client {
return &Client{
clientId: cfg.ClientId,
log: logger.New(cfg.Verbosity).With().Logger(),
rl: ratelimit.New(1, ratelimit.WithoutSlack),
apiURL: "https://api.trakt.tv",
apiTimeout: 30 * time.Second,
}
}
func (c *Client) getAuthHeaders() map[string]string {
return map[string]string{
"trakt-api-key": c.clientId,
"trakt-api-version": "2",
}
}

48
nabarr.go Normal file
View File

@@ -0,0 +1,48 @@
package nabarr
import (
"github.com/antonmedv/expr/vm"
"github.com/l3uddz/nabarr/media"
"time"
)
type ExprProgram struct {
expression string
Program *vm.Program
}
func (p *ExprProgram) String() string {
return p.expression
}
func NewExprProgram(expression string, vm *vm.Program) *ExprProgram {
return &ExprProgram{
expression: expression,
Program: vm,
}
}
type ExprEnv struct {
media.Item
Now func() time.Time
}
func NewExprEnv(media *media.Item) *ExprEnv {
return &ExprEnv{
Item: *media,
Now: func() time.Time { return time.Now().UTC() },
}
}
func StringOrDefault(currentValue *string, defaultValue string) string {
if currentValue == nil {
return defaultValue
}
return *currentValue
}
func Uint64OrDefault(currentValue *uint64, defaultValue uint64) uint64 {
if currentValue == nil {
return defaultValue
}
return *currentValue
}

19
pvr.go Normal file
View File

@@ -0,0 +1,19 @@
package nabarr
import "time"
type PvrConfig struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
URL string `yaml:"url"`
ApiKey string `yaml:"api_key"`
QualityProfile string `yaml:"quality_profile"`
RootFolder string `yaml:"root_folder"`
Filters PvrFilters `yaml:"filters"`
CacheDuration time.Duration `yaml:"cache_duration"`
Verbosity string `yaml:"verbosity,omitempty"`
}
type PvrFilters struct {
Ignores []string
}

159
radarr/api.go Normal file
View File

@@ -0,0 +1,159 @@
package radarr
import (
"encoding/json"
"errors"
"fmt"
"github.com/l3uddz/nabarr/media"
"github.com/l3uddz/nabarr/util"
"github.com/lucperkins/rek"
"net/url"
"strconv"
"strings"
)
var (
ErrItemNotFound = errors.New("not found")
)
func (c *Client) getSystemStatus() (*systemStatus, error) {
// send request
resp, err := rek.Get(util.JoinURL(c.apiURL, "/system/status"), rek.Headers(c.apiHeaders),
rek.Timeout(c.apiTimeout))
if err != nil {
return nil, fmt.Errorf("request system status: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 {
return nil, fmt.Errorf("validate system status response: %s", resp.Status())
}
// decode response
b := new(systemStatus)
if err := json.NewDecoder(resp.Body()).Decode(b); err != nil {
return nil, fmt.Errorf("decode system status response: %w", err)
}
return b, nil
}
func (c *Client) getQualityProfileId(profileName string) (int, error) {
// send request
resp, err := rek.Get(util.JoinURL(c.apiURL, "/profile"), rek.Headers(c.apiHeaders),
rek.Timeout(c.apiTimeout))
if err != nil {
return 0, fmt.Errorf("request quality profiles: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 {
return 0, fmt.Errorf("validate quality profiles response: %s", resp.Status())
}
// decode response
b := new([]qualityProfile)
if err := json.NewDecoder(resp.Body()).Decode(b); err != nil {
return 0, fmt.Errorf("decode quality profiles response: %w", err)
}
// find quality profile
for _, profile := range *b {
if strings.EqualFold(profile.Name, profileName) {
return profile.Id, nil
}
}
return 0, errors.New("quality profile not found")
}
func (c *Client) lookupMediaItem(item *media.Item) (*lookupRequest, error) {
// determine metadata id to use
mdType := "imdb"
mdId := item.ImdbId
if item.TmdbId != "" && item.TmdbId != "0" {
// radarr prefers tmdb
mdType = "tmdb"
mdId = item.TmdbId
}
// prepare request
reqUrl, err := util.URLWithQuery(util.JoinURL(c.apiURL, "/movie/lookup"),
url.Values{"term": []string{fmt.Sprintf("%s:%s", mdType, mdId)}})
if err != nil {
return nil, fmt.Errorf("generate movie lookup request url: %w", err)
}
// send request
resp, err := rek.Get(reqUrl, rek.Headers(c.apiHeaders), rek.Timeout(c.apiTimeout))
if err != nil {
return nil, fmt.Errorf("request movie lookup: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 {
return nil, fmt.Errorf("validate movie lookup response: %s", resp.Status())
}
// decode response
b := new([]lookupRequest)
if err := json.NewDecoder(resp.Body()).Decode(b); err != nil {
return nil, fmt.Errorf("decode movie lookup response: %w", err)
}
// find movie
for _, s := range *b {
switch mdType {
case "tmdb":
if strconv.Itoa(s.TmdbId) == item.TmdbId {
return &s, nil
}
default:
if s.ImdbId == item.ImdbId {
return &s, nil
}
}
}
return nil, fmt.Errorf("movie lookup %sId: %v: %w", mdType, mdId, ErrItemNotFound)
}
func (c *Client) AddMediaItem(item *media.Item) error {
// prepare request
req := addRequest{
Title: item.Title,
TitleSlug: item.Slug,
Year: item.Year,
QualityProfileId: c.qualityProfileId,
Images: []string{},
Monitored: true,
RootFolderPath: c.rootFolder,
MinimumAvailability: "released",
AddOptions: addOptions{
SearchForMovie: true,
IgnoreEpisodesWithFiles: false,
IgnoreEpisodesWithoutFiles: false,
},
TmdbId: util.Atoi(item.TmdbId, 0),
ImdbId: item.ImdbId,
}
// send request
resp, err := rek.Post(util.JoinURL(c.apiURL, "/movie"), rek.Headers(c.apiHeaders), rek.Json(req),
rek.Timeout(c.apiTimeout))
if err != nil {
return fmt.Errorf("request add movie: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 && resp.StatusCode() != 201 {
return fmt.Errorf("validate add movie response: %s", resp.Status())
}
return nil
}

47
radarr/expression.go Normal file
View File

@@ -0,0 +1,47 @@
package radarr
import (
"fmt"
"github.com/antonmedv/expr"
"github.com/l3uddz/nabarr"
"github.com/l3uddz/nabarr/media"
"github.com/pkg/errors"
)
func (c *Client) compileExpressions(filters nabarr.PvrFilters) error {
exprEnv := &nabarr.ExprEnv{}
// compile ignores
for _, ignoreExpr := range filters.Ignores {
program, err := expr.Compile(ignoreExpr, expr.Env(exprEnv), expr.AsBool())
if err != nil {
return fmt.Errorf("ignore expression: %v: %w", ignoreExpr, err)
}
c.ignoresExpr = append(c.ignoresExpr, nabarr.NewExprProgram(ignoreExpr, program))
}
return nil
}
func (c *Client) ShouldIgnore(mediaItem *media.Item) (bool, string, error) {
exprItem := nabarr.NewExprEnv(mediaItem)
for _, expression := range c.ignoresExpr {
result, err := expr.Run(expression.Program, exprItem)
if err != nil {
return true, expression.String(), fmt.Errorf("checking ignore expression: %w", err)
}
expResult, ok := result.(bool)
if !ok {
return true, expression.String(), errors.New("type assert ignore expression result")
}
if expResult {
return true, expression.String(), nil
}
}
return false, "", nil
}

228
radarr/queue.go Normal file
View File

@@ -0,0 +1,228 @@
package radarr
import (
"errors"
"fmt"
"github.com/l3uddz/nabarr/media"
"github.com/lefelys/state"
)
func (c *Client) QueueFeedItem(item *media.FeedItem) {
c.queue <- item
}
func (c *Client) Start() state.State {
st, tail := state.WithShutdown()
go c.queueProcessor(tail)
return st
}
func (c *Client) queueProcessor(tail state.ShutdownTail) {
for {
select {
case <-tail.End():
// shutdown
tail.Done()
return
case feedItem := <-c.queue:
// stop processing
if feedItem == nil {
tail.Done()
return
}
// validate item has required id(s)
if feedItem.ImdbId == "" {
continue
}
// check cache / add item to cache
pvrCacheBucket := fmt.Sprintf("pvr_%s_%s", c.Type(), c.name)
cacheKey := fmt.Sprintf("imdb_%s", feedItem.ImdbId)
if !c.testMode {
// not running in test mode, so use cache
if cacheValue, err := c.cache.Get(pvrCacheBucket, cacheKey); err == nil {
// item already exists in the cache
switch string(cacheValue) {
case c.name:
// perm cache entry, item exists in the pvr
continue
case c.cacheFiltersHash:
// temp cache entry, item recently checked with the same filters
continue
}
}
// insert temp cache entry
if err := c.cache.Put(pvrCacheBucket, cacheKey, []byte(c.cacheFiltersHash), c.cacheTempDuration); err != nil {
c.log.Error().
Err(err).
Msg("Failed storing item in temp cache")
}
}
// get media info
mediaItem, err := c.m.GetMovieInfo(feedItem)
if err != nil {
if errors.Is(err, media.ErrItemNotFound) {
c.log.Debug().
Err(err).
Str("feed_title", feedItem.Title).
Str("feed_imdb_id", feedItem.ImdbId).
Str("feed_name", feedItem.Feed).
Msg("Item was not found on trakt")
continue
}
c.log.Error().
Err(err).
Str("feed_title", feedItem.Title).
Str("feed_imdb_id", feedItem.ImdbId).
Str("feed_name", feedItem.Feed).
Msg("Failed finding item on trakt")
continue
}
if c.testMode {
c.log.Debug().
Interface("trakt_item", mediaItem).
Msg("Item found on trakt")
}
// validate tmdbId was found
if mediaItem.TmdbId == "" || mediaItem.TmdbId == "0" {
c.log.Warn().
Str("feed_title", mediaItem.FeedTitle).
Str("feed_imdb_id", feedItem.ImdbId).
Str("feed_name", feedItem.Feed).
Msg("Item had no tmdbId on trakt")
continue
}
// trakt expression check
ignore, filter, err := c.ShouldIgnore(mediaItem)
if err != nil {
c.log.Error().
Err(err).
Str("feed_title", mediaItem.FeedTitle).
Str("trakt_title", mediaItem.Title).
Str("trakt_imdb_id", mediaItem.ImdbId).
Str("feed_name", feedItem.Feed).
Str("ignore_filter", filter).
Msg("Failed checking item against ignore filters")
continue
}
if ignore {
c.log.Debug().
Str("feed_title", mediaItem.FeedTitle).
Str("trakt_title", mediaItem.Title).
Str("trakt_imdb_id", mediaItem.ImdbId).
Str("feed_name", feedItem.Feed).
Str("ignore_filter", filter).
Msg("Item matched ignore filters")
continue
}
// lookup item in pvr
s, err := c.lookupMediaItem(mediaItem)
if err != nil {
if errors.Is(err, ErrItemNotFound) {
// the item was not found
c.log.Warn().
Err(err).
Str("feed_title", mediaItem.FeedTitle).
Str("feed_imdb_id", feedItem.ImdbId).
Str("feed_name", feedItem.Feed).
Msg("Item was not found via pvr lookup")
continue
}
c.log.Error().
Err(err).
Str("feed_title", mediaItem.FeedTitle).
Str("feed_imdb_id", feedItem.ImdbId).
Str("feed_name", feedItem.Feed).
Msg("Failed finding item via pvr lookup")
}
if s.Id > 0 {
// item already existed in pvr
c.log.Debug().
Str("pvr_title", s.Title).
Int("pvr_year", s.Year).
Str("pvr_imdb_id", s.ImdbId).
Int("pvr_tmdb_id", s.TmdbId).
Str("feed_name", feedItem.Feed).
Msg("Item already existed in pvr")
// add item to perm cache (items already in pvr)
if !c.testMode {
if err := c.cache.Put(pvrCacheBucket, cacheKey, []byte(c.name), 0); err != nil {
c.log.Error().
Err(err).
Msg("Failed storing item in perm cache")
}
}
continue
}
// add item to pvr
c.log.Debug().
Str("feed_title", mediaItem.FeedTitle).
Str("trakt_title", mediaItem.Title).
Str("trakt_imdb_id", mediaItem.ImdbId).
Str("trakt_tmdb_id", mediaItem.TmdbId).
Int("trakt_year", mediaItem.Year).
Str("feed_name", feedItem.Feed).
Msg("Sending item to pvr")
if s.TitleSlug != "" {
// use slug from pvr search
mediaItem.Slug = s.TitleSlug
}
if c.testMode {
c.log.Info().
Err(err).
Str("trakt_title", mediaItem.Title).
Str("trakt_imdb_id", mediaItem.ImdbId).
Str("trakt_tmdb_id", mediaItem.TmdbId).
Int("trakt_year", mediaItem.Year).
Str("feed_name", feedItem.Feed).
Msg("Added item (test mode)")
continue
}
if err := c.AddMediaItem(mediaItem); err != nil {
c.log.Error().
Err(err).
Str("feed_title", mediaItem.FeedTitle).
Str("trakt_title", mediaItem.Title).
Str("trakt_imdb_id", mediaItem.ImdbId).
Str("trakt_tmdb_id", mediaItem.TmdbId).
Int("trakt_year", mediaItem.Year).
Str("feed_name", feedItem.Feed).
Msg("Failed adding item to pvr")
}
// add item to perm cache (item was added to pvr)
if !c.testMode {
if err := c.cache.Put(pvrCacheBucket, cacheKey, []byte(c.name), 0); err != nil {
c.log.Error().
Err(err).
Msg("Failed storing item in perm cache")
}
}
c.log.Info().
Err(err).
Str("trakt_title", mediaItem.Title).
Str("trakt_imdb_id", mediaItem.ImdbId).
Str("trakt_tmdb_id", mediaItem.TmdbId).
Int("trakt_year", mediaItem.Year).
Str("feed_name", feedItem.Feed).
Msg("Added item")
}
}
}

110
radarr/radarr.go Normal file
View File

@@ -0,0 +1,110 @@
package radarr
import (
"fmt"
"github.com/l3uddz/nabarr"
"github.com/l3uddz/nabarr/cache"
"github.com/l3uddz/nabarr/logger"
"github.com/l3uddz/nabarr/media"
"github.com/l3uddz/nabarr/util"
"github.com/rs/zerolog"
"strings"
"time"
)
type Client struct {
pvrType string
name string
testMode bool
rootFolder string
qualityProfileId int
apiURL string
apiHeaders map[string]string
apiTimeout time.Duration
cache *cache.Client
cacheTempDuration time.Duration
cacheFiltersHash string
queue chan *media.FeedItem
m *media.Client
log zerolog.Logger
ignoresExpr []*nabarr.ExprProgram
}
func New(c nabarr.PvrConfig, mode string, m *media.Client, cc *cache.Client) (*Client, error) {
l := logger.New(c.Verbosity).With().
Str("pvr_name", c.Name).
Str("pvr_type", c.Type).
Logger()
// set config defaults (if not set)
if c.CacheDuration == 0 {
c.CacheDuration = 24 * time.Hour
}
// set api url
apiURL := ""
if strings.Contains(strings.ToLower(c.URL), "/api") {
apiURL = c.URL
} else {
apiURL = util.JoinURL(c.URL, "/api")
}
// set api headers
apiHeaders := map[string]string{
"X-Api-Key": c.ApiKey,
}
// create client
cl := &Client{
pvrType: "radarr",
name: strings.ToLower(c.Name),
testMode: strings.EqualFold(mode, "test"),
rootFolder: c.RootFolder,
cache: cc,
cacheTempDuration: c.CacheDuration,
cacheFiltersHash: util.AsSHA256(c.Filters),
queue: make(chan *media.FeedItem, 1024),
apiURL: apiURL,
apiHeaders: apiHeaders,
apiTimeout: 60 * time.Second,
m: m,
log: l,
}
// compile expressions
if err := cl.compileExpressions(c.Filters); err != nil {
return nil, fmt.Errorf("compile expressions: %w", err)
}
// validate api access
ss, err := cl.getSystemStatus()
if err != nil {
return nil, fmt.Errorf("validate api: %w", err)
}
// get quality profile
if qid, err := cl.getQualityProfileId(c.QualityProfile); err != nil {
return nil, fmt.Errorf("get quality profile: %v: %w", c.QualityProfile, err)
} else {
cl.qualityProfileId = qid
}
cl.log.Info().
Str("pvr_version", ss.Version).
Msg("Initialised")
return cl, nil
}
func (c *Client) Type() string {
return c.pvrType
}

39
radarr/struct.go Normal file
View File

@@ -0,0 +1,39 @@
package radarr
type systemStatus struct {
Version string
}
type qualityProfile struct {
Name string
Id int
}
type lookupRequest struct {
Id int `json:"id,omitempty"`
Title string `json:"title"`
TitleSlug string `json:"titleSlug"`
Year int `json:"year,omitempty"`
ImdbId string `json:"imdbId"`
TmdbId int `json:"tmdbId"`
}
type addRequest struct {
Title string `json:"title"`
TitleSlug string `json:"titleSlug"`
Year int `json:"year"`
QualityProfileId int `json:"qualityProfileId"`
Images []string `json:"images"`
Monitored bool `json:"monitored"`
RootFolderPath string `json:"rootFolderPath"`
MinimumAvailability string `json:"minimumAvailability"`
AddOptions addOptions `json:"addOptions"`
TmdbId int `json:"tmdbId,omitempty"`
ImdbId string `json:"imdbId,omitempty"`
}
type addOptions struct {
SearchForMovie bool `json:"searchForMovie"`
IgnoreEpisodesWithFiles bool `json:"ignoreEpisodesWithFiles"`
IgnoreEpisodesWithoutFiles bool `json:"ignoreEpisodesWithoutFiles"`
}

88
rss/job.go Normal file
View File

@@ -0,0 +1,88 @@
package rss
import (
"fmt"
"github.com/l3uddz/nabarr/cmd/nabarr/pvr"
"github.com/robfig/cron/v3"
"time"
)
func (c *Client) AddJob(feed feedItem) error {
// prepare job
if feed.Cron == "" {
feed.Cron = "*/15 * * * *"
}
if feed.CacheDuration == 0 {
feed.CacheDuration = (24 * time.Hour) * 28
}
// create job
job := &rssJob{
name: feed.Name,
log: c.log.With().Str("feed_name", feed.Name).Logger(),
url: feed.URL,
pvrs: make(map[string]pvr.PVR, 0),
attempts: 0,
errors: make([]error, 0),
cron: c.cron,
cache: c.cache,
cacheDuration: feed.CacheDuration,
cacheFiltersHash: c.cacheFiltersHash,
}
// add pvrs
for _, p := range feed.Pvrs {
po, exists := c.pvrs[p]
if !exists {
return fmt.Errorf("pvr object does not exist: %v", p)
}
job.pvrs[p] = po
}
// schedule job
if id, err := c.cron.AddJob(feed.Cron, cron.NewChain(
cron.SkipIfStillRunning(cron.DiscardLogger)).Then(job),
); err != nil {
return fmt.Errorf("add job: %w", err)
} else {
job.jobID = id
}
job.log.Info().Msg("Initialised")
return nil
}
func (j *rssJob) Run() {
// increase attempt counter
j.attempts++
// run job
err := j.process()
// handle job response
switch {
case err == nil:
// job completed successfully
j.attempts = 0
j.errors = j.errors[:0]
return
default:
j.log.Warn().
Err(err).
Int("attempts", j.attempts).
Msg("Unexpected error occurred")
j.errors = append(j.errors, err)
}
if j.attempts > 5 {
j.log.Error().
Errs("error", j.errors).
Int("attempts", j.attempts).
Msg("Consecutive errors occurred while refreshing rss, job has been stopped...")
j.cron.Remove(j.jobID)
}
}

136
rss/process.go Normal file
View File

@@ -0,0 +1,136 @@
package rss
import (
"encoding/xml"
"fmt"
"github.com/l3uddz/nabarr/media"
"github.com/lucperkins/rek"
"sort"
"strings"
"time"
)
func (j *rssJob) process() error {
// retrieve feed items
j.log.Debug().Msg("Refreshing")
items, err := j.getFeed()
if err != nil {
return fmt.Errorf("get feed: %w", err)
}
// add feed items to pvrs
if len(items) == 0 {
j.log.Debug().Msg("Refreshed, no items to queue")
return nil
}
for p, _ := range items {
j.queueItemWithPvrs(&items[p])
}
j.log.Info().
Int("count", len(items)).
Msg("Queued items")
return nil
}
func (j *rssJob) queueItemWithPvrs(item *media.FeedItem) {
for _, pvr := range j.pvrs {
switch {
case item.TvdbId != "" && pvr.Type() == "sonarr":
// tvdbId is present, queue with sonarr
pvr.QueueFeedItem(item)
case item.ImdbId != "" && pvr.Type() == "radarr":
// imdbId is present, queue with radarr
pvr.QueueFeedItem(item)
}
}
}
func (j *rssJob) getFeed() ([]media.FeedItem, error) {
// request feed
res, err := rek.Get(j.url, rek.Timeout(30*time.Minute))
if err != nil {
return nil, fmt.Errorf("request feed: %w", err)
}
defer res.Body().Close()
// validate response
if res.StatusCode() != 200 {
return nil, fmt.Errorf("validate response: %s", res.Status())
}
// decode response
b := new(media.Rss)
if err := xml.NewDecoder(res.Body()).Decode(b); err != nil {
return nil, fmt.Errorf("decode feed: %w", err)
}
// prepare result
items := make([]media.FeedItem, 0)
if len(b.Channel.Items) < 1 {
return items, nil
}
// sort response items
sort.SliceStable(b.Channel.Items, func(i, j int) bool {
return b.Channel.Items[i].PubDate.After(b.Channel.Items[j].PubDate.Time)
})
// process feed items
for p, i := range b.Channel.Items {
// ignore items
if i.GUID == "" {
// items must always have a guid
continue
}
// guid seen before?
cacheKey := fmt.Sprintf("%s_%s", j.name, i.GUID)
if cacheValue, err := j.cache.Get(j.name, cacheKey); err == nil {
if string(cacheValue) == j.cacheFiltersHash {
// item has been seen before and the filters have not changed
continue
}
// item has been seen, however the filters have changed since it was last seen, re-process
}
// process feed item attributes
for _, a := range i.Attributes {
switch strings.ToLower(a.Name) {
case "language":
b.Channel.Items[p].Language = a.Value
case "tvdb", "tvdbid":
b.Channel.Items[p].TvdbId = a.Value
case "imdb", "imdbid":
if strings.HasPrefix(a.Value, "tt") {
b.Channel.Items[p].ImdbId = a.Value
} else {
b.Channel.Items[p].ImdbId = fmt.Sprintf("tt%s", a.Value)
}
}
}
// validate item
switch {
case b.Channel.Items[p].TvdbId == "", b.Channel.Items[p].TvdbId == "0":
continue
case b.Channel.Items[p].ImdbId == "":
continue
}
// add validated item for processing
b.Channel.Items[p].Feed = j.name
items = append(items, b.Channel.Items[p])
// add item to temp cache (to prevent re-processing)
if err := j.cache.Put(j.name, cacheKey, []byte(j.cacheFiltersHash), j.cacheDuration); err != nil {
j.log.Error().
Err(err).
Str("guid", i.GUID).
Msg("Failed storing item in temp cache")
}
}
return items, nil
}

62
rss/rss.go Normal file
View File

@@ -0,0 +1,62 @@
package rss
import (
"github.com/l3uddz/nabarr/cache"
"github.com/l3uddz/nabarr/cmd/nabarr/pvr"
"github.com/l3uddz/nabarr/logger"
"github.com/lefelys/state"
"github.com/robfig/cron/v3"
"github.com/rs/zerolog"
"time"
)
type Client struct {
cron *cron.Cron
cache *cache.Client
cacheFiltersHash string
pvrs map[string]pvr.PVR
log zerolog.Logger
}
func New(c Config, cc *cache.Client, cfh string, pvrs map[string]pvr.PVR) *Client {
return &Client{
cron: cron.New(cron.WithChain(
cron.Recover(cron.DefaultLogger),
)),
cache: cc,
cacheFiltersHash: cfh,
pvrs: pvrs,
log: logger.New(c.Verbosity).With().Logger(),
}
}
func (c *Client) Start() state.State {
c.cron.Start()
st, tail := state.WithShutdown()
ticker := time.NewTicker(1 * time.Second)
go func() {
for {
select {
case <-tail.End():
ticker.Stop()
// shutdown cron
ctx := c.cron.Stop()
select {
case <-ctx.Done():
case <-time.After(5 * time.Second):
}
tail.Done()
return
case <-ticker.C:
}
}
}()
return st
}

39
rss/struct.go Normal file
View File

@@ -0,0 +1,39 @@
package rss
import (
"github.com/l3uddz/nabarr/cache"
"github.com/l3uddz/nabarr/cmd/nabarr/pvr"
"github.com/robfig/cron/v3"
"github.com/rs/zerolog"
"time"
)
type feedItem struct {
Name string `yaml:"name"`
URL string `yaml:"url"`
Cron string `yaml:"cron"`
CacheDuration time.Duration `yaml:"cache_duration"`
Pvrs []string `yaml:"pvrs"`
}
type Config struct {
Feeds []feedItem `yaml:"feeds"`
Verbosity string `yaml:"verbosity,omitempty"`
}
type rssJob struct {
name string
log zerolog.Logger
url string
pvrs map[string]pvr.PVR
attempts int
errors []error
cron *cron.Cron
cache *cache.Client
cacheDuration time.Duration
cacheFiltersHash string
jobID cron.EntryID
}

149
sonarr/api.go Normal file
View File

@@ -0,0 +1,149 @@
package sonarr
import (
"encoding/json"
"errors"
"fmt"
"github.com/l3uddz/nabarr/media"
"github.com/l3uddz/nabarr/util"
"github.com/lucperkins/rek"
"net/url"
"strconv"
"strings"
)
var (
ErrItemNotFound = errors.New("not found")
)
func (c *Client) getSystemStatus() (*systemStatus, error) {
// send request
resp, err := rek.Get(util.JoinURL(c.apiURL, "/system/status"), rek.Headers(c.apiHeaders),
rek.Timeout(c.apiTimeout))
if err != nil {
return nil, fmt.Errorf("request system status: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 {
return nil, fmt.Errorf("validate system status response: %s", resp.Status())
}
// decode response
b := new(systemStatus)
if err := json.NewDecoder(resp.Body()).Decode(b); err != nil {
return nil, fmt.Errorf("decode system status response: %w", err)
}
return b, nil
}
func (c *Client) getQualityProfileId(profileName string) (int, error) {
// send request
resp, err := rek.Get(util.JoinURL(c.apiURL, "/profile"), rek.Headers(c.apiHeaders),
rek.Timeout(c.apiTimeout))
if err != nil {
return 0, fmt.Errorf("request quality profiles: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 {
return 0, fmt.Errorf("validate quality profiles response: %s", resp.Status())
}
// decode response
b := new([]qualityProfile)
if err := json.NewDecoder(resp.Body()).Decode(b); err != nil {
return 0, fmt.Errorf("decode quality profiles response: %w", err)
}
// find quality profile
for _, profile := range *b {
if strings.EqualFold(profile.Name, profileName) {
return profile.Id, nil
}
}
return 0, errors.New("quality profile not found")
}
func (c *Client) lookupMediaItem(item *media.Item) (*lookupRequest, error) {
// prepare request
reqUrl, err := util.URLWithQuery(util.JoinURL(c.apiURL, "/series/lookup"),
url.Values{"term": []string{fmt.Sprintf("tvdb:%s", item.TvdbId)}})
if err != nil {
return nil, fmt.Errorf("generate series lookup request url: %w", err)
}
// send request
resp, err := rek.Get(reqUrl, rek.Headers(c.apiHeaders), rek.Timeout(c.apiTimeout))
if err != nil {
return nil, fmt.Errorf("request series lookup: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 {
return nil, fmt.Errorf("validate series lookup response: %s", resp.Status())
}
// decode response
b := new([]lookupRequest)
if err := json.NewDecoder(resp.Body()).Decode(b); err != nil {
return nil, fmt.Errorf("decode series lookup response: %w", err)
}
// find series
for _, s := range *b {
if strconv.Itoa(s.TvdbId) == item.TvdbId {
return &s, nil
}
}
return nil, fmt.Errorf("series lookup tvdbId: %v: %w", item.TvdbId, ErrItemNotFound)
}
func (c *Client) AddMediaItem(item *media.Item) error {
// prepare request
tvdbId, err := strconv.Atoi(item.TvdbId)
if err != nil {
return fmt.Errorf("converting tvdb id to int: %q", item.TvdbId)
}
req := addRequest{
Title: item.Title,
TitleSlug: item.Slug,
Year: item.Year,
QualityProfileId: c.qualityProfileId,
Images: []string{},
Tags: []string{},
Monitored: true,
RootFolderPath: c.rootFolder,
AddOptions: addOptions{
SearchForMissingEpisodes: true,
IgnoreEpisodesWithFiles: false,
IgnoreEpisodesWithoutFiles: false,
},
Seasons: []string{},
SeriesType: "standard",
SeasonFolder: true,
TvdbId: tvdbId,
}
// send request
resp, err := rek.Post(util.JoinURL(c.apiURL, "/series"), rek.Headers(c.apiHeaders), rek.Json(req),
rek.Timeout(c.apiTimeout))
if err != nil {
return fmt.Errorf("request add series: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 && resp.StatusCode() != 201 {
return fmt.Errorf("validate add series response: %s", resp.Status())
}
return nil
}

47
sonarr/expression.go Normal file
View File

@@ -0,0 +1,47 @@
package sonarr
import (
"fmt"
"github.com/antonmedv/expr"
"github.com/l3uddz/nabarr"
"github.com/l3uddz/nabarr/media"
"github.com/pkg/errors"
)
func (c *Client) compileExpressions(filters nabarr.PvrFilters) error {
exprEnv := &nabarr.ExprEnv{}
// compile ignores
for _, ignoreExpr := range filters.Ignores {
program, err := expr.Compile(ignoreExpr, expr.Env(exprEnv), expr.AsBool())
if err != nil {
return fmt.Errorf("ignore expression: %v: %w", ignoreExpr, err)
}
c.ignoresExpr = append(c.ignoresExpr, nabarr.NewExprProgram(ignoreExpr, program))
}
return nil
}
func (c *Client) ShouldIgnore(mediaItem *media.Item) (bool, string, error) {
exprItem := nabarr.NewExprEnv(mediaItem)
for _, expression := range c.ignoresExpr {
result, err := expr.Run(expression.Program, exprItem)
if err != nil {
return true, expression.String(), fmt.Errorf("checking ignore expression: %w", err)
}
expResult, ok := result.(bool)
if !ok {
return true, expression.String(), errors.New("type assert ignore expression result")
}
if expResult {
return true, expression.String(), nil
}
}
return false, "", nil
}

213
sonarr/queue.go Normal file
View File

@@ -0,0 +1,213 @@
package sonarr
import (
"errors"
"fmt"
"github.com/l3uddz/nabarr/media"
"github.com/lefelys/state"
)
func (c *Client) QueueFeedItem(item *media.FeedItem) {
c.queue <- item
}
func (c *Client) Start() state.State {
st, tail := state.WithShutdown()
go c.queueProcessor(tail)
return st
}
func (c *Client) queueProcessor(tail state.ShutdownTail) {
for {
select {
case <-tail.End():
// shutdown
tail.Done()
return
case feedItem := <-c.queue:
// stop processing
if feedItem == nil {
tail.Done()
return
}
// validate item has required id(s)
if feedItem.TvdbId == "" {
continue
}
// check cache / add item to cache
pvrCacheBucket := fmt.Sprintf("pvr_%s_%s", c.Type(), c.name)
cacheKey := fmt.Sprintf("tvdb_%s", feedItem.TvdbId)
if !c.testMode {
// not running in test mode, so use cache
if cacheValue, err := c.cache.Get(pvrCacheBucket, cacheKey); err == nil {
// item already exists in the cache
switch string(cacheValue) {
case c.name:
// perm cache entry, item exists in the pvr
continue
case c.cacheFiltersHash:
// temp cache entry, item recently checked with the same filters
continue
}
}
// insert temp cache entry
if err := c.cache.Put(pvrCacheBucket, cacheKey, []byte(c.cacheFiltersHash), c.cacheTempDuration); err != nil {
c.log.Error().
Err(err).
Msg("Failed storing item in temp cache")
}
}
// get media info
mediaItem, err := c.m.GetShowInfo(feedItem)
if err != nil {
if errors.Is(err, media.ErrItemNotFound) {
c.log.Debug().
Err(err).
Str("feed_title", feedItem.Title).
Str("feed_tvdb_id", feedItem.TvdbId).
Str("feed_name", feedItem.Feed).
Msg("Item was not found on trakt")
continue
}
c.log.Error().
Err(err).
Str("feed_title", feedItem.Title).
Str("feed_tvdb_id", feedItem.TvdbId).
Str("feed_name", feedItem.Feed).
Msg("Failed finding item on trakt")
continue
}
if c.testMode {
c.log.Debug().
Interface("trakt_item", mediaItem).
Msg("Item found on trakt")
}
// trakt expression check
ignore, filter, err := c.ShouldIgnore(mediaItem)
if err != nil {
c.log.Error().
Err(err).
Str("feed_title", mediaItem.FeedTitle).
Str("trakt_title", mediaItem.Title).
Str("trakt_tvdb_id", mediaItem.TvdbId).
Str("feed_name", feedItem.Feed).
Str("ignore_filter", filter).
Msg("Failed checking item against ignore filters")
continue
}
if ignore {
c.log.Debug().
Str("feed_title", mediaItem.FeedTitle).
Str("trakt_title", mediaItem.Title).
Str("trakt_tvdb_id", mediaItem.TvdbId).
Str("feed_name", feedItem.Feed).
Str("ignore_filter", filter).
Msg("Item matched ignore filters")
continue
}
// lookup item in pvr
s, err := c.lookupMediaItem(mediaItem)
if err != nil {
if errors.Is(err, ErrItemNotFound) {
// the item was not found
c.log.Warn().
Err(err).
Str("feed_title", mediaItem.FeedTitle).
Str("feed_tvdb_id", feedItem.TvdbId).
Str("feed_name", feedItem.Feed).
Msg("Item was not found via pvr lookup")
continue
}
c.log.Error().
Err(err).
Str("feed_title", mediaItem.FeedTitle).
Str("feed_tvdb_id", feedItem.TvdbId).
Str("feed_name", feedItem.Feed).
Msg("Failed finding item via pvr lookup")
}
if s.Id > 0 {
// item already existed in pvr
c.log.Debug().
Str("pvr_title", s.Title).
Int("pvr_year", s.Year).
Int("pvr_tvdb_id", s.TvdbId).
Str("feed_name", feedItem.Feed).
Msg("Item already existed in pvr")
// add item to perm cache (items already in pvr)
if !c.testMode {
if err := c.cache.Put(pvrCacheBucket, cacheKey, []byte(c.name), 0); err != nil {
c.log.Error().
Err(err).
Msg("Failed storing item in perm cache")
}
}
continue
}
// add item to pvr
c.log.Debug().
Str("feed_title", mediaItem.FeedTitle).
Str("trakt_title", mediaItem.Title).
Str("trakt_tvdb_id", mediaItem.TvdbId).
Int("trakt_year", mediaItem.Year).
Str("feed_name", feedItem.Feed).
Msg("Sending item to pvr")
if s.TitleSlug != "" {
// use slug from pvr search
mediaItem.Slug = s.TitleSlug
}
if c.testMode {
c.log.Info().
Err(err).
Str("trakt_title", mediaItem.Title).
Str("trakt_tvdb_id", mediaItem.TvdbId).
Int("trakt_year", mediaItem.Year).
Str("feed_name", feedItem.Feed).
Msg("Added item (test mode)")
continue
}
if err := c.AddMediaItem(mediaItem); err != nil {
c.log.Error().
Err(err).
Str("feed_title", mediaItem.FeedTitle).
Str("trakt_title", mediaItem.Title).
Str("trakt_tvdb_id", mediaItem.TvdbId).
Int("trakt_year", mediaItem.Year).
Str("feed_name", feedItem.Feed).
Msg("Failed adding item to pvr")
}
// add item to perm cache (item was added to pvr)
if !c.testMode {
if err := c.cache.Put(pvrCacheBucket, cacheKey, []byte(c.name), 0); err != nil {
c.log.Error().
Err(err).
Msg("Failed storing item in perm cache")
}
}
c.log.Info().
Err(err).
Str("trakt_title", mediaItem.Title).
Str("trakt_tvdb_id", mediaItem.TvdbId).
Int("trakt_year", mediaItem.Year).
Str("feed_name", feedItem.Feed).
Msg("Added item")
}
}
}

110
sonarr/sonarr.go Normal file
View File

@@ -0,0 +1,110 @@
package sonarr
import (
"fmt"
"github.com/l3uddz/nabarr"
"github.com/l3uddz/nabarr/cache"
"github.com/l3uddz/nabarr/logger"
"github.com/l3uddz/nabarr/media"
"github.com/l3uddz/nabarr/util"
"github.com/rs/zerolog"
"strings"
"time"
)
type Client struct {
pvrType string
name string
testMode bool
rootFolder string
qualityProfileId int
apiURL string
apiHeaders map[string]string
apiTimeout time.Duration
cache *cache.Client
cacheTempDuration time.Duration
cacheFiltersHash string
queue chan *media.FeedItem
m *media.Client
log zerolog.Logger
ignoresExpr []*nabarr.ExprProgram
}
func New(c nabarr.PvrConfig, mode string, m *media.Client, cc *cache.Client) (*Client, error) {
l := logger.New(c.Verbosity).With().
Str("pvr_name", c.Name).
Str("pvr_type", c.Type).
Logger()
// set config defaults (if not set)
if c.CacheDuration == 0 {
c.CacheDuration = 24 * time.Hour
}
// set api url
apiURL := ""
if strings.Contains(strings.ToLower(c.URL), "/api") {
apiURL = c.URL
} else {
apiURL = util.JoinURL(c.URL, "/api")
}
// set api headers
apiHeaders := map[string]string{
"X-Api-Key": c.ApiKey,
}
// create client
cl := &Client{
pvrType: "sonarr",
name: strings.ToLower(c.Name),
testMode: strings.EqualFold(mode, "test"),
rootFolder: c.RootFolder,
cache: cc,
cacheTempDuration: c.CacheDuration,
cacheFiltersHash: util.AsSHA256(c.Filters),
queue: make(chan *media.FeedItem, 1024),
apiURL: apiURL,
apiHeaders: apiHeaders,
apiTimeout: 60 * time.Second,
m: m,
log: l,
}
// compile expressions
if err := cl.compileExpressions(c.Filters); err != nil {
return nil, fmt.Errorf("compile expressions: %w", err)
}
// validate api access
ss, err := cl.getSystemStatus()
if err != nil {
return nil, fmt.Errorf("validate api: %w", err)
}
// get quality profile
if qid, err := cl.getQualityProfileId(c.QualityProfile); err != nil {
return nil, fmt.Errorf("get quality profile: %v: %w", c.QualityProfile, err)
} else {
cl.qualityProfileId = qid
}
cl.log.Info().
Str("pvr_version", ss.Version).
Msg("Initialised")
return cl, nil
}
func (c *Client) Type() string {
return c.pvrType
}

40
sonarr/struct.go Normal file
View File

@@ -0,0 +1,40 @@
package sonarr
type systemStatus struct {
Version string
}
type qualityProfile struct {
Name string
Id int
}
type lookupRequest struct {
Id int `json:"id,omitempty"`
Title string `json:"title"`
TitleSlug string `json:"titleSlug"`
Year int `json:"year,omitempty"`
TvdbId int `json:"tvdbId"`
}
type addRequest struct {
Title string `json:"title"`
TitleSlug string `json:"titleSlug"`
Year int `json:"year"`
QualityProfileId int `json:"qualityProfileId"`
Images []string `json:"images"`
Tags []string `json:"tags"`
Monitored bool `json:"monitored"`
RootFolderPath string `json:"rootFolderPath"`
AddOptions addOptions `json:"addOptions"`
Seasons []string `json:"seasons"`
SeriesType string `json:"seriesType"`
SeasonFolder bool `json:"seasonFolder"`
TvdbId int `json:"tvdbId"`
}
type addOptions struct {
SearchForMissingEpisodes bool `json:"searchForMissingEpisodes"`
IgnoreEpisodesWithFiles bool `json:"ignoreEpisodesWithFiles"`
IgnoreEpisodesWithoutFiles bool `json:"ignoreEpisodesWithoutFiles"`
}

32
util/misc.go Normal file
View File

@@ -0,0 +1,32 @@
package util
import (
"crypto/sha256"
"fmt"
"strconv"
"strings"
)
func Atoi(val string, defaultVal int) int {
n, err := strconv.Atoi(val)
if err != nil {
return defaultVal
}
return n
}
func Atof64(val string, defaultVal float64) float64 {
n, err := strconv.ParseFloat(strings.TrimSpace(val), 64)
if err != nil {
return defaultVal
}
return n
}
func AsSHA256(o interface{}) string {
// credits: https://blog.8bitzen.com/posts/22-08-2019-how-to-hash-a-struct-in-go
h := sha256.New()
h.Write([]byte(fmt.Sprintf("%v", o)))
return fmt.Sprintf("%x", h.Sum(nil))
}

24
util/url.go Normal file
View File

@@ -0,0 +1,24 @@
package util
import (
"fmt"
"net/url"
"path"
"strings"
)
func JoinURL(base string, paths ...string) string {
// credits: https://stackoverflow.com/a/57220413
p := path.Join(paths...)
return fmt.Sprintf("%s/%s", strings.TrimRight(base, "/"), strings.TrimLeft(p, "/"))
}
func URLWithQuery(base string, q url.Values) (string, error) {
u, err := url.Parse(base)
if err != nil {
return "", fmt.Errorf("url parse: %w", err)
}
u.RawQuery = q.Encode()
return u.String(), nil
}