feat: indexers and download clients you add and configure (M502 #5518)
release / govulncheck (push) Successful in 34s
release / web (push) Successful in 1m13s
release / go (push) Successful in 2m12s
release / integration (push) Successful in 4m6s
release / Build signed APK (releases and dev) (push) Successful in 8m41s
release / android (push) Successful in 9m47s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 59s
release / Verify release artifacts (tag releases only) (push) Skipped

Admin → Integrations gains "Indexers and download clients": two lists of
connections the operator adds, Fabled Librarian's model. Nothing is seeded
or copied from Lidarr.

- Indexers: Prowlarr (its native /api/v1/search, which asks every indexer
  at once; Prowlarr has no aggregate Torznab feed) and any Torznab feed.
- Download clients: qBittorrent (WebUI v2, with re-login on an expired
  session) and FabledExchange, one entry with a switch per job:
  downloads (/api/downloads), search (/api/search) and release details
  (/api/torrents/{infohash}). All three start on; its key is optional.
- Each entry: enable, name, address, a write-only key or password, Test.
  A download client's Test lists its torrents and checks the downloads
  folder is visible and writable from Minstrel; an Exchange's Test
  reports each job switched on.
- Download clients carry a path mapping (Minstrel's /downloads vs the
  client's own name for it); search sources carry searches per hour.

Migration 0079 adds acquisition_services with its CHECKs. Ported from
Fabled Librarian: internal/indexer (Torznab, 429 Retry-After, magnet
redirects), internal/prowlarr, internal/exchange, internal/downloader,
and internal/acquire's one builder per kind, which Test already uses
so a passing Test vouches for the path a grab will take.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 20:34:51 -04:00
co-authored by Claude Opus 5.5
parent 5306a261c1
commit a2e1890d4c
35 changed files with 4966 additions and 2 deletions
+76
View File
@@ -0,0 +1,76 @@
-- Indexers and download clients (M502 #5518).
-- name: ListAcquisitionServices :many
-- Returns no secret, only whether one is set: this feeds the settings page.
SELECT code, kind, protocol, label, enabled, base_url,
(api_key <> '')::boolean AS has_api_key,
(password <> '')::boolean AS has_password,
username, category, timeout_ms,
serves_search, serves_downloads, serves_enrichment,
local_path, remote_path, searches_per_hour,
created_at, updated_at
FROM acquisition_services
ORDER BY kind, label, code;
-- name: GetAcquisitionService :one
-- The full row, credentials included. For building a client, never for a
-- response.
SELECT * FROM acquisition_services WHERE code = sqlc.arg(code);
-- name: ListUsableAcquisitionServices :many
-- Every enabled, addressed service doing one job. An exchange row does a job
-- when its switch for it is on; any other row does the job its kind names.
-- Ordered by code so a fan-out is deterministic.
SELECT * FROM acquisition_services
WHERE enabled AND base_url <> ''
AND CASE WHEN protocol = 'exchange' THEN
CASE sqlc.arg(job)::text
WHEN 'search' THEN serves_search
WHEN 'download' THEN serves_downloads
WHEN 'enrichment' THEN serves_enrichment
ELSE false
END
ELSE kind = sqlc.arg(job)::text
END
ORDER BY code;
-- name: CreateAcquisitionService :one
-- ON CONFLICT so the caller can try the next code when this one is taken.
INSERT INTO acquisition_services
(code, kind, protocol, label, category, timeout_ms, searches_per_hour)
VALUES
(sqlc.arg(code), sqlc.arg(kind), sqlc.arg(protocol), sqlc.arg(label),
sqlc.arg(category), sqlc.arg(timeout_ms), sqlc.arg(searches_per_hour))
ON CONFLICT (code) DO NOTHING
RETURNING *;
-- name: UpdateAcquisitionService :one
-- Everything on the settings form except the credentials, which have their
-- own write-only calls.
UPDATE acquisition_services SET
label = sqlc.arg(label),
enabled = sqlc.arg(enabled),
base_url = sqlc.arg(base_url),
username = sqlc.arg(username),
category = sqlc.arg(category),
timeout_ms = sqlc.arg(timeout_ms),
serves_search = sqlc.arg(serves_search),
serves_downloads = sqlc.arg(serves_downloads),
serves_enrichment = sqlc.arg(serves_enrichment),
local_path = sqlc.arg(local_path),
remote_path = sqlc.arg(remote_path),
searches_per_hour = sqlc.arg(searches_per_hour),
updated_at = now()
WHERE code = sqlc.arg(code)
RETURNING *;
-- name: DeleteAcquisitionService :execrows
DELETE FROM acquisition_services WHERE code = sqlc.arg(code);
-- name: SetAcquisitionServiceAPIKey :execrows
UPDATE acquisition_services SET api_key = sqlc.arg(api_key), updated_at = now()
WHERE code = sqlc.arg(code);
-- name: SetAcquisitionServicePassword :execrows
UPDATE acquisition_services SET password = sqlc.arg(password), updated_at = now()
WHERE code = sqlc.arg(code);