feat: add jello-types crate and update dependencies with backtrace support
This commit is contained in:
78
Cargo.lock
generated
78
Cargo.lock
generated
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
8
jello-types/Cargo.toml
Normal 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
6
jello-types/src/lib.rs
Normal 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>,
|
||||||
|
}
|
||||||
66
src/cli.rs
66
src/cli.rs
@@ -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(),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
221
store/src/lib.rs
221
store/src/lib.rs
@@ -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
226
store/src/redb.rs
Normal 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
0
store/src/sqlite.rs
Normal file
0
store/src/toml.rs
Normal file
0
store/src/toml.rs
Normal 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)),
|
||||||
|
|||||||
@@ -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
85
ui-iced/src/video.rs
Normal 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()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user