Sonarr v3 shim for Jellyseerr #84
Merged
naps62-yolo
merged 2 commits from 2026-08-22 23:33:09 +01:00
issue/43-sonarr-shim into main
Labels
Clear labels
area/api
area/ci
area/compat
area/core
area/daemon
area/db
area/dl
area/indexer
area/infra
area/meta
area/parse
area/probe
area/subs
area/web
difficulty/easy
difficulty/hard
difficulty/moderate
difficulty/trivial
phase/1-skeleton
phase/2-logic
phase/3-sourcing
phase/4-movies
phase/5-ui
phase/6-tv
phase/7-people
phase/8-compat
phase/9-subtitles
type/bug
type/chore
type/feature
type/test
arr-api
CI and test harness
arr-compat
arr-core
arr-daemon
arr-db
arr-dl
arr-indexer
workspace and tooling
arr-meta
arr-parse
arr-probe
arr-subs crate: providers, extraction, translation, sync
web/
bounded, obvious approach, few files
subtle correctness or cross-cutting
multiple files, judgement, an interface to design
one file, mechanical, no design decisions
workspace, CI, config, database, empty API and SPA
parsing and policy engine, pure, no network
TMDB and Prowlarr, read-only
movies end to end
search, buckets, library views, queues
seasons, episodes, tracking, derived status
owner tags and notifications
Jellyseerr shim
build order: subtitles, DESIGN.md §15
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: yolo/arr#84
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Adds the Sonarr v3 series, lookup, root/profile, language-profile, and tag surface used by Jellyseerr.
Season requests populate TMDB seasons and episodes atomically, then set
wantedonly on episodes in selected seasons. Sonarr vocabulary remains isolated fromarr-core.Closes #43
Reviewed
a2901d4. Findings inline. One that has no single line to sit on: the duplicate check inseries.rs:76runs outside the transaction, so two concurrent adds of the same show hitUNIQUE(tmdb_id)and surface as a 500 instead of the validation error.The season-request mapping itself matches §4.1 — episodes of unselected seasons are inserted with
wanted = 0,auto_trackstays false, and the test proves the wanted set.@@ -108,10 +110,15 @@ fn endpoints() -> Router<CompatState> {.route("/rootFolder", get(system::root_folders)).route("/qualityprofile", get(system::quality_profiles)).route("/qualityProfile", get(system::quality_profiles)).route("/languageprofile", get(system::language_profiles))system/statusanswersappName: "Radarr"and version 5.14.0.9383 on the Sonarr side too.lib.rs:45says Jellyseerr branches on the major version to pick a dialect, and 5.x reads as a v4/v5-era Sonarr — which contradicts this line adding the v3-onlylanguageprofileendpoint.@@ -112,3 +116,4 @@.route("/movie", get(movies::list).post(movies::add)).route("/movie/lookup", get(movies::lookup)).route("/movie/{movie_id}", get(movies::get)).route("/series", get(series::list).post(series::add))No PUT on
/series. A second season request for a show already in the library hits the duplicate check atseries.rs:76and gets a 400 — Sonarr's flow for that case is an update of the existing season list.As written, only the first season request per show works. Implementing the update or stating the limitation in the PR body both settle it.
@@ -337,0 +370,4 @@}impl SeriesResource {pub(crate) fn from_search(hit: &arr_meta::SeriesSearchResult) -> Self {blank()leavesseasons: [], sofrom_searchresults carry no season list —series/lookup?term=tvdb:Nreturns a series with no seasons while thetmdb:branch fills them fromfrom_tmdb.A client that builds its season selection from the lookup result then posts an empty
seasonsarray: series created, nothing marked wanted, no error anywhere. The new test only asserts the season count on thetmdb:branch, so it passes either way. Fetch full detail for tvdb and title hits.@@ -337,0 +409,4 @@title: row.title.clone(),sort_title: row.title.to_lowercase(),year: row.year.unwrap_or_default(),tvdb_id: 0,tvdb_id: 0on every library row, and migration 0005 has no column to store one. SoGET /seriesand every already-added series reporttvdbId: 0— the key Sonarr consumers match a series on.arr_meta::Series.tvdb_idis already fetched at add time; persisting it costs one column. Related:ListQueryfilters ontmdbId, where Sonarr's list filter istvdbId.@@ -337,0 +478,4 @@#[derive(Debug, Clone, Deserialize)]#[serde(rename_all = "camelCase")]pub struct AddSeries {AddSeriesrequirestmdbIdand has notvdbId. Sonarr v3's series resource carries notmdbIdat all — a Sonarr-shaped POST identifies the show bytvdbId, and that body is rejected by the Json extractor with a 422 beforeaddever runs.#43's acceptance is a season request from the live Jellyseerr, so this decides whether the PR meets it. Either accept
tvdbIdand resolve it throughfind_series_by_tvdb, or paste the captured payload showing Jellyseerr echoestmdbIdback from the lookup result.@@ -337,0 +490,4 @@pub quality_profile_id: Option<i64>,#[serde(default)]pub language_profile_id: Option<i64>,#[serde(default)]monitored,seasonFolder,qualityProfileId,languageProfileIdandaddOptionsare read and dropped with no comment.AddMovieright above documents each dropped field and why it is dropped; this struct should follow it.@@ -0,0 +97,4 @@}let mut tx = state.pool().begin().await?;let series_id = sqlx::query(These three inserts use runtime
sqlx::query/query_scalarwhile every SELECT in the file uses the checked macros. CLAUDE.md requires compile-time-checked queries.series.rs:319also parksuse sqlx::Row as _;at the bottom of the file purely to support the resulting.get::<i64, _>(0). Both disappear withquery!/query_scalar!.@@ -0,0 +240,4 @@).fetch_all(state.pool()).await?;let all_seasons = sqlx::query!(load_seriesreads the wholeseasonstable on every call — includingGET /series/{id}, which needs one series' seasons — then filters in Rust, so the cost is O(series × seasons) per request.getlikewise loads every series' tags to take one entry. Both should be constrained by series id in SQL.@@ -17,3 +16,1 @@////// TV roots are excluded rather than merged: they belong to the `series`/// half of the shim, and a Radarr offered a TV root would file films into it./// `GET /api/v3/rootfolder` — the real roots (§5.1).Dropping the
kind == "movie"filter offers TV roots and TV profiles to the Radarr half, and movie roots to the Sonarr half. The comment being deleted here warned about exactly that.Both shims share one
/api/v3mount, so nothing distinguishes the caller, and a wrong pick fails later as a 400 fromresolve_rooton every request. Jellyseerr appends/api/v3to whatever base URL it is given, so mounting the Sonarr surface under its own prefix would let the per-kind filters come back.Reviewed
17262dd. No findings — the new head only mergesorigin/main;arr-compatandarr-metaare byte-identical toa2901d4, so the findings on that review still stand as posted.