feat: add jello-types crate and update dependencies with backtrace support

This commit is contained in:
uttarayan21
2025-12-09 23:28:51 +05:30
parent 05ae9ff570
commit 73fcf9bad1
15 changed files with 636 additions and 507 deletions

78
Cargo.lock generated
View File

@@ -37,6 +37,15 @@ dependencies = [
"nom 7.1.3", "nom 7.1.3",
] ]
[[package]]
name = "addr2line"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
dependencies = [
"gimli",
]
[[package]] [[package]]
name = "adler2" name = "adler2"
version = "2.0.1" version = "2.0.1"
@@ -222,7 +231,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a"
dependencies = [ dependencies = [
"object", "object 0.32.2",
] ]
[[package]] [[package]]
@@ -649,6 +658,21 @@ dependencies = [
"arrayvec", "arrayvec",
] ]
[[package]]
name = "backtrace"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object 0.37.3",
"rustc-demangle",
"windows-link 0.2.1",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
@@ -1167,6 +1191,17 @@ dependencies = [
"clap_derive", "clap_derive",
] ]
[[package]]
name = "clap-verbosity-flag"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d92b1fab272fe943881b77cc6e920d6543e5b1bfadbd5ed81c7c5a755742394"
dependencies = [
"clap",
"log",
"tracing-core",
]
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.53" version = "4.5.53"
@@ -1316,6 +1351,16 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "color-backtrace"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308329d5d62e877ba02943db3a8e8c052de9fde7ab48283395ba0e6494efbabd"
dependencies = [
"backtrace",
"termcolor",
]
[[package]] [[package]]
name = "color_quant" name = "color_quant"
version = "1.1.0" version = "1.1.0"
@@ -2504,6 +2549,12 @@ dependencies = [
"weezl", "weezl",
] ]
[[package]]
name = "gimli"
version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
[[package]] [[package]]
name = "gio-sys" name = "gio-sys"
version = "0.21.2" version = "0.21.2"
@@ -3859,7 +3910,9 @@ version = "0.1.0"
dependencies = [ dependencies = [
"api", "api",
"clap", "clap",
"clap-verbosity-flag",
"clap_complete", "clap_complete",
"color-backtrace",
"dotenvy", "dotenvy",
"error-stack", "error-stack",
"thiserror 2.0.17", "thiserror 2.0.17",
@@ -3871,6 +3924,14 @@ dependencies = [
"ui-iced", "ui-iced",
] ]
[[package]]
name = "jello-types"
version = "0.1.0"
dependencies = [
"serde",
"uuid",
]
[[package]] [[package]]
name = "jiff" name = "jiff"
version = "0.2.16" version = "0.2.16"
@@ -5129,6 +5190,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "object"
version = "0.37.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.3"
@@ -6147,6 +6217,12 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "rustc-demangle"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "1.1.0" version = "1.1.0"

View File

@@ -5,7 +5,7 @@ members = [
"typegen", "typegen",
"ui-gpui", "ui-gpui",
"ui-iced", "ui-iced",
"crates/iced_video_player", "store", "crates/iced_video_player", "store", "jello-types",
] ]
[workspace.dependencies] [workspace.dependencies]
iced = { git = "https://github.com/iced-rs/iced", features = [ iced = { git = "https://github.com/iced-rs/iced", features = [
@@ -27,7 +27,9 @@ license = "MIT"
[dependencies] [dependencies]
api = { version = "0.1.0", path = "api" } api = { version = "0.1.0", path = "api" }
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
clap-verbosity-flag = { version = "3.0.4", features = ["tracing"] }
clap_complete = "4.5" clap_complete = "4.5"
color-backtrace = "0.7.2"
dotenvy = "0.15.7" dotenvy = "0.15.7"
error-stack = "0.6" error-stack = "0.6"
thiserror = "2.0" thiserror = "2.0"

View File

@@ -590,7 +590,10 @@ pub struct BaseItemDto {
/// Gets or sets the trickplay manifest. /// Gets or sets the trickplay manifest.
#[serde(rename = "Trickplay")] #[serde(rename = "Trickplay")]
pub trickplay: Option< pub trickplay: Option<
std::collections::HashMap<String, Option<std::collections::HashMap<String, TrickplayInfo>>>, std::collections::HashMap<
String,
Option<std::collections::HashMap<String, TrickplayInfo>>,
>,
>, >,
/// Gets or sets the type of the location. /// Gets or sets the type of the location.
#[serde(rename = "LocationType")] #[serde(rename = "LocationType")]
@@ -1522,7 +1525,9 @@ pub struct EncodingOptions {
pub hardware_decoding_codecs: Option<Vec<String>>, pub hardware_decoding_codecs: Option<Vec<String>>,
/// Gets or sets the file extensions on-demand metadata based keyframe extraction is enabled for. /// Gets or sets the file extensions on-demand metadata based keyframe extraction is enabled for.
#[serde(rename = "AllowOnDemandMetadataBasedKeyframeExtractionForExtensions")] #[serde(rename = "AllowOnDemandMetadataBasedKeyframeExtractionForExtensions")]
pub allow_on_demand_metadata_based_keyframe_extraction_for_extensions: Option<Vec<String>>, pub allow_on_demand_metadata_based_keyframe_extraction_for_extensions: Option<
Vec<String>,
>,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct EndPointInfo { pub struct EndPointInfo {

View File

@@ -55,7 +55,7 @@ impl JellyfinClient {
pub fn pre_authenticated(token: impl AsRef<str>, config: JellyfinConfig) -> Result<Self> { pub fn pre_authenticated(token: impl AsRef<str>, config: JellyfinConfig) -> Result<Self> {
let auth_header = core::iter::once(( let auth_header = core::iter::once((
reqwest::header::HeaderName::from_static("X-Emby-Authorization"), reqwest::header::HeaderName::from_static("x-emby-authorization"),
reqwest::header::HeaderValue::from_str(&format!( reqwest::header::HeaderValue::from_str(&format!(
"MediaBrowser Client=\"{}\", Device=\"{}\", DeviceId=\"{}\", Version=\"{}\"", "MediaBrowser Client=\"{}\", Device=\"{}\", DeviceId=\"{}\", Version=\"{}\"",
config.client_name, config.device_name, config.device_id, config.version config.client_name, config.device_name, config.device_id, config.version

8
jello-types/Cargo.toml Normal file
View File

@@ -0,0 +1,8 @@
[package]
name = "jello-types"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = { version = "1.0.228", features = ["derive"] }
uuid = { version = "1.18.1", features = ["serde"] }

6
jello-types/src/lib.rs Normal file
View File

@@ -0,0 +1,6 @@
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct User {
id: uuid::Uuid,
name: Option<String>,
primary_image_tag: Option<String>,
}

View File

@@ -1,36 +1,38 @@
#[derive(Debug, clap::Parser)] #[derive(Debug, clap::Parser)]
pub struct Cli { pub struct Cli {
#[clap(subcommand)] // #[clap(subcommand)]
pub cmd: SubCommand, // pub cmd: SubCommand,
#[command(flatten)]
pub verbosity: clap_verbosity_flag::Verbosity,
} }
#[derive(Debug, clap::Subcommand)] // #[derive(Debug, clap::Subcommand)]
pub enum SubCommand { // pub enum SubCommand {
#[clap(name = "add")] // #[clap(name = "add")]
Add(Add), // Add(Add),
#[clap(name = "list")] // #[clap(name = "list")]
List(List), // List(List),
#[clap(name = "completions")] // #[clap(name = "completions")]
Completions { shell: clap_complete::Shell }, // Completions { shell: clap_complete::Shell },
} // }
//
#[derive(Debug, clap::Args)] // #[derive(Debug, clap::Args)]
pub struct Add { // pub struct Add {
#[clap(short, long)] // #[clap(short, long)]
pub name: String, // pub name: String,
} // }
//
#[derive(Debug, clap::Args)] // #[derive(Debug, clap::Args)]
pub struct List {} // pub struct List {}
//
impl Cli { // impl Cli {
pub fn completions(shell: clap_complete::Shell) { // pub fn completions(shell: clap_complete::Shell) {
let mut command = <Cli as clap::CommandFactory>::command(); // let mut command = <Cli as clap::CommandFactory>::command();
clap_complete::generate( // clap_complete::generate(
shell, // shell,
&mut command, // &mut command,
env!("CARGO_BIN_NAME"), // env!("CARGO_BIN_NAME"),
&mut std::io::stdout(), // &mut std::io::stdout(),
); // );
} // }
} // }

View File

@@ -1,9 +1,14 @@
mod cli;
mod errors; mod errors;
use api::JellyfinConfig; use api::JellyfinConfig;
use errors::*; use errors::*;
fn main() -> Result<()> { fn main() -> Result<()> {
tracing_subscriber::fmt::init(); color_backtrace::install();
let args = <cli::Cli as clap::Parser>::parse();
tracing_subscriber::fmt()
.with_max_level(args.verbosity)
.init();
ui_iced::ui().change_context(Error)?; ui_iced::ui().change_context(Error)?;
Ok(()) Ok(())
} }

View File

@@ -1,217 +1,10 @@
use std::{ pub mod redb;
borrow::Borrow, pub mod sqlite;
collections::VecDeque, pub mod toml;
marker::PhantomData,
path::Path,
sync::{Arc, RwLock, atomic::AtomicBool},
};
use futures::task::AtomicWaker; pub trait Store {
use redb::{Error, Key, ReadableDatabase, TableDefinition, Value}; fn image(&self, id: &str) -> Option<Vec<u8>>;
use serde::{Serialize, de::DeserializeOwned}; fn save_image(&mut self, id: &str, data: &[u8]);
const USERS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("users");
const SERVERS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("servers");
const SETTINGS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("settings");
#[derive(Debug)]
pub struct TableInner<T> {
db: Arc<T>,
} }
impl<T> Clone for TableInner<T> { pub struct Settings {}
fn clone(&self) -> Self {
Self {
db: Arc::clone(&self.db),
}
}
}
impl<T> TableInner<T> {
fn new(db: Arc<T>) -> Self {
Self { db }
}
}
impl TableInner<DatabaseHandle> {
async fn get<'a, K: Key, V: Serialize + DeserializeOwned>(
&self,
table: TableDefinition<'static, K, Vec<u8>>,
key: impl Borrow<K::SelfType<'a>>,
) -> Result<Option<V>> {
let db: &redb::Database = &self.db.as_ref().database;
let db_reader = db.begin_read()?;
let table = db_reader.open_table(table)?;
table
.get(key)?
.map(|value| bson::deserialize_from_slice(&value.value()))
.transpose()
.map_err(|e| redb::Error::Io(std::io::Error::other(e)))
}
async fn insert<'a, 'b, K: Key + Send, V: Serialize + DeserializeOwned + Send + 'a>(
&'b self,
table: TableDefinition<'static, K, Vec<u8>>,
key: impl Borrow<K::SelfType<'a>> + Send + 'b,
value: V,
) -> Result<Option<V>> {
let db: &redb::Database = &self.db.as_ref().database;
// self.db
// .writing
// .store(true, std::sync::atomic::Ordering::SeqCst);
let out = tokio::task::spawn_blocking(move || -> Result<Option<V>> {
let db_writer = db.begin_write()?;
let out = {
let mut table = db_writer.open_table(table)?;
let serialized_value = bson::serialize_to_vec(&value)
.map_err(|e| redb::Error::Io(std::io::Error::other(e)))?;
let previous = table.insert(key, &serialized_value)?;
let out = previous
.map(|value| bson::deserialize_from_slice(&value.value()))
.transpose()
.map_err(|e| redb::Error::Io(std::io::Error::other(e)));
out
};
db_writer.commit()?;
out
})
.await
.expect("Failed to run blocking task")?;
Ok(out)
}
}
// impl<K: Key, V: Serialize + DeserializeOwned> Table<K, V> for TableInner {
// async fn get(&self, key: K) -> Result<Option<Value>> {}
// async fn insert(&self, key: K, value: V) -> Result<Option<Value>> {}
// async fn modify(&self, key: K, v: FnOnce(V) -> V) -> Result<bool> {}
// async fn remove(&self, key: K) -> Result<Option<Value>> {}
// }
#[derive(Debug)]
pub struct Users<T>(TableInner<T>);
impl<T> Clone for Users<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Users<T> {
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = USERS;
}
#[derive(Debug)]
pub struct Servers<T>(TableInner<T>);
impl<T> Clone for Servers<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Servers<T> {
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = SERVERS;
}
#[derive(Debug)]
pub struct Settings<T>(TableInner<T>);
impl<T> Clone for Settings<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Settings<T> {
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = SETTINGS;
}
#[derive(Debug, Clone)]
pub struct Database {
users: Users<DatabaseHandle>,
servers: Servers<DatabaseHandle>,
settings: Settings<DatabaseHandle>,
handle: Arc<DatabaseHandle>,
}
#[derive(Debug)]
pub struct DatabaseHandle {
database: redb::Database,
writing: AtomicBool,
wakers: RwLock<VecDeque<AtomicWaker>>,
}
#[derive(Debug)]
pub struct DatabaseWriterGuard<'a> {
handle: &'a DatabaseHandle,
dropper: Arc<AtomicBool>,
}
// impl Drop for DatabaseWriterGuard<'_> {
// fn drop(&mut self) {
// self.handle
// .writing
// .store(false, std::sync::atomic::Ordering::SeqCst);
// let is_panicking = std::thread::panicking();
// let Ok(writer) = self.handle.wakers.write() else {
// if is_panicking {
// return;
// } else {
// panic!("Wakers lock poisoned");
// }
// }
// if let Some(waker) = (self.handle.wakers.write()).pop() {
// waker.wake();
// };
// // let mut wakers = self.handle.wakers.write().expect();
// // if let Some(waker) = self.handle.wakers.write().expect("Wakers lock poisoned").pop_front() {
// // waker.wake();
// // }
// // while let Some(waker) = wakers.pop_front() {
// // waker.wake();
// // }
// }
// }
type Result<O, E = redb::Error> = core::result::Result<O, E>;
pub trait Table<K: Key> {
fn insert<V: Serialize + DeserializeOwned>(
&self,
key: K,
value: V,
) -> impl Future<Output = Result<Option<V>>> + Send;
fn modify<V: Serialize + DeserializeOwned, O: Serialize + DeserializeOwned>(
&self,
key: K,
v: impl FnOnce(V) -> O,
) -> impl Future<Output = Result<bool>> + Send;
fn remove<V: Serialize + DeserializeOwned>(
&self,
key: K,
) -> impl Future<Output = Result<Option<V>>> + Send;
fn get<V: Serialize + DeserializeOwned>(
&self,
key: K,
) -> impl Future<Output = Result<Option<V>>> + Send;
}
impl Database {
pub fn create(path: impl AsRef<Path>) -> Result<Self, Error> {
let writing = AtomicBool::new(false);
let wakers = RwLock::new(VecDeque::new());
let db = redb::Database::create(path)?;
let db = Arc::new(DatabaseHandle {
database: db,
writing,
wakers,
});
let table_inner = TableInner::new(Arc::clone(&db));
let users = Users(table_inner.clone());
let servers = Servers(table_inner.clone());
let settings = Settings(table_inner.clone());
Ok(Self {
servers,
users,
settings,
handle: db,
})
}
}

226
store/src/redb.rs Normal file
View File

@@ -0,0 +1,226 @@
use std::{
borrow::Borrow,
collections::VecDeque,
marker::PhantomData,
path::Path,
sync::{Arc, RwLock, atomic::AtomicBool},
};
use futures::task::AtomicWaker;
use redb::{Error, Key, ReadableDatabase, TableDefinition, Value};
use serde::{Serialize, de::DeserializeOwned};
const USERS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("users");
const SERVERS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("servers");
const SETTINGS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("settings");
#[derive(Debug)]
pub struct TableInner<T> {
db: Arc<T>,
}
impl<T> Clone for TableInner<T> {
fn clone(&self) -> Self {
Self {
db: Arc::clone(&self.db),
}
}
}
impl<T> TableInner<T> {
fn new(db: Arc<T>) -> Self {
Self { db }
}
}
impl TableInner<DatabaseHandle> {
async fn get<'a, K: Key, V: Serialize + DeserializeOwned>(
&self,
table: TableDefinition<'static, K, Vec<u8>>,
key: impl Borrow<K::SelfType<'a>>,
) -> Result<Option<V>> {
let db: &redb::Database = &self.db.as_ref().database;
let db_reader = db.begin_read()?;
let table = db_reader.open_table(table)?;
table
.get(key)?
.map(|value| bson::deserialize_from_slice(&value.value()))
.transpose()
.map_err(|e| redb::Error::Io(std::io::Error::other(e)))
}
async fn insert<
'a,
'b,
K: Key + Send + Sync,
V: Serialize + DeserializeOwned + Send + Sync + 'a,
>(
&'b self,
table: TableDefinition<'static, K, Vec<u8>>,
key: impl Borrow<K::SelfType<'a>> + Send + 'b,
value: V,
) -> Result<Option<V>> {
let db: &redb::Database = &self.db.as_ref().database;
// self.db
// .writing
// .store(true, std::sync::atomic::Ordering::SeqCst);
// let out = tokio::task::spawn_blocking(move || -> Result<Option<V>>
let out = tokio::task::spawn_blocking(|| -> Result<Option<V>> {
let db_writer = db.begin_write()?;
let out = {
let mut table = db_writer.open_table(table)?;
let serialized_value = bson::serialize_to_vec(&value)
.map_err(|e| redb::Error::Io(std::io::Error::other(e)))?;
let previous = table.insert(key, &serialized_value)?;
let out = previous
.map(|value| bson::deserialize_from_slice(&value.value()))
.transpose()
.map_err(|e| redb::Error::Io(std::io::Error::other(e)));
out
};
db_writer.commit()?;
out
})
.await
.expect("Task panicked");
out
}
}
// impl<K: Key, V: Serialize + DeserializeOwned> Table<K, V> for TableInner {
// async fn get(&self, key: K) -> Result<Option<Value>> {}
// async fn insert(&self, key: K, value: V) -> Result<Option<Value>> {}
// async fn modify(&self, key: K, v: FnOnce(V) -> V) -> Result<bool> {}
// async fn remove(&self, key: K) -> Result<Option<Value>> {}
// }
#[derive(Debug)]
pub struct Users<T>(TableInner<T>);
impl<T> Clone for Users<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Users<T> {
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = USERS;
}
#[derive(Debug)]
pub struct Servers<T>(TableInner<T>);
impl<T> Clone for Servers<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Servers<T> {
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = SERVERS;
}
#[derive(Debug)]
pub struct Settings<T>(TableInner<T>);
impl<T> Clone for Settings<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Settings<T> {
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = SETTINGS;
}
#[derive(Debug, Clone)]
pub struct Database {
users: Users<DatabaseHandle>,
servers: Servers<DatabaseHandle>,
settings: Settings<DatabaseHandle>,
handle: Arc<DatabaseHandle>,
}
#[derive(Debug)]
pub struct DatabaseHandle {
database: redb::Database,
writing: AtomicBool,
wakers: RwLock<VecDeque<AtomicWaker>>,
}
#[derive(Debug)]
pub struct DatabaseWriterGuard<'a> {
handle: &'a DatabaseHandle,
dropper: Arc<AtomicBool>,
}
// impl Drop for DatabaseWriterGuard<'_> {
// fn drop(&mut self) {
// self.handle
// .writing
// .store(false, std::sync::atomic::Ordering::SeqCst);
// let is_panicking = std::thread::panicking();
// let Ok(writer) = self.handle.wakers.write() else {
// if is_panicking {
// return;
// } else {
// panic!("Wakers lock poisoned");
// }
// }
// if let Some(waker) = (self.handle.wakers.write()).pop() {
// waker.wake();
// };
// // let mut wakers = self.handle.wakers.write().expect();
// // if let Some(waker) = self.handle.wakers.write().expect("Wakers lock poisoned").pop_front() {
// // waker.wake();
// // }
// // while let Some(waker) = wakers.pop_front() {
// // waker.wake();
// // }
// }
// }
type Result<O, E = redb::Error> = core::result::Result<O, E>;
pub trait Table<K: Key> {
fn insert<V: Serialize + DeserializeOwned>(
&self,
key: K,
value: V,
) -> impl Future<Output = Result<Option<V>>> + Send;
fn modify<V: Serialize + DeserializeOwned, O: Serialize + DeserializeOwned>(
&self,
key: K,
v: impl FnOnce(V) -> O,
) -> impl Future<Output = Result<bool>> + Send;
fn remove<V: Serialize + DeserializeOwned>(
&self,
key: K,
) -> impl Future<Output = Result<Option<V>>> + Send;
fn get<V: Serialize + DeserializeOwned>(
&self,
key: K,
) -> impl Future<Output = Result<Option<V>>> + Send;
}
impl Database {
pub fn create(path: impl AsRef<Path>) -> Result<Self, Error> {
let writing = AtomicBool::new(false);
let wakers = RwLock::new(VecDeque::new());
let db = redb::Database::create(path)?;
let db = Arc::new(DatabaseHandle {
database: db,
writing,
wakers,
});
let table_inner = TableInner::new(Arc::clone(&db));
let users = Users(table_inner.clone());
let servers = Servers(table_inner.clone());
let settings = Settings(table_inner.clone());
Ok(Self {
servers,
users,
settings,
handle: db,
})
}
}

0
store/src/sqlite.rs Normal file
View File

0
store/src/toml.rs Normal file
View File

View File

@@ -1,4 +1,6 @@
// mod settings; mod settings;
mod video;
mod shared_string; mod shared_string;
use iced_video_player::{Video, VideoPlayer}; use iced_video_player::{Video, VideoPlayer};
use shared_string::SharedString; use shared_string::SharedString;
@@ -9,9 +11,9 @@ mod blur_hash;
use blur_hash::BlurHash; use blur_hash::BlurHash;
mod preview; mod preview;
use preview::Preview; // use preview::Preview;
use iced::{Alignment, Element, Length, Shadow, Task, widget::*}; use iced::{Alignment, Element, Length, Task, widget::*};
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -104,37 +106,57 @@ pub enum Screen {
User, User,
Video, Video,
} }
#[derive(Debug, Clone)]
pub struct Config {
pub server_url: Option<String>,
pub device_id: Option<String>,
pub device_name: Option<String>,
pub client_name: Option<String>,
pub version: Option<String>,
}
impl Default for Config {
fn default() -> Self {
Config {
server_url: Some("http://localhost:8096".to_string()),
device_id: Some("jello-iced".to_string()),
device_name: Some("Jello Iced".to_string()),
client_name: Some("Jello".to_string()),
version: Some("0.1.0".to_string()),
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct State { struct State {
loading: Option<Loading>, loading: Option<Loading>,
current: Option<uuid::Uuid>, current: Option<uuid::Uuid>,
cache: ItemCache, cache: ItemCache,
jellyfin_client: api::JellyfinClient, jellyfin_client: Option<api::JellyfinClient>,
messages: Vec<String>, messages: Vec<String>,
history: Vec<Option<uuid::Uuid>>, history: Vec<Option<uuid::Uuid>>,
query: Option<String>, query: Option<String>,
screen: Screen, screen: Screen,
// Login form state settings: settings::SettingsState,
username_input: String,
password_input: String,
is_authenticated: bool, is_authenticated: bool,
// Video
video: Option<Arc<Video>>, video: Option<Arc<Video>>,
} }
impl State { impl State {
pub fn new(jellyfin_client: api::JellyfinClient) -> Self { pub fn new() -> Self {
State { State {
loading: None, loading: None,
current: None, current: None,
cache: ItemCache::default(), cache: ItemCache::default(),
jellyfin_client, jellyfin_client: None,
messages: Vec::new(), messages: Vec::new(),
history: Vec::new(), history: Vec::new(),
query: None, query: None,
screen: Screen::Home, screen: Screen::Home,
username_input: String::new(), settings: settings::SettingsState::default(),
password_input: String::new(), // username_input: String::new(),
// password_input: String::new(),
is_authenticated: false, is_authenticated: false,
video: None, video: None,
} }
@@ -143,8 +165,7 @@ impl State {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Message { pub enum Message {
OpenSettings, Settings(settings::SettingsMessage),
CloseSettings,
Refresh, Refresh,
Search, Search,
SearchQueryChanged(String), SearchQueryChanged(String),
@@ -154,95 +175,22 @@ pub enum Message {
SetToken(String), SetToken(String),
Back, Back,
Home, Home,
// Login-related messages // Login {
UsernameChanged(String), // username: String,
PasswordChanged(String), // password: String,
Login, // config: api::JellyfinConfig,
LoginSuccess(String), // },
LoadedClient(api::JellyfinClient, bool), // LoginSuccess(String),
Logout, // LoadedClient(api::JellyfinClient, bool),
Video(VideoMessage), // Logout,
} Video(video::VideoMessage),
#[derive(Debug, Clone)]
pub enum VideoMessage {
EndOfStream,
Open(url::Url),
Pause,
Play,
Seek(f64),
Stop,
Test,
} }
fn update(state: &mut State, message: Message) -> Task<Message> { fn update(state: &mut State, message: Message) -> Task<Message> {
// if let Some(client) = state.jellyfin_client.clone() {
match message { match message {
Message::OpenSettings => { Message::Settings(msg) => settings::update(&mut state.settings, msg),
state.screen = Screen::Settings; Message::OpenItem(id) if let Some(client) = state.jellyfin_client.clone() => {
Task::none()
}
Message::CloseSettings => {
state.screen = Screen::Home;
Task::none()
}
Message::UsernameChanged(username) => {
state.username_input = username;
Task::none()
}
Message::PasswordChanged(password) => {
state.password_input = password;
Task::none()
}
Message::Login => {
let username = state.username_input.clone();
let password = state.password_input.clone();
let config = (*state.jellyfin_client.config).clone();
Task::perform(
async move { api::JellyfinClient::authenticate(username, password, config).await },
|result| match result {
Ok(client) => Message::LoadedClient(client, true),
Err(e) => Message::Error(format!("Login failed: {}", e)),
},
)
}
Message::LoginSuccess(token) => {
state.jellyfin_client.set_token(token.clone());
state.is_authenticated = true;
state.password_input.clear();
state.messages.push("Login successful!".to_string());
state.screen = Screen::Home;
// Save token and refresh items
let client = state.jellyfin_client.clone();
Task::perform(
async move {
let _ = client.save_token(".session").await;
},
|_| Message::Refresh,
)
}
Message::LoadedClient(client, is_authenticated) => {
state.jellyfin_client = client;
state.is_authenticated = is_authenticated;
if is_authenticated {
Task::done(Message::Refresh)
} else {
Task::none()
}
}
Message::Logout => {
state.is_authenticated = false;
state.jellyfin_client.set_token("");
state.cache = ItemCache::default();
state.current = None;
state.username_input.clear();
state.password_input.clear();
state.messages.push("Logged out successfully".to_string());
Task::none()
}
Message::OpenItem(id) => {
let client = state.jellyfin_client.clone();
use api::jellyfin::BaseItemKind::*; use api::jellyfin::BaseItemKind::*;
if let Some(cached) = id.as_ref().and_then(|id| state.cache.get(id)) if let Some(cached) = id.as_ref().and_then(|id| state.cache.get(id))
&& matches!(cached._type, Video | Movie | Episode) && matches!(cached._type, Video | Movie | Episode)
@@ -250,7 +198,7 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
let url = client let url = client
.stream_url(id.expect("ID exists")) .stream_url(id.expect("ID exists"))
.expect("Failed to get stream URL"); .expect("Failed to get stream URL");
Task::done(Message::Video(VideoMessage::Open(url))) Task::done(Message::Video(video::VideoMessage::Open(url)))
} else { } else {
Task::perform( Task::perform(
async move { async move {
@@ -273,9 +221,9 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
state.current = id; state.current = id;
Task::none() Task::none()
} }
Message::Refresh => { Message::Refresh if let Some(client) = state.jellyfin_client.clone() => {
// Handle refresh logic // Handle refresh logic
let client = state.jellyfin_client.clone(); // let client = state.jellyfin_client.clone();
let current = state.current; let current = state.current;
Task::perform( Task::perform(
async move { async move {
@@ -298,7 +246,10 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
} }
Message::SetToken(token) => { Message::SetToken(token) => {
tracing::info!("Authenticated with token: {}", token); tracing::info!("Authenticated with token: {}", token);
state.jellyfin_client.set_token(token); state
.jellyfin_client
.as_mut()
.map(|mut client| client.set_token(token));
state.is_authenticated = true; state.is_authenticated = true;
Task::none() Task::none()
} }
@@ -315,9 +266,9 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
// Handle search query change // Handle search query change
Task::none() Task::none()
} }
Message::Search => { Message::Search if let Some(client) = state.jellyfin_client.clone() => {
// Handle search action // Handle search action
let client = state.jellyfin_client.clone(); // let client = state.jellyfin_client.clone();
let query = state.query.clone().unwrap_or_default(); let query = state.query.clone().unwrap_or_default();
Task::perform(async move { client.search(query).await }, |r| match r { Task::perform(async move { client.search(query).await }, |r| match r {
Err(e) => Message::Error(format!("Search failed: {}", e)), Err(e) => Message::Error(format!("Search failed: {}", e)),
@@ -327,63 +278,14 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
} }
}) })
} }
Message::Video(msg) => match msg { Message::Video(msg) => video::update(state, msg),
VideoMessage::EndOfStream => { _ => todo!(),
state.video = None;
Task::none()
}
VideoMessage::Open(url) => {
state.video = Video::new(&url)
.inspect_err(|err| {
tracing::error!("Failed to play video at {}: {:?}", url, err);
})
.ok()
.map(Arc::new);
Task::none()
}
VideoMessage::Pause => {
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) {
video.set_paused(true);
}
Task::none()
}
VideoMessage::Play => {
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) {
video.set_paused(false);
}
Task::none()
}
VideoMessage::Seek(position) => {
// if let Some(ref video) = state.video {
// // video.seek(position, true);
// }
Task::none()
}
VideoMessage::Stop => {
state.video = None;
Task::none()
}
VideoMessage::Test => {
let url = url::Url::parse(
// "file:///home/servius/Projects/jello/crates/iced_video_player/.media/test.mp4",
"https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm",
)
.unwrap();
state.video = Video::new(&url)
.inspect_err(|err| {
tracing::error!("{err:?}");
})
.ok()
.map(Arc::new);
Task::none()
}
},
} }
} }
fn view(state: &State) -> Element<'_, Message> { fn view(state: &State) -> Element<'_, Message> {
match state.screen { match state.screen {
// Screen::Settings => settings::settings(state), Screen::Settings => settings::settings(state),
Screen::Home | _ => home(state), Screen::Home | _ => home(state),
} }
} }
@@ -395,25 +297,9 @@ fn home(state: &State) -> Element<'_, Message> {
.into() .into()
} }
fn player(video: &Video) -> Element<'_, Message> {
container(
VideoPlayer::new(video)
.width(Length::Fill)
.height(Length::Fill)
.content_fit(iced::ContentFit::Contain)
.on_end_of_stream(Message::Video(VideoMessage::EndOfStream)),
)
.style(|_| container::background(iced::Color::BLACK))
.width(Length::Fill)
.height(Length::Fill)
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.into()
}
fn body(state: &State) -> Element<'_, Message> { fn body(state: &State) -> Element<'_, Message> {
if let Some(ref video) = state.video { if let Some(ref video) = state.video {
player(video) video::player(video)
} else { } else {
scrollable( scrollable(
container( container(
@@ -436,7 +322,13 @@ fn header(state: &State) -> Element<'_, Message> {
row([ row([
container( container(
Button::new( Button::new(
Text::new(state.jellyfin_client.config.server_url.as_str()) Text::new(
state
.jellyfin_client
.as_ref()
.map(|c| c.config.server_url.as_str())
.unwrap_or("No Server"),
)
.align_x(Alignment::Start), .align_x(Alignment::Start),
) )
.on_press(Message::Home), .on_press(Message::Home),
@@ -452,9 +344,11 @@ fn header(state: &State) -> Element<'_, Message> {
container( container(
row([ row([
button("Refresh").on_press(Message::Refresh).into(), button("Refresh").on_press(Message::Refresh).into(),
button("Settings").on_press(Message::OpenSettings).into(), button("Settings")
.on_press(Message::Settings(settings::SettingsMessage::Open))
.into(),
button("TestVideo") button("TestVideo")
.on_press(Message::Video(VideoMessage::Test)) .on_press(Message::Video(video::VideoMessage::Test))
.into(), .into(),
]) ])
.spacing(10), .spacing(10),
@@ -544,20 +438,20 @@ fn card(item: &Item) -> Element<'_, Message> {
fn init() -> (State, Task<Message>) { fn init() -> (State, Task<Message>) {
// Create a default config for initial state // Create a default config for initial state
let default_config = api::JellyfinConfig {
server_url: "http://localhost:8096".parse().expect("Valid URL"), // let default_config = api::JellyfinConfig {
device_id: "jello-iced".to_string(), // server_url: "http://localhost:8096".parse().expect("Valid URL"),
device_name: "Jello Iced".to_string(), // device_id: "jello-iced".to_string(),
client_name: "Jello".to_string(), // device_name: "Jello Iced".to_string(),
version: "0.1.0".to_string(), // client_name: "Jello".to_string(),
}; // version: "0.1.0".to_string(),
let default_client = api::JellyfinClient::new_with_config(default_config); // };
// let default_client = api::JellyfinClient::new_with_config(default_config);
( (
State::new(default_client), State::new(),
Task::perform( Task::perform(
async move { async move {
// Load config from file
let config_str = std::fs::read_to_string("config.toml") let config_str = std::fs::read_to_string("config.toml")
.map_err(|e| api::JellyfinApiError::IoError(e))?; .map_err(|e| api::JellyfinApiError::IoError(e))?;
let config: api::JellyfinConfig = toml::from_str(&config_str).map_err(|e| { let config: api::JellyfinConfig = toml::from_str(&config_str).map_err(|e| {
@@ -581,8 +475,9 @@ fn init() -> (State, Task<Message>) {
} }
}, },
|result: Result<_, api::JellyfinApiError>| match result { |result: Result<_, api::JellyfinApiError>| match result {
Ok((client, is_authenticated)) => Message::LoadedClient(client, is_authenticated), // Ok((client, is_authenticated)) => Message::LoadedClient(client, is_authenticated),
Err(e) => Message::Error(format!("Initialization failed: {}", e)), Err(e) => Message::Error(format!("Initialization failed: {}", e)),
_ => Message::Error("Login Unimplemented".to_string()),
}, },
) )
.chain(Task::done(Message::Refresh)), .chain(Task::done(Message::Refresh)),

View File

@@ -1,8 +1,26 @@
use crate::*; use crate::*;
use iced::Element;
pub fn settings(state: &State) -> Element<'_, Message> {} pub fn settings(state: &State) -> Element<'_, Message> {
empty()
}
#[derive(Debug, Clone)] pub fn update(_state: &mut SettingsState, message: SettingsMessage) -> Task<Message> {
match message {
SettingsMessage::Open => {}
SettingsMessage::Close => {}
SettingsMessage::Select(screen) => {
tracing::trace!("Switching settings screen to {:?}", screen);
}
}
Task::none()
}
pub fn empty() -> Element<'static, Message> {
column([]).into()
}
#[derive(Debug, Clone, Default)]
pub struct SettingsState { pub struct SettingsState {
login_form: LoginForm, login_form: LoginForm,
server_form: ServerForm, server_form: ServerForm,
@@ -16,8 +34,9 @@ pub enum SettingsMessage {
Select(SettingsScreen), Select(SettingsScreen),
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Default)]
pub enum SettingsScreen { pub enum SettingsScreen {
#[default]
Main, Main,
Users, Users,
Servers, Servers,
@@ -37,20 +56,27 @@ pub struct UserItem {
pub name: SharedString, pub name: SharedString,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Default)]
pub struct LoginForm { pub struct LoginForm {
username: String, username: Option<String>,
password: String, password: Option<String>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Default)]
pub struct ServerForm { pub struct ServerForm {
name: String, name: Option<String>,
url: String, url: Option<String>,
} }
mod screens { mod screens {
pub fn main(state: &State) -> Element<'_, Message> {} use super::*;
pub fn server(state: &State) -> Element<'_, Message> {} pub fn main(state: &State) -> Element<'_, Message> {
pub fn user(state: &State) -> Element<'_, Message> {} empty()
}
pub fn server(state: &State) -> Element<'_, Message> {
empty()
}
pub fn user(state: &State) -> Element<'_, Message> {
empty()
}
} }

85
ui-iced/src/video.rs Normal file
View File

@@ -0,0 +1,85 @@
use super::*;
#[derive(Debug, Clone)]
pub enum VideoMessage {
EndOfStream,
Open(url::Url),
Pause,
Play,
Seek(f64),
Stop,
Test,
}
pub fn update(state: &mut State, message: VideoMessage) -> Task<Message> {
match message {
VideoMessage::EndOfStream => {
state.video = None;
Task::none()
}
VideoMessage::Open(url) => {
state.video = Video::new(&url)
.inspect_err(|err| {
tracing::error!("Failed to play video at {}: {:?}", url, err);
})
.inspect(|video| {
tracing::info!("Framerate {}", video.framerate());
})
.ok()
.map(Arc::new);
Task::none()
}
VideoMessage::Pause => {
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) {
video.set_paused(true);
}
Task::none()
}
VideoMessage::Play => {
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) {
video.set_paused(false);
}
Task::none()
}
VideoMessage::Seek(position) => {
// if let Some(ref video) = state.video {
// // video.seek(position, true);
// }
Task::none()
}
VideoMessage::Stop => {
state.video = None;
Task::none()
}
VideoMessage::Test => {
let url = url::Url::parse(
// "file:///home/servius/Projects/jello/crates/iced_video_player/.media/test.mp4",
"https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm",
// "https://www.youtube.com/watch?v=QbUUaXGA3C4",
)
.unwrap();
state.video = Video::new(&url)
.inspect_err(|err| {
tracing::error!("{err:?}");
})
.ok()
.map(Arc::new);
Task::none()
}
}
}
pub fn player(video: &Video) -> Element<'_, Message> {
container(
VideoPlayer::new(video)
.width(Length::Fill)
.height(Length::Fill)
.content_fit(iced::ContentFit::Contain)
.on_end_of_stream(Message::Video(VideoMessage::EndOfStream)),
)
.style(|_| container::background(iced::Color::BLACK))
.width(Length::Fill)
.height(Length::Fill)
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.into()
}