feat: add jello-types crate and update dependencies with backtrace support
This commit is contained in:
78
Cargo.lock
generated
78
Cargo.lock
generated
@@ -37,6 +37,15 @@ dependencies = [
|
||||
"nom 7.1.3",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
8
jello-types/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "jello-types"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
uuid = { version = "1.18.1", features = ["serde"] }
|
||||
6
jello-types/src/lib.rs
Normal file
6
jello-types/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct User {
|
||||
id: uuid::Uuid,
|
||||
name: Option<String>,
|
||||
primary_image_tag: Option<String>,
|
||||
}
|
||||
66
src/cli.rs
66
src/cli.rs
@@ -1,36 +1,38 @@
|
||||
#[derive(Debug, clap::Parser)]
|
||||
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(),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
221
store/src/lib.rs
221
store/src/lib.rs
@@ -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
226
store/src/redb.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::VecDeque,
|
||||
marker::PhantomData,
|
||||
path::Path,
|
||||
sync::{Arc, RwLock, atomic::AtomicBool},
|
||||
};
|
||||
|
||||
use futures::task::AtomicWaker;
|
||||
use redb::{Error, Key, ReadableDatabase, TableDefinition, Value};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
|
||||
const USERS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("users");
|
||||
const SERVERS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("servers");
|
||||
const SETTINGS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("settings");
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TableInner<T> {
|
||||
db: Arc<T>,
|
||||
}
|
||||
|
||||
impl<T> Clone for TableInner<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
db: Arc::clone(&self.db),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> TableInner<T> {
|
||||
fn new(db: Arc<T>) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
}
|
||||
|
||||
impl TableInner<DatabaseHandle> {
|
||||
async fn get<'a, K: Key, V: Serialize + DeserializeOwned>(
|
||||
&self,
|
||||
table: TableDefinition<'static, K, Vec<u8>>,
|
||||
key: impl Borrow<K::SelfType<'a>>,
|
||||
) -> Result<Option<V>> {
|
||||
let db: &redb::Database = &self.db.as_ref().database;
|
||||
let db_reader = db.begin_read()?;
|
||||
let table = db_reader.open_table(table)?;
|
||||
table
|
||||
.get(key)?
|
||||
.map(|value| bson::deserialize_from_slice(&value.value()))
|
||||
.transpose()
|
||||
.map_err(|e| redb::Error::Io(std::io::Error::other(e)))
|
||||
}
|
||||
|
||||
async fn insert<
|
||||
'a,
|
||||
'b,
|
||||
K: Key + Send + Sync,
|
||||
V: Serialize + DeserializeOwned + Send + Sync + 'a,
|
||||
>(
|
||||
&'b self,
|
||||
table: TableDefinition<'static, K, Vec<u8>>,
|
||||
key: impl Borrow<K::SelfType<'a>> + Send + 'b,
|
||||
value: V,
|
||||
) -> Result<Option<V>> {
|
||||
let db: &redb::Database = &self.db.as_ref().database;
|
||||
// self.db
|
||||
// .writing
|
||||
// .store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
|
||||
// let out = tokio::task::spawn_blocking(move || -> Result<Option<V>>
|
||||
|
||||
let out = tokio::task::spawn_blocking(|| -> Result<Option<V>> {
|
||||
let db_writer = db.begin_write()?;
|
||||
let out = {
|
||||
let mut table = db_writer.open_table(table)?;
|
||||
let serialized_value = bson::serialize_to_vec(&value)
|
||||
.map_err(|e| redb::Error::Io(std::io::Error::other(e)))?;
|
||||
let previous = table.insert(key, &serialized_value)?;
|
||||
let out = previous
|
||||
.map(|value| bson::deserialize_from_slice(&value.value()))
|
||||
.transpose()
|
||||
.map_err(|e| redb::Error::Io(std::io::Error::other(e)));
|
||||
out
|
||||
};
|
||||
db_writer.commit()?;
|
||||
out
|
||||
})
|
||||
.await
|
||||
.expect("Task panicked");
|
||||
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
// impl<K: Key, V: Serialize + DeserializeOwned> Table<K, V> for TableInner {
|
||||
// async fn get(&self, key: K) -> Result<Option<Value>> {}
|
||||
// async fn insert(&self, key: K, value: V) -> Result<Option<Value>> {}
|
||||
// async fn modify(&self, key: K, v: FnOnce(V) -> V) -> Result<bool> {}
|
||||
// async fn remove(&self, key: K) -> Result<Option<Value>> {}
|
||||
// }
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Users<T>(TableInner<T>);
|
||||
|
||||
impl<T> Clone for Users<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
impl<T> Users<T> {
|
||||
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = USERS;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Servers<T>(TableInner<T>);
|
||||
impl<T> Clone for Servers<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
impl<T> Servers<T> {
|
||||
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = SERVERS;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Settings<T>(TableInner<T>);
|
||||
impl<T> Clone for Settings<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
impl<T> Settings<T> {
|
||||
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = SETTINGS;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Database {
|
||||
users: Users<DatabaseHandle>,
|
||||
servers: Servers<DatabaseHandle>,
|
||||
settings: Settings<DatabaseHandle>,
|
||||
handle: Arc<DatabaseHandle>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DatabaseHandle {
|
||||
database: redb::Database,
|
||||
writing: AtomicBool,
|
||||
wakers: RwLock<VecDeque<AtomicWaker>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DatabaseWriterGuard<'a> {
|
||||
handle: &'a DatabaseHandle,
|
||||
dropper: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
// impl Drop for DatabaseWriterGuard<'_> {
|
||||
// fn drop(&mut self) {
|
||||
// self.handle
|
||||
// .writing
|
||||
// .store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
// let is_panicking = std::thread::panicking();
|
||||
// let Ok(writer) = self.handle.wakers.write() else {
|
||||
// if is_panicking {
|
||||
// return;
|
||||
// } else {
|
||||
// panic!("Wakers lock poisoned");
|
||||
// }
|
||||
// }
|
||||
// if let Some(waker) = (self.handle.wakers.write()).pop() {
|
||||
// waker.wake();
|
||||
// };
|
||||
// // let mut wakers = self.handle.wakers.write().expect();
|
||||
// // if let Some(waker) = self.handle.wakers.write().expect("Wakers lock poisoned").pop_front() {
|
||||
// // waker.wake();
|
||||
// // }
|
||||
// // while let Some(waker) = wakers.pop_front() {
|
||||
// // waker.wake();
|
||||
// // }
|
||||
// }
|
||||
// }
|
||||
|
||||
type Result<O, E = redb::Error> = core::result::Result<O, E>;
|
||||
|
||||
pub trait Table<K: Key> {
|
||||
fn insert<V: Serialize + DeserializeOwned>(
|
||||
&self,
|
||||
key: K,
|
||||
value: V,
|
||||
) -> impl Future<Output = Result<Option<V>>> + Send;
|
||||
fn modify<V: Serialize + DeserializeOwned, O: Serialize + DeserializeOwned>(
|
||||
&self,
|
||||
key: K,
|
||||
v: impl FnOnce(V) -> O,
|
||||
) -> impl Future<Output = Result<bool>> + Send;
|
||||
fn remove<V: Serialize + DeserializeOwned>(
|
||||
&self,
|
||||
key: K,
|
||||
) -> impl Future<Output = Result<Option<V>>> + Send;
|
||||
fn get<V: Serialize + DeserializeOwned>(
|
||||
&self,
|
||||
key: K,
|
||||
) -> impl Future<Output = Result<Option<V>>> + Send;
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn create(path: impl AsRef<Path>) -> Result<Self, Error> {
|
||||
let writing = AtomicBool::new(false);
|
||||
let wakers = RwLock::new(VecDeque::new());
|
||||
let db = redb::Database::create(path)?;
|
||||
let db = Arc::new(DatabaseHandle {
|
||||
database: db,
|
||||
writing,
|
||||
wakers,
|
||||
});
|
||||
let table_inner = TableInner::new(Arc::clone(&db));
|
||||
let users = Users(table_inner.clone());
|
||||
let servers = Servers(table_inner.clone());
|
||||
let settings = Settings(table_inner.clone());
|
||||
Ok(Self {
|
||||
servers,
|
||||
users,
|
||||
settings,
|
||||
handle: db,
|
||||
})
|
||||
}
|
||||
}
|
||||
0
store/src/sqlite.rs
Normal file
0
store/src/sqlite.rs
Normal file
0
store/src/toml.rs
Normal file
0
store/src/toml.rs
Normal file
@@ -1,4 +1,6 @@
|
||||
// mod settings;
|
||||
mod settings;
|
||||
mod video;
|
||||
|
||||
mod shared_string;
|
||||
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)),
|
||||
|
||||
@@ -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
85
ui-iced/src/video.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use super::*;
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum VideoMessage {
|
||||
EndOfStream,
|
||||
Open(url::Url),
|
||||
Pause,
|
||||
Play,
|
||||
Seek(f64),
|
||||
Stop,
|
||||
Test,
|
||||
}
|
||||
|
||||
pub fn update(state: &mut State, message: VideoMessage) -> Task<Message> {
|
||||
match message {
|
||||
VideoMessage::EndOfStream => {
|
||||
state.video = None;
|
||||
Task::none()
|
||||
}
|
||||
VideoMessage::Open(url) => {
|
||||
state.video = Video::new(&url)
|
||||
.inspect_err(|err| {
|
||||
tracing::error!("Failed to play video at {}: {:?}", url, err);
|
||||
})
|
||||
.inspect(|video| {
|
||||
tracing::info!("Framerate {}", video.framerate());
|
||||
})
|
||||
.ok()
|
||||
.map(Arc::new);
|
||||
Task::none()
|
||||
}
|
||||
VideoMessage::Pause => {
|
||||
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) {
|
||||
video.set_paused(true);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
VideoMessage::Play => {
|
||||
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) {
|
||||
video.set_paused(false);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
VideoMessage::Seek(position) => {
|
||||
// if let Some(ref video) = state.video {
|
||||
// // video.seek(position, true);
|
||||
// }
|
||||
Task::none()
|
||||
}
|
||||
VideoMessage::Stop => {
|
||||
state.video = None;
|
||||
Task::none()
|
||||
}
|
||||
VideoMessage::Test => {
|
||||
let url = url::Url::parse(
|
||||
// "file:///home/servius/Projects/jello/crates/iced_video_player/.media/test.mp4",
|
||||
"https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm",
|
||||
// "https://www.youtube.com/watch?v=QbUUaXGA3C4",
|
||||
)
|
||||
.unwrap();
|
||||
state.video = Video::new(&url)
|
||||
.inspect_err(|err| {
|
||||
tracing::error!("{err:?}");
|
||||
})
|
||||
.ok()
|
||||
.map(Arc::new);
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn player(video: &Video) -> Element<'_, Message> {
|
||||
container(
|
||||
VideoPlayer::new(video)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.content_fit(iced::ContentFit::Contain)
|
||||
.on_end_of_stream(Message::Video(VideoMessage::EndOfStream)),
|
||||
)
|
||||
.style(|_| container::background(iced::Color::BLACK))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.align_x(Alignment::Center)
|
||||
.align_y(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
Reference in New Issue
Block a user