-
Notifications
You must be signed in to change notification settings - Fork 3
feat: Sonarr/Radarr scan_source.v1 plugin #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
937c922
feat: scaffold autoscan-arr scan_source plugin
fluxis af523f6
feat(arr): poll history for imports and renames
fluxis ccf86a2
feat(arr): boundary-safe path rewrites
fluxis dc62892
feat: PollChanges returns Silo-native paths from arr history (credent…
fluxis 29e1822
fix(arr): never regress the returned marker on empty polls
fluxis 9f584da
test: end-to-end PollChanges over go-plugin gRPC against stub arr
fluxis 43667d0
docs: README (scan_source contract, credential-from-host, config)
fluxis b3a33e3
fix(arr): apply most-specific path rewrite regardless of order
fluxis 62cec06
fix(arr): clamp a future-dated marker to now
fluxis 60cf349
refactor(arr): return raw source paths; host now owns path rewrites
fluxis 07c78f5
build: depend on merged silo-plugin-sdk via pseudo-version (drop loca…
fluxis 0551dd4
fix(arr): paginate history to avoid 1MiB truncation stall
fluxis 974a786
fix(arr): error on an unparseable marker instead of resetting to now
fluxis 44f21df
fix(deps): bump grpc to v1.81.1 (GHSA-p77j-4mvh-x3m3)
fluxis 95698ef
fix(arr): composite (date,id) marker to avoid dropping same-second hi…
fluxis 9543404
docs: fix README endpoint, build targets, and SDK dependency note
fluxis 463ba3f
docs: add doc comments across the plugin (coverage)
fluxis 9c8835c
fix(arr): preserve composite marker on idle polls; honest page-cap wa…
fluxis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| /plugin | ||
| /dist/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| .PHONY: build test lint clean build-all | ||
|
|
||
| BINARY=plugin | ||
| PLATFORMS=linux/amd64 linux/arm64 darwin/arm64 | ||
| VERSION ?= $(shell git describe --tags --always 2>/dev/null | sed 's/^v//') | ||
| LDFLAGS=-s -w -X main.version=$(VERSION) | ||
|
|
||
| build: | ||
| go build -trimpath -ldflags="$(LDFLAGS)" -o $(BINARY) . | ||
|
|
||
| test: | ||
| go test ./... | ||
|
|
||
| lint: | ||
| golangci-lint run ./... | ||
|
|
||
| clean: | ||
| rm -f $(BINARY) | ||
|
|
||
| build-all: | ||
| @mkdir -p dist | ||
| @for platform in $(PLATFORMS); do \ | ||
| GOOS=$${platform%%/*} GOARCH=$${platform##*/} CGO_ENABLED=0 \ | ||
| go build -trimpath -ldflags="$(LDFLAGS)" -o dist/$(BINARY)-$${platform%%/*}-$${platform##*/} .; \ | ||
| done | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,52 @@ | ||
| # Silo Autoscan ARR Plugin | ||
| # silo-plugin-autoscan-arr | ||
|
|
||
| First-party Silo autoscan plugin for ARR integrations. | ||
| A Silo plugin that implements the **`scan_source.v1`** capability for **Sonarr / Radarr**. | ||
| When Silo's host polls it, the plugin reads the arr instance's recent history, | ||
| extracts imported and renamed file paths, and hands the **raw arr-side paths** back to | ||
| the host. The host applies any configured path rewrites and triggers targeted library | ||
| rescans. | ||
|
|
||
| ## Status | ||
| ## How it works | ||
|
|
||
| Silo's host owns the polling timer and calls `PollChanges(marker, connection)` on | ||
| each tick. The plugin: | ||
|
|
||
| 1. Reads the resolved arr `{base_url, api_key}` from `PollChangesRequest.connection` | ||
| (the host resolves the operator's connection — own credentials or a reused | ||
| Requests link — and passes them per call). **The plugin stores no credentials.** | ||
| 2. Queries arr `/api/v3/history` (paginated: `page` / `pageSize` / | ||
| `sortKey=date` / `sortDirection=descending`), bounded to a 24h max-lookback | ||
| with a 1-minute overlap so boundary events aren't missed and a long-idle | ||
| source doesn't replay full history. | ||
| 3. Extracts `downloadFolderImported` (new files) and `episodeFileRenamed` / | ||
| `movieFileRenamed` (both the new and old path). Deletes are ignored — upgrade | ||
| deletes are covered by the paired import. | ||
| 4. Returns the **raw arr-side paths** in `PollChangesResponse.source_paths` plus an | ||
| opaque `next_marker` (a composite `<RFC3339>|<id>` cursor that never regresses | ||
| below the caller's marker; a bare RFC3339 string is still accepted for backward | ||
| compatibility). **Path rewrites are applied by the host**, not the plugin. | ||
|
|
||
| This repository is an early scaffold. Implementation details, plugin capabilities, and host integration contracts are still being defined. | ||
| ## Configuration | ||
|
|
||
| ## Development | ||
| The plugin has **no configuration**. `global_config_schema` is empty (`[]`). | ||
|
|
||
| The arr connection (URL + API key) is supplied by the host on every poll via | ||
| `PollChangesRequest.connection` — it is never stored by the plugin. Path rewrites are | ||
| configured in the host and applied after the plugin returns source paths. | ||
|
|
||
| ## Building | ||
|
|
||
| ```sh | ||
| go test ./... | ||
| go build . | ||
| make build # build the binary for the current platform | ||
| make build-all # cross-platform binaries (dist/) | ||
| go test ./... # unit tests | ||
| go test -tags integration ./... # spawns the real binary over go-plugin gRPC | ||
| ``` | ||
|
|
||
| ## License | ||
| ## Status | ||
|
|
||
| License details will be added before the first release. | ||
| Depends on `silo-plugin-sdk` ≥ the release that adds `scan_source.v1` with the | ||
| `connection` field on `PollChangesRequest` and `source_paths` on `PollChangesResponse`. | ||
| `go.mod` depends on the SDK via a pseudo-version (no local `replace`); bump it to a | ||
| tagged release once one is published. Catalog registration in `silo-plugins` is a | ||
| separate step. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| //go:build integration | ||
|
|
||
| // End-to-end test: builds the real plugin binary, spawns it over go-plugin | ||
| // (the same handshake silo's host uses), and drives PollChanges across the | ||
| // gRPC boundary against a stub arr — exercising the scan_source.v1 contract | ||
| // including the resolved-connection delivery. Run: go test -tags integration . | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "testing" | ||
| "time" | ||
|
|
||
| plugin "github.com/hashicorp/go-plugin" | ||
|
|
||
| pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" | ||
| "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/runtime" | ||
| ) | ||
|
|
||
| func TestEndToEndPollChangesOverGRPC(t *testing.T) { | ||
| const apiKey = "secret-key" | ||
| const importedPath = "/data/media/tv/Show/Season 01/Show - S01E01.mkv" | ||
|
|
||
| gotAPIKey := make(chan string, 1) | ||
| stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| select { | ||
| case gotAPIKey <- r.Header.Get("X-Api-Key"): | ||
| default: | ||
| } | ||
| // Compute a fresh "just now" date on every request so the record always falls | ||
| // within the paged query window (querySince = now - overlap) regardless of how | ||
| // long the binary build took before the first poll arrives. | ||
| freshDate := time.Now().UTC().Add(-10 * time.Second).Format(time.RFC3339) | ||
| // Return the paged envelope that the plugin now expects from /api/v3/history. | ||
| _, _ = w.Write([]byte(`{"page":1,"pageSize":200,"totalRecords":2,"records":[ | ||
| {"eventType":"downloadFolderImported","date":"` + freshDate + `","data":{"importedPath":"` + importedPath + `"}}, | ||
| {"eventType":"grabbed","date":"` + freshDate + `","data":{"importedPath":"/ignored"}} | ||
| ]}`)) | ||
| })) | ||
| defer stub.Close() | ||
|
|
||
| // Build the real plugin binary (uses this module's go.mod + SDK replace). | ||
| bin := filepath.Join(t.TempDir(), "autoscan-arr") | ||
| if out, err := exec.Command("go", "build", "-o", bin, ".").CombinedOutput(); err != nil { | ||
| t.Fatalf("build plugin: %v\n%s", err, out) | ||
| } | ||
|
|
||
| // Spawn it exactly as the host does. | ||
| client := plugin.NewClient(&plugin.ClientConfig{ | ||
| HandshakeConfig: runtime.HandshakeConfig(), | ||
| Plugins: runtime.DefaultPluginSet(runtime.CapabilityServers{}), | ||
| Cmd: exec.Command(bin), | ||
| AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, | ||
| }) | ||
| defer client.Kill() | ||
|
|
||
| rpc, err := client.Client() | ||
| if err != nil { | ||
| t.Fatalf("connect to plugin: %v", err) | ||
| } | ||
| raw, err := rpc.Dispense(runtime.PluginSetName) | ||
| if err != nil { | ||
| t.Fatalf("dispense: %v", err) | ||
| } | ||
| rt, ok := raw.(*runtime.Client) | ||
| if !ok { | ||
| t.Fatalf("dispensed %T, want *runtime.Client", raw) | ||
| } | ||
|
|
||
| ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | ||
| defer cancel() | ||
|
|
||
| // First poll: empty marker => "from now"; connection carries the creds. | ||
| resp, err := rt.ScanSource().PollChanges(ctx, &pluginv1.PollChangesRequest{ | ||
| CapabilityId: "arr", | ||
| Connection: &pluginv1.ResolvedConnection{BaseUrl: stub.URL, ApiKey: apiKey}, | ||
| }) | ||
| if err != nil { | ||
| t.Fatalf("PollChanges: %v", err) | ||
| } | ||
|
|
||
| // The plugin must have reached the stub using the api key from the request. | ||
| select { | ||
| case k := <-gotAPIKey: | ||
| if k != apiKey { | ||
| t.Fatalf("arr received X-Api-Key %q, want %q", k, apiKey) | ||
| } | ||
| case <-time.After(5 * time.Second): | ||
| t.Fatal("plugin never called the stub arr") | ||
| } | ||
|
|
||
| // The raw arr-side imported path must come back (grabbed ignored); no | ||
| // rewriting applied — the host owns that. Marker non-empty. | ||
| var found bool | ||
| for _, p := range resp.GetSourcePaths() { | ||
| if p == importedPath { | ||
| found = true | ||
| } | ||
| } | ||
| if !found { | ||
| t.Fatalf("imported path missing from source_paths %v", resp.GetSourcePaths()) | ||
| } | ||
| if resp.GetNextMarker() == "" { | ||
| t.Fatal("expected a non-empty next_marker") | ||
| } | ||
|
|
||
| // Nil connection must be rejected (no creds to poll with). | ||
| if _, err := rt.ScanSource().PollChanges(ctx, &pluginv1.PollChangesRequest{CapabilityId: "arr"}); err == nil { | ||
| t.Fatal("PollChanges with no connection should error") | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| module github.com/Silo-Server/silo-plugin-autoscan-arr | ||
|
|
||
| go 1.26.3 | ||
|
|
||
| require ( | ||
| github.com/Silo-Server/silo-plugin-sdk v0.4.1-0.20260603030807-807b07e785b2 | ||
| github.com/hashicorp/go-plugin v1.8.0 | ||
| ) | ||
|
|
||
| require ( | ||
| github.com/fatih/color v1.13.0 // indirect | ||
| github.com/golang/protobuf v1.5.4 // indirect | ||
| github.com/hashicorp/go-hclog v1.6.3 // indirect | ||
| github.com/hashicorp/yamux v0.1.2 // indirect | ||
| github.com/mattn/go-colorable v0.1.12 // indirect | ||
| github.com/mattn/go-isatty v0.0.17 // indirect | ||
| github.com/oklog/run v1.1.0 // indirect | ||
| golang.org/x/net v0.51.0 // indirect | ||
| golang.org/x/sys v0.42.0 // indirect | ||
| golang.org/x/text v0.34.0 // indirect | ||
| google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect | ||
| google.golang.org/grpc v1.81.1 // indirect | ||
| google.golang.org/protobuf v1.36.11 // indirect | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| github.com/Silo-Server/silo-plugin-sdk v0.4.1-0.20260603030807-807b07e785b2 h1:t7pdSay39b6doyYhT+GiMWUMW1sTtj5Xw8k5fBPxiCA= | ||
| github.com/Silo-Server/silo-plugin-sdk v0.4.1-0.20260603030807-807b07e785b2/go.mod h1:etqmxLTwjxpFH9goAjBDfNDoqHMv2/sqUXu8yx3hNfA= | ||
| github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= | ||
| github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= | ||
| 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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
| github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= | ||
| github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= | ||
| github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= | ||
| github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= | ||
| github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= | ||
| github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= | ||
| github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= | ||
| github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= | ||
| 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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||
| github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||
| github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= | ||
| github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= | ||
| github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs= | ||
| github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= | ||
| github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= | ||
| github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= | ||
| github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= | ||
| github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= | ||
| github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= | ||
| github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= | ||
| github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= | ||
| github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= | ||
| github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= | ||
| github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= | ||
| github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= | ||
| github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= | ||
| github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= | ||
| 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||
| github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= | ||
| github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= | ||
| go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= | ||
| go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= | ||
| go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= | ||
| go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= | ||
| go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= | ||
| go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= | ||
| go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= | ||
| go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= | ||
| go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= | ||
| go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= | ||
| go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= | ||
| go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= | ||
| golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= | ||
| golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= | ||
| golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
| golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
| golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= | ||
| golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= | ||
| golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= | ||
| golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= | ||
| gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= | ||
| gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= | ||
| google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= | ||
| google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= | ||
| google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= | ||
| google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= | ||
| google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= | ||
| google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= | ||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| // Package arr polls a Sonarr/Radarr instance's history for recently-changed | ||
| // file paths. Paths are returned as-is (raw arr-side); the host applies rewrites. | ||
| package arr | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "strings" | ||
| "time" | ||
| ) | ||
|
|
||
| // maxResponseBody caps the arr history response read to 1 MiB. | ||
| const maxResponseBody = 1 << 20 | ||
|
|
||
| // defaultTimeout bounds a single arr HTTP request. | ||
| const defaultTimeout = 30 * time.Second | ||
|
|
||
| // client is a minimal arr HTTP client. It authenticates with the X-Api-Key | ||
| // header and caps the response body. Credentials are passed per call (the | ||
| // plugin stores none). | ||
| type client struct { | ||
| baseURL string | ||
| apiKey string | ||
| httpClient *http.Client | ||
| } | ||
|
|
||
| // newClient builds an arr HTTP client. The base URL is trimmed of trailing | ||
| // slashes and surrounding whitespace, the api key is trimmed, and a default | ||
| // 30s-timeout http.Client is used when httpClient is nil (tests inject their | ||
| // own). | ||
| func newClient(baseURL, apiKey string, httpClient *http.Client) *client { | ||
| if httpClient == nil { | ||
| httpClient = &http.Client{Timeout: defaultTimeout} | ||
| } | ||
| return &client{ | ||
| baseURL: strings.TrimRight(strings.TrimSpace(baseURL), "/"), | ||
| apiKey: strings.TrimSpace(apiKey), | ||
| httpClient: httpClient, | ||
| } | ||
| } | ||
|
|
||
| // getJSON issues a GET against the arr API and decodes the JSON response into | ||
| // dest. The response body is limited to maxResponseBody bytes. | ||
| func (c *client) getJSON(ctx context.Context, path string, dest any) error { | ||
| if c.baseURL == "" { | ||
| return fmt.Errorf("arr: base url is required") | ||
| } | ||
| if c.apiKey == "" { | ||
| return fmt.Errorf("arr: api key is required") | ||
| } | ||
|
|
||
| req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil) | ||
| if err != nil { | ||
| return fmt.Errorf("arr: create request: %w", err) | ||
| } | ||
| req.Header.Set("Accept", "application/json") | ||
| req.Header.Set("X-Api-Key", c.apiKey) | ||
|
|
||
| resp, err := c.httpClient.Do(req) | ||
| if err != nil { | ||
| return fmt.Errorf("arr: request failed: %w", err) | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode >= 400 { | ||
| body, _ := io.ReadAll(io.LimitReader(resp.Body, maxResponseBody)) | ||
| return fmt.Errorf("arr: HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) | ||
| } | ||
| if err := json.NewDecoder(io.LimitReader(resp.Body, maxResponseBody)).Decode(dest); err != nil { | ||
| return fmt.Errorf("arr: decode response: %w", err) | ||
| } | ||
| return nil | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.