126 lines
4.6 KiB
Rust
126 lines
4.6 KiB
Rust
//! arr-compat — the Radarr v3 slice Jellyseerr calls. See DESIGN.md §9.4.
|
|
//!
|
|
//! Jellyseerr already does Jellyfin user auth, discovery and request approval,
|
|
//! none of which this project is trying to solve. So it stays, and it keeps
|
|
//! talking to what it believes is a Radarr. This crate is the translation
|
|
//! layer and nothing else:
|
|
//!
|
|
//! - It maps onto the real domain (`roots`, `movies`, `owners`) and holds no
|
|
//! state of its own.
|
|
//! - It never reaches into `arr-core`, and `arr-core` never learns what a
|
|
//! "quality profile" is. Radarr's vocabulary stops at this crate's edge.
|
|
//! - Where Radarr's model and this one disagree, this one wins and the
|
|
//! difference is absorbed here. Policy attaches to a root (§5.1), so the
|
|
//! quality profile Jellyseerr sends is read, acknowledged and dropped; the
|
|
//! root folder path is what decides which policy a request lands under.
|
|
//!
|
|
//! What is deliberately not here: everything Jellyseerr does not call. This is
|
|
//! not an attempt at Radarr compatibility in general.
|
|
|
|
mod error;
|
|
mod model;
|
|
mod movies;
|
|
mod series;
|
|
mod system;
|
|
|
|
use std::sync::Arc;
|
|
|
|
use arr_db::Db;
|
|
use arr_meta::TmdbClient;
|
|
use axum::routing::get;
|
|
use axum::Router;
|
|
|
|
pub use error::CompatError;
|
|
pub use model::{
|
|
AddMovie, AddOptions, AddSeries, AddSeriesOptions, MovieResource, QualityProfile, RootFolder,
|
|
SeasonResource, SeriesResource, SystemStatus, Tag,
|
|
};
|
|
|
|
/// Everything here hangs off `/api/v3`, the prefix Radarr v3 serves and the
|
|
/// one Jellyseerr appends to whatever URL an operator types into it.
|
|
pub const API_PREFIX: &str = "/api/v3";
|
|
|
|
/// What the shim answers `system/status` with.
|
|
///
|
|
/// Jellyseerr branches on the major version to decide which Radarr dialect to
|
|
/// speak, so this is not decoration: it has to name a real release that serves
|
|
/// `/api/v3`, and it has to stay put.
|
|
pub const RADARR_VERSION: &str = "5.14.0.9383";
|
|
|
|
/// What the shim needs to answer: the real database, and TMDB for `movie/lookup`.
|
|
///
|
|
/// Cheap to clone — [`Db`] is a pool handle and the TMDB client is shared
|
|
/// behind an [`Arc`] so its response cache is not duplicated per clone.
|
|
#[derive(Debug, Clone)]
|
|
pub struct CompatState {
|
|
database: Db,
|
|
tmdb: Option<Arc<TmdbClient>>,
|
|
}
|
|
|
|
impl CompatState {
|
|
/// The shim over a migrated database, with no TMDB client.
|
|
///
|
|
/// Without one `movie/lookup` is the only endpoint that stops working, and
|
|
/// it fails loudly rather than answering an empty list — an empty lookup
|
|
/// looks to Jellyseerr like "TMDB has never heard of this film".
|
|
#[must_use]
|
|
pub fn new(database: Db) -> Self {
|
|
Self {
|
|
database,
|
|
tmdb: None,
|
|
}
|
|
}
|
|
|
|
/// Attach the TMDB client `movie/lookup` searches through.
|
|
#[must_use]
|
|
pub fn with_tmdb(mut self, tmdb: Arc<TmdbClient>) -> Self {
|
|
self.tmdb = Some(tmdb);
|
|
self
|
|
}
|
|
|
|
pub(crate) fn pool(&self) -> &sqlx::SqlitePool {
|
|
self.database.pool()
|
|
}
|
|
|
|
pub(crate) fn database(&self) -> &Db {
|
|
&self.database
|
|
}
|
|
|
|
pub(crate) fn tmdb(&self) -> Result<&TmdbClient, CompatError> {
|
|
self.tmdb
|
|
.as_deref()
|
|
.ok_or_else(|| CompatError::Upstream("TMDB is not configured".into()))
|
|
}
|
|
}
|
|
|
|
/// The shim, mounted under [`API_PREFIX`], ready to `merge` into the app.
|
|
pub fn router(state: CompatState) -> Router {
|
|
Router::new().nest(API_PREFIX, endpoints().with_state(state))
|
|
}
|
|
|
|
/// Radarr runs on ASP.NET, whose routing is case-insensitive, and Jellyseerr
|
|
/// takes advantage: it asks for `qualityProfile` while the documentation says
|
|
/// `qualityprofile`. `axum` matches exactly, so both spellings are registered
|
|
/// rather than lower-casing every incoming path and losing the ability to see
|
|
/// what was actually requested in a trace.
|
|
fn endpoints() -> Router<CompatState> {
|
|
Router::new()
|
|
.route("/system/status", get(system::status))
|
|
.route("/rootfolder", get(system::root_folders))
|
|
.route("/rootFolder", get(system::root_folders))
|
|
.route("/qualityprofile", get(system::quality_profiles))
|
|
.route("/qualityProfile", get(system::quality_profiles))
|
|
.route("/languageprofile", get(system::language_profiles))
|
|
.route("/languageProfile", get(system::language_profiles))
|
|
.route("/tag", get(system::tags))
|
|
.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))
|
|
.route("/series/lookup", get(series::lookup))
|
|
.route("/series/{series_id}", get(series::get))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|