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",
]
[[package]]
name = "addr2line"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.1"
@@ -222,7 +231,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a"
dependencies = [
"object",
"object 0.32.2",
]
[[package]]
@@ -649,6 +658,21 @@ dependencies = [
"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]]
name = "base64"
version = "0.22.1"
@@ -1167,6 +1191,17 @@ dependencies = [
"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]]
name = "clap_builder"
version = "4.5.53"
@@ -1316,6 +1351,16 @@ dependencies = [
"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]]
name = "color_quant"
version = "1.1.0"
@@ -2504,6 +2549,12 @@ dependencies = [
"weezl",
]
[[package]]
name = "gimli"
version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
[[package]]
name = "gio-sys"
version = "0.21.2"
@@ -3859,7 +3910,9 @@ version = "0.1.0"
dependencies = [
"api",
"clap",
"clap-verbosity-flag",
"clap_complete",
"color-backtrace",
"dotenvy",
"error-stack",
"thiserror 2.0.17",
@@ -3871,6 +3924,14 @@ dependencies = [
"ui-iced",
]
[[package]]
name = "jello-types"
version = "0.1.0"
dependencies = [
"serde",
"uuid",
]
[[package]]
name = "jiff"
version = "0.2.16"
@@ -5129,6 +5190,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "object"
version = "0.37.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -6147,6 +6217,12 @@ dependencies = [
"walkdir",
]
[[package]]
name = "rustc-demangle"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]]
name = "rustc-hash"
version = "1.1.0"

View File

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

View File

@@ -566,7 +566,7 @@ pub struct BaseItemDto {
#[serde(rename = "SeriesThumbImageTag")]
pub series_thumb_image_tag: Option<String>,
/** Gets or sets the blurhashes for the image tags.
Maps image type to dictionary mapping image tag to blurhash value.*/
Maps image type to dictionary mapping image tag to blurhash value.*/
#[serde(rename = "ImageBlurHashes")]
pub image_blur_hashes: BaseItemDtoImageBlurHashes,
/// Gets or sets the series studio.
@@ -590,7 +590,10 @@ pub struct BaseItemDto {
/// Gets or sets the trickplay manifest.
#[serde(rename = "Trickplay")]
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.
#[serde(rename = "LocationType")]
@@ -1522,7 +1525,9 @@ pub struct EncodingOptions {
pub hardware_decoding_codecs: Option<Vec<String>>,
/// Gets or sets the file extensions on-demand metadata based keyframe extraction is enabled for.
#[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)]
pub struct EndPointInfo {
@@ -1541,9 +1546,9 @@ pub struct ExternalIdInfo {
#[serde(rename = "Key")]
pub key: String,
/** Gets or sets the specific media type for this id. This is used to distinguish between the different
external id types for providers with multiple ids.
A null value indicates there is no specific media type associated with the external id, or this is the
default id for the external provider so there is no need to specify a type.*/
external id types for providers with multiple ids.
A null value indicates there is no specific media type associated with the external id, or this is the
default id for the external provider so there is no need to specify a type.*/
#[serde(rename = "Type")]
pub _type: Option<ExternalIdMediaType>,
/// Gets or sets the URL format string.
@@ -2439,7 +2444,7 @@ pub struct MediaSourceInfo {
#[serde(rename = "Name")]
pub name: Option<String>,
/** Gets or sets a value indicating whether the media is remote.
Differentiate internet url vs local network.*/
Differentiate internet url vs local network.*/
#[serde(rename = "IsRemote")]
pub is_remote: bool,
#[serde(rename = "ETag")]
@@ -2501,7 +2506,7 @@ pub struct MediaSourceInfo {
#[serde(rename = "TranscodingUrl")]
pub transcoding_url: Option<String>,
/** Media streaming protocol.
Lowercase for backwards compatibility.*/
Lowercase for backwards compatibility.*/
#[serde(rename = "TranscodingSubProtocol")]
pub transcoding_sub_protocol: MediaStreamProtocol,
#[serde(rename = "TranscodingContainer")]
@@ -2652,8 +2657,8 @@ pub struct MediaStream {
#[serde(rename = "RealFrameRate")]
pub real_frame_rate: Option<f32>,
/** Gets the framerate used as reference.
Prefer AverageFrameRate, if that is null or an unrealistic value
then fallback to RealFrameRate.*/
Prefer AverageFrameRate, if that is null or an unrealistic value
then fallback to RealFrameRate.*/
#[serde(rename = "ReferenceFrameRate")]
pub reference_frame_rate: Option<f32>,
/// Gets or sets the profile.
@@ -2715,7 +2720,7 @@ pub struct MediaUpdateInfoPathDto {
#[serde(rename = "Path")]
pub path: Option<String>,
/** Gets or sets media update type.
Created, Modified, Deleted.*/
Created, Modified, Deleted.*/
#[serde(rename = "UpdateType")]
pub update_type: Option<String>,
}
@@ -2964,7 +2969,7 @@ pub struct NetworkConfiguration {
#[serde(rename = "EnablePublishedServerUriByRequest")]
pub enable_published_server_uri_by_request: bool,
/** Gets or sets the PublishedServerUriBySubnet
Gets or sets PublishedServerUri to advertise for specific subnets.*/
Gets or sets PublishedServerUri to advertise for specific subnets.*/
#[serde(rename = "PublishedServerUriBySubnet")]
pub published_server_uri_by_subnet: Vec<String>,
/// Gets or sets the filter for remote IP connectivity. Used in conjunction with <seealso cref="P:MediaBrowser.Common.Net.NetworkConfiguration.IsRemoteIPFilterBlacklist" />.
@@ -3028,11 +3033,11 @@ pub struct OpenLiveStreamDto {
#[serde(rename = "AlwaysBurnInSubtitleWhenTranscoding")]
pub always_burn_in_subtitle_when_transcoding: Option<bool>,
/** A MediaBrowser.Model.Dlna.DeviceProfile represents a set of metadata which determines which content a certain device is able to play.
<br />
Specifically, it defines the supported <see cref="P:MediaBrowser.Model.Dlna.DeviceProfile.ContainerProfiles">containers</see> and
<see cref="P:MediaBrowser.Model.Dlna.DeviceProfile.CodecProfiles">codecs</see> (video and/or audio, including codec profiles and levels)
the device is able to direct play (without transcoding or remuxing),
as well as which <see cref="P:MediaBrowser.Model.Dlna.DeviceProfile.TranscodingProfiles">containers/codecs to transcode to</see> in case it isn't.*/
<br />
Specifically, it defines the supported <see cref="P:MediaBrowser.Model.Dlna.DeviceProfile.ContainerProfiles">containers</see> and
<see cref="P:MediaBrowser.Model.Dlna.DeviceProfile.CodecProfiles">codecs</see> (video and/or audio, including codec profiles and levels)
the device is able to direct play (without transcoding or remuxing),
as well as which <see cref="P:MediaBrowser.Model.Dlna.DeviceProfile.TranscodingProfiles">containers/codecs to transcode to</see> in case it isn't.*/
#[serde(rename = "DeviceProfile")]
pub device_profile: Option<DeviceProfile>,
/// Gets or sets the device play protocols.
@@ -3068,7 +3073,7 @@ pub struct PackageInfo {
#[serde(rename = "category")]
pub category: String,
/** Gets or sets the guid of the assembly associated with this plugin.
This is used to identify the proper item for automatic updates.*/
This is used to identify the proper item for automatic updates.*/
#[serde(rename = "guid")]
pub guid: uuid::Uuid,
/// Gets or sets the versions.
@@ -3187,11 +3192,11 @@ pub struct PlaybackInfoDto {
#[serde(rename = "LiveStreamId")]
pub live_stream_id: Option<String>,
/** A MediaBrowser.Model.Dlna.DeviceProfile represents a set of metadata which determines which content a certain device is able to play.
<br />
Specifically, it defines the supported <see cref="P:MediaBrowser.Model.Dlna.DeviceProfile.ContainerProfiles">containers</see> and
<see cref="P:MediaBrowser.Model.Dlna.DeviceProfile.CodecProfiles">codecs</see> (video and/or audio, including codec profiles and levels)
the device is able to direct play (without transcoding or remuxing),
as well as which <see cref="P:MediaBrowser.Model.Dlna.DeviceProfile.TranscodingProfiles">containers/codecs to transcode to</see> in case it isn't.*/
<br />
Specifically, it defines the supported <see cref="P:MediaBrowser.Model.Dlna.DeviceProfile.ContainerProfiles">containers</see> and
<see cref="P:MediaBrowser.Model.Dlna.DeviceProfile.CodecProfiles">codecs</see> (video and/or audio, including codec profiles and levels)
the device is able to direct play (without transcoding or remuxing),
as well as which <see cref="P:MediaBrowser.Model.Dlna.DeviceProfile.TranscodingProfiles">containers/codecs to transcode to</see> in case it isn't.*/
#[serde(rename = "DeviceProfile")]
pub device_profile: Option<DeviceProfile>,
/// Gets or sets a value indicating whether to enable direct play.
@@ -4388,7 +4393,7 @@ pub struct ServerConfiguration {
#[serde(rename = "PreviousVersion")]
pub previous_version: Option<String>,
/** Gets or sets the stringified PreviousVersion to be stored/loaded,
because System.Version itself isn't xml-serializable.*/
because System.Version itself isn't xml-serializable.*/
#[serde(rename = "PreviousVersionStr")]
pub previous_version_str: Option<String>,
/// Gets or sets a value indicating whether to enable prometheus metrics exporting.
@@ -4441,12 +4446,12 @@ pub struct ServerConfiguration {
#[serde(rename = "MaxAudiobookResume")]
pub max_audiobook_resume: i32,
/** Gets or sets the threshold in minutes after a inactive session gets closed automatically.
If set to 0 the check for inactive sessions gets disabled.*/
If set to 0 the check for inactive sessions gets disabled.*/
#[serde(rename = "InactiveSessionThreshold")]
pub inactive_session_threshold: i32,
/** Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed
Some delay is necessary with some items because their creation is not atomic. It involves the creation of several
different directories and files.*/
Some delay is necessary with some items because their creation is not atomic. It involves the creation of several
different directories and files.*/
#[serde(rename = "LibraryMonitorDelay")]
pub library_monitor_delay: i32,
/// Gets or sets the duration in seconds that we will wait after a library updated event before executing the library changed notification.
@@ -5415,7 +5420,7 @@ pub struct TrickplayOptions {
#[serde(rename = "EnableHwEncoding")]
pub enable_hw_encoding: bool,
/** Gets or sets a value indicating whether to only extract key frames.
Significantly faster, but is not compatible with all decoders and/or video files.*/
Significantly faster, but is not compatible with all decoders and/or video files.*/
#[serde(rename = "EnableKeyFrameOnlyExtraction")]
pub enable_key_frame_only_extraction: bool,
/// Gets or sets the behavior used by trickplay provider on library scan/update.
@@ -5707,7 +5712,7 @@ pub struct UserDto {
#[serde(rename = "ServerId")]
pub server_id: Option<String>,
/** Gets or sets the name of the server.
This is not used by the server and is for client-side usage only.*/
This is not used by the server and is for client-side usage only.*/
#[serde(rename = "ServerName")]
pub server_name: Option<String>,
/// Gets or sets the id.

View File

@@ -55,7 +55,7 @@ impl JellyfinClient {
pub fn pre_authenticated(token: impl AsRef<str>, config: JellyfinConfig) -> Result<Self> {
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!(
"MediaBrowser Client=\"{}\", Device=\"{}\", DeviceId=\"{}\", 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)]
pub struct Cli {
#[clap(subcommand)]
pub cmd: SubCommand,
// #[clap(subcommand)]
// pub cmd: SubCommand,
#[command(flatten)]
pub verbosity: clap_verbosity_flag::Verbosity,
}
#[derive(Debug, clap::Subcommand)]
pub enum SubCommand {
#[clap(name = "add")]
Add(Add),
#[clap(name = "list")]
List(List),
#[clap(name = "completions")]
Completions { shell: clap_complete::Shell },
}
#[derive(Debug, clap::Args)]
pub struct Add {
#[clap(short, long)]
pub name: String,
}
#[derive(Debug, clap::Args)]
pub struct List {}
impl Cli {
pub fn completions(shell: clap_complete::Shell) {
let mut command = <Cli as clap::CommandFactory>::command();
clap_complete::generate(
shell,
&mut command,
env!("CARGO_BIN_NAME"),
&mut std::io::stdout(),
);
}
}
// #[derive(Debug, clap::Subcommand)]
// pub enum SubCommand {
// #[clap(name = "add")]
// Add(Add),
// #[clap(name = "list")]
// List(List),
// #[clap(name = "completions")]
// Completions { shell: clap_complete::Shell },
// }
//
// #[derive(Debug, clap::Args)]
// pub struct Add {
// #[clap(short, long)]
// pub name: String,
// }
//
// #[derive(Debug, clap::Args)]
// pub struct List {}
//
// impl Cli {
// pub fn completions(shell: clap_complete::Shell) {
// let mut command = <Cli as clap::CommandFactory>::command();
// clap_complete::generate(
// shell,
// &mut command,
// env!("CARGO_BIN_NAME"),
// &mut std::io::stdout(),
// );
// }
// }

View File

@@ -1,9 +1,14 @@
mod cli;
mod errors;
use api::JellyfinConfig;
use errors::*;
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)?;
Ok(())
}

View File

@@ -1,217 +1,10 @@
use std::{
borrow::Borrow,
collections::VecDeque,
marker::PhantomData,
path::Path,
sync::{Arc, RwLock, atomic::AtomicBool},
};
pub mod redb;
pub mod sqlite;
pub mod toml;
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>,
pub trait Store {
fn image(&self, id: &str) -> Option<Vec<u8>>;
fn save_image(&mut self, id: &str, data: &[u8]);
}
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, 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,
})
}
}
pub struct Settings {}

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;
use iced_video_player::{Video, VideoPlayer};
use shared_string::SharedString;
@@ -9,9 +11,9 @@ mod blur_hash;
use blur_hash::BlurHash;
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};
#[derive(Debug, Clone)]
@@ -104,37 +106,57 @@ pub enum Screen {
User,
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)]
struct State {
loading: Option<Loading>,
current: Option<uuid::Uuid>,
cache: ItemCache,
jellyfin_client: api::JellyfinClient,
jellyfin_client: Option<api::JellyfinClient>,
messages: Vec<String>,
history: Vec<Option<uuid::Uuid>>,
query: Option<String>,
screen: Screen,
// Login form state
username_input: String,
password_input: String,
settings: settings::SettingsState,
is_authenticated: bool,
// Video
video: Option<Arc<Video>>,
}
impl State {
pub fn new(jellyfin_client: api::JellyfinClient) -> Self {
pub fn new() -> Self {
State {
loading: None,
current: None,
cache: ItemCache::default(),
jellyfin_client,
jellyfin_client: None,
messages: Vec::new(),
history: Vec::new(),
query: None,
screen: Screen::Home,
username_input: String::new(),
password_input: String::new(),
settings: settings::SettingsState::default(),
// username_input: String::new(),
// password_input: String::new(),
is_authenticated: false,
video: None,
}
@@ -143,8 +165,7 @@ impl State {
#[derive(Debug, Clone)]
pub enum Message {
OpenSettings,
CloseSettings,
Settings(settings::SettingsMessage),
Refresh,
Search,
SearchQueryChanged(String),
@@ -154,95 +175,22 @@ pub enum Message {
SetToken(String),
Back,
Home,
// Login-related messages
UsernameChanged(String),
PasswordChanged(String),
Login,
LoginSuccess(String),
LoadedClient(api::JellyfinClient, bool),
Logout,
Video(VideoMessage),
}
#[derive(Debug, Clone)]
pub enum VideoMessage {
EndOfStream,
Open(url::Url),
Pause,
Play,
Seek(f64),
Stop,
Test,
// Login {
// username: String,
// password: String,
// config: api::JellyfinConfig,
// },
// LoginSuccess(String),
// LoadedClient(api::JellyfinClient, bool),
// Logout,
Video(video::VideoMessage),
}
fn update(state: &mut State, message: Message) -> Task<Message> {
// if let Some(client) = state.jellyfin_client.clone() {
match message {
Message::OpenSettings => {
state.screen = Screen::Settings;
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();
Message::Settings(msg) => settings::update(&mut state.settings, msg),
Message::OpenItem(id) if let Some(client) = state.jellyfin_client.clone() => {
use api::jellyfin::BaseItemKind::*;
if let Some(cached) = id.as_ref().and_then(|id| state.cache.get(id))
&& matches!(cached._type, Video | Movie | Episode)
@@ -250,7 +198,7 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
let url = client
.stream_url(id.expect("ID exists"))
.expect("Failed to get stream URL");
Task::done(Message::Video(VideoMessage::Open(url)))
Task::done(Message::Video(video::VideoMessage::Open(url)))
} else {
Task::perform(
async move {
@@ -273,9 +221,9 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
state.current = id;
Task::none()
}
Message::Refresh => {
Message::Refresh if let Some(client) = state.jellyfin_client.clone() => {
// Handle refresh logic
let client = state.jellyfin_client.clone();
// let client = state.jellyfin_client.clone();
let current = state.current;
Task::perform(
async move {
@@ -298,7 +246,10 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
}
Message::SetToken(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;
Task::none()
}
@@ -315,9 +266,9 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
// Handle search query change
Task::none()
}
Message::Search => {
Message::Search if let Some(client) = state.jellyfin_client.clone() => {
// Handle search action
let client = state.jellyfin_client.clone();
// let client = state.jellyfin_client.clone();
let query = state.query.clone().unwrap_or_default();
Task::perform(async move { client.search(query).await }, |r| match r {
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 {
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);
})
.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()
}
},
Message::Video(msg) => video::update(state, msg),
_ => todo!(),
}
}
fn view(state: &State) -> Element<'_, Message> {
match state.screen {
// Screen::Settings => settings::settings(state),
Screen::Settings => settings::settings(state),
Screen::Home | _ => home(state),
}
}
@@ -395,25 +297,9 @@ fn home(state: &State) -> Element<'_, Message> {
.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> {
if let Some(ref video) = state.video {
player(video)
video::player(video)
} else {
scrollable(
container(
@@ -436,7 +322,13 @@ fn header(state: &State) -> Element<'_, Message> {
row([
container(
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),
)
.on_press(Message::Home),
@@ -452,9 +344,11 @@ fn header(state: &State) -> Element<'_, Message> {
container(
row([
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")
.on_press(Message::Video(VideoMessage::Test))
.on_press(Message::Video(video::VideoMessage::Test))
.into(),
])
.spacing(10),
@@ -544,20 +438,20 @@ fn card(item: &Item) -> Element<'_, Message> {
fn init() -> (State, Task<Message>) {
// Create a default config for initial state
let default_config = api::JellyfinConfig {
server_url: "http://localhost:8096".parse().expect("Valid URL"),
device_id: "jello-iced".to_string(),
device_name: "Jello Iced".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_config = api::JellyfinConfig {
// server_url: "http://localhost:8096".parse().expect("Valid URL"),
// device_id: "jello-iced".to_string(),
// device_name: "Jello Iced".to_string(),
// client_name: "Jello".to_string(),
// version: "0.1.0".to_string(),
// };
// let default_client = api::JellyfinClient::new_with_config(default_config);
(
State::new(default_client),
State::new(),
Task::perform(
async move {
// Load config from file
let config_str = std::fs::read_to_string("config.toml")
.map_err(|e| api::JellyfinApiError::IoError(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 {
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)),
_ => Message::Error("Login Unimplemented".to_string()),
},
)
.chain(Task::done(Message::Refresh)),

View File

@@ -1,8 +1,26 @@
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 {
login_form: LoginForm,
server_form: ServerForm,
@@ -16,8 +34,9 @@ pub enum SettingsMessage {
Select(SettingsScreen),
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub enum SettingsScreen {
#[default]
Main,
Users,
Servers,
@@ -37,20 +56,27 @@ pub struct UserItem {
pub name: SharedString,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct LoginForm {
username: String,
password: String,
username: Option<String>,
password: Option<String>,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct ServerForm {
name: String,
url: String,
name: Option<String>,
url: Option<String>,
}
mod screens {
pub fn main(state: &State) -> Element<'_, Message> {}
pub fn server(state: &State) -> Element<'_, Message> {}
pub fn user(state: &State) -> Element<'_, Message> {}
use super::*;
pub fn main(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()
}