diff options
| author | metamuffin <metamuffin@disroot.org> | 2026-01-24 04:31:48 +0100 |
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2026-01-24 04:31:48 +0100 |
| commit | b2e88a8beabf04adc28947cf82996e8692a68b71 (patch) | |
| tree | 23d66c8672b69cce7835ffabae4092669062ada8 | |
| parent | 774f64c0789529884dd7a5232f190e347ad29532 (diff) | |
| download | jellything-b2e88a8beabf04adc28947cf82996e8692a68b71.tar jellything-b2e88a8beabf04adc28947cf82996e8692a68b71.tar.bz2 jellything-b2e88a8beabf04adc28947cf82996e8692a68b71.tar.zst | |
move things around; kv crate
70 files changed, 591 insertions, 1060 deletions
@@ -522,6 +522,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] +name = "chacha20" +version = "0.10.0-rc.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31cd65b2ca03198c223cd9a8fa1152c4ec251cd79049f6dc584152ad3fb5ba9d" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core 0.10.0-rc-5", +] + +[[package]] name = "chashmap" version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1233,6 +1244,19 @@ dependencies = [ ] [[package]] +name = "getrandom" +version = "0.4.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b99f0d993a2b9b97b9a201193aa8ad21305cde06a3be9a7e1f8f4201e5cc27e" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core 0.10.0-rc-5", + "wasip2", +] + +[[package]] name = "ghash" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1797,10 +1821,10 @@ dependencies = [ "anyhow", "base64", "humansize", + "jellykv", "log", "percent-encoding", "rand 0.9.2", - "rocksdb", "serde", "serde_json", "sha2", @@ -1820,10 +1844,9 @@ version = "0.1.0" dependencies = [ "anyhow", "bytemuck", + "jellykv", "jellyobject", "log", - "redb", - "rocksdb", "serde", "serde_json", ] @@ -1865,6 +1888,16 @@ dependencies = [ ] [[package]] +name = "jellykv" +version = "0.1.0" +dependencies = [ + "anyhow", + "rand 0.10.0-rc.7", + "redb", + "rocksdb", +] + +[[package]] name = "jellylogic" version = "0.1.0" dependencies = [ @@ -1943,8 +1976,10 @@ dependencies = [ "jellycache", "jellycommon", "jellyimport", + "jellykv", "jellystream", "jellytranscoder", + "jellyui", "log", "rand 0.9.2", "rocket", @@ -2936,6 +2971,17 @@ dependencies = [ ] [[package]] +name = "rand" +version = "0.10.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d3e6a909ceda8ceb12ef039b675ecf4bbe6def127e773cac109ab8347633766" +dependencies = [ + "chacha20", + "getrandom 0.4.0-rc.0", + "rand_core 0.10.0-rc-5", +] + +[[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2989,6 +3035,12 @@ dependencies = [ ] [[package]] +name = "rand_core" +version = "0.10.0-rc-5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05a06e03bd1f2ae861ab9e7498b6c64ed3dadb9ce175c0464a2522a5f23c0045" + +[[package]] name = "rand_distr" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -17,6 +17,7 @@ members = [ "ui/client-style", "remuxer", "common/object", + "kv", ] resolver = "3" diff --git a/cache/Cargo.toml b/cache/Cargo.toml index c545fd2..cd891b8 100644 --- a/cache/Cargo.toml +++ b/cache/Cargo.toml @@ -13,4 +13,4 @@ rand = "0.9.2" serde = "1.0.228" serde_json = "1.0.145" percent-encoding = "2.3.2" -rocksdb = { version = "0.24.0", features = ["multi-threaded-cf"] } +jellykv = { path = "../kv" } diff --git a/cache/src/backends/mod.rs b/cache/src/backends/mod.rs deleted file mode 100644 index 52a954b..0000000 --- a/cache/src/backends/mod.rs +++ /dev/null @@ -1,28 +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 dummy; -pub mod filesystem; -pub mod rocksdb; - -use crate::{ - CONF, - backends::{dummy::Dummy, filesystem::Filesystem, rocksdb::Rocksdb}, -}; -use anyhow::{Result, bail}; - -pub(crate) trait CacheStorage: Send + Sync + 'static { - fn store(&self, key: String, value: &[u8]) -> Result<()>; - fn read(&self, key: &str) -> Result<Option<Vec<u8>>>; -} - -pub fn init_backend() -> Result<Box<dyn CacheStorage>> { - Ok(match CONF.driver.as_str() { - "filesystem" => Box::new(Filesystem::new(&CONF)), - "rocksdb" => Box::new(Rocksdb::new(&CONF)?), - "dummy" => Box::new(Dummy), - _ => bail!("unknown driver"), - }) -} diff --git a/cache/src/backends/rocksdb.rs b/cache/src/backends/rocksdb.rs deleted file mode 100644 index 9db86dd..0000000 --- a/cache/src/backends/rocksdb.rs +++ /dev/null @@ -1,26 +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::{Config, backends::CacheStorage}; -use anyhow::Result; -use rocksdb::DB; - -pub struct Rocksdb(DB); - -impl Rocksdb { - pub fn new(config: &Config) -> Result<Self> { - Ok(Self(rocksdb::DB::open_default(config.path.clone())?)) - } -} - -impl CacheStorage for Rocksdb { - fn store(&self, key: String, value: &[u8]) -> Result<()> { - Ok(self.0.put(key, value)?) - } - fn read(&self, key: &str) -> Result<Option<Vec<u8>>> { - Ok(self.0.get(key)?) - } -} diff --git a/cache/src/lib.rs b/cache/src/lib.rs index 9559fbc..be2b331 100644 --- a/cache/src/lib.rs +++ b/cache/src/lib.rs @@ -3,65 +3,41 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -mod backends; mod helper; -use crate::backends::{CacheStorage, init_backend}; use anyhow::{Context, Result, anyhow}; +pub use helper::{EscapeKey, HashKey}; +use jellykv::BlobStorage; use log::{info, warn}; use serde::{Deserialize, Serialize}; use std::{ any::Any, collections::{BTreeMap, HashMap}, hash::{DefaultHasher, Hash, Hasher}, - path::PathBuf, sync::{ - Arc, LazyLock, Mutex, OnceLock, RwLock, + Arc, LazyLock, Mutex, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering}, }, time::Instant, }; -pub use helper::{EscapeKey, HashKey}; - -#[derive(Debug, Deserialize)] -pub struct Config { - driver: String, - path: PathBuf, - max_in_memory_cache_size: usize, -} - const CACHE_GENERATION_BUCKET_COUNT: usize = 1024; pub static CACHE_GENERATION_LOCKS: LazyLock<[Mutex<()>; CACHE_GENERATION_BUCKET_COUNT]> = LazyLock::new(|| [(); CACHE_GENERATION_BUCKET_COUNT].map(|_| Mutex::new(()))); thread_local! { pub static WITHIN_CACHE_FILE: AtomicBool = const { AtomicBool::new(false) }; } -pub static CONF_PRELOAD: std::sync::Mutex<Option<Config>> = std::sync::Mutex::new(None); -static CONF: LazyLock<Config> = LazyLock::new(|| { - CONF_PRELOAD - .lock() - .unwrap() - .take() - .expect("cache config not preloaded. logic error") -}); - -static CACHE_STORE: OnceLock<Box<dyn CacheStorage>> = OnceLock::new(); - -pub fn init_cache() -> Result<()> { - CACHE_STORE - .set(init_backend().context("cache backend")?) - .map_err(|_| ()) - .unwrap(); - Ok(()) +pub struct Cache { + storage: Box<dyn BlobStorage>, + memory_cache: RwLock<HashMap<String, InMemoryCacheEntry>>, + memory_cache_size: AtomicUsize, + max_memory_cache_size: usize, } -pub fn init_cache_dummy() -> Result<()> { - *CONF_PRELOAD.lock().unwrap() = Some(Config { - driver: "dummy".to_string(), - path: PathBuf::default(), - max_in_memory_cache_size: 0, - }); - init_cache() + +pub struct InMemoryCacheEntry { + size: usize, + last_access: Instant, + object: Arc<dyn Any + Send + Sync + 'static>, } fn bucket(key: &str) -> usize { @@ -70,122 +46,130 @@ fn bucket(key: &str) -> usize { h.finish() as usize % CACHE_GENERATION_BUCKET_COUNT } -pub fn cache(key: &str, generate: impl FnOnce() -> Result<Vec<u8>>) -> Result<Vec<u8>> { - // we need a lock even if it exists since somebody might be still in the process of writing. - let already_within = WITHIN_CACHE_FILE.with(|a| a.swap(true, Ordering::Relaxed)); - let _guard = if already_within { - // TODO stupid hack to avoid deadlock for nested calls; not locking is fine but might cause double-generating - CACHE_GENERATION_LOCKS[bucket(key)].try_lock().ok() - } else { - CACHE_GENERATION_LOCKS[bucket(key)].lock().ok() - }; - - let store = CACHE_STORE.get().unwrap(); - - let out = match store.read(&key)? { - Some(x) => x, - None => { - let value = generate()?; - store.store(key.to_owned(), &value)?; - value +impl Cache { + pub fn new(storage: Box<dyn BlobStorage>, max_memory_cache_size: usize) -> Self { + Self { + max_memory_cache_size, + storage, + memory_cache: HashMap::new().into(), + memory_cache_size: AtomicUsize::new(0), } - }; - - if !already_within { - WITHIN_CACHE_FILE.with(|a| a.swap(false, Ordering::Relaxed)); } - drop(_guard); - Ok(out) -} + pub fn cache(&self, key: &str, generate: impl FnOnce() -> Result<Vec<u8>>) -> Result<Vec<u8>> { + // we need a lock even if it exists since somebody might be still in the process of writing. + let already_within = WITHIN_CACHE_FILE.with(|a| a.swap(true, Ordering::Relaxed)); + let _guard = if already_within { + // TODO stupid hack to avoid deadlock for nested calls; not locking is fine but might cause double-generating + CACHE_GENERATION_LOCKS[bucket(key)].try_lock().ok() + } else { + CACHE_GENERATION_LOCKS[bucket(key)].lock().ok() + }; -pub fn cache_read(key: &str) -> Result<Option<Vec<u8>>> { - CACHE_STORE.get().unwrap().read(key) -} -pub fn cache_store(key: String, generate: impl FnOnce() -> Result<Vec<u8>>) -> Result<String> { - cache(&key, generate)?; - Ok(key) -} + let out = match self.storage.read(&key)? { + Some(x) => x, + None => { + let value = generate()?; + self.storage.store(key, &value)?; + value + } + }; -pub struct InMemoryCacheEntry { - size: usize, - last_access: Instant, - object: Arc<dyn Any + Send + Sync + 'static>, -} -pub static CACHE_IN_MEMORY_OBJECTS: LazyLock<RwLock<HashMap<String, InMemoryCacheEntry>>> = - LazyLock::new(|| RwLock::new(HashMap::new())); -pub static CACHE_IN_MEMORY_SIZE: AtomicUsize = AtomicUsize::new(0); + if !already_within { + WITHIN_CACHE_FILE.with(|a| a.swap(false, Ordering::Relaxed)); + } + drop(_guard); + Ok(out) + } -pub fn cache_memory<Fun, T>(key: &str, mut generate: Fun) -> Result<Arc<T>, anyhow::Error> -where - Fun: FnMut() -> Result<T, anyhow::Error>, - T: Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static, -{ - if !key.ends_with(".json") { - warn!("cache_memory key not ending in .json: {key:?}") + pub fn cache_read(&self, key: &str) -> Result<Option<Vec<u8>>> { + self.storage.read(key) + } + pub fn cache_store( + &self, + key: String, + generate: impl FnOnce() -> Result<Vec<u8>>, + ) -> Result<String> { + self.cache(&key, generate)?; + Ok(key) } + pub fn cache_memory<Fun, T>( + &self, + key: &str, + mut generate: Fun, + ) -> Result<Arc<T>, anyhow::Error> + where + Fun: FnMut() -> Result<T, anyhow::Error>, + T: Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static, { - let mut g = CACHE_IN_MEMORY_OBJECTS.write().unwrap(); - if let Some(entry) = g.get_mut(key) { - entry.last_access = Instant::now(); - let object = entry - .object - .clone() - .downcast::<T>() - .map_err(|_| anyhow!("inconsistent types for in-memory cache"))?; - return Ok(object); + if !key.ends_with(".json") { + warn!("cache_memory key not ending in .json: {key:?}") } - } - let data = cache(&key, move || { - let object = generate()?; - Ok(serde_json::to_vec(&object)?) - })?; - let size = data.len(); - let object = serde_json::from_slice::<T>(&data).context("decoding cache object")?; - let object = Arc::new(object); + { + let mut g = self.memory_cache.write().unwrap(); + if let Some(entry) = g.get_mut(key) { + entry.last_access = Instant::now(); + let object = entry + .object + .clone() + .downcast::<T>() + .map_err(|_| anyhow!("inconsistent types for in-memory cache"))?; + return Ok(object); + } + } - { - let mut g = CACHE_IN_MEMORY_OBJECTS.write().unwrap(); - g.insert( - key.to_owned(), - InMemoryCacheEntry { - size, - last_access: Instant::now(), - object: object.clone(), - }, - ); - CACHE_IN_MEMORY_SIZE.fetch_add(size, Ordering::Relaxed); - } + let data = self.cache(&key, move || { + let object = generate()?; + Ok(serde_json::to_vec(&object)?) + })?; + let size = data.len(); + let object = serde_json::from_slice::<T>(&data).context("decoding cache object")?; + let object = Arc::new(object); - cleanup_cache(); + { + let mut g = self.memory_cache.write().unwrap(); + g.insert( + key.to_owned(), + InMemoryCacheEntry { + size, + last_access: Instant::now(), + object: object.clone(), + }, + ); + self.memory_cache_size.fetch_add(size, Ordering::Relaxed); + } - Ok(object) -} + self.cleanup_cache(); -pub fn cleanup_cache() { - let current_size = CACHE_IN_MEMORY_SIZE.load(Ordering::Relaxed); - if current_size < CONF.max_in_memory_cache_size { - return; + Ok(object) } - info!("running cache eviction"); - let mut g = CACHE_IN_MEMORY_OBJECTS.write().unwrap(); - // TODO: if two entries have *exactly* the same size, only one of the will be remove; this is fine for now - let mut k = BTreeMap::new(); - for (loc, entry) in g.iter() { - k.insert(entry.last_access.elapsed(), (loc.to_owned(), entry.size)); - } - let mut reduction = 0; - for (loc, size) in k.values().rev().take(k.len().div_ceil(2)) { - g.remove(loc); - reduction += size; - } - CACHE_IN_MEMORY_SIZE.fetch_sub(reduction, Ordering::Relaxed); - drop(g); + fn cleanup_cache(&self) { + let current_size = self.memory_cache_size.load(Ordering::Relaxed); + if current_size < self.max_memory_cache_size { + return; + } + info!("running cache eviction"); + let mut g = self.memory_cache.write().unwrap(); - info!( - "done, {} freed", - humansize::format_size(reduction, humansize::DECIMAL) - ); + // TODO: if two entries have *exactly* the same size, only one of the will be remove; this is fine for now + let mut k = BTreeMap::new(); + for (loc, entry) in g.iter() { + k.insert(entry.last_access.elapsed(), (loc.to_owned(), entry.size)); + } + let mut reduction = 0; + for (loc, size) in k.values().rev().take(k.len().div_ceil(2)) { + g.remove(loc); + reduction += size; + } + self.memory_cache_size + .fetch_sub(reduction, Ordering::Relaxed); + drop(g); + + info!( + "done, {} freed", + humansize::format_size(reduction, humansize::DECIMAL) + ); + } } diff --git a/database/Cargo.toml b/database/Cargo.toml index d15cee6..078694e 100644 --- a/database/Cargo.toml +++ b/database/Cargo.toml @@ -8,7 +8,6 @@ jellyobject = { path = "../common/object" } serde = { version = "1.0.228", features = ["derive"] } log = { workspace = true } serde_json = "1.0.145" -redb = "3.1.0" anyhow = "1.0.100" -rocksdb = { version = "0.24.0", features = ["multi-threaded-cf"] } bytemuck = { version = "1.24.0", features = ["derive"] } +jellykv = { path = "../kv" } diff --git a/database/src/lib.rs b/database/src/lib.rs index cd9777d..d5d6d21 100644 --- a/database/src/lib.rs +++ b/database/src/lib.rs @@ -3,13 +3,14 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -pub mod backends; -pub mod sort; +pub mod filter; pub mod prefix_iterator; -pub mod table; pub mod query; -pub mod filter; +pub mod sort; +pub mod table; #[cfg(test)] pub mod test_shared; pub type Pad32 = u32; + +pub use jellykv as kv; diff --git a/database/src/sort/mod.rs b/database/src/sort/mod.rs index b058766..e87630c 100644 --- a/database/src/sort/mod.rs +++ b/database/src/sort/mod.rs @@ -4,8 +4,9 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::{backends::WriteTransaction, table::RowNum}; +use crate::table::RowNum; use anyhow::Result; +use jellykv::WriteTransaction; use jellyobject::Object; pub mod none; diff --git a/database/src/sort/none.rs b/database/src/sort/none.rs index 9285998..65454a2 100644 --- a/database/src/sort/none.rs +++ b/database/src/sort/none.rs @@ -5,12 +5,12 @@ */ use crate::{ - backends::WriteTransaction, filter::binning::Binning, sort::Index, table::{RowNum, TableNum}, }; use anyhow::Result; +use jellykv::WriteTransaction; use jellyobject::Object; pub struct UnsortedIndex { diff --git a/database/src/sort/value.rs b/database/src/sort/value.rs index 6eb638c..bd79db9 100644 --- a/database/src/sort/value.rs +++ b/database/src/sort/value.rs @@ -5,13 +5,13 @@ */ use crate::{ - backends::WriteTransaction, filter::binning::Binning, query::{MultiBehaviour, ValueSortComponent}, sort::Index, table::{RowNum, TableNum}, }; use anyhow::Result; +use jellykv::WriteTransaction; use jellyobject::Object; pub struct ValueIndex { diff --git a/database/src/table.rs b/database/src/table.rs index 4535b4c..c3b4342 100644 --- a/database/src/table.rs +++ b/database/src/table.rs @@ -4,12 +4,9 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::{ - backends::{ReadTransaction, WriteTransaction}, - query::Query, - sort::Index, -}; +use crate::{query::Query, sort::Index}; use anyhow::{Result, anyhow}; +use jellykv::{ReadTransaction, WriteTransaction}; use jellyobject::ObjectBuffer; pub type TableNum = u64; @@ -107,15 +104,15 @@ impl Table { #[cfg(test)] mod test { use crate::{ - backends::{Database, new_memory}, table::Table, test_shared::{NAME, new_bob}, }; use anyhow::Result; + use jellykv::Database; #[test] pub fn insert_get() -> Result<()> { - let db = new_memory(); + let db = jellykv_memory::new(); let table = Table::new(5); let mut bob_row = 0; @@ -136,7 +133,7 @@ mod test { #[test] pub fn update() -> Result<()> { - let db = new_memory(); + let db = jellykv_memory::new(); let table = Table::new(5); let mut bob_row = 0; diff --git a/import/src/lib.rs b/import/src/lib.rs index 56da625..7d48867 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -9,17 +9,17 @@ pub mod plugins; pub mod reporting; use crate::{ - plugins::{ImportContext, ImportPlugin, infojson::is_info_json, init_plugins, misc::is_cover}, + plugins::{PluginContext, ImportPlugin, infojson::is_info_json, init_plugins, misc::is_cover}, reporting::IMPORT_PROGRESS, }; use anyhow::{Context, Result, anyhow}; -use jellycache::{HashKey, cache_memory, cache_store}; +use jellycache::{Cache, HashKey}; use jellycommon::{ jellyobject::{self, ObjectBuffer, Path as TagPath, fields}, *, }; use jellydb::{ - backends::Database, + kv::Database, query::{Filter, Query, Sort}, table::{RowNum, Table}, }; @@ -84,7 +84,8 @@ pub fn is_importing() -> bool { } #[derive(Clone)] -pub struct DatabaseTables { +pub struct ImportConfig { + pub cache: Arc<Cache>, pub db: Arc<dyn Database>, pub nodes: Arc<Table>, pub import_meta: Arc<Table>, @@ -102,7 +103,7 @@ fn node_slug_query(slug: &str) -> Query { } } -impl DatabaseTables { +impl ImportConfig { pub fn update_node( &self, node: RowNum, @@ -148,7 +149,7 @@ impl DatabaseTables { } } -pub async fn import_wrap(db: DatabaseTables, incremental: bool) -> Result<()> { +pub async fn import_wrap(db: ImportConfig, incremental: bool) -> Result<()> { let _sem = IMPORT_SEM.try_acquire().context("already importing")?; let rt = Handle::current(); @@ -170,7 +171,7 @@ pub async fn import_wrap(db: DatabaseTables, incremental: bool) -> Result<()> { Ok(()) } -fn import(dba: DatabaseTables, rt: &Handle, incremental: bool) -> Result<()> { +fn import(dba: ImportConfig, rt: &Handle, incremental: bool) -> Result<()> { let plugins = init_plugins(&CONF.api); let files = Mutex::new(Vec::new()); import_traverse( @@ -230,7 +231,7 @@ pub struct InheritedFlags { fn import_traverse( path: &Path, - dba: &DatabaseTables, + dba: &ImportConfig, incremental: bool, parent: Option<RowNum>, mut iflags: InheritedFlags, @@ -297,7 +298,7 @@ fn import_traverse( } fn import_file( - dba: &DatabaseTables, + dba: &ImportConfig, rt: &Handle, pending_nodes: &Mutex<HashSet<RowNum>>, plugins: &[Box<dyn ImportPlugin>], @@ -306,7 +307,7 @@ fn import_file( iflags: InheritedFlags, ) { let mut all_ok = true; - let ct = ImportContext { + let ct = PluginContext { dba, rt, iflags, @@ -412,7 +413,7 @@ fn import_file( } fn process_node( - dba: &DatabaseTables, + dba: &ImportConfig, rt: &Handle, plugins: &[Box<dyn ImportPlugin>], pending_nodes: &Mutex<HashSet<RowNum>>, @@ -433,7 +434,7 @@ fn process_node( reporting::catch( p.process( - &ImportContext { + &PluginContext { dba, rt, iflags: InheritedFlags::default(), @@ -447,7 +448,7 @@ fn process_node( } } -fn compare_mtime(dba: &DatabaseTables, path: &Path) -> Result<bool> { +fn compare_mtime(dba: &ImportConfig, path: &Path) -> Result<bool> { let meta = path.metadata()?; let mtime = meta.modified()?.duration_since(UNIX_EPOCH)?.as_secs(); let mut was_changed = false; @@ -474,7 +475,7 @@ fn compare_mtime(dba: &DatabaseTables, path: &Path) -> Result<bool> { Ok(was_changed) } -fn update_mtime(dba: &DatabaseTables, path: &Path) -> Result<()> { +fn update_mtime(dba: &ImportConfig, path: &Path) -> Result<()> { let meta = path.metadata()?; let mtime = meta.modified()?.duration_since(UNIX_EPOCH)?.as_secs(); dba.db.write_transaction(&mut |txn| { @@ -522,46 +523,47 @@ fn get_node_slug(path: &Path) -> Option<String> { } } -pub fn read_media_metadata(path: &Path) -> Result<Arc<matroska::Segment>> { - cache_memory( - &format!("media/metadata/{}.json", HashKey(path)), - move || { - let media = File::open(path)?; - let mut media = create_demuxer_autodetect(Box::new(media))? - .ok_or(anyhow!("media format unknown"))?; +pub fn read_media_metadata(cache: &Cache, path: &Path) -> Result<Arc<matroska::Segment>> { + cache + .cache_memory( + &format!("media/metadata/{}.json", HashKey(path)), + move || { + let media = File::open(path)?; + let mut media = create_demuxer_autodetect(Box::new(media))? + .ok_or(anyhow!("media format unknown"))?; - let info = media.info()?; - let tracks = media.tracks()?; - let tags = media.tags()?; - let mut attachments = media.attachments()?; - let chapters = media.chapters()?; + let info = media.info()?; + let tracks = media.tracks()?; + let tags = media.tags()?; + let mut attachments = media.attachments()?; + let chapters = media.chapters()?; - // Replace data of useful attachments with cache key; delete data of all others - if let Some(attachments) = &mut attachments { - for att in &mut attachments.files { - if let Some(fname) = is_useful_attachment(&att) { - let key = cache_store( - format!("media/attachment/{}-{fname}", HashKey(path)), - || Ok(att.data.clone()), - )?; - att.data = key.as_bytes().to_vec(); - } else { - att.data.clear(); + // Replace data of useful attachments with cache key; delete data of all others + if let Some(attachments) = &mut attachments { + for att in &mut attachments.files { + if let Some(fname) = is_useful_attachment(&att) { + let key = cache.cache_store( + format!("media/attachment/{}-{fname}", HashKey(path)), + || Ok(att.data.clone()), + )?; + att.data = key.as_bytes().to_vec(); + } else { + att.data.clear(); + } } } - } - Ok(Segment { - info, - tracks, - tags: tags.into_iter().collect(), - attachments, - chapters, - ..Default::default() - }) - }, - ) - .context("reading media metadata") + Ok(Segment { + info, + tracks, + tags: tags.into_iter().collect(), + attachments, + chapters, + ..Default::default() + }) + }, + ) + .context("reading media metadata") } pub fn is_useful_attachment(a: &AttachedFile) -> Option<&'static str> { diff --git a/import/src/plugins/acoustid.rs b/import/src/plugins/acoustid.rs index 9edcb63..9891927 100644 --- a/import/src/plugins/acoustid.rs +++ b/import/src/plugins/acoustid.rs @@ -5,7 +5,7 @@ */ use crate::{ USER_AGENT, - plugins::{ImportContext, ImportPlugin, PluginInfo}, + plugins::{PluginContext, ImportPlugin, PluginInfo}, }; use anyhow::{Context, Result}; use jellycache::{HashKey, cache_memory}; @@ -169,7 +169,7 @@ impl ImportPlugin for AcoustID { ..Default::default() } } - fn media(&self, ct: &ImportContext, node: RowNum, path: &Path, _seg: &Segment) -> Result<()> { + fn media(&self, ct: &PluginContext, node: RowNum, path: &Path, _seg: &Segment) -> Result<()> { if !ct.iflags.use_acoustid { return Ok(()); } diff --git a/import/src/plugins/infojson.rs b/import/src/plugins/infojson.rs index 3e6bdb8..d62983d 100644 --- a/import/src/plugins/infojson.rs +++ b/import/src/plugins/infojson.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::plugins::{ImportContext, ImportPlugin, PluginInfo}; +use crate::plugins::{PluginContext, ImportPlugin, PluginInfo}; use anyhow::{Context, Result, anyhow}; use chrono::{Utc, format::Parsed}; use jellycache::cache_read; @@ -171,7 +171,7 @@ impl ImportPlugin for Infojson { ..Default::default() } } - fn file(&self, ct: &ImportContext, parent: RowNum, path: &Path) -> Result<()> { + fn file(&self, ct: &PluginContext, parent: RowNum, path: &Path) -> Result<()> { let filename = path.file_name().unwrap().to_string_lossy(); if filename != "channel.info.json" { return Ok(()); @@ -208,7 +208,7 @@ impl ImportPlugin for Infojson { }) } - fn media(&self, ct: &ImportContext, row: RowNum, _path: &Path, seg: &Segment) -> Result<()> { + fn media(&self, ct: &PluginContext, row: RowNum, _path: &Path, seg: &Segment) -> Result<()> { let infojson = seg .attachments .iter() diff --git a/import/src/plugins/media_info.rs b/import/src/plugins/media_info.rs index b750070..9b79efa 100644 --- a/import/src/plugins/media_info.rs +++ b/import/src/plugins/media_info.rs @@ -4,7 +4,7 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::plugins::{ImportContext, ImportPlugin, PluginInfo}; +use crate::plugins::{PluginContext, ImportPlugin, PluginInfo}; use anyhow::Result; use jellycommon::{ jellyobject::{Object, ObjectBuffer}, @@ -23,7 +23,7 @@ impl ImportPlugin for MediaInfo { ..Default::default() } } - fn media(&self, ct: &ImportContext, row: RowNum, path: &Path, seg: &Segment) -> Result<()> { + fn media(&self, ct: &PluginContext, row: RowNum, path: &Path, seg: &Segment) -> Result<()> { let size = path.metadata()?.len(); ct.dba.db.write_transaction(&mut |txn| { let mut node = ct.dba.nodes.get(txn, row)?.unwrap(); diff --git a/import/src/plugins/misc.rs b/import/src/plugins/misc.rs index ff08d87..babbcec 100644 --- a/import/src/plugins/misc.rs +++ b/import/src/plugins/misc.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::plugins::{ImportContext, ImportPlugin, PluginInfo}; +use crate::plugins::{PluginContext, ImportPlugin, PluginInfo}; use anyhow::{Context, Result, bail}; use jellycache::{HashKey, cache_store}; use jellycommon::{jellyobject::inspect::Inspector, *}; @@ -22,7 +22,7 @@ impl ImportPlugin for ImageFiles { ..Default::default() } } - fn file(&self, ct: &ImportContext, row: RowNum, path: &Path) -> Result<()> { + fn file(&self, ct: &PluginContext, row: RowNum, path: &Path) -> Result<()> { let filename = path.file_name().unwrap().to_string_lossy(); let slot = match filename.as_ref() { "poster.jpeg" | "poster.webp" | "poster.png" => PICT_COVER, @@ -66,7 +66,7 @@ impl ImportPlugin for ImageAttachments { ..Default::default() } } - fn media(&self, ct: &ImportContext, row: RowNum, _path: &Path, seg: &Segment) -> Result<()> { + fn media(&self, ct: &PluginContext, row: RowNum, _path: &Path, seg: &Segment) -> Result<()> { let Some(cover) = seg .attachments .iter() @@ -94,7 +94,7 @@ impl ImportPlugin for General { ..Default::default() } } - fn instruction(&self, ct: &ImportContext, node: RowNum, line: &str) -> Result<()> { + fn instruction(&self, ct: &PluginContext, node: RowNum, line: &str) -> Result<()> { if line == "hidden" { ct.dba.update_node(node, |node| { node.as_object().insert(NO_VISIBILITY, VISI_HIDDEN) @@ -144,7 +144,7 @@ impl ImportPlugin for Children { ..Default::default() } } - fn file(&self, ct: &ImportContext, parent: RowNum, path: &Path) -> Result<()> { + fn file(&self, ct: &PluginContext, parent: RowNum, path: &Path) -> Result<()> { // TODO use idents // let filename = path.file_name().unwrap().to_string_lossy(); // if filename.as_ref() == "children" { @@ -176,7 +176,7 @@ impl ImportPlugin for EpisodeIndex { ..Default::default() } } - fn media(&self, ct: &ImportContext, node: RowNum, path: &Path, _seg: &Segment) -> Result<()> { + fn media(&self, ct: &PluginContext, node: RowNum, path: &Path, _seg: &Segment) -> Result<()> { let filename = path.file_name().unwrap().to_string_lossy(); if let Some(cap) = RE_EPISODE_FILENAME.captures(&filename) { if let Some(episode) = cap.name("episode").map(|m| m.as_str()) { diff --git a/import/src/plugins/mod.rs b/import/src/plugins/mod.rs index 91be437..d70c6e1 100644 --- a/import/src/plugins/mod.rs +++ b/import/src/plugins/mod.rs @@ -15,15 +15,15 @@ pub mod vgmdb; pub mod wikidata; pub mod wikimedia_commons; -use crate::{ApiSecrets, DatabaseTables, InheritedFlags}; +use crate::{ApiSecrets, ImportConfig, InheritedFlags}; use anyhow::Result; use jellydb::table::RowNum; use jellyremuxer::matroska::Segment; use std::{collections::HashSet, path::Path, sync::Mutex}; use tokio::runtime::Handle; -pub struct ImportContext<'a> { - pub dba: &'a DatabaseTables, +pub struct PluginContext<'a> { + pub dba: &'a ImportConfig, pub rt: &'a Handle, pub iflags: InheritedFlags, pub pending_nodes: &'a Mutex<HashSet<RowNum>>, @@ -40,19 +40,19 @@ pub struct PluginInfo { pub trait ImportPlugin: Send + Sync { fn info(&self) -> PluginInfo; - fn file(&self, ct: &ImportContext, parent: RowNum, path: &Path) -> Result<()> { + fn file(&self, ct: &PluginContext, parent: RowNum, path: &Path) -> Result<()> { let _ = (ct, parent, path); Ok(()) } - fn media(&self, ct: &ImportContext, node: RowNum, path: &Path, seg: &Segment) -> Result<()> { + fn media(&self, ct: &PluginContext, node: RowNum, path: &Path, seg: &Segment) -> Result<()> { let _ = (ct, node, path, seg); Ok(()) } - fn instruction(&self, ct: &ImportContext, node: RowNum, line: &str) -> Result<()> { + fn instruction(&self, ct: &PluginContext, node: RowNum, line: &str) -> Result<()> { let _ = (ct, node, line); Ok(()) } - fn process(&self, ct: &ImportContext, node: RowNum) -> Result<()> { + fn process(&self, ct: &PluginContext, node: RowNum) -> Result<()> { let _ = (ct, node); Ok(()) } diff --git a/import/src/plugins/tags.rs b/import/src/plugins/tags.rs index bfafbe8..e0652d4 100644 --- a/import/src/plugins/tags.rs +++ b/import/src/plugins/tags.rs @@ -4,7 +4,7 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::plugins::{ImportContext, ImportPlugin, PluginInfo}; +use crate::plugins::{PluginContext, ImportPlugin, PluginInfo}; use anyhow::Result; use jellycommon::*; use jellydb::table::RowNum; @@ -20,7 +20,7 @@ impl ImportPlugin for Tags { ..Default::default() } } - fn media(&self, ct: &ImportContext, node: RowNum, _path: &Path, seg: &Segment) -> Result<()> { + fn media(&self, ct: &PluginContext, node: RowNum, _path: &Path, seg: &Segment) -> Result<()> { let tags = seg .tags .first() diff --git a/import/src/plugins/tmdb.rs b/import/src/plugins/tmdb.rs index 6b70d46..cf50938 100644 --- a/import/src/plugins/tmdb.rs +++ b/import/src/plugins/tmdb.rs @@ -5,7 +5,7 @@ */ use crate::{ USER_AGENT, - plugins::{ImportContext, ImportPlugin, PluginInfo}, + plugins::{PluginContext, ImportPlugin, PluginInfo}, }; use anyhow::{Context, Result, anyhow, bail}; use chrono::{Utc, format::Parsed}; @@ -162,14 +162,14 @@ impl ImportPlugin for Tmdb { ..Default::default() } } - fn process(&self, ct: &ImportContext, node: RowNum) -> Result<()> { + fn process(&self, ct: &PluginContext, node: RowNum) -> Result<()> { self.process_primary(ct, node)?; self.process_episode(ct, node)?; Ok(()) } } impl Tmdb { - fn process_primary(&self, ct: &ImportContext, node: RowNum) -> Result<()> { + fn process_primary(&self, ct: &PluginContext, node: RowNum) -> Result<()> { let data = ct.dba.get_node(node)?.unwrap(); let data = data.as_object(); @@ -238,7 +238,7 @@ impl Tmdb { })?; Ok(()) } - fn process_episode(&self, ct: &ImportContext, node: RowNum) -> Result<()> { + fn process_episode(&self, ct: &PluginContext, node: RowNum) -> Result<()> { let data = ct.dba.get_node(node)?.unwrap(); let data = data.as_object(); diff --git a/import/src/plugins/trakt.rs b/import/src/plugins/trakt.rs index 7530449..3569454 100644 --- a/import/src/plugins/trakt.rs +++ b/import/src/plugins/trakt.rs @@ -5,7 +5,7 @@ */ use crate::{ USER_AGENT, - plugins::{ImportContext, ImportPlugin, PluginInfo}, + plugins::{PluginContext, ImportPlugin, PluginInfo}, }; use anyhow::{Context, Result, anyhow, bail}; use jellycache::{HashKey, cache_memory}; @@ -380,7 +380,7 @@ impl ImportPlugin for Trakt { ..Default::default() } } - fn instruction(&self, ct: &ImportContext, node: RowNum, line: &str) -> Result<()> { + fn instruction(&self, ct: &PluginContext, node: RowNum, line: &str) -> Result<()> { use jellycommon::*; if let Some(value) = line.strip_prefix("trakt-").or(line.strip_prefix("trakt=")) { let (ty, id) = value.split_once(":").unwrap_or(("movie", value)); @@ -398,7 +398,7 @@ impl ImportPlugin for Trakt { } Ok(()) } - fn process(&self, ct: &ImportContext, node: RowNum) -> Result<()> { + fn process(&self, ct: &PluginContext, node: RowNum) -> Result<()> { self.process_primary(ct, node)?; self.process_episode(ct, node)?; Ok(()) @@ -406,7 +406,7 @@ impl ImportPlugin for Trakt { } impl Trakt { - fn process_primary(&self, ct: &ImportContext, node: RowNum) -> Result<()> { + fn process_primary(&self, ct: &PluginContext, node: RowNum) -> Result<()> { let data = ct.dba.get_node(node)?.unwrap(); let data = data.as_object(); let (trakt_kind, trakt_id): (_, u64) = if let Some(id) = data @@ -492,7 +492,7 @@ impl Trakt { })?; Ok(()) } - fn process_episode(&self, ct: &ImportContext, node: RowNum) -> Result<()> { + fn process_episode(&self, ct: &PluginContext, node: RowNum) -> Result<()> { let node_data = ct.dba.get_node(node)?.unwrap(); let node_data = node_data.as_object(); diff --git a/kv/Cargo.toml b/kv/Cargo.toml new file mode 100644 index 0000000..ab24e25 --- /dev/null +++ b/kv/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "jellykv" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.100" +rand = "0.10.0-rc.7" +rocksdb = { version = "0.24.0", features = ["multi-threaded-cf"] } +redb = "3.1.0" diff --git a/cache/src/backends/dummy.rs b/kv/src/dummy.rs index 7b0efa5..bb56111 100644 --- a/cache/src/backends/dummy.rs +++ b/kv/src/dummy.rs @@ -4,12 +4,13 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::backends::CacheStorage; use anyhow::Result; +use crate::BlobStorage; + pub struct Dummy; -impl CacheStorage for Dummy { - fn store(&self, _key: String, _value: &[u8]) -> Result<()> { +impl BlobStorage for Dummy { + fn store(&self, _key: &str, _value: &[u8]) -> Result<()> { Ok(()) } fn read(&self, _key: &str) -> Result<Option<Vec<u8>>> { diff --git a/cache/src/backends/filesystem.rs b/kv/src/filesystem.rs index f1bbdf9..c0e0276 100644 --- a/cache/src/backends/filesystem.rs +++ b/kv/src/filesystem.rs @@ -4,28 +4,27 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::{Config, backends::CacheStorage}; +use crate::BlobStorage; use anyhow::{Result, bail}; use rand::random; use std::{ fs::{File, create_dir_all, rename}, io::{ErrorKind, Read, Write}, - path::PathBuf, + path::{Path, PathBuf}, }; -pub struct Filesystem(PathBuf); +pub fn new(path: &Path) -> Filesystem { + Filesystem(path.to_owned()) +} +pub struct Filesystem(PathBuf); impl Filesystem { - pub fn new(config: &Config) -> Self { - Self(config.path.clone()) - } fn temp_path(&self) -> PathBuf { self.0.join(format!("temp-{:016x}", random::<u128>())) } } - -impl CacheStorage for Filesystem { - fn store(&self, key: String, value: &[u8]) -> Result<()> { +impl BlobStorage for Filesystem { + fn store(&self, key: &str, value: &[u8]) -> Result<()> { let temp = self.temp_path(); let out = self.0.join(&key); create_dir_all(out.parent().unwrap())?; diff --git a/database/src/backends/mod.rs b/kv/src/lib.rs index 1a3998f..0e9b78e 100644 --- a/database/src/backends/mod.rs +++ b/kv/src/lib.rs @@ -4,16 +4,15 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ -mod memory; -mod redb; -mod rocksdb; +pub mod dummy; +pub mod filesystem; +pub mod memory; +pub mod redb; +pub mod rocksdb; -use anyhow::{Result, bail}; -use std::{path::Path, sync::Arc}; +pub use anyhow; -pub use memory::new as new_memory; -pub use redb::new as new_redb; -pub use rocksdb::new as new_rocksdb; +use anyhow::Result; pub type WriteTxnFunction = dyn FnMut(&mut dyn WriteTransaction) -> Result<()>; pub type ReadTxnFunction = dyn FnMut(&dyn ReadTransaction) -> Result<()>; @@ -39,11 +38,7 @@ pub trait ReadTransaction { ) -> Result<Box<dyn Iterator<Item = Result<Vec<u8>>> + 'a>>; } -pub fn create_database(driver: &str, path: &Path) -> Result<Arc<dyn Database>> { - Ok(match driver { - "rocksdb" => Arc::new(rocksdb::new(path)?), - "redb" => Arc::new(redb::new(path)?), - "memory" => Arc::new(memory::new()), - _ => bail!("unknown db driver"), - }) +pub trait BlobStorage: Send + Sync + 'static { + fn store(&self, key: &str, value: &[u8]) -> Result<()>; + fn read(&self, key: &str) -> Result<Option<Vec<u8>>>; } diff --git a/database/src/backends/memory.rs b/kv/src/memory.rs index e010a84..ad13350 100644 --- a/database/src/backends/memory.rs +++ b/kv/src/memory.rs @@ -4,7 +4,7 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::backends::{Database, ReadTransaction, WriteTransaction}; +use crate::{Database, ReadTransaction, WriteTransaction}; use anyhow::Result; use std::{ collections::BTreeMap, @@ -12,13 +12,13 @@ use std::{ }; type MemdbInner = BTreeMap<Vec<u8>, Vec<u8>>; -type Memdb = RwLock<MemdbInner>; +type Memory = RwLock<MemdbInner>; -pub fn new() -> Memdb { - Memdb::default() +pub fn new() -> Memory { + Default::default() } -impl Database for Memdb { +impl Database for Memory { fn write_transaction( &self, f: &mut dyn FnMut(&mut dyn WriteTransaction) -> Result<()>, @@ -34,11 +34,11 @@ impl Database for Memdb { } impl WriteTransaction for RwLockWriteGuard<'_, MemdbInner> { fn set(&mut self, key: &[u8], value: &[u8]) -> Result<()> { - (**self).insert(key.to_vec(), value.to_vec()); + self.insert(key.to_vec(), value.to_vec()); Ok(()) } fn del(&mut self, key: &[u8]) -> Result<()> { - (**self).remove(key); + self.remove(key); Ok(()) } } diff --git a/database/src/backends/redb.rs b/kv/src/redb.rs index c0887c8..e43fdfb 100644 --- a/database/src/backends/redb.rs +++ b/kv/src/redb.rs @@ -4,7 +4,7 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::backends::{Database, ReadTransaction, WriteTransaction}; +use crate::{Database, ReadTransaction, WriteTransaction}; use anyhow::Result; use redb::{AccessGuard, ReadableDatabase, ReadableTable, StorageError, Table, TableDefinition}; use std::path::Path; @@ -49,7 +49,7 @@ impl WriteTransaction for Table<'_, &[u8], &[u8]> { } impl ReadTransaction for Table<'_, &[u8], &[u8]> { fn get<'a>(&'a self, key: &[u8]) -> Result<Option<Vec<u8>>> { - match <Self as ReadableTable<_, _>>::get(self, key)? { + match ReadableTable::get(self, key)? { Some(v) => Ok(Some(v.value().to_vec())), None => Ok(None), } diff --git a/database/src/backends/rocksdb.rs b/kv/src/rocksdb.rs index 056af9e..8aa559c 100644 --- a/database/src/backends/rocksdb.rs +++ b/kv/src/rocksdb.rs @@ -4,11 +4,12 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::backends::{Database, ReadTransaction, WriteTransaction}; use anyhow::Result; use rocksdb::{Direction, ErrorKind, IteratorMode, OptimisticTransactionDB}; use std::path::Path; +use crate::{BlobStorage, Database, ReadTransaction, WriteTransaction}; + pub fn new(path: &Path) -> Result<OptimisticTransactionDB> { Ok(OptimisticTransactionDB::open_default(path)?) } @@ -43,6 +44,7 @@ impl Database for OptimisticTransactionDB { } } } + impl WriteTransaction for rocksdb::Transaction<'_, OptimisticTransactionDB> { fn set(&mut self, key: &[u8], value: &[u8]) -> Result<()> { Ok(self.put(key, value)?) @@ -52,6 +54,7 @@ impl WriteTransaction for rocksdb::Transaction<'_, OptimisticTransactionDB> { Ok(self.delete(key)?) } } + impl ReadTransaction for rocksdb::Transaction<'_, OptimisticTransactionDB> { fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>> { Ok(self.get(key)?) @@ -76,3 +79,12 @@ impl ReadTransaction for rocksdb::Transaction<'_, OptimisticTransactionDB> { }))) } } + +impl BlobStorage for OptimisticTransactionDB { + fn store(&self, key: &str, value: &[u8]) -> Result<()> { + Ok(self.put(key, value)?) + } + fn read(&self, key: &str) -> Result<Option<Vec<u8>>> { + Ok(self.get(key)?) + } +} diff --git a/logic/src/admin/mod.rs b/logic/src/admin/mod.rs deleted file mode 100644 index 76599d5..0000000 --- a/logic/src/admin/mod.rs +++ /dev/null @@ -1,48 +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 log; -pub mod user; - -use crate::{DATABASE, session::Session}; -use anyhow::{Result, anyhow}; -use jellyimport::{import_wrap, reporting::IMPORT_ERRORS}; -use rand::Rng; -use tokio::{spawn, task::spawn_blocking}; - -pub async fn get_import_errors(_session: &Session) -> Vec<String> { - IMPORT_ERRORS.read().await.to_owned() -} -pub fn list_invites(session: &Session) -> Result<Vec<String>> { - session.assert_admin()?; - DATABASE.list_invites() -} - -pub fn create_invite(session: &Session) -> Result<String> { - session.assert_admin()?; - let i = format!("{}", rand::rng().random::<u128>()); - DATABASE.create_invite(&i)?; - Ok(i) -} -pub fn delete_invite(session: &Session, invite: &str) -> Result<()> { - session.assert_admin()?; - if !DATABASE.delete_invite(invite)? { - Err(anyhow!("invite does not exist"))?; - }; - Ok(()) -} -pub async fn update_search_index(session: &Session) -> Result<()> { - session.assert_admin()?; - spawn_blocking(move || DATABASE.search_create_index()).await? -} -pub async fn do_import(session: &Session, incremental: bool) -> Result<()> { - session.assert_admin()?; - if !incremental { - DATABASE.clear_nodes()?; - } - spawn(import_wrap((*DATABASE).clone(), incremental)); - Ok(()) -} diff --git a/logic/src/admin/user.rs b/logic/src/admin/user.rs deleted file mode 100644 index a5f1b24..0000000 --- a/logic/src/admin/user.rs +++ /dev/null @@ -1,51 +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::{DATABASE, session::Session}; -use anyhow::{Result, anyhow}; - -pub fn admin_users(session: &Session) -> Result<ApiAdminUsersResponse> { - session.assert_admin()?; - // TODO dont return useless info like passwords - Ok(ApiAdminUsersResponse { - users: DATABASE.list_users()?, - }) -} -pub fn get_user(session: &Session, username: &str) -> Result<User> { - session.assert_admin()?; - DATABASE - .get_user(username)? - .ok_or(anyhow!("user not found")) -} -pub fn delete_user(session: &Session, username: &str) -> Result<()> { - session.assert_admin()?; - if !DATABASE.delete_user(username)? { - Err(anyhow!("user did not exist"))?; - } - Ok(()) -} - -pub enum GrantState { - Grant, - Revoke, - Unset, -} -pub fn update_user_perms( - session: &Session, - username: &str, - perm: UserPermission, - action: GrantState, -) -> Result<()> { - session.assert_admin()?; - DATABASE.update_user(username, |user| { - match action { - GrantState::Grant => drop(user.permissions.0.insert(perm.clone(), true)), - GrantState::Revoke => drop(user.permissions.0.insert(perm.clone(), false)), - GrantState::Unset => drop(user.permissions.0.remove(&perm)), - } - Ok(()) - }) -} diff --git a/logic/src/lib.rs b/logic/src/lib.rs index 6589f08..7a1bf46 100644 --- a/logic/src/lib.rs +++ b/logic/src/lib.rs @@ -5,7 +5,6 @@ */ #![feature(duration_constructors)] -pub mod admin; pub mod assets; pub mod filter_sort; pub mod home; @@ -13,7 +12,6 @@ pub mod items; pub mod login; pub mod node; pub mod permission; -pub mod search; pub mod session; pub mod stats; diff --git a/logic/src/search.rs b/logic/src/search.rs deleted file mode 100644 index 69f532f..0000000 --- a/logic/src/search.rs +++ /dev/null @@ -1,24 +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::{DATABASE, node::DatabaseNodeUserDataExt, session::Session}; -use anyhow::Result; -use std::time::Instant; - -pub fn search(session: &Session, query: &str, page: Option<usize>) -> Result<ApiSearchResponse> { - let timing = Instant::now(); - let (count, ids) = DATABASE.search(query, 32, page.unwrap_or_default() * 32)?; - let mut results = ids - .into_iter() - .map(|id| DATABASE.get_node_with_userdata(id, session)) - .collect::<Result<Vec<_>, anyhow::Error>>()?; - results.retain(|(n, _)| n.visibility >= Visibility::Reduced); - let duration = timing.elapsed(); - Ok(ApiSearchResponse { - count, - results, - duration, - }) -} diff --git a/server/Cargo.toml b/server/Cargo.toml index 01e918e..01854b5 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -9,7 +9,8 @@ jellystream = { path = "../stream" } jellytranscoder = { path = "../transcoder" } jellyimport = { path = "../import" } jellycache = { path = "../cache" } -# jellyui = { path = "../ui" } +jellyui = { path = "../ui" } +jellykv = { path = "../kv" } anyhow = { workspace = true } async-recursion = "1.1.1" 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}; diff --git a/ui/src/lib.rs b/ui/src/lib.rs index 3b04b40..f3a1282 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -17,9 +17,9 @@ use serde::{Deserialize, Serialize}; #[rustfmt::skip] #[derive(Debug, Deserialize, Serialize, Default)] pub struct Config { - brand: String, - slogan: String, - logo: bool, + pub brand: String, + pub slogan: String, + pub logo: bool, } pub struct RenderInfo<'a> { |