aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2026-01-24 04:31:48 +0100
committermetamuffin <metamuffin@disroot.org>2026-01-24 04:31:48 +0100
commitb2e88a8beabf04adc28947cf82996e8692a68b71 (patch)
tree23d66c8672b69cce7835ffabae4092669062ada8
parent774f64c0789529884dd7a5232f190e347ad29532 (diff)
downloadjellything-b2e88a8beabf04adc28947cf82996e8692a68b71.tar
jellything-b2e88a8beabf04adc28947cf82996e8692a68b71.tar.bz2
jellything-b2e88a8beabf04adc28947cf82996e8692a68b71.tar.zst
move things around; kv crate
-rw-r--r--Cargo.lock58
-rw-r--r--Cargo.toml1
-rw-r--r--cache/Cargo.toml2
-rw-r--r--cache/src/backends/mod.rs28
-rw-r--r--cache/src/backends/rocksdb.rs26
-rw-r--r--cache/src/lib.rs260
-rw-r--r--database/Cargo.toml3
-rw-r--r--database/src/lib.rs9
-rw-r--r--database/src/sort/mod.rs3
-rw-r--r--database/src/sort/none.rs2
-rw-r--r--database/src/sort/value.rs2
-rw-r--r--database/src/table.rs13
-rw-r--r--import/src/lib.rs100
-rw-r--r--import/src/plugins/acoustid.rs4
-rw-r--r--import/src/plugins/infojson.rs6
-rw-r--r--import/src/plugins/media_info.rs4
-rw-r--r--import/src/plugins/misc.rs12
-rw-r--r--import/src/plugins/mod.rs14
-rw-r--r--import/src/plugins/tags.rs4
-rw-r--r--import/src/plugins/tmdb.rs8
-rw-r--r--import/src/plugins/trakt.rs10
-rw-r--r--kv/Cargo.toml10
-rw-r--r--kv/src/dummy.rs (renamed from cache/src/backends/dummy.rs)7
-rw-r--r--kv/src/filesystem.rs (renamed from cache/src/backends/filesystem.rs)17
-rw-r--r--kv/src/lib.rs (renamed from database/src/backends/mod.rs)25
-rw-r--r--kv/src/memory.rs (renamed from database/src/backends/memory.rs)14
-rw-r--r--kv/src/redb.rs (renamed from database/src/backends/redb.rs)4
-rw-r--r--kv/src/rocksdb.rs (renamed from database/src/backends/rocksdb.rs)14
-rw-r--r--logic/src/admin/mod.rs48
-rw-r--r--logic/src/admin/user.rs51
-rw-r--r--logic/src/lib.rs2
-rw-r--r--logic/src/search.rs24
-rw-r--r--server/Cargo.toml3
-rw-r--r--server/src/api.rs1
-rw-r--r--server/src/compat/jellyfin/mod.rs2
-rw-r--r--server/src/compat/mod.rs2
-rw-r--r--server/src/compat/youtube.rs2
-rw-r--r--server/src/config.rs53
-rw-r--r--server/src/helper/accept.rs72
-rw-r--r--server/src/helper/asset.rs37
-rw-r--r--server/src/helper/filter_sort.rs162
-rw-r--r--server/src/helper/language.rs60
-rw-r--r--server/src/helper/mod.rs69
-rw-r--r--server/src/helper/node_id.rs17
-rw-r--r--server/src/helper/session.rs67
-rw-r--r--server/src/logic/playersync.rs2
-rw-r--r--server/src/logic/stream.rs2
-rw-r--r--server/src/logic/userdata.rs2
-rw-r--r--server/src/main.rs28
-rw-r--r--server/src/request_info.rs128
-rw-r--r--server/src/request_info/session.rs15
-rw-r--r--server/src/responders/cache.rs (renamed from server/src/helper/cache.rs)0
-rw-r--r--server/src/responders/cors.rs (renamed from server/src/helper/cors.rs)0
-rw-r--r--server/src/responders/mod.rs8
-rw-r--r--server/src/routes.rs100
-rw-r--r--server/src/ui/account/mod.rs2
-rw-r--r--server/src/ui/account/settings.rs2
-rw-r--r--server/src/ui/admin/import.rs2
-rw-r--r--server/src/ui/admin/log.rs2
-rw-r--r--server/src/ui/admin/mod.rs2
-rw-r--r--server/src/ui/admin/user.rs2
-rw-r--r--server/src/ui/assets.rs2
-rw-r--r--server/src/ui/home.rs2
-rw-r--r--server/src/ui/items.rs2
-rw-r--r--server/src/ui/mod.rs2
-rw-r--r--server/src/ui/node.rs2
-rw-r--r--server/src/ui/player.rs2
-rw-r--r--server/src/ui/search.rs2
-rw-r--r--server/src/ui/stats.rs2
-rw-r--r--ui/src/lib.rs6
70 files changed, 591 insertions, 1060 deletions
diff --git a/Cargo.lock b/Cargo.lock
index d02588e..354f8f8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index fdca40a..ec8dd23 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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> {