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