mirror of
https://github.com/onedr0p/exportarr.git
synced 2026-02-06 10:57:32 +00:00
Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a3db16074 | ||
|
|
86a455425a | ||
|
|
f4282b23cb | ||
|
|
32206e0195 | ||
|
|
d02dcfc638 | ||
|
|
e272e7d2d6 | ||
|
|
6f2cbc6b77 | ||
|
|
0641a89f16 | ||
|
|
6df7bec85d | ||
|
|
67e213dea8 | ||
|
|
3cd394508c | ||
|
|
32cfa9e4ac | ||
|
|
b9f5074fbb | ||
|
|
c983dc5d73 | ||
|
|
389d695698 | ||
|
|
eb2493be64 | ||
|
|
1e0d2f4e97 | ||
|
|
b8b6aeac4b | ||
|
|
3d6df3896b | ||
|
|
367f6031df | ||
|
|
1c6341f0b1 | ||
|
|
9cc130410f | ||
|
|
04561a3c3d | ||
|
|
305ecf665e | ||
|
|
3eff0ca85f | ||
|
|
9d7eaf4aa8 | ||
|
|
fae1d714ce | ||
|
|
0089def832 | ||
|
|
260ab02f5f | ||
|
|
0225f50153 | ||
|
|
e9a64ee311 | ||
|
|
a9a21b8126 | ||
|
|
5f58308c3e | ||
|
|
330f0b0abb | ||
|
|
1f39314323 | ||
|
|
c4597fb481 | ||
|
|
40225cedcf | ||
|
|
5d768b256d | ||
|
|
f86fe3bfc9 | ||
|
|
def78d0339 | ||
|
|
130eb4ce15 | ||
|
|
baacf8df6e | ||
|
|
4ace1abc7a | ||
|
|
72d0253efa | ||
|
|
7b0c581020 | ||
|
|
acb2097c4b | ||
|
|
7d73b433bb | ||
|
|
e312a81d4b | ||
|
|
7778f75502 | ||
|
|
481a1686f9 | ||
|
|
587c6a1a7c | ||
|
|
a5a595a650 | ||
|
|
091fa156db | ||
|
|
168fac32f6 | ||
|
|
d3eb0e22be | ||
|
|
2078395212 | ||
|
|
d064d5d4bb | ||
|
|
4b1ee958a8 | ||
|
|
9173dbdca7 | ||
|
|
dba01a7134 | ||
|
|
c01ec8a7cb | ||
|
|
643a925701 | ||
|
|
cd2441e39f | ||
|
|
42bdd7908f | ||
|
|
7d1af014c0 | ||
|
|
17e1c08791 | ||
|
|
5fd5008d91 | ||
|
|
07175a6b01 | ||
|
|
327c7a1dab | ||
|
|
dfc7028b96 | ||
|
|
cb96e5dca2 | ||
|
|
c03e86b3b2 | ||
|
|
ebe23a146f | ||
|
|
89ac18a844 | ||
|
|
f40b71c1cf | ||
|
|
6de6a2c23f | ||
|
|
af49a5d4fe | ||
|
|
72c2d6ce18 | ||
|
|
ddb0aa5a18 | ||
|
|
dc97704375 | ||
|
|
ebafeb2b9e | ||
|
|
d4dde1a55c | ||
|
|
b1c1026505 | ||
|
|
bc0dd989b8 | ||
|
|
0b552eac7a | ||
|
|
fc830f4cd7 | ||
|
|
3d11ef4c16 | ||
|
|
862f5f4cde | ||
|
|
950cb75dbd | ||
|
|
a5966c3d2c | ||
|
|
e8b408abb1 | ||
|
|
0ba1cd2d80 | ||
|
|
67335fdd3f | ||
|
|
2ee793e8b4 | ||
|
|
3e6b595b84 | ||
|
|
e401f676cf | ||
|
|
10597c440a | ||
|
|
0963316789 | ||
|
|
86cd0a5681 | ||
|
|
1db0b074fc | ||
|
|
63ea3fa3bf |
10
.github/ISSUE_TEMPLATE/config.yml
vendored
10
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,10 +1,6 @@
|
||||
---
|
||||
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Discuss on GitHub
|
||||
url: https://github.com/k8s-at-home/organization/discussions
|
||||
about: Ask and answer questions here
|
||||
- name: Discuss on Discord
|
||||
url: https://discord.com/invite/sTMX7Vh
|
||||
about: Join our Discord community
|
||||
- name: Discuss on Discord
|
||||
url: https://discord.gg/home-operations
|
||||
about: Join our Discord community
|
||||
|
||||
9
.github/actions/docker-image/action.yaml
vendored
9
.github/actions/docker-image/action.yaml
vendored
@ -27,12 +27,11 @@ runs:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
flavor: |
|
||||
latest=${{ fromJSON(inputs.latest) }}
|
||||
prefix=v
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{version}},prefix=v
|
||||
type=semver,pattern={{major}}.{{minor}},prefix=v
|
||||
type=semver,pattern={{major}},prefix=v
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@ -51,7 +50,7 @@ runs:
|
||||
|
||||
- name: Build Docker Image
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
|
||||
6
.github/actions/go-release/action.yaml
vendored
6
.github/actions/go-release/action.yaml
vendored
@ -11,14 +11,14 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ">=1.19"
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
args: release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ inputs.token }}
|
||||
|
||||
13
.github/actions/lint/action.yaml
vendored
13
.github/actions/lint/action.yaml
vendored
@ -6,13 +6,12 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ">=1.19"
|
||||
go-version: ">=1.23"
|
||||
|
||||
# Run golangcilint before `go get` is ran
|
||||
# https://github.com/golangci/golangci-lint-action/issues/23
|
||||
- uses: golangci/golangci-lint-action@v4
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v1.55.1
|
||||
args: --timeout 5m --config .github/lint/golangci.yaml
|
||||
version: v2.1
|
||||
args: --timeout=5m --config=.github/lint/golangci.yaml
|
||||
|
||||
42
.github/lint/golangci.yaml
vendored
42
.github/lint/golangci.yaml
vendored
@ -1,18 +1,26 @@
|
||||
---
|
||||
run:
|
||||
timeout: 3m
|
||||
version: "2"
|
||||
|
||||
linters:
|
||||
# https://golangci-lint.run/usage/linters/#enabled-by-default
|
||||
enable:
|
||||
- errcheck
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- typecheck
|
||||
- unused
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: '(.+)_test\.go'
|
||||
linters:
|
||||
- errcheck
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- errcheck
|
||||
path: (.+)_test\.go
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
formatters:
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
6
.github/workflows/on-merge.yml
vendored
6
.github/workflows/on-merge.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Tests
|
||||
uses: ./.github/actions/tests
|
||||
@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Lint
|
||||
uses: ./.github/actions/lint
|
||||
@ -29,7 +29,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build
|
||||
uses: ./.github/actions/docker-image
|
||||
|
||||
4
.github/workflows/on-pr.yml
vendored
4
.github/workflows/on-pr.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Lint
|
||||
uses: ./.github/actions/lint
|
||||
@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Tests
|
||||
uses: ./.github/actions/tests
|
||||
8
.github/workflows/on-release.yml
vendored
8
.github/workflows/on-release.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Tests
|
||||
uses: ./.github/actions/tests
|
||||
@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Lint
|
||||
uses: ./.github/actions/lint
|
||||
@ -32,7 +32,7 @@ jobs:
|
||||
- lint
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build
|
||||
uses: ./.github/actions/docker-image
|
||||
@ -46,7 +46,7 @@ jobs:
|
||||
- lint
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Release
|
||||
uses: ./.github/actions/go-release
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
version: 2
|
||||
project_name: "exportarr"
|
||||
archives:
|
||||
- id: main
|
||||
@ -21,6 +23,7 @@ builds:
|
||||
- "linux"
|
||||
- "freebsd"
|
||||
- "openbsd"
|
||||
- "windows"
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
@ -32,5 +35,7 @@ builds:
|
||||
goarch: arm64
|
||||
- goos: openbsd
|
||||
goarch: arm64
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
|
||||
11
.renovaterc.json5
Normal file
11
.renovaterc.json5
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
$schema: "https://docs.renovatebot.com/renovate-schema.json",
|
||||
extends: [
|
||||
"config:recommended"
|
||||
],
|
||||
postUpdateOptions: [
|
||||
"gomodTidy",
|
||||
"gomodUpdateImportPaths"
|
||||
],
|
||||
ignoreDeps: ["golang.org/x/exp"]
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
FROM golang:1.22.0-alpine as builder
|
||||
FROM golang:1.24.6-alpine as builder
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT=""
|
||||
|
||||
45
README.md
45
README.md
@ -1,10 +1,13 @@
|
||||
# exportarr
|
||||
# Exportarr
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Exportarr is in maintenance mode, trying to collect metrics from these applications is not up to my standards — meaning gathering this data from the Sonarr, Radarr etc... APIs is not ideal. If these apps ever expose a stats API endpoint that can be use to collect data instead I might be willing on refactoring it to support pulling data from that instead.
|
||||
|
||||
AIO Prometheus Exporter for Sonarr, Radarr, Lidarr, Prowlarr, Readarr, Bazarr and Sabnzbd
|
||||
|
||||
[](https://goreportcard.com/report/github.com/onedr0p/exportarr)
|
||||
|
||||
Note: This exporter will not gather metrics from all apps at once, and instead you an `exportarr` instance for each app. Be sure to see the examples below for more information.
|
||||
Note: This exporter will not gather metrics from all apps at once. You will need an `exportarr` instance for each app. Be sure to see the examples below for more information.
|
||||
|
||||

|
||||
|
||||
@ -23,7 +26,7 @@ See examples in the [examples/kubernetes](./examples/kubernetes/) directory.
|
||||
_Replace `$app`, `$port` and `$apikey` with one of the support apps, port and api key_
|
||||
|
||||
```sh
|
||||
# PORT must be unique across all exportarr instances
|
||||
# PORT must be unique across all Exportarr instances
|
||||
docker run --name exportarr_$app \
|
||||
-e PORT=9707 \
|
||||
-e URL="http://x.x.x.x:$port" \
|
||||
@ -42,7 +45,7 @@ _Replace `$app`, `$port` and `$apikey` with one of the support apps, port and ap
|
||||
```sh
|
||||
./exportarr $app --help
|
||||
|
||||
# --port must be unique across all exportarr instances
|
||||
# --port must be unique across all Exportarr instances
|
||||
./exportarr $app \
|
||||
--port 9707 \
|
||||
--url "http://x.x.x.x:$port" \
|
||||
@ -55,28 +58,28 @@ Visit http://127.0.0.1:9707/metrics to see the app metrics
|
||||
|
||||
| Environment Variable | CLI Flag | Description | Default | Required |
|
||||
| :-----------------------------: | ------------------------------ | -------------------------------------------------------------- | -------------------- | :------: |
|
||||
| `PORT` | `--port` or `-p` | The port exportarr will listen on | | ✅ |
|
||||
| `URL` | `--url` or `-u` | The full URL to Sonarr, Radarr, or Lidarr | | ✅ |
|
||||
| `API_KEY` | `--api-key` or `-a` | API Key for Sonarr, Radarr or Lidarr | | ❌ |
|
||||
| `API_KEY_FILE` | `--api-key-file` | API Key file location for Sonarr, Radarr or Lidarr | | ❌ |
|
||||
| `CONFIG` | `--config` or `-c` | Path to Sonarr, Radarr or Lidarr's `config.xml` (advanced) | | ❌ |
|
||||
| `INTERFACE` | `--interface` or `-i` | The interface IP exportarr will listen on | `0.0.0.0` | ❌ |
|
||||
| `LOG_LEVEL` | `--log-level` or `-l` | Set the default Log Level | `INFO` | ❌ |
|
||||
| `DISABLE_SSL_VERIFY` | `--disable-ssl-verify` | Set to `true` to disable SSL verification | `false` | ❌ |
|
||||
| `AUTH_PASSWORD` | `--auth-password` | Set to your basic or form auth password | | ❌ |
|
||||
| `AUTH_USERNAME` | `--auth-username` | Set to your basic or form auth username | | ❌ |
|
||||
| `FORM_AUTH` | `--form-auth` | Use Form Auth instead of basic auth | `false` | ❌ |
|
||||
| `ENABLE_ADDITIONAL_METRICS` | `--enable-additional-metrics` | Set to `true` to enable gathering of additional metrics (slow) | `false` | ❌ |
|
||||
| `ENABLE_UNKNOWN_QUEUE_ITEMS` | `--enable-unknown-queue-items` | Set to `true` to enable gathering unknown queue items | `false` | ❌ |
|
||||
| `PROWLARR__BACKFILL` | `--backfill` | Set to `true` to enable backfill of historical metrics | `false` | ❌ |
|
||||
| `PROWLARR__BACKFILL_SINCE_DATE` | `--backfill-since-date` | Set a date from which to start the backfill | `1970-01-01` (epoch) | ❌ |
|
||||
| `PORT` | `--port` or `-p` | The port Exportarr will listen on | | ✅ |
|
||||
| `URL` | `--url` or `-u` | The full URL to Sonarr, Radarr, or Lidarr | | ✅ |
|
||||
| `API_KEY` | `--api-key` or `-a` | API Key for Sonarr, Radarr or Lidarr | | ❌ |
|
||||
| `API_KEY_FILE` | `--api-key-file` | API Key file location for Sonarr, Radarr or Lidarr | | ❌ |
|
||||
| `CONFIG` | `--config` or `-c` | Path to Sonarr, Radarr or Lidarr's `config.xml` (advanced) | | ❌ |
|
||||
| `INTERFACE` | `--interface` or `-i` | The interface IP Exportarr will listen on | `0.0.0.0` | ❌ |
|
||||
| `LOG_LEVEL` | `--log-level` or `-l` | Set the default Log Level | `INFO` | ❌ |
|
||||
| `DISABLE_SSL_VERIFY` | `--disable-ssl-verify` | Set to `true` to disable SSL verification | `false` | ❌ |
|
||||
| `AUTH_PASSWORD` | `--auth-password` | Set to your basic or form auth password | | ❌ |
|
||||
| `AUTH_USERNAME` | `--auth-username` | Set to your basic or form auth username | | ❌ |
|
||||
| `FORM_AUTH` | `--form-auth` | Use Form Auth instead of basic auth | `false` | ❌ |
|
||||
| `ENABLE_ADDITIONAL_METRICS` | `--enable-additional-metrics` | Set to `true` to enable gathering of additional metrics (slow) | `false` | ❌ |
|
||||
| `ENABLE_UNKNOWN_QUEUE_ITEMS` | `--enable-unknown-queue-items` | Set to `true` to enable gathering unknown queue items | `false` | ❌ |
|
||||
| `PROWLARR__BACKFILL` | `--backfill` | Set to `true` to enable backfill of historical metrics | `false` | ❌ |
|
||||
| `PROWLARR__BACKFILL_SINCE_DATE` | `--backfill-since-date` | Set a date from which to start the backfill | `1970-01-01` (epoch) | ❌ |
|
||||
|
||||
### Prowlarr Backfill
|
||||
|
||||
The prowlarr collector is a little different than other collectors as it's hitting an actual "stats" endpoint, collecting counters of events that happened in a small time window, rather than getting all-time statistics like the other collectors. This means that by default, when you start the prowlarr collector, collected stats will start from that moment (all counters will start from zero).
|
||||
The Prowlarr collector is a little different than other collectors as it's hitting an actual "stats" endpoint, collecting counters of events that happened in a small time window, rather than getting all-time statistics like the other collectors. This means that by default, when you start the Prowlarr collector, collected stats will start from that moment (all counters will start from zero).
|
||||
|
||||
To backfill all Prowlarr Data, either use `PROWLARR__BACKFILL` or `--backfill`.
|
||||
|
||||
Note that the first request can be extremely slow, depending on how long your prowlarr instance has been running. You can also specify a start date to limit the backfill if the backfill is timing out:
|
||||
Note that the first request can be extremely slow, depending on how long your Prowlarr instance has been running. You can also specify a start date to limit the backfill if the backfill is timing out:
|
||||
|
||||
`PROWLARR__BACKFILL_DATE_SINCE=2023-03-01` or `--backfill-date-since=2023-03-01`
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
version: "3.7"
|
||||
services:
|
||||
sonarr-exporter:
|
||||
image: ghcr.io/onedr0p/exportarr:v1.5.3
|
||||
image: ghcr.io/onedr0p/exportarr:v2.0
|
||||
container_name: sonarr-exporter
|
||||
command: ["sonarr"]
|
||||
environment:
|
||||
@ -15,7 +15,7 @@ services:
|
||||
- "9707:9707"
|
||||
restart: unless-stopped
|
||||
radarr-exporter:
|
||||
image: ghcr.io/onedr0p/exportarr:v1.5.3
|
||||
image: ghcr.io/onedr0p/exportarr:v2.0
|
||||
container_name: radarr-exporter
|
||||
command: ["radarr"]
|
||||
environment:
|
||||
@ -28,7 +28,7 @@ services:
|
||||
- "9708:9708"
|
||||
restart: unless-stopped
|
||||
lidarr-exporter:
|
||||
image: ghcr.io/onedr0p/exportarr:v1.5.3
|
||||
image: ghcr.io/onedr0p/exportarr:v2.0
|
||||
container_name: lidarr-exporter
|
||||
command: ["lidarr"]
|
||||
environment:
|
||||
@ -41,22 +41,22 @@ services:
|
||||
- "9709:9709"
|
||||
restart: unless-stopped
|
||||
prowlarr-exporter:
|
||||
image: ghcr.io/onedr0p/exportarr:v1.5.3
|
||||
image: ghcr.io/onedr0p/exportarr:v2.0
|
||||
container_name: prowlarr-exporter
|
||||
command: ["prowlarr"]
|
||||
environment:
|
||||
PORT: 9710
|
||||
URL: "http://x.x.x.x:9696" # or; http://prowlarr:8080
|
||||
APIKEY: "abc"
|
||||
# PROWLARR_BACKFILL: true # optional
|
||||
# PROWLARR_BACKFILL_SINCE_DATE: "2023-03-01" # optional
|
||||
# PROWLARR__BACKFILL: true # optional
|
||||
# PROWLARR__BACKFILL_SINCE_DATE: "2023-03-01" # optional
|
||||
# networks:
|
||||
# - your_custom_network # optional
|
||||
ports:
|
||||
- "9710:9710"
|
||||
restart: unless-stopped
|
||||
sabnzbd-exporter:
|
||||
image: ghcr.io/onedr0p/exportarr:v1.5.3
|
||||
image: ghcr.io/onedr0p/exportarr:v2.0
|
||||
container_name: sabnzbd-exporter
|
||||
command: ["sabnzbd"]
|
||||
environment:
|
||||
@ -68,3 +68,29 @@ services:
|
||||
ports:
|
||||
- "9711:9711"
|
||||
restart: unless-stopped
|
||||
bazarr-exporter:
|
||||
image: ghcr.io/onedr0p/exportarr:v2.0
|
||||
container_name: bazarr-exporter
|
||||
command: ["bazarr"]
|
||||
environment:
|
||||
PORT: 9712
|
||||
URL: "http://x.x.x.x:6767" # or; http://bazarr:6767
|
||||
APIKEY: "xxx"
|
||||
# networks:
|
||||
# - your_custom_network # optional
|
||||
ports:
|
||||
- "9712:9712"
|
||||
restart: unless-stopped
|
||||
readarr-exporter:
|
||||
image: ghcr.io/onedr0p/exportarr:v2.0
|
||||
container_name: readarr-exporter
|
||||
command: ["readarr"]
|
||||
environment:
|
||||
PORT: 9713
|
||||
URL: "http://x.x.x.x:8787" # or; http://readarr:8787
|
||||
APIKEY: "xxx"
|
||||
# networks:
|
||||
# - your_custom_network # optional
|
||||
ports:
|
||||
- "9713:9713"
|
||||
restart: unless-stopped
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
57
go.mod
57
go.mod
@ -1,42 +1,47 @@
|
||||
module github.com/onedr0p/exportarr
|
||||
|
||||
go 1.19
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.6
|
||||
|
||||
require (
|
||||
github.com/gookit/validate v1.5.2
|
||||
github.com/knadh/koanf/providers/confmap v0.1.0
|
||||
github.com/knadh/koanf/providers/env v0.1.0
|
||||
github.com/knadh/koanf/providers/file v0.1.0
|
||||
github.com/knadh/koanf/providers/posflag v0.1.0
|
||||
github.com/knadh/koanf/v2 v2.1.0
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225
|
||||
golang.org/x/sync v0.6.0
|
||||
github.com/gookit/validate v1.5.6
|
||||
github.com/knadh/koanf/providers/confmap v1.0.0
|
||||
github.com/knadh/koanf/providers/env v1.1.0
|
||||
github.com/knadh/koanf/providers/file v1.2.1
|
||||
github.com/knadh/koanf/providers/posflag v1.0.1
|
||||
github.com/knadh/koanf/v2 v2.3.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.11.1
|
||||
go.uber.org/zap v1.27.1
|
||||
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8
|
||||
golang.org/x/sync v0.19.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
|
||||
github.com/gookit/filter v1.2.1 // indirect
|
||||
github.com/gookit/goutil v0.6.15 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/gookit/filter v1.2.3 // indirect
|
||||
github.com/gookit/goutil v0.7.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/knadh/koanf/maps v0.1.1 // indirect
|
||||
github.com/knadh/koanf/maps v0.1.2 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
126
go.sum
126
go.sum
@ -1,79 +1,93 @@
|
||||
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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
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/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c=
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||
github.com/gookit/filter v1.2.1 h1:37XivkBm2E5qe1KaGdJ5ZfF5l9NYdGWfLEeQadJD8O4=
|
||||
github.com/gookit/filter v1.2.1/go.mod h1:rxynQFr793x+XDwnRmJFEb53zDw0Zqx3OD7TXWoR9mQ=
|
||||
github.com/gookit/goutil v0.6.15 h1:mMQ0ElojNZoyPD0eVROk5QXJPh2uKR4g06slgPDF5Jo=
|
||||
github.com/gookit/goutil v0.6.15/go.mod h1:qdKdYEHQdEtyH+4fNdQNZfJHhI0jUZzHxQVAV3DaMDY=
|
||||
github.com/gookit/validate v1.5.2 h1:i5I2OQ7WYHFRPRATGu9QarR9snnNHydvwSuHXaRWAV0=
|
||||
github.com/gookit/validate v1.5.2/go.mod h1:yuPy2WwDlwGRa06fFJ5XIO8QEwhRnTC2LmxmBa5SE14=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/gookit/filter v1.2.3 h1:Zo7cBOtsVzAoa/jtf+Ury6zlsbJXqInFdUpbbnB2vMM=
|
||||
github.com/gookit/filter v1.2.3/go.mod h1:nFLJcOV8dRgS1iiX23gUQgmHUhpuS40qCvAGgIvA1pM=
|
||||
github.com/gookit/goutil v0.7.1 h1:AaFJPN9mrdeYBv8HOybri26EHGCC34WJVT7jUStGJsI=
|
||||
github.com/gookit/goutil v0.7.1/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU=
|
||||
github.com/gookit/validate v1.5.6 h1:D6vbSZzreuKYpeeXm5FDDEJy3K5E4lcWsQE4saSMZbU=
|
||||
github.com/gookit/validate v1.5.6/go.mod h1:WYEHndRNepIIkM+6CtgEX9MQ9ToIQRhXxmz5oLHF/fc=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
|
||||
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||
github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU=
|
||||
github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU=
|
||||
github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg=
|
||||
github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ=
|
||||
github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c=
|
||||
github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA=
|
||||
github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U=
|
||||
github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0=
|
||||
github.com/knadh/koanf/v2 v2.1.0 h1:eh4QmHHBuU8BybfIJ8mB8K8gsGCD/AUQTdwGq/GzId8=
|
||||
github.com/knadh/koanf/v2 v2.1.0/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
|
||||
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||
github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE=
|
||||
github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A=
|
||||
github.com/knadh/koanf/providers/env v1.1.0 h1:U2VXPY0f+CsNDkvdsG8GcsnK4ah85WwWyJgef9oQMSc=
|
||||
github.com/knadh/koanf/providers/env v1.1.0/go.mod h1:QhHHHZ87h9JxJAn2czdEl6pdkNnDh/JS1Vtsyt65hTY=
|
||||
github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM=
|
||||
github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
|
||||
github.com/knadh/koanf/providers/posflag v1.0.1 h1:EnMxHSrPkYCFnKgBUl5KBgrjed8gVFrcXDzaW4l/C6Y=
|
||||
github.com/knadh/koanf/providers/posflag v1.0.1/go.mod h1:3Wn3+YG3f4ljzRyCUgIwH7G0sZ1pMjCOsNBovrbKmAk=
|
||||
github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
|
||||
github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
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.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc=
|
||||
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@ -9,18 +9,17 @@ import (
|
||||
|
||||
"github.com/onedr0p/exportarr/internal/arr/config"
|
||||
"github.com/onedr0p/exportarr/internal/client"
|
||||
base_client "github.com/onedr0p/exportarr/internal/client"
|
||||
)
|
||||
|
||||
type Client = base_client.Client
|
||||
type QueryParams = base_client.QueryParams
|
||||
type Client = client.Client
|
||||
type QueryParams = client.QueryParams
|
||||
|
||||
func NewClient(config *config.ArrConfig) (*Client, error) {
|
||||
auth, err := NewAuth(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return base_client.NewClient(config.BaseURL(), config.DisableSSLVerify, auth)
|
||||
return client.NewClient(config.BaseURL(), config.DisableSSLVerify, auth)
|
||||
}
|
||||
|
||||
func NewAuth(config *config.ArrConfig) (client.Authenticator, error) {
|
||||
@ -97,7 +96,7 @@ func (a *FormAuth) Auth(req *http.Request) error {
|
||||
|
||||
authReq, err := http.NewRequest("POST", u.String(), strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to renew FormAuth Cookie: %w", err)
|
||||
return fmt.Errorf("failed to renew FormAuth Cookie: %w", err)
|
||||
}
|
||||
|
||||
authReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
@ -105,18 +104,18 @@ func (a *FormAuth) Auth(req *http.Request) error {
|
||||
|
||||
client := &http.Client{Transport: a.Transport, CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
if req.URL.Query().Get("loginFailed") == "true" {
|
||||
return fmt.Errorf("Failed to renew FormAuth Cookie: Login Failed")
|
||||
return fmt.Errorf("failed to renew FormAuth Cookie: Login Failed")
|
||||
}
|
||||
return http.ErrUseLastResponse
|
||||
}}
|
||||
|
||||
authResp, err := client.Do(authReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to renew FormAuth Cookie: %w", err)
|
||||
return fmt.Errorf("failed to renew FormAuth Cookie: %w", err)
|
||||
}
|
||||
|
||||
if authResp.StatusCode != 302 {
|
||||
return fmt.Errorf("Failed to renew FormAuth Cookie: Received Status Code %d", authResp.StatusCode)
|
||||
return fmt.Errorf("failed to renew FormAuth Cookie: Received Status Code %d", authResp.StatusCode)
|
||||
}
|
||||
|
||||
found := false
|
||||
@ -129,7 +128,7 @@ func (a *FormAuth) Auth(req *http.Request) error {
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("Failed to renew FormAuth Cookie: No Cookie with suffix 'arrAuth' found")
|
||||
return fmt.Errorf("failed to renew FormAuth Cookie: No Cookie with suffix 'arrAuth' found")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -44,7 +44,7 @@ func TestBazarrCollect(t *testing.T) {
|
||||
b, err := os.ReadFile(bazarr_test_fixtures_path + "expected_metrics.txt")
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.Replace(string(b), "SOMEURL", ts.URL, -1)
|
||||
expected := strings.ReplaceAll(string(b), "SOMEURL", ts.URL)
|
||||
f := strings.NewReader(expected)
|
||||
collections := []string{
|
||||
"bazarr_episode_subtitles_downloaded_total",
|
||||
@ -97,17 +97,20 @@ func TestBazarrCollect_Concurrency(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
require.Contains(r.URL.Path, "/api/")
|
||||
if r.URL.Path == "/api/series" {
|
||||
|
||||
switch r.URL.Path {
|
||||
case "/api/series":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json, err := os.ReadFile(bazarr_test_fixtures_path + "concurrency/series.json")
|
||||
require.NoError(err)
|
||||
_, err = w.Write(json)
|
||||
require.NoError(err)
|
||||
} else if r.URL.Path == "/api/episodes" {
|
||||
|
||||
case "/api/episodes":
|
||||
seriesIDs := r.URL.Query()["seriesid[]"]
|
||||
require.Len(seriesIDs, 2)
|
||||
|
||||
if slices.Contains(seriesIDs, "944") && slices.Contains(seriesIDs, "945") {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json, err := os.ReadFile(bazarr_test_fixtures_path + "concurrency/episodes944_945.json")
|
||||
@ -123,7 +126,8 @@ func TestBazarrCollect_Concurrency(t *testing.T) {
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
} else {
|
||||
|
||||
default:
|
||||
ts2, err := newTestBazarrServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Contains(r.URL.Path, "/api/")
|
||||
})
|
||||
@ -149,7 +153,7 @@ func TestBazarrCollect_Concurrency(t *testing.T) {
|
||||
b, err := os.ReadFile(bazarr_test_fixtures_path + "concurrency/expected_metrics.txt")
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.Replace(string(b), "SOMEURL", ts.URL, -1)
|
||||
expected := strings.ReplaceAll(string(b), "SOMEURL", ts.URL)
|
||||
f := strings.NewReader(expected)
|
||||
collections := []string{
|
||||
"bazarr_episode_subtitles_downloaded_total",
|
||||
|
||||
@ -71,8 +71,8 @@ func TestSystemHealthCollect(t *testing.T) {
|
||||
b, err := os.ReadFile(test_util.COMMON_FIXTURES_PATH + "expected_health_metrics.txt")
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.Replace(string(b), "SOMEURL", ts.URL, -1)
|
||||
expected = strings.Replace(expected, "APP", tt.config.App, -1)
|
||||
expected := strings.ReplaceAll(string(b), "SOMEURL", ts.URL)
|
||||
expected = strings.ReplaceAll(expected, "APP", tt.config.App)
|
||||
|
||||
f := strings.NewReader(expected)
|
||||
|
||||
|
||||
@ -71,8 +71,8 @@ func TestHistoryCollect(t *testing.T) {
|
||||
b, err := os.ReadFile(test_util.COMMON_FIXTURES_PATH + "expected_history_metrics.txt")
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.Replace(string(b), "SOMEURL", ts.URL, -1)
|
||||
expected = strings.Replace(expected, "APP", tt.config.App, -1)
|
||||
expected := strings.ReplaceAll(string(b), "SOMEURL", ts.URL)
|
||||
expected = strings.ReplaceAll(expected, "APP", tt.config.App)
|
||||
|
||||
f := strings.NewReader(expected)
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ package collector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/onedr0p/exportarr/internal/arr/client"
|
||||
"github.com/onedr0p/exportarr/internal/arr/config"
|
||||
@ -99,7 +100,7 @@ func NewLidarrCollector(c *config.ArrConfig) *lidarrCollector {
|
||||
songsQualitiesMetric: prometheus.NewDesc(
|
||||
"lidarr_songs_quality_total",
|
||||
"Total number of downloaded songs by quality",
|
||||
[]string{"quality"},
|
||||
[]string{"quality", "weight"},
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
errorMetric: prometheus.NewDesc(
|
||||
@ -144,6 +145,7 @@ func (collector *lidarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
songs = 0
|
||||
songsDownloaded = 0
|
||||
songsQualities = map[string]int{}
|
||||
qualityWeights = map[string]string{}
|
||||
)
|
||||
|
||||
artists := model.Artist{}
|
||||
@ -197,6 +199,19 @@ func (collector *lidarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
albumGenres[genre]++
|
||||
}
|
||||
}
|
||||
|
||||
qualities := model.Qualities{}
|
||||
if err := c.DoRequest("qualitydefinition", &qualities); err != nil {
|
||||
log.Errorw("Error getting qualities",
|
||||
"error", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return
|
||||
}
|
||||
for _, q := range qualities {
|
||||
if q.Quality.Name != "" {
|
||||
qualityWeights[q.Quality.Name] = strconv.Itoa(q.Weight)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -226,7 +241,7 @@ func (collector *lidarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
|
||||
if len(songsQualities) > 0 {
|
||||
for qualityName, count := range songsQualities {
|
||||
ch <- prometheus.MustNewConstMetric(collector.songsQualitiesMetric, prometheus.GaugeValue, float64(count), qualityName)
|
||||
ch <- prometheus.MustNewConstMetric(collector.songsQualitiesMetric, prometheus.GaugeValue, float64(count), qualityName, qualityWeights[qualityName])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
96
internal/arr/collector/lidarr_test.go
Normal file
96
internal/arr/collector/lidarr_test.go
Normal file
@ -0,0 +1,96 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/onedr0p/exportarr/internal/arr/config"
|
||||
"github.com/onedr0p/exportarr/internal/test_util"
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const lidarr_test_fixtures_path = "../test_fixtures/lidarr/"
|
||||
|
||||
func newTestLidarrServer(t *testing.T, fn func(http.ResponseWriter, *http.Request)) (*httptest.Server, error) {
|
||||
return test_util.NewTestServer(t, lidarr_test_fixtures_path, fn)
|
||||
}
|
||||
|
||||
func TestLidarrCollect(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *config.ArrConfig
|
||||
expected_metrics_file string
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
config: &config.ArrConfig{
|
||||
App: "lidarr",
|
||||
ApiVersion: "v3",
|
||||
},
|
||||
expected_metrics_file: "expected_metrics.txt",
|
||||
},
|
||||
{
|
||||
name: "additional_metrics",
|
||||
config: &config.ArrConfig{
|
||||
App: "lidarr",
|
||||
ApiVersion: "v3",
|
||||
EnableAdditionalMetrics: true,
|
||||
},
|
||||
expected_metrics_file: "expected_metrics_extended.txt",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ts, err := newTestLidarrServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Contains(r.URL.Path, "/api/")
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
defer ts.Close()
|
||||
|
||||
tt.config.URL = ts.URL
|
||||
tt.config.ApiKey = test_util.API_KEY
|
||||
|
||||
collector := NewLidarrCollector(tt.config)
|
||||
require.NoError(err)
|
||||
|
||||
b, err := os.ReadFile(lidarr_test_fixtures_path + tt.expected_metrics_file)
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.ReplaceAll(string(b), "SOMEURL", ts.URL)
|
||||
f := strings.NewReader(expected)
|
||||
|
||||
require.NotPanics(func() {
|
||||
err = testutil.CollectAndCompare(collector, f)
|
||||
})
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLidarrCollect_FailureDoesntPanic(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
config := &config.ArrConfig{
|
||||
URL: ts.URL,
|
||||
ApiKey: test_util.API_KEY,
|
||||
}
|
||||
collector := NewRadarrCollector(config)
|
||||
|
||||
f := strings.NewReader("")
|
||||
|
||||
require.NotPanics(func() {
|
||||
err := testutil.CollectAndCompare(collector, f)
|
||||
require.Error(err)
|
||||
}, "Collecting metrics should not panic on failure")
|
||||
}
|
||||
@ -51,9 +51,10 @@ func (collector *queueCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
params := client.QueryParams{}
|
||||
params.Add("page", "1")
|
||||
if collector.config.EnableUnknownQueueItems {
|
||||
if collector.config.App == "sonarr" {
|
||||
switch collector.config.App {
|
||||
case "sonarr":
|
||||
params.Add("includeUnknownSeriesItems", "true")
|
||||
} else if collector.config.App == "radarr" {
|
||||
case "radarr":
|
||||
params.Add("includeUnknownMovieItems", "true")
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,8 +71,8 @@ func TestQueueCollect(t *testing.T) {
|
||||
b, err := os.ReadFile(test_util.COMMON_FIXTURES_PATH + "expected_queue_metrics.txt")
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.Replace(string(b), "SOMEURL", ts.URL, -1)
|
||||
expected = strings.Replace(expected, "APP", tt.config.App, -1)
|
||||
expected := strings.ReplaceAll(string(b), "SOMEURL", ts.URL)
|
||||
expected = strings.ReplaceAll(expected, "APP", tt.config.App)
|
||||
|
||||
f := strings.NewReader(expected)
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/onedr0p/exportarr/internal/arr/client"
|
||||
"github.com/onedr0p/exportarr/internal/arr/config"
|
||||
"github.com/onedr0p/exportarr/internal/arr/model"
|
||||
@ -17,6 +19,7 @@ type radarrCollector struct {
|
||||
movieUnmonitoredMetric *prometheus.Desc // Total number of unmonitored movies
|
||||
movieWantedMetric *prometheus.Desc // Total number of wanted movies
|
||||
movieMissingMetric *prometheus.Desc // Total number of missing movies
|
||||
movieCutoffUnmetMetric *prometheus.Desc // Total number of movies with cutoff unmet
|
||||
movieQualitiesMetric *prometheus.Desc // Total number of movies by quality
|
||||
movieFileSizeMetric *prometheus.Desc // Total fizesize of all movies in bytes
|
||||
errorMetric *prometheus.Desc // Error Description for use with InvalidMetric
|
||||
@ -68,6 +71,12 @@ func NewRadarrCollector(c *config.ArrConfig) *radarrCollector {
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
movieCutoffUnmetMetric: prometheus.NewDesc(
|
||||
"radarr_movie_cutoff_unmet_total",
|
||||
"Total number of movies with cutoff unmet",
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
movieFileSizeMetric: prometheus.NewDesc(
|
||||
"radarr_movie_filesize_total",
|
||||
"Total filesize of all movies",
|
||||
@ -77,7 +86,7 @@ func NewRadarrCollector(c *config.ArrConfig) *radarrCollector {
|
||||
movieQualitiesMetric: prometheus.NewDesc(
|
||||
"radarr_movie_quality_total",
|
||||
"Total number of downloaded movies by quality",
|
||||
[]string{"quality"},
|
||||
[]string{"quality", "weight"},
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
movieTagsMetric: prometheus.NewDesc(
|
||||
@ -103,6 +112,7 @@ func (collector *radarrCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- collector.movieUnmonitoredMetric
|
||||
ch <- collector.movieWantedMetric
|
||||
ch <- collector.movieMissingMetric
|
||||
ch <- collector.movieCutoffUnmetMetric
|
||||
ch <- collector.movieFileSizeMetric
|
||||
ch <- collector.movieQualitiesMetric
|
||||
ch <- collector.movieTagsMetric
|
||||
@ -129,6 +139,7 @@ func (collector *radarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
Label string
|
||||
Movies int
|
||||
}{}
|
||||
qualityWeights = map[string]string{}
|
||||
)
|
||||
|
||||
movies := model.Movie{}
|
||||
@ -186,6 +197,30 @@ func (collector *radarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
|
||||
qualityDefs := model.Qualities{}
|
||||
if err := c.DoRequest("qualitydefinition", &qualityDefs); err != nil {
|
||||
log.Errorw("Error getting qualities",
|
||||
"error", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return
|
||||
}
|
||||
for _, q := range qualityDefs {
|
||||
if q.Quality.Name != "" {
|
||||
qualityWeights[q.Quality.Name] = strconv.Itoa(q.Weight)
|
||||
}
|
||||
}
|
||||
|
||||
moviesCutoffUnmet := model.CutoffUnmetMovies{}
|
||||
moviesCutoffUnmetParams := client.QueryParams{}
|
||||
params.Add("sortKey", "airDateUtc")
|
||||
|
||||
if err := c.DoRequest("wanted/cutoff", &moviesCutoffUnmet, moviesCutoffUnmetParams); err != nil {
|
||||
log.Errorw("Error getting cutoff unmet",
|
||||
"error", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(collector.movieEdition, prometheus.GaugeValue, float64(editions))
|
||||
ch <- prometheus.MustNewConstMetric(collector.movieMetric, prometheus.GaugeValue, float64(len(movies)))
|
||||
ch <- prometheus.MustNewConstMetric(collector.movieDownloadedMetric, prometheus.GaugeValue, float64(downloaded))
|
||||
@ -193,12 +228,13 @@ func (collector *radarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
ch <- prometheus.MustNewConstMetric(collector.movieUnmonitoredMetric, prometheus.GaugeValue, float64(unmonitored))
|
||||
ch <- prometheus.MustNewConstMetric(collector.movieWantedMetric, prometheus.GaugeValue, float64(wanted))
|
||||
ch <- prometheus.MustNewConstMetric(collector.movieMissingMetric, prometheus.GaugeValue, float64(missing))
|
||||
ch <- prometheus.MustNewConstMetric(collector.movieCutoffUnmetMetric, prometheus.GaugeValue, float64(moviesCutoffUnmet.TotalRecords))
|
||||
ch <- prometheus.MustNewConstMetric(collector.movieFileSizeMetric, prometheus.GaugeValue, float64(fileSize))
|
||||
|
||||
if len(qualities) > 0 {
|
||||
for qualityName, count := range qualities {
|
||||
ch <- prometheus.MustNewConstMetric(collector.movieQualitiesMetric, prometheus.GaugeValue, float64(count),
|
||||
qualityName,
|
||||
qualityName, qualityWeights[qualityName],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ func TestRadarrCollect(t *testing.T) {
|
||||
b, err := os.ReadFile(radarr_test_fixtures_path + "expected_metrics.txt")
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.Replace(string(b), "SOMEURL", ts.URL, -1)
|
||||
expected := strings.ReplaceAll(string(b), "SOMEURL", ts.URL)
|
||||
f := strings.NewReader(expected)
|
||||
|
||||
require.NotPanics(func() {
|
||||
|
||||
@ -40,7 +40,7 @@ func TestReadarrCollect(t *testing.T) {
|
||||
b, err := os.ReadFile(readarr_test_fixtures_path + "expected_metrics.txt")
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.Replace(string(b), "SOMEURL", ts.URL, -1)
|
||||
expected := strings.ReplaceAll(string(b), "SOMEURL", ts.URL)
|
||||
f := strings.NewReader(expected)
|
||||
|
||||
require.NotPanics(func() {
|
||||
|
||||
@ -71,8 +71,8 @@ func TestRootFolderCollect(t *testing.T) {
|
||||
b, err := os.ReadFile(test_util.COMMON_FIXTURES_PATH + "expected_rootfolder_metrics.txt")
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.Replace(string(b), "SOMEURL", ts.URL, -1)
|
||||
expected = strings.Replace(expected, "APP", tt.config.App, -1)
|
||||
expected := strings.ReplaceAll(string(b), "SOMEURL", ts.URL)
|
||||
expected = strings.ReplaceAll(expected, "APP", tt.config.App)
|
||||
|
||||
f := strings.NewReader(expected)
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ package collector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/onedr0p/exportarr/internal/arr/client"
|
||||
@ -18,6 +19,7 @@ type sonarrCollector struct {
|
||||
seriesMonitoredMetric *prometheus.Desc // Total number of monitored series
|
||||
seriesUnmonitoredMetric *prometheus.Desc // Total number of unmonitored series
|
||||
seriesFileSizeMetric *prometheus.Desc // Total fizesize of all series in bytes
|
||||
seriesTagsMetric *prometheus.Desc // Total number of series by tag
|
||||
seasonMetric *prometheus.Desc // Total number of seasons
|
||||
seasonDownloadedMetric *prometheus.Desc // Total number of downloaded seasons
|
||||
seasonMonitoredMetric *prometheus.Desc // Total number of monitored seasons
|
||||
@ -27,6 +29,7 @@ type sonarrCollector struct {
|
||||
episodeUnmonitoredMetric *prometheus.Desc // Total number of unmonitored episodes
|
||||
episodeDownloadedMetric *prometheus.Desc // Total number of downloaded episodes
|
||||
episodeMissingMetric *prometheus.Desc // Total number of missing episodes
|
||||
episodeCutoffUnmetMetric *prometheus.Desc // Total number of episodes with cutoff unmet
|
||||
episodeQualitiesMetric *prometheus.Desc // Total number of episodes by quality
|
||||
errorMetric *prometheus.Desc // Error Description for use with InvalidMetric
|
||||
}
|
||||
@ -64,6 +67,12 @@ func NewSonarrCollector(conf *config.ArrConfig) *sonarrCollector {
|
||||
nil,
|
||||
prometheus.Labels{"url": conf.URL},
|
||||
),
|
||||
seriesTagsMetric: prometheus.NewDesc(
|
||||
"sonarr_series_tag_total",
|
||||
"Total number of downloaded series by tag",
|
||||
[]string{"tag"},
|
||||
prometheus.Labels{"url": conf.URL},
|
||||
),
|
||||
seasonMetric: prometheus.NewDesc(
|
||||
"sonarr_season_total",
|
||||
"Total number of seasons",
|
||||
@ -118,10 +127,16 @@ func NewSonarrCollector(conf *config.ArrConfig) *sonarrCollector {
|
||||
nil,
|
||||
prometheus.Labels{"url": conf.URL},
|
||||
),
|
||||
episodeCutoffUnmetMetric: prometheus.NewDesc(
|
||||
"sonarr_episode_cutoff_unmet_total",
|
||||
"Total number of episodes with cutoff unmet",
|
||||
nil,
|
||||
prometheus.Labels{"url": conf.URL},
|
||||
),
|
||||
episodeQualitiesMetric: prometheus.NewDesc(
|
||||
"sonarr_episode_quality_total",
|
||||
"Total number of downloaded episodes by quality",
|
||||
[]string{"quality"},
|
||||
[]string{"quality", "weight"},
|
||||
prometheus.Labels{"url": conf.URL},
|
||||
),
|
||||
errorMetric: prometheus.NewDesc(
|
||||
@ -139,6 +154,7 @@ func (collector *sonarrCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- collector.seriesMonitoredMetric
|
||||
ch <- collector.seriesUnmonitoredMetric
|
||||
ch <- collector.seriesFileSizeMetric
|
||||
ch <- collector.seriesTagsMetric
|
||||
ch <- collector.seasonMetric
|
||||
ch <- collector.seasonDownloadedMetric
|
||||
ch <- collector.seasonMonitoredMetric
|
||||
@ -148,6 +164,7 @@ func (collector *sonarrCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- collector.episodeUnmonitoredMetric
|
||||
ch <- collector.episodeDownloadedMetric
|
||||
ch <- collector.episodeMissingMetric
|
||||
ch <- collector.episodeCutoffUnmetMetric
|
||||
ch <- collector.episodeQualitiesMetric
|
||||
}
|
||||
|
||||
@ -175,6 +192,7 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
episodesMonitored = 0
|
||||
episodesUnmonitored = 0
|
||||
episodesQualities = map[string]int{}
|
||||
qualityWeights = map[string]string{}
|
||||
)
|
||||
|
||||
cseries := []time.Duration{}
|
||||
@ -249,6 +267,20 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
episodesUnmonitored++
|
||||
}
|
||||
}
|
||||
|
||||
qualities := model.Qualities{}
|
||||
if err := c.DoRequest("qualitydefinition", &qualities); err != nil {
|
||||
log.Errorw("Error getting qualities",
|
||||
"error", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return
|
||||
}
|
||||
for _, q := range qualities {
|
||||
if q.Quality.Name != "" {
|
||||
qualityWeights[q.Quality.Name] = strconv.Itoa(q.Weight)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugw("Extra options completed",
|
||||
"duration", time.Since(textra))
|
||||
}
|
||||
@ -271,11 +303,35 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
return
|
||||
}
|
||||
|
||||
episodesCutoffUnmet := model.CutoffUnmet{}
|
||||
|
||||
// Cutoff unmet endpoint uses the same params as missing
|
||||
if err := c.DoRequest("wanted/cutoff", &episodesCutoffUnmet, params); err != nil {
|
||||
log.Errorw("Error getting cutoff unmet",
|
||||
"error", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get tag details for series
|
||||
tagObjects := model.TagSeries{}
|
||||
if err := c.DoRequest("tag/detail", &tagObjects); err != nil {
|
||||
log.Errorw("Error getting tags",
|
||||
"error", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(collector.seriesMetric, prometheus.GaugeValue, float64(len(series)))
|
||||
ch <- prometheus.MustNewConstMetric(collector.seriesDownloadedMetric, prometheus.GaugeValue, float64(seriesDownloaded))
|
||||
ch <- prometheus.MustNewConstMetric(collector.seriesMonitoredMetric, prometheus.GaugeValue, float64(seriesMonitored))
|
||||
ch <- prometheus.MustNewConstMetric(collector.seriesUnmonitoredMetric, prometheus.GaugeValue, float64(seriesUnmonitored))
|
||||
ch <- prometheus.MustNewConstMetric(collector.seriesFileSizeMetric, prometheus.GaugeValue, float64(seriesFileSize))
|
||||
for _, tag := range tagObjects {
|
||||
ch <- prometheus.MustNewConstMetric(collector.seriesTagsMetric, prometheus.GaugeValue, float64(len(tag.SeriesIds)),
|
||||
tag.Label,
|
||||
)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(collector.seasonMetric, prometheus.GaugeValue, float64(seasons))
|
||||
ch <- prometheus.MustNewConstMetric(collector.seasonDownloadedMetric, prometheus.GaugeValue, float64(seasonsDownloaded))
|
||||
ch <- prometheus.MustNewConstMetric(collector.seasonMonitoredMetric, prometheus.GaugeValue, float64(seasonsMonitored))
|
||||
@ -283,6 +339,7 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
ch <- prometheus.MustNewConstMetric(collector.episodeMetric, prometheus.GaugeValue, float64(episodes))
|
||||
ch <- prometheus.MustNewConstMetric(collector.episodeDownloadedMetric, prometheus.GaugeValue, float64(episodesDownloaded))
|
||||
ch <- prometheus.MustNewConstMetric(collector.episodeMissingMetric, prometheus.GaugeValue, float64(episodesMissing.TotalRecords))
|
||||
ch <- prometheus.MustNewConstMetric(collector.episodeCutoffUnmetMetric, prometheus.GaugeValue, float64(episodesCutoffUnmet.TotalRecords))
|
||||
|
||||
if collector.config.EnableAdditionalMetrics {
|
||||
ch <- prometheus.MustNewConstMetric(collector.episodeMonitoredMetric, prometheus.GaugeValue, float64(episodesMonitored))
|
||||
@ -291,7 +348,7 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
if len(episodesQualities) > 0 {
|
||||
for qualityName, count := range episodesQualities {
|
||||
ch <- prometheus.MustNewConstMetric(collector.episodeQualitiesMetric, prometheus.GaugeValue, float64(count),
|
||||
qualityName,
|
||||
qualityName, qualityWeights[qualityName],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,7 +62,7 @@ func TestSonarrCollect(t *testing.T) {
|
||||
b, err := os.ReadFile(sonarr_test_fixtures_path + tt.expected_metrics_file)
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.Replace(string(b), "SOMEURL", ts.URL, -1)
|
||||
expected := strings.ReplaceAll(string(b), "SOMEURL", ts.URL)
|
||||
f := strings.NewReader(expected)
|
||||
|
||||
require.NotPanics(func() {
|
||||
|
||||
@ -71,8 +71,8 @@ func TestStatusCollect(t *testing.T) {
|
||||
b, err := os.ReadFile(test_util.COMMON_FIXTURES_PATH + "expected_status_metrics.txt")
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.Replace(string(b), "SOMEURL", ts.URL, -1)
|
||||
expected = strings.Replace(expected, "APP", tt.config.App, -1)
|
||||
expected := strings.ReplaceAll(string(b), "SOMEURL", ts.URL)
|
||||
expected = strings.ReplaceAll(expected, "APP", tt.config.App)
|
||||
|
||||
f := strings.NewReader(expected)
|
||||
|
||||
|
||||
@ -74,8 +74,8 @@ func LoadArrConfig(conf base_config.Config, flags *flag.FlagSet) (*ArrConfig, er
|
||||
// Environment
|
||||
err = k.Load(env.Provider("", ".", func(s string) string {
|
||||
s = strings.ToLower(s)
|
||||
s = strings.Replace(s, "__", ".", -1)
|
||||
s = strings.Replace(s, "_", "-", -1)
|
||||
s = strings.ReplaceAll(s, "__", ".")
|
||||
s = strings.ReplaceAll(s, "_", "-")
|
||||
return backwardsCompatibilityTransforms(s)
|
||||
}), nil)
|
||||
if err != nil {
|
||||
|
||||
@ -25,10 +25,10 @@ func (b BazarrConfig) Validate() error {
|
||||
return v.Errors
|
||||
}
|
||||
if b.SeriesBatchSize < 1 {
|
||||
return fmt.Errorf("series-batch-size must be greater than zero.")
|
||||
return fmt.Errorf("series-batch-size must be greater than zero")
|
||||
}
|
||||
if b.SeriesBatchConcurrency < 1 {
|
||||
return fmt.Errorf("series-batch-concurrency must be greater than zero.")
|
||||
return fmt.Errorf("series-batch-concurrency must be greater than zero")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -39,7 +39,6 @@ func (p *XML) Marshal(o map[string]interface{}) ([]byte, error) {
|
||||
|
||||
func (p *XML) Merge(baseURL string) func(src, dest map[string]interface{}) error {
|
||||
return func(src, dest map[string]interface{}) error {
|
||||
|
||||
if src["api-key"] != nil && src["api-key"].(string) != "" {
|
||||
dest["api-key"] = src["api-key"]
|
||||
}
|
||||
@ -50,7 +49,18 @@ func (p *XML) Merge(baseURL string) func(src, dest map[string]interface{}) error
|
||||
}
|
||||
|
||||
// Add or replace target port
|
||||
u.Host = u.Hostname() + ":" + src["target-port"].(string)
|
||||
target_port := src["target-port"].(string)
|
||||
if len(target_port) > 0 {
|
||||
// AKA Port is defined in the config xml so we use this value as the source
|
||||
// of truth.
|
||||
u.Host = u.Hostname() + ":" + target_port
|
||||
|
||||
// If Port is NOT defined in the XML it may be configured by the
|
||||
// *ARR__SERVER__PORT environment varible passed directly to *arr services.
|
||||
// In this case, we do not want to override whatever URL (that may include
|
||||
// the port) passed to exportarr by other means.
|
||||
}
|
||||
|
||||
u = u.JoinPath(src["url-base"].(string))
|
||||
dest["url"] = u.String()
|
||||
return nil
|
||||
|
||||
@ -23,3 +23,9 @@ type TagMovies []struct {
|
||||
Label string `json:"label"`
|
||||
MovieIds []int `json:"movieIds"`
|
||||
}
|
||||
|
||||
// CutoffUnmetMovies - Stores struct of JSON response
|
||||
// https://radarr.video/docs/api/#/Cutoff/get_api_v3_wanted_cutoff
|
||||
type CutoffUnmetMovies struct {
|
||||
TotalRecords int `json:"totalRecords"`
|
||||
}
|
||||
|
||||
@ -51,3 +51,20 @@ type SystemHealthMessage struct {
|
||||
Message string `json:"message"`
|
||||
WikiURL string `json:"wikiUrl"`
|
||||
}
|
||||
|
||||
// Qualities - Stores struct of JSON response
|
||||
// https://wiki.servarr.com/en/sonarr/settings#quality-1
|
||||
type Qualities []struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Weight int `json:"weight"`
|
||||
MinSize float32 `json:"minSize"`
|
||||
MaxSize float32 `json:"maxSize"`
|
||||
PrefferredSize float32 `json:"preferredSize"`
|
||||
Quality struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Source string `json:"source"`
|
||||
Resolution int `json:"resolution"`
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,6 +42,12 @@ type Missing struct {
|
||||
TotalRecords int `json:"totalRecords"`
|
||||
}
|
||||
|
||||
// CutoffUnmet - Stores struct of JSON response
|
||||
// https://sonarr.tv/docs/api/#/Cutoff/get_api_v3_wanted_cutoff
|
||||
type CutoffUnmet struct {
|
||||
TotalRecords int `json:"totalRecords"`
|
||||
}
|
||||
|
||||
// EpisodeFile - Stores struct of JSON response
|
||||
// https://github.com/Sonarr/Sonarr/wiki/EpisodeFile
|
||||
type EpisodeFile []struct {
|
||||
@ -64,3 +70,11 @@ type Episode []struct {
|
||||
HasFile bool `json:"hasFile"`
|
||||
Monitored bool `json:"monitored"`
|
||||
}
|
||||
|
||||
// TagSeries - Stores struct of JSON response for tag details
|
||||
// https://sonarr.tv/docs/api/#/TagDetails/get_api_v3_tag_detail
|
||||
type TagSeries []struct {
|
||||
ID int `json:"id"`
|
||||
Label string `json:"label"`
|
||||
SeriesIds []int `json:"seriesIds"`
|
||||
}
|
||||
|
||||
25
internal/arr/test_fixtures/lidarr/expected_metrics.txt
Normal file
25
internal/arr/test_fixtures/lidarr/expected_metrics.txt
Normal file
@ -0,0 +1,25 @@
|
||||
# HELP lidarr_albums_missing_total Total number of missing albums
|
||||
# TYPE lidarr_albums_missing_total gauge
|
||||
lidarr_albums_missing_total{url="SOMEURL"} 321
|
||||
# HELP lidarr_albums_total Total number of albums
|
||||
# TYPE lidarr_albums_total gauge
|
||||
lidarr_albums_total{url="SOMEURL"} 7
|
||||
# HELP lidarr_artists_filesize_bytes Total fizesize of all artists in bytes
|
||||
# TYPE lidarr_artists_filesize_bytes gauge
|
||||
lidarr_artists_filesize_bytes{url="SOMEURL"} 105
|
||||
# HELP lidarr_artists_genres_total Total number of artists by genre
|
||||
# TYPE lidarr_artists_genres_total gauge
|
||||
lidarr_artists_genres_total{genre="Prog",url="SOMEURL"} 1
|
||||
lidarr_artists_genres_total{genre="Rock",url="SOMEURL"} 1
|
||||
# HELP lidarr_artists_monitored_total Total number of monitored artists
|
||||
# TYPE lidarr_artists_monitored_total gauge
|
||||
lidarr_artists_monitored_total{url="SOMEURL"} 1
|
||||
# HELP lidarr_artists_total Total number of artists
|
||||
# TYPE lidarr_artists_total gauge
|
||||
lidarr_artists_total{url="SOMEURL"} 2
|
||||
# HELP lidarr_songs_downloaded_total Total number of downloaded songs
|
||||
# TYPE lidarr_songs_downloaded_total gauge
|
||||
lidarr_songs_downloaded_total{url="SOMEURL"} 9
|
||||
# HELP lidarr_songs_total Total number of songs
|
||||
# TYPE lidarr_songs_total gauge
|
||||
lidarr_songs_total{url="SOMEURL"} 13
|
||||
@ -0,0 +1,36 @@
|
||||
# HELP lidarr_albums_genres_total Total number of albums by genre
|
||||
# TYPE lidarr_albums_genres_total gauge
|
||||
lidarr_albums_genres_total{genre="Prog",url="SOMEURL"} 2
|
||||
lidarr_albums_genres_total{genre="Rock",url="SOMEURL"} 2
|
||||
# HELP lidarr_albums_missing_total Total number of missing albums
|
||||
# TYPE lidarr_albums_missing_total gauge
|
||||
lidarr_albums_missing_total{url="SOMEURL"} 321
|
||||
# HELP lidarr_albums_monitored_total Total number of albums
|
||||
# TYPE lidarr_albums_monitored_total gauge
|
||||
lidarr_albums_monitored_total{url="SOMEURL"} 2
|
||||
# HELP lidarr_albums_total Total number of albums
|
||||
# TYPE lidarr_albums_total gauge
|
||||
lidarr_albums_total{url="SOMEURL"} 7
|
||||
# HELP lidarr_artists_filesize_bytes Total fizesize of all artists in bytes
|
||||
# TYPE lidarr_artists_filesize_bytes gauge
|
||||
lidarr_artists_filesize_bytes{url="SOMEURL"} 105
|
||||
# HELP lidarr_artists_genres_total Total number of artists by genre
|
||||
# TYPE lidarr_artists_genres_total gauge
|
||||
lidarr_artists_genres_total{genre="Prog",url="SOMEURL"} 1
|
||||
lidarr_artists_genres_total{genre="Rock",url="SOMEURL"} 1
|
||||
# HELP lidarr_artists_monitored_total Total number of monitored artists
|
||||
# TYPE lidarr_artists_monitored_total gauge
|
||||
lidarr_artists_monitored_total{url="SOMEURL"} 1
|
||||
# HELP lidarr_artists_total Total number of artists
|
||||
# TYPE lidarr_artists_total gauge
|
||||
lidarr_artists_total{url="SOMEURL"} 2
|
||||
# HELP lidarr_songs_downloaded_total Total number of downloaded songs
|
||||
# TYPE lidarr_songs_downloaded_total gauge
|
||||
lidarr_songs_downloaded_total{url="SOMEURL"} 9
|
||||
# HELP lidarr_songs_quality_total Total number of downloaded songs by quality
|
||||
# TYPE lidarr_songs_quality_total gauge
|
||||
lidarr_songs_quality_total{quality="High",url="SOMEURL",weight="10"} 2
|
||||
lidarr_songs_quality_total{quality="Medium",url="SOMEURL",weight="5"} 2
|
||||
# HELP lidarr_songs_total Total number of songs
|
||||
# TYPE lidarr_songs_total gauge
|
||||
lidarr_songs_total{url="SOMEURL"} 13
|
||||
8
internal/arr/test_fixtures/lidarr/v3_album.json
Normal file
8
internal/arr/test_fixtures/lidarr/v3_album.json
Normal file
@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"monitored": true
|
||||
},
|
||||
{
|
||||
"monitored": false
|
||||
}
|
||||
]
|
||||
27
internal/arr/test_fixtures/lidarr/v3_artist.json
Normal file
27
internal/arr/test_fixtures/lidarr/v3_artist.json
Normal file
@ -0,0 +1,27 @@
|
||||
[
|
||||
{
|
||||
"monitored": true,
|
||||
"statistics": {
|
||||
"albumCount": 1,
|
||||
"trackFileCount": 2,
|
||||
"totalTrackCount": 4,
|
||||
"sizeOnDisk": 5
|
||||
},
|
||||
"genres": [
|
||||
"Rock"
|
||||
]
|
||||
},
|
||||
{
|
||||
"ended": true,
|
||||
"monitored": false,
|
||||
"statistics": {
|
||||
"albumCount": 6,
|
||||
"trackFileCount": 7,
|
||||
"totalTrackCount": 9,
|
||||
"sizeOnDisk": 100
|
||||
},
|
||||
"genres": [
|
||||
"Prog"
|
||||
]
|
||||
}
|
||||
]
|
||||
14
internal/arr/test_fixtures/lidarr/v3_qualitydefinition.json
Normal file
14
internal/arr/test_fixtures/lidarr/v3_qualitydefinition.json
Normal file
@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"quality": {
|
||||
"name": "Medium"
|
||||
},
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"quality": {
|
||||
"name": "High"
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
16
internal/arr/test_fixtures/lidarr/v3_trackfile.json
Normal file
16
internal/arr/test_fixtures/lidarr/v3_trackfile.json
Normal file
@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"quality": {
|
||||
"quality": {
|
||||
"name": "High"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"quality": {
|
||||
"quality": {
|
||||
"name": "Medium"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
3
internal/arr/test_fixtures/lidarr/v3_wanted_missing.json
Normal file
3
internal/arr/test_fixtures/lidarr/v3_wanted_missing.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"totalRecords": 321
|
||||
}
|
||||
@ -10,14 +10,17 @@ radarr_movie_filesize_total{url="SOMEURL"} 1.47062956689e+11
|
||||
# HELP radarr_movie_missing_total Total number of missing movies
|
||||
# TYPE radarr_movie_missing_total gauge
|
||||
radarr_movie_missing_total{url="SOMEURL"} 2
|
||||
# HELP radarr_movie_cutoff_unmet_total Total number of movies with cutoff unmet
|
||||
# TYPE radarr_movie_cutoff_unmet_total gauge
|
||||
radarr_movie_cutoff_unmet_total{url="SOMEURL"} 1179
|
||||
# HELP radarr_movie_monitored_total Total number of monitored movies
|
||||
# TYPE radarr_movie_monitored_total gauge
|
||||
radarr_movie_monitored_total{url="SOMEURL"} 7
|
||||
# HELP radarr_movie_quality_total Total number of downloaded movies by quality
|
||||
# TYPE radarr_movie_quality_total gauge
|
||||
radarr_movie_quality_total{quality="Bluray-1080p",url="SOMEURL"} 1
|
||||
radarr_movie_quality_total{quality="Bluray-2160p",url="SOMEURL"} 2
|
||||
radarr_movie_quality_total{quality="Remux-2160p",url="SOMEURL"} 1
|
||||
radarr_movie_quality_total{quality="Bluray-1080p",url="SOMEURL",weight="5"} 1
|
||||
radarr_movie_quality_total{quality="Bluray-2160p",url="SOMEURL",weight="10"} 2
|
||||
radarr_movie_quality_total{quality="Remux-2160p",url="SOMEURL",weight="15"} 1
|
||||
# HELP radarr_movie_tag_total Total number of downloaded movies by tag
|
||||
# TYPE radarr_movie_tag_total gauge
|
||||
radarr_movie_tag_total{tag="somelabel",url="SOMEURL"} 3
|
||||
|
||||
20
internal/arr/test_fixtures/radarr/v3_qualitydefinition.json
Normal file
20
internal/arr/test_fixtures/radarr/v3_qualitydefinition.json
Normal file
@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"quality": {
|
||||
"name": "Bluray-1080p"
|
||||
},
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"quality": {
|
||||
"name": "Bluray-2160p"
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"quality": {
|
||||
"name": "Remux-2160p"
|
||||
},
|
||||
"weight": 15
|
||||
}
|
||||
]
|
||||
3
internal/arr/test_fixtures/radarr/v3_wanted_cutoff.json
Normal file
3
internal/arr/test_fixtures/radarr/v3_wanted_cutoff.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"totalRecords": 1179
|
||||
}
|
||||
@ -4,6 +4,9 @@ sonarr_episode_downloaded_total{url="SOMEURL"} 285
|
||||
# HELP sonarr_episode_missing_total Total number of missing episodes
|
||||
# TYPE sonarr_episode_missing_total gauge
|
||||
sonarr_episode_missing_total{url="SOMEURL"} 1179
|
||||
# HELP sonarr_episode_cutoff_unmet_total Total number of episodes with cutoff unmet
|
||||
# TYPE sonarr_episode_cutoff_unmet_total gauge
|
||||
sonarr_episode_cutoff_unmet_total{url="SOMEURL"} 1179
|
||||
# HELP sonarr_episode_total Total number of episodes
|
||||
# TYPE sonarr_episode_total gauge
|
||||
sonarr_episode_total{url="SOMEURL"} 675
|
||||
@ -25,6 +28,10 @@ sonarr_series_downloaded_total{url="SOMEURL"} 5
|
||||
# HELP sonarr_series_filesize_bytes Total fizesize of all series in bytes
|
||||
# TYPE sonarr_series_filesize_bytes gauge
|
||||
sonarr_series_filesize_bytes{url="SOMEURL"} 7.91293980833e+11
|
||||
# HELP sonarr_series_tag_total Total number of downloaded series by tag
|
||||
# TYPE sonarr_series_tag_total gauge
|
||||
sonarr_series_tag_total{tag="comedy",url="SOMEURL"} 3
|
||||
sonarr_series_tag_total{tag="drama",url="SOMEURL"} 2
|
||||
# HELP sonarr_series_monitored_total Total number of monitored series
|
||||
# TYPE sonarr_series_monitored_total gauge
|
||||
sonarr_series_monitored_total{url="SOMEURL"} 5
|
||||
|
||||
@ -4,13 +4,16 @@ sonarr_episode_downloaded_total{url="SOMEURL"} 285
|
||||
# HELP sonarr_episode_missing_total Total number of missing episodes
|
||||
# TYPE sonarr_episode_missing_total gauge
|
||||
sonarr_episode_missing_total{url="SOMEURL"} 1179
|
||||
# HELP sonarr_episode_cutoff_unmet_total Total number of episodes with cutoff unmet
|
||||
# TYPE sonarr_episode_cutoff_unmet_total gauge
|
||||
sonarr_episode_cutoff_unmet_total{url="SOMEURL"} 1179
|
||||
# HELP sonarr_episode_monitored_total Total number of monitored episodes
|
||||
# TYPE sonarr_episode_monitored_total gauge
|
||||
sonarr_episode_monitored_total{url="SOMEURL"} 12
|
||||
# HELP sonarr_episode_quality_total Total number of downloaded episodes by quality
|
||||
# TYPE sonarr_episode_quality_total gauge
|
||||
sonarr_episode_quality_total{quality="WEBDL-1080p",url="SOMEURL"} 12
|
||||
sonarr_episode_quality_total{quality="WEBRip-1080p",url="SOMEURL"} 6
|
||||
sonarr_episode_quality_total{quality="WEBDL-1080p",url="SOMEURL",weight="10"} 12
|
||||
sonarr_episode_quality_total{quality="WEBRip-1080p",url="SOMEURL",weight="5"} 6
|
||||
# HELP sonarr_episode_total Total number of episodes
|
||||
# TYPE sonarr_episode_total gauge
|
||||
sonarr_episode_total{url="SOMEURL"} 675
|
||||
@ -35,6 +38,10 @@ sonarr_series_downloaded_total{url="SOMEURL"} 5
|
||||
# HELP sonarr_series_filesize_bytes Total fizesize of all series in bytes
|
||||
# TYPE sonarr_series_filesize_bytes gauge
|
||||
sonarr_series_filesize_bytes{url="SOMEURL"} 7.91293980833e+11
|
||||
# HELP sonarr_series_tag_total Total number of downloaded series by tag
|
||||
# TYPE sonarr_series_tag_total gauge
|
||||
sonarr_series_tag_total{tag="comedy",url="SOMEURL"} 3
|
||||
sonarr_series_tag_total{tag="drama",url="SOMEURL"} 2
|
||||
# HELP sonarr_series_monitored_total Total number of monitored series
|
||||
# TYPE sonarr_series_monitored_total gauge
|
||||
sonarr_series_monitored_total{url="SOMEURL"} 5
|
||||
|
||||
14
internal/arr/test_fixtures/sonarr/v3_qualitydefinition.json
Normal file
14
internal/arr/test_fixtures/sonarr/v3_qualitydefinition.json
Normal file
@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"quality": {
|
||||
"name": "WEBRip-1080p"
|
||||
},
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"quality": {
|
||||
"name": "WEBDL-1080p"
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
33
internal/arr/test_fixtures/sonarr/v3_tag_detail.json
Normal file
33
internal/arr/test_fixtures/sonarr/v3_tag_detail.json
Normal file
@ -0,0 +1,33 @@
|
||||
[
|
||||
{
|
||||
"label": "comedy",
|
||||
"delayProfileIds": [],
|
||||
"importListIds": [],
|
||||
"notificationIds": [],
|
||||
"restrictionIds": [],
|
||||
"indexerIds": [],
|
||||
"downloadClientIds": [],
|
||||
"autoTagIds": [],
|
||||
"seriesIds": [
|
||||
37,
|
||||
21,
|
||||
46
|
||||
],
|
||||
"id": 14
|
||||
},
|
||||
{
|
||||
"label": "drama",
|
||||
"delayProfileIds": [],
|
||||
"importListIds": [],
|
||||
"notificationIds": [],
|
||||
"restrictionIds": [],
|
||||
"indexerIds": [],
|
||||
"downloadClientIds": [],
|
||||
"autoTagIds": [],
|
||||
"seriesIds": [
|
||||
371,
|
||||
464
|
||||
],
|
||||
"id": 196
|
||||
}
|
||||
]
|
||||
3
internal/arr/test_fixtures/sonarr/v3_wanted_cutoff.json
Normal file
3
internal/arr/test_fixtures/sonarr/v3_wanted_cutoff.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"totalRecords": 1179
|
||||
}
|
||||
@ -1,3 +1,3 @@
|
||||
{
|
||||
"totalRecords": 1179
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ func NewClient(baseURL string, insecureSkipVerify bool, auth Authenticator) (*Cl
|
||||
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to parse URL(%s): %w", baseURL, err)
|
||||
return nil, fmt.Errorf("failed to parse URL(%s): %w", baseURL, err)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
@ -43,7 +43,7 @@ func (c *Client) unmarshalBody(b io.Reader, target interface{}) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// return recovered panic as error
|
||||
err = fmt.Errorf("Recovered from panic: %s", r)
|
||||
err = fmt.Errorf("recovered from panic: %s", r)
|
||||
|
||||
log := zap.S()
|
||||
if zap.S().Level() == zap.DebugLevel {
|
||||
@ -83,11 +83,11 @@ func (c *Client) DoRequest(endpoint string, target interface{}, queryParams ...Q
|
||||
|
||||
req, err := http.NewRequest("GET", url.String(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create HTTP Request(%s): %w", url, err)
|
||||
return fmt.Errorf("failed to create HTTP Request(%s): %w", url, err)
|
||||
}
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to execute HTTP Request(%s): %w", url, err)
|
||||
return fmt.Errorf("failed to execute HTTP Request(%s): %w", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return c.unmarshalBody(resp.Body, target)
|
||||
|
||||
@ -26,7 +26,7 @@ func (t *ExportarrTransport) RoundTrip(req *http.Request) (*http.Response, error
|
||||
if t.auth != nil {
|
||||
err := t.auth.Auth(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error authenticating request: %w", err)
|
||||
return nil, fmt.Errorf("error authenticating request: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,19 +40,19 @@ func (t *ExportarrTransport) RoundTrip(req *http.Request) (*http.Response, error
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error sending HTTP Request: %w", err)
|
||||
return nil, fmt.Errorf("error sending HTTP Request: %w", err)
|
||||
} else {
|
||||
return nil, fmt.Errorf("Received Server Error Status Code: %d", resp.StatusCode)
|
||||
return nil, fmt.Errorf("received Server Error Status Code: %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
if resp.StatusCode >= 400 && resp.StatusCode <= 499 {
|
||||
return nil, fmt.Errorf("Received Client Error Status Code: %d", resp.StatusCode)
|
||||
return nil, fmt.Errorf("received Client Error Status Code: %d", resp.StatusCode)
|
||||
}
|
||||
if resp.StatusCode >= 300 && resp.StatusCode <= 399 {
|
||||
if location, err := resp.Location(); err == nil {
|
||||
return nil, fmt.Errorf("Received Redirect Status Code: %d, Location: %s", resp.StatusCode, location.String())
|
||||
return nil, fmt.Errorf("received Redirect Status Code: %d, Location: %s", resp.StatusCode, location.String())
|
||||
} else {
|
||||
return nil, fmt.Errorf("Received Redirect Status Code: %d, ", resp.StatusCode)
|
||||
return nil, fmt.Errorf("received Redirect Status Code: %d, ", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
|
||||
@ -57,8 +57,8 @@ func LoadConfig(flags *flag.FlagSet) (*Config, error) {
|
||||
// Environment
|
||||
err = k.Load(env.Provider("", ".", func(s string) string {
|
||||
s = strings.ToLower(s)
|
||||
s = strings.Replace(s, "__", ".", -1)
|
||||
s = strings.Replace(s, "_", "-", -1)
|
||||
s = strings.ReplaceAll(s, "__", ".")
|
||||
s = strings.ReplaceAll(s, "_", "-")
|
||||
return backwardsCompatibilityTransforms(s)
|
||||
}), nil)
|
||||
if err != nil {
|
||||
@ -75,11 +75,11 @@ func LoadConfig(flags *flag.FlagSet) (*Config, error) {
|
||||
if apiKeyFile != "" {
|
||||
data, err := os.ReadFile(apiKeyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't Read API Key file %w", err)
|
||||
return nil, fmt.Errorf("couldn't Read API Key file %w", err)
|
||||
}
|
||||
|
||||
if err := k.Set("api-key", string(data)); err != nil {
|
||||
return nil, fmt.Errorf("Couldn't merge api-key into config: %w", err)
|
||||
if err := k.Set("api-key", strings.TrimSpace(string(data))); err != nil {
|
||||
return nil, fmt.Errorf("couldn't merge api-key into config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ func (s serverStatCache) Update(stat model.ServerStat) (ServerStats, error) {
|
||||
if stat.DayParsed == "" && s.todayKey != "" {
|
||||
// If the day parsed is empty, it means there are no server side stats.
|
||||
// If we have exportarr stats, something likely went wrong,
|
||||
return s, errors.New("No Parsed Dates from Server, but cache is not empty")
|
||||
return s, errors.New("no Parsed Dates from Server, but cache is not empty")
|
||||
}
|
||||
s.total = stat.Total
|
||||
|
||||
|
||||
@ -176,7 +176,7 @@ func NewSabnzbdCollector(config *config.SabnzbdConfig) (*SabnzbdCollector, error
|
||||
auther := auth.ApiKeyAuth{ApiKey: config.ApiKey}
|
||||
client, err := client.NewClient(config.URL, config.DisableSSLVerify, auther)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to build client: %w", err)
|
||||
return nil, fmt.Errorf("failed to build client: %w", err)
|
||||
}
|
||||
|
||||
return &SabnzbdCollector{
|
||||
@ -197,7 +197,7 @@ func (s *SabnzbdCollector) getQueueStats() (*model.QueueStats, error) {
|
||||
|
||||
err := s.doRequest("queue", stats)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to get queue stats: %w", err)
|
||||
return nil, fmt.Errorf("failed to get queue stats: %w", err)
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
@ -207,7 +207,7 @@ func (s *SabnzbdCollector) getServerStats() (*model.ServerStats, error) {
|
||||
var stats = &model.ServerStats{}
|
||||
err := s.doRequest("server_stats", stats)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to get server stats: %w", err)
|
||||
return nil, fmt.Errorf("failed to get server stats: %w", err)
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
@ -57,7 +57,7 @@ func TestCollect(t *testing.T) {
|
||||
b, err := os.ReadFile("../test_fixtures/expected_metrics.txt")
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.Replace(string(b), "http://127.0.0.1:39965", ts.URL, -1)
|
||||
expected := strings.ReplaceAll(string(b), "http://127.0.0.1:39965", ts.URL)
|
||||
f := strings.NewReader(expected)
|
||||
|
||||
require.NotPanics(func() {
|
||||
|
||||
@ -149,7 +149,7 @@ func (q *QueueStats) UnmarshalJSON(data []byte) error {
|
||||
q.TimeEstimate, err = parseDuration(queue["timeleft"], err)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error parsing queue stats: %w", err)
|
||||
return fmt.Errorf("error parsing queue stats: %w", err)
|
||||
}
|
||||
|
||||
q.DownloadDirDiskspaceTotal *= GB
|
||||
@ -193,7 +193,7 @@ func parseFloat(s interface{}, prevErr error) (float64, error) {
|
||||
|
||||
f, ok := s.(string)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("Invalid float: %v", s)
|
||||
return 0, fmt.Errorf("invalid float: %v", s)
|
||||
}
|
||||
|
||||
if f == "" {
|
||||
@ -220,7 +220,7 @@ func parseSize(s interface{}, prevErr error) (float64, error) {
|
||||
|
||||
sz, ok := s.(string)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("Invalid float: %v", s)
|
||||
return 0, fmt.Errorf("invalid float: %v", s)
|
||||
}
|
||||
|
||||
fields := strings.Fields(strings.TrimSpace(sz))
|
||||
@ -229,7 +229,7 @@ func parseSize(s interface{}, prevErr error) (float64, error) {
|
||||
}
|
||||
|
||||
if len(fields) > 2 {
|
||||
return 0, fmt.Errorf("Invalid size: %s", sz)
|
||||
return 0, fmt.Errorf("invalid size: %s", sz)
|
||||
}
|
||||
|
||||
ret, err := strconv.ParseFloat(fields[0], 64)
|
||||
@ -255,7 +255,7 @@ func parseSize(s interface{}, prevErr error) (float64, error) {
|
||||
case "PB", "P":
|
||||
return ret * 1024 * 1024 * 1024 * 1024 * 1024, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("Invalid size suffix: %s", sz)
|
||||
return 0, fmt.Errorf("invalid size suffix: %s", sz)
|
||||
}
|
||||
}
|
||||
|
||||
@ -271,7 +271,7 @@ func parseDuration(sd interface{}, prevErr error) (time.Duration, error) {
|
||||
|
||||
s, ok := sd.(string)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("Invalid float: %v", sd)
|
||||
return 0, fmt.Errorf("invalid float: %v", sd)
|
||||
}
|
||||
|
||||
if s == "" {
|
||||
@ -280,7 +280,7 @@ func parseDuration(sd interface{}, prevErr error) (time.Duration, error) {
|
||||
|
||||
fields := strings.Split(strings.TrimSpace(s), ":")
|
||||
if len(fields) < 1 || len(fields) > 4 {
|
||||
return 0, fmt.Errorf("Invalid duration: %s", s)
|
||||
return 0, fmt.Errorf("invalid duration: %s", s)
|
||||
}
|
||||
|
||||
intFields := make([]int, len(fields))
|
||||
@ -290,7 +290,7 @@ func parseDuration(sd interface{}, prevErr error) (time.Duration, error) {
|
||||
// Reverse the order of the fields
|
||||
intFields[len(intFields)-1-i], err = strconv.Atoi(f)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Invalid integer in duration: %s: %w", f, err)
|
||||
return 0, fmt.Errorf("invalid integer in duration: %s: %w", f, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ func NewTestServer(t *testing.T, fixture_dir string, fn func(http.ResponseWriter
|
||||
fn(w, r)
|
||||
require.NotEmpty(t, r.URL.Path)
|
||||
// turns /api/some/path into some_path
|
||||
endpoint := strings.Replace(strings.Replace(r.URL.Path, "/api/", "", -1), "/", "_", -1)
|
||||
endpoint := strings.ReplaceAll(strings.ReplaceAll(r.URL.Path, "/api/", ""), "/", "_")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// NOTE: this assumes there is a file that matches the some_path
|
||||
json, err := os.ReadFile(filepath.Join(fixture_dir, endpoint+".json"))
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"postUpdateOptions": [
|
||||
"gomodTidy",
|
||||
"gomodUpdateImportPaths"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user