diff options
Diffstat (limited to 'server/src')
36 files changed, 256 insertions, 601 deletions
diff --git a/server/src/api.rs b/server/src/api.rs index fe68b1a..45bcd90 100644 --- a/server/src/api.rs +++ b/server/src/api.rs @@ -4,7 +4,6 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ use super::ui::error::MyResult; -use crate::helper::A; use rocket::{get, post, response::Redirect, serde::json::Json}; use serde_json::{Value, json}; diff --git a/server/src/compat/jellyfin/mod.rs b/server/src/compat/jellyfin/mod.rs index db60530..8fa44cb 100644 --- a/server/src/compat/jellyfin/mod.rs +++ b/server/src/compat/jellyfin/mod.rs @@ -5,7 +5,7 @@ */ pub mod models; -use crate::{helper::A, ui::error::MyResult}; +use crate::{request_helpers::A, ui::error::MyResult}; use anyhow::anyhow; use jellycommon::{ api::{NodeFilterSort, SortOrder, SortProperty}, diff --git a/server/src/compat/mod.rs b/server/src/compat/mod.rs index 85fb566..859b60a 100644 --- a/server/src/compat/mod.rs +++ b/server/src/compat/mod.rs @@ -3,5 +3,5 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -pub mod jellyfin; +// pub mod jellyfin; pub mod youtube; diff --git a/server/src/compat/youtube.rs b/server/src/compat/youtube.rs index ef9e09d..5e86014 100644 --- a/server/src/compat/youtube.rs +++ b/server/src/compat/youtube.rs @@ -3,7 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::{helper::A, ui::error::MyResult}; +use crate::{request_info::A, ui::error::MyResult}; use anyhow::anyhow; use jellycommon::{ routes::{u_node_slug, u_node_slug_player}, diff --git a/server/src/config.rs b/server/src/config.rs index f552306..73d6b73 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -6,20 +6,33 @@ use anyhow::{Context, Result, anyhow}; use jellycache::init_cache; -use jellylogic::init_database; use serde::Deserialize; -use std::env::{args, var}; +use std::{ + env::{args, var}, + path::PathBuf, + sync::{LazyLock, Mutex}, +}; use tokio::fs::read_to_string; +static CONF_PRELOAD: Mutex<Option<AllConfigs>> = Mutex::new(None); +pub static CONF: LazyLock<AllConfigs> = + LazyLock::new(|| CONF_PRELOAD.lock().unwrap().take().unwrap()); + +pub struct AllConfigs { + pub ui: jellyui::Config, + pub transcoder: jellytranscoder::Config, + pub stream: jellystream::Config, + pub cache: jellycache::Config, + pub import: jellyimport::Config, + pub server: Config, +} + #[derive(Debug, Deserialize)] -struct Config { - transcoder: jellytranscoder::Config, - ui: jellyui::Config, - stream: jellystream::Config, - cache: jellycache::Config, - server: crate::Config, - logic: jellylogic::Config, - import: jellyimport::Config, +pub struct Config { + pub asset_path: PathBuf, + pub cookie_key: Option<String>, + pub tls: bool, + pub hostname: String, } pub async fn load_config() -> Result<()> { @@ -30,19 +43,17 @@ pub async fn load_config() -> Result<()> { "No config supplied. Use first argument or JELLYTHING_CONFIG environment variable." ))?; - let config_raw = read_to_string(path).await.context("reading main config")?; - let config: Config = serde_yaml_ng::from_str(&config_raw).context("parsing main config")?; - - *jellystream::CONF_PRELOAD.lock().unwrap() = Some(config.stream); - *jellytranscoder::CONF_PRELOAD.lock().unwrap() = Some(config.transcoder); - *jellycache::CONF_PRELOAD.lock().unwrap() = Some(config.cache); - *jellylogic::CONF_PRELOAD.lock().unwrap() = Some(config.logic); - *jellyimport::CONF_PRELOAD.lock().unwrap() = Some(config.import); - *crate::CONF_PRELOAD.lock().unwrap() = Some(config.server); - *jellyui::CONF_PRELOAD.lock().unwrap() = Some(config.ui); + let config = read_to_string(path).await.context("reading main config")?; + *CONF_PRELOAD.lock().unwrap() = Some(AllConfigs { + ui: serde_yaml_ng::from_str(&config).context("ui config")?, + transcoder: serde_yaml_ng::from_str(&config).context("transcoder config")?, + stream: serde_yaml_ng::from_str(&config).context("stream config")?, + cache: serde_yaml_ng::from_str(&config).context("cache config")?, + import: serde_yaml_ng::from_str(&config).context("import config")?, + server: serde_yaml_ng::from_str(&config).context("server config")?, + }); init_cache()?; - init_database()?; Ok(()) } diff --git a/server/src/helper/accept.rs b/server/src/helper/accept.rs deleted file mode 100644 index cbbc843..0000000 --- a/server/src/helper/accept.rs +++ /dev/null @@ -1,72 +0,0 @@ -/* - This file is part of jellything (https://codeberg.org/metamuffin/jellything) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2026 metamuffin <metamuffin.org> -*/ -use rocket::{ - http::MediaType, - outcome::Outcome, - request::{self, FromRequest}, - Request, -}; -use std::ops::Deref; - -#[derive(Debug, Default)] -pub enum Accept { - #[default] - Other, - Json, - Image, - Media, - Html, -} -impl Accept { - pub fn from_request_ut(request: &Request) -> Self { - if let Some(a) = request.accept() { - if a.preferred().exact_eq(&MediaType::JSON) { - Accept::Json - } else { - Accept::Other - } - } else { - Accept::Other - } - } - - pub fn is_json(&self) -> bool { - matches!(self, Self::Json) - } -} - -pub struct AcceptJson(bool); -impl Deref for AcceptJson { - type Target = bool; - fn deref(&self) -> &Self::Target { - &self.0 - } -} -impl<'r> FromRequest<'r> for AcceptJson { - type Error = (); - - fn from_request<'life0, 'async_trait>( - request: &'r Request<'life0>, - ) -> ::core::pin::Pin< - Box< - dyn ::core::future::Future<Output = request::Outcome<Self, Self::Error>> - + ::core::marker::Send - + 'async_trait, - >, - > - where - 'r: 'async_trait, - 'life0: 'async_trait, - Self: 'async_trait, - { - Box::pin(async move { - Outcome::Success(AcceptJson(matches!( - Accept::from_request_ut(request), - Accept::Json - ))) - }) - } -} diff --git a/server/src/helper/asset.rs b/server/src/helper/asset.rs deleted file mode 100644 index ac58687..0000000 --- a/server/src/helper/asset.rs +++ /dev/null @@ -1,37 +0,0 @@ -/* - This file is part of jellything (https://codeberg.org/metamuffin/jellything) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2026 metamuffin <metamuffin.org> -*/ - -use crate::helper::A; -use jellycommon::Asset; -use rocket::{ - http::uri::fmt::{FromUriParam, Path, UriDisplay}, - request::{FromParam, FromSegments}, -}; -use std::fmt::Write; - -impl<'a> FromParam<'a> for A<Asset> { - type Error = (); - fn from_param(param: &'a str) -> Result<Self, Self::Error> { - Ok(A(Asset(param.to_owned()))) - } -} -impl UriDisplay<Path> for A<Asset> { - fn fmt(&self, f: &mut rocket::http::uri::fmt::Formatter<'_, Path>) -> std::fmt::Result { - write!(f, "{}", self.0 .0) - } -} -impl FromUriParam<Path, Asset> for A<Asset> { - type Target = A<Asset>; - fn from_uri_param(param: Asset) -> Self::Target { - A(param) - } -} -impl<'r> FromSegments<'r> for A<Asset> { - type Error = (); - fn from_segments(segments: rocket::http::uri::Segments<'r, Path>) -> Result<Self, Self::Error> { - Ok(A(Asset(segments.collect::<Vec<_>>().join("/")))) - } -} diff --git a/server/src/helper/filter_sort.rs b/server/src/helper/filter_sort.rs deleted file mode 100644 index 7d66b38..0000000 --- a/server/src/helper/filter_sort.rs +++ /dev/null @@ -1,162 +0,0 @@ -/* - This file is part of jellything (https://codeberg.org/metamuffin/jellything) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2026 metamuffin <metamuffin.org> -*/ - -use super::A; -use jellycommon::{ - api::{FilterProperty, NodeFilterSort, SortOrder, SortProperty}, - user::{PlayerKind, Theme}, -}; -use rocket::{ - async_trait, - form::{DataField, FromForm, FromFormField, Result, ValueField}, - UriDisplayQuery, -}; - -impl From<ANodeFilterSort> for NodeFilterSort { - fn from(val: ANodeFilterSort) -> Self { - NodeFilterSort { - sort_by: val.sort_by.map(|e| match e { - ASortProperty::ReleaseDate => SortProperty::ReleaseDate, - ASortProperty::Title => SortProperty::Title, - ASortProperty::Index => SortProperty::Index, - ASortProperty::Duration => SortProperty::Duration, - ASortProperty::RatingRottenTomatoes => SortProperty::RatingRottenTomatoes, - ASortProperty::RatingMetacritic => SortProperty::RatingMetacritic, - ASortProperty::RatingImdb => SortProperty::RatingImdb, - ASortProperty::RatingTmdb => SortProperty::RatingTmdb, - ASortProperty::RatingYoutubeViews => SortProperty::RatingYoutubeViews, - ASortProperty::RatingYoutubeLikes => SortProperty::RatingYoutubeLikes, - ASortProperty::RatingYoutubeFollowers => SortProperty::RatingYoutubeFollowers, - ASortProperty::RatingUser => SortProperty::RatingUser, - ASortProperty::RatingLikesDivViews => SortProperty::RatingLikesDivViews, - }), - filter_kind: val.filter_kind.map(|l| { - l.into_iter() - .map(|e| match e { - AFilterProperty::FederationLocal => FilterProperty::FederationLocal, - AFilterProperty::FederationRemote => FilterProperty::FederationRemote, - AFilterProperty::Watched => FilterProperty::Watched, - AFilterProperty::Unwatched => FilterProperty::Unwatched, - AFilterProperty::WatchProgress => FilterProperty::WatchProgress, - AFilterProperty::KindMovie => FilterProperty::KindMovie, - AFilterProperty::KindVideo => FilterProperty::KindVideo, - AFilterProperty::KindShortFormVideo => FilterProperty::KindShortFormVideo, - AFilterProperty::KindMusic => FilterProperty::KindMusic, - AFilterProperty::KindCollection => FilterProperty::KindCollection, - AFilterProperty::KindChannel => FilterProperty::KindChannel, - AFilterProperty::KindShow => FilterProperty::KindShow, - AFilterProperty::KindSeries => FilterProperty::KindSeries, - AFilterProperty::KindSeason => FilterProperty::KindSeason, - AFilterProperty::KindEpisode => FilterProperty::KindEpisode, - }) - .collect() - }), - sort_order: val.sort_order.map(|e| match e { - ASortOrder::Ascending => SortOrder::Ascending, - ASortOrder::Descending => SortOrder::Descending, - }), - } - } -} - -#[async_trait] -impl<'v> FromFormField<'v> for A<Theme> { - fn from_value(field: ValueField<'v>) -> Result<'v, Self> { - Err(field.unexpected())? - } - async fn from_data(field: DataField<'v, '_>) -> Result<'v, Self> { - Err(field.unexpected())? - } -} - -#[async_trait] -impl<'v> FromFormField<'v> for A<PlayerKind> { - fn from_value(field: ValueField<'v>) -> Result<'v, Self> { - Err(field.unexpected())? - } - async fn from_data(field: DataField<'v, '_>) -> Result<'v, Self> { - Err(field.unexpected())? - } -} - -#[derive(FromForm, UriDisplayQuery, Clone)] -pub struct ANodeFilterSort { - sort_by: Option<ASortProperty>, - filter_kind: Option<Vec<AFilterProperty>>, - sort_order: Option<ASortOrder>, -} - -#[derive(FromFormField, UriDisplayQuery, Clone)] -enum AFilterProperty { - #[field(value = "fed_local")] - FederationLocal, - #[field(value = "fed_remote")] - FederationRemote, - #[field(value = "watched")] - Watched, - #[field(value = "unwatched")] - Unwatched, - #[field(value = "watch_progress")] - WatchProgress, - #[field(value = "kind_movie")] - KindMovie, - #[field(value = "kind_video")] - KindVideo, - #[field(value = "kind_short_form_video")] - KindShortFormVideo, - #[field(value = "kind_music")] - KindMusic, - #[field(value = "kind_collection")] - KindCollection, - #[field(value = "kind_channel")] - KindChannel, - #[field(value = "kind_show")] - KindShow, - #[field(value = "kind_series")] - KindSeries, - #[field(value = "kind_season")] - KindSeason, - #[field(value = "kind_episode")] - KindEpisode, -} - -#[derive(FromFormField, UriDisplayQuery, Clone)] -enum ASortProperty { - #[field(value = "release_date")] - ReleaseDate, - #[field(value = "title")] - Title, - #[field(value = "index")] - Index, - #[field(value = "duration")] - Duration, - #[field(value = "rating_rt")] - RatingRottenTomatoes, - #[field(value = "rating_mc")] - RatingMetacritic, - #[field(value = "rating_imdb")] - RatingImdb, - #[field(value = "rating_tmdb")] - RatingTmdb, - #[field(value = "rating_yt_views")] - RatingYoutubeViews, - #[field(value = "rating_yt_likes")] - RatingYoutubeLikes, - #[field(value = "rating_yt_followers")] - RatingYoutubeFollowers, - #[field(value = "rating_user")] - RatingUser, - #[field(value = "rating_loved")] - RatingLikesDivViews, -} - -#[derive(FromFormField, UriDisplayQuery, Clone)] -enum ASortOrder { - #[field(value = "ascending")] - Ascending, - #[field(value = "descending")] - Descending, -} diff --git a/server/src/helper/language.rs b/server/src/helper/language.rs deleted file mode 100644 index e106e12..0000000 --- a/server/src/helper/language.rs +++ /dev/null @@ -1,60 +0,0 @@ -/* - This file is part of jellything (https://codeberg.org/metamuffin/jellything) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2026 metamuffin <metamuffin.org> -*/ -use jellyui::locale::Language; -use rocket::{ - outcome::Outcome, - request::{self, FromRequest}, - Request, -}; -use std::ops::Deref; - -pub struct AcceptLanguage(pub Language); -impl Deref for AcceptLanguage { - type Target = Language; - fn deref(&self) -> &Self::Target { - &self.0 - } -} -impl<'r> FromRequest<'r> for AcceptLanguage { - type Error = (); - - fn from_request<'life0, 'async_trait>( - request: &'r Request<'life0>, - ) -> ::core::pin::Pin< - Box< - dyn ::core::future::Future<Output = request::Outcome<Self, Self::Error>> - + ::core::marker::Send - + 'async_trait, - >, - > - where - 'r: 'async_trait, - 'life0: 'async_trait, - Self: 'async_trait, - { - Box::pin(async move { Outcome::Success(AcceptLanguage(lang_from_request(request))) }) - } -} - -pub(crate) fn lang_from_request(request: &Request) -> Language { - request - .headers() - .get_one("accept-language") - .and_then(|h| { - h.split(",") - .filter_map(|e| { - let code = e.split(";").next()?; - let code = code.split_once("-").unwrap_or((code, "")).0; - match code { - "en" => Some(Language::English), - "de" => Some(Language::German), - _ => None, - } - }) - .next() - }) - .unwrap_or(Language::English) -} diff --git a/server/src/helper/mod.rs b/server/src/helper/mod.rs deleted file mode 100644 index f52fcac..0000000 --- a/server/src/helper/mod.rs +++ /dev/null @@ -1,69 +0,0 @@ -/* - This file is part of jellything (https://codeberg.org/metamuffin/jellything) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2026 metamuffin <metamuffin.org> -*/ -pub mod accept; -pub mod cache; -pub mod cors; -pub mod filter_sort; -pub mod language; -pub mod node_id; -pub mod session; -pub mod asset; - -use crate::ui::error::{MyError, MyResult}; -use accept::Accept; -use jellyimport::is_importing; -use jellylogic::session::Session; -use jellyui::{ - locale::Language, - scaffold::{RenderInfo, SessionInfo}, -}; -use language::lang_from_request; -use rocket::{ - async_trait, - http::Status, - request::{FromRequest, Outcome}, - Request, -}; -use session::session_from_request; - -#[derive(Debug, Clone, Copy, Default)] -pub struct A<T>(pub T); - -pub struct RequestInfo { - pub lang: Language, - pub accept: Accept, - pub session: Session, -} - -impl RequestInfo { - pub async fn from_request_ut(request: &Request<'_>) -> MyResult<Self> { - Ok(Self { - lang: lang_from_request(request), - accept: Accept::from_request_ut(request), - session: session_from_request(request).await?, - }) - } - pub fn render_info(&self) -> RenderInfo { - RenderInfo { - importing: is_importing(), - session: Some(SessionInfo { - user: self.session.user.clone(), // TODO no clone? - }), - lang: self.lang, - } - } -} - -#[async_trait] -impl<'r> FromRequest<'r> for RequestInfo { - type Error = MyError; - async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> { - match Self::from_request_ut(request).await { - Ok(a) => Outcome::Success(a), - Err(a) => Outcome::Error((Status::BadRequest, a)), - } - } -} diff --git a/server/src/helper/node_id.rs b/server/src/helper/node_id.rs deleted file mode 100644 index 5c2e52a..0000000 --- a/server/src/helper/node_id.rs +++ /dev/null @@ -1,17 +0,0 @@ -/* - This file is part of jellything (https://codeberg.org/metamuffin/jellything) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2026 metamuffin <metamuffin.org> -*/ - -use super::A; -use jellycommon::NodeID; -use rocket::request::FromParam; -use std::str::FromStr; - -impl<'a> FromParam<'a> for A<NodeID> { - type Error = (); - fn from_param(param: &'a str) -> Result<Self, Self::Error> { - NodeID::from_str(param).map_err(|_| ()).map(A) - } -} diff --git a/server/src/helper/session.rs b/server/src/helper/session.rs deleted file mode 100644 index 61f6d66..0000000 --- a/server/src/helper/session.rs +++ /dev/null @@ -1,67 +0,0 @@ -/* - This file is part of jellything (https://codeberg.org/metamuffin/jellything) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2026 metamuffin <metamuffin.org> -*/ -use super::A; -use crate::ui::error::MyError; -use anyhow::anyhow; -use jellylogic::session::{bypass_auth_session, token_to_session, Session}; -use log::warn; -use rocket::{ - async_trait, - http::Status, - outcome::Outcome, - request::{self, FromRequest}, - Request, -}; - -pub(super) async fn session_from_request(req: &Request<'_>) -> Result<Session, MyError> { - if cfg!(feature = "bypass-auth") { - Ok(bypass_auth_session()?) - } else { - let token = req - .query_value("session") - .map(|e| e.unwrap()) - .or_else(|| req.query_value("api_key").map(|e| e.unwrap())) - .or_else(|| req.headers().get_one("X-MediaBrowser-Token")) - .or_else(|| { - req.headers() - .get_one("Authorization") - .and_then(parse_jellyfin_auth) - }) // for jellyfin compat - .or(req.cookies().get("session").map(|cookie| cookie.value())) - .ok_or(anyhow!("not logged in"))?; - - // jellyfin urlescapes the token for *some* requests - let token = token.replace("%3D", "="); - Ok(token_to_session(&token)?) - } -} - -fn parse_jellyfin_auth(h: &str) -> Option<&str> { - for tok in h.split(" ") { - if let Some(tok) = tok.strip_prefix("Token=\"") { - if let Some(tok) = tok.strip_suffix("\"") { - return Some(tok); - } - } - } - None -} - -#[async_trait] -impl<'r> FromRequest<'r> for A<Session> { - type Error = MyError; - async fn from_request<'life0>( - request: &'r Request<'life0>, - ) -> request::Outcome<Self, Self::Error> { - match session_from_request(request).await { - Ok(x) => Outcome::Success(A(x)), - Err(e) => { - warn!("authentificated route rejected: {e:?}"); - Outcome::Forward(Status::Unauthorized) - } - } - } -} diff --git a/server/src/logic/playersync.rs b/server/src/logic/playersync.rs index b4cc51b..6c1f9f4 100644 --- a/server/src/logic/playersync.rs +++ b/server/src/logic/playersync.rs @@ -7,7 +7,7 @@ use rocket_ws::{stream::DuplexStream, Channel, Message, WebSocket}; use serde::{Deserialize, Serialize}; use tokio::sync::broadcast::{self, Sender}; -use crate::helper::cors::Cors; +use crate::request_info::cors::Cors; #[derive(Default)] pub struct PlayersyncChannels { diff --git a/server/src/logic/stream.rs b/server/src/logic/stream.rs index 36d2ec1..55d6850 100644 --- a/server/src/logic/stream.rs +++ b/server/src/logic/stream.rs @@ -3,7 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::{helper::A, ui::error::MyError}; +use crate::{request_info::A, ui::error::MyError}; use anyhow::{anyhow, Result}; use jellycommon::{api::NodeFilterSort, stream::StreamSpec, NodeID, TrackSource}; use jellylogic::{node::get_node, session::Session}; diff --git a/server/src/logic/userdata.rs b/server/src/logic/userdata.rs index 2dd3a85..104de4a 100644 --- a/server/src/logic/userdata.rs +++ b/server/src/logic/userdata.rs @@ -3,7 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::{helper::A, ui::error::MyResult}; +use crate::{request_info::A, ui::error::MyResult}; use jellycommon::{ api::NodeFilterSort, routes::u_node_id, diff --git a/server/src/main.rs b/server/src/main.rs index e17083b..be1aba4 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -11,36 +11,18 @@ use crate::logger::setup_logger; use config::load_config; use log::{error, info, warn}; use routes::build_rocket; -use serde::{Deserialize, Serialize}; -use std::sync::Mutex; -use std::{path::PathBuf, process::exit, sync::LazyLock}; +use std::process::exit; pub mod api; pub mod compat; pub mod config; -pub mod helper; pub mod logger; pub mod logic; +pub mod request_info; +pub mod responders; pub mod routes; pub mod ui; -#[derive(Debug, Deserialize, Serialize, Default)] -pub struct Config { - asset_path: PathBuf, - cookie_key: Option<String>, - tls: bool, - hostname: String, -} - -pub static CONF_PRELOAD: Mutex<Option<Config>> = Mutex::new(None); -static CONF: LazyLock<Config> = LazyLock::new(|| { - CONF_PRELOAD - .lock() - .unwrap() - .take() - .expect("cache config not preloaded. logic error") -}); - #[rocket::main] async fn main() { setup_logger(); @@ -53,10 +35,6 @@ async fn main() { #[cfg(feature = "bypass-auth")] logger::warn!("authentification bypass enabled"); - if let Err(e) = create_admin_account() { - error!("failed to create admin account: {e:?}"); - } - let r = build_rocket().launch().await; match r { Ok(_) => warn!("server shutdown"), diff --git a/server/src/request_info.rs b/server/src/request_info.rs new file mode 100644 index 0000000..779b4e1 --- /dev/null +++ b/server/src/request_info.rs @@ -0,0 +1,128 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2026 metamuffin <metamuffin.org> +*/ + +use crate::ui::error::{MyError, MyResult}; +use anyhow::anyhow; +use jellycommon::jellyobject::ObjectBuffer; +use jellyui::RenderInfo; +use rocket::{ + Request, async_trait, + http::{MediaType, Status}, + request::{FromRequest, Outcome}, +}; + +pub struct RequestInfo<'a> { + pub lang: &'a str, + pub accept: Accept, + pub user: Option<ObjectBuffer>, +} + +#[async_trait] +impl<'r> FromRequest<'r> for RequestInfo<'r> { + type Error = MyError; + async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> { + match Self::from_request_ut(request).await { + Ok(a) => Outcome::Success(a), + Err(a) => Outcome::Error((Status::BadRequest, a)), + } + } +} + +impl<'a> RequestInfo<'a> { + pub async fn from_request_ut(request: &'a Request<'_>) -> MyResult<Self> { + Ok(Self { + lang: accept_language(request), + accept: Accept::from_request_ut(request), + user: None, + // session: session_from_request(request).await?, + }) + } + pub fn render_info(&self) -> RenderInfo<'a> { + RenderInfo { + lang: self.lang, + status_message: None, + user: self.user.as_ref().map(|u| u.as_object()), + config: CONF.ui, + } + } +} + +#[derive(Debug, Default)] +pub enum Accept { + #[default] + Other, + Json, + Image, + Media, + Html, +} +impl Accept { + pub fn from_request_ut(request: &Request) -> Self { + if let Some(a) = request.accept() { + if a.preferred().exact_eq(&MediaType::JSON) { + Accept::Json + } else { + Accept::Other + } + } else { + Accept::Other + } + } + + pub fn is_json(&self) -> bool { + matches!(self, Self::Json) + } +} + +pub(super) fn accept_language<'a>(request: &'a Request<'_>) -> &'a str { + request + .headers() + .get_one("accept-language") + .and_then(|h| { + h.split(",") + .filter_map(|e| { + let code = e.split(";").next()?; + let code = code.split_once("-").unwrap_or((code, "")).0; + Some(code) + }) + .next() + }) + .unwrap_or("en") +} + +pub(super) async fn session_from_request(req: &Request<'_>) -> Result<Session, MyError> { + if cfg!(feature = "bypass-auth") { + Ok(bypass_auth_session()?) + } else { + let token = req + .query_value("session") + .map(|e| e.unwrap()) + .or_else(|| req.query_value("api_key").map(|e| e.unwrap())) + .or_else(|| req.headers().get_one("X-MediaBrowser-Token")) + .or_else(|| { + req.headers() + .get_one("Authorization") + .and_then(parse_jellyfin_auth) + }) // for jellyfin compat + .or(req.cookies().get("session").map(|cookie| cookie.value())) + .ok_or(anyhow!("not logged in"))?; + + // jellyfin urlescapes the token for *some* requests + let token = token.replace("%3D", "="); + Ok(token_to_session(&token)?) + } +} + +fn parse_jellyfin_auth(h: &str) -> Option<&str> { + for tok in h.split(" ") { + if let Some(tok) = tok.strip_prefix("Token=\"") + && let Some(tok) = tok.strip_suffix("\"") + { + return Some(tok); + } + } + None +} diff --git a/server/src/request_info/session.rs b/server/src/request_info/session.rs new file mode 100644 index 0000000..d032659 --- /dev/null +++ b/server/src/request_info/session.rs @@ -0,0 +1,15 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2026 metamuffin <metamuffin.org> +*/ +use super::A; +use crate::ui::error::MyError; +use anyhow::anyhow; +use log::warn; +use rocket::{ + Request, async_trait, + http::Status, + outcome::Outcome, + request::{self, FromRequest}, +}; diff --git a/server/src/helper/cache.rs b/server/src/responders/cache.rs index a943de8..a943de8 100644 --- a/server/src/helper/cache.rs +++ b/server/src/responders/cache.rs diff --git a/server/src/helper/cors.rs b/server/src/responders/cors.rs index 875b1e5..875b1e5 100644 --- a/server/src/helper/cors.rs +++ b/server/src/responders/cors.rs diff --git a/server/src/responders/mod.rs b/server/src/responders/mod.rs new file mode 100644 index 0000000..b62fe40 --- /dev/null +++ b/server/src/responders/mod.rs @@ -0,0 +1,8 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2026 metamuffin <metamuffin.org> +*/ + +pub mod cache; +pub mod cors; diff --git a/server/src/routes.rs b/server/src/routes.rs index 2d3e790..3b410f7 100644 --- a/server/src/routes.rs +++ b/server/src/routes.rs @@ -1,9 +1,9 @@ +use crate::config::CONF; /* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::CONF; use crate::logic::playersync::{PlayersyncChannels, r_playersync}; use crate::ui::account::{r_account_login, r_account_logout, r_account_register}; use crate::ui::admin::import::{r_admin_import, r_admin_import_post, r_admin_import_stream}; @@ -31,22 +31,22 @@ use crate::ui::{ use crate::{ api::{r_api_account_login, r_api_root, r_nodes_modified_since, r_version}, compat::{ - jellyfin::{ - r_jellyfin_artists, r_jellyfin_branding_configuration, r_jellyfin_branding_css, - r_jellyfin_displaypreferences_usersettings, - r_jellyfin_displaypreferences_usersettings_post, r_jellyfin_items, - r_jellyfin_items_image_primary, r_jellyfin_items_images_backdrop, - r_jellyfin_items_intros, r_jellyfin_items_item, r_jellyfin_items_playbackinfo, - r_jellyfin_items_similar, r_jellyfin_livetv_programs_recommended, r_jellyfin_persons, - r_jellyfin_playback_bitratetest, r_jellyfin_quickconnect_enabled, - r_jellyfin_sessions_capabilities_full, r_jellyfin_sessions_playing, - r_jellyfin_sessions_playing_progress, r_jellyfin_shows_nextup, r_jellyfin_socket, - r_jellyfin_system_endpoint, r_jellyfin_system_info, r_jellyfin_system_info_public, - r_jellyfin_system_info_public_case, r_jellyfin_users_authenticatebyname, - r_jellyfin_users_authenticatebyname_case, r_jellyfin_users_id, r_jellyfin_users_items, - r_jellyfin_users_items_item, r_jellyfin_users_public, r_jellyfin_users_views, - r_jellyfin_video_stream, - }, + // jellyfin::{ + // r_jellyfin_artists, r_jellyfin_branding_configuration, r_jellyfin_branding_css, + // r_jellyfin_displaypreferences_usersettings, + // r_jellyfin_displaypreferences_usersettings_post, r_jellyfin_items, + // r_jellyfin_items_image_primary, r_jellyfin_items_images_backdrop, + // r_jellyfin_items_intros, r_jellyfin_items_item, r_jellyfin_items_playbackinfo, + // r_jellyfin_items_similar, r_jellyfin_livetv_programs_recommended, r_jellyfin_persons, + // r_jellyfin_playback_bitratetest, r_jellyfin_quickconnect_enabled, + // r_jellyfin_sessions_capabilities_full, r_jellyfin_sessions_playing, + // r_jellyfin_sessions_playing_progress, r_jellyfin_shows_nextup, r_jellyfin_socket, + // r_jellyfin_system_endpoint, r_jellyfin_system_info, r_jellyfin_system_info_public, + // r_jellyfin_system_info_public_case, r_jellyfin_users_authenticatebyname, + // r_jellyfin_users_authenticatebyname_case, r_jellyfin_users_id, r_jellyfin_users_items, + // r_jellyfin_users_items_item, r_jellyfin_users_public, r_jellyfin_users_views, + // r_jellyfin_video_stream, + // }, youtube::{r_youtube_channel, r_youtube_embed, r_youtube_watch}, }, logic::{ @@ -161,39 +161,39 @@ pub fn build_rocket() -> Rocket<Build> { r_api_root, r_version, // Compat - r_jellyfin_artists, - r_jellyfin_branding_configuration, - r_jellyfin_branding_css, - r_jellyfin_displaypreferences_usersettings_post, - r_jellyfin_displaypreferences_usersettings, - r_jellyfin_items_image_primary, - r_jellyfin_items_images_backdrop, - r_jellyfin_items_intros, - r_jellyfin_items_item, - r_jellyfin_items_playbackinfo, - r_jellyfin_items_similar, - r_jellyfin_items, - r_jellyfin_livetv_programs_recommended, - r_jellyfin_persons, - r_jellyfin_playback_bitratetest, - r_jellyfin_quickconnect_enabled, - r_jellyfin_sessions_capabilities_full, - r_jellyfin_sessions_playing_progress, - r_jellyfin_sessions_playing, - r_jellyfin_shows_nextup, - r_jellyfin_socket, - r_jellyfin_system_endpoint, - r_jellyfin_system_info_public_case, - r_jellyfin_system_info_public, - r_jellyfin_system_info, - r_jellyfin_users_authenticatebyname, - r_jellyfin_users_authenticatebyname_case, - r_jellyfin_users_id, - r_jellyfin_users_items_item, - r_jellyfin_users_items, - r_jellyfin_users_public, - r_jellyfin_users_views, - r_jellyfin_video_stream, + // r_jellyfin_artists, + // r_jellyfin_branding_configuration, + // r_jellyfin_branding_css, + // r_jellyfin_displaypreferences_usersettings_post, + // r_jellyfin_displaypreferences_usersettings, + // r_jellyfin_items_image_primary, + // r_jellyfin_items_images_backdrop, + // r_jellyfin_items_intros, + // r_jellyfin_items_item, + // r_jellyfin_items_playbackinfo, + // r_jellyfin_items_similar, + // r_jellyfin_items, + // r_jellyfin_livetv_programs_recommended, + // r_jellyfin_persons, + // r_jellyfin_playback_bitratetest, + // r_jellyfin_quickconnect_enabled, + // r_jellyfin_sessions_capabilities_full, + // r_jellyfin_sessions_playing_progress, + // r_jellyfin_sessions_playing, + // r_jellyfin_shows_nextup, + // r_jellyfin_socket, + // r_jellyfin_system_endpoint, + // r_jellyfin_system_info_public_case, + // r_jellyfin_system_info_public, + // r_jellyfin_system_info, + // r_jellyfin_users_authenticatebyname, + // r_jellyfin_users_authenticatebyname_case, + // r_jellyfin_users_id, + // r_jellyfin_users_items_item, + // r_jellyfin_users_items, + // r_jellyfin_users_public, + // r_jellyfin_users_views, + // r_jellyfin_video_stream, r_youtube_channel, r_youtube_embed, r_youtube_watch, diff --git a/server/src/ui/account/mod.rs b/server/src/ui/account/mod.rs index d731f0f..35cf52e 100644 --- a/server/src/ui/account/mod.rs +++ b/server/src/ui/account/mod.rs @@ -7,7 +7,7 @@ pub mod settings; use super::error::MyError; use crate::{ - helper::{language::AcceptLanguage, A}, + request_info::{language::AcceptLanguage, A}, ui::{error::MyResult, home::rocket_uri_macro_r_home}, }; use anyhow::anyhow; diff --git a/server/src/ui/account/settings.rs b/server/src/ui/account/settings.rs index 491e82e..167731f 100644 --- a/server/src/ui/account/settings.rs +++ b/server/src/ui/account/settings.rs @@ -5,7 +5,7 @@ */ use super::format_form_error; use crate::{ - helper::{RequestInfo, A}, + request_info::{RequestInfo, A}, ui::error::MyResult, }; use jellycommon::{ diff --git a/server/src/ui/admin/import.rs b/server/src/ui/admin/import.rs index 52add7f..fb33c67 100644 --- a/server/src/ui/admin/import.rs +++ b/server/src/ui/admin/import.rs @@ -7,7 +7,7 @@ use std::time::Duration; use crate::{ - helper::{A, RequestInfo}, + request_info::{A, RequestInfo}, ui::error::MyResult, }; use jellycommon::routes::u_admin_import; diff --git a/server/src/ui/admin/log.rs b/server/src/ui/admin/log.rs index ef84704..e948daa 100644 --- a/server/src/ui/admin/log.rs +++ b/server/src/ui/admin/log.rs @@ -4,7 +4,7 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ use crate::{ - helper::{A, RequestInfo}, + request_info::{A, RequestInfo}, ui::error::MyResult, }; use jellylogic::{ diff --git a/server/src/ui/admin/mod.rs b/server/src/ui/admin/mod.rs index 0cc226b..9759627 100644 --- a/server/src/ui/admin/mod.rs +++ b/server/src/ui/admin/mod.rs @@ -8,7 +8,7 @@ pub mod log; pub mod user; use super::error::MyResult; -use crate::helper::RequestInfo; +use crate::request_info::RequestInfo; use jellycommon::routes::u_admin_dashboard; use jellyimport::is_importing; use jellylogic::admin::{create_invite, delete_invite, list_invites, update_search_index}; diff --git a/server/src/ui/admin/user.rs b/server/src/ui/admin/user.rs index 3d56f46..03950df 100644 --- a/server/src/ui/admin/user.rs +++ b/server/src/ui/admin/user.rs @@ -3,7 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::{helper::RequestInfo, ui::error::MyResult}; +use crate::{request_info::RequestInfo, ui::error::MyResult}; use anyhow::Context; use jellycommon::{ routes::{u_admin_user, u_admin_users}, diff --git a/server/src/ui/assets.rs b/server/src/ui/assets.rs index 738d1de..6dc6731 100644 --- a/server/src/ui/assets.rs +++ b/server/src/ui/assets.rs @@ -4,7 +4,7 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ use super::error::MyResult; -use crate::helper::{A, cache::CacheControlImage}; +use crate::request_info::{A, cache::CacheControlImage}; use anyhow::{Context, anyhow}; use jellycommon::{Asset, NodeID, PictureSlot, api::NodeFilterSort}; use jellylogic::{assets::get_node_thumbnail, node::get_node, session::Session}; diff --git a/server/src/ui/home.rs b/server/src/ui/home.rs index db57880..499c3cd 100644 --- a/server/src/ui/home.rs +++ b/server/src/ui/home.rs @@ -5,7 +5,7 @@ */ use super::error::MyResult; -use crate::helper::{accept::Accept, RequestInfo}; +use crate::request_info::{accept::Accept, RequestInfo}; use jellycommon::api::ApiHomeResponse; use jellyui::{home::HomePage, render_page}; use rocket::{get, response::content::RawHtml, serde::json::Json, Either}; diff --git a/server/src/ui/items.rs b/server/src/ui/items.rs index bf99ef2..86aaf12 100644 --- a/server/src/ui/items.rs +++ b/server/src/ui/items.rs @@ -4,7 +4,7 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ use super::error::MyError; -use crate::helper::{accept::Accept, filter_sort::ANodeFilterSort, RequestInfo}; +use crate::request_info::{accept::Accept, filter_sort::ANodeFilterSort, RequestInfo}; use jellycommon::api::ApiItemsResponse; use jellylogic::items::all_items; use jellyui::{items::ItemsPage, render_page}; diff --git a/server/src/ui/mod.rs b/server/src/ui/mod.rs index aca6c33..946401e 100644 --- a/server/src/ui/mod.rs +++ b/server/src/ui/mod.rs @@ -4,7 +4,7 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ use crate::{ - helper::{language::AcceptLanguage, A}, + request_info::{language::AcceptLanguage, A}, CONF, }; use error::MyResult; diff --git a/server/src/ui/node.rs b/server/src/ui/node.rs index 4b452f3..fac1909 100644 --- a/server/src/ui/node.rs +++ b/server/src/ui/node.rs @@ -4,7 +4,7 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ use super::error::MyResult; -use crate::helper::{filter_sort::ANodeFilterSort, RequestInfo, A}; +use crate::request_info::{filter_sort::ANodeFilterSort, RequestInfo, A}; use jellycommon::{ api::{ApiNodeResponse, NodeFilterSort}, NodeID, diff --git a/server/src/ui/player.rs b/server/src/ui/player.rs index 7b69aab..d43f3ea 100644 --- a/server/src/ui/player.rs +++ b/server/src/ui/player.rs @@ -5,7 +5,7 @@ */ use super::error::MyResult; use crate::{ - helper::{RequestInfo, A}, + request_info::{RequestInfo, A}, CONF, }; use jellycommon::{ diff --git a/server/src/ui/search.rs b/server/src/ui/search.rs index 47c39c8..8a67672 100644 --- a/server/src/ui/search.rs +++ b/server/src/ui/search.rs @@ -4,7 +4,7 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ use super::error::MyResult; -use crate::helper::RequestInfo; +use crate::request_info::RequestInfo; use anyhow::anyhow; use jellycommon::api::ApiSearchResponse; use jellylogic::search::search; diff --git a/server/src/ui/stats.rs b/server/src/ui/stats.rs index 0c5d2cd..b0225e6 100644 --- a/server/src/ui/stats.rs +++ b/server/src/ui/stats.rs @@ -4,7 +4,7 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ use super::error::MyError; -use crate::helper::RequestInfo; +use crate::request_info::RequestInfo; use jellycommon::api::ApiStatsResponse; use jellylogic::stats::stats; use jellyui::{render_page, stats::StatsPage}; |