diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bc7461ac..83ae47b5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: fetch-depth: '0' - uses: actions/setup-go@v3 with: - go-version: 'stable' + go-version: '1.20' - name: go-generate run: go generate ./... - name: make-signdmg @@ -60,7 +60,7 @@ jobs: fetch-depth: '0' - uses: actions/setup-go@v3 with: - go-version: 'stable' + go-version: '1.20' - name: make-release id: release run: | diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml index 35c386d4..4629d4e2 100644 --- a/examples/docker-compose.yml +++ b/examples/docker-compose.yml @@ -90,6 +90,16 @@ services: - UN_CMDHOOK_0_SHELL=false - UN_CMDHOOK_0_EXCLUDE_0= - UN_CMDHOOK_0_EVENTS_0=0 + # Web Server Config + - UN_WEBSERVER_METRICS=false + - UN_WEBSERVER_LISTEN_ADDR=0.0.0.0:5656 + - UN_WEBSERVER_LOG_FILE= + - UN_WEBSERVER_LOG_FILES=10 + - UN_WEBSERVER_LOG_FILE_MB=10 + - UN_WEBSERVER_SSL_CERT_FILE= + - UN_WEBSERVER_SSL_KEY_FILE= + - UN_WEBSERVER_URLBASE=/ + - UN_WEBSERVER_UPSTREAMS= security_opt: - no-new-privileges:true diff --git a/examples/unpackerr.conf.example b/examples/unpackerr.conf.example index 80a6f580..7fe22400 100644 --- a/examples/unpackerr.conf.example +++ b/examples/unpackerr.conf.example @@ -51,6 +51,26 @@ parallel = 1 file_mode = "0644" dir_mode = "0755" +[webserver] +## The web server currently only supports metrics; set this to true if you wish to use it. + metrics = false +## This may be set to a port or an ip:port to bind a specific IP. 0.0.0.0 binds ALL IPs. + listen_addr = "0.0.0.0:5656" +## Recommend setting a log file for HTTP requests. Otherwise, they go with other logs. + log_file = "" +## This app automatically rotates logs. Set these to the size and number to keep. + log_files = 10 + log_file_mb = 10 +## Set both of these to valid file paths to enable HTTPS/TLS. + ssl_cert_file = "" + ssl_key_file = "" +## Base URL from which to serve content. + urlbase = "/" +## Upstreams should be set to the IP or CIDR of your trusted upstream proxy. +## Setting this correctly allows X-Forwarded-For to be used in logs. +## In the future it may control auth proxy trust. Must be a list of strings. + upstreams = [ ] # example: upstreams = [ "127.0.0.1/32", "10.1.2.0/24" ] + ##-Notes-#######-READ THIS!!!-################################################## ## The following sections can be repeated if you have more than one Sonarr, ## ## Radarr or Lidarr, Readarr, Folder, Webhook, or Command Hook. ## diff --git a/go.mod b/go.mod index 22776d19..0680e30c 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,24 @@ module github.com/Unpackerr/unpackerr -go 1.19 +go 1.20 require ( github.com/fsnotify/fsnotify v1.6.0 github.com/gen2brain/dlgs v0.0.0-20220603100644-40c77870fa8d - github.com/getlantern/systray v1.2.1 + github.com/getlantern/systray v1.2.2 github.com/gonutz/w32 v1.0.0 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b + github.com/julienschmidt/httprouter v1.3.0 + github.com/lestrrat-go/apache-logformat/v2 v2.0.6 github.com/mitchellh/go-homedir v1.1.0 + github.com/prometheus/client_golang v1.15.1 github.com/radovskyb/watcher v1.0.7 github.com/spf13/pflag v1.0.6-0.20201009195203-85dd5c8bc61c golang.org/x/mod v0.10.0 golift.io/cnfg v0.2.2 - golift.io/cnfgfile v0.0.0-20230324082957-6be76a6e033e - golift.io/rotatorr v0.0.0-20230317103044-d974d22ee164 - golift.io/starr v0.14.1-0.20230105161307-212936057ff2 + golift.io/cnfgfile v0.0.0-20230519070633-a07e5db66d2d + golift.io/rotatorr v0.0.0-20230520191821-3b26224a1624 + golift.io/starr v0.14.1-0.20230519072620-7ed4f9cf36d2 golift.io/version v0.0.2 golift.io/xtractr v0.2.2 ) @@ -23,35 +26,46 @@ require ( require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/andybalholm/brotli v1.0.5 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/sevenzip v1.4.1 // indirect github.com/bodgit/windows v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect github.com/getlantern/errors v1.0.3 // indirect - github.com/getlantern/golog v0.0.0-20230206140254-6d0a2e0f79af // indirect + github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 // indirect github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc // indirect github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 // indirect - github.com/getlantern/ops v0.0.0-20230424193308-26325dfed3cf // indirect + github.com/getlantern/ops v0.0.0-20230519221840-1283e026181c // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-stack/stack v1.8.1 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/kdomanski/iso9660 v0.3.3 // indirect + github.com/kdomanski/iso9660 v0.3.5 // indirect github.com/klauspost/compress v1.16.5 // indirect + github.com/lestrrat-go/strftime v1.0.6 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/nwaples/rardecode v1.1.3 // indirect github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect github.com/pierrec/lz4/v4 v4.1.17 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.10.0 // indirect github.com/ulikunitz/xz v0.5.11 // indirect - go.opentelemetry.io/otel v1.14.0 // indirect - go.opentelemetry.io/otel/trace v1.14.0 // indirect - go.uber.org/atomic v1.10.0 // indirect + go.opentelemetry.io/otel v1.16.0 // indirect + go.opentelemetry.io/otel/metric v1.16.0 // indirect + go.opentelemetry.io/otel/trace v1.16.0 // indirect + go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.24.0 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect - golang.org/x/net v0.9.0 // indirect - golang.org/x/sys v0.7.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index afdf9b7d..d13c13cb 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/ github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= github.com/bodgit/sevenzip v1.4.1 h1:otI04zch761Czrw1J3q0z6RXERRFH2eAOw7TMxG9hWo= @@ -30,6 +32,8 @@ github.com/bodgit/sevenzip v1.4.1/go.mod h1:wzG7p52yTqFNEmEWYn3ibtMa0eXaP0EFjrWx github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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= @@ -39,6 +43,8 @@ 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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gen2brain/dlgs v0.0.0-20220603100644-40c77870fa8d h1:dHYKX8CBAs1zSGXm3q3M15CLAEwPEkwrK1ed8FCo+Xo= @@ -51,8 +57,8 @@ github.com/getlantern/errors v1.0.1/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHq github.com/getlantern/errors v1.0.3 h1:Ne4Ycj7NI1BtSyAfVeAT/DNoxz7/S2BUc3L2Ht1YSHE= github.com/getlantern/errors v1.0.3/go.mod h1:m8C7H1qmouvsGpwQqk/6NUpIVMpfzUPn608aBZDYV04= github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc= -github.com/getlantern/golog v0.0.0-20230206140254-6d0a2e0f79af h1:cvD5qCZpH/Q32Ae0i1W1lRkVuM21czEZaJpTuRiJjc4= -github.com/getlantern/golog v0.0.0-20230206140254-6d0a2e0f79af/go.mod h1:+ZU1h+iOVqWReBpky6d5Y2WL0sF2Llxu+QcxJFs2+OU= +github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 h1:NlQedYmPI3pRAXJb+hLVVDGqfvvXGRPV8vp7XOjKAZ0= +github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65/go.mod h1:+ZU1h+iOVqWReBpky6d5Y2WL0sF2Llxu+QcxJFs2+OU= github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o= github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc h1:sue+aeVx7JF5v36H1HfvcGFImLpSD5goj8d+MitovDU= github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc/go.mod h1:D9RWpXy/EFPYxiKUURo2TB8UBosbqkiLhttRrZYtvqM= @@ -61,10 +67,10 @@ github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 h1:cSrD9ryDfTV2y github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770/go.mod h1:GOQsoDnEHl6ZmNIL+5uVo+JWRFWozMEp18Izcb++H+A= github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA= github.com/getlantern/ops v0.0.0-20220713155959-1315d978fff7/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA= -github.com/getlantern/ops v0.0.0-20230424193308-26325dfed3cf h1:q8nsH0Lx9fP8HY6T9rA1zogvOzO9JtbUI5BXkh7wxxI= -github.com/getlantern/ops v0.0.0-20230424193308-26325dfed3cf/go.mod h1:R7HfJVLsnSeqaDWkiUlU+ANBjac4oYmXGrrps8vW7CM= -github.com/getlantern/systray v1.2.1 h1:udsC2k98v2hN359VTFShuQW6GGprRprw6kD6539JikI= -github.com/getlantern/systray v1.2.1/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM= +github.com/getlantern/ops v0.0.0-20230519221840-1283e026181c h1:qcPAzA1ZDnwx618jAgQmxo6UvJkw2SkM1L4ofncmEhI= +github.com/getlantern/ops v0.0.0-20230519221840-1283e026181c/go.mod h1:g2ueCncOwWenlAr56Fh90FwsACkelqqtFUDLAHg1mng= +github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE= +github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE= 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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -90,6 +96,9 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 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.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/gonutz/w32 v1.0.0 h1:3t1z6ZfkFvirjFYBx9pHeHBuKoN/VBVk9yHb/m2Ll/k= github.com/gonutz/w32 v1.0.0/go.mod h1:Rc/YP5K9gv0FW4p6X9qL3E7Y56lfMflEol1fLElfMW4= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -98,6 +107,7 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a 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.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -121,16 +131,29 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 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/kdomanski/iso9660 v0.3.3 h1:cNwM9L2L1Hzc5hZWGy6fPJ92UyWDccaY69DmEPlfDNY= -github.com/kdomanski/iso9660 v0.3.3/go.mod h1:K+UlIGxKgtrdAWyoigPnFbeQLVs/Xudz4iztWFThBwo= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kdomanski/iso9660 v0.3.5 h1:LO1n75zPjLeDQkz0Pyk1eZ7JGinjKjk2C174GSABVwY= +github.com/kdomanski/iso9660 v0.3.5/go.mod h1:K+UlIGxKgtrdAWyoigPnFbeQLVs/Xudz4iztWFThBwo= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -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/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 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/lestrrat-go/apache-logformat/v2 v2.0.6 h1:MDyexlEMjFnXXuNemorO/SgUjkpWz6rKmYPkx1CgQg8= +github.com/lestrrat-go/apache-logformat/v2 v2.0.6/go.mod h1:meGwIaUOWH7yPDXUSxMTVTMdH9HkGTO1oN2z/4n/yPs= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= +github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= +github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ= +github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= @@ -139,15 +162,26 @@ github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgF github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -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 v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= +github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/procfs v0.10.0 h1:UkG7GPYkO4UZyLnyXjaWYcgOSONqwdBqFUT95ugmt6I= +github.com/prometheus/procfs v0.10.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE= github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/spf13/pflag v1.0.6-0.20201009195203-85dd5c8bc61c h1:zqmyTlQyufRC65JnImJ6H1Sf7BDj8bG31EV919NVEQc= github.com/spf13/pflag v1.0.6-0.20201009195203-85dd5c8bc61c/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -159,8 +193,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -170,14 +204,16 @@ 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.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo= -go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM= -go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= +go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= +go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= +go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= +go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo= -go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= -go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= +go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= +go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= @@ -239,8 +275,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/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= @@ -266,7 +302,7 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w 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-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -274,9 +310,10 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -324,12 +361,12 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golift.io/cnfg v0.2.2 h1:m60kGVZQLuzAx939fLX8wSEKk2fpZdX6+HOjdYuUfCw= golift.io/cnfg v0.2.2/go.mod h1:XXOjkKsn3GJonpWRKKkU3k98AWgvkM28W90IdV4rRkc= -golift.io/cnfgfile v0.0.0-20230324082957-6be76a6e033e h1:QATb4OzNFaru2rukoNRo+TIApM8G+wE0+VHB3tyR3EY= -golift.io/cnfgfile v0.0.0-20230324082957-6be76a6e033e/go.mod h1:K+Dg62wTc+z8b3iE8brS7rrDSRevWBQzSPNu8HnE+JI= -golift.io/rotatorr v0.0.0-20230317103044-d974d22ee164 h1:3buKpmb715F3AHZKVhL9+ZhioMdjKAbuLeuo5Qcc190= -golift.io/rotatorr v0.0.0-20230317103044-d974d22ee164/go.mod h1:010QO3LP1dFibSi7K5Uqi7v/C6j6XAEhCkgYuSpFlHc= -golift.io/starr v0.14.1-0.20230105161307-212936057ff2 h1:U4c05hn3ruhuTSF1YlfUz87WqbBqTuOzVCzX0RoZxYI= -golift.io/starr v0.14.1-0.20230105161307-212936057ff2/go.mod h1:nxaRArgcVRjXO74QjTtu0y9/b/OYSQl1nyq0Tz8LDFQ= +golift.io/cnfgfile v0.0.0-20230519070633-a07e5db66d2d h1:LydDVem7g3k4w8mNdoLYCIGJ4JyVQcGMbZZXwyRkjj8= +golift.io/cnfgfile v0.0.0-20230519070633-a07e5db66d2d/go.mod h1:K+Dg62wTc+z8b3iE8brS7rrDSRevWBQzSPNu8HnE+JI= +golift.io/rotatorr v0.0.0-20230520191821-3b26224a1624 h1:gPe9Rl6e6Lbp5pbanmn2ADlQEBFVoKgGbmPJJtURCbk= +golift.io/rotatorr v0.0.0-20230520191821-3b26224a1624/go.mod h1:mXltnLJV69BHLXCexH3uulsIUBCzq8v69Bh77FMXYBo= +golift.io/starr v0.14.1-0.20230519072620-7ed4f9cf36d2 h1:idrmdxuoZu1Sx5dqRyyQoUjH1MdJGSGo+bsHcxE/NhA= +golift.io/starr v0.14.1-0.20230519072620-7ed4f9cf36d2/go.mod h1:C8jb7qynUVJMrCOjYzBhfaudeX03f2eXhcnk56XTcCo= golift.io/version v0.0.2 h1:i0gXRuSDHKs4O0sVDUg4+vNIuOxYoXhaxspftu2FRTE= golift.io/version v0.0.2/go.mod h1:76aHNz8/Pm7CbuxIsDi97jABL5Zui3f2uZxDm4vB6hU= golift.io/xtractr v0.2.2 h1:MvujxeuX629d1rQs2VJbbcvYMvMmN5SzIkEflU5ryOc= @@ -367,9 +404,14 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac 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/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= 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/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/unpackerr/apps.go b/pkg/unpackerr/apps.go index cde10ddb..0800eff8 100644 --- a/pkg/unpackerr/apps.go +++ b/pkg/unpackerr/apps.go @@ -6,6 +6,7 @@ import ( "sync" "golift.io/cnfg" + "golift.io/starr" ) /* This file contains all the unique bits for each app. When adding a new app, @@ -28,11 +29,6 @@ const ( // These are the names used to identify each app. const ( - Sonarr = "Sonarr" - Radarr = "Radarr" - Lidarr = "Lidarr" - Readarr = "Readarr" - Whisparr = "Whisparr" FolderString = "Folder" ) @@ -63,6 +59,7 @@ type Config struct { Buffer uint `json:"buffer" toml:"buffer" xml:"buffer" yaml:"buffer"` //nolint:lll // undocumented. KeepHistory uint `json:"keepHistory" toml:"keep_history" xml:"keep_history" yaml:"keepHistory"` //nolint:lll // undocumented. Passwords StringSlice `json:"passwords" toml:"passwords" xml:"password" yaml:"passwords"` + Webserver *WebServer `json:"webserver" toml:"webserver" xml:"webserver" yaml:"webserver"` Lidarr []*LidarrConfig `json:"lidarr,omitempty" toml:"lidarr" xml:"lidarr" yaml:"lidarr,omitempty"` Radarr []*RadarrConfig `json:"radarr,omitempty" toml:"radarr" xml:"radarr" yaml:"radarr,omitempty"` Whisparr []*RadarrConfig `json:"whisparr,omitempty" toml:"whisparr" xml:"whisparr" yaml:"whisparr,omitempty"` @@ -142,24 +139,24 @@ func (u *Unpackerr) validateApps() error { return nil } -func (u *Unpackerr) haveQitem(name, app string) bool { +func (u *Unpackerr) haveQitem(name string, app starr.App) bool { switch app { - case Lidarr: + case starr.Lidarr: return u.haveLidarrQitem(name) - case Radarr: + case starr.Radarr: return u.haveRadarrQitem(name) - case Readarr: + case starr.Readarr: return u.haveReadarrQitem(name) - case Sonarr: + case starr.Sonarr: return u.haveSonarrQitem(name) - case Whisparr: + case starr.Whisparr: return u.haveWhisparrQitem(name) default: return false } } -// StringSlice allows a special environment variable unmarshaller for a lost of strings. +// StringSlice allows a special environment variable unmarshaller for a lot of strings. type StringSlice []string // UnmarshalENV turns environment variables into a string slice. diff --git a/pkg/unpackerr/cmdhook.go b/pkg/unpackerr/cmdhook.go index a6765578..dd3d459c 100644 --- a/pkg/unpackerr/cmdhook.go +++ b/pkg/unpackerr/cmdhook.go @@ -52,7 +52,7 @@ func (u *Unpackerr) runCmdhookWithLog(hook *WebhookConfig, payload *WebhookPaylo switch { case err != nil: - u.Printf("[ERROR] Command Hook (%s) %s: %v: %s", payload.Event, hook.Name, err, out.String()) + u.Errorf("Command Hook (%s) %s: %v: %s", payload.Event, hook.Name, err, out.String()) hook.fails++ case hook.Silent || out == nil: u.Printf("[Cmdhook] Queue: %d/%d. Ran command %s", len(u.hookChan), cap(u.hookChan), hook.Name) @@ -121,7 +121,7 @@ func (u *Unpackerr) logCmdhook() { if len(u.Cmdhook) == 1 { pfx = " => Command Hook Config: 1 cmd" } else { - u.Print(" => Command Hook Configs:", len(u.Cmdhook), "cmds") + u.Printf(" => Command Hook Configs: %d commands", len(u.Cmdhook)) pfx = " => Command" } diff --git a/pkg/unpackerr/folder.go b/pkg/unpackerr/folder.go index 5da724a0..0896f5ca 100644 --- a/pkg/unpackerr/folder.go +++ b/pkg/unpackerr/folder.go @@ -39,6 +39,7 @@ type Folders struct { Events chan *eventData Updates chan *xtractr.Response Printf func(msg string, v ...interface{}) + Errorf func(msg string, v ...interface{}) Debugf func(msg string, v ...interface{}) FSNotify *fsnotify.Watcher Watcher *watcher.Watcher @@ -72,7 +73,7 @@ func (u *Unpackerr) logFolders() { folder.Path, epath, folder.DeleteAfter, folder.DeleteOrig, !folder.DisableLog, folder.MoveBack, folder.ExtractISOs, u.Buffer) } else { - u.Print(" => Folder Config:", count, "paths,", "event buffer:", u.Buffer) + u.Printf(" => Folder Config: %d paths, event buffer: %d ", count, u.Buffer) for _, folder := range u.Folders { if epath = ""; folder.ExtractPath != "" { @@ -100,7 +101,7 @@ func (u *Unpackerr) PollFolders() { u.Folders, flist = u.checkFolders() if err = u.newFolderWatcher(); err != nil { - u.Print("[ERROR] Watching Folders:", err) + u.Errorf("Watching Folders: %s", err) return } // do not close either watcher. @@ -110,7 +111,7 @@ func (u *Unpackerr) PollFolders() { } go u.folders.watchFSNotify() - u.Print("[Folder] Watching (fsnotify):", strings.Join(flist, ", ")) + u.Printf("[Folder] Watching (fsnotify): %s", strings.Join(flist, ", ")) if u.Folder.Interval.Duration == 0 { return @@ -118,7 +119,7 @@ func (u *Unpackerr) PollFolders() { go func() { if err := u.folders.Watcher.Start(u.Folder.Interval.Duration); err != nil { - u.Print("[ERROR] Folder poller stopped:", err) + u.Errorf("Folder poller stopped: %v", err) } }() u.Printf("[Folder] Polling @ %v: %s", u.Folder.Interval, strings.Join(flist, ", ")) @@ -134,6 +135,7 @@ func (u *Unpackerr) newFolderWatcher() error { Events: make(chan *eventData, u.Config.Buffer), Updates: make(chan *xtractr.Response, updateChanBuf), Debugf: u.Debugf, + Errorf: u.Errorf, Printf: u.Printf, } @@ -154,11 +156,11 @@ func (u *Unpackerr) newFolderWatcher() error { for _, folder := range u.Folders { if err := u.folders.Watcher.Add(folder.Path); err != nil { - u.Printf("[ERROR] Folder '%s' (cannot poll): %v", folder.Path, err) + u.Errorf("Folder '%s' (cannot poll): %v", folder.Path, err) } if err := fsn.Add(folder.Path); err != nil { - u.Printf("[ERROR] Folder '%s' (cannot watch): %v", folder.Path, err) + u.Errorf("Folder '%s' (cannot watch): %v", folder.Path, err) } } @@ -201,16 +203,16 @@ func (u *Unpackerr) checkFolders() ([]*FolderConfig, []string) { for _, folder := range u.Folders { path, err := filepath.Abs(folder.Path) if err != nil { - u.Printf("[ERROR] Folder '%s' (bad path): %v", folder.Path, err) + u.Errorf("Folder '%s' (bad path): %v", folder.Path, err) continue } folder.Path = path // rewrite it. might not be safe. if stat, err := os.Stat(folder.Path); err != nil { - u.Printf("[ERROR] Folder '%s' (cannot watch): %v", folder.Path, err) + u.Errorf("Folder '%s' (cannot watch): %v", folder.Path, err) continue } else if !stat.IsDir() { - u.Printf("[ERROR] Folder '%s' (cannot watch): not a folder", folder.Path) + u.Errorf("Folder '%s' (cannot watch): not a folder", folder.Path) continue } @@ -250,7 +252,7 @@ func (u *Unpackerr) extractFolder(name string, folder *Folder) { LogFile: !folder.cnfg.DisableLog, }) if err != nil { - u.Print("[ERROR]", err) + u.Errorf("[ERROR] %v", err) return } @@ -273,9 +275,9 @@ func (u *Unpackerr) folderXtractrCallback(resp *xtractr.Response) { folder.step = EXTRACTING case resp.Error != nil: - u.Printf("[Folder] Extraction Error: %s: %v", resp.X.Name, resp.Error) - folder.step = EXTRACTFAILED + u.Errorf("[Folder] %s: %s: %v", folder.step.String(), resp.X.Name, resp.Error) + u.updateMetrics(resp, FolderString, folder.cnfg.Path) for _, v := range resp.Archives { folder.rars = append(folder.rars, v...) @@ -285,6 +287,7 @@ func (u *Unpackerr) folderXtractrCallback(resp *xtractr.Response) { folder.rars = append(folder.rars, v...) } + u.updateMetrics(resp, FolderString, folder.cnfg.Path) u.Printf("[Folder] Extraction Finished: %s => elapsed: %v, archives: %d, "+ "extra archives: %d, files extracted: %d, written: %dMiB", resp.X.Name, resp.Elapsed.Round(time.Second), len(folder.rars), @@ -312,9 +315,9 @@ func (f *Folders) watchFSNotify() { for { select { case err := <-f.Watcher.Error: - f.Printf("[ERROR] watcher: %v", err) + f.Errorf("watcher: %v", err) case err := <-f.FSNotify.Errors: - f.Printf("[ERROR] fsnotify: %v", err) + f.Errorf("fsnotify: %v", err) case event, ok := <-f.FSNotify.Events: if !ok { return @@ -378,7 +381,7 @@ func (f *Folders) processEvent(event *eventData) { if err := f.Add(dirPath); err != nil { if !errors.Is(err, os.ErrNotExist) { - f.Printf("[ERROR] Folder: Tracking New Item: %v: %v", dirPath, err) + f.Errorf("Folder: Tracking New Item: %v: %v", dirPath, err) } return diff --git a/pkg/unpackerr/handlers.go b/pkg/unpackerr/handlers.go index a82b00e0..040b9aa4 100644 --- a/pkg/unpackerr/handlers.go +++ b/pkg/unpackerr/handlers.go @@ -16,7 +16,8 @@ type Extract struct { Syncthing bool Retries uint Path string - App string + App starr.App + URL string Updated time.Time DeleteDelay time.Duration DeleteOrig bool @@ -122,7 +123,7 @@ func (u *Unpackerr) handleCompletedDownload(name string, x *Extract) { u.Printf("[%s] Extraction Queued: %s, extractable files: %d, delete orig: %v, items in queue: %d", item.App, item.Path, len(files), item.DeleteOrig, queueSize) - u.updateHistory(item.App + ": " + item.Path) + u.updateHistory(string(item.App) + ": " + item.Path) } func (u *Unpackerr) getPasswordFromPath(s string) string { @@ -177,7 +178,7 @@ func (u *Unpackerr) checkExtractDone() { } } -// handleXtractrCallback handles callbacks from the xtractr library for sonarr/radarr/lidarr. +// handleXtractrCallback handles callbacks from the xtractr library for starr apps (not folders). // This takes the provided info and logs it then sends it the queue update method. func (u *Unpackerr) handleXtractrCallback(resp *xtractr.Response) { switch { @@ -187,17 +188,19 @@ func (u *Unpackerr) handleXtractrCallback(resp *xtractr.Response) { case resp.Error != nil: u.Printf("Extraction Error: %s: %v", resp.X.Name, resp.Error) u.updateQueueStatus(&newStatus{Name: resp.X.Name, Status: EXTRACTFAILED, Resp: resp}, true) + u.updateMetrics(resp, u.Map[resp.X.Name].App, u.Map[resp.X.Name].URL) default: u.Printf("Extraction Finished: %s => elapsed: %v, archives: %d, extra archives: %d, "+ "files extracted: %d, wrote: %dMiB", resp.X.Name, resp.Elapsed.Round(time.Second), len(resp.Archives), len(resp.Extras), len(resp.NewFiles), resp.Size/mebiByte) + u.updateMetrics(resp, u.Map[resp.X.Name].App, u.Map[resp.X.Name].URL) u.updateQueueStatus(&newStatus{Name: resp.X.Name, Status: EXTRACTED, Resp: resp}, true) } } // Looking for a message that looks like: // "No files found are eligible for import in /downloads/Downloading/Space.Warriors.S99E88.GrOuP.1080p.WEB.x264". -func (u *Unpackerr) getDownloadPath(s []*starr.StatusMessage, app, title string, paths []string) string { +func (u *Unpackerr) getDownloadPath(s []*starr.StatusMessage, app starr.App, title string, paths []string) string { var errs []error for _, path := range paths { diff --git a/pkg/unpackerr/lidarr.go b/pkg/unpackerr/lidarr.go index 2d079898..c6faf441 100644 --- a/pkg/unpackerr/lidarr.go +++ b/pkg/unpackerr/lidarr.go @@ -6,6 +6,7 @@ import ( "net/http" "strings" "sync" + "time" "golift.io/starr" "golift.io/starr/lidarr" @@ -25,7 +26,7 @@ func (u *Unpackerr) validateLidarr() error { for i := range u.Lidarr { if u.Lidarr[i].URL == "" { - u.Printf("Missing Lidarr URL in one of your configurations, skipped and ignored.") + u.Errorf("Missing Lidarr URL in one of your configurations, skipped and ignored.") continue } @@ -82,7 +83,7 @@ func (u *Unpackerr) logLidarr() { u.Lidarr[0].ValidSSL, u.Lidarr[0].Protocols, u.Lidarr[0].Syncthing, u.Lidarr[0].DeleteOrig, u.Lidarr[0].DeleteDelay.Duration, u.Lidarr[0].Paths) } else { - u.Print(" => Lidarr Config:", c, "servers") + u.Printf(" => Lidarr Config: %d servers", c) for _, f := range u.Lidarr { u.Printf(" => Server: %s, apikey:%v, timeout:%v, verify ssl:%v, protos:%s, "+ @@ -98,19 +99,20 @@ func (u *Unpackerr) getLidarrQueue() { for _, server := range u.Lidarr { if server.APIKey == "" { u.Debugf("Lidarr (%s): skipped, no API key", server.URL) - continue } + start := time.Now() + queue, err := server.GetQueue(DefaultQueuePageSize, DefaultQueuePageSize) if err != nil { - u.Printf("[ERROR] Lidarr (%s): %v", server.URL, err) - + u.saveQueueMetrics(0, start, starr.Lidarr, server.URL, err) return } // Only update if there was not an error fetching. server.Queue = queue + u.saveQueueMetrics(server.Queue.TotalRecords, start, starr.Lidarr, server.URL, nil) if !u.Activity || queue.TotalRecords > 0 { u.Printf("[Lidarr] Updated (%s): %d Items Queued, %d Retrieved", server.URL, queue.TotalRecords, len(queue.Records)) @@ -128,18 +130,19 @@ func (u *Unpackerr) checkLidarrQueue() { for _, q := range server.Queue.Records { switch x, ok := u.Map[q.Title]; { case ok && x.Status == EXTRACTED && u.isComplete(q.Status, q.Protocol, server.Protocols): - u.Debugf("%s (%s): Item Waiting for Import (%s): %v", Lidarr, server.URL, q.Protocol, q.Title) + u.Debugf("%s (%s): Item Waiting for Import (%s): %v", starr.Lidarr, server.URL, q.Protocol, q.Title) case (!ok || x.Status < QUEUED) && u.isComplete(q.Status, q.Protocol, server.Protocols): // This shoehorns the Lidarr OutputPath into a StatusMessage that getDownloadPath can parse. q.StatusMessages = append(q.StatusMessages, &starr.StatusMessage{Title: q.Title, Messages: []string{prefixPathMsg + q.OutputPath}}) u.handleCompletedDownload(q.Title, &Extract{ - App: Lidarr, + App: starr.Lidarr, + URL: server.URL, DeleteOrig: server.DeleteOrig, DeleteDelay: server.DeleteDelay.Duration, Syncthing: server.Syncthing, - Path: u.getDownloadPath(q.StatusMessages, Lidarr, q.Title, server.Paths), + Path: u.getDownloadPath(q.StatusMessages, starr.Lidarr, q.Title, server.Paths), IDs: map[string]interface{}{ "title": q.Title, "artistId": q.ArtistID, @@ -151,7 +154,7 @@ func (u *Unpackerr) checkLidarrQueue() { fallthrough default: u.Debugf("%s: (%s): %s (%s:%d%%): %v", - Lidarr, server.URL, q.Status, q.Protocol, percent(q.Sizeleft, q.Size), q.Title) + starr.Lidarr, server.URL, q.Status, q.Protocol, percent(q.Sizeleft, q.Size), q.Title) } } } diff --git a/pkg/unpackerr/logs.go b/pkg/unpackerr/logs.go index e72f5c55..0f49edb7 100644 --- a/pkg/unpackerr/logs.go +++ b/pkg/unpackerr/logs.go @@ -82,25 +82,23 @@ func (status ExtractStatus) String() string { // Debugf writes Debug log lines... to stdout and/or a file. func (l *Logger) Debugf(msg string, v ...interface{}) { - if l.debug { - err := l.Logger.Output(callDepth, "[DEBUG] "+fmt.Sprintf(msg, v...)) - if err != nil { - fmt.Println("Logger Error:", err) //nolint:forbidigo - } + err := l.Debug.Output(callDepth, fmt.Sprintf(msg, v...)) + if err != nil { + fmt.Println("Logger Error:", err) //nolint:forbidigo } } -// Print writes log lines... to stdout and/or a file. -func (l *Logger) Print(v ...interface{}) { - err := l.Logger.Output(callDepth, fmt.Sprintln(v...)) +// Printf writes log lines... to stdout and/or a file. +func (l *Logger) Printf(msg string, v ...interface{}) { + err := l.Info.Output(callDepth, fmt.Sprintf(msg, v...)) if err != nil { fmt.Println("Logger Error:", err) //nolint:forbidigo } } -// Printf writes log lines... to stdout and/or a file. -func (l *Logger) Printf(msg string, v ...interface{}) { - err := l.Logger.Output(callDepth, fmt.Sprintf(msg, v...)) +// Errorf writes log errors... to stdout and/or a file. +func (l *Logger) Errorf(msg string, v ...interface{}) { + err := l.Error.Output(callDepth, fmt.Sprintf(msg, v...)) if err != nil { fmt.Println("Logger Error:", err) //nolint:forbidigo } @@ -108,53 +106,22 @@ func (l *Logger) Printf(msg string, v ...interface{}) { // logCurrentQueue prints the number of things happening. func (u *Unpackerr) logCurrentQueue() { - var ( - waiting uint - queued uint - extracting uint - failed uint - extracted uint - imported uint - deleted uint - hookOK, hookFail = u.WebhookCounts() - cmdOK, cmdFail = u.CmdhookCounts() - ) - - for name := range u.Map { - switch u.Map[name].Status { - case WAITING: - waiting++ - case QUEUED: - queued++ - case EXTRACTING: - extracting++ - case DELETEFAILED, EXTRACTFAILED: - failed++ - case EXTRACTED: - extracted++ - case DELETED, DELETING: - deleted++ - case IMPORTED: - imported++ - } - } + s := u.stats() u.Printf("[Unpackerr] Queue: [%d waiting] [%d queued] [%d extracting] [%d extracted] [%d imported]"+ - " [%d failed] [%d deleted]", waiting, queued, extracting, extracted, imported, failed, deleted) + " [%d failed] [%d deleted]", s.Waiting, s.Queued, s.Extracting, s.Extracted, s.Imported, s.Failed, s.Deleted) u.Printf("[Unpackerr] Totals: [%d retries] [%d finished] [%d|%d webhooks]"+ " [%d|%d cmdhooks] [stacks; event:%d, hook:%d, del:%d]", - u.Retries, u.Finished, hookOK, hookFail, cmdOK, cmdFail, + u.Retries, u.Finished, s.HookOK, s.HookFail, s.CmdOK, s.CmdFail, len(u.folders.Events)+len(u.updates)+len(u.folders.Updates), len(u.hookChan), len(u.delChan)) - u.updateTray(u.Retries, u.Finished, waiting, queued, extracting, failed, extracted, imported, deleted, - hookOK, hookFail, uint(len(u.folders.Events)+len(u.updates)+len(u.folders.Updates)+len(u.delChan)+len(u.hookChan))) + u.updateTray(s, uint(len(u.folders.Events)+len(u.updates)+len(u.folders.Updates)+len(u.delChan)+len(u.hookChan))) } // setupLogging splits log write into a file and/or stdout. func (u *Unpackerr) setupLogging() { - u.Logger.debug = u.Config.Debug - - if u.Logger.Logger.SetFlags(log.LstdFlags); u.Config.Debug { - u.Logger.Logger.SetFlags(log.Lshortfile | log.Lmicroseconds | log.Ldate) + if u.Config.Debug { + u.Logger.Info.SetFlags(log.Lshortfile | log.Lmicroseconds | log.Ldate) + u.Logger.Error.SetFlags(log.Lshortfile | log.Lmicroseconds | log.Ldate) } logFile, err := homedir.Expand(u.Config.LogFile) @@ -162,9 +129,8 @@ func (u *Unpackerr) setupLogging() { logFile = u.Config.LogFile } - u.Config.LogFile = logFile rotate := &rotatorr.Config{ - Filepath: u.Config.LogFile, // log file name. + Filepath: logFile, // log file name. FileSize: int64(u.Config.LogFileMb) * megabyte, // megabytes Rotatorr: &timerotator.Layout{ FileCount: u.Config.LogFiles, @@ -173,27 +139,64 @@ func (u *Unpackerr) setupLogging() { DirMode: logsDirMode, } - var writer io.Writer + if logFile != "" { + u.rotatorr = rotatorr.NewMust(rotate) + } switch { // only use MultiWriter if we have > 1 writer. - case !u.Config.Quiet && u.Config.LogFile != "": - u.rotatorr = rotatorr.NewMust(rotate) - writer = io.MultiWriter(u.rotatorr, os.Stdout) - log.SetOutput(writer) - case !u.Config.Quiet && u.Config.LogFile == "": - writer = os.Stdout - case u.Config.LogFile == "": - writer = io.Discard // default is "nothing" + case !u.Config.Quiet && logFile != "": + u.updateLogOutput(io.MultiWriter(u.rotatorr, os.Stdout)) + case !u.Config.Quiet && logFile == "": + u.updateLogOutput(os.Stdout) + case logFile == "": + u.updateLogOutput(io.Discard) // default is "nothing" default: - u.rotatorr = rotatorr.NewMust(rotate) - writer = u.rotatorr - log.SetOutput(writer) + u.updateLogOutput(u.rotatorr) } +} - u.Logger.Logger.SetOutput(writer) +func (u *Unpackerr) updateLogOutput(writer io.Writer) { + if u.Webserver != nil && u.Webserver.LogFile != "" { + u.setupHTTPLogging() + } else { + u.Logger.HTTP.SetOutput(writer) + } + + if u.Config.Debug { + u.Logger.Debug.SetOutput(writer) + } + + log.SetOutput(writer) // catch out-of-scope garbage + u.Logger.Info.SetOutput(writer) + u.Logger.Error.SetOutput(writer) u.postLogRotate("", "") } +func (u *Unpackerr) setupHTTPLogging() { + logFile, err := homedir.Expand(u.Webserver.LogFile) + if err != nil { + logFile = u.Webserver.LogFile + } + + rotate := &rotatorr.Config{ + Filepath: logFile, // log file name. + FileSize: int64(u.Webserver.LogFileMb) * megabyte, // megabytes + Rotatorr: &timerotator.Layout{FileCount: u.Webserver.LogFiles}, + DirMode: logsDirMode, + } + + switch { // only use MultiWriter if we have > 1 writer. + case !u.Config.Quiet && logFile != "": + u.Logger.HTTP.SetOutput(io.MultiWriter(rotatorr.NewMust(rotate), os.Stdout)) + case !u.Config.Quiet && logFile == "": + u.Logger.HTTP.SetOutput(os.Stdout) + case u.Config.Quiet && logFile == "": + u.Logger.HTTP.SetOutput(io.Discard) + default: // u.Config.Quiet && logFile != "" + u.Logger.HTTP.SetOutput(rotatorr.NewMust(rotate)) + } +} + func (u *Unpackerr) postLogRotate(_, newFile string) { if newFile != "" { go u.Printf("Rotated log file to: %s", newFile) @@ -207,7 +210,7 @@ func (u *Unpackerr) postLogRotate(_, newFile string) { // logStartupInfo prints info about our startup config. func (u *Unpackerr) logStartupInfo(msg string) { u.Printf("==> %s <==", helpLink) - u.Print("==> Startup Settings <==") + u.Printf("==> Startup Settings <==") u.logSonarr() u.logRadarr() u.logLidarr() @@ -238,4 +241,5 @@ func (u *Unpackerr) logStartupInfo(msg string) { u.logWebhook() u.logCmdhook() + u.logWebserver() } diff --git a/pkg/unpackerr/metrics.go b/pkg/unpackerr/metrics.go new file mode 100644 index 00000000..3292f7aa --- /dev/null +++ b/pkg/unpackerr/metrics.go @@ -0,0 +1,186 @@ +package unpackerr + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "golift.io/starr" + "golift.io/version" + "golift.io/xtractr" +) + +// metrics holds the non-custom Prometheus collector metrics for the app. +type metrics struct { + AppQueueErr *prometheus.CounterVec + AppQueueGet *prometheus.CounterVec + AppQueues *prometheus.GaugeVec + AppRequests *prometheus.GaugeVec + ArchivesRead *prometheus.CounterVec + BytesWritten *prometheus.CounterVec + ExtractTime *prometheus.HistogramVec + FilesExtracted *prometheus.CounterVec + Uptime prometheus.CounterFunc +} + +// MetricsCollector is used to plug into a custom Prometheus metrics collector. +type MetricsCollector struct { + *Unpackerr + counter *prometheus.Desc + gauge *prometheus.Desc + buffer *prometheus.Desc +} + +// Describe satisfies the Prometheus custom metrics collector. +func (c *MetricsCollector) Describe(ch chan<- *prometheus.Desc) { + for _, desc := range []*prometheus.Desc{c.counter, c.gauge, c.buffer} { + ch <- desc + } +} + +// Collect satisfies the Prometheus custom metrics collector. +func (c *MetricsCollector) Collect(ch chan<- prometheus.Metric) { + stats := c.stats() + ch <- prometheus.MustNewConstMetric(c.gauge, prometheus.GaugeValue, float64(stats.Waiting), "waiting") + ch <- prometheus.MustNewConstMetric(c.gauge, prometheus.GaugeValue, float64(stats.Queued), "queued") + ch <- prometheus.MustNewConstMetric(c.gauge, prometheus.GaugeValue, float64(stats.Extracting), "extracting") + ch <- prometheus.MustNewConstMetric(c.gauge, prometheus.GaugeValue, float64(stats.Failed), "failed") + ch <- prometheus.MustNewConstMetric(c.gauge, prometheus.GaugeValue, float64(stats.Extracted), "extracted") + ch <- prometheus.MustNewConstMetric(c.gauge, prometheus.GaugeValue, float64(stats.Imported), "imported") + ch <- prometheus.MustNewConstMetric(c.gauge, prometheus.GaugeValue, float64(stats.Deleted), "deleted") + ch <- prometheus.MustNewConstMetric(c.counter, prometheus.CounterValue, float64(stats.HookOK), "hook_ok") + ch <- prometheus.MustNewConstMetric(c.counter, prometheus.CounterValue, float64(stats.HookFail), "hook_fail") + ch <- prometheus.MustNewConstMetric(c.counter, prometheus.CounterValue, float64(stats.CmdOK), "cmd_ok") + ch <- prometheus.MustNewConstMetric(c.counter, prometheus.CounterValue, float64(stats.CmdFail), "cmd_fail") + ch <- prometheus.MustNewConstMetric(c.counter, prometheus.CounterValue, float64(c.Retries), "retries") + ch <- prometheus.MustNewConstMetric(c.counter, prometheus.CounterValue, float64(c.Finished), "finished") + ch <- prometheus.MustNewConstMetric(c.buffer, prometheus.GaugeValue, float64(len(c.folders.Events)), "folder_events") + ch <- prometheus.MustNewConstMetric(c.buffer, prometheus.GaugeValue, float64(len(c.updates)), "xtractr_updates") + ch <- prometheus.MustNewConstMetric(c.buffer, prometheus.GaugeValue, float64(len(c.folders.Updates)), "folder_updates") + ch <- prometheus.MustNewConstMetric(c.buffer, prometheus.GaugeValue, float64(len(c.delChan)), "deletes") + ch <- prometheus.MustNewConstMetric(c.buffer, prometheus.GaugeValue, float64(len(c.hookChan)), "hooks") +} + +// updateMetrics observes metrics for each completed extraction. The url for a folder is the watch path. +func (u *Unpackerr) updateMetrics(resp *xtractr.Response, app starr.App, url string) { + if u.metrics == nil { + return + } + + u.metrics.ArchivesRead.WithLabelValues(string(app), url).Add(float64(mapLen(resp.Archives) + mapLen(resp.Extras))) + u.metrics.BytesWritten.WithLabelValues(string(app), url).Add(float64(resp.Size)) + u.metrics.ExtractTime.WithLabelValues(string(app), url).Observe(resp.Elapsed.Seconds()) + u.metrics.FilesExtracted.WithLabelValues(string(app), url).Add(float64(len(resp.NewFiles))) +} + +// saveQueueMetrics observes metrics for each starr app queue request. +func (u *Unpackerr) saveQueueMetrics(size int, start time.Time, app starr.App, url string, err error) { + if err != nil { + u.Errorf("%s (%s): %v", app, url, err) + } + + if u.metrics == nil { + return + } + + if err != nil { + u.metrics.AppQueueErr.WithLabelValues(string(app), url).Inc() + } + + u.metrics.AppQueueGet.WithLabelValues(string(app), url).Inc() + u.metrics.AppQueues.WithLabelValues(string(app), url).Set(float64(size)) + u.metrics.AppRequests.WithLabelValues(string(app), url).Set(time.Since(start).Seconds()) +} + +// setupMetrics is called once on startup if metrics are enabled. +func (u *Unpackerr) setupMetrics() { + prometheus.MustRegister(&MetricsCollector{ + Unpackerr: u, + counter: prometheus.NewDesc("unpackerr_counters", "Unpackerr queue counters", []string{"name"}, nil), + gauge: prometheus.NewDesc("unpackerr_gauges", "Unpackerr queue gauges", []string{"name"}, nil), + buffer: prometheus.NewDesc("unpackerr_buffers", "Unpackerr channel buffer gauges", []string{"name"}, nil), + }) + + u.metrics = &metrics{ + AppQueueErr: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "unpackerr_app_queue_fetch_errors_total", + Help: "Total times the starr activity queue fetch returned an error", + }, []string{"app", "url"}), + AppQueueGet: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "unpackerr_app_queue_fetch_total", + Help: "Total times the starr activity queue was fetched", + }, []string{"app", "url"}), + AppQueues: promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "unpackerr_app_queue_size", + Help: "The total number of items queued in a Starr app", + }, []string{"app", "url"}), + AppRequests: promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "unpackerr_app_queue_fetch_time_seconds", + Help: "The duration of queue fetch API requests to Starr apps", + }, []string{"app", "url"}), + ArchivesRead: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "unpackerr_archives_read_total", + Help: "The total number of archive files read", + }, []string{"app", "url"}), + BytesWritten: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "unpackerr_bytes_written_total", + Help: "The total number bytes written to disk", + }, []string{"app", "url"}), + ExtractTime: promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "unpackerr_extract_time_seconds", + Help: "The duration of extractions", + Buckets: []float64{10, 60, 300, 1800, 3600, 7200, 14400}, + }, []string{"app", "url"}), + FilesExtracted: promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "unpackerr_files_extracted_total", + Help: "The total number files written to disk", + }, []string{"app", "url"}), + Uptime: promauto.NewCounterFunc(prometheus.CounterOpts{ + Name: "unpackerr_uptime_seconds_total", + Help: "Duration Unpackerr has been running in seconds", + }, func() float64 { return time.Since(version.Started).Seconds() }), + } +} + +// Stats is filled and returned when a stats request is issued. +type Stats struct { + Waiting uint + Queued uint + Extracting uint + Failed uint + Extracted uint + Imported uint + Deleted uint + HookOK uint + HookFail uint + CmdOK uint + CmdFail uint +} + +// stats compiles and builds the statistics for the app. +func (u *Unpackerr) stats() *Stats { + stats := &Stats{} + stats.HookOK, stats.HookFail = u.WebhookCounts() + stats.CmdOK, stats.CmdFail = u.CmdhookCounts() + + for name := range u.Map { + switch u.Map[name].Status { + case WAITING: + stats.Waiting++ + case QUEUED: + stats.Queued++ + case EXTRACTING: + stats.Extracting++ + case DELETEFAILED, EXTRACTFAILED: + stats.Failed++ + case EXTRACTED: + stats.Extracted++ + case DELETED, DELETING: + stats.Deleted++ + case IMPORTED: + stats.Imported++ + } + } + + return stats +} diff --git a/pkg/unpackerr/radarr.go b/pkg/unpackerr/radarr.go index 15471df8..acd70220 100644 --- a/pkg/unpackerr/radarr.go +++ b/pkg/unpackerr/radarr.go @@ -6,6 +6,7 @@ import ( "net/http" "strings" "sync" + "time" "golift.io/starr" "golift.io/starr/radarr" @@ -25,7 +26,7 @@ func (u *Unpackerr) validateRadarr() error { for i := range u.Radarr { if u.Radarr[i].URL == "" { - u.Printf("Missing Radarr URL in one of your configurations, skipped and ignored.") + u.Errorf("Missing Radarr URL in one of your configurations, skipped and ignored.") continue } @@ -82,7 +83,7 @@ func (u *Unpackerr) logRadarr() { u.Radarr[0].ValidSSL, u.Radarr[0].Protocols, u.Radarr[0].Syncthing, u.Radarr[0].DeleteOrig, u.Radarr[0].DeleteDelay.Duration, u.Radarr[0].Paths) } else { - u.Print(" => Radarr Config:", c, "servers") + u.Printf(" => Radarr Config: %d servers", c) for _, f := range u.Radarr { u.Printf(" => Server: %s, apikey:%v, timeout:%v, verify ssl:%v, protos:%s, "+ @@ -98,19 +99,20 @@ func (u *Unpackerr) getRadarrQueue() { for _, server := range u.Radarr { if server.APIKey == "" { u.Debugf("Radarr (%s): skipped, no API key", server.URL) - continue } + start := time.Now() + queue, err := server.GetQueue(DefaultQueuePageSize, 1) if err != nil { - u.Printf("[ERROR] Radarr (%s): %v", server.URL, err) - + u.saveQueueMetrics(0, start, starr.Radarr, server.URL, err) return } // Only update if there was not an error fetching. server.Queue = queue + u.saveQueueMetrics(server.Queue.TotalRecords, start, starr.Radarr, server.URL, nil) if !u.Activity || queue.TotalRecords > 0 { u.Printf("[Radarr] Updated (%s): %d Items Queued, %d Retrieved", server.URL, queue.TotalRecords, len(queue.Records)) @@ -128,18 +130,19 @@ func (u *Unpackerr) checkRadarrQueue() { for _, q := range server.Queue.Records { switch x, ok := u.Map[q.Title]; { case ok && x.Status == EXTRACTED && u.isComplete(q.Status, q.Protocol, server.Protocols): - u.Debugf("%s (%s): Item Waiting for Import (%s): %v", Radarr, server.URL, q.Protocol, q.Title) + u.Debugf("%s (%s): Item Waiting for Import (%s): %v", starr.Radarr, server.URL, q.Protocol, q.Title) case (!ok || x.Status < QUEUED) && u.isComplete(q.Status, q.Protocol, server.Protocols): // This shoehorns the Radarr OutputPath into a StatusMessage that getDownloadPath can parse. q.StatusMessages = append(q.StatusMessages, &starr.StatusMessage{Title: q.Title, Messages: []string{prefixPathMsg + q.OutputPath}}) u.handleCompletedDownload(q.Title, &Extract{ - App: Radarr, + App: starr.Radarr, + URL: server.URL, DeleteOrig: server.DeleteOrig, DeleteDelay: server.DeleteDelay.Duration, Syncthing: server.Syncthing, - Path: u.getDownloadPath(q.StatusMessages, Radarr, q.Title, server.Paths), + Path: u.getDownloadPath(q.StatusMessages, starr.Radarr, q.Title, server.Paths), IDs: map[string]interface{}{ "downloadId": q.DownloadID, "title": q.Title, @@ -150,7 +153,7 @@ func (u *Unpackerr) checkRadarrQueue() { fallthrough default: u.Debugf("%s: (%s): %s (%s:%d%%): %v", - Radarr, server.URL, q.Status, q.Protocol, percent(q.Sizeleft, q.Size), q.Title) + starr.Radarr, server.URL, q.Status, q.Protocol, percent(q.Sizeleft, q.Size), q.Title) } } } diff --git a/pkg/unpackerr/readarr.go b/pkg/unpackerr/readarr.go index c30281d1..927b8d9f 100644 --- a/pkg/unpackerr/readarr.go +++ b/pkg/unpackerr/readarr.go @@ -6,6 +6,7 @@ import ( "net/http" "strings" "sync" + "time" "golift.io/starr" "golift.io/starr/readarr" @@ -25,7 +26,7 @@ func (u *Unpackerr) validateReadarr() error { for i := range u.Readarr { if u.Readarr[i].URL == "" { - u.Printf("Missing Readarr URL in one of your configurations, skipped and ignored.") + u.Errorf("Missing Readarr URL in one of your configurations, skipped and ignored.") continue } @@ -82,7 +83,7 @@ func (u *Unpackerr) logReadarr() { u.Readarr[0].ValidSSL, u.Readarr[0].Protocols, u.Readarr[0].Syncthing, u.Readarr[0].DeleteOrig, u.Readarr[0].DeleteDelay.Duration, u.Readarr[0].Paths) } else { - u.Print(" => Readarr Config:", c, "servers") + u.Printf(" => Readarr Config: %d servers", c) for _, f := range u.Readarr { u.Printf(" => Server: %s, apikey:%v, timeout:%v, verify ssl:%v, protos:%s, "+ @@ -98,19 +99,20 @@ func (u *Unpackerr) getReadarrQueue() { for _, server := range u.Readarr { if server.APIKey == "" { u.Debugf("Readarr (%s): skipped, no API key", server.URL) - continue } + start := time.Now() + queue, err := server.GetQueue(DefaultQueuePageSize, DefaultQueuePageSize) if err != nil { - u.Printf("[ERROR] Readarr (%s): %v", server.URL, err) - + u.saveQueueMetrics(0, start, starr.Readarr, server.URL, err) return } // Only update if there was not an error fetching. server.Queue = queue + u.saveQueueMetrics(server.Queue.TotalRecords, start, starr.Readarr, server.URL, nil) if !u.Activity || queue.TotalRecords > 0 { u.Printf("[Readarr] Updated (%s): %d Items Queued, %d Retrieved", server.URL, queue.TotalRecords, len(queue.Records)) @@ -128,18 +130,19 @@ func (u *Unpackerr) checkReadarrQueue() { for _, q := range server.Queue.Records { switch x, ok := u.Map[q.Title]; { case ok && x.Status == EXTRACTED && u.isComplete(q.Status, q.Protocol, server.Protocols): - u.Debugf("%s (%s): Item Waiting for Import (%s): %v", Readarr, server.URL, q.Protocol, q.Title) + u.Debugf("%s (%s): Item Waiting for Import (%s): %v", starr.Readarr, server.URL, q.Protocol, q.Title) case (!ok || x.Status < QUEUED) && u.isComplete(q.Status, q.Protocol, server.Protocols): // This shoehorns the Readar OutputPath into a StatusMessage that getDownloadPath can parse. q.StatusMessages = append(q.StatusMessages, &starr.StatusMessage{Title: q.Title, Messages: []string{prefixPathMsg + q.OutputPath}}) u.handleCompletedDownload(q.Title, &Extract{ - App: Readarr, + App: starr.Readarr, + URL: server.URL, DeleteOrig: server.DeleteOrig, DeleteDelay: server.DeleteDelay.Duration, Syncthing: server.Syncthing, - Path: u.getDownloadPath(q.StatusMessages, Readarr, q.Title, server.Paths), + Path: u.getDownloadPath(q.StatusMessages, starr.Readarr, q.Title, server.Paths), IDs: map[string]interface{}{ "title": q.Title, "authorId": q.AuthorID, @@ -151,7 +154,7 @@ func (u *Unpackerr) checkReadarrQueue() { fallthrough default: u.Debugf("%s: (%s): %s (%s:%d%%): %v", - Readarr, server.URL, q.Status, q.Protocol, percent(q.Sizeleft, q.Size), q.Title) + starr.Readarr, server.URL, q.Status, q.Protocol, percent(q.Sizeleft, q.Size), q.Title) } } } diff --git a/pkg/unpackerr/sonarr.go b/pkg/unpackerr/sonarr.go index ed83909c..bab682b4 100644 --- a/pkg/unpackerr/sonarr.go +++ b/pkg/unpackerr/sonarr.go @@ -6,6 +6,7 @@ import ( "net/http" "strings" "sync" + "time" "golift.io/starr" "golift.io/starr/sonarr" @@ -25,7 +26,7 @@ func (u *Unpackerr) validateSonarr() error { for i := range u.Sonarr { if u.Sonarr[i].URL == "" { - u.Printf("Missing Sonarr URL in one of your configurations, skipped and ignored.") + u.Errorf("Missing Sonarr URL in one of your configurations, skipped and ignored.") continue } @@ -82,7 +83,7 @@ func (u *Unpackerr) logSonarr() { u.Sonarr[0].ValidSSL, u.Sonarr[0].Protocols, u.Sonarr[0].Syncthing, u.Sonarr[0].DeleteOrig, u.Sonarr[0].DeleteDelay.Duration, u.Sonarr[0].Paths) } else { - u.Print(" => Sonarr Config:", c, "servers") + u.Printf(" => Sonarr Config: %d servers", c) for _, f := range u.Sonarr { u.Printf(" => Server: %s, apikey:%v, timeout:%v, verify ssl:%v, protos:%s, "+ @@ -98,19 +99,20 @@ func (u *Unpackerr) getSonarrQueue() { for _, server := range u.Sonarr { if server.APIKey == "" { u.Debugf("Sonarr (%s): skipped, no API key", server.URL) - continue } + start := time.Now() + queue, err := server.GetQueue(DefaultQueuePageSize, 1) if err != nil { - u.Printf("[ERROR] Sonarr (%s): %v", server.URL, err) - + u.saveQueueMetrics(0, start, starr.Sonarr, server.URL, err) return } // Only update if there was not an error fetching. server.Queue = queue + u.saveQueueMetrics(server.Queue.TotalRecords, start, starr.Sonarr, server.URL, nil) if !u.Activity || queue.TotalRecords > 0 { u.Printf("[Sonarr] Updated (%s): %d Items Queued, %d Retrieved", server.URL, queue.TotalRecords, len(queue.Records)) @@ -128,18 +130,19 @@ func (u *Unpackerr) checkSonarrQueue() { for _, q := range server.Queue.Records { switch x, ok := u.Map[q.Title]; { case ok && x.Status == EXTRACTED && u.isComplete(q.Status, q.Protocol, server.Protocols): - u.Debugf("%s (%s): Item Waiting for Import: %v", Sonarr, server.URL, q.Title) + u.Debugf("%s (%s): Item Waiting for Import: %v", starr.Sonarr, server.URL, q.Title) case (!ok || x.Status < QUEUED) && u.isComplete(q.Status, q.Protocol, server.Protocols): // This shoehorns the Sonarr OutputPath into a StatusMessage that getDownloadPath can parse. q.StatusMessages = append(q.StatusMessages, &starr.StatusMessage{Title: q.Title, Messages: []string{prefixPathMsg + q.OutputPath}}) u.handleCompletedDownload(q.Title, &Extract{ - App: Sonarr, + App: starr.Sonarr, + URL: server.URL, DeleteOrig: server.DeleteOrig, DeleteDelay: server.DeleteDelay.Duration, Syncthing: server.Syncthing, - Path: u.getDownloadPath(q.StatusMessages, Sonarr, q.Title, server.Paths), + Path: u.getDownloadPath(q.StatusMessages, starr.Sonarr, q.Title, server.Paths), IDs: map[string]interface{}{ "title": q.Title, "downloadId": q.DownloadID, @@ -151,7 +154,7 @@ func (u *Unpackerr) checkSonarrQueue() { fallthrough default: u.Debugf("%s (%s): %s (%s:%d%%): %v (Ep: %v)", - Sonarr, server.URL, q.Status, q.Protocol, percent(q.Sizeleft, q.Size), q.Title, q.EpisodeID) + starr.Sonarr, server.URL, q.Status, q.Protocol, percent(q.Sizeleft, q.Size), q.Title, q.EpisodeID) } } } diff --git a/pkg/unpackerr/start.go b/pkg/unpackerr/start.go index 917e874f..2102bf80 100644 --- a/pkg/unpackerr/start.go +++ b/pkg/unpackerr/start.go @@ -27,7 +27,7 @@ const ( defaultStartDelay = time.Minute minimumDeleteDelay = time.Second defaultDeleteDelay = 5 * time.Minute - defaultHistory = 10 // items keps in history. + defaultHistory = 10 // items kept in history. suffix = "_unpackerred" // suffix for unpacked folders. mebiByte = 1024 * 1024 // Used to turn bytes in MiB. updateChanBuf = 100 // Size of xtractr callback update channels. @@ -45,6 +45,7 @@ type Unpackerr struct { *Config *History *xtractr.Xtractr + metrics *metrics folders *Folders sigChan chan os.Signal updates chan *xtractr.Response @@ -58,8 +59,10 @@ type Unpackerr struct { // Logger provides a struct we can pass into other packages. type Logger struct { - debug bool - Logger *log.Logger + HTTP *log.Logger + Info *log.Logger + Error *log.Logger + Debug *log.Logger } // Flags are our CLI input flags. @@ -100,8 +103,20 @@ func New() *Unpackerr { RetryDelay: cnfg.Duration{Duration: defaultRetryDelay}, StartDelay: cnfg.Duration{Duration: defaultStartDelay}, DeleteDelay: cnfg.Duration{Duration: defaultDeleteDelay}, + Webserver: &WebServer{ + Metrics: false, + LogFiles: defaultLogFiles, + LogFileMb: defaultLogFileMb, + ListenAddr: "0.0.0.0:5656", + URLBase: "/", + }, + }, + Logger: &Logger{ + HTTP: log.New(io.Discard, "", 0), + Info: log.New(io.Discard, "[INFO] ", log.LstdFlags), + Error: log.New(io.Discard, "[ERROR] ", log.LstdFlags), + Debug: log.New(io.Discard, "[DEBUG] ", log.Lshortfile|log.Lmicroseconds|log.Ldate), }, - Logger: &Logger{Logger: log.New(io.Discard, "", 0)}, } } @@ -152,6 +167,7 @@ func Start() (err error) { } go u.watchDeleteChannel() + u.startWebServer() u.watchWorkThread() u.startTray() // runs tray or waits for exit depending on hasGUI. diff --git a/pkg/unpackerr/tray.go b/pkg/unpackerr/tray.go index 08d10bc9..d4d0463b 100644 --- a/pkg/unpackerr/tray.go +++ b/pkg/unpackerr/tray.go @@ -44,7 +44,7 @@ func (u *Unpackerr) readyTray() { if err == nil { systray.SetTemplateIcon(b, b) } else { - u.Printf("[ERROR] Reading Icon: %v", err) + u.Errorf("Reading Icon: %v", err) systray.SetTitle("DNC") } @@ -186,38 +186,23 @@ func (u *Unpackerr) makeStatsChannels() { u.menu["stats_stacks"].Disable() } -func (u *Unpackerr) updateTray( - retries, - finished, - waiting, - queued, - extracting, - failed, - extracted, - imported, - deleted, - hookOK, - hookFail, - stacks uint, -) { +func (u *Unpackerr) updateTray(s *Stats, stacks uint) { if !ui.HasGUI() { return } - const baseTen = 10 - - u.menu["stats_waiting"].SetTitle("Waiting: " + strconv.FormatUint(uint64(waiting), baseTen)) - u.menu["stats_queued"].SetTitle("Queued: " + strconv.FormatUint(uint64(queued), baseTen)) - u.menu["stats_extracting"].SetTitle("Extracting: " + strconv.FormatUint(uint64(extracting), baseTen)) - u.menu["stats_failed"].SetTitle("Failed: " + strconv.FormatUint(uint64(failed), baseTen)) - u.menu["stats_extracted"].SetTitle("Extracted: " + strconv.FormatUint(uint64(extracted), baseTen)) - u.menu["stats_imported"].SetTitle("Imported: " + strconv.FormatUint(uint64(imported), baseTen)) - u.menu["stats_deleted"].SetTitle("Deleted: " + strconv.FormatUint(uint64(deleted), baseTen)) - u.menu["stats_finished"].SetTitle("Finished: " + strconv.FormatUint(uint64(finished), baseTen)) - u.menu["stats_retries"].SetTitle("Retries: " + strconv.FormatUint(uint64(retries), baseTen)) - u.menu["stats_hookOK"].SetTitle("Webhooks: " + strconv.FormatUint(uint64(hookOK), baseTen)) - u.menu["stats_hookFail"].SetTitle("Hook Errors: " + strconv.FormatUint(uint64(hookFail), baseTen)) - u.menu["stats_stacks"].SetTitle("Loop Stacks: " + strconv.FormatUint(uint64(stacks), baseTen)) + u.menu["stats_waiting"].SetTitle("Waiting: " + strconv.FormatUint(uint64(s.Waiting), 10)) + u.menu["stats_queued"].SetTitle("Queued: " + strconv.FormatUint(uint64(s.Queued), 10)) + u.menu["stats_extracting"].SetTitle("Extracting: " + strconv.FormatUint(uint64(s.Extracting), 10)) + u.menu["stats_failed"].SetTitle("Failed: " + strconv.FormatUint(uint64(s.Failed), 10)) + u.menu["stats_extracted"].SetTitle("Extracted: " + strconv.FormatUint(uint64(s.Extracted), 10)) + u.menu["stats_imported"].SetTitle("Imported: " + strconv.FormatUint(uint64(s.Imported), 10)) + u.menu["stats_deleted"].SetTitle("Deleted: " + strconv.FormatUint(uint64(s.Deleted), 10)) + u.menu["stats_finished"].SetTitle("Finished: " + strconv.FormatUint(uint64(u.Finished), 10)) + u.menu["stats_retries"].SetTitle("Retries: " + strconv.FormatUint(uint64(u.Retries), 10)) + u.menu["stats_hookOK"].SetTitle("Webhooks: " + strconv.FormatUint(uint64(s.HookOK), 10)) + u.menu["stats_hookFail"].SetTitle("Hook Errors: " + strconv.FormatUint(uint64(s.HookFail), 10)) + u.menu["stats_stacks"].SetTitle("Loop Stacks: " + strconv.FormatUint(uint64(stacks), 10)) } func (u *Unpackerr) watchKillerChannels() { @@ -239,16 +224,16 @@ func (u *Unpackerr) rotateLogs() { u.Printf("User Requested: Rotate Log File!") if _, err := u.rotatorr.Rotate(); err != nil { - u.Printf("[ERROR] Rotating Log Files: %v", err) + u.Errorf("Rotating Log Files: %v", err) } } func (u *Unpackerr) checkForUpdate() { - u.Print("User Requested: Update Check") + u.Printf("User Requested: Update Check") switch update, err := update.Check("Unpackerr/unpackerr", version.Version); { case err != nil: - u.Printf("[ERROR] Update Check: %v", err) + u.Errorf("Update Check: %v", err) _, _ = ui.Error("Unpackerr", "Failure checking version on GitHub: "+err.Error()) case update.Outdate: yes, _ := ui.Question("Unpackerr", "An Update is available! Download?\n\n"+ diff --git a/pkg/unpackerr/tray_other.go b/pkg/unpackerr/tray_other.go index 5cfa7122..9225358c 100644 --- a/pkg/unpackerr/tray_other.go +++ b/pkg/unpackerr/tray_other.go @@ -15,7 +15,7 @@ func (u *Unpackerr) startTray() { u.Printf("[unpackerr] Need help? %s\n=====> Exiting! Caught Signal: %v", helpLink, <-u.sigChan) } -func (u *Unpackerr) updateTray(_, _, _, _, _, _, _, _, _, _, _, _ uint) { +func (u *Unpackerr) updateTray(_ *Stats, _ uint) { // there is no tray. } diff --git a/pkg/unpackerr/webhook.go b/pkg/unpackerr/webhook.go index d8d9d7fc..6f7c0247 100644 --- a/pkg/unpackerr/webhook.go +++ b/pkg/unpackerr/webhook.go @@ -14,6 +14,7 @@ import ( "time" "golift.io/cnfg" + "golift.io/starr" "golift.io/version" ) @@ -152,10 +153,10 @@ func (u *Unpackerr) sendWebhookWithLog(hook *WebhookConfig, payload *WebhookPayl var body bytes.Buffer if tmpl, err := hook.Template(); err != nil { - u.Printf("[ERROR] Webhook Template (%s = %s): %v", payload.Path, payload.Event, err) + u.Errorf("Webhook Template (%s = %s): %v", payload.Path, payload.Event, err) return } else if err = tmpl.Execute(&body, payload); err != nil { - u.Printf("[ERROR] Webhook Payload (%s = %s): %v", payload.Path, payload.Event, err) + u.Errorf("Webhook Payload (%s = %s): %v", payload.Path, payload.Event, err) return } @@ -163,7 +164,7 @@ func (u *Unpackerr) sendWebhookWithLog(hook *WebhookConfig, payload *WebhookPayl if reply, err := hook.Send(&body); err != nil { u.Debugf("Webhook Payload: %s", b) - u.Printf("[ERROR] Webhook (%s = %s): %s: %v", payload.Path, payload.Event, hook.Name, err) + u.Errorf("Webhook (%s = %s): %s: %v", payload.Path, payload.Event, hook.Name, err) u.Debugf("Webhook Response: %s", string(reply)) } else if !hook.Silent { u.Debugf("Webhook Payload: %s", b) @@ -264,7 +265,7 @@ func (u *Unpackerr) logWebhook() { if len(u.Webhook) == 1 { pfx = " => Webhook Config: 1 URL" } else { - u.Print(" => Webhook Configs:", len(u.Webhook), "URLs") + u.Printf(" => Webhook Configs: %d URLs", len(u.Webhook)) pfx = " => URL" } @@ -308,9 +309,9 @@ func logEvents(events []ExtractStatus) (s string) { } // Excluded returns true if an app is in the Exclude slice. -func (w *WebhookConfig) Excluded(app string) bool { +func (w *WebhookConfig) Excluded(app starr.App) bool { for _, a := range w.Exclude { - if strings.EqualFold(a, app) { + if strings.EqualFold(a, string(app)) { return true } } diff --git a/pkg/unpackerr/webhooks_templates.go b/pkg/unpackerr/webhooks_templates.go index 49736352..11310658 100644 --- a/pkg/unpackerr/webhooks_templates.go +++ b/pkg/unpackerr/webhooks_templates.go @@ -10,12 +10,13 @@ import ( "time" "golift.io/cnfg" + "golift.io/starr" ) // WebhookPayload defines the data sent to notifarr.com (and other) webhooks. type WebhookPayload struct { Path string `json:"path"` // Path for the extracted item. - App string `json:"app"` // Application Triggering Event + App starr.App `json:"app"` // Application Triggering Event IDs map[string]interface{} `json:"ids,omitempty"` // Arbitrary IDs from each app. Event ExtractStatus `json:"unpackerr_eventtype"` // The type of the event. Time time.Time `json:"time"` // Time of this event. diff --git a/pkg/unpackerr/webserver.go b/pkg/unpackerr/webserver.go new file mode 100644 index 00000000..f8e84d9c --- /dev/null +++ b/pkg/unpackerr/webserver.go @@ -0,0 +1,204 @@ +package unpackerr + +import ( + "errors" + "fmt" + "net" + "net/http" + "path" + "strings" + + "github.com/julienschmidt/httprouter" + apachelog "github.com/lestrrat-go/apache-logformat/v2" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +type WebServer struct { + Metrics bool `toml:"metrics" json:"metrics" xml:"metrics" yaml:"metrics"` + LogFiles int `json:"logFiles" toml:"log_files" xml:"log_files" yaml:"logFiles"` + LogFileMb int `json:"logFileMb" toml:"log_file_mb" xml:"log_file_mb" yaml:"logFileMb"` + ListenAddr string `toml:"listen_addr" json:"listenAddr" xml:"listen_addr" yaml:"listenAddr"` + LogFile string `json:"logFile" toml:"log_file" xml:"log_file" yaml:"logFile"` + SSLCrtFile string `json:"sslCertFile" toml:"ssl_cert_file" xml:"ssl_cert_file" yaml:"sslCertFile"` + SSLKeyFile string `json:"sslKeyFile" toml:"ssl_key_file" xml:"ssl_key_file" yaml:"sslKeyFile"` + URLBase string `json:"urlbase" toml:"urlbase" xml:"urlbase" yaml:"urlbase"` + Upstreams StringSlice `json:"upstreams" toml:"upstreams" xml:"upstreams" yaml:"upstreams"` + allow AllowedIPs + router *httprouter.Router + server *http.Server +} + +func (w *WebServer) Enabled() bool { + return w != nil && w.Metrics && w.ListenAddr != "" +} + +func (u *Unpackerr) logWebserver() { + if !u.Webserver.Enabled() { + u.Printf(" => Webserver Disabled") + return + } + + addr := u.Webserver.ListenAddr + if !strings.Contains(addr, ":") { + addr = "0.0.0.0:" + addr + } + + ssl := "" + if u.Webserver.SSLCrtFile != "" && u.Webserver.SSLKeyFile != "" { + ssl = "s" + } + + u.Printf(" => Starting webserver. Listen address: http%s://%v%s (%d upstreams)", + ssl, addr, u.Webserver.URLBase, len(u.Webserver.Upstreams)) +} + +func (u *Unpackerr) startWebServer() { + if !u.Webserver.Enabled() { + return + } + + addr := u.Webserver.ListenAddr + if !strings.Contains(addr, ":") { + addr = "0.0.0.0:" + addr + } + + u.Webserver.URLBase = strings.TrimSuffix(path.Join("/", u.Webserver.URLBase), "/") + "/" + u.Webserver.allow = MakeIPs(u.Webserver.Upstreams) + u.Webserver.router = httprouter.New() + apache, _ := apachelog.New(`%{X-Forwarded-For}i %l - %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"`) + + // Make a multiplexer because websockets can't use apache log. + smx := http.NewServeMux() + smx.Handle(path.Join(u.Webserver.URLBase, "ws"), u.fixForwardedFor(u.Webserver.router)) + smx.Handle("/", u.fixForwardedFor(apache.Wrap(u.Webserver.router, u.Logger.HTTP.Writer()))) + u.webRoutes() + + u.Webserver.server = &http.Server{ + Addr: addr, + Handler: smx, + ReadTimeout: 0, + ReadHeaderTimeout: defaultTimeout, + WriteTimeout: 0, + IdleTimeout: defaultTimeout, + ErrorLog: u.Logger.Error, + } + + go u.runWebServer() +} + +func (u *Unpackerr) webRoutes() { + u.Webserver.router.GET(path.Join(u.Webserver.URLBase, "/"), Index) + + if !u.Webserver.Metrics { + return + } + + u.setupMetrics() + u.Webserver.router.Handler(http.MethodGet, "/metrics", promhttp.Handler()) + + if u.Webserver.URLBase != "/" { + // Metrics get served from both paths. + u.Webserver.router.Handler(http.MethodGet, path.Join(u.Webserver.URLBase, "/metrics"), promhttp.Handler()) + } +} + +// runWebServer starts the http or https listener. +func (u *Unpackerr) runWebServer() { + var err error + + if u.Webserver.SSLCrtFile != "" && u.Webserver.SSLKeyFile != "" { + err = u.Webserver.server.ListenAndServeTLS(u.Webserver.SSLCrtFile, u.Webserver.SSLKeyFile) + } else { + err = u.Webserver.server.ListenAndServe() + } + + if err != nil && !errors.Is(http.ErrServerClosed, err) { + u.Errorf("Web Server Failed: %v", err) + } +} + +func Index(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { + fmt.Fprint(w, "Welcome!\n") +} + +// fixForwardedFor sets the X-Forwarded-For header to the client IP +// under specific circumstances. +func (u *Unpackerr) fixForwardedFor(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { //nolint:varnamelen + if x := r.Header.Get("X-Forwarded-For"); x == "" || !u.Webserver.allow.Contains(r.RemoteAddr) { + r.Header.Set("X-Forwarded-For", + strings.Trim(r.RemoteAddr[:strings.LastIndex(r.RemoteAddr, ":")], "[]")) + } else if l := strings.LastIndexAny(x, ", "); l != -1 { + r.Header.Set("X-Forwarded-For", strings.Trim(x[l:len(x)-1], ", ")) + } + + next.ServeHTTP(w, r) + }) +} + +/* This is a helper method to check if an IP is in a list/cidr. */ + +// AllowedIPs determines who can set x-forwarded-for. +type AllowedIPs struct { + Input []string + Nets []*net.IPNet +} + +var _ = fmt.Stringer(AllowedIPs{}) + +// String turns a list of allowedIPs into a printable masterpiece. +func (n AllowedIPs) String() (s string) { + if len(n.Nets) < 1 { + return "(none)" + } + + for i := range n.Nets { + if s != "" { + s += ", " + } + + s += n.Nets[i].String() + } + + return s +} + +// Contains returns true if an IP is allowed. +func (n AllowedIPs) Contains(ip string) bool { + ip = strings.Trim(ip[:strings.LastIndex(ip, ":")], "[]") + + for i := range n.Nets { + if n.Nets[i].Contains(net.ParseIP(ip)) { + return true + } + } + + return false +} + +// MakeIPs turns a list of CIDR strings (or plain IPs) into a list of net.IPNet. +// This "allowed" list is later used to check incoming IPs from web requests. +func MakeIPs(upstreams []string) AllowedIPs { + a := AllowedIPs{ + Input: make([]string, len(upstreams)), + Nets: []*net.IPNet{}, + } + + for idx, ipAddr := range upstreams { + a.Input[idx] = ipAddr + + if !strings.Contains(ipAddr, "/") { + if strings.Contains(ipAddr, ":") { + ipAddr += "/128" + } else { + ipAddr += "/32" + } + } + + if _, i, err := net.ParseCIDR(ipAddr); err == nil { + a.Nets = append(a.Nets, i) + } + } + + return a +} diff --git a/pkg/unpackerr/whisparr.go b/pkg/unpackerr/whisparr.go index e1a3e21d..8d36b529 100644 --- a/pkg/unpackerr/whisparr.go +++ b/pkg/unpackerr/whisparr.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "strings" + "time" "golift.io/starr" "golift.io/starr/radarr" @@ -29,7 +30,7 @@ func (u *Unpackerr) validateWhisparr() error { for i := range u.Whisparr { if u.Whisparr[i].URL == "" { - u.Printf("Missing Whisparr URL in one of your configurations, skipped and ignored.") + u.Errorf("Missing Whisparr URL in one of your configurations, skipped and ignored.") continue } @@ -87,7 +88,7 @@ func (u *Unpackerr) logWhisparr() { u.Whisparr[0].ValidSSL, u.Whisparr[0].Protocols, u.Whisparr[0].Syncthing, u.Whisparr[0].DeleteOrig, u.Whisparr[0].DeleteDelay.Duration, u.Whisparr[0].Paths) } else if count != 0 { - u.Print(" => Whisparr Config:", count, "servers") + u.Printf(" => Whisparr Config: %d servers", count) for _, f := range u.Whisparr { u.Printf(" => Server: %s, apikey:%v, timeout:%v, verify ssl:%v, protos:%s, "+ @@ -103,19 +104,20 @@ func (u *Unpackerr) getWhisparrQueue() { for _, server := range u.Whisparr { if server.APIKey == "" { u.Debugf("Whisparr (%s): skipped, no API key", server.URL) - continue } + start := time.Now() + queue, err := server.GetQueue(DefaultQueuePageSize, 1) if err != nil { - u.Printf("[ERROR] Whisparr (%s): %v", server.URL, err) - + u.saveQueueMetrics(0, start, starr.Whisparr, server.URL, err) return } // Only update if there was not an error fetching. server.Queue = queue + u.saveQueueMetrics(server.Queue.TotalRecords, start, starr.Whisparr, server.URL, nil) if !u.Activity || queue.TotalRecords > 0 { u.Printf("[Whisparr] Updated (%s): %d Items Queued, %d Retrieved", @@ -134,17 +136,18 @@ func (u *Unpackerr) checkWhisparrQueue() { for _, q := range server.Queue.Records { switch x, ok := u.Map[q.Title]; { case ok && x.Status == EXTRACTED && u.isComplete(q.Status, q.Protocol, server.Protocols): - u.Debugf("%s (%s): Item Waiting for Import (%s): %v", Whisparr, server.URL, q.Protocol, q.Title) + u.Debugf("%s (%s): Item Waiting for Import (%s): %v", starr.Whisparr, server.URL, q.Protocol, q.Title) case (!ok || x.Status < QUEUED) && u.isComplete(q.Status, q.Protocol, server.Protocols): // This shoehorns the Whisparr OutputPath into a StatusMessage that getDownloadPath can parse. q.StatusMessages = append(q.StatusMessages, &starr.StatusMessage{Title: q.Title, Messages: []string{prefixPathMsg + q.OutputPath}}) u.handleCompletedDownload(q.Title, &Extract{ - App: Whisparr, + App: starr.Whisparr, + URL: server.URL, DeleteOrig: server.DeleteOrig, DeleteDelay: server.DeleteDelay.Duration, - Path: u.getDownloadPath(q.StatusMessages, Whisparr, q.Title, server.Paths), + Path: u.getDownloadPath(q.StatusMessages, starr.Whisparr, q.Title, server.Paths), IDs: map[string]interface{}{ "downloadId": q.DownloadID, "title": q.Title, @@ -155,7 +158,7 @@ func (u *Unpackerr) checkWhisparrQueue() { fallthrough default: u.Debugf("%s: (%s): %s (%s:%d%%): %v", - Whisparr, server.URL, q.Status, q.Protocol, percent(q.Sizeleft, q.Size), q.Title) + starr.Whisparr, server.URL, q.Status, q.Protocol, percent(q.Sizeleft, q.Size), q.Title) } } }