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

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

78
Cargo.lock generated
View File

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

View File

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

View File

@@ -566,7 +566,7 @@ pub struct BaseItemDto {
#[serde(rename = "SeriesThumbImageTag")] #[serde(rename = "SeriesThumbImageTag")]
pub series_thumb_image_tag: Option<String>, pub series_thumb_image_tag: Option<String>,
/** Gets or sets the blurhashes for the image tags. /** 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")] #[serde(rename = "ImageBlurHashes")]
pub image_blur_hashes: BaseItemDtoImageBlurHashes, pub image_blur_hashes: BaseItemDtoImageBlurHashes,
/// Gets or sets the series studio. /// Gets or sets the series studio.
@@ -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 {
@@ -1541,9 +1546,9 @@ pub struct ExternalIdInfo {
#[serde(rename = "Key")] #[serde(rename = "Key")]
pub key: String, pub key: String,
/** Gets or sets the specific media type for this id. This is used to distinguish between the different /** 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. 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 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.*/ default id for the external provider so there is no need to specify a type.*/
#[serde(rename = "Type")] #[serde(rename = "Type")]
pub _type: Option<ExternalIdMediaType>, pub _type: Option<ExternalIdMediaType>,
/// Gets or sets the URL format string. /// Gets or sets the URL format string.
@@ -2439,7 +2444,7 @@ pub struct MediaSourceInfo {
#[serde(rename = "Name")] #[serde(rename = "Name")]
pub name: Option<String>, pub name: Option<String>,
/** Gets or sets a value indicating whether the media is remote. /** 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")] #[serde(rename = "IsRemote")]
pub is_remote: bool, pub is_remote: bool,
#[serde(rename = "ETag")] #[serde(rename = "ETag")]
@@ -2501,7 +2506,7 @@ pub struct MediaSourceInfo {
#[serde(rename = "TranscodingUrl")] #[serde(rename = "TranscodingUrl")]
pub transcoding_url: Option<String>, pub transcoding_url: Option<String>,
/** Media streaming protocol. /** Media streaming protocol.
Lowercase for backwards compatibility.*/ Lowercase for backwards compatibility.*/
#[serde(rename = "TranscodingSubProtocol")] #[serde(rename = "TranscodingSubProtocol")]
pub transcoding_sub_protocol: MediaStreamProtocol, pub transcoding_sub_protocol: MediaStreamProtocol,
#[serde(rename = "TranscodingContainer")] #[serde(rename = "TranscodingContainer")]
@@ -2652,8 +2657,8 @@ pub struct MediaStream {
#[serde(rename = "RealFrameRate")] #[serde(rename = "RealFrameRate")]
pub real_frame_rate: Option<f32>, pub real_frame_rate: Option<f32>,
/** Gets the framerate used as reference. /** Gets the framerate used as reference.
Prefer AverageFrameRate, if that is null or an unrealistic value Prefer AverageFrameRate, if that is null or an unrealistic value
then fallback to RealFrameRate.*/ then fallback to RealFrameRate.*/
#[serde(rename = "ReferenceFrameRate")] #[serde(rename = "ReferenceFrameRate")]
pub reference_frame_rate: Option<f32>, pub reference_frame_rate: Option<f32>,
/// Gets or sets the profile. /// Gets or sets the profile.
@@ -2715,7 +2720,7 @@ pub struct MediaUpdateInfoPathDto {
#[serde(rename = "Path")] #[serde(rename = "Path")]
pub path: Option<String>, pub path: Option<String>,
/** Gets or sets media update type. /** Gets or sets media update type.
Created, Modified, Deleted.*/ Created, Modified, Deleted.*/
#[serde(rename = "UpdateType")] #[serde(rename = "UpdateType")]
pub update_type: Option<String>, pub update_type: Option<String>,
} }
@@ -2964,7 +2969,7 @@ pub struct NetworkConfiguration {
#[serde(rename = "EnablePublishedServerUriByRequest")] #[serde(rename = "EnablePublishedServerUriByRequest")]
pub enable_published_server_uri_by_request: bool, pub enable_published_server_uri_by_request: bool,
/** Gets or sets the PublishedServerUriBySubnet /** 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")] #[serde(rename = "PublishedServerUriBySubnet")]
pub published_server_uri_by_subnet: Vec<String>, 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" />. /// 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")] #[serde(rename = "AlwaysBurnInSubtitleWhenTranscoding")]
pub always_burn_in_subtitle_when_transcoding: Option<bool>, 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. /** A MediaBrowser.Model.Dlna.DeviceProfile represents a set of metadata which determines which content a certain device is able to play.
<br /> <br />
Specifically, it defines the supported <see cref="P:MediaBrowser.Model.Dlna.DeviceProfile.ContainerProfiles">containers</see> and 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) <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), 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.*/ 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")] #[serde(rename = "DeviceProfile")]
pub device_profile: Option<DeviceProfile>, pub device_profile: Option<DeviceProfile>,
/// Gets or sets the device play protocols. /// Gets or sets the device play protocols.
@@ -3068,7 +3073,7 @@ pub struct PackageInfo {
#[serde(rename = "category")] #[serde(rename = "category")]
pub category: String, pub category: String,
/** Gets or sets the guid of the assembly associated with this plugin. /** 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")] #[serde(rename = "guid")]
pub guid: uuid::Uuid, pub guid: uuid::Uuid,
/// Gets or sets the versions. /// Gets or sets the versions.
@@ -3187,11 +3192,11 @@ pub struct PlaybackInfoDto {
#[serde(rename = "LiveStreamId")] #[serde(rename = "LiveStreamId")]
pub live_stream_id: Option<String>, 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. /** A MediaBrowser.Model.Dlna.DeviceProfile represents a set of metadata which determines which content a certain device is able to play.
<br /> <br />
Specifically, it defines the supported <see cref="P:MediaBrowser.Model.Dlna.DeviceProfile.ContainerProfiles">containers</see> and 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) <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), 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.*/ 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")] #[serde(rename = "DeviceProfile")]
pub device_profile: Option<DeviceProfile>, pub device_profile: Option<DeviceProfile>,
/// Gets or sets a value indicating whether to enable direct play. /// Gets or sets a value indicating whether to enable direct play.
@@ -4388,7 +4393,7 @@ pub struct ServerConfiguration {
#[serde(rename = "PreviousVersion")] #[serde(rename = "PreviousVersion")]
pub previous_version: Option<String>, pub previous_version: Option<String>,
/** Gets or sets the stringified PreviousVersion to be stored/loaded, /** 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")] #[serde(rename = "PreviousVersionStr")]
pub previous_version_str: Option<String>, pub previous_version_str: Option<String>,
/// Gets or sets a value indicating whether to enable prometheus metrics exporting. /// Gets or sets a value indicating whether to enable prometheus metrics exporting.
@@ -4441,12 +4446,12 @@ pub struct ServerConfiguration {
#[serde(rename = "MaxAudiobookResume")] #[serde(rename = "MaxAudiobookResume")]
pub max_audiobook_resume: i32, pub max_audiobook_resume: i32,
/** Gets or sets the threshold in minutes after a inactive session gets closed automatically. /** 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")] #[serde(rename = "InactiveSessionThreshold")]
pub inactive_session_threshold: i32, 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 /** 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 Some delay is necessary with some items because their creation is not atomic. It involves the creation of several
different directories and files.*/ different directories and files.*/
#[serde(rename = "LibraryMonitorDelay")] #[serde(rename = "LibraryMonitorDelay")]
pub library_monitor_delay: i32, 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. /// 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")] #[serde(rename = "EnableHwEncoding")]
pub enable_hw_encoding: bool, pub enable_hw_encoding: bool,
/** Gets or sets a value indicating whether to only extract key frames. /** 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")] #[serde(rename = "EnableKeyFrameOnlyExtraction")]
pub enable_key_frame_only_extraction: bool, pub enable_key_frame_only_extraction: bool,
/// Gets or sets the behavior used by trickplay provider on library scan/update. /// Gets or sets the behavior used by trickplay provider on library scan/update.
@@ -5707,7 +5712,7 @@ pub struct UserDto {
#[serde(rename = "ServerId")] #[serde(rename = "ServerId")]
pub server_id: Option<String>, pub server_id: Option<String>,
/** Gets or sets the name of the server. /** 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")] #[serde(rename = "ServerName")]
pub server_name: Option<String>, pub server_name: Option<String>,
/// Gets or sets the id. /// 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> { pub fn pre_authenticated(token: impl AsRef<str>, config: JellyfinConfig) -> Result<Self> {
let auth_header = core::iter::once(( let auth_header = core::iter::once((
reqwest::header::HeaderName::from_static("X-Emby-Authorization"), reqwest::header::HeaderName::from_static("x-emby-authorization"),
reqwest::header::HeaderValue::from_str(&format!( reqwest::header::HeaderValue::from_str(&format!(
"MediaBrowser Client=\"{}\", Device=\"{}\", DeviceId=\"{}\", Version=\"{}\"", "MediaBrowser Client=\"{}\", Device=\"{}\", DeviceId=\"{}\", Version=\"{}\"",
config.client_name, config.device_name, config.device_id, config.version config.client_name, config.device_name, config.device_id, config.version

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

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

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

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

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

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