mirror of
https://github.com/onedr0p/exportarr.git
synced 2026-02-06 10:57:32 +00:00
Compare commits
146 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 | ||
|
|
8161da029e | ||
|
|
793371b2a3 | ||
|
|
582c27972d | ||
|
|
0cb318682e | ||
|
|
f9543a2207 | ||
|
|
56c90ac0ec | ||
|
|
9439b4a634 | ||
|
|
f12eac158b | ||
|
|
6e75f42405 | ||
|
|
933ed39eba | ||
|
|
7523925fa6 | ||
|
|
e398da13ca | ||
|
|
e69c8646c4 | ||
|
|
1aae2a6a18 | ||
|
|
a9033bdc1a | ||
|
|
00e6929b8c | ||
|
|
dfdb9eb657 | ||
|
|
e0b1924a8d | ||
|
|
782270c0dd | ||
|
|
cb86b8d14c | ||
|
|
3c2312fa8f | ||
|
|
817ac68c9b | ||
|
|
3276edb0e9 | ||
|
|
4fd39caea8 | ||
|
|
e5a13f8ed7 | ||
|
|
6afddc328d | ||
|
|
e732d200a9 | ||
|
|
4a2c58739a | ||
|
|
54377efa17 | ||
|
|
b6dd1a160f | ||
|
|
e42774b10c | ||
|
|
6b722c65ad | ||
|
|
2710c3d477 | ||
|
|
294191ecae | ||
|
|
8c51f278ff | ||
|
|
3155232c1f | ||
|
|
6ff960d0d1 | ||
|
|
b8426af721 | ||
|
|
780dc842c3 | ||
|
|
4d71594773 | ||
|
|
b8b7cc6be3 | ||
|
|
2574b477ee | ||
|
|
214c3243a3 | ||
|
|
e37208419f | ||
|
|
955edb2046 |
3
.env.dist
Normal file
3
.env.dist
Normal file
@ -0,0 +1,3 @@
|
||||
APP_API_KEY=RADARR_API_KEY
|
||||
APP_URL=RADARR_URL
|
||||
APP_NAME=radarr
|
||||
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
|
||||
|
||||
25
.github/actions/docker-image/action.yaml
vendored
25
.github/actions/docker-image/action.yaml
vendored
@ -6,6 +6,16 @@ inputs:
|
||||
token:
|
||||
description: Github token
|
||||
required: true
|
||||
push:
|
||||
description: Push Images to GHCR
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
latest:
|
||||
description: Update latest tag
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
@ -16,13 +26,12 @@ runs:
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
flavor: |
|
||||
latest=true
|
||||
prefix=v
|
||||
latest=${{ fromJSON(inputs.latest) }}
|
||||
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
|
||||
@ -39,14 +48,14 @@ runs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ inputs.token }}
|
||||
|
||||
- name: Build and Push
|
||||
- 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
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
push: ${{ fromJSON(inputs.push) }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
|
||||
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@v4
|
||||
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 }}
|
||||
|
||||
17
.github/actions/lint/action.yaml
vendored
Normal file
17
.github/actions/lint/action.yaml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Tests
|
||||
description: Runs Go tests
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ">=1.23"
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.1
|
||||
args: --timeout=5m --config=.github/lint/golangci.yaml
|
||||
9
.github/actions/tests/action.yaml
vendored
9
.github/actions/tests/action.yaml
vendored
@ -10,7 +10,14 @@ runs:
|
||||
with:
|
||||
go-version: ">=1.19"
|
||||
|
||||
- name: Tidy
|
||||
- name: Check Go Fmt
|
||||
shell: bash
|
||||
run: |
|
||||
go version
|
||||
go fmt ./...
|
||||
git diff --exit-code
|
||||
|
||||
- name: Check Go Mod
|
||||
shell: bash
|
||||
run: |
|
||||
go version
|
||||
|
||||
BIN
.github/images/dashboard-1.png
vendored
BIN
.github/images/dashboard-1.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 261 KiB After Width: | Height: | Size: 63 KiB |
BIN
.github/images/dashboard-bazarr.png
vendored
Normal file
BIN
.github/images/dashboard-bazarr.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 398 KiB |
26
.github/lint/golangci.yaml
vendored
Normal file
26
.github/lint/golangci.yaml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
version: "2"
|
||||
|
||||
linters:
|
||||
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$
|
||||
@ -1,35 +1,38 @@
|
||||
---
|
||||
name: ci
|
||||
name: On Merge
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Tests
|
||||
uses: ./.github/actions/tests
|
||||
|
||||
release-image:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
needs: tests
|
||||
if: github.event_name == 'push'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Lint
|
||||
uses: ./.github/actions/lint
|
||||
|
||||
release-image:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build
|
||||
uses: ./.github/actions/docker-image
|
||||
with:
|
||||
token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
latest: false
|
||||
27
.github/workflows/on-pr.yml
vendored
Normal file
27
.github/workflows/on-pr.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
---
|
||||
name: On Pull Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Lint
|
||||
uses: ./.github/actions/lint
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Tests
|
||||
uses: ./.github/actions/tests
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
name: release
|
||||
name: Versioned Release
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -11,17 +11,28 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Tests
|
||||
uses: ./.github/actions/tests
|
||||
|
||||
release-image:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
needs: tests
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Lint
|
||||
uses: ./.github/actions/lint
|
||||
|
||||
release-image:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- tests
|
||||
- lint
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build
|
||||
uses: ./.github/actions/docker-image
|
||||
@ -30,10 +41,12 @@ jobs:
|
||||
|
||||
release-binaries:
|
||||
runs-on: ubuntu-latest
|
||||
needs: tests
|
||||
needs:
|
||||
- tests
|
||||
- lint
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Release
|
||||
uses: ./.github/actions/go-release
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -11,3 +11,6 @@
|
||||
# Operating System
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
.env
|
||||
covprofile
|
||||
|
||||
@ -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.21.1-alpine as builder
|
||||
FROM golang:1.24.6-alpine as builder
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT=""
|
||||
|
||||
30
Makefile
Normal file
30
Makefile
Normal file
@ -0,0 +1,30 @@
|
||||
-include .env
|
||||
|
||||
.PHONY: build run check fmt tidy lint test
|
||||
|
||||
build:
|
||||
docker build . -t exportarr:local
|
||||
|
||||
run:
|
||||
docker rm --force exportarr || echo ""
|
||||
docker run --name exportarr \
|
||||
-e PORT=9707 \
|
||||
-e URL="${APP_URL}" \
|
||||
-e APIKEY="${APP_API_KEY}" \
|
||||
-e LOG_LEVEL="debug" \
|
||||
-p 9707:9707 \
|
||||
-d exportarr:local ${APP_NAME}
|
||||
|
||||
check: fmt tidy lint test
|
||||
|
||||
fmt:
|
||||
go fmt ./...
|
||||
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
lint:
|
||||
golangci-lint run -c .github/lint/golangci.yaml
|
||||
|
||||
test:
|
||||
go test -v -race -covermode atomic -coverprofile=covprofile ./...
|
||||
55
README.md
55
README.md
@ -1,12 +1,15 @@
|
||||
# exportarr
|
||||
# Exportarr
|
||||
|
||||
AIO Prometheus Exporter for Sonarr, Radarr, Lidarr, Prowlarr, Readarr, and Sabnzbd
|
||||
> [!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.
|
||||
|
||||

|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
@ -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" \
|
||||
@ -53,30 +56,30 @@ Visit http://127.0.0.1:9707/metrics to see the app metrics
|
||||
|
||||
## Configuration
|
||||
|
||||
| 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) | ❌ |
|
||||
| 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) | ❌ |
|
||||
|
||||
### 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 backill all Prowlarr Data, either use `PROWLARR__BACKFILL` or `--backfill`.
|
||||
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`
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package main
|
||||
|
||||
import "github.com/onedr0p/exportarr/internal/commands"
|
||||
import (
|
||||
"github.com/onedr0p/exportarr/internal/commands"
|
||||
)
|
||||
|
||||
var (
|
||||
appName = "exportarr"
|
||||
@ -10,10 +12,13 @@ var (
|
||||
)
|
||||
|
||||
func main() {
|
||||
commands.Execute(commands.AppInfo{
|
||||
err := commands.Execute(commands.AppInfo{
|
||||
Name: appName,
|
||||
Version: version,
|
||||
BuildTime: buildTime,
|
||||
Revision: revision,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,4 +1,14 @@
|
||||
{
|
||||
"__inputs": [
|
||||
{
|
||||
"name": "DS_PROMETHEUS",
|
||||
"label": "Prometheus",
|
||||
"description": "",
|
||||
"type": "datasource",
|
||||
"pluginId": "prometheus",
|
||||
"pluginName": "Prometheus"
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
62
go.mod
62
go.mod
@ -1,47 +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.1
|
||||
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.0.1
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.8.4
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
|
||||
golang.org/x/sync v0.3.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/golang/protobuf v1.5.3 // indirect
|
||||
github.com/gookit/filter v1.2.0 // indirect
|
||||
github.com/gookit/goutil v0.6.12 // 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/kr/text v0.2.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // 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/mapstructure v1.5.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.4.1-0.20230718164431-9a2bf3000d16 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.11.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
go.uber.org/atomic v1.7.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.12.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
google.golang.org/protobuf v1.31.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
|
||||
)
|
||||
|
||||
218
go.sum
218
go.sum
@ -1,173 +1,93 @@
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
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.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
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/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/gookit/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI=
|
||||
github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg=
|
||||
github.com/gookit/filter v1.1.4 h1:SXd6PEumiP/0jtF2crQRaz1wmKwHbW9xg5Ds6/ZP16w=
|
||||
github.com/gookit/filter v1.1.4/go.mod h1:0CEPQvudso375RitQf9X8HerUg9cz8N7c/yn6b1RMzM=
|
||||
github.com/gookit/filter v1.2.0 h1:r7E01dHVkysb5WgzooiGsfblHGShEZCeGcyYM+5IpYU=
|
||||
github.com/gookit/filter v1.2.0/go.mod h1:bXs9RcB4Blxwny970opiwABeIEqQ/gzOMmHBhKwBdms=
|
||||
github.com/gookit/goutil v0.5.12/go.mod h1:6vhWm/bSYXGE8poqFbFz6IGM7jV2r6qVhyK567SX/AI=
|
||||
github.com/gookit/goutil v0.5.15 h1:FaRyj0uVqi7j92QHsG+2Sc1VZ7/7ma77UD3/wBpwyTc=
|
||||
github.com/gookit/goutil v0.5.15/go.mod h1:ozPE16eJS9f89aVbVk05ocEJsia3KPrYUqPTs8GvUTw=
|
||||
github.com/gookit/goutil v0.6.12 h1:73vPUcTtVGXbhSzBOFcnSB1aJl7Jq9np3RAE50yIDZc=
|
||||
github.com/gookit/goutil v0.6.12/go.mod h1:g6krlFib8xSe3G1h02IETowOtrUGpAmetT8IevDpvpM=
|
||||
github.com/gookit/validate v1.4.6 h1:Ix8NRy2+6z4YGHWXgZL9+emy9wRI2GWyhW2smPcIlSU=
|
||||
github.com/gookit/validate v1.4.6/go.mod h1:1rjeYaYlMK/8od4oge5C+Gt/3DnHkXymLPda7+3urC8=
|
||||
github.com/gookit/validate v1.5.1 h1:rPp64QZQJM+fysGFAhKpvekQAav4Ok6sjfTs9ZtxcpA=
|
||||
github.com/gookit/validate v1.5.1/go.mod h1:SskOHUQokzMNt6T3r7N+N/4me/6fxDx+tmoXf/3ZQog=
|
||||
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.0.1 h1:1dYGITt1I23x8cfx8ZnldtezdyaZtfAuRtIFOiRzK7g=
|
||||
github.com/knadh/koanf/v2 v2.0.1/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus=
|
||||
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/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
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/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/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
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.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
|
||||
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
|
||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
||||
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
||||
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.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
|
||||
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
|
||||
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.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
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.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
|
||||
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.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.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
|
||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
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,15 +9,17 @@ import (
|
||||
|
||||
"github.com/onedr0p/exportarr/internal/arr/config"
|
||||
"github.com/onedr0p/exportarr/internal/client"
|
||||
base_client "github.com/onedr0p/exportarr/internal/client"
|
||||
)
|
||||
|
||||
func NewClient(config *config.ArrConfig) (*base_client.Client, error) {
|
||||
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) {
|
||||
@ -88,11 +90,13 @@ func (a *FormAuth) Auth(req *http.Request) error {
|
||||
}
|
||||
|
||||
u := a.AuthBaseURL.JoinPath("login")
|
||||
u.Query().Add("ReturnUrl", "/general/settings")
|
||||
vals := u.Query()
|
||||
vals.Add("ReturnUrl", "/general/settings")
|
||||
u.RawQuery = vals.Encode()
|
||||
|
||||
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")
|
||||
@ -100,27 +104,31 @@ 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
|
||||
for _, cookie := range authResp.Cookies() {
|
||||
if strings.HasSuffix(cookie.Name, "arrAuth") {
|
||||
copy := *cookie
|
||||
a.cookie = ©
|
||||
found = true
|
||||
break
|
||||
}
|
||||
return fmt.Errorf("Failed to renew FormAuth Cookie: No Cookie with suffix 'arrAuth' found")
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("failed to renew FormAuth Cookie: No Cookie with suffix 'arrAuth' found")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -183,7 +183,7 @@ func TestRoundTrip_Retries(t *testing.T) {
|
||||
{
|
||||
name: "Err",
|
||||
testFunc: func(req *http.Request) (*http.Response, error) {
|
||||
return nil, &http.ProtocolError{}
|
||||
return nil, http.ErrNotSupported
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
597
internal/arr/collector/bazarr.go
Normal file
597
internal/arr/collector/bazarr.go
Normal file
@ -0,0 +1,597 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/onedr0p/exportarr/internal/arr/client"
|
||||
"github.com/onedr0p/exportarr/internal/arr/config"
|
||||
"github.com/onedr0p/exportarr/internal/arr/model"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type stats struct {
|
||||
downloaded int
|
||||
monitored int
|
||||
unmonitored int
|
||||
missing int
|
||||
wanted int
|
||||
fileSize int64
|
||||
history int
|
||||
languages map[string]int
|
||||
scores map[string]int
|
||||
providers map[string]int
|
||||
mut sync.Mutex
|
||||
}
|
||||
|
||||
func newStats() *stats {
|
||||
return &stats{
|
||||
languages: make(map[string]int),
|
||||
scores: make(map[string]int),
|
||||
providers: make(map[string]int),
|
||||
}
|
||||
}
|
||||
|
||||
type bazarrCollector struct {
|
||||
config *config.ArrConfig // App configuration
|
||||
subtitlesHistoryMetric *prometheus.Desc // Total number of subtitles history
|
||||
subtitlesDownloadedMetric *prometheus.Desc // Total number of subtitles downloaded
|
||||
subtitlesMonitoredMetric *prometheus.Desc // Total number of subtitles monitored
|
||||
subtitlesUnmonitoredMetric *prometheus.Desc // Total number of subtitles unmonitored
|
||||
subtitlesWantedMetric *prometheus.Desc // Total number of wanted subtitle
|
||||
subtitlesMissingMetric *prometheus.Desc // Total number of missing subtitle
|
||||
subtitlesFileSizeMetric *prometheus.Desc // Total fizesize of all subtitle in bytes
|
||||
|
||||
episodeSubtitlesHistoryMetric *prometheus.Desc // Total number of episode subtitles history
|
||||
episodeSubtitlesDownloadedMetric *prometheus.Desc // Total number of episode subtitles downloaded
|
||||
episodeSubtitlesMonitoredMetric *prometheus.Desc // Total number of episode subtitles monitored
|
||||
episodeSubtitlesUnmonitoredMetric *prometheus.Desc // Total number of episode subtitles unmonitored
|
||||
episodeSubtitlesWantedMetric *prometheus.Desc // Total number of episode wanted subtitle
|
||||
episodeSubtitlesMissingMetric *prometheus.Desc // Total number of episode missing subtitle
|
||||
episodeSubtitlesFileSizeMetric *prometheus.Desc // Total fizesize of all episode subtitle in bytes
|
||||
|
||||
movieSubtitlesHistoryMetric *prometheus.Desc // Total number of movie subtitles history
|
||||
movieSubtitlesDownloadedMetric *prometheus.Desc // Total number of movie subtitles downloaded
|
||||
movieSubtitlesMonitoredMetric *prometheus.Desc // Total number of movie subtitles monitored
|
||||
movieSubtitlesUnmonitoredMetric *prometheus.Desc // Total number of movie subtitles unmonitored
|
||||
movieSubtitlesWantedMetric *prometheus.Desc // Total number of movie wanted subtitle
|
||||
movieSubtitlesMissingMetric *prometheus.Desc // Total number of movie missing subtitle
|
||||
movieSubtitlesFileSizeMetric *prometheus.Desc // Total fizesize of all movie subtitle in bytes
|
||||
|
||||
subtitlesLanguageMetric *prometheus.Desc // Total number of subtitle by language
|
||||
subtitlesScoreMetric *prometheus.Desc // Total number of subtitle by score
|
||||
subtitlesProviderMetric *prometheus.Desc // Total number of subtitle by provider
|
||||
|
||||
systemHealthMetric *prometheus.Desc // Total number of health issues
|
||||
systemStatusMetric *prometheus.Desc // Total number of system statuses
|
||||
errorMetric *prometheus.Desc // Error Description for use with InvalidMetric
|
||||
}
|
||||
|
||||
func createIDBatches(ids []string, batchSize int) [][]string {
|
||||
if len(ids) == 0 {
|
||||
return [][]string{}
|
||||
}
|
||||
items := ids
|
||||
ret := [][]string{}
|
||||
for batchSize < len(items) {
|
||||
ret = append(ret, items[0:batchSize:batchSize])
|
||||
items = items[batchSize:]
|
||||
}
|
||||
return append(ret, items)
|
||||
}
|
||||
|
||||
func NewBazarrCollector(c *config.ArrConfig) *bazarrCollector {
|
||||
subtitleText := "subtitles"
|
||||
episodeText := "episode"
|
||||
movieText := "movie"
|
||||
return &bazarrCollector{
|
||||
config: c,
|
||||
subtitlesHistoryMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_history_total", c.App, subtitleText),
|
||||
fmt.Sprintf("Total number of history %s", subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
subtitlesDownloadedMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_downloaded_total", c.App, subtitleText),
|
||||
fmt.Sprintf("Total number of downloaded %s", subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
subtitlesMonitoredMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_monitored_total", c.App, subtitleText),
|
||||
fmt.Sprintf("Total number of monitored %s", subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
subtitlesUnmonitoredMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_unmonitored_total", c.App, subtitleText),
|
||||
fmt.Sprintf("Total number of unmonitored %s", subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
subtitlesWantedMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_wanted_total", c.App, subtitleText),
|
||||
fmt.Sprintf("Total number of wanted %s", subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
subtitlesMissingMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_missing_total", c.App, subtitleText),
|
||||
fmt.Sprintf("Total number of missing %s", subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
subtitlesFileSizeMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_filesize_total", c.App, subtitleText),
|
||||
fmt.Sprintf("Total filesize of all %s", subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
episodeSubtitlesHistoryMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_%s_history_total", c.App, episodeText, subtitleText),
|
||||
fmt.Sprintf("Total number of history %s %s", episodeText, subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
episodeSubtitlesDownloadedMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_%s_downloaded_total", c.App, episodeText, subtitleText),
|
||||
fmt.Sprintf("Total number of downloaded %s %s", episodeText, subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
episodeSubtitlesMonitoredMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_%s_monitored_total", c.App, episodeText, subtitleText),
|
||||
fmt.Sprintf("Total number of monitored %s %s", episodeText, subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
episodeSubtitlesUnmonitoredMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_%s_unmonitored_total", c.App, episodeText, subtitleText),
|
||||
fmt.Sprintf("Total number of unmonitored %s %s", episodeText, subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
episodeSubtitlesWantedMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_%s_wanted_total", c.App, episodeText, subtitleText),
|
||||
fmt.Sprintf("Total number of wanted %s %s", episodeText, subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
episodeSubtitlesMissingMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_%s_missing_total", c.App, episodeText, subtitleText),
|
||||
fmt.Sprintf("Total number of missing %s %s", episodeText, subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
episodeSubtitlesFileSizeMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_%s_filesize_total", c.App, episodeText, subtitleText),
|
||||
fmt.Sprintf("Total filesize of all %s %s", episodeText, subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
movieSubtitlesHistoryMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_%s_history_total", c.App, movieText, subtitleText),
|
||||
fmt.Sprintf("Total number of history %s %s", movieText, subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
movieSubtitlesDownloadedMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_%s_downloaded_total", c.App, movieText, subtitleText),
|
||||
fmt.Sprintf("Total number of downloaded %s %s", movieText, subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
movieSubtitlesMonitoredMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_%s_monitored_total", c.App, movieText, subtitleText),
|
||||
fmt.Sprintf("Total number of monitored %s %s", movieText, subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
movieSubtitlesUnmonitoredMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_%s_unmonitored_total", c.App, movieText, subtitleText),
|
||||
fmt.Sprintf("Total number of unmonitored %s %s", movieText, subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
movieSubtitlesWantedMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_%s_wanted_total", c.App, movieText, subtitleText),
|
||||
fmt.Sprintf("Total number of wanted %s %s", movieText, subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
movieSubtitlesMissingMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_%s_missing_total", c.App, movieText, subtitleText),
|
||||
fmt.Sprintf("Total number of missing %s %s", movieText, subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
movieSubtitlesFileSizeMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_%s_filesize_total", c.App, movieText, subtitleText),
|
||||
fmt.Sprintf("Total filesize of all %s %s", movieText, subtitleText),
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
subtitlesLanguageMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_language_total", c.App, subtitleText),
|
||||
fmt.Sprintf("Total number of downloaded %s by language", subtitleText),
|
||||
[]string{"language"},
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
subtitlesScoreMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_score_total", c.App, subtitleText),
|
||||
fmt.Sprintf("Total number of downloaded %s by score", subtitleText),
|
||||
[]string{"score"},
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
subtitlesProviderMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_%s_provider_total", c.App, subtitleText),
|
||||
fmt.Sprintf("Total number of downloaded %s by provider", subtitleText),
|
||||
[]string{"provider"},
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
systemHealthMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_system_health_issues", c.App),
|
||||
"Total number of health issues by object and issue",
|
||||
[]string{"object", "issue"},
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
systemStatusMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_system_status", c.App),
|
||||
"System Status",
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
errorMetric: prometheus.NewDesc(
|
||||
fmt.Sprintf("%s_collector_error", c.App),
|
||||
"Error while collecting metrics",
|
||||
nil,
|
||||
prometheus.Labels{"url": c.URL},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (collector *bazarrCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- collector.subtitlesHistoryMetric
|
||||
ch <- collector.subtitlesDownloadedMetric
|
||||
ch <- collector.subtitlesMonitoredMetric
|
||||
ch <- collector.subtitlesUnmonitoredMetric
|
||||
ch <- collector.subtitlesWantedMetric
|
||||
ch <- collector.subtitlesMissingMetric
|
||||
ch <- collector.subtitlesFileSizeMetric
|
||||
|
||||
ch <- collector.episodeSubtitlesHistoryMetric
|
||||
ch <- collector.episodeSubtitlesDownloadedMetric
|
||||
ch <- collector.episodeSubtitlesMonitoredMetric
|
||||
ch <- collector.episodeSubtitlesUnmonitoredMetric
|
||||
ch <- collector.episodeSubtitlesWantedMetric
|
||||
ch <- collector.episodeSubtitlesMissingMetric
|
||||
ch <- collector.episodeSubtitlesFileSizeMetric
|
||||
|
||||
ch <- collector.movieSubtitlesHistoryMetric
|
||||
ch <- collector.movieSubtitlesDownloadedMetric
|
||||
ch <- collector.movieSubtitlesMonitoredMetric
|
||||
ch <- collector.movieSubtitlesUnmonitoredMetric
|
||||
ch <- collector.movieSubtitlesWantedMetric
|
||||
ch <- collector.movieSubtitlesMissingMetric
|
||||
ch <- collector.movieSubtitlesFileSizeMetric
|
||||
ch <- collector.movieSubtitlesFileSizeMetric
|
||||
|
||||
ch <- collector.subtitlesScoreMetric
|
||||
ch <- collector.subtitlesLanguageMetric
|
||||
ch <- collector.subtitlesProviderMetric
|
||||
|
||||
ch <- collector.systemStatusMetric
|
||||
ch <- collector.systemHealthMetric
|
||||
}
|
||||
|
||||
func (collector *bazarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
log := zap.S().With("collector", "bazarr")
|
||||
c, err := client.NewClient(collector.config)
|
||||
if err != nil {
|
||||
log.Errorw("Error creating client", "error", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return
|
||||
}
|
||||
tseries := time.Now()
|
||||
|
||||
collector.EpisodeMovieMetrics(ch, c)
|
||||
collector.SystemMetrics(ch, c)
|
||||
|
||||
mt := time.Since(tseries)
|
||||
log.Debugw("All Completed", "duration", mt)
|
||||
}
|
||||
|
||||
func (collector *bazarrCollector) EpisodeMovieMetrics(ch chan<- prometheus.Metric, c *client.Client) {
|
||||
|
||||
episodeStats := newStats()
|
||||
if collector.config.EnableAdditionalMetrics {
|
||||
episodeStats = collector.CollectEpisodeStats(ch, c)
|
||||
if episodeStats == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
movieStats := collector.CollectMovieStats(ch, c)
|
||||
if movieStats == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if collector.config.EnableAdditionalMetrics {
|
||||
ch <- prometheus.MustNewConstMetric(collector.episodeSubtitlesHistoryMetric, prometheus.GaugeValue, float64(episodeStats.history))
|
||||
ch <- prometheus.MustNewConstMetric(collector.episodeSubtitlesDownloadedMetric, prometheus.GaugeValue, float64(episodeStats.downloaded))
|
||||
ch <- prometheus.MustNewConstMetric(collector.episodeSubtitlesMonitoredMetric, prometheus.GaugeValue, float64(episodeStats.monitored))
|
||||
ch <- prometheus.MustNewConstMetric(collector.episodeSubtitlesUnmonitoredMetric, prometheus.GaugeValue, float64(episodeStats.unmonitored))
|
||||
ch <- prometheus.MustNewConstMetric(collector.episodeSubtitlesWantedMetric, prometheus.GaugeValue, float64(episodeStats.wanted))
|
||||
ch <- prometheus.MustNewConstMetric(collector.episodeSubtitlesMissingMetric, prometheus.GaugeValue, float64(episodeStats.missing))
|
||||
ch <- prometheus.MustNewConstMetric(collector.episodeSubtitlesFileSizeMetric, prometheus.GaugeValue, float64(episodeStats.fileSize))
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(collector.movieSubtitlesHistoryMetric, prometheus.GaugeValue, float64(movieStats.history))
|
||||
ch <- prometheus.MustNewConstMetric(collector.movieSubtitlesDownloadedMetric, prometheus.GaugeValue, float64(movieStats.downloaded))
|
||||
ch <- prometheus.MustNewConstMetric(collector.movieSubtitlesMonitoredMetric, prometheus.GaugeValue, float64(movieStats.monitored))
|
||||
ch <- prometheus.MustNewConstMetric(collector.movieSubtitlesUnmonitoredMetric, prometheus.GaugeValue, float64(movieStats.unmonitored))
|
||||
ch <- prometheus.MustNewConstMetric(collector.movieSubtitlesWantedMetric, prometheus.GaugeValue, float64(movieStats.wanted))
|
||||
ch <- prometheus.MustNewConstMetric(collector.movieSubtitlesMissingMetric, prometheus.GaugeValue, float64(movieStats.missing))
|
||||
ch <- prometheus.MustNewConstMetric(collector.movieSubtitlesFileSizeMetric, prometheus.GaugeValue, float64(movieStats.fileSize))
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(collector.subtitlesDownloadedMetric, prometheus.GaugeValue, float64(episodeStats.downloaded)+float64(movieStats.downloaded))
|
||||
ch <- prometheus.MustNewConstMetric(collector.subtitlesMonitoredMetric, prometheus.GaugeValue, float64(episodeStats.monitored)+float64(movieStats.monitored))
|
||||
ch <- prometheus.MustNewConstMetric(collector.subtitlesUnmonitoredMetric, prometheus.GaugeValue, float64(episodeStats.unmonitored)+float64(movieStats.unmonitored))
|
||||
ch <- prometheus.MustNewConstMetric(collector.subtitlesWantedMetric, prometheus.GaugeValue, float64(episodeStats.wanted)+float64(movieStats.wanted))
|
||||
ch <- prometheus.MustNewConstMetric(collector.subtitlesMissingMetric, prometheus.GaugeValue, float64(episodeStats.missing)+float64(movieStats.missing))
|
||||
ch <- prometheus.MustNewConstMetric(collector.subtitlesFileSizeMetric, prometheus.GaugeValue, float64(episodeStats.fileSize)+float64(movieStats.fileSize))
|
||||
ch <- prometheus.MustNewConstMetric(collector.subtitlesHistoryMetric, prometheus.GaugeValue, float64(episodeStats.history)+float64(movieStats.history))
|
||||
|
||||
// just shove them into episode stats to avoid extra looping
|
||||
if len(movieStats.languages) > 0 {
|
||||
for languageName, count := range movieStats.languages {
|
||||
episodeStats.languages[languageName] += count
|
||||
}
|
||||
}
|
||||
|
||||
if len(episodeStats.languages) > 0 {
|
||||
for languageName, count := range episodeStats.languages {
|
||||
ch <- prometheus.MustNewConstMetric(collector.subtitlesLanguageMetric, prometheus.GaugeValue, float64(count),
|
||||
languageName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// just shove them into episode stats to avoid extra looping
|
||||
if len(movieStats.scores) > 0 {
|
||||
for score, count := range movieStats.scores {
|
||||
episodeStats.scores[score] += count
|
||||
}
|
||||
}
|
||||
|
||||
if len(episodeStats.scores) > 0 {
|
||||
for score, count := range episodeStats.scores {
|
||||
ch <- prometheus.MustNewConstMetric(collector.subtitlesScoreMetric, prometheus.GaugeValue, float64(count),
|
||||
score,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// just shove them into episode stats to avoid extra looping
|
||||
if len(movieStats.providers) > 0 {
|
||||
for providerName, count := range movieStats.providers {
|
||||
episodeStats.providers[providerName] += count
|
||||
}
|
||||
}
|
||||
|
||||
if len(episodeStats.providers) > 0 {
|
||||
for providerName, count := range episodeStats.providers {
|
||||
ch <- prometheus.MustNewConstMetric(collector.subtitlesProviderMetric, prometheus.GaugeValue, float64(count),
|
||||
providerName,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (collector *bazarrCollector) CollectEpisodeStats(ch chan<- prometheus.Metric, c *client.Client) *stats {
|
||||
log := zap.S().With("collector", "bazarr")
|
||||
episodeStats := newStats()
|
||||
|
||||
mseries := time.Now()
|
||||
|
||||
series := model.BazarrSeries{}
|
||||
if err := c.DoRequest("series", &series); err != nil {
|
||||
log.Errorw("Error getting series",
|
||||
"error", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
ids := []string{}
|
||||
for _, s := range series.Data {
|
||||
ids = append(ids, fmt.Sprintf("%d", s.Id))
|
||||
}
|
||||
batches := createIDBatches(ids, collector.config.Bazarr.SeriesBatchSize)
|
||||
|
||||
eg := errgroup.Group{}
|
||||
sem := make(chan int, collector.config.Bazarr.SeriesBatchConcurrency) // limit concurrency via semaphore
|
||||
for _, batch := range batches {
|
||||
params := client.QueryParams{"seriesid[]": batch}
|
||||
sem <- 1 //
|
||||
eg.Go(func() error {
|
||||
defer func() { <-sem }()
|
||||
episodes := model.BazarrEpisodes{}
|
||||
if err := c.DoRequest("episodes", &episodes, params); err != nil {
|
||||
return err
|
||||
}
|
||||
episodeStats.mut.Lock()
|
||||
defer episodeStats.mut.Unlock()
|
||||
for _, e := range episodes.Data {
|
||||
if !e.Monitored {
|
||||
episodeStats.unmonitored++
|
||||
} else {
|
||||
episodeStats.monitored++
|
||||
}
|
||||
|
||||
if len(e.MissingSubtitles) > 0 {
|
||||
episodeStats.missing += len(e.MissingSubtitles)
|
||||
if len(e.Subtitles) == 0 {
|
||||
episodeStats.wanted++
|
||||
}
|
||||
}
|
||||
|
||||
if len(e.Subtitles) > 0 {
|
||||
episodeStats.downloaded += len(e.Subtitles)
|
||||
for _, subtitle := range e.Subtitles {
|
||||
if subtitle.Language != "" {
|
||||
episodeStats.languages[subtitle.Language]++
|
||||
} else if subtitle.Code2 != "" {
|
||||
episodeStats.languages[subtitle.Code2]++
|
||||
} else if subtitle.Code3 != "" {
|
||||
episodeStats.languages[subtitle.Code3]++
|
||||
}
|
||||
|
||||
if subtitle.Size != 0 {
|
||||
episodeStats.fileSize += subtitle.Size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
log.Errorw("Error getting episodes subtitles", "error", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
history := model.BazarrHistory{}
|
||||
if err := c.DoRequest("episodes/history", &history); err != nil {
|
||||
log.Errorw("Error getting episodes history",
|
||||
"error", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return nil
|
||||
}
|
||||
episodeStats.history = history.TotalRecords
|
||||
|
||||
for _, m := range history.Data {
|
||||
if m.Score != "" {
|
||||
episodeStats.scores[m.Score]++
|
||||
}
|
||||
if m.Provider != "" {
|
||||
episodeStats.providers[m.Provider]++
|
||||
}
|
||||
}
|
||||
|
||||
et := time.Since(mseries)
|
||||
log.Debugw("episode completed", "duration", et)
|
||||
|
||||
return episodeStats
|
||||
}
|
||||
|
||||
func (collector *bazarrCollector) CollectMovieStats(ch chan<- prometheus.Metric, c *client.Client) *stats {
|
||||
log := zap.S().With("collector", "bazarr")
|
||||
mseries := time.Now()
|
||||
movieStats := new(stats)
|
||||
movieStats.languages = make(map[string]int)
|
||||
movieStats.scores = make(map[string]int)
|
||||
movieStats.providers = make(map[string]int)
|
||||
|
||||
movies := model.BazarrMovies{}
|
||||
if err := c.DoRequest("movies", &movies); err != nil {
|
||||
log.Errorw("Error getting subtitles", "error", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, m := range movies.Data {
|
||||
if !m.Monitored {
|
||||
movieStats.unmonitored++
|
||||
} else {
|
||||
movieStats.monitored++
|
||||
}
|
||||
|
||||
if len(m.MissingSubtitles) > 0 {
|
||||
movieStats.missing += len(m.MissingSubtitles)
|
||||
if len(m.Subtitles) == 0 {
|
||||
movieStats.wanted++
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.Subtitles) > 0 {
|
||||
movieStats.downloaded += len(m.Subtitles)
|
||||
for _, subtitle := range m.Subtitles {
|
||||
if subtitle.Language != "" {
|
||||
movieStats.languages[subtitle.Language]++
|
||||
} else if subtitle.Code2 != "" {
|
||||
movieStats.languages[subtitle.Code2]++
|
||||
} else if subtitle.Code3 != "" {
|
||||
movieStats.languages[subtitle.Code3]++
|
||||
}
|
||||
|
||||
if subtitle.Size != 0 {
|
||||
movieStats.fileSize += subtitle.Size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bazarr keeps separate histories for TV vs Movies, and therefore cannot leverage the shared HistoryCollector.
|
||||
history := model.BazarrHistory{}
|
||||
if err := c.DoRequest("movies/history", &history); err != nil {
|
||||
log.Errorw("Error getting movies history",
|
||||
"error", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
movieStats.history = history.TotalRecords
|
||||
|
||||
for _, m := range history.Data {
|
||||
if m.Score != "" {
|
||||
movieStats.scores[m.Score]++
|
||||
}
|
||||
if m.Provider != "" {
|
||||
movieStats.providers[m.Provider]++
|
||||
}
|
||||
}
|
||||
|
||||
mt := time.Since(mseries)
|
||||
log.Debugw("Movies completed", "duration", mt)
|
||||
|
||||
return movieStats
|
||||
}
|
||||
|
||||
func (collector *bazarrCollector) SystemMetrics(ch chan<- prometheus.Metric, c *client.Client) {
|
||||
log := zap.S().With("collector", "bazarr")
|
||||
|
||||
health := model.BazarrHealth{}
|
||||
if err := c.DoRequest("system/health", &health); err != nil {
|
||||
log.Errorw("Error getting movies history",
|
||||
"error", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Group metrics by issue & object
|
||||
// Bazarr uses it's own health message format, and therefore cannot leverage the shared HealthCollector
|
||||
if len(health.Data) > 0 {
|
||||
for _, s := range health.Data {
|
||||
ch <- prometheus.MustNewConstMetric(collector.systemHealthMetric, prometheus.GaugeValue, float64(1),
|
||||
s.Issue, s.Object,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ch <- prometheus.MustNewConstMetric(collector.systemHealthMetric, prometheus.GaugeValue, float64(0), "", "")
|
||||
}
|
||||
|
||||
// Bazarr uses it's own status format. and therefore cannot leverage the shared StatusCollector
|
||||
systemStatus := model.BazarrStatus{}
|
||||
if err := c.DoRequest("system/status", &systemStatus); err != nil {
|
||||
ch <- prometheus.MustNewConstMetric(collector.systemStatusMetric, prometheus.GaugeValue, float64(0.0))
|
||||
log.Errorw("Error getting system status", "error", err)
|
||||
} else {
|
||||
ch <- prometheus.MustNewConstMetric(collector.systemStatusMetric, prometheus.GaugeValue, float64(1.0))
|
||||
}
|
||||
|
||||
}
|
||||
225
internal/arr/collector/bazarr_test.go
Normal file
225
internal/arr/collector/bazarr_test.go
Normal file
@ -0,0 +1,225 @@
|
||||
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"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const bazarr_test_fixtures_path = "../test_fixtures/bazarr/"
|
||||
|
||||
func newTestBazarrServer(t *testing.T, fn func(http.ResponseWriter, *http.Request)) (*httptest.Server, error) {
|
||||
return test_util.NewTestServer(t, bazarr_test_fixtures_path, fn)
|
||||
}
|
||||
|
||||
func TestBazarrCollect(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ts, err := newTestBazarrServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Contains(r.URL.Path, "/api/")
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
defer ts.Close()
|
||||
|
||||
config := &config.ArrConfig{
|
||||
URL: ts.URL,
|
||||
App: "bazarr",
|
||||
ApiKey: test_util.API_KEY,
|
||||
Bazarr: config.BazarrConfig{
|
||||
SeriesBatchSize: 1,
|
||||
SeriesBatchConcurrency: 1,
|
||||
},
|
||||
}
|
||||
collector := NewBazarrCollector(config)
|
||||
require.NoError(err)
|
||||
|
||||
b, err := os.ReadFile(bazarr_test_fixtures_path + "expected_metrics.txt")
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.ReplaceAll(string(b), "SOMEURL", ts.URL)
|
||||
f := strings.NewReader(expected)
|
||||
collections := []string{
|
||||
"bazarr_episode_subtitles_downloaded_total",
|
||||
"bazarr_episode_subtitles_filesize_total",
|
||||
"bazarr_episode_subtitles_history_total",
|
||||
"bazarr_episode_subtitles_missing_total",
|
||||
"bazarr_episode_subtitles_monitored_total",
|
||||
"bazarr_episode_subtitles_unmonitored_total",
|
||||
"bazarr_episode_subtitles_wanted_total",
|
||||
"bazarr_movie_subtitles_downloaded_total",
|
||||
"bazarr_movie_subtitles_filesize_total",
|
||||
"bazarr_movie_subtitles_history_total",
|
||||
"bazarr_movie_subtitles_missing_total",
|
||||
"bazarr_movie_subtitles_monitored_total",
|
||||
"bazarr_movie_subtitles_unmonitored_total",
|
||||
"bazarr_movie_subtitles_wanted_total",
|
||||
"bazarr_scrape_duration_seconds",
|
||||
"bazarr_scrape_requests_total",
|
||||
"bazarr_subtitles_downloaded_total",
|
||||
"bazarr_subtitles_filesize_total",
|
||||
"bazarr_subtitles_history_total",
|
||||
"bazarr_subtitles_language_total",
|
||||
"bazarr_subtitles_missing_total",
|
||||
"bazarr_subtitles_monitored_total",
|
||||
"bazarr_subtitles_provider_total",
|
||||
"bazarr_subtitles_score_total",
|
||||
"bazarr_subtitles_unmonitored_total",
|
||||
"bazarr_subtitles_wanted_total",
|
||||
"bazarr_system_health_issues",
|
||||
"bazarr_system_status",
|
||||
"exportarr_app_info",
|
||||
}
|
||||
|
||||
require.NotPanics(func() {
|
||||
err = testutil.CollectAndCompare(collector, f,
|
||||
collections...,
|
||||
)
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
// TODO: can this become more magic?
|
||||
totalLanguages := 1
|
||||
totalScores := 15
|
||||
totalProviders := 3
|
||||
|
||||
require.GreaterOrEqual(len(collections)+totalLanguages+totalProviders+totalScores, testutil.CollectAndCount(collector))
|
||||
}
|
||||
|
||||
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/")
|
||||
|
||||
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)
|
||||
|
||||
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")
|
||||
require.NoError(err)
|
||||
_, err = w.Write(json)
|
||||
require.NoError(err)
|
||||
} else if slices.Contains(seriesIDs, "946") && slices.Contains(seriesIDs, "947") {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json, err := os.ReadFile(bazarr_test_fixtures_path + "concurrency/episodes946_947.json")
|
||||
require.NoError(err)
|
||||
_, err = w.Write(json)
|
||||
require.NoError(err)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
default:
|
||||
ts2, err := newTestBazarrServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Contains(r.URL.Path, "/api/")
|
||||
})
|
||||
require.NoError(err)
|
||||
ts2.Config.Handler.ServeHTTP(w, r)
|
||||
}
|
||||
}))
|
||||
|
||||
defer ts.Close()
|
||||
|
||||
config := &config.ArrConfig{
|
||||
URL: ts.URL,
|
||||
App: "bazarr",
|
||||
ApiKey: test_util.API_KEY,
|
||||
EnableAdditionalMetrics: true,
|
||||
Bazarr: config.BazarrConfig{
|
||||
SeriesBatchSize: 2,
|
||||
SeriesBatchConcurrency: 2,
|
||||
},
|
||||
}
|
||||
collector := NewBazarrCollector(config)
|
||||
|
||||
b, err := os.ReadFile(bazarr_test_fixtures_path + "concurrency/expected_metrics.txt")
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.ReplaceAll(string(b), "SOMEURL", ts.URL)
|
||||
f := strings.NewReader(expected)
|
||||
collections := []string{
|
||||
"bazarr_episode_subtitles_downloaded_total",
|
||||
"bazarr_episode_subtitles_filesize_total",
|
||||
"bazarr_episode_subtitles_history_total",
|
||||
"bazarr_episode_subtitles_missing_total",
|
||||
"bazarr_episode_subtitles_monitored_total",
|
||||
"bazarr_episode_subtitles_unmonitored_total",
|
||||
"bazarr_episode_subtitles_wanted_total",
|
||||
"bazarr_movie_subtitles_downloaded_total",
|
||||
"bazarr_movie_subtitles_filesize_total",
|
||||
"bazarr_movie_subtitles_history_total",
|
||||
"bazarr_movie_subtitles_missing_total",
|
||||
"bazarr_movie_subtitles_monitored_total",
|
||||
"bazarr_movie_subtitles_unmonitored_total",
|
||||
"bazarr_movie_subtitles_wanted_total",
|
||||
"bazarr_scrape_duration_seconds",
|
||||
"bazarr_scrape_requests_total",
|
||||
"bazarr_subtitles_downloaded_total",
|
||||
"bazarr_subtitles_filesize_total",
|
||||
"bazarr_subtitles_history_total",
|
||||
"bazarr_subtitles_language_total",
|
||||
"bazarr_subtitles_missing_total",
|
||||
"bazarr_subtitles_monitored_total",
|
||||
"bazarr_subtitles_provider_total",
|
||||
"bazarr_subtitles_score_total",
|
||||
"bazarr_subtitles_unmonitored_total",
|
||||
"bazarr_subtitles_wanted_total",
|
||||
"bazarr_system_health_issues",
|
||||
"bazarr_system_status",
|
||||
"exportarr_app_info",
|
||||
}
|
||||
|
||||
require.NotPanics(func() {
|
||||
err = testutil.CollectAndCompare(collector, f,
|
||||
collections...,
|
||||
)
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
// TODO: can this become more magic?
|
||||
totalLanguages := 1
|
||||
totalScores := 15
|
||||
totalProviders := 3
|
||||
|
||||
require.GreaterOrEqual(len(collections)+totalLanguages+totalProviders+totalScores, testutil.CollectAndCount(collector))
|
||||
}
|
||||
|
||||
func TestBazarrCollect_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 := NewBazarrCollector(config)
|
||||
|
||||
f := strings.NewReader("")
|
||||
|
||||
require.NotPanics(func() {
|
||||
err := testutil.CollectAndCompare(collector, f)
|
||||
require.Error(err)
|
||||
}, "Collecting metrics should not panic on failure")
|
||||
}
|
||||
107
internal/arr/collector/health_test.go
Normal file
107
internal/arr/collector/health_test.go
Normal file
@ -0,0 +1,107 @@
|
||||
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"
|
||||
)
|
||||
|
||||
func TestSystemHealthCollect(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
config *config.ArrConfig
|
||||
path string
|
||||
}{
|
||||
{
|
||||
name: "radarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "radarr",
|
||||
ApiVersion: "v3",
|
||||
},
|
||||
path: "/api/v3/health",
|
||||
},
|
||||
{
|
||||
name: "sonarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "sonarr",
|
||||
ApiVersion: "v3",
|
||||
},
|
||||
path: "/api/v3/health",
|
||||
},
|
||||
{
|
||||
name: "lidarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "lidarr",
|
||||
ApiVersion: "v1",
|
||||
},
|
||||
path: "/api/v1/health",
|
||||
},
|
||||
{
|
||||
name: "readarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "readarr",
|
||||
ApiVersion: "v1",
|
||||
},
|
||||
path: "/api/v1/health",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ts, err := test_util.NewTestSharedServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Contains(r.URL.Path, tt.path)
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
defer ts.Close()
|
||||
|
||||
tt.config.URL = ts.URL
|
||||
tt.config.ApiKey = test_util.API_KEY
|
||||
|
||||
collector := NewSystemHealthCollector(tt.config)
|
||||
|
||||
b, err := os.ReadFile(test_util.COMMON_FIXTURES_PATH + "expected_health_metrics.txt")
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.ReplaceAll(string(b), "SOMEURL", ts.URL)
|
||||
expected = strings.ReplaceAll(expected, "APP", tt.config.App)
|
||||
|
||||
f := strings.NewReader(expected)
|
||||
|
||||
require.NotPanics(func() {
|
||||
err = testutil.CollectAndCompare(collector, f)
|
||||
})
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemHealthCollect_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 := NewSystemHealthCollector(config)
|
||||
|
||||
f := strings.NewReader("")
|
||||
|
||||
require.NotPanics(func() {
|
||||
err := testutil.CollectAndCompare(collector, f)
|
||||
require.Error(err)
|
||||
}, "Collecting metrics should not panic on failure")
|
||||
}
|
||||
107
internal/arr/collector/history_test.go
Normal file
107
internal/arr/collector/history_test.go
Normal file
@ -0,0 +1,107 @@
|
||||
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"
|
||||
)
|
||||
|
||||
func TestHistoryCollect(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
config *config.ArrConfig
|
||||
path string
|
||||
}{
|
||||
{
|
||||
name: "radarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "radarr",
|
||||
ApiVersion: "v3",
|
||||
},
|
||||
path: "/api/v3/history",
|
||||
},
|
||||
{
|
||||
name: "sonarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "sonarr",
|
||||
ApiVersion: "v3",
|
||||
},
|
||||
path: "/api/v3/history",
|
||||
},
|
||||
{
|
||||
name: "lidarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "lidarr",
|
||||
ApiVersion: "v1",
|
||||
},
|
||||
path: "/api/v1/history",
|
||||
},
|
||||
{
|
||||
name: "readarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "readarr",
|
||||
ApiVersion: "v1",
|
||||
},
|
||||
path: "/api/v1/history",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ts, err := test_util.NewTestSharedServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Contains(r.URL.Path, tt.path)
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
defer ts.Close()
|
||||
|
||||
tt.config.URL = ts.URL
|
||||
tt.config.ApiKey = test_util.API_KEY
|
||||
|
||||
collector := NewHistoryCollector(tt.config)
|
||||
|
||||
b, err := os.ReadFile(test_util.COMMON_FIXTURES_PATH + "expected_history_metrics.txt")
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.ReplaceAll(string(b), "SOMEURL", ts.URL)
|
||||
expected = strings.ReplaceAll(expected, "APP", tt.config.App)
|
||||
|
||||
f := strings.NewReader(expected)
|
||||
|
||||
require.NotPanics(func() {
|
||||
err = testutil.CollectAndCompare(collector, f)
|
||||
})
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHistoryCollect_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 := NewHistoryCollector(config)
|
||||
|
||||
f := strings.NewReader("")
|
||||
|
||||
require.NotPanics(func() {
|
||||
err := testutil.CollectAndCompare(collector, f)
|
||||
require.Error(err)
|
||||
}, "Collecting metrics should not panic on failure")
|
||||
}
|
||||
@ -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{}
|
||||
@ -168,7 +170,10 @@ func (collector *lidarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
|
||||
if collector.config.EnableAdditionalMetrics {
|
||||
songFile := model.SongFile{}
|
||||
params := map[string]string{"artistid": fmt.Sprintf("%d", s.Id)}
|
||||
|
||||
params := client.QueryParams{}
|
||||
params.Add("artistid", fmt.Sprintf("%d", s.Id))
|
||||
|
||||
if err := c.DoRequest("trackfile", &songFile, params); err != nil {
|
||||
log.Errorw("Error getting trackfile", "error", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
@ -181,7 +186,6 @@ func (collector *lidarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
}
|
||||
|
||||
album := model.Album{}
|
||||
params = map[string]string{"artistid": fmt.Sprintf("%d", s.Id)}
|
||||
if err := c.DoRequest("album", &album, params); err != nil {
|
||||
log.Errorw("Error getting album", "error", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
@ -195,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -224,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")
|
||||
}
|
||||
@ -317,10 +317,11 @@ func (collector *prowlarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
stats := model.IndexerStatResponse{}
|
||||
startDate := collector.lastStatUpdate.In(time.UTC)
|
||||
endDate := time.Now().In(time.UTC)
|
||||
params := map[string]string{
|
||||
"startDate": startDate.Format(time.RFC3339),
|
||||
"endDate": endDate.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
params := client.QueryParams{}
|
||||
params.Add("startDate", startDate.Format(time.RFC3339))
|
||||
params.Add("endDate", endDate.Format(time.RFC3339))
|
||||
|
||||
if err := c.DoRequest("indexerstats", &stats, params); err != nil {
|
||||
log.Errorf("Error getting indexer stats: %s", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
|
||||
@ -48,12 +48,14 @@ func (collector *queueCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
return
|
||||
}
|
||||
|
||||
params := map[string]string{"page": "1"}
|
||||
params := client.QueryParams{}
|
||||
params.Add("page", "1")
|
||||
if collector.config.EnableUnknownQueueItems {
|
||||
if collector.config.App == "sonarr" {
|
||||
params["includeUnknownSeriesItems"] = "true"
|
||||
} else if collector.config.App == "radarr" {
|
||||
params["includeUnknownMovieItems"] = "true"
|
||||
switch collector.config.App {
|
||||
case "sonarr":
|
||||
params.Add("includeUnknownSeriesItems", "true")
|
||||
case "radarr":
|
||||
params.Add("includeUnknownMovieItems", "true")
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,7 +73,7 @@ func (collector *queueCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
queueStatusAll = append(queueStatusAll, queue.Records...)
|
||||
if totalPages > 1 {
|
||||
for page := 2; page <= totalPages; page++ {
|
||||
params["page"] = fmt.Sprintf("%d", page)
|
||||
params.Set("page", fmt.Sprintf("%d", page))
|
||||
if err := c.DoRequest("queue", &queue, params); err != nil {
|
||||
log.Errorw("Error getting queue page",
|
||||
"page", page,
|
||||
|
||||
107
internal/arr/collector/queue_test.go
Normal file
107
internal/arr/collector/queue_test.go
Normal file
@ -0,0 +1,107 @@
|
||||
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"
|
||||
)
|
||||
|
||||
func TestQueueCollect(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
config *config.ArrConfig
|
||||
path string
|
||||
}{
|
||||
{
|
||||
name: "radarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "radarr",
|
||||
ApiVersion: "v3",
|
||||
},
|
||||
path: "/api/v3/queue",
|
||||
},
|
||||
{
|
||||
name: "sonarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "sonarr",
|
||||
ApiVersion: "v3",
|
||||
},
|
||||
path: "/api/v3/queue",
|
||||
},
|
||||
{
|
||||
name: "lidarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "lidarr",
|
||||
ApiVersion: "v1",
|
||||
},
|
||||
path: "/api/v1/queue",
|
||||
},
|
||||
{
|
||||
name: "readarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "readarr",
|
||||
ApiVersion: "v1",
|
||||
},
|
||||
path: "/api/v1/queue",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ts, err := test_util.NewTestSharedServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Contains(r.URL.Path, tt.path)
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
defer ts.Close()
|
||||
|
||||
tt.config.URL = ts.URL
|
||||
tt.config.ApiKey = test_util.API_KEY
|
||||
|
||||
collector := NewQueueCollector(tt.config)
|
||||
|
||||
b, err := os.ReadFile(test_util.COMMON_FIXTURES_PATH + "expected_queue_metrics.txt")
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.ReplaceAll(string(b), "SOMEURL", ts.URL)
|
||||
expected = strings.ReplaceAll(expected, "APP", tt.config.App)
|
||||
|
||||
f := strings.NewReader(expected)
|
||||
|
||||
require.NotPanics(func() {
|
||||
err = testutil.CollectAndCompare(collector, f)
|
||||
})
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueueCollect_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 := NewQueueCollector(config)
|
||||
|
||||
f := strings.NewReader("")
|
||||
|
||||
require.NotPanics(func() {
|
||||
err := testutil.CollectAndCompare(collector, f)
|
||||
require.Error(err)
|
||||
}, "Collecting metrics should not panic on failure")
|
||||
}
|
||||
@ -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,8 +112,10 @@ 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
|
||||
}
|
||||
|
||||
func (collector *radarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
@ -117,21 +128,26 @@ func (collector *radarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
}
|
||||
var fileSize int64
|
||||
var (
|
||||
editions = 0
|
||||
editions = 0
|
||||
downloaded = 0
|
||||
monitored = 0
|
||||
unmonitored = 0
|
||||
missing = 0
|
||||
wanted = 0
|
||||
qualities = map[string]int{}
|
||||
tags = []struct {
|
||||
Label string
|
||||
tags = []struct {
|
||||
Label string
|
||||
Movies int
|
||||
}{}
|
||||
qualityWeights = map[string]string{}
|
||||
)
|
||||
|
||||
movies := model.Movie{}
|
||||
params := client.QueryParams{}
|
||||
params.Add("excludeLocalCovers", "true")
|
||||
|
||||
// https://radarr.video/docs/api/#/Movie/get_api_v3_movie
|
||||
if err := c.DoRequest("movie", &movies); err != nil {
|
||||
if err := c.DoRequest("movie", &movies, params); err != nil {
|
||||
log.Errorw("Error getting movies", "error", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return
|
||||
@ -166,22 +182,44 @@ func (collector *radarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
tagObjects := model.TagMovies{}
|
||||
// https://radarr.video/docs/api/#/TagDetails/get_api_v3_tag_detail
|
||||
if err := c.DoRequest("tag/detail", &tagObjects); err != nil {
|
||||
log.Errorw("Error getting Tags", "error", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return
|
||||
}
|
||||
log.Errorw("Error getting Tags", "error", err)
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return
|
||||
}
|
||||
for _, s := range tagObjects {
|
||||
tag := struct {
|
||||
Label string
|
||||
Label string
|
||||
Movies int
|
||||
}{
|
||||
Label: s.Label,
|
||||
Label: s.Label,
|
||||
Movies: len(s.MovieIds),
|
||||
}
|
||||
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)))
|
||||
@ -190,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],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
72
internal/arr/collector/radarr_test.go
Normal file
72
internal/arr/collector/radarr_test.go
Normal file
@ -0,0 +1,72 @@
|
||||
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 radarr_test_fixtures_path = "../test_fixtures/radarr/"
|
||||
|
||||
func newTestRadarrServer(t *testing.T, fn func(http.ResponseWriter, *http.Request)) (*httptest.Server, error) {
|
||||
return test_util.NewTestServer(t, radarr_test_fixtures_path, fn)
|
||||
}
|
||||
|
||||
func TestRadarrCollect(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ts, err := newTestRadarrServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Contains(r.URL.Path, "/api/")
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
defer ts.Close()
|
||||
|
||||
config := &config.ArrConfig{
|
||||
URL: ts.URL,
|
||||
App: "radarr",
|
||||
ApiKey: test_util.API_KEY,
|
||||
ApiVersion: "v3",
|
||||
}
|
||||
collector := NewRadarrCollector(config)
|
||||
require.NoError(err)
|
||||
|
||||
b, err := os.ReadFile(radarr_test_fixtures_path + "expected_metrics.txt")
|
||||
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 TestRadarrCollect_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")
|
||||
}
|
||||
@ -111,6 +111,7 @@ func (c *readarrCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- c.authorUnmonitoredMetric
|
||||
ch <- c.authorFileSizeMetric
|
||||
ch <- c.bookMetric
|
||||
ch <- c.bookGrabbedMetric
|
||||
ch <- c.bookDownloadedMetric
|
||||
ch <- c.bookMonitoredMetric
|
||||
ch <- c.bookUnmonitoredMetric
|
||||
|
||||
72
internal/arr/collector/readarr_test.go
Normal file
72
internal/arr/collector/readarr_test.go
Normal file
@ -0,0 +1,72 @@
|
||||
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 readarr_test_fixtures_path = "../test_fixtures/readarr/"
|
||||
|
||||
func newTestReadarrServer(t *testing.T, fn func(http.ResponseWriter, *http.Request)) (*httptest.Server, error) {
|
||||
return test_util.NewTestServer(t, readarr_test_fixtures_path, fn)
|
||||
}
|
||||
|
||||
func TestReadarrCollect(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ts, err := newTestReadarrServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Contains(r.URL.Path, "/api/")
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
defer ts.Close()
|
||||
|
||||
config := &config.ArrConfig{
|
||||
URL: ts.URL,
|
||||
App: "radarr",
|
||||
ApiKey: test_util.API_KEY,
|
||||
ApiVersion: "v1",
|
||||
}
|
||||
collector := NewReadarrCollector(config)
|
||||
require.NoError(err)
|
||||
|
||||
b, err := os.ReadFile(readarr_test_fixtures_path + "expected_metrics.txt")
|
||||
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 TestReadarrCollect_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 := NewReadarrCollector(config)
|
||||
|
||||
f := strings.NewReader("")
|
||||
|
||||
require.NotPanics(func() {
|
||||
err := testutil.CollectAndCompare(collector, f)
|
||||
require.Error(err)
|
||||
}, "Collecting metrics should not panic on failure")
|
||||
}
|
||||
107
internal/arr/collector/rootfolder_test.go
Normal file
107
internal/arr/collector/rootfolder_test.go
Normal file
@ -0,0 +1,107 @@
|
||||
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"
|
||||
)
|
||||
|
||||
func TestRootFolderCollect(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
config *config.ArrConfig
|
||||
path string
|
||||
}{
|
||||
{
|
||||
name: "radarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "radarr",
|
||||
ApiVersion: "v3",
|
||||
},
|
||||
path: "/api/v3/rootfolder",
|
||||
},
|
||||
{
|
||||
name: "sonarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "sonarr",
|
||||
ApiVersion: "v3",
|
||||
},
|
||||
path: "/api/v3/rootfolder",
|
||||
},
|
||||
{
|
||||
name: "lidarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "lidarr",
|
||||
ApiVersion: "v1",
|
||||
},
|
||||
path: "/api/v1/rootfolder",
|
||||
},
|
||||
{
|
||||
name: "readarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "readarr",
|
||||
ApiVersion: "v1",
|
||||
},
|
||||
path: "/api/v1/rootfolder",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ts, err := test_util.NewTestSharedServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Contains(r.URL.Path, tt.path)
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
defer ts.Close()
|
||||
|
||||
tt.config.URL = ts.URL
|
||||
tt.config.ApiKey = test_util.API_KEY
|
||||
|
||||
collector := NewRootFolderCollector(tt.config)
|
||||
|
||||
b, err := os.ReadFile(test_util.COMMON_FIXTURES_PATH + "expected_rootfolder_metrics.txt")
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.ReplaceAll(string(b), "SOMEURL", ts.URL)
|
||||
expected = strings.ReplaceAll(expected, "APP", tt.config.App)
|
||||
|
||||
f := strings.NewReader(expected)
|
||||
|
||||
require.NotPanics(func() {
|
||||
err = testutil.CollectAndCompare(collector, f)
|
||||
})
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootFolderCollect_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 := NewRootFolderCollector(config)
|
||||
|
||||
f := strings.NewReader("")
|
||||
|
||||
require.NotPanics(func() {
|
||||
err := testutil.CollectAndCompare(collector, f)
|
||||
require.Error(err)
|
||||
}, "Collecting metrics should not panic on failure")
|
||||
}
|
||||
@ -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{}
|
||||
@ -219,7 +237,10 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
if collector.config.EnableAdditionalMetrics {
|
||||
textra := time.Now()
|
||||
episodeFile := model.EpisodeFile{}
|
||||
params := map[string]string{"seriesId": fmt.Sprintf("%d", s.Id)}
|
||||
|
||||
params := client.QueryParams{}
|
||||
params.Add("seriesId", fmt.Sprintf("%d", s.Id))
|
||||
|
||||
if err := c.DoRequest("episodefile", &episodeFile, params); err != nil {
|
||||
log.Errorw("Error getting episodefile",
|
||||
"error", err)
|
||||
@ -246,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))
|
||||
}
|
||||
@ -257,7 +292,10 @@ func (collector *sonarrCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
}
|
||||
|
||||
episodesMissing := model.Missing{}
|
||||
params := map[string]string{"sortKey": "airDateUtc"}
|
||||
|
||||
params := client.QueryParams{}
|
||||
params.Add("sortKey", "airDateUtc")
|
||||
|
||||
if err := c.DoRequest("wanted/missing", &episodesMissing, params); err != nil {
|
||||
log.Errorw("Error getting missing",
|
||||
"error", err)
|
||||
@ -265,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))
|
||||
@ -277,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))
|
||||
@ -285,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],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
96
internal/arr/collector/sonarr_test.go
Normal file
96
internal/arr/collector/sonarr_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 sonarr_test_fixtures_path = "../test_fixtures/sonarr/"
|
||||
|
||||
func newTestSonarrServer(t *testing.T, fn func(http.ResponseWriter, *http.Request)) (*httptest.Server, error) {
|
||||
return test_util.NewTestServer(t, sonarr_test_fixtures_path, fn)
|
||||
}
|
||||
|
||||
func TestSonarrCollect(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *config.ArrConfig
|
||||
expected_metrics_file string
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
config: &config.ArrConfig{
|
||||
App: "sonarr",
|
||||
ApiVersion: "v3",
|
||||
},
|
||||
expected_metrics_file: "expected_metrics.txt",
|
||||
},
|
||||
{
|
||||
name: "additional_metrics",
|
||||
config: &config.ArrConfig{
|
||||
App: "sonarr",
|
||||
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 := newTestSonarrServer(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 := NewSonarrCollector(tt.config)
|
||||
require.NoError(err)
|
||||
|
||||
b, err := os.ReadFile(sonarr_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 TestSonarrCollect_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")
|
||||
}
|
||||
@ -12,7 +12,6 @@ import (
|
||||
|
||||
type systemStatusCollector struct {
|
||||
config *config.ArrConfig // App configuration
|
||||
configFile *model.Config // *arr configuration from config.xml
|
||||
systemStatus *prometheus.Desc // Total number of system statuses
|
||||
errorMetric *prometheus.Desc // Error Description for use with InvalidMetric
|
||||
}
|
||||
|
||||
107
internal/arr/collector/status_test.go
Normal file
107
internal/arr/collector/status_test.go
Normal file
@ -0,0 +1,107 @@
|
||||
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"
|
||||
)
|
||||
|
||||
func TestStatusCollect(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
config *config.ArrConfig
|
||||
path string
|
||||
}{
|
||||
{
|
||||
name: "radarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "radarr",
|
||||
ApiVersion: "v3",
|
||||
},
|
||||
path: "/api/v3/system/status",
|
||||
},
|
||||
{
|
||||
name: "sonarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "sonarr",
|
||||
ApiVersion: "v3",
|
||||
},
|
||||
path: "/api/v3/system/status",
|
||||
},
|
||||
{
|
||||
name: "lidarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "lidarr",
|
||||
ApiVersion: "v1",
|
||||
},
|
||||
path: "/api/v1/system/status",
|
||||
},
|
||||
{
|
||||
name: "readarr",
|
||||
config: &config.ArrConfig{
|
||||
App: "readarr",
|
||||
ApiVersion: "v1",
|
||||
},
|
||||
path: "/api/v1/system/status",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ts, err := test_util.NewTestSharedServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Contains(r.URL.Path, tt.path)
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
defer ts.Close()
|
||||
|
||||
tt.config.URL = ts.URL
|
||||
tt.config.ApiKey = test_util.API_KEY
|
||||
|
||||
collector := NewSystemStatusCollector(tt.config)
|
||||
|
||||
b, err := os.ReadFile(test_util.COMMON_FIXTURES_PATH + "expected_status_metrics.txt")
|
||||
require.NoError(err)
|
||||
|
||||
expected := strings.ReplaceAll(string(b), "SOMEURL", ts.URL)
|
||||
expected = strings.ReplaceAll(expected, "APP", tt.config.App)
|
||||
|
||||
f := strings.NewReader(expected)
|
||||
|
||||
require.NotPanics(func() {
|
||||
err = testutil.CollectAndCompare(collector, f)
|
||||
})
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusCollect_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 := NewSystemStatusCollector(config)
|
||||
|
||||
f := strings.NewReader("")
|
||||
|
||||
require.NotPanics(func() {
|
||||
err := testutil.CollectAndCompare(collector, f)
|
||||
require.Error(err)
|
||||
}, "Collecting metrics should not panic on failure")
|
||||
}
|
||||
@ -32,17 +32,18 @@ func RegisterArrFlags(flags *flag.FlagSet) {
|
||||
|
||||
type ArrConfig struct {
|
||||
App string `koanf:"app"`
|
||||
ApiVersion string `koanf:"api-version" validate:"required|in:v1,v3"`
|
||||
ApiVersion string `koanf:"api-version"`
|
||||
XMLConfig string `koanf:"config"`
|
||||
AuthUsername string `koanf:"auth-username"`
|
||||
AuthPassword string `koanf:"auth-password"`
|
||||
FormAuth bool `koanf:"form-auth"`
|
||||
EnableUnknownQueueItems bool `koanf:"enable-unknown-queue-items"`
|
||||
EnableAdditionalMetrics bool `koanf:"enable-additional-metrics"`
|
||||
URL string `koanf:"url" validate:"required|url"` // stores rendered Arr URL (with api version)
|
||||
ApiKey string `koanf:"api-key" validate:"required|regex:(^[a-z0-9]{32}$)"` // stores the API key
|
||||
DisableSSLVerify bool `koanf:"disable-ssl-verify"` // stores the disable SSL verify flag
|
||||
URL string `koanf:"url" validate:"required|url"` // stores rendered Arr URL (with api version)
|
||||
ApiKey string `koanf:"api-key" validate:"required|regex:(^[a-zA-Z0-9]{20,32}$)"` // stores the API key
|
||||
DisableSSLVerify bool `koanf:"disable-ssl-verify"` // stores the disable SSL verify flag
|
||||
Prowlarr ProwlarrConfig `koanf:"prowlarr"`
|
||||
Bazarr BazarrConfig `koanf:"bazarr"`
|
||||
k *koanf.Koanf
|
||||
}
|
||||
|
||||
@ -73,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 {
|
||||
@ -129,7 +130,7 @@ func (c *ArrConfig) Validate() error {
|
||||
|
||||
func (c ArrConfig) Messages() map[string]string {
|
||||
return validate.MS{
|
||||
"ApiKey.regex": "api-key must be a 32 character hex string",
|
||||
"ApiKey.regex": "api-key must be a 20-32 character alphanumeric string",
|
||||
"LogLevel.ValidateLogLevel": "log-level must be one of: debug, info, warn, error, dpanic, panic, fatal",
|
||||
}
|
||||
}
|
||||
|
||||
@ -209,23 +209,41 @@ func TestValidate(t *testing.T) {
|
||||
},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "good-api-key-32-len",
|
||||
config: &ArrConfig{
|
||||
URL: "http://localhost",
|
||||
ApiKey: "abcdefABCDEF0123456789abcdef0123",
|
||||
ApiVersion: "v3",
|
||||
},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "good-api-key-32-len",
|
||||
config: &ArrConfig{
|
||||
URL: "http://localhost",
|
||||
ApiKey: "abcdefABCDEF01234567",
|
||||
ApiVersion: "v3",
|
||||
},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "bad-api-key",
|
||||
config: &ArrConfig{
|
||||
URL: "http://localhost",
|
||||
ApiKey: "abcdef0123456789abcdef01234567",
|
||||
ApiKey: "abcdef0123456789abc",
|
||||
ApiVersion: "v3",
|
||||
},
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
name: "bad-api-version",
|
||||
name: "no-api-version",
|
||||
config: &ArrConfig{
|
||||
URL: "http://localhost",
|
||||
ApiKey: "abcdef0123456789abcdef0123456789",
|
||||
ApiVersion: "v2",
|
||||
ApiVersion: "",
|
||||
},
|
||||
valid: false,
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "password-needs-username",
|
||||
|
||||
61
internal/arr/config/bazarr.go
Normal file
61
internal/arr/config/bazarr.go
Normal file
@ -0,0 +1,61 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gookit/validate"
|
||||
"github.com/knadh/koanf/providers/posflag"
|
||||
"github.com/knadh/koanf/v2"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type BazarrConfig struct {
|
||||
SeriesBatchSize int `koanf:"series-batch-size"`
|
||||
SeriesBatchConcurrency int `koanf:"series-batch-concurrency"`
|
||||
}
|
||||
|
||||
func RegisterBazarrFlags(flags *flag.FlagSet) {
|
||||
flags.Int("series-batch-size", 300, "Number of Series to retrieve from Bazarr in each API Call")
|
||||
flags.Int("series-batch-concurrency", 10, "Calls to make to Bazarrr Concurrently")
|
||||
}
|
||||
|
||||
func (b BazarrConfig) Validate() error {
|
||||
v := validate.Struct(b)
|
||||
if !v.Validate() {
|
||||
return v.Errors
|
||||
}
|
||||
if b.SeriesBatchSize < 1 {
|
||||
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 nil
|
||||
}
|
||||
|
||||
func (b BazarrConfig) Translate() map[string]string {
|
||||
return validate.MS{
|
||||
"SeriesBatchSize": "series-batch-size",
|
||||
"SeriesBatchConcurrency": "series-batch-concurrency",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ArrConfig) LoadBazarrConfig(flags *flag.FlagSet) error {
|
||||
err := c.k.Load(posflag.Provider(flags, ".", c.k), nil, koanf.WithMergeFunc(func(src, dest map[string]interface{}) error {
|
||||
dest["bazarr"] = src
|
||||
return nil
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Bazarr = BazarrConfig{
|
||||
SeriesBatchSize: 300,
|
||||
SeriesBatchConcurrency: 10,
|
||||
}
|
||||
err = c.k.Unmarshal("bazarr", &c.Bazarr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
|
||||
64
internal/arr/model/bazarr.go
Normal file
64
internal/arr/model/bazarr.go
Normal file
@ -0,0 +1,64 @@
|
||||
package model
|
||||
|
||||
// Subtitle - Stores struct of JSON response
|
||||
// https://github.com/morpheus65535/bazarr/blob/master/bazarr/api/swaggerui.py#L12
|
||||
type Subtitle []struct {
|
||||
Size int64 `json:"file_size"`
|
||||
Language string `json:"name"`
|
||||
Code2 string `json:"code2"`
|
||||
Code3 string `json:"code3"`
|
||||
}
|
||||
|
||||
// Series - Stores struct of JSON response
|
||||
// /api/swagger.json#/definitions/SeriesGetResponse
|
||||
// https://github.com/morpheus65535/bazarr/blob/master/bazarr/api/series/series.py
|
||||
type BazarrSeries struct {
|
||||
Data []struct {
|
||||
Id int `json:"sonarrSeriesId"`
|
||||
Monitored bool `json:"monitored"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type BazarrEpisodes struct {
|
||||
Data []struct {
|
||||
Id int `json:"sonarrEpisodeId"`
|
||||
SeriesId int `json:"sonarrSeriesId"`
|
||||
Monitored bool `json:"monitored"`
|
||||
Subtitles Subtitle `json:"subtitles"`
|
||||
MissingSubtitles Subtitle `json:"missing_subtitles"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// /api/swagger.json#/definitions/MoviesGetResponse
|
||||
// https://github.com/morpheus65535/bazarr/blob/master/bazarr/api/movies/movies.py
|
||||
type BazarrMovies struct {
|
||||
Data []struct {
|
||||
Id int `json:"radarrId"`
|
||||
Monitored bool `json:"monitored"`
|
||||
Subtitles Subtitle `json:"subtitles"`
|
||||
MissingSubtitles Subtitle `json:"missing_subtitles"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type BazarrHistory struct {
|
||||
Data []struct {
|
||||
Score string `json:"score"`
|
||||
Provider string `json:"provider"`
|
||||
} `json:"data"`
|
||||
TotalRecords int `json:"total"`
|
||||
}
|
||||
|
||||
type BazarrHealth struct {
|
||||
Data []struct {
|
||||
Object string `json:"object"`
|
||||
Issue string `json:"issue"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type BazarrStatus struct {
|
||||
Data struct {
|
||||
Version string `json:"bazarr_version"`
|
||||
PythonVersion string `json:"python_version"`
|
||||
StartTime float32 `json:"start_time"`
|
||||
} `json:"data"`
|
||||
}
|
||||
@ -6,9 +6,9 @@ type Movie []struct {
|
||||
HasFile bool `json:"hasFile"`
|
||||
Available bool `json:"isAvailable"`
|
||||
Monitored bool `json:"monitored"`
|
||||
MovieFile struct {
|
||||
Edition string `json:"edition"`
|
||||
Size int64 `json:"size"`
|
||||
MovieFile struct {
|
||||
Edition string `json:"edition"`
|
||||
Size int64 `json:"size"`
|
||||
Quality struct {
|
||||
Quality struct {
|
||||
Name string `json:"name"`
|
||||
@ -19,7 +19,13 @@ type Movie []struct {
|
||||
}
|
||||
|
||||
type TagMovies []struct {
|
||||
ID int `json:"id"`
|
||||
Label string `json:"label"`
|
||||
MovieIds []int `json:"movieIds"`
|
||||
ID int `json:"id"`
|
||||
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"`
|
||||
}
|
||||
|
||||
@ -0,0 +1,72 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"missing_subtitles": [],
|
||||
"monitored": true,
|
||||
"sonarrEpisodeId": 81771,
|
||||
"sonarrSeriesId": 944,
|
||||
"subtitles": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"file_size": 44567
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"missing_subtitles": [],
|
||||
"monitored": true,
|
||||
"sonarrEpisodeId": 81771,
|
||||
"sonarrSeriesId": 944,
|
||||
"subtitles": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"file_size": 40577
|
||||
},
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"file_size": 41234
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"missing_subtitles": [],
|
||||
"monitored": true,
|
||||
"sonarrEpisodeId": 81771,
|
||||
"sonarrSeriesId": 945,
|
||||
"subtitles": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"file_size": 44567
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"missing_subtitles": [],
|
||||
"monitored": true,
|
||||
"sonarrEpisodeId": 81771,
|
||||
"sonarrSeriesId": 945,
|
||||
"subtitles": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"file_size": 40577
|
||||
},
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"file_size": 41234
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"missing_subtitles": [],
|
||||
"monitored": true,
|
||||
"sonarrEpisodeId": 81771,
|
||||
"sonarrSeriesId": 946,
|
||||
"subtitles": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"file_size": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"missing_subtitles": [],
|
||||
"monitored": true,
|
||||
"sonarrEpisodeId": 81771,
|
||||
"sonarrSeriesId": 946,
|
||||
"subtitles": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"file_size": 123
|
||||
},
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"file_size": 41234
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"missing_subtitles": [],
|
||||
"monitored": true,
|
||||
"sonarrEpisodeId": 81771,
|
||||
"sonarrSeriesId": 947,
|
||||
"subtitles": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"file_size": 44567
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"missing_subtitles": [],
|
||||
"monitored": true,
|
||||
"sonarrEpisodeId": 81771,
|
||||
"sonarrSeriesId": 947,
|
||||
"subtitles": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"file_size": 40577
|
||||
},
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"file_size": 41234
|
||||
},
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"file_size": 619234
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
# HELP bazarr_episode_subtitles_downloaded_total Total number of downloaded episode subtitles
|
||||
# TYPE bazarr_episode_subtitles_downloaded_total gauge
|
||||
bazarr_episode_subtitles_downloaded_total{url="SOMEURL"} 13
|
||||
# HELP bazarr_episode_subtitles_filesize_total Total filesize of all episode subtitles
|
||||
# TYPE bazarr_episode_subtitles_filesize_total gauge
|
||||
bazarr_episode_subtitles_filesize_total{url="SOMEURL"} 1.039726e+06
|
||||
# HELP bazarr_episode_subtitles_history_total Total number of history episode subtitles
|
||||
# TYPE bazarr_episode_subtitles_history_total gauge
|
||||
bazarr_episode_subtitles_history_total{url="SOMEURL"} 71633
|
||||
# HELP bazarr_episode_subtitles_missing_total Total number of missing episode subtitles
|
||||
# TYPE bazarr_episode_subtitles_missing_total gauge
|
||||
bazarr_episode_subtitles_missing_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_episode_subtitles_monitored_total Total number of monitored episode subtitles
|
||||
# TYPE bazarr_episode_subtitles_monitored_total gauge
|
||||
bazarr_episode_subtitles_monitored_total{url="SOMEURL"} 8
|
||||
# HELP bazarr_episode_subtitles_unmonitored_total Total number of unmonitored episode subtitles
|
||||
# TYPE bazarr_episode_subtitles_unmonitored_total gauge
|
||||
bazarr_episode_subtitles_unmonitored_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_episode_subtitles_wanted_total Total number of wanted episode subtitles
|
||||
# TYPE bazarr_episode_subtitles_wanted_total gauge
|
||||
bazarr_episode_subtitles_wanted_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_movie_subtitles_downloaded_total Total number of downloaded movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_downloaded_total gauge
|
||||
bazarr_movie_subtitles_downloaded_total{url="SOMEURL"} 10
|
||||
# HELP bazarr_movie_subtitles_filesize_total Total filesize of all movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_filesize_total gauge
|
||||
bazarr_movie_subtitles_filesize_total{url="SOMEURL"} 785627
|
||||
# HELP bazarr_movie_subtitles_history_total Total number of history movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_history_total gauge
|
||||
bazarr_movie_subtitles_history_total{url="SOMEURL"} 3511
|
||||
# HELP bazarr_movie_subtitles_missing_total Total number of missing movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_missing_total gauge
|
||||
bazarr_movie_subtitles_missing_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_movie_subtitles_monitored_total Total number of monitored movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_monitored_total gauge
|
||||
bazarr_movie_subtitles_monitored_total{url="SOMEURL"} 10
|
||||
# HELP bazarr_movie_subtitles_unmonitored_total Total number of unmonitored movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_unmonitored_total gauge
|
||||
bazarr_movie_subtitles_unmonitored_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_movie_subtitles_wanted_total Total number of wanted movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_wanted_total gauge
|
||||
bazarr_movie_subtitles_wanted_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_subtitles_downloaded_total Total number of downloaded subtitles
|
||||
# TYPE bazarr_subtitles_downloaded_total gauge
|
||||
bazarr_subtitles_downloaded_total{url="SOMEURL"} 23
|
||||
# HELP bazarr_subtitles_filesize_total Total filesize of all subtitles
|
||||
# TYPE bazarr_subtitles_filesize_total gauge
|
||||
bazarr_subtitles_filesize_total{url="SOMEURL"} 1.825353e+06
|
||||
# HELP bazarr_subtitles_history_total Total number of history subtitles
|
||||
# TYPE bazarr_subtitles_history_total gauge
|
||||
bazarr_subtitles_history_total{url="SOMEURL"} 75144
|
||||
# HELP bazarr_subtitles_language_total Total number of downloaded subtitles by language
|
||||
# TYPE bazarr_subtitles_language_total gauge
|
||||
bazarr_subtitles_language_total{language="English",url="SOMEURL"} 23
|
||||
# HELP bazarr_subtitles_missing_total Total number of missing subtitles
|
||||
# TYPE bazarr_subtitles_missing_total gauge
|
||||
bazarr_subtitles_missing_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_subtitles_monitored_total Total number of monitored subtitles
|
||||
# TYPE bazarr_subtitles_monitored_total gauge
|
||||
bazarr_subtitles_monitored_total{url="SOMEURL"} 18
|
||||
# HELP bazarr_subtitles_provider_total Total number of downloaded subtitles by provider
|
||||
# TYPE bazarr_subtitles_provider_total gauge
|
||||
bazarr_subtitles_provider_total{provider="opensubtitlescom",url="SOMEURL"} 12
|
||||
bazarr_subtitles_provider_total{provider="podnapisi",url="SOMEURL"} 2
|
||||
bazarr_subtitles_provider_total{provider="yifysubtitles",url="SOMEURL"} 6
|
||||
# HELP bazarr_subtitles_score_total Total number of downloaded subtitles by score
|
||||
# TYPE bazarr_subtitles_score_total gauge
|
||||
bazarr_subtitles_score_total{score="100.0%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="75.0%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="77.5%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="81.67%",url="SOMEURL"} 2
|
||||
bazarr_subtitles_score_total{score="83.33%",url="SOMEURL"} 2
|
||||
bazarr_subtitles_score_total{score="85.0%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="86.67%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="87.5%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="91.67%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="91.94%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="93.89%",url="SOMEURL"} 3
|
||||
bazarr_subtitles_score_total{score="94.17%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="94.44%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="97.5%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="99.17%",url="SOMEURL"} 2
|
||||
# HELP bazarr_subtitles_unmonitored_total Total number of unmonitored subtitles
|
||||
# TYPE bazarr_subtitles_unmonitored_total gauge
|
||||
bazarr_subtitles_unmonitored_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_subtitles_wanted_total Total number of wanted subtitles
|
||||
# TYPE bazarr_subtitles_wanted_total gauge
|
||||
bazarr_subtitles_wanted_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_system_health_issues Total number of health issues by object and issue
|
||||
# TYPE bazarr_system_health_issues gauge
|
||||
bazarr_system_health_issues{issue="some/path",object="incorrectly configured!!!",url="SOMEURL"} 1
|
||||
# HELP bazarr_system_status System Status
|
||||
# TYPE bazarr_system_status gauge
|
||||
bazarr_system_status{url="SOMEURL"} 1
|
||||
20
internal/arr/test_fixtures/bazarr/concurrency/series.json
Normal file
20
internal/arr/test_fixtures/bazarr/concurrency/series.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"monitored": true,
|
||||
"sonarrSeriesId": 944
|
||||
},
|
||||
{
|
||||
"monitored": true,
|
||||
"sonarrSeriesId": 945
|
||||
},
|
||||
{
|
||||
"monitored": true,
|
||||
"sonarrSeriesId": 946
|
||||
},
|
||||
{
|
||||
"monitored": true,
|
||||
"sonarrSeriesId": 947
|
||||
}
|
||||
]
|
||||
}
|
||||
2904
internal/arr/test_fixtures/bazarr/episodes.json
Normal file
2904
internal/arr/test_fixtures/bazarr/episodes.json
Normal file
File diff suppressed because it is too large
Load Diff
439
internal/arr/test_fixtures/bazarr/episodes_history.json
Normal file
439
internal/arr/test_fixtures/bazarr/episodes_history.json
Normal file
@ -0,0 +1,439 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"seriesTitle": "Secrets of the Dead",
|
||||
"monitored": false,
|
||||
"episode_number": "13x2",
|
||||
"episodeTitle": "The Lost Diary of Dr. Livingstone",
|
||||
"timestamp": "3 days ago",
|
||||
"subs_id": "7919065",
|
||||
"description": "English HI subtitles downloaded from opensubtitlescom with a score of 94.17%.",
|
||||
"sonarrSeriesId": 203,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": true
|
||||
},
|
||||
"score": "94.17%",
|
||||
"tags": [
|
||||
"alsotorrent"
|
||||
],
|
||||
"action": 1,
|
||||
"subtitles_path": "/tv/Documentaires/tv shows/Secrets of the Dead/Season 13/Secrets.of.the.Dead.s13e02.en.srt",
|
||||
"sonarrEpisodeId": 12907,
|
||||
"provider": "opensubtitlescom",
|
||||
"upgradable": true,
|
||||
"parsed_timestamp": "09/22/23 17:50:02",
|
||||
"blacklisted": false,
|
||||
"matches": [
|
||||
"title",
|
||||
"season",
|
||||
"year",
|
||||
"edition",
|
||||
"source",
|
||||
"streaming_service",
|
||||
"resolution",
|
||||
"series_imdb_id",
|
||||
"series",
|
||||
"other",
|
||||
"episode"
|
||||
],
|
||||
"dont_matches": [
|
||||
"hearing_impaired",
|
||||
"audio_codec",
|
||||
"video_codec",
|
||||
"hash",
|
||||
"release_group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"seriesTitle": "Nature",
|
||||
"monitored": false,
|
||||
"episode_number": "33x9",
|
||||
"episodeTitle": "Penguin Post Office",
|
||||
"timestamp": "3 days ago",
|
||||
"subs_id": "7959695",
|
||||
"description": "English subtitles downloaded from opensubtitlescom with a score of 91.94%.",
|
||||
"sonarrSeriesId": 175,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": false
|
||||
},
|
||||
"score": "91.94%",
|
||||
"tags": [],
|
||||
"action": 1,
|
||||
"subtitles_path": "/tv/Documentaires/tv shows/Nature/Season 33/Nature.s33e09.en.srt",
|
||||
"sonarrEpisodeId": 11526,
|
||||
"provider": "opensubtitlescom",
|
||||
"upgradable": true,
|
||||
"parsed_timestamp": "09/22/23 17:48:26",
|
||||
"blacklisted": false,
|
||||
"matches": [
|
||||
"season",
|
||||
"year",
|
||||
"edition",
|
||||
"streaming_service",
|
||||
"hearing_impaired",
|
||||
"series_imdb_id",
|
||||
"series",
|
||||
"episode"
|
||||
],
|
||||
"dont_matches": [
|
||||
"source",
|
||||
"resolution",
|
||||
"audio_codec",
|
||||
"video_codec",
|
||||
"hash",
|
||||
"release_group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"seriesTitle": "Hoarders",
|
||||
"monitored": true,
|
||||
"episode_number": "1x1",
|
||||
"episodeTitle": "Jennifer and Ron & Jill",
|
||||
"timestamp": "4 days ago",
|
||||
"subs_id": "8121698",
|
||||
"description": "English HI subtitles downloaded from opensubtitlescom with a score of 91.67%.",
|
||||
"sonarrSeriesId": 1512,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": true
|
||||
},
|
||||
"score": "91.67%",
|
||||
"tags": [],
|
||||
"action": 1,
|
||||
"subtitles_path": "/tv/Tv Shows/Hoarders/Season 1/Hoarders.S01E01.Jennifer.and.Ron.&.Jill.en.srt",
|
||||
"sonarrEpisodeId": 142668,
|
||||
"provider": "opensubtitlescom",
|
||||
"upgradable": true,
|
||||
"parsed_timestamp": "09/22/23 12:45:18",
|
||||
"blacklisted": false,
|
||||
"matches": [
|
||||
"season",
|
||||
"year",
|
||||
"edition",
|
||||
"streaming_service",
|
||||
"series_imdb_id",
|
||||
"series",
|
||||
"other",
|
||||
"episode"
|
||||
],
|
||||
"dont_matches": [
|
||||
"source",
|
||||
"hearing_impaired",
|
||||
"resolution",
|
||||
"audio_codec",
|
||||
"video_codec",
|
||||
"hash",
|
||||
"release_group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"seriesTitle": "Billions",
|
||||
"monitored": true,
|
||||
"episode_number": "7x7",
|
||||
"episodeTitle": "DMV",
|
||||
"timestamp": "4 days ago",
|
||||
"subs_id": "8322339",
|
||||
"description": "English subtitles downloaded from opensubtitlescom with a score of 93.89%.",
|
||||
"sonarrSeriesId": 498,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": false
|
||||
},
|
||||
"score": "93.89%",
|
||||
"tags": [],
|
||||
"action": 1,
|
||||
"subtitles_path": "/tv/Tv Shows/Billions/Season 7/Billions.S07E07.DMV.en.srt",
|
||||
"sonarrEpisodeId": 140377,
|
||||
"provider": "opensubtitlescom",
|
||||
"upgradable": true,
|
||||
"parsed_timestamp": "09/22/23 12:19:57",
|
||||
"blacklisted": false,
|
||||
"matches": [
|
||||
"season",
|
||||
"year",
|
||||
"edition",
|
||||
"source",
|
||||
"streaming_service",
|
||||
"hearing_impaired",
|
||||
"series_imdb_id",
|
||||
"series",
|
||||
"other",
|
||||
"episode"
|
||||
],
|
||||
"dont_matches": [
|
||||
"resolution",
|
||||
"audio_codec",
|
||||
"video_codec",
|
||||
"hash",
|
||||
"release_group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"seriesTitle": "Self Made: Inspired By The Life Of Madam C.J. Walker",
|
||||
"monitored": true,
|
||||
"episode_number": "1x3",
|
||||
"episodeTitle": "The Walker Girl",
|
||||
"timestamp": "4 days ago",
|
||||
"subs_id": "5215972",
|
||||
"description": "English subtitles downloaded from opensubtitlescom with a score of 93.89%.",
|
||||
"sonarrSeriesId": 1501,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": false
|
||||
},
|
||||
"score": "93.89%",
|
||||
"tags": [],
|
||||
"action": 1,
|
||||
"subtitles_path": "/tv/Tv Shows/Self Made - Inspired By The Life Of Madam C.J. Walker/Season 1/Self.Made-.Inspired.By.The.Life.Of.Madam.C.J.Walker.S01E03.The.Walker.Girl.en.srt",
|
||||
"sonarrEpisodeId": 141490,
|
||||
"provider": "opensubtitlescom",
|
||||
"upgradable": true,
|
||||
"parsed_timestamp": "09/22/23 11:23:29",
|
||||
"blacklisted": false,
|
||||
"matches": [
|
||||
"season",
|
||||
"year",
|
||||
"edition",
|
||||
"source",
|
||||
"hearing_impaired",
|
||||
"series_imdb_id",
|
||||
"series",
|
||||
"episode"
|
||||
],
|
||||
"dont_matches": [
|
||||
"streaming_service",
|
||||
"resolution",
|
||||
"audio_codec",
|
||||
"video_codec",
|
||||
"hash",
|
||||
"release_group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"seriesTitle": "Self Made: Inspired By The Life Of Madam C.J. Walker",
|
||||
"monitored": true,
|
||||
"episode_number": "1x1",
|
||||
"episodeTitle": "The Fight of the Century",
|
||||
"timestamp": "4 days ago",
|
||||
"subs_id": "5215994",
|
||||
"description": "English subtitles downloaded from opensubtitlescom with a score of 93.89%.",
|
||||
"sonarrSeriesId": 1501,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": false
|
||||
},
|
||||
"score": "93.89%",
|
||||
"tags": [],
|
||||
"action": 1,
|
||||
"subtitles_path": "/tv/Tv Shows/Self Made - Inspired By The Life Of Madam C.J. Walker/Season 1/Self.Made-.Inspired.By.The.Life.Of.Madam.C.J.Walker.S01E01.The.Fight.of.the.Century.en.srt",
|
||||
"sonarrEpisodeId": 141488,
|
||||
"provider": "opensubtitlescom",
|
||||
"upgradable": true,
|
||||
"parsed_timestamp": "09/22/23 11:23:17",
|
||||
"blacklisted": false,
|
||||
"matches": [
|
||||
"season",
|
||||
"year",
|
||||
"edition",
|
||||
"source",
|
||||
"hearing_impaired",
|
||||
"series_imdb_id",
|
||||
"series",
|
||||
"episode"
|
||||
],
|
||||
"dont_matches": [
|
||||
"streaming_service",
|
||||
"resolution",
|
||||
"audio_codec",
|
||||
"video_codec",
|
||||
"hash",
|
||||
"release_group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"seriesTitle": "Bering Sea Gold",
|
||||
"monitored": true,
|
||||
"episode_number": "16x8",
|
||||
"episodeTitle": "Jane's Affliction",
|
||||
"timestamp": "4 days ago",
|
||||
"subs_id": "8323507",
|
||||
"description": "English subtitles downloaded from opensubtitlescom with a score of 94.44%.",
|
||||
"sonarrSeriesId": 1136,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": false
|
||||
},
|
||||
"score": "94.44%",
|
||||
"tags": [],
|
||||
"action": 1,
|
||||
"subtitles_path": "/tv/Documentaires/tv shows/Bering Sea Gold/Season 16/Bering.Sea.Gold.S16E08.Jane's.Affliction.en.srt",
|
||||
"sonarrEpisodeId": 141312,
|
||||
"provider": "opensubtitlescom",
|
||||
"upgradable": true,
|
||||
"parsed_timestamp": "09/22/23 05:22:09",
|
||||
"blacklisted": false,
|
||||
"matches": [
|
||||
"season",
|
||||
"year",
|
||||
"edition",
|
||||
"source",
|
||||
"hearing_impaired",
|
||||
"series_imdb_id",
|
||||
"series",
|
||||
"video_codec",
|
||||
"other",
|
||||
"episode"
|
||||
],
|
||||
"dont_matches": [
|
||||
"streaming_service",
|
||||
"resolution",
|
||||
"audio_codec",
|
||||
"hash",
|
||||
"release_group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"seriesTitle": "Archer (2009)",
|
||||
"monitored": true,
|
||||
"episode_number": "14x5",
|
||||
"episodeTitle": "Keys Open Doors",
|
||||
"timestamp": "4 days ago",
|
||||
"subs_id": "7-JH",
|
||||
"description": "English subtitles downloaded from podnapisi with a score of 99.17%.",
|
||||
"sonarrSeriesId": 306,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": false
|
||||
},
|
||||
"score": "99.17%",
|
||||
"tags": [],
|
||||
"action": 1,
|
||||
"subtitles_path": "/tv/Cartoons/archer (2009)/Season 14/Archer.(2009).S14E05.Keys.Open.Doors.en.srt",
|
||||
"sonarrEpisodeId": 141182,
|
||||
"provider": "podnapisi",
|
||||
"upgradable": false,
|
||||
"parsed_timestamp": "09/22/23 05:21:14",
|
||||
"blacklisted": false,
|
||||
"matches": [
|
||||
"streaming_service",
|
||||
"resolution",
|
||||
"other",
|
||||
"episode",
|
||||
"season",
|
||||
"year",
|
||||
"edition",
|
||||
"source",
|
||||
"hearing_impaired",
|
||||
"series",
|
||||
"video_codec",
|
||||
"release_group"
|
||||
],
|
||||
"dont_matches": [
|
||||
"hash",
|
||||
"audio_codec"
|
||||
]
|
||||
},
|
||||
{
|
||||
"seriesTitle": "Sex Education",
|
||||
"monitored": true,
|
||||
"episode_number": "4x1",
|
||||
"episodeTitle": "Episode 1",
|
||||
"timestamp": "4 days ago",
|
||||
"subs_id": "8322567",
|
||||
"description": "English subtitles downloaded from opensubtitlescom with a score of 99.17%.",
|
||||
"sonarrSeriesId": 896,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": false
|
||||
},
|
||||
"score": "99.17%",
|
||||
"tags": [],
|
||||
"action": 1,
|
||||
"subtitles_path": "/tv/Tv Shows/Sex Education/Season 4/Sex.Education.S04E01.Episode.1.en.srt",
|
||||
"sonarrEpisodeId": 140680,
|
||||
"provider": "opensubtitlescom",
|
||||
"upgradable": false,
|
||||
"parsed_timestamp": "09/22/23 05:17:26",
|
||||
"blacklisted": false,
|
||||
"matches": [
|
||||
"season",
|
||||
"year",
|
||||
"edition",
|
||||
"source",
|
||||
"streaming_service",
|
||||
"resolution",
|
||||
"hearing_impaired",
|
||||
"series_imdb_id",
|
||||
"series",
|
||||
"video_codec",
|
||||
"release_group",
|
||||
"episode"
|
||||
],
|
||||
"dont_matches": [
|
||||
"hash",
|
||||
"audio_codec"
|
||||
]
|
||||
},
|
||||
{
|
||||
"seriesTitle": "Welcome to Wrexham",
|
||||
"monitored": true,
|
||||
"episode_number": "2x3",
|
||||
"episodeTitle": "Nott Yet",
|
||||
"timestamp": "4 days ago",
|
||||
"subs_id": "8322172",
|
||||
"description": "English subtitles downloaded from opensubtitlescom with a score of 100.0%.",
|
||||
"sonarrSeriesId": 1456,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": false
|
||||
},
|
||||
"score": "100.0%",
|
||||
"tags": [],
|
||||
"action": 1,
|
||||
"subtitles_path": "/tv/Tv Shows/Welcome to Wrexham/Season 2/Welcome.to.Wrexham.S02E03.Nott.Yet.en.srt",
|
||||
"sonarrEpisodeId": 140238,
|
||||
"provider": "opensubtitlescom",
|
||||
"upgradable": false,
|
||||
"parsed_timestamp": "09/22/23 05:16:55",
|
||||
"blacklisted": false,
|
||||
"matches": [
|
||||
"hash",
|
||||
"hearing_impaired"
|
||||
],
|
||||
"dont_matches": []
|
||||
}
|
||||
],
|
||||
"total": 71633
|
||||
}
|
||||
66
internal/arr/test_fixtures/bazarr/expected_metrics.txt
Normal file
66
internal/arr/test_fixtures/bazarr/expected_metrics.txt
Normal file
@ -0,0 +1,66 @@
|
||||
# HELP bazarr_movie_subtitles_downloaded_total Total number of downloaded movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_downloaded_total gauge
|
||||
bazarr_movie_subtitles_downloaded_total{url="SOMEURL"} 10
|
||||
# HELP bazarr_movie_subtitles_filesize_total Total filesize of all movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_filesize_total gauge
|
||||
bazarr_movie_subtitles_filesize_total{url="SOMEURL"} 785627
|
||||
# HELP bazarr_movie_subtitles_history_total Total number of history movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_history_total gauge
|
||||
bazarr_movie_subtitles_history_total{url="SOMEURL"} 3511
|
||||
# HELP bazarr_movie_subtitles_missing_total Total number of missing movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_missing_total gauge
|
||||
bazarr_movie_subtitles_missing_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_movie_subtitles_monitored_total Total number of monitored movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_monitored_total gauge
|
||||
bazarr_movie_subtitles_monitored_total{url="SOMEURL"} 10
|
||||
# HELP bazarr_movie_subtitles_unmonitored_total Total number of unmonitored movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_unmonitored_total gauge
|
||||
bazarr_movie_subtitles_unmonitored_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_movie_subtitles_wanted_total Total number of wanted movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_wanted_total gauge
|
||||
bazarr_movie_subtitles_wanted_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_subtitles_downloaded_total Total number of downloaded subtitles
|
||||
# TYPE bazarr_subtitles_downloaded_total gauge
|
||||
bazarr_subtitles_downloaded_total{url="SOMEURL"} 10
|
||||
# HELP bazarr_subtitles_filesize_total Total filesize of all subtitles
|
||||
# TYPE bazarr_subtitles_filesize_total gauge
|
||||
bazarr_subtitles_filesize_total{url="SOMEURL"} 785627
|
||||
# HELP bazarr_subtitles_history_total Total number of history subtitles
|
||||
# TYPE bazarr_subtitles_history_total gauge
|
||||
bazarr_subtitles_history_total{url="SOMEURL"} 3511
|
||||
# HELP bazarr_subtitles_language_total Total number of downloaded subtitles by language
|
||||
# TYPE bazarr_subtitles_language_total gauge
|
||||
bazarr_subtitles_language_total{language="English",url="SOMEURL"} 10
|
||||
# HELP bazarr_subtitles_missing_total Total number of missing subtitles
|
||||
# TYPE bazarr_subtitles_missing_total gauge
|
||||
bazarr_subtitles_missing_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_subtitles_monitored_total Total number of monitored subtitles
|
||||
# TYPE bazarr_subtitles_monitored_total gauge
|
||||
bazarr_subtitles_monitored_total{url="SOMEURL"} 10
|
||||
# HELP bazarr_subtitles_provider_total Total number of downloaded subtitles by provider
|
||||
# TYPE bazarr_subtitles_provider_total gauge
|
||||
bazarr_subtitles_provider_total{provider="opensubtitlescom",url="SOMEURL"} 3
|
||||
bazarr_subtitles_provider_total{provider="podnapisi",url="SOMEURL"} 1
|
||||
bazarr_subtitles_provider_total{provider="yifysubtitles",url="SOMEURL"} 6
|
||||
# HELP bazarr_subtitles_score_total Total number of downloaded subtitles by score
|
||||
# TYPE bazarr_subtitles_score_total gauge
|
||||
bazarr_subtitles_score_total{score="75.0%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="77.5%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="81.67%",url="SOMEURL"} 2
|
||||
bazarr_subtitles_score_total{score="83.33%",url="SOMEURL"} 2
|
||||
bazarr_subtitles_score_total{score="85.0%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="86.67%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="87.5%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="97.5%",url="SOMEURL"} 1
|
||||
# HELP bazarr_subtitles_unmonitored_total Total number of unmonitored subtitles
|
||||
# TYPE bazarr_subtitles_unmonitored_total gauge
|
||||
bazarr_subtitles_unmonitored_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_subtitles_wanted_total Total number of wanted subtitles
|
||||
# TYPE bazarr_subtitles_wanted_total gauge
|
||||
bazarr_subtitles_wanted_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_system_health_issues Total number of health issues by object and issue
|
||||
# TYPE bazarr_system_health_issues gauge
|
||||
bazarr_system_health_issues{issue="some/path",object="incorrectly configured!!!",url="SOMEURL"} 1
|
||||
# HELP bazarr_system_status System Status
|
||||
# TYPE bazarr_system_status gauge
|
||||
bazarr_system_status{url="SOMEURL"} 1
|
||||
@ -0,0 +1,94 @@
|
||||
# HELP bazarr_episode_subtitles_downloaded_total Total number of downloaded episode subtitles
|
||||
# TYPE bazarr_episode_subtitles_downloaded_total gauge
|
||||
bazarr_episode_subtitles_downloaded_total{url="SOMEURL"} 100
|
||||
# HELP bazarr_episode_subtitles_filesize_total Total filesize of all episode subtitles
|
||||
# TYPE bazarr_episode_subtitles_filesize_total gauge
|
||||
bazarr_episode_subtitles_filesize_total{url="SOMEURL"} 4.133798e+06
|
||||
# HELP bazarr_episode_subtitles_history_total Total number of history episode subtitles
|
||||
# TYPE bazarr_episode_subtitles_history_total gauge
|
||||
bazarr_episode_subtitles_history_total{url="SOMEURL"} 71633
|
||||
# HELP bazarr_episode_subtitles_missing_total Total number of missing episode subtitles
|
||||
# TYPE bazarr_episode_subtitles_missing_total gauge
|
||||
bazarr_episode_subtitles_missing_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_episode_subtitles_monitored_total Total number of monitored episode subtitles
|
||||
# TYPE bazarr_episode_subtitles_monitored_total gauge
|
||||
bazarr_episode_subtitles_monitored_total{url="SOMEURL"} 16
|
||||
# HELP bazarr_episode_subtitles_unmonitored_total Total number of unmonitored episode subtitles
|
||||
# TYPE bazarr_episode_subtitles_unmonitored_total gauge
|
||||
bazarr_episode_subtitles_unmonitored_total{url="SOMEURL"} 84
|
||||
# HELP bazarr_episode_subtitles_wanted_total Total number of wanted episode subtitles
|
||||
# TYPE bazarr_episode_subtitles_wanted_total gauge
|
||||
bazarr_episode_subtitles_wanted_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_movie_subtitles_downloaded_total Total number of downloaded movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_downloaded_total gauge
|
||||
bazarr_movie_subtitles_downloaded_total{url="SOMEURL"} 10
|
||||
# HELP bazarr_movie_subtitles_filesize_total Total filesize of all movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_filesize_total gauge
|
||||
bazarr_movie_subtitles_filesize_total{url="SOMEURL"} 785627
|
||||
# HELP bazarr_movie_subtitles_history_total Total number of history movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_history_total gauge
|
||||
bazarr_movie_subtitles_history_total{url="SOMEURL"} 3511
|
||||
# HELP bazarr_movie_subtitles_missing_total Total number of missing movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_missing_total gauge
|
||||
bazarr_movie_subtitles_missing_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_movie_subtitles_monitored_total Total number of monitored movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_monitored_total gauge
|
||||
bazarr_movie_subtitles_monitored_total{url="SOMEURL"} 10
|
||||
# HELP bazarr_movie_subtitles_unmonitored_total Total number of unmonitored movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_unmonitored_total gauge
|
||||
bazarr_movie_subtitles_unmonitored_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_movie_subtitles_wanted_total Total number of wanted movie subtitles
|
||||
# TYPE bazarr_movie_subtitles_wanted_total gauge
|
||||
bazarr_movie_subtitles_wanted_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_subtitles_downloaded_total Total number of downloaded subtitles
|
||||
# TYPE bazarr_subtitles_downloaded_total gauge
|
||||
bazarr_subtitles_downloaded_total{url="SOMEURL"} 110
|
||||
# HELP bazarr_subtitles_filesize_total Total filesize of all subtitles
|
||||
# TYPE bazarr_subtitles_filesize_total gauge
|
||||
bazarr_subtitles_filesize_total{url="SOMEURL"} 4.919425e+06
|
||||
# HELP bazarr_subtitles_history_total Total number of history subtitles
|
||||
# TYPE bazarr_subtitles_history_total gauge
|
||||
bazarr_subtitles_history_total{url="SOMEURL"} 75144
|
||||
# HELP bazarr_subtitles_language_total Total number of downloaded subtitles by language
|
||||
# TYPE bazarr_subtitles_language_total gauge
|
||||
bazarr_subtitles_language_total{language="English",url="SOMEURL"} 110
|
||||
# HELP bazarr_subtitles_missing_total Total number of missing subtitles
|
||||
# TYPE bazarr_subtitles_missing_total gauge
|
||||
bazarr_subtitles_missing_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_subtitles_monitored_total Total number of monitored subtitles
|
||||
# TYPE bazarr_subtitles_monitored_total gauge
|
||||
bazarr_subtitles_monitored_total{url="SOMEURL"} 26
|
||||
# HELP bazarr_subtitles_provider_total Total number of downloaded subtitles by provider
|
||||
# TYPE bazarr_subtitles_provider_total gauge
|
||||
bazarr_subtitles_provider_total{provider="opensubtitlescom",url="SOMEURL"} 12
|
||||
bazarr_subtitles_provider_total{provider="podnapisi",url="SOMEURL"} 2
|
||||
bazarr_subtitles_provider_total{provider="yifysubtitles",url="SOMEURL"} 6
|
||||
# HELP bazarr_subtitles_score_total Total number of downloaded subtitles by score
|
||||
# TYPE bazarr_subtitles_score_total gauge
|
||||
bazarr_subtitles_score_total{score="100.0%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="75.0%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="77.5%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="81.67%",url="SOMEURL"} 2
|
||||
bazarr_subtitles_score_total{score="83.33%",url="SOMEURL"} 2
|
||||
bazarr_subtitles_score_total{score="85.0%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="86.67%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="87.5%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="91.67%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="91.94%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="93.89%",url="SOMEURL"} 3
|
||||
bazarr_subtitles_score_total{score="94.17%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="94.44%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="97.5%",url="SOMEURL"} 1
|
||||
bazarr_subtitles_score_total{score="99.17%",url="SOMEURL"} 2
|
||||
# HELP bazarr_subtitles_unmonitored_total Total number of unmonitored subtitles
|
||||
# TYPE bazarr_subtitles_unmonitored_total gauge
|
||||
bazarr_subtitles_unmonitored_total{url="SOMEURL"} 84
|
||||
# HELP bazarr_subtitles_wanted_total Total number of wanted subtitles
|
||||
# TYPE bazarr_subtitles_wanted_total gauge
|
||||
bazarr_subtitles_wanted_total{url="SOMEURL"} 0
|
||||
# HELP bazarr_system_health_issues Total number of health issues by object and issue
|
||||
# TYPE bazarr_system_health_issues gauge
|
||||
bazarr_system_health_issues{issue="some/path",object="incorrectly configured!!!",url="SOMEURL"} 1
|
||||
# HELP bazarr_system_status System Status
|
||||
# TYPE bazarr_system_status gauge
|
||||
bazarr_system_status{url="SOMEURL"} 1
|
||||
453
internal/arr/test_fixtures/bazarr/movies.json
Normal file
453
internal/arr/test_fixtures/bazarr/movies.json
Normal file
@ -0,0 +1,453 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"alternativeTitles": [
|
||||
"公元前10,000年",
|
||||
"10000 A.C.",
|
||||
"10, 000 A. C.",
|
||||
"紀元前1万年",
|
||||
"10 000 prije Krista"
|
||||
],
|
||||
"audio_language": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng"
|
||||
}
|
||||
],
|
||||
"fanart": "/images/movies/MediaCover/2/fanart.jpg?lastWrite=638282370533957460",
|
||||
"imdbId": "tt0443649",
|
||||
"missing_subtitles": [],
|
||||
"monitored": true,
|
||||
"overview": "A prehistoric epic that follows a young mammoth hunter's journey through uncharted territory to secure the future of his tribe.",
|
||||
"path": "/movies/Movies/10,000 BC (2008)/10,000 BC (2008) WEBDL-1080p.mkv",
|
||||
"poster": "/images/movies/MediaCover/2/poster-500.jpg?lastWrite=637936848730561157",
|
||||
"profileId": 1,
|
||||
"radarrId": 2,
|
||||
"sceneName": "10000.B.C.2008.1080p.HMAX.WEB-DL.DDP.5.1.H.264-PiRaTeS",
|
||||
"subtitles": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"path": "/movies/Movies/10,000 BC (2008)/10,000 BC (2008) WEBDL-1080p.en.srt",
|
||||
"forced": false,
|
||||
"hi": false,
|
||||
"file_size": 38725
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"title": "10,000 BC",
|
||||
"year": "2008"
|
||||
},
|
||||
{
|
||||
"alternativeTitles": [
|
||||
"10 důvodů, proč tě nenávidím",
|
||||
"对面的恶女看过来",
|
||||
"我恨你的10件事",
|
||||
"Ten Things I Hate About You",
|
||||
"10 razones para odiarte",
|
||||
"Dix bonnes raisons de te larguer",
|
||||
"내가 널 사랑할 수 없는 10가지 이유"
|
||||
],
|
||||
"audio_language": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng"
|
||||
}
|
||||
],
|
||||
"fanart": "/images/movies/MediaCover/1/fanart.jpg?lastWrite=638136454307026836",
|
||||
"imdbId": "tt0147800",
|
||||
"missing_subtitles": [],
|
||||
"monitored": true,
|
||||
"overview": "On the first day at his new school, Cameron instantly falls for Bianca, the gorgeous girl of his dreams. The only problem is that Bianca is forbidden to date until her ill-tempered, completely un-dateable older sister Kat goes out, too. In an attempt to solve his problem, Cameron singles out the only guy who could possibly be a match for Kat: a mysterious bad boy with a nasty reputation of his own.",
|
||||
"path": "/movies/Movies/10 Things I Hate About You 1999/10.Things.I.Hate.About.You.1999.1080p.BluRay.x264-OEM.mkv",
|
||||
"poster": "/images/movies/MediaCover/1/poster-500.jpg?lastWrite=637942058966272660",
|
||||
"profileId": 1,
|
||||
"radarrId": 1,
|
||||
"sceneName": null,
|
||||
"subtitles": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"path": "/movies/Movies/10 Things I Hate About You 1999/10.Things.I.Hate.About.You.1999.1080p.BluRay.x264-OEM.eng.srt",
|
||||
"forced": false,
|
||||
"hi": false,
|
||||
"file_size": 91164
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"title": "10 Things I Hate About You",
|
||||
"year": "1999"
|
||||
},
|
||||
{
|
||||
"alternativeTitles": [
|
||||
"De 101 Dalmatinerna II – Tuffs Äventyr i London",
|
||||
"Les 101 Dalmatiens II",
|
||||
"101 Dalmatiërs II: Het Avontuur Van Vlek In Londen",
|
||||
"101 Dalmatians 2",
|
||||
"101 Dalmatinů II: Flíčkova londýnská dobrodružství",
|
||||
"101 dalmatialaista 2: Pikku Kikero Lontoossa",
|
||||
"101 Dalmatíuhundar 2: Ævintýri Patch í London",
|
||||
"101 Dalmatinere II: Kvik på eventyr i London",
|
||||
"۱۰۱ سگ خالدار قسمت دوم",
|
||||
"101 Dalmations 2 - Patch's London Adventure",
|
||||
"101 Dalmatinere II Flekkens Londoneventyr",
|
||||
"101 마리의 달마시안 개 2 : 패치의 런던 대모험",
|
||||
"Les 101 Dalmatiens 2 : Sur la Trace des Héros",
|
||||
"Les 101 Dalmatiens 2",
|
||||
"101 dalmatyńczyków II: Londyńska przygoda",
|
||||
"101 Dalmatiner 2",
|
||||
"101 Dalmatiens 2 - Sur La Trace Des Héros",
|
||||
"101 Dalmatiner II: Auf kleinen Pfoten zum großen Star",
|
||||
"101 kiskutya II. - Paca és Agyar",
|
||||
"101忠狗续集:伦敦大冒险",
|
||||
"101 Dalmatiner 2 - Auf kleinen Pfoten zum großen Star!",
|
||||
"101 Dalmatinere II"
|
||||
],
|
||||
"audio_language": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng"
|
||||
}
|
||||
],
|
||||
"fanart": "/images/movies/MediaCover/1899/fanart.jpg?lastWrite=638243232599068199",
|
||||
"imdbId": "tt0324941",
|
||||
"missing_subtitles": [],
|
||||
"monitored": true,
|
||||
"overview": "Being one of 101 takes its toll on Patch, who doesn't feel unique. When he's accidentally left behind on moving day, he meets his idol, Thunderbolt, who enlists him on a publicity campaign.",
|
||||
"path": "/movies/Movies/101 Dalmatians II Patch's London Adventure (2003)/101 Dalmatians 2 - Patch's London Adventure (2003).avi",
|
||||
"poster": "/images/movies/MediaCover/1899/poster-500.jpg?lastWrite=638222406529360598",
|
||||
"profileId": 1,
|
||||
"radarrId": 1899,
|
||||
"sceneName": null,
|
||||
"subtitles": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"path": "/movies/Movies/101 Dalmatians II Patch's London Adventure (2003)/101 Dalmatians 2 - Patch's London Adventure (2003).eng.srt",
|
||||
"forced": false,
|
||||
"hi": false,
|
||||
"file_size": 80771
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"title": "101 Dalmatians II: Patch's London Adventure",
|
||||
"year": "2002"
|
||||
},
|
||||
{
|
||||
"alternativeTitles": [],
|
||||
"audio_language": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng"
|
||||
}
|
||||
],
|
||||
"fanart": "/images/movies/MediaCover/1806/fanart.jpg?lastWrite=638273674447347685",
|
||||
"imdbId": "tt0492931",
|
||||
"missing_subtitles": [],
|
||||
"monitored": true,
|
||||
"overview": "A look at the state of the global environment including visionary and practical solutions for restoring the planet's ecosystems. Featuring ongoing dialogues of experts from all over the world, including former Soviet Prime Minister Mikhail Gorbachev, renowned scientist Stephen Hawking, former head of the CIA R. James Woolse",
|
||||
"path": "/movies/Documentaires/movies/The 11th Hour (2007)/The 11th Hour (2007) 1080p.mp4",
|
||||
"poster": "/images/movies/MediaCover/1806/poster-500.jpg?lastWrite=638274545114206678",
|
||||
"profileId": 1,
|
||||
"radarrId": 1806,
|
||||
"sceneName": null,
|
||||
"subtitles": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"path": "/movies/Documentaires/movies/The 11th Hour (2007)/The 11th Hour (2007) 1080p.en.srt",
|
||||
"forced": false,
|
||||
"hi": false,
|
||||
"file_size": 132918
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"title": "The 11th Hour",
|
||||
"year": "2007"
|
||||
},
|
||||
{
|
||||
"alternativeTitles": [
|
||||
"Twelve Rounds",
|
||||
"12 Runden",
|
||||
"ฝ่าวิกฤติ 12 รอบระห่ำนรก",
|
||||
"Shoot & Run",
|
||||
"12ラウンド"
|
||||
],
|
||||
"audio_language": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng"
|
||||
}
|
||||
],
|
||||
"fanart": "/images/movies/MediaCover/3/fanart.jpg?lastWrite=637894352222998171",
|
||||
"imdbId": "tt1160368",
|
||||
"missing_subtitles": [],
|
||||
"monitored": true,
|
||||
"overview": "When New Orleans Police Detective Danny Fisher stops a brilliant thief from getting away with a multimillion-dollar heist, the thief's girlfriend is accidentally killed. After escaping from prison, the criminal mastermind enacts his revenge, taunting Danny with 12 rounds of near-impossible puzzles and tasks that he must somehow complete to save the life of the woman he loves.",
|
||||
"path": "/movies/Movies/12 Rounds 2009/12 Rounds (2009) WEBDL-720p.mkv",
|
||||
"poster": "/images/movies/MediaCover/3/poster-500.jpg?lastWrite=638304136083869005",
|
||||
"profileId": 1,
|
||||
"radarrId": 3,
|
||||
"sceneName": "12.Rounds.2009.720p.NORDIC.WEB-DL.H.264.DD5.1-TWA",
|
||||
"subtitles": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"path": "/movies/Movies/12 Rounds 2009/12 Rounds (2009) WEBDL-720p.en.srt",
|
||||
"forced": false,
|
||||
"hi": false,
|
||||
"file_size": 101226
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"title": "12 Rounds",
|
||||
"year": "2009"
|
||||
},
|
||||
{
|
||||
"alternativeTitles": [
|
||||
"12 Anos de Escravidão",
|
||||
"دوازده سال بردگی",
|
||||
"노예 12년",
|
||||
"12 години в робство",
|
||||
"La véritable histoire extraordinaire de Solomon Northup",
|
||||
"12 años de esclavitud",
|
||||
"12 let v řetězech",
|
||||
"被奪走的十二年",
|
||||
"为奴12年",
|
||||
"Esclave pendant douze ans",
|
||||
"それでも夜は明ける:2013"
|
||||
],
|
||||
"audio_language": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng"
|
||||
}
|
||||
],
|
||||
"fanart": "/images/movies/MediaCover/4/fanart.jpg?lastWrite=637943795848530072",
|
||||
"imdbId": "tt2024544",
|
||||
"missing_subtitles": [],
|
||||
"monitored": true,
|
||||
"overview": "In the pre-Civil War United States, Solomon Northup, a free black man from upstate New York, is abducted and sold into slavery. Facing cruelty as well as unexpected kindnesses Solomon struggles not only to stay alive, but to retain his dignity. In the twelfth year of his unforgettable odyssey, Solomon’s chance meeting with a Canadian abolitionist will forever alter his life.",
|
||||
"path": "/movies/Movies/12 Years a Slave (2013)/12 Years a Slave.mkv",
|
||||
"poster": "/images/movies/MediaCover/4/poster-500.jpg?lastWrite=638267581290585680",
|
||||
"profileId": 1,
|
||||
"radarrId": 4,
|
||||
"sceneName": null,
|
||||
"subtitles": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"path": "/movies/Movies/12 Years a Slave (2013)/12 Years a Slave.en.srt",
|
||||
"forced": false,
|
||||
"hi": false,
|
||||
"file_size": 96000
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"title": "12 Years a Slave",
|
||||
"year": "2013"
|
||||
},
|
||||
{
|
||||
"alternativeTitles": [
|
||||
"127 timmar",
|
||||
"127 Horas",
|
||||
"מאה עשרים ושבע שעות",
|
||||
"127 Godzin"
|
||||
],
|
||||
"audio_language": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng"
|
||||
}
|
||||
],
|
||||
"fanart": "/images/movies/MediaCover/5/fanart.jpg?lastWrite=638302393279249501",
|
||||
"imdbId": "tt1542344",
|
||||
"missing_subtitles": [],
|
||||
"monitored": true,
|
||||
"overview": "The true story of mountain climber Aron Ralston's remarkable adventure to save himself after a fallen boulder crashes on his arm and traps him in an isolated canyon in Utah.",
|
||||
"path": "/movies/Movies/127 Hours (2010)/127 Hours (2010) Bluray-1080p.mkv",
|
||||
"poster": "/images/movies/MediaCover/5/poster-500.jpg?lastWrite=637693906415508232",
|
||||
"profileId": 1,
|
||||
"radarrId": 5,
|
||||
"sceneName": "127.Hours.2010.BluRay.10Bit.1080p.DD5.1.H265-d3g",
|
||||
"subtitles": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"path": "/movies/Movies/127 Hours (2010)/127 Hours (2010) Bluray-1080p.en.srt",
|
||||
"forced": false,
|
||||
"hi": false,
|
||||
"file_size": 40598
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"title": "127 Hours",
|
||||
"year": "2010"
|
||||
},
|
||||
{
|
||||
"alternativeTitles": [
|
||||
"Thirteen Assassins",
|
||||
"殊死血战",
|
||||
"Jûsan-nin no shikaku",
|
||||
"Trece asesinos",
|
||||
"13인의 자객"
|
||||
],
|
||||
"audio_language": [
|
||||
{
|
||||
"name": "Japanese",
|
||||
"code2": "ja",
|
||||
"code3": "jpn"
|
||||
},
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng"
|
||||
}
|
||||
],
|
||||
"fanart": "/images/movies/MediaCover/6/fanart.jpg?lastWrite=637955943961598556",
|
||||
"imdbId": "tt1436045",
|
||||
"missing_subtitles": [],
|
||||
"monitored": true,
|
||||
"overview": "A bravado period action film set at the end of Japan's feudal era in which a group of unemployed samurai are enlisted to bring down a sadistic lord and prevent him from ascending to the throne and plunging the country into a war-torn future.",
|
||||
"path": "/movies/Movies/13 Assassins 2010/13.assassins.mkv",
|
||||
"poster": "/images/movies/MediaCover/6/poster-500.jpg?lastWrite=638303264533780846",
|
||||
"profileId": 1,
|
||||
"radarrId": 6,
|
||||
"sceneName": null,
|
||||
"subtitles": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"path": "/movies/Movies/13 Assassins 2010/13.assassins.eng.srt",
|
||||
"forced": false,
|
||||
"hi": false,
|
||||
"file_size": 72508
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"title": "13 Assassins",
|
||||
"year": "2010"
|
||||
},
|
||||
{
|
||||
"alternativeTitles": [
|
||||
"The Thirteenth Warrior",
|
||||
"El guerrero número trece",
|
||||
"Le 13è Guerrier",
|
||||
"O 13º Guerreiro",
|
||||
"Schwarze Nebel",
|
||||
"El guerrero número 13",
|
||||
"Den 13. kriger",
|
||||
"O 13 Guerreiro",
|
||||
"Il 13-esimo guerriero",
|
||||
"Il 13 guerriero",
|
||||
"Тринадцатый Воин",
|
||||
"13 Guerreros",
|
||||
"El guerrero n 13",
|
||||
"El guerrero No 13",
|
||||
"Il tredicesimo guerriero",
|
||||
"13-й воїн",
|
||||
"13 воїн",
|
||||
"Vikingové",
|
||||
"Der 13. Krieger",
|
||||
"El guerrero nº 13",
|
||||
"Trzynasty Wojownik"
|
||||
],
|
||||
"audio_language": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng"
|
||||
}
|
||||
],
|
||||
"fanart": "/images/movies/MediaCover/1016/fanart.jpg?lastWrite=638246714532440250",
|
||||
"imdbId": "tt0120657",
|
||||
"missing_subtitles": [],
|
||||
"monitored": true,
|
||||
"overview": "A Muslim ambassador exiled from his homeland, Ahmad ibn Fadlan finds himself in the company of Vikings. While the behavior of the Norsemen initially offends ibn Fadlan, the more cultured outsider grows to respect the tough, if uncouth, warriors. During their travels together, ibn Fadlan and the Vikings get word of an evil presence closing in, and they must fight the frightening and formidable force, which was previously thought to exist only in legend.",
|
||||
"path": "/movies/Movies/The 13th Warrior (1999)/The 13th Warrior.1999.BR-Rip.mkv",
|
||||
"poster": "/images/movies/MediaCover/1016/poster-500.jpg?lastWrite=637909957915208046",
|
||||
"profileId": 1,
|
||||
"radarrId": 1016,
|
||||
"sceneName": null,
|
||||
"subtitles": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"path": "/movies/Movies/The 13th Warrior (1999)/The 13th Warrior.1999.BR-Rip.srt",
|
||||
"forced": false,
|
||||
"hi": false,
|
||||
"file_size": 47482
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"title": "The 13th Warrior",
|
||||
"year": "1999"
|
||||
},
|
||||
{
|
||||
"alternativeTitles": [
|
||||
"Room 1408",
|
||||
"第1408号房间",
|
||||
"幻影凶间",
|
||||
"Chambre 1408",
|
||||
"1408 - Stephen King",
|
||||
"La Habitación 1408",
|
||||
"Zimmer 1408 - Schliesse diese Tuere nicht",
|
||||
"1408幻影凶间",
|
||||
"1408 ห้องสุสานแตก"
|
||||
],
|
||||
"audio_language": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng"
|
||||
},
|
||||
{
|
||||
"name": "French",
|
||||
"code2": "fr",
|
||||
"code3": "fra"
|
||||
}
|
||||
],
|
||||
"fanart": "/images/movies/MediaCover/7/fanart.jpg?lastWrite=638290198220002732",
|
||||
"imdbId": "tt0450385",
|
||||
"missing_subtitles": [],
|
||||
"monitored": true,
|
||||
"overview": "A man who specializes in debunking paranormal occurrences checks into the fabled room 1408 in the Dolphin Hotel. Soon after settling in, he confronts genuine terror.",
|
||||
"path": "/movies/Movies/1408 (2007)/1408 (2007) WEBDL-1080p.mkv",
|
||||
"poster": "/images/movies/MediaCover/7/poster-500.jpg?lastWrite=637873441925392357",
|
||||
"profileId": 1,
|
||||
"radarrId": 7,
|
||||
"sceneName": null,
|
||||
"subtitles": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"path": "/movies/Movies/1408 (2007)/1408 (2007) WEBDL-1080p.en.srt",
|
||||
"forced": false,
|
||||
"hi": false,
|
||||
"file_size": 84235
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"title": "1408",
|
||||
"year": "2007"
|
||||
}
|
||||
],
|
||||
"total": 2105
|
||||
}
|
||||
396
internal/arr/test_fixtures/bazarr/movies_history.json
Normal file
396
internal/arr/test_fixtures/bazarr/movies_history.json
Normal file
@ -0,0 +1,396 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"action": 1,
|
||||
"title": "Jefftowne",
|
||||
"timestamp": "4 days ago",
|
||||
"description": "English HI subtitles downloaded from opensubtitlescom with a score of 75.0%.",
|
||||
"radarrId": 1445,
|
||||
"monitored": true,
|
||||
"path": null,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": true
|
||||
},
|
||||
"tags": [],
|
||||
"score": "75.0%",
|
||||
"subs_id": "3560560",
|
||||
"provider": "opensubtitlescom",
|
||||
"subtitles_path": "/movies/Documentaires/movies/Jefftowne (1998)/Jefftowne (1998).en.srt",
|
||||
"upgradable": true,
|
||||
"parsed_timestamp": "09/22/23 07:36:38",
|
||||
"blacklisted": false,
|
||||
"matches": [
|
||||
"title",
|
||||
"year",
|
||||
"edition",
|
||||
"streaming_service",
|
||||
"imdb_id",
|
||||
"other"
|
||||
],
|
||||
"dont_matches": [
|
||||
"source",
|
||||
"hearing_impaired",
|
||||
"resolution",
|
||||
"audio_codec",
|
||||
"video_codec",
|
||||
"hash",
|
||||
"release_group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"action": 1,
|
||||
"title": "Fall of Japan: In Color",
|
||||
"timestamp": "4 days ago",
|
||||
"description": "English subtitles downloaded from opensubtitlescom with a score of 81.67%.",
|
||||
"radarrId": 1978,
|
||||
"monitored": true,
|
||||
"path": null,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": false
|
||||
},
|
||||
"tags": [],
|
||||
"score": "81.67%",
|
||||
"subs_id": "8125336",
|
||||
"provider": "opensubtitlescom",
|
||||
"subtitles_path": "/movies/Documentaires/movies/Fall of Japan in Color (2015)/Fall Of Japan In Color (2015) WEBRip-1080p.en.srt",
|
||||
"upgradable": true,
|
||||
"parsed_timestamp": "09/22/23 07:33:46",
|
||||
"blacklisted": false,
|
||||
"matches": [
|
||||
"title",
|
||||
"year",
|
||||
"edition",
|
||||
"source",
|
||||
"streaming_service",
|
||||
"hearing_impaired",
|
||||
"imdb_id",
|
||||
"other"
|
||||
],
|
||||
"dont_matches": [
|
||||
"resolution",
|
||||
"audio_codec",
|
||||
"video_codec",
|
||||
"hash",
|
||||
"release_group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"action": 1,
|
||||
"title": "Bill Burr: I'm Sorry You Feel That Way",
|
||||
"timestamp": "4 days ago",
|
||||
"description": "English HI subtitles downloaded from opensubtitlescom with a score of 86.67%.",
|
||||
"radarrId": 147,
|
||||
"monitored": true,
|
||||
"path": null,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": true
|
||||
},
|
||||
"tags": [],
|
||||
"score": "86.67%",
|
||||
"subs_id": "3589469",
|
||||
"provider": "opensubtitlescom",
|
||||
"subtitles_path": "/movies/Movies/Bill Burr Im Sorry You Feel That Way (2014)/Bill Burr I'm Sorry You Feel That Way (2014) WEBRip-1080p.en.srt",
|
||||
"upgradable": true,
|
||||
"parsed_timestamp": "09/22/23 07:31:33",
|
||||
"blacklisted": false,
|
||||
"matches": [
|
||||
"title",
|
||||
"year",
|
||||
"edition",
|
||||
"source",
|
||||
"resolution",
|
||||
"imdb_id",
|
||||
"audio_codec",
|
||||
"video_codec",
|
||||
"other"
|
||||
],
|
||||
"dont_matches": [
|
||||
"hash",
|
||||
"release_group",
|
||||
"streaming_service",
|
||||
"hearing_impaired"
|
||||
]
|
||||
},
|
||||
{
|
||||
"action": 1,
|
||||
"title": "Carrie Fisher: Wishful Drinking",
|
||||
"timestamp": "4 days ago",
|
||||
"description": "English subtitles downloaded from yifysubtitles with a score of 85.0%.",
|
||||
"radarrId": 1556,
|
||||
"monitored": true,
|
||||
"path": null,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": false
|
||||
},
|
||||
"tags": [],
|
||||
"score": "85.0%",
|
||||
"subs_id": "/subtitles/carrie-fisher-wishful-drinking-2010-english-yify-337916",
|
||||
"provider": "yifysubtitles",
|
||||
"subtitles_path": "/movies/Documentaires/movies/Wishful Drinking (2010)/Carrie Fisher Wishful Drinking (2010) WEBRip-1080p.en.srt",
|
||||
"upgradable": true,
|
||||
"parsed_timestamp": "09/21/23 22:31:04",
|
||||
"blacklisted": false,
|
||||
"matches": [],
|
||||
"dont_matches": [
|
||||
"title",
|
||||
"year",
|
||||
"edition",
|
||||
"source",
|
||||
"streaming_service",
|
||||
"resolution",
|
||||
"hearing_impaired",
|
||||
"audio_codec",
|
||||
"video_codec",
|
||||
"hash",
|
||||
"release_group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"action": 1,
|
||||
"title": "Role Models",
|
||||
"timestamp": "5 days ago",
|
||||
"description": "English subtitles downloaded from podnapisi with a score of 87.5%.",
|
||||
"radarrId": 1662,
|
||||
"monitored": true,
|
||||
"path": null,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": false
|
||||
},
|
||||
"tags": [],
|
||||
"score": "87.5%",
|
||||
"subs_id": "nigG",
|
||||
"provider": "podnapisi",
|
||||
"subtitles_path": "/movies/Movies/Role Models (2008)/Role Models (2008) Bluray-1080p.en.srt",
|
||||
"upgradable": true,
|
||||
"parsed_timestamp": "09/21/23 06:46:25",
|
||||
"blacklisted": false,
|
||||
"matches": [
|
||||
"title",
|
||||
"other",
|
||||
"year",
|
||||
"edition",
|
||||
"source",
|
||||
"streaming_service",
|
||||
"resolution",
|
||||
"hearing_impaired",
|
||||
"audio_codec",
|
||||
"video_codec"
|
||||
],
|
||||
"dont_matches": [
|
||||
"hash",
|
||||
"release_group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"action": 1,
|
||||
"title": "Girl in the Basement",
|
||||
"timestamp": "6 days ago",
|
||||
"description": "English subtitles downloaded from yifysubtitles with a score of 97.5%.",
|
||||
"radarrId": 2098,
|
||||
"monitored": true,
|
||||
"path": null,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": false
|
||||
},
|
||||
"tags": [],
|
||||
"score": "97.5%",
|
||||
"subs_id": "/subtitles/girl-in-the-basement-2021-english-yify-324062",
|
||||
"provider": "yifysubtitles",
|
||||
"subtitles_path": "/movies/Movies/Girl in the Basement (2021)/Girl in the Basement (2021) WEBRip-1080p.en.srt",
|
||||
"upgradable": false,
|
||||
"parsed_timestamp": "09/20/23 04:33:40",
|
||||
"blacklisted": false,
|
||||
"matches": [],
|
||||
"dont_matches": [
|
||||
"title",
|
||||
"year",
|
||||
"edition",
|
||||
"source",
|
||||
"streaming_service",
|
||||
"resolution",
|
||||
"hearing_impaired",
|
||||
"audio_codec",
|
||||
"video_codec",
|
||||
"hash",
|
||||
"release_group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"action": 1,
|
||||
"title": "Book of Blood",
|
||||
"timestamp": "6 days ago",
|
||||
"description": "English subtitles downloaded from yifysubtitles with a score of 77.5%.",
|
||||
"radarrId": 1904,
|
||||
"monitored": true,
|
||||
"path": null,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": false
|
||||
},
|
||||
"tags": [],
|
||||
"score": "77.5%",
|
||||
"subs_id": "/subtitles/book-of-blood-2009-english-yify-96527",
|
||||
"provider": "yifysubtitles",
|
||||
"subtitles_path": "/movies/Movies/Book Of Blood (2009)/Book of Blood (2009) WEBDL-1080p.en.srt",
|
||||
"upgradable": true,
|
||||
"parsed_timestamp": "09/20/23 04:32:33",
|
||||
"blacklisted": false,
|
||||
"matches": [],
|
||||
"dont_matches": [
|
||||
"title",
|
||||
"year",
|
||||
"edition",
|
||||
"source",
|
||||
"streaming_service",
|
||||
"resolution",
|
||||
"hearing_impaired",
|
||||
"audio_codec",
|
||||
"video_codec",
|
||||
"hash",
|
||||
"release_group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"action": 1,
|
||||
"title": "The Lego Ninjago Movie",
|
||||
"timestamp": "6 days ago",
|
||||
"description": "English subtitles downloaded from yifysubtitles with a score of 81.67%.",
|
||||
"radarrId": 1139,
|
||||
"monitored": true,
|
||||
"path": null,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": false
|
||||
},
|
||||
"tags": [],
|
||||
"score": "81.67%",
|
||||
"subs_id": "/subtitles/the-lego-ninjago-movie-2017-english-yify-30023",
|
||||
"provider": "yifysubtitles",
|
||||
"subtitles_path": "/movies/Movies/The LEGO Ninjago Movie (2017)/The Lego Ninjago Movie (2017) Bluray-1080p.en.srt",
|
||||
"upgradable": true,
|
||||
"parsed_timestamp": "09/20/23 04:31:37",
|
||||
"blacklisted": false,
|
||||
"matches": [],
|
||||
"dont_matches": [
|
||||
"title",
|
||||
"year",
|
||||
"edition",
|
||||
"source",
|
||||
"streaming_service",
|
||||
"resolution",
|
||||
"hearing_impaired",
|
||||
"audio_codec",
|
||||
"video_codec",
|
||||
"hash",
|
||||
"release_group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"action": 1,
|
||||
"title": "Bill Burr: You People Are All The Same",
|
||||
"timestamp": "6 days ago",
|
||||
"description": "English subtitles downloaded from yifysubtitles with a score of 83.33%.",
|
||||
"radarrId": 152,
|
||||
"monitored": true,
|
||||
"path": null,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": false
|
||||
},
|
||||
"tags": [],
|
||||
"score": "83.33%",
|
||||
"subs_id": "/subtitles/bill-burr-you-people-are-all-the-same-2012-english-yify-517553",
|
||||
"provider": "yifysubtitles",
|
||||
"subtitles_path": "/movies/Movies/Bill Burr You People Are All The Same (2012)/Bill Burr You People Are All The Same (2012) WEBRip-1080p.en.srt",
|
||||
"upgradable": true,
|
||||
"parsed_timestamp": "09/20/23 04:31:02",
|
||||
"blacklisted": false,
|
||||
"matches": [],
|
||||
"dont_matches": [
|
||||
"title",
|
||||
"year",
|
||||
"edition",
|
||||
"source",
|
||||
"streaming_service",
|
||||
"resolution",
|
||||
"hearing_impaired",
|
||||
"audio_codec",
|
||||
"video_codec",
|
||||
"hash",
|
||||
"release_group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"action": 1,
|
||||
"title": "Blockers",
|
||||
"timestamp": "6 days ago",
|
||||
"description": "English subtitles downloaded from yifysubtitles with a score of 83.33%.",
|
||||
"radarrId": 423,
|
||||
"monitored": true,
|
||||
"path": null,
|
||||
"language": {
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng",
|
||||
"forced": false,
|
||||
"hi": false
|
||||
},
|
||||
"tags": [],
|
||||
"score": "83.33%",
|
||||
"subs_id": "/subtitles/blockers-2018-english-yify-4538",
|
||||
"provider": "yifysubtitles",
|
||||
"subtitles_path": "/movies/Movies/Blockers (2018)/Blockers (2018) Bluray-1080p.en.srt",
|
||||
"upgradable": true,
|
||||
"parsed_timestamp": "09/20/23 00:27:24",
|
||||
"blacklisted": false,
|
||||
"matches": [],
|
||||
"dont_matches": [
|
||||
"title",
|
||||
"year",
|
||||
"edition",
|
||||
"source",
|
||||
"streaming_service",
|
||||
"resolution",
|
||||
"hearing_impaired",
|
||||
"audio_codec",
|
||||
"video_codec",
|
||||
"hash",
|
||||
"release_group"
|
||||
]
|
||||
}
|
||||
],
|
||||
"total": 3511
|
||||
}
|
||||
33
internal/arr/test_fixtures/bazarr/series.json
Normal file
33
internal/arr/test_fixtures/bazarr/series.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"alternativeTitles": [
|
||||
"Los 100",
|
||||
"Les 100"
|
||||
],
|
||||
"audio_language": [
|
||||
{
|
||||
"name": "English",
|
||||
"code2": "en",
|
||||
"code3": "eng"
|
||||
}
|
||||
],
|
||||
"episodeFileCount": 100,
|
||||
"episodeMissingCount": 0,
|
||||
"fanart": "/images/series/MediaCover/944/fanart.jpg",
|
||||
"imdbId": "tt2661044",
|
||||
"monitored": true,
|
||||
"overview": "Set ninety-seven years after a nuclear war has destroyed civilization, when a spaceship housing humanity's lone survivors sends one hundred juvenile delinquents back to Earth, in hopes of possibly re-populating the planet.",
|
||||
"path": "/tv/Tv Shows/the 100 (2014)",
|
||||
"poster": "/images/series/MediaCover/944/poster-250.jpg",
|
||||
"profileId": 1,
|
||||
"seriesType": "standard",
|
||||
"sonarrSeriesId": 944,
|
||||
"tags": [],
|
||||
"title": "The 100",
|
||||
"tvdbId": 268592,
|
||||
"year": "2014"
|
||||
}
|
||||
],
|
||||
"total": 1486
|
||||
}
|
||||
8
internal/arr/test_fixtures/bazarr/system_health.json
Normal file
8
internal/arr/test_fixtures/bazarr/system_health.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"object": "some/path",
|
||||
"issue": "incorrectly configured!!!"
|
||||
}
|
||||
]
|
||||
}
|
||||
14
internal/arr/test_fixtures/bazarr/system_status.json
Normal file
14
internal/arr/test_fixtures/bazarr/system_status.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"data": {
|
||||
"bazarr_version": "unknown",
|
||||
"package_version": "",
|
||||
"sonarr_version": "",
|
||||
"radarr_version": "",
|
||||
"operating_system": "macOS-13.3.1-arm64-arm-64bit",
|
||||
"python_version": "3.11.5",
|
||||
"bazarr_directory": "/Users/xxx/Sites/bazarr",
|
||||
"bazarr_config_directory": "/Users/xxx/Sites/bazarr/data",
|
||||
"start_time": 1695715754.889793,
|
||||
"timezone": "xx/xxx"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
# HELP APP_system_health_issues Total number of health issues by source, type, message and wikiurl
|
||||
# TYPE APP_system_health_issues gauge
|
||||
APP_system_health_issues{message="Indexers unavailable due to failures for more than 6 hours: SomeIndexer",source="IndexerLongTermStatusCheck",type="warning",url="SOMEURL",wikiurl="https://wiki.servarr.com/readarr/system#indexers-are-unavailable-due-to-failures"} 1
|
||||
@ -0,0 +1,3 @@
|
||||
# HELP APP_history_total Total number of item in the history
|
||||
# TYPE APP_history_total gauge
|
||||
APP_history_total{url="SOMEURL"} 1368
|
||||
@ -0,0 +1,3 @@
|
||||
# HELP APP_queue_total Total number of items in the queue by status, download_status, and download_state
|
||||
# TYPE APP_queue_total gauge
|
||||
APP_queue_total{download_state="downloading",download_status="warning",status="completed",url="SOMEURL"} 1
|
||||
@ -0,0 +1,3 @@
|
||||
# HELP APP_rootfolder_freespace_bytes Root folder space in bytes by path
|
||||
# TYPE APP_rootfolder_freespace_bytes gauge
|
||||
APP_rootfolder_freespace_bytes{path="/media/books/",url="SOMEURL"} 3.2147635175424e+13
|
||||
@ -0,0 +1,3 @@
|
||||
# HELP APP_system_status System Status
|
||||
# TYPE APP_system_status gauge
|
||||
APP_system_status{url="SOMEURL"} 1
|
||||
8
internal/arr/test_fixtures/common/v1_health.json
Normal file
8
internal/arr/test_fixtures/common/v1_health.json
Normal file
@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"source": "IndexerLongTermStatusCheck",
|
||||
"type": "warning",
|
||||
"message": "Indexers unavailable due to failures for more than 6 hours: SomeIndexer",
|
||||
"wikiUrl": "https://wiki.servarr.com/readarr/system#indexers-are-unavailable-due-to-failures"
|
||||
}
|
||||
]
|
||||
8
internal/arr/test_fixtures/common/v1_history.json
Normal file
8
internal/arr/test_fixtures/common/v1_history.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"page": 1,
|
||||
"pageSize": 10,
|
||||
"sortKey": "date",
|
||||
"sortDirection": "descending",
|
||||
"totalRecords": 1368,
|
||||
"records": []
|
||||
}
|
||||
69
internal/arr/test_fixtures/common/v1_queue.json
Normal file
69
internal/arr/test_fixtures/common/v1_queue.json
Normal file
@ -0,0 +1,69 @@
|
||||
{
|
||||
"page": 1,
|
||||
"pageSize": 10,
|
||||
"sortKey": "timeleft",
|
||||
"sortDirection": "ascending",
|
||||
"totalRecords": 1,
|
||||
"records": [
|
||||
{
|
||||
"movieId": 91,
|
||||
"languages": [],
|
||||
"quality": {
|
||||
"quality": {
|
||||
"id": 7,
|
||||
"name": "Bluray-1080p",
|
||||
"source": "bluray",
|
||||
"resolution": 1080,
|
||||
"modifier": "none"
|
||||
},
|
||||
"revision": {
|
||||
"version": 2,
|
||||
"real": 0,
|
||||
"isRepack": false
|
||||
}
|
||||
},
|
||||
"customFormats": [
|
||||
{
|
||||
"id": 6,
|
||||
"name": "Special Edition"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "Repack/Proper"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "DTS-ES"
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"name": "HD Bluray Tier 02"
|
||||
}
|
||||
],
|
||||
"customFormatScore": 1880,
|
||||
"size": 24397988960,
|
||||
"title": "Some.Movie.1.Has.A.Title-1080P",
|
||||
"sizeleft": 0,
|
||||
"timeleft": "00:00:00",
|
||||
"estimatedCompletionTime": "2023-10-17T23:23:09Z",
|
||||
"status": "completed",
|
||||
"trackedDownloadStatus": "warning",
|
||||
"trackedDownloadState": "downloading",
|
||||
"statusMessages": [
|
||||
{
|
||||
"title": "Some.Movie.1.Has.A.Title-1080P",
|
||||
"messages": [
|
||||
"Found matching movie via grab history, but release was matched to movie by ID. Manual Import required."
|
||||
]
|
||||
}
|
||||
],
|
||||
"errorMessage": "",
|
||||
"downloadId": "SABnzbd_nzo_asdf1234",
|
||||
"protocol": "usenet",
|
||||
"downloadClient": "SabNZBd",
|
||||
"indexer": "Some Indexer",
|
||||
"outputPath": "/media/.downloads/complete/movies/Some.Movie.1.Has.A.Title-1080P",
|
||||
"id": 8537983
|
||||
}
|
||||
]
|
||||
}
|
||||
6
internal/arr/test_fixtures/common/v1_rootfolder.json
Normal file
6
internal/arr/test_fixtures/common/v1_rootfolder.json
Normal file
@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"path": "/media/books/",
|
||||
"freeSpace": 32147635175424
|
||||
}
|
||||
]
|
||||
32
internal/arr/test_fixtures/common/v1_system_status.json
Normal file
32
internal/arr/test_fixtures/common/v1_system_status.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"appName": "Radarr",
|
||||
"instanceName": "Radarr",
|
||||
"version": "5.0.3.8127",
|
||||
"buildTime": "2023-10-07T22:36:20Z",
|
||||
"isDebug": false,
|
||||
"isProduction": true,
|
||||
"isAdmin": false,
|
||||
"isUserInteractive": true,
|
||||
"startupPath": "/app/bin",
|
||||
"appData": "/config",
|
||||
"osName": "alpine",
|
||||
"osVersion": "3.18.4",
|
||||
"isNetCore": true,
|
||||
"isLinux": true,
|
||||
"isOsx": false,
|
||||
"isWindows": false,
|
||||
"isDocker": false,
|
||||
"mode": "console",
|
||||
"branch": "develop",
|
||||
"databaseType": "sqLite",
|
||||
"databaseVersion": "3.41.2",
|
||||
"authentication": "none",
|
||||
"migrationVersion": 233,
|
||||
"urlBase": "",
|
||||
"runtimeVersion": "6.0.21",
|
||||
"runtimeName": "netcore",
|
||||
"startTime": "2023-10-13T20:45:26Z",
|
||||
"packageVersion": "5.0.3.8127",
|
||||
"packageAuthor": "[onedr0p](https://github.com/onedr0p)",
|
||||
"packageUpdateMechanism": "docker"
|
||||
}
|
||||
8
internal/arr/test_fixtures/common/v3_health.json
Normal file
8
internal/arr/test_fixtures/common/v3_health.json
Normal file
@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"source": "IndexerLongTermStatusCheck",
|
||||
"type": "warning",
|
||||
"message": "Indexers unavailable due to failures for more than 6 hours: SomeIndexer",
|
||||
"wikiUrl": "https://wiki.servarr.com/readarr/system#indexers-are-unavailable-due-to-failures"
|
||||
}
|
||||
]
|
||||
8
internal/arr/test_fixtures/common/v3_history.json
Normal file
8
internal/arr/test_fixtures/common/v3_history.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"page": 1,
|
||||
"pageSize": 10,
|
||||
"sortKey": "date",
|
||||
"sortDirection": "descending",
|
||||
"totalRecords": 1368,
|
||||
"records": []
|
||||
}
|
||||
69
internal/arr/test_fixtures/common/v3_queue.json
Normal file
69
internal/arr/test_fixtures/common/v3_queue.json
Normal file
@ -0,0 +1,69 @@
|
||||
{
|
||||
"page": 1,
|
||||
"pageSize": 10,
|
||||
"sortKey": "timeleft",
|
||||
"sortDirection": "ascending",
|
||||
"totalRecords": 1,
|
||||
"records": [
|
||||
{
|
||||
"movieId": 91,
|
||||
"languages": [],
|
||||
"quality": {
|
||||
"quality": {
|
||||
"id": 7,
|
||||
"name": "Bluray-1080p",
|
||||
"source": "bluray",
|
||||
"resolution": 1080,
|
||||
"modifier": "none"
|
||||
},
|
||||
"revision": {
|
||||
"version": 2,
|
||||
"real": 0,
|
||||
"isRepack": false
|
||||
}
|
||||
},
|
||||
"customFormats": [
|
||||
{
|
||||
"id": 6,
|
||||
"name": "Special Edition"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "Repack/Proper"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "DTS-ES"
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"name": "HD Bluray Tier 02"
|
||||
}
|
||||
],
|
||||
"customFormatScore": 1880,
|
||||
"size": 24397988960,
|
||||
"title": "Some.Movie.1.Has.A.Title-1080P",
|
||||
"sizeleft": 0,
|
||||
"timeleft": "00:00:00",
|
||||
"estimatedCompletionTime": "2023-10-17T23:23:09Z",
|
||||
"status": "completed",
|
||||
"trackedDownloadStatus": "warning",
|
||||
"trackedDownloadState": "downloading",
|
||||
"statusMessages": [
|
||||
{
|
||||
"title": "Some.Movie.1.Has.A.Title-1080P",
|
||||
"messages": [
|
||||
"Found matching movie via grab history, but release was matched to movie by ID. Manual Import required."
|
||||
]
|
||||
}
|
||||
],
|
||||
"errorMessage": "",
|
||||
"downloadId": "SABnzbd_nzo_asdf1234",
|
||||
"protocol": "usenet",
|
||||
"downloadClient": "SabNZBd",
|
||||
"indexer": "Some Indexer",
|
||||
"outputPath": "/media/.downloads/complete/movies/Some.Movie.1.Has.A.Title-1080P",
|
||||
"id": 8537983
|
||||
}
|
||||
]
|
||||
}
|
||||
6
internal/arr/test_fixtures/common/v3_rootfolder.json
Normal file
6
internal/arr/test_fixtures/common/v3_rootfolder.json
Normal file
@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"path": "/media/books/",
|
||||
"freeSpace": 32147635175424
|
||||
}
|
||||
]
|
||||
32
internal/arr/test_fixtures/common/v3_system_status.json
Normal file
32
internal/arr/test_fixtures/common/v3_system_status.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"appName": "Radarr",
|
||||
"instanceName": "Radarr",
|
||||
"version": "5.0.3.8127",
|
||||
"buildTime": "2023-10-07T22:36:20Z",
|
||||
"isDebug": false,
|
||||
"isProduction": true,
|
||||
"isAdmin": false,
|
||||
"isUserInteractive": true,
|
||||
"startupPath": "/app/bin",
|
||||
"appData": "/config",
|
||||
"osName": "alpine",
|
||||
"osVersion": "3.18.4",
|
||||
"isNetCore": true,
|
||||
"isLinux": true,
|
||||
"isOsx": false,
|
||||
"isWindows": false,
|
||||
"isDocker": false,
|
||||
"mode": "console",
|
||||
"branch": "develop",
|
||||
"databaseType": "sqLite",
|
||||
"databaseVersion": "3.41.2",
|
||||
"authentication": "none",
|
||||
"migrationVersion": 233,
|
||||
"urlBase": "",
|
||||
"runtimeVersion": "6.0.21",
|
||||
"runtimeName": "netcore",
|
||||
"startTime": "2023-10-13T20:45:26Z",
|
||||
"packageVersion": "5.0.3.8127",
|
||||
"packageAuthor": "[onedr0p](https://github.com/onedr0p)",
|
||||
"packageUpdateMechanism": "docker"
|
||||
}
|
||||
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
|
||||
}
|
||||
36
internal/arr/test_fixtures/radarr/expected_metrics.txt
Normal file
36
internal/arr/test_fixtures/radarr/expected_metrics.txt
Normal file
@ -0,0 +1,36 @@
|
||||
# HELP radarr_movie_downloaded_total Total number of downloaded movies
|
||||
# TYPE radarr_movie_downloaded_total gauge
|
||||
radarr_movie_downloaded_total{url="SOMEURL"} 4
|
||||
# HELP radarr_movie_editions Total number of movies with `edition` set
|
||||
# TYPE radarr_movie_editions gauge
|
||||
radarr_movie_editions{url="SOMEURL"} 2
|
||||
# HELP radarr_movie_filesize_total Total filesize of all movies
|
||||
# TYPE radarr_movie_filesize_total gauge
|
||||
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",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
|
||||
radarr_movie_tag_total{tag="someotherlabel",url="SOMEURL"} 3
|
||||
# HELP radarr_movie_total Total number of movies
|
||||
# TYPE radarr_movie_total gauge
|
||||
radarr_movie_total{url="SOMEURL"} 8
|
||||
# HELP radarr_movie_unmonitored_total Total number of unmonitored movies
|
||||
# TYPE radarr_movie_unmonitored_total gauge
|
||||
radarr_movie_unmonitored_total{url="SOMEURL"} 1
|
||||
# HELP radarr_movie_wanted_total Total number of wanted movies
|
||||
# TYPE radarr_movie_wanted_total gauge
|
||||
radarr_movie_wanted_total{url="SOMEURL"} 1
|
||||
93
internal/arr/test_fixtures/radarr/v3_movie.json
Normal file
93
internal/arr/test_fixtures/radarr/v3_movie.json
Normal file
@ -0,0 +1,93 @@
|
||||
[
|
||||
{
|
||||
"status": "released",
|
||||
"hasFile": true,
|
||||
"monitored": true,
|
||||
"isAvailable": true,
|
||||
"movieFile": {
|
||||
"size": 75973665026,
|
||||
"quality": {
|
||||
"quality": {
|
||||
"name": "Remux-2160p"
|
||||
}
|
||||
}
|
||||
},
|
||||
"qualityProfileId": 11
|
||||
},
|
||||
{
|
||||
"status": "released",
|
||||
"hasFile": true,
|
||||
"monitored": true,
|
||||
"isAvailable": true,
|
||||
"movieFile": {
|
||||
"size": 29102046776,
|
||||
"quality": {
|
||||
"quality": {
|
||||
"name": "Bluray-2160p"
|
||||
}
|
||||
}
|
||||
},
|
||||
"qualityProfileId": 10
|
||||
},
|
||||
{
|
||||
"status": "released",
|
||||
"hasFile": true,
|
||||
"qualityProfileId": 10,
|
||||
"monitored": true,
|
||||
"isAvailable": true,
|
||||
"movieFile": {
|
||||
"size": 27913422877,
|
||||
"quality": {
|
||||
"quality": {
|
||||
"name": "Bluray-2160p"
|
||||
}
|
||||
},
|
||||
"edition": "someOther"
|
||||
}
|
||||
},
|
||||
{
|
||||
"status": "announced",
|
||||
"hasFile": false,
|
||||
"qualityProfileId": 10,
|
||||
"monitored": true,
|
||||
"isAvailable": true
|
||||
},
|
||||
{
|
||||
"status": "announced",
|
||||
"hasFile": false,
|
||||
"qualityProfileId": 11,
|
||||
"monitored": true,
|
||||
"isAvailable": true
|
||||
},
|
||||
{
|
||||
"status": "announced",
|
||||
"hasFile": false,
|
||||
"qualityProfileId": 10,
|
||||
"monitored": false,
|
||||
"isAvailable": true
|
||||
},
|
||||
{
|
||||
"status": "announced",
|
||||
"hasFile": false,
|
||||
"qualityProfileId": 10,
|
||||
"monitored": true,
|
||||
"isAvailable": false
|
||||
},
|
||||
{
|
||||
"status": "released",
|
||||
"hasFile": true,
|
||||
"qualityProfileId": 9,
|
||||
"monitored": true,
|
||||
"minimumAvailability": "announced",
|
||||
"isAvailable": true,
|
||||
"movieFile": {
|
||||
"size": 14073822010,
|
||||
"quality": {
|
||||
"quality": {
|
||||
"name": "Bluray-1080p"
|
||||
}
|
||||
},
|
||||
"edition": "IMAX"
|
||||
}
|
||||
}
|
||||
]
|
||||
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
|
||||
}
|
||||
]
|
||||
34
internal/arr/test_fixtures/radarr/v3_tag_detail.json
Normal file
34
internal/arr/test_fixtures/radarr/v3_tag_detail.json
Normal file
@ -0,0 +1,34 @@
|
||||
[
|
||||
{
|
||||
"label": "somelabel",
|
||||
"delayProfileIds": [],
|
||||
"importListIds": [],
|
||||
"notificationIds": [],
|
||||
"releaseProfileIds": [],
|
||||
"indexerIds": [],
|
||||
"downloadClientIds": [],
|
||||
"autoTagIds": [],
|
||||
"movieIds": [
|
||||
37,
|
||||
47,
|
||||
69
|
||||
],
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"label": "someotherlabel",
|
||||
"delayProfileIds": [],
|
||||
"importListIds": [],
|
||||
"notificationIds": [],
|
||||
"releaseProfileIds": [],
|
||||
"indexerIds": [],
|
||||
"downloadClientIds": [],
|
||||
"autoTagIds": [],
|
||||
"movieIds": [
|
||||
12,
|
||||
34,
|
||||
99
|
||||
],
|
||||
"id": 2
|
||||
}
|
||||
]
|
||||
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
|
||||
}
|
||||
33
internal/arr/test_fixtures/readarr/expected_metrics.txt
Normal file
33
internal/arr/test_fixtures/readarr/expected_metrics.txt
Normal file
@ -0,0 +1,33 @@
|
||||
# HELP readarr_author_downloaded_total Total number of downloaded authors
|
||||
# TYPE readarr_author_downloaded_total gauge
|
||||
readarr_author_downloaded_total{url="SOMEURL"} 0
|
||||
# HELP readarr_author_filesize_bytes Total filesize of all authors in bytes
|
||||
# TYPE readarr_author_filesize_bytes gauge
|
||||
readarr_author_filesize_bytes{url="SOMEURL"} 4.55628734e+08
|
||||
# HELP readarr_author_monitored_total Total number of monitored authors
|
||||
# TYPE readarr_author_monitored_total gauge
|
||||
readarr_author_monitored_total{url="SOMEURL"} 3
|
||||
# HELP readarr_author_total Total number of authors
|
||||
# TYPE readarr_author_total gauge
|
||||
readarr_author_total{url="SOMEURL"} 3
|
||||
# HELP readarr_author_unmonitored_total Total number of unmonitored authors
|
||||
# TYPE readarr_author_unmonitored_total gauge
|
||||
readarr_author_unmonitored_total{url="SOMEURL"} 0
|
||||
# HELP readarr_book_downloaded_total Total number of downloaded books
|
||||
# TYPE readarr_book_downloaded_total gauge
|
||||
readarr_book_downloaded_total{url="SOMEURL"} 40
|
||||
# HELP readarr_book_grabbed_total Total number of grabbed books
|
||||
# TYPE readarr_book_grabbed_total gauge
|
||||
readarr_book_grabbed_total{url="SOMEURL"} 0
|
||||
# HELP readarr_book_missing_total Total number of missing books
|
||||
# TYPE readarr_book_missing_total gauge
|
||||
readarr_book_missing_total{url="SOMEURL"} 0
|
||||
# HELP readarr_book_monitored_total Total number of monitored books
|
||||
# TYPE readarr_book_monitored_total gauge
|
||||
readarr_book_monitored_total{url="SOMEURL"} 2
|
||||
# HELP readarr_book_total Total number of books
|
||||
# TYPE readarr_book_total gauge
|
||||
readarr_book_total{url="SOMEURL"} 72
|
||||
# HELP readarr_book_unmonitored_total Total number of unmonitored books
|
||||
# TYPE readarr_book_unmonitored_total gauge
|
||||
readarr_book_unmonitored_total{url="SOMEURL"} 1
|
||||
38
internal/arr/test_fixtures/readarr/v1_author.json
Normal file
38
internal/arr/test_fixtures/readarr/v1_author.json
Normal file
@ -0,0 +1,38 @@
|
||||
[
|
||||
{
|
||||
"monitored": true,
|
||||
"statistics": {
|
||||
"bookFileCount": 33,
|
||||
"bookCount": 62,
|
||||
"availableBookCount": 33,
|
||||
"totalBookCount": 86,
|
||||
"sizeOnDisk": 417389300,
|
||||
"percentOfBooks": 53.225806451612903225806451610
|
||||
},
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"monitored": true,
|
||||
"statistics": {
|
||||
"bookFileCount": 2,
|
||||
"bookCount": 3,
|
||||
"availableBookCount": 2,
|
||||
"totalBookCount": 3,
|
||||
"sizeOnDisk": 17804301,
|
||||
"percentOfBooks": 66.666666666666666666666666670
|
||||
},
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"monitored": true,
|
||||
"statistics": {
|
||||
"bookFileCount": 5,
|
||||
"bookCount": 7,
|
||||
"availableBookCount": 5,
|
||||
"totalBookCount": 14,
|
||||
"sizeOnDisk": 20435133,
|
||||
"percentOfBooks": 71.428571428571428571428571430
|
||||
},
|
||||
"id": 3
|
||||
}
|
||||
]
|
||||
36
internal/arr/test_fixtures/readarr/v1_book.json
Normal file
36
internal/arr/test_fixtures/readarr/v1_book.json
Normal file
@ -0,0 +1,36 @@
|
||||
[
|
||||
{
|
||||
"monitored": true,
|
||||
"statistics": {
|
||||
"bookFileCount": 1,
|
||||
"bookCount": 1,
|
||||
"totalBookCount": 1,
|
||||
"sizeOnDisk": 1038934,
|
||||
"percentOfBooks": 100
|
||||
},
|
||||
"grabbed": false
|
||||
},
|
||||
{
|
||||
|
||||
"monitored": false,
|
||||
"statistics": {
|
||||
"bookFileCount": 1,
|
||||
"bookCount": 1,
|
||||
"totalBookCount": 1,
|
||||
"sizeOnDisk": 1551533,
|
||||
"percentOfBooks": 100
|
||||
},
|
||||
"grabbed": false
|
||||
},
|
||||
{
|
||||
"monitored": true,
|
||||
"statistics": {
|
||||
"bookFileCount": 1,
|
||||
"bookCount": 1,
|
||||
"totalBookCount": 1,
|
||||
"sizeOnDisk": 96020157,
|
||||
"percentOfBooks": 100
|
||||
},
|
||||
"grabbed": false
|
||||
}
|
||||
]
|
||||
43
internal/arr/test_fixtures/sonarr/expected_metrics.txt
Normal file
43
internal/arr/test_fixtures/sonarr/expected_metrics.txt
Normal file
@ -0,0 +1,43 @@
|
||||
# HELP sonarr_episode_downloaded_total Total number of downloaded episodes
|
||||
# TYPE sonarr_episode_downloaded_total gauge
|
||||
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
|
||||
# HELP sonarr_season_downloaded_total Total number of downloaded seasons
|
||||
# TYPE sonarr_season_downloaded_total gauge
|
||||
sonarr_season_downloaded_total{url="SOMEURL"} 6
|
||||
# HELP sonarr_season_monitored_total Total number of monitored seasons
|
||||
# TYPE sonarr_season_monitored_total gauge
|
||||
sonarr_season_monitored_total{url="SOMEURL"} 7
|
||||
# HELP sonarr_season_total Total number of seasons
|
||||
# TYPE sonarr_season_total gauge
|
||||
sonarr_season_total{url="SOMEURL"} 33
|
||||
# HELP sonarr_season_unmonitored_total Total number of unmonitored seasons
|
||||
# TYPE sonarr_season_unmonitored_total gauge
|
||||
sonarr_season_unmonitored_total{url="SOMEURL"} 5
|
||||
# HELP sonarr_series_downloaded_total Total number of downloaded series
|
||||
# TYPE sonarr_series_downloaded_total gauge
|
||||
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
|
||||
# HELP sonarr_series_total Total number of series
|
||||
# TYPE sonarr_series_total gauge
|
||||
sonarr_series_total{url="SOMEURL"} 6
|
||||
# HELP sonarr_series_unmonitored_total Total number of unmonitored series
|
||||
# TYPE sonarr_series_unmonitored_total gauge
|
||||
sonarr_series_unmonitored_total{url="SOMEURL"} 1
|
||||
@ -0,0 +1,53 @@
|
||||
# HELP sonarr_episode_downloaded_total Total number of downloaded episodes
|
||||
# TYPE sonarr_episode_downloaded_total gauge
|
||||
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",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
|
||||
# HELP sonarr_episode_unmonitored_total Total number of unmonitored episodes
|
||||
# TYPE sonarr_episode_unmonitored_total gauge
|
||||
sonarr_episode_unmonitored_total{url="SOMEURL"} 6
|
||||
# HELP sonarr_season_downloaded_total Total number of downloaded seasons
|
||||
# TYPE sonarr_season_downloaded_total gauge
|
||||
sonarr_season_downloaded_total{url="SOMEURL"} 6
|
||||
# HELP sonarr_season_monitored_total Total number of monitored seasons
|
||||
# TYPE sonarr_season_monitored_total gauge
|
||||
sonarr_season_monitored_total{url="SOMEURL"} 7
|
||||
# HELP sonarr_season_total Total number of seasons
|
||||
# TYPE sonarr_season_total gauge
|
||||
sonarr_season_total{url="SOMEURL"} 33
|
||||
# HELP sonarr_season_unmonitored_total Total number of unmonitored seasons
|
||||
# TYPE sonarr_season_unmonitored_total gauge
|
||||
sonarr_season_unmonitored_total{url="SOMEURL"} 5
|
||||
# HELP sonarr_series_downloaded_total Total number of downloaded series
|
||||
# TYPE sonarr_series_downloaded_total gauge
|
||||
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
|
||||
# HELP sonarr_series_total Total number of series
|
||||
# TYPE sonarr_series_total gauge
|
||||
sonarr_series_total{url="SOMEURL"} 6
|
||||
# HELP sonarr_series_unmonitored_total Total number of unmonitored series
|
||||
# TYPE sonarr_series_unmonitored_total gauge
|
||||
sonarr_series_unmonitored_total{url="SOMEURL"} 1
|
||||
45
internal/arr/test_fixtures/sonarr/import json.py
Normal file
45
internal/arr/test_fixtures/sonarr/import json.py
Normal file
@ -0,0 +1,45 @@
|
||||
import json
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
data = None
|
||||
with open("v3_series.json") as f:
|
||||
data = json.load(f)
|
||||
|
||||
new_data = []
|
||||
for series in data:
|
||||
new_series = {
|
||||
"id": series["id"],
|
||||
"monitored": series["monitored"],
|
||||
"seasons": [],
|
||||
"statistics": {
|
||||
"seasonCount": series["statistics"]["seasonCount"],
|
||||
"episodeFileCount": series["statistics"]["episodeFileCount"],
|
||||
"episodeCount": series["statistics"]["episodeCount"],
|
||||
"totalEpisodeCount": series["statistics"]["totalEpisodeCount"],
|
||||
"sizeOnDisk": series["statistics"]["sizeOnDisk"],
|
||||
"percentOfEpisodes": series["statistics"]["percentOfEpisodes"],
|
||||
},
|
||||
}
|
||||
|
||||
for season in series["seasons"]:
|
||||
new_season = {
|
||||
"monitored": season["monitored"],
|
||||
"statistics": {
|
||||
"episodeFileCount": season["statistics"]["episodeFileCount"],
|
||||
"episodeCount": season["statistics"]["episodeCount"],
|
||||
"totalEpisodeCount": season["statistics"]["totalEpisodeCount"],
|
||||
"sizeOnDisk": season["statistics"]["sizeOnDisk"],
|
||||
"percentOfEpisodes": season["statistics"]["percentOfEpisodes"],
|
||||
},
|
||||
}
|
||||
if "episodeFileCount" in season.keys():
|
||||
new_season["episodeFileCount"] = season["episodeFileCount"]
|
||||
if "episodeCount" in season.keys():
|
||||
new_season["episodeCount"] = season["episodeCount"]
|
||||
if "totalEpisodeCount" in season.keys():
|
||||
new_season["totalEpisodeCount"] = season["totalEpisodeCount"]
|
||||
new_series["seasons"].append(new_season)
|
||||
new_data.append(new_series)
|
||||
with open("v3_series_new.json", "w") as f:
|
||||
json.dump(new_data, f, indent=4)
|
||||
17
internal/arr/test_fixtures/sonarr/v3_episode.json
Normal file
17
internal/arr/test_fixtures/sonarr/v3_episode.json
Normal file
@ -0,0 +1,17 @@
|
||||
[
|
||||
{
|
||||
"episodeFileId": 0,
|
||||
"hasFile": false,
|
||||
"monitored": false
|
||||
},
|
||||
{
|
||||
"episodeFileId": 0,
|
||||
"hasFile": false,
|
||||
"monitored": true
|
||||
},
|
||||
{
|
||||
"episodeFileId": 0,
|
||||
"hasFile": true,
|
||||
"monitored": true
|
||||
}
|
||||
]
|
||||
35
internal/arr/test_fixtures/sonarr/v3_episodefile.json
Normal file
35
internal/arr/test_fixtures/sonarr/v3_episodefile.json
Normal file
@ -0,0 +1,35 @@
|
||||
[
|
||||
{
|
||||
"size": 3542020991,
|
||||
"quality": {
|
||||
"quality": {
|
||||
"id": 15,
|
||||
"name": "WEBRip-1080p",
|
||||
"source": "webRip",
|
||||
"resolution": 1080
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"size": 3525173623,
|
||||
"quality": {
|
||||
"quality": {
|
||||
"id": 3,
|
||||
"name": "WEBDL-1080p",
|
||||
"source": "web",
|
||||
"resolution": 1080
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"size": 3525173625,
|
||||
"quality": {
|
||||
"quality": {
|
||||
"id": 3,
|
||||
"name": "WEBDL-1080p",
|
||||
"source": "web",
|
||||
"resolution": 1080
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user