diff options
author | metamuffin <metamuffin@disroot.org> | 2025-04-30 10:47:54 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2025-04-30 10:47:54 +0200 |
commit | a2ef3f6ec4c830611fde1a2e935588ccbbc61c03 (patch) | |
tree | ddcc1cb501e6c7237edd491aa7136d02150d03d3 | |
parent | 212a0f23bc894faf88e159560c113f504349cc05 (diff) | |
download | jellything-a2ef3f6ec4c830611fde1a2e935588ccbbc61c03.tar jellything-a2ef3f6ec4c830611fde1a2e935588ccbbc61c03.tar.bz2 jellything-a2ef3f6ec4c830611fde1a2e935588ccbbc61c03.tar.zst |
config works
-rw-r--r-- | Cargo.lock | 26 | ||||
-rw-r--r-- | base/src/assetfed.rs | 4 | ||||
-rw-r--r-- | base/src/federation.rs | 108 | ||||
-rw-r--r-- | base/src/lib.rs | 56 | ||||
-rw-r--r-- | base/src/permission.rs | 3 | ||||
-rw-r--r-- | cache/src/lib.rs | 5 | ||||
-rw-r--r-- | common/src/config.rs | 12 | ||||
-rw-r--r-- | import/src/lib.rs | 64 | ||||
-rw-r--r-- | logic/src/lib.rs | 30 | ||||
-rw-r--r-- | logic/src/login.rs | 18 | ||||
-rw-r--r-- | logic/src/session.rs | 19 | ||||
-rw-r--r-- | server/Cargo.toml | 24 | ||||
-rw-r--r-- | server/src/compat/jellyfin/mod.rs | 10 | ||||
-rw-r--r-- | server/src/config.rs | 43 | ||||
-rw-r--r-- | server/src/logic/stream.rs | 3 | ||||
-rw-r--r-- | server/src/main.rs | 48 | ||||
-rw-r--r-- | server/src/routes.rs | 13 | ||||
-rw-r--r-- | server/src/ui/admin/mod.rs | 23 | ||||
-rw-r--r-- | server/src/ui/assets.rs | 59 | ||||
-rw-r--r-- | server/src/ui/error.rs | 2 | ||||
-rw-r--r-- | server/src/ui/mod.rs | 3 | ||||
-rw-r--r-- | server/src/ui/player.rs | 3 | ||||
-rw-r--r-- | stream/src/lib.rs | 10 | ||||
-rw-r--r-- | tool/src/add.rs | 12 | ||||
-rw-r--r-- | tool/src/main.rs | 62 | ||||
-rw-r--r-- | transcoder/src/lib.rs | 11 | ||||
-rw-r--r-- | ui/src/admin/mod.rs | 2 | ||||
-rw-r--r-- | ui/src/lib.rs | 15 |
28 files changed, 394 insertions, 294 deletions
@@ -1927,6 +1927,7 @@ dependencies = [ "rocket_ws", "serde", "serde_json", + "serde_yml", "tokio", "tokio-util", ] @@ -2117,6 +2118,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] +name = "libyml" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" +dependencies = [ + "anyhow", + "version_check", +] + +[[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3436,6 +3447,21 @@ dependencies = [ ] [[package]] +name = "serde_yml" +version = "0.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" +dependencies = [ + "indexmap", + "itoa", + "libyml", + "memchr", + "ryu", + "serde", + "version_check", +] + +[[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/base/src/assetfed.rs b/base/src/assetfed.rs index 621169f..ea62e0d 100644 --- a/base/src/assetfed.rs +++ b/base/src/assetfed.rs @@ -3,7 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::SECRETS; +use crate::CONF; use aes_gcm_siv::{ aead::{generic_array::GenericArray, Aead}, Aes256GcmSiv, KeyInit, @@ -20,7 +20,7 @@ use std::{path::PathBuf, sync::LazyLock}; const VERSION: u32 = 3; static ASSET_KEY: LazyLock<Aes256GcmSiv> = LazyLock::new(|| { - if let Some(sk) = &SECRETS.session_key { + if let Some(sk) = &CONF.asset_key { let r = base64::engine::general_purpose::STANDARD .decode(sk) .expect("key invalid; should be valid base64"); diff --git a/base/src/federation.rs b/base/src/federation.rs index 879ce96..b24d113 100644 --- a/base/src/federation.rs +++ b/base/src/federation.rs @@ -3,62 +3,62 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::SECRETS; -use anyhow::anyhow; -use jellyclient::{Instance, Session}; -use jellycommon::{config::FederationAccount, user::CreateSessionParams}; -use std::{collections::HashMap, sync::Arc}; -use tokio::sync::RwLock; -pub struct Federation { - instances: HashMap<String, Instance>, - sessions: RwLock<HashMap<String, Arc<Session>>>, -} +// use anyhow::anyhow; +// use jellyclient::{Instance, Session}; +// use jellycommon::{config::FederationAccount, user::CreateSessionParams}; +// use std::{collections::HashMap, sync::Arc}; +// use tokio::sync::RwLock; -impl Federation { - pub fn initialize() -> Self { - let instances = SECRETS - .federation - .iter() - .map(|(k, FederationAccount { tls, .. })| { - (k.to_owned(), Instance::new(k.to_owned(), *tls)) - }) - .collect::<HashMap<_, _>>(); +// pub struct Federation { +// instances: HashMap<String, Instance>, +// sessions: RwLock<HashMap<String, Arc<Session>>>, +// } - Self { - instances, - sessions: Default::default(), - } - } +// impl Federation { +// pub fn initialize() -> Self { +// let instances = SECRETS +// .federation +// .iter() +// .map(|(k, FederationAccount { tls, .. })| { +// (k.to_owned(), Instance::new(k.to_owned(), *tls)) +// }) +// .collect::<HashMap<_, _>>(); - pub fn get_instance(&self, host: &String) -> anyhow::Result<&Instance> { - self.instances.get(host).ok_or(anyhow!("unknown instance")) - } +// Self { +// instances, +// sessions: Default::default(), +// } +// } - pub async fn get_session(&self, host: &String) -> anyhow::Result<Arc<Session>> { - let mut w = self.sessions.write().await; - if let Some(s) = w.get(host) { - Ok(s.to_owned()) - } else { - let FederationAccount { - username, password, .. - } = SECRETS - .federation - .get(host) - .ok_or(anyhow!("no credentials of the remote server"))?; - let s = Arc::new( - self.get_instance(host)? - .to_owned() - .login(CreateSessionParams { - username: username.to_owned(), - password: password.to_owned(), - expire: None, - drop_permissions: None, - }) - .await?, - ); - w.insert(host.to_owned(), s.clone()); - Ok(s) - } - } -} +// pub fn get_instance(&self, host: &String) -> anyhow::Result<&Instance> { +// self.instances.get(host).ok_or(anyhow!("unknown instance")) +// } + +// pub async fn get_session(&self, host: &String) -> anyhow::Result<Arc<Session>> { +// let mut w = self.sessions.write().await; +// if let Some(s) = w.get(host) { +// Ok(s.to_owned()) +// } else { +// let FederationAccount { +// username, password, .. +// } = SECRETS +// .federation +// .get(host) +// .ok_or(anyhow!("no credentials of the remote server"))?; +// let s = Arc::new( +// self.get_instance(host)? +// .to_owned() +// .login(CreateSessionParams { +// username: username.to_owned(), +// password: password.to_owned(), +// expire: None, +// drop_permissions: None, +// }) +// .await?, +// ); +// w.insert(host.to_owned(), s.clone()); +// Ok(s) +// } +// } +// } diff --git a/base/src/lib.rs b/base/src/lib.rs index 010e908..55a9927 100644 --- a/base/src/lib.rs +++ b/base/src/lib.rs @@ -9,47 +9,21 @@ pub mod federation; pub mod permission; pub use jellycommon as common; +use serde::{Deserialize, Serialize}; +use std::sync::LazyLock; +use std::sync::Mutex; -use jellycommon::config::{GlobalConfig, SecretsConfig}; -use std::sync::{ - atomic::{AtomicBool, Ordering}, - LazyLock, -}; - -pub static CONF: LazyLock<GlobalConfig> = LazyLock::new(load_config); -pub static SECRETS: LazyLock<SecretsConfig> = LazyLock::new(load_secrets); -pub static USE_TEST: AtomicBool = AtomicBool::new(false); - -pub fn use_test_config() { - USE_TEST.store(true, Ordering::Relaxed) +#[rustfmt::skip] +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct Config { + asset_key: Option<String>, } -pub fn load_config() -> GlobalConfig { - if USE_TEST.load(Ordering::Relaxed) { - return GlobalConfig::default(); - } - serde_yaml::from_reader( - std::fs::File::open(std::env::var("JELLYTHING_CONFIG").unwrap_or_else(|_| { - if std::env::args() - .next() - .unwrap_or_default() - .ends_with("jellything") - { - std::env::args().nth(1).expect( - "First argument or JELLYTHING_CONFIG must specify the configuration to use.", - ) - } else { - panic!("JELLYTHING_CONFIG variable is required.") - } - })) - .expect("config cannot be read"), - ) - .expect("config invalid") -} -fn load_secrets() -> SecretsConfig { - if USE_TEST.load(Ordering::Relaxed) { - return SecretsConfig::default(); - } - serde_yaml::from_reader(std::fs::File::open(&CONF.secrets_path).expect("secrets file missing")) - .expect("secrets config invalid") -} +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") +}); diff --git a/base/src/permission.rs b/base/src/permission.rs index 55d0870..7914f0b 100644 --- a/base/src/permission.rs +++ b/base/src/permission.rs @@ -3,7 +3,6 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::CONF; use anyhow::anyhow; use jellycommon::{ user::{PermissionSet, UserPermission}, @@ -22,7 +21,7 @@ impl PermissionSetExt for PermissionSet { fn check_explicit(&self, perm: &UserPermission) -> Option<bool> { self.0 .get(perm) - .or(CONF.default_permission_set.0.get(perm)) + // .or(CONF.default_permission_set.0.get(perm)) .copied() } fn assert(&self, perm: &UserPermission) -> Result<(), anyhow::Error> { diff --git a/cache/src/lib.rs b/cache/src/lib.rs index 2d2cfa3..2d31d6c 100644 --- a/cache/src/lib.rs +++ b/cache/src/lib.rs @@ -35,13 +35,14 @@ pub struct Config { max_in_memory_cache_size: usize, } +pub static CONF_PRELOAD: std::sync::Mutex<Option<Config>> = std::sync::Mutex::new(None); static CONF: LazyLock<Config> = LazyLock::new(|| { CONF_PRELOAD - .blocking_lock() + .lock() + .unwrap() .take() .expect("cache config not preloaded. logic error") }); -static CONF_PRELOAD: Mutex<Option<Config>> = Mutex::const_new(None); #[derive(Debug, Encode, Decode, Serialize, Clone)] pub struct CachePath(pub PathBuf); diff --git a/common/src/config.rs b/common/src/config.rs index 5dc2d14..9368247 100644 --- a/common/src/config.rs +++ b/common/src/config.rs @@ -35,8 +35,6 @@ pub struct SecretsConfig { #[serde(default)] pub federation: HashMap<String, FederationAccount>, #[serde(default)] - pub api: ApiSecrets, - #[serde(default)] pub cookie_key: Option<String>, #[serde(default)] pub session_key: Option<String>, @@ -51,16 +49,6 @@ pub struct FederationAccount { pub tls: bool, } -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct ApiSecrets { - pub acoustid: Option<String>, - pub tmdb: Option<String>, - pub tvdb: Option<String>, - pub imdb: Option<String>, - pub omdb: Option<String>, - pub fanart_tv: Option<String>, - pub trakt: Option<String>, -} fn login_expire() -> i64 { 60 * 60 * 24 diff --git a/import/src/lib.rs b/import/src/lib.rs index 2f7383a..da339d8 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -4,6 +4,16 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ #![feature(duration_constants)] + +pub mod acoustid; +pub mod infojson; +pub mod musicbrainz; +pub mod tmdb; +pub mod trakt; +pub mod vgmdb; +pub mod wikidata; +pub mod wikimedia_commons; + use acoustid::{acoustid_fingerprint, AcoustID}; use anyhow::{anyhow, bail, Context, Result}; use infojson::YVideo; @@ -11,7 +21,6 @@ use jellybase::{ assetfed::AssetInner, common::{Chapter, MediaInfo, Node, NodeID, NodeKind, Rating, SourceTrack, SourceTrackKind}, database::Database, - CONF, SECRETS, }; use jellycache::cache_file; use jellyclient::{ @@ -24,18 +33,19 @@ use log::info; use musicbrainz::MusicBrainz; use rayon::iter::{ParallelBridge, ParallelIterator}; use regex::Regex; +use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeMap, HashMap}, fs::{read_to_string, File}, io::BufReader, - path::Path, + path::{Path, PathBuf}, sync::LazyLock, time::UNIX_EPOCH, }; use tmdb::Tmdb; use tokio::{ runtime::Handle, - sync::{RwLock, Semaphore}, + sync::{Mutex, RwLock, Semaphore}, task::spawn_blocking, }; use trakt::Trakt; @@ -43,14 +53,31 @@ use vgmdb::Vgmdb; use wikidata::Wikidata; use wikimedia_commons::WikimediaCommons; -pub mod acoustid; -pub mod infojson; -pub mod musicbrainz; -pub mod tmdb; -pub mod trakt; -pub mod vgmdb; -pub mod wikidata; -pub mod wikimedia_commons; +#[rustfmt::skip] +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct Config { + media_path: PathBuf, + api: ApiSecrets, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct ApiSecrets { + pub acoustid: Option<String>, + pub tmdb: Option<String>, + pub tvdb: Option<String>, + pub imdb: Option<String>, + pub omdb: Option<String>, + pub fanart_tv: Option<String>, + pub trakt: Option<String>, +} + +pub static CONF_PRELOAD: Mutex<Option<Config>> = Mutex::const_new(None); +static CONF: LazyLock<Config> = LazyLock::new(|| { + CONF_PRELOAD + .blocking_lock() + .take() + .expect("cache config not preloaded. logic error") +}); pub const USER_AGENT: &'static str = concat!( "jellything/", @@ -78,6 +105,15 @@ pub fn is_importing() -> bool { IMPORT_SEM.available_permits() == 0 } +pub fn get_trakt() -> Result<Trakt> { + Ok(Trakt::new( + CONF.api + .trakt + .as_ref() + .ok_or(anyhow!("no trakt api key configured"))?, + )) +} + pub async fn import_wrap(db: Database, incremental: bool) -> Result<()> { let _sem = IMPORT_SEM.try_acquire()?; @@ -95,9 +131,9 @@ pub async fn import_wrap(db: Database, incremental: bool) -> Result<()> { fn import(db: &Database, incremental: bool) -> Result<()> { let apis = Apis { - trakt: SECRETS.api.trakt.as_ref().map(|key| Trakt::new(key)), - tmdb: SECRETS.api.tmdb.as_ref().map(|key| Tmdb::new(key)), - acoustid: SECRETS.api.acoustid.as_ref().map(|key| AcoustID::new(key)), + trakt: CONF.api.trakt.as_ref().map(|key| Trakt::new(key)), + tmdb: CONF.api.tmdb.as_ref().map(|key| Tmdb::new(key)), + acoustid: CONF.api.acoustid.as_ref().map(|key| AcoustID::new(key)), musicbrainz: MusicBrainz::new(), wikidata: Wikidata::new(), wikimedia_commons: WikimediaCommons::new(), diff --git a/logic/src/lib.rs b/logic/src/lib.rs index 54c9c40..64656f5 100644 --- a/logic/src/lib.rs +++ b/logic/src/lib.rs @@ -3,14 +3,36 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -#![feature(duration_constructors)] +#![feature(duration_constructors, let_chains)] +pub mod admin; pub mod filter_sort; pub mod home; +pub mod items; +pub mod login; pub mod node; pub mod search; pub mod session; pub mod stats; -pub mod items; -pub mod admin; -pub mod login; + +use serde::{Deserialize, Serialize}; +use std::sync::LazyLock; +use std::sync::Mutex; + +#[rustfmt::skip] +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct Config { + login_expire: i64, + session_key: Option<String>, + admin_username:Option<String>, + admin_password:Option<String>, +} + +pub static CONF_PRELOAD: Mutex<Option<Config>> = Mutex::new(None); +static CONF: LazyLock<Config> = LazyLock::new(|| { + CONF_PRELOAD + .lock() + .unwrap() + .take() + .expect("logic config not preloaded. logic error") +}); diff --git a/logic/src/login.rs b/logic/src/login.rs index e9c2f93..26a6b7f 100644 --- a/logic/src/login.rs +++ b/logic/src/login.rs @@ -3,13 +3,27 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::session::create; +use crate::{CONF, session::create}; use anyhow::{Result, anyhow}; use argon2::{Argon2, PasswordHasher, password_hash::Salt}; -use jellybase::{CONF, database::Database}; +use jellybase::database::Database; use jellycommon::user::UserPermission; +use log::info; use std::{collections::HashSet, time::Duration}; +pub fn create_admin_account(database: &Database) -> Result<()> { + if let Some(username) = &CONF.admin_username + && let Some(password) = &CONF.admin_password + { + database + .create_admin_user(username, hash_password(username, password)) + .unwrap(); + } else { + info!("admin account disabled") + } + Ok(()) +} + pub fn login_logic( database: &Database, username: &str, diff --git a/logic/src/session.rs b/logic/src/session.rs index bc7f137..72a1089 100644 --- a/logic/src/session.rs +++ b/logic/src/session.rs @@ -3,13 +3,13 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ +use crate::CONF; use aes_gcm_siv::{ KeyInit, aead::{Aead, generic_array::GenericArray}, }; use anyhow::anyhow; use base64::Engine; -use jellybase::SECRETS; use jellycommon::{ chrono::{DateTime, Utc}, user::{PermissionSet, User}, @@ -32,7 +32,7 @@ pub struct SessionData { } static SESSION_KEY: LazyLock<[u8; 32]> = LazyLock::new(|| { - if let Some(sk) = &SECRETS.session_key { + if let Some(sk) = &CONF.session_key { let r = base64::engine::general_purpose::STANDARD .decode(sk) .expect("key invalid; should be valid base64"); @@ -85,9 +85,20 @@ pub fn validate(token: &str) -> anyhow::Result<String> { Ok(session_data.username) } +#[cfg(test)] +fn load_test_config() { + use crate::{CONF_PRELOAD, Config}; + *CONF_PRELOAD.lock().unwrap() = Some(Config { + login_expire: 10, + session_key: None, + admin_password: None, + admin_username: None, + }); +} + #[test] fn test() { - jellybase::use_test_config(); + load_test_config(); let tok = create( "blub".to_string(), jellycommon::user::PermissionSet::default(), @@ -98,7 +109,7 @@ fn test() { #[test] fn test_crypto() { - jellybase::use_test_config(); + load_test_config(); let nonce = [(); 12].map(|_| rand::random()); let cipher = aes_gcm_siv::Aes256GcmSiv::new_from_slice(&*SESSION_KEY).unwrap(); let plaintext = b"testing stuff---"; diff --git a/server/Cargo.toml b/server/Cargo.toml index 57d0c29..be8abdb 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -13,25 +13,23 @@ jellycache = { path = "../cache" } jellyui = { path = "../ui" } jellylogic = { path = "../logic" } -serde = { version = "1.0.217", features = ["derive", "rc"] } -bincode = { version = "2.0.0-rc.3", features = ["serde", "derive"] } -serde_json = "1.0.138" - -log = { workspace = true } anyhow = { workspace = true } -env_logger = "0.11.6" -rand = "0.9.0" +async-recursion = "1.1.1" base64 = "0.22.1" -chrono = { version = "0.4.39", features = ["serde"] } +bincode = { version = "2.0.0-rc.3", features = ["serde", "derive"] } chashmap = "2.2.2" - -async-recursion = "1.1.1" +chrono = { version = "0.4.39", features = ["serde"] } +env_logger = "0.11.6" futures = "0.3.31" -tokio = { workspace = true } -tokio-util = { version = "0.7.13", features = ["io", "io-util"] } - +log = { workspace = true } +rand = "0.9.0" rocket = { workspace = true, features = ["secrets", "json"] } rocket_ws = { workspace = true } +serde = { version = "1.0.217", features = ["derive", "rc"] } +serde_json = "1.0.138" +serde_yml = "0.0.12" +tokio = { workspace = true } +tokio-util = { version = "0.7.13", features = ["io", "io-util"] } [build-dependencies] glob = "0.3.2" diff --git a/server/src/compat/jellyfin/mod.rs b/server/src/compat/jellyfin/mod.rs index f999060..1c602ca 100644 --- a/server/src/compat/jellyfin/mod.rs +++ b/server/src/compat/jellyfin/mod.rs @@ -7,7 +7,7 @@ pub mod models; use crate::{helper::A, ui::error::MyResult}; use anyhow::{anyhow, Context}; -use jellybase::{database::Database, CONF}; +use jellybase::database::Database; use jellycommon::{ api::{FilterProperty, NodeFilterSort, SortOrder, SortProperty}, routes::{u_asset, u_node_slug_backdrop, u_node_slug_poster}, @@ -19,7 +19,7 @@ use jellylogic::{ filter_sort::filter_and_sort_nodes, login::login_logic, node::DatabaseNodeUserDataExt, session::Session, }; -use jellyui::node_page::aspect_class; +use jellyui::{get_brand, get_slogan, node_page::aspect_class}; use models::*; use rocket::{ get, @@ -47,7 +47,7 @@ pub fn r_jellyfin_system_info_public_case() -> Json<Value> { pub fn r_jellyfin_system_info_public() -> Json<Value> { Json(json!({ "LocalAddress": LOCAL_ADDRESS, - "ServerName": CONF.brand.clone(), + "ServerName": get_brand(), "Version": VERSION, "ProductName": "Jellything", "OperatingSystem": "", @@ -59,7 +59,7 @@ pub fn r_jellyfin_system_info_public() -> Json<Value> { #[get("/Branding/Configuration")] pub fn r_jellyfin_branding_configuration() -> Json<Value> { Json(json!({ - "LoginDisclaimer": format!("{} - {}", CONF.brand, CONF.slogan), + "LoginDisclaimer": format!("{} - {}", get_brand(), get_slogan()), "CustomCss": "", "SplashscreenEnabled": false, })) @@ -122,7 +122,7 @@ pub fn r_jellyfin_system_info(_session: A<Session>) -> Json<Value> { "EncoderLocation": "System", "SystemArchitecture": "X64", "LocalAddress": LOCAL_ADDRESS, - "ServerName": CONF.brand, + "ServerName": get_brand(), "Version": VERSION, "OperatingSystem": "", "Id": SERVER_ID diff --git a/server/src/config.rs b/server/src/config.rs new file mode 100644 index 0000000..27074b4 --- /dev/null +++ b/server/src/config.rs @@ -0,0 +1,43 @@ +/* + 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) 2025 metamuffin <metamuffin.org> +*/ + +use anyhow::{anyhow, Context, Result}; +use serde::Deserialize; +use std::env::{args, var}; +use tokio::fs::read_to_string; + +#[derive(Debug, Deserialize)] +struct Config { + transcoder: jellytranscoder::Config, + ui: jellyui::Config, + stream: jellystream::Config, + cache: jellycache::Config, + server: crate::Config, + base: jellybase::Config, + logic: jellylogic::Config, +} + +pub async fn load_config() -> Result<()> { + let path = args() + .nth(1) + .or_else(|| var("JELLYTHING_CONFIG").ok()) + .ok_or(anyhow!( + "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_yml::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); + *jellybase::CONF_PRELOAD.lock().unwrap() = Some(config.base); + *crate::CONF_PRELOAD.lock().unwrap() = Some(config.server); + *jellyui::CONF_PRELOAD.lock().unwrap() = Some(config.ui); + + Ok(()) +} diff --git a/server/src/logic/stream.rs b/server/src/logic/stream.rs index 9d4db6d..89589c7 100644 --- a/server/src/logic/stream.rs +++ b/server/src/logic/stream.rs @@ -5,7 +5,7 @@ */ use crate::{database::Database, helper::A, ui::error::MyError}; use anyhow::{anyhow, Result}; -use jellybase::{assetfed::AssetInner, federation::Federation}; +use jellybase::assetfed::AssetInner; use jellycommon::{stream::StreamSpec, TrackSource}; use jellylogic::session::Session; use jellystream::SMediaInfo; @@ -43,7 +43,6 @@ pub async fn r_stream_head( #[get("/n/<id>/stream?<spec..>")] pub async fn r_stream( _session: A<Session>, - _federation: &State<Federation>, db: &State<Database>, id: &str, range: Option<RequestRange>, diff --git a/server/src/main.rs b/server/src/main.rs index 94a53c7..ea75208 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -8,45 +8,65 @@ #![recursion_limit = "4096"] use anyhow::Context; +use config::load_config; use database::Database; -use jellybase::{federation::Federation, CONF, SECRETS}; -use jellylogic::{admin::log::enable_logging, login::hash_password}; +use jellylogic::{admin::log::enable_logging, login::create_admin_account}; use log::{error, info, warn}; use routes::build_rocket; -use tokio::fs::create_dir_all; +use serde::{Deserialize, Serialize}; +use std::sync::Mutex; +use std::{path::PathBuf, process::exit, sync::LazyLock}; pub use jellybase::database; pub mod api; pub mod compat; +pub mod config; pub mod helper; pub mod locale; pub mod logic; pub mod routes; pub mod ui; +#[rustfmt::skip] +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct Config { + database_path: PathBuf, + 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() { enable_logging(); + info!("loading config..."); + if let Err(e) = load_config().await { + error!("error {e:?}"); + exit(1); + } + #[cfg(feature = "bypass-auth")] log::warn!("authentification bypass enabled"); - create_dir_all(&CONF.cache_path).await.unwrap(); let database = Database::open(&CONF.database_path) .context("opening database") .unwrap(); - let federation = Federation::initialize(); - if let Some(username) = &CONF.admin_username - && let Some(password) = &SECRETS.admin_password - { - database - .create_admin_user(username, hash_password(username, password)) - .unwrap(); - } else { - info!("admin account disabled") + if let Err(e) = create_admin_account(&database) { + error!("failed to create admin account: {e:?}"); } - let r = build_rocket(database, federation).launch().await; + let r = build_rocket(database).launch().await; match r { Ok(_) => warn!("server shutdown"), Err(e) => error!("server exited: {e}"), diff --git a/server/src/routes.rs b/server/src/routes.rs index ef7b067..da3a389 100644 --- a/server/src/routes.rs +++ b/server/src/routes.rs @@ -13,8 +13,8 @@ use crate::ui::{ }, admin::{ log::{r_admin_log, r_admin_log_stream}, - r_admin_dashboard, r_admin_delete_cache, r_admin_import, r_admin_invite, - r_admin_remove_invite, r_admin_transcode_posters, r_admin_update_search, + r_admin_dashboard, r_admin_import, r_admin_invite, r_admin_remove_invite, + r_admin_transcode_posters, r_admin_update_search, user::{r_admin_remove_user, r_admin_user, r_admin_user_permission, r_admin_users}, }, assets::{r_asset, r_item_backdrop, r_item_poster, r_node_thumbnail, r_person_asset}, @@ -28,6 +28,7 @@ use crate::ui::{ stats::r_stats, style::{r_assets_font, r_assets_js, r_assets_js_map, r_assets_style}, }; +use crate::CONF; use crate::{ api::{ r_api_account_login, r_api_asset_token_raw, r_api_nodes_modified_since, r_api_root, @@ -61,7 +62,6 @@ use crate::{ }, }; use base64::Engine; -use jellybase::{federation::Federation, CONF, SECRETS}; use log::warn; use rand::random; use rocket::{ @@ -76,7 +76,7 @@ macro_rules! uri { }; } -pub fn build_rocket(database: Database, federation: Federation) -> Rocket<Build> { +pub fn build_rocket(database: Database) -> Rocket<Build> { rocket::build() .configure(Config { address: std::env::var("BIND_ADDR") @@ -86,8 +86,7 @@ pub fn build_rocket(database: Database, federation: Federation) -> Rocket<Build> .map(|e| e.parse().unwrap()) .unwrap_or(8000), secret_key: SecretKey::derive_from( - SECRETS - .cookie_key + CONF.cookie_key .clone() .unwrap_or_else(|| { warn!("cookie_key not configured, generating a random one."); @@ -99,7 +98,6 @@ pub fn build_rocket(database: Database, federation: Federation) -> Rocket<Build> ..Default::default() }) .manage(database) - .manage(federation) .manage(PlayersyncChannels::default()) .attach(AdHoc::on_response("set server header", |_req, res| { res.set_header(Header::new("server", "jellything")); @@ -129,7 +127,6 @@ pub fn build_rocket(database: Database, federation: Federation) -> Rocket<Build> r_account_settings_post, r_account_settings, r_admin_dashboard, - r_admin_delete_cache, r_admin_import, r_admin_invite, r_admin_log_stream, diff --git a/server/src/ui/admin/mod.rs b/server/src/ui/admin/mod.rs index 3a9e4e2..62c5940 100644 --- a/server/src/ui/admin/mod.rs +++ b/server/src/ui/admin/mod.rs @@ -12,7 +12,7 @@ use super::{ }; use crate::{database::Database, helper::A, locale::AcceptLanguage}; use anyhow::{anyhow, Context}; -use jellybase::{assetfed::AssetInner, federation::Federation, CONF}; +use jellybase::assetfed::AssetInner; use jellycommon::routes::u_admin_dashboard; use jellyimport::{import_wrap, is_importing, IMPORT_ERRORS}; use jellylogic::session::AdminSession; @@ -103,7 +103,6 @@ pub async fn r_admin_remove_invite( pub async fn r_admin_import( session: A<AdminSession>, database: &State<Database>, - _federation: &State<Federation>, incremental: bool, ) -> MyResult<Redirect> { drop(session); @@ -139,26 +138,6 @@ pub async fn r_admin_update_search( Ok(Redirect::temporary(u_admin_dashboard())) } -#[post("/admin/delete_cache")] -pub async fn r_admin_delete_cache( - session: A<AdminSession>, - database: &State<Database>, -) -> MyResult<Redirect> { - drop(session); - let t = Instant::now(); - let r = tokio::fs::remove_dir_all(&CONF.cache_path).await; - tokio::fs::create_dir(&CONF.cache_path).await?; - // admin_dashboard( - // database, - // Some( - // r.map_err(|e| e.into()) - // .map(|_| format!("Cache deleted; took {:?}", t.elapsed())), - // ), - // ) - // .await - Ok(Redirect::temporary(u_admin_dashboard())) -} - static SEM_TRANSCODING: Semaphore = Semaphore::const_new(1); fn is_transcoding() -> bool { SEM_TRANSCODING.available_permits() == 0 diff --git a/server/src/ui/assets.rs b/server/src/ui/assets.rs index 3b9319e..596661a 100644 --- a/server/src/ui/assets.rs +++ b/server/src/ui/assets.rs @@ -4,11 +4,9 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::error::MyResult; -use crate::helper::{cache::CacheControlFile, A}; +use crate::{helper::{cache::CacheControlFile, A}, CONF}; use anyhow::{anyhow, bail, Context}; -use base64::Engine; -use jellybase::{assetfed::AssetInner, database::Database, federation::Federation, CONF}; -use jellycache::async_cache_file; +use jellybase::{assetfed::AssetInner, database::Database}; use jellycommon::{LocalTrack, NodeID, PeopleGroup, SourceTrackKind, TrackSource}; use jellylogic::session::Session; use log::info; @@ -21,22 +19,23 @@ pub const AVIF_SPEED: u8 = 5; #[get("/asset/<token>?<width>")] pub async fn r_asset( _session: A<Session>, - fed: &State<Federation>, token: &str, width: Option<usize>, ) -> MyResult<(ContentType, CacheControlFile)> { let width = width.unwrap_or(2048); let asset = AssetInner::deser(token)?; - let path = if let AssetInner::Federated { host, asset } = asset { - let session = fed.get_session(&host).await?; + let path = + // if let AssetInner::Federated { host, asset } = asset { + // let session = fed.get_session(&host).await?; - let asset = base64::engine::general_purpose::URL_SAFE.encode(asset); - async_cache_file("fed-asset", &asset, |out| async { - session.asset(out, &asset, width).await - }) - .await? - } else { + // let asset = base64::engine::general_purpose::URL_SAFE.encode(asset); + // async_cache_file("fed-asset", &asset, |out| async { + // session.asset(out, &asset, width).await + // }) + // .await? + // } else + { let source = resolve_asset(asset).await.context("resolving asset")?; // fit the resolution into a finite set so the maximum cache is finite too. @@ -56,7 +55,7 @@ pub async fn resolve_asset(asset: AssetInner) -> anyhow::Result<PathBuf> { match asset { AssetInner::Cache(c) => Ok(c.abs()), AssetInner::Assets(c) => Ok(CONF.asset_path.join(c)), - AssetInner::Media(c) => Ok(CONF.media_path.join(c)), + AssetInner::Media(c) => Ok(c), _ => bail!("wrong asset type"), } } @@ -138,7 +137,6 @@ pub async fn r_person_asset( pub async fn r_node_thumbnail( _session: A<Session>, db: &State<Database>, - fed: &State<Federation>, id: A<NodeID>, t: f64, width: Option<usize>, @@ -146,7 +144,7 @@ pub async fn r_node_thumbnail( let node = db.get_node(id.0)?.ok_or(anyhow!("node does not exist"))?; let media = node.media.as_ref().ok_or(anyhow!("no media"))?; - let (thumb_track_index, thumb_track) = media + let (thumb_track_index, _thumb_track) = media .tracks .iter() .enumerate() @@ -171,23 +169,24 @@ pub async fn r_node_thumbnail( return Err(anyhow!("track set to wrong asset type").into()); }; // the track selected might be different from thumb_track - jellytranscoder::thumbnail::create_thumbnail(&CONF.media_path.join(path), t).await? + jellytranscoder::thumbnail::create_thumbnail(&path, t).await? } TrackSource::Remote(_) => { - // TODO in the new system this is preferrably a property of node ext for regular fed - let session = fed - .get_session( - thumb_track - .federated - .last() - .ok_or(anyhow!("federation broken"))?, - ) - .await?; + // // TODO in the new system this is preferrably a property of node ext for regular fed + // let session = fed + // .get_session( + // thumb_track + // .federated + // .last() + // .ok_or(anyhow!("federation broken"))?, + // ) + // .await?; - async_cache_file("fed-thumb", (id.0, t as i64), |out| { - session.node_thumbnail(out, id.0.into(), 2048, t) - }) - .await? + // async_cache_file("fed-thumb", (id.0, t as i64), |out| { + // session.node_thumbnail(out, id.0.into(), 2048, t) + // }) + // .await? + todo!() } }; diff --git a/server/src/ui/error.rs b/server/src/ui/error.rs index 0ea1a8d..05249af 100644 --- a/server/src/ui/error.rs +++ b/server/src/ui/error.rs @@ -3,7 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use jellybase::CONF; +use crate::CONF; use log::info; use rocket::{ catch, diff --git a/server/src/ui/mod.rs b/server/src/ui/mod.rs index 17a8b6f..041dadc 100644 --- a/server/src/ui/mod.rs +++ b/server/src/ui/mod.rs @@ -3,10 +3,9 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::{helper::A, locale::AcceptLanguage}; +use crate::{helper::A, locale::AcceptLanguage, CONF}; use error::MyResult; use home::rocket_uri_macro_r_home; -use jellybase::CONF; use jellylogic::session::Session; use jellyui::{render_page, scaffold::RenderInfo, CustomPage}; use rocket::{ diff --git a/server/src/ui/player.rs b/server/src/ui/player.rs index 94ca6ac..300e9d2 100644 --- a/server/src/ui/player.rs +++ b/server/src/ui/player.rs @@ -4,8 +4,7 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::error::MyResult; -use crate::{database::Database, helper::A, locale::AcceptLanguage}; -use jellybase::CONF; +use crate::{database::Database, helper::A, locale::AcceptLanguage, CONF}; use jellycommon::{ api::NodeFilterSort, stream::{StreamContainer, StreamSpec}, diff --git a/stream/src/lib.rs b/stream/src/lib.rs index ccc5cb9..8352eaf 100644 --- a/stream/src/lib.rs +++ b/stream/src/lib.rs @@ -24,13 +24,12 @@ use std::{ io::SeekFrom, ops::Range, path::PathBuf, - sync::{Arc, LazyLock}, + sync::{Arc, LazyLock, Mutex}, }; use stream_info::{stream_info, write_stream_info}; use tokio::{ fs::File, io::{duplex, AsyncReadExt, AsyncSeekExt, AsyncWriteExt, DuplexStream}, - sync::Mutex, }; #[rustfmt::skip] @@ -43,13 +42,14 @@ pub struct Config { #[serde(default)] pub offer_av1: bool, } +pub static CONF_PRELOAD: Mutex<Option<Config>> = Mutex::new(None); static CONF: LazyLock<Config> = LazyLock::new(|| { CONF_PRELOAD - .blocking_lock() + .lock() + .unwrap() .take() - .expect("cache config not preloaded. logic error") + .expect("stream config not preloaded. logic error") }); -static CONF_PRELOAD: Mutex<Option<Config>> = Mutex::const_new(None); #[derive(Debug)] pub struct SMediaInfo { diff --git a/tool/src/add.rs b/tool/src/add.rs index 2179a40..04328b2 100644 --- a/tool/src/add.rs +++ b/tool/src/add.rs @@ -4,11 +4,9 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use crate::cli::Action; -use anyhow::anyhow; use dialoguer::{theme::ColorfulTheme, Confirm, FuzzySelect, Input}; -use jellybase::SECRETS; use jellycommon::TraktKind; -use jellyimport::trakt::Trakt; +use jellyimport::get_trakt; use log::warn; use std::{ fmt::Display, @@ -37,13 +35,7 @@ pub async fn add(action: Action) -> anyhow::Result<()> { .interact_text() .unwrap(); - let trakt = Trakt::new( - SECRETS - .api - .trakt - .as_ref() - .ok_or(anyhow!("no trakt api key configured"))?, - ); + let trakt = get_trakt()?; let results = trakt.search(search_kinds, &name).await?; diff --git a/tool/src/main.rs b/tool/src/main.rs index 9ce7cf1..adf0a35 100644 --- a/tool/src/main.rs +++ b/tool/src/main.rs @@ -4,17 +4,12 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ -use anyhow::anyhow; use clap::Parser; -use jellybase::{CONF, SECRETS}; -use jellyclient::Instance; -use jellycommon::user::CreateSessionParams; use jellytool::{ add::add, cli::{Action, Args}, migrate::migrate, }; -use log::info; fn main() -> anyhow::Result<()> { env_logger::builder() @@ -30,34 +25,35 @@ fn main() -> anyhow::Result<()> { .unwrap() .block_on(add(a)), a @ Action::Migrate { .. } => migrate(a), - Action::Reimport { - hostname, - no_tls, - no_incremental, - } => tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap() - .block_on(async move { - let inst = Instance::new(hostname.unwrap_or(CONF.hostname.clone()), !no_tls); - info!("login"); - let session = inst - .login(CreateSessionParams { - drop_permissions: None, - expire: None, - password: SECRETS - .admin_password - .clone() - .ok_or(anyhow!("admin account required"))?, - username: CONF - .admin_username - .clone() - .ok_or(anyhow!("admin account required"))?, - }) - .await?; + _ => Ok(()), + // Action::Reimport { + // hostname, + // no_tls, + // no_incremental, + // } => tokio::runtime::Builder::new_multi_thread() + // .enable_all() + // .build() + // .unwrap() + // .block_on(async move { + // let inst = Instance::new(hostname.unwrap_or(CONF.hostname.clone()), !no_tls); + // info!("login"); + // let session = inst + // .login(CreateSessionParams { + // drop_permissions: None, + // expire: None, + // password: SECRETS + // .admin_password + // .clone() + // .ok_or(anyhow!("admin account required"))?, + // username: CONF + // .admin_username + // .clone() + // .ok_or(anyhow!("admin account required"))?, + // }) + // .await?; - session.reimport(!no_incremental).await?; - Ok(()) - }), + // session.reimport(!no_incremental).await?; + // Ok(()) + // }), } } diff --git a/transcoder/src/lib.rs b/transcoder/src/lib.rs index c49f52c..665f470 100644 --- a/transcoder/src/lib.rs +++ b/transcoder/src/lib.rs @@ -6,8 +6,8 @@ #![feature(exit_status_error)] use serde::{Deserialize, Serialize}; -use std::sync::LazyLock; -use tokio::sync::{Mutex, Semaphore}; +use std::sync::{LazyLock, Mutex}; +use tokio::sync::Semaphore; pub mod fragment; pub mod image; @@ -27,13 +27,14 @@ pub struct Config { pub x264_preset: Option<String>, } +pub static CONF_PRELOAD: Mutex<Option<Config>> = Mutex::new(None); static CONF: LazyLock<Config> = LazyLock::new(|| { CONF_PRELOAD - .blocking_lock() + .lock() + .unwrap() .take() - .expect("cache config not preloaded. logic error") + .expect("transcoder config not preloaded. logic error") }); -static CONF_PRELOAD: Mutex<Option<Config>> = Mutex::const_new(None); static LOCAL_IMAGE_TRANSCODING_TASKS: Semaphore = Semaphore::const_new(8); static LOCAL_VIDEO_TRANSCODING_TASKS: Semaphore = Semaphore::const_new(2); diff --git a/ui/src/admin/mod.rs b/ui/src/admin/mod.rs index 74a5e1a..ade0d97 100644 --- a/ui/src/admin/mod.rs +++ b/ui/src/admin/mod.rs @@ -4,8 +4,8 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ -pub mod user; pub mod log; +pub mod user; use crate::{Page, locale::Language, scaffold::FlashDisplay}; use jellycommon::routes::{ diff --git a/ui/src/lib.rs b/ui/src/lib.rs index 4f1901a..cbfc298 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -3,9 +3,12 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ +pub mod account; +pub mod admin; pub mod filter_sort; pub mod format; pub mod home; +pub mod items; pub mod locale; pub mod node_card; pub mod node_page; @@ -13,9 +16,6 @@ pub mod props; pub mod scaffold; pub mod search; pub mod stats; -pub mod items; -pub mod admin; -pub mod account; use locale::Language; use markup::DynRender; @@ -41,7 +41,14 @@ static CONF: LazyLock<Config> = LazyLock::new(|| { .take() .expect("cache config not preloaded. logic error") }); -static CONF_PRELOAD: Mutex<Option<Config>> = Mutex::new(None); +pub static CONF_PRELOAD: Mutex<Option<Config>> = Mutex::new(None); + +pub fn get_brand() -> String { + CONF.brand.clone() +} +pub fn get_slogan() -> String { + CONF.slogan.clone() +} /// render as supertrait would be possible but is not /// dyn compatible and I really dont want to expose generics |