feat: add jello-types crate and update dependencies with backtrace support
This commit is contained in:
78
Cargo.lock
generated
78
Cargo.lock
generated
@@ -37,6 +37,15 @@ dependencies = [
|
|||||||
"nom 7.1.3",
|
"nom 7.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "addr2line"
|
||||||
|
version = "0.25.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
|
||||||
|
dependencies = [
|
||||||
|
"gimli",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "adler2"
|
name = "adler2"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@@ -222,7 +231,7 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a"
|
checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"object",
|
"object 0.32.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -649,6 +658,21 @@ dependencies = [
|
|||||||
"arrayvec",
|
"arrayvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "backtrace"
|
||||||
|
version = "0.3.76"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
|
||||||
|
dependencies = [
|
||||||
|
"addr2line",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"miniz_oxide",
|
||||||
|
"object 0.37.3",
|
||||||
|
"rustc-demangle",
|
||||||
|
"windows-link 0.2.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
@@ -1167,6 +1191,17 @@ dependencies = [
|
|||||||
"clap_derive",
|
"clap_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap-verbosity-flag"
|
||||||
|
version = "3.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d92b1fab272fe943881b77cc6e920d6543e5b1bfadbd5ed81c7c5a755742394"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"log",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.53"
|
version = "4.5.53"
|
||||||
@@ -1316,6 +1351,16 @@ dependencies = [
|
|||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "color-backtrace"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "308329d5d62e877ba02943db3a8e8c052de9fde7ab48283395ba0e6494efbabd"
|
||||||
|
dependencies = [
|
||||||
|
"backtrace",
|
||||||
|
"termcolor",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "color_quant"
|
name = "color_quant"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -2504,6 +2549,12 @@ dependencies = [
|
|||||||
"weezl",
|
"weezl",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gimli"
|
||||||
|
version = "0.32.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gio-sys"
|
name = "gio-sys"
|
||||||
version = "0.21.2"
|
version = "0.21.2"
|
||||||
@@ -3859,7 +3910,9 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"api",
|
"api",
|
||||||
"clap",
|
"clap",
|
||||||
|
"clap-verbosity-flag",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
|
"color-backtrace",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"error-stack",
|
"error-stack",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
@@ -3871,6 +3924,14 @@ dependencies = [
|
|||||||
"ui-iced",
|
"ui-iced",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jello-types"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jiff"
|
name = "jiff"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@@ -5129,6 +5190,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "object"
|
||||||
|
version = "0.37.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
@@ -6147,6 +6217,12 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-demangle"
|
||||||
|
version = "0.1.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ members = [
|
|||||||
"typegen",
|
"typegen",
|
||||||
"ui-gpui",
|
"ui-gpui",
|
||||||
"ui-iced",
|
"ui-iced",
|
||||||
"crates/iced_video_player", "store",
|
"crates/iced_video_player", "store", "jello-types",
|
||||||
]
|
]
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
iced = { git = "https://github.com/iced-rs/iced", features = [
|
iced = { git = "https://github.com/iced-rs/iced", features = [
|
||||||
@@ -27,7 +27,9 @@ license = "MIT"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
api = { version = "0.1.0", path = "api" }
|
api = { version = "0.1.0", path = "api" }
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
clap-verbosity-flag = { version = "3.0.4", features = ["tracing"] }
|
||||||
clap_complete = "4.5"
|
clap_complete = "4.5"
|
||||||
|
color-backtrace = "0.7.2"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
error-stack = "0.6"
|
error-stack = "0.6"
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ pub struct ActivityLogEntryQueryResult {
|
|||||||
#[serde(rename = "StartIndex")]
|
#[serde(rename = "StartIndex")]
|
||||||
pub start_index: i32,
|
pub start_index: i32,
|
||||||
}
|
}
|
||||||
/** Activity log entry start message.
|
/** Activity log entry start message.
|
||||||
Data is the timing data encoded as "$initialDelay,$interval" in ms.*/
|
Data is the timing data encoded as "$initialDelay,$interval" in ms.*/
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct ActivityLogEntryStartMessage {
|
pub struct ActivityLogEntryStartMessage {
|
||||||
@@ -290,7 +290,7 @@ pub struct AuthenticationResult {
|
|||||||
#[serde(rename = "ServerId")]
|
#[serde(rename = "ServerId")]
|
||||||
pub server_id: Option<String>,
|
pub server_id: Option<String>,
|
||||||
}
|
}
|
||||||
/** This is strictly used as a data transfer object from the api layer.
|
/** This is strictly used as a data transfer object from the api layer.
|
||||||
This holds information about a BaseItem in a format that is convenient for the client.*/
|
This holds information about a BaseItem in a format that is convenient for the client.*/
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct BaseItemDto {
|
pub struct BaseItemDto {
|
||||||
@@ -565,8 +565,8 @@ pub struct BaseItemDto {
|
|||||||
/// Gets or sets the series thumb image tag.
|
/// Gets or sets the series thumb image tag.
|
||||||
#[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")]
|
||||||
@@ -721,7 +724,7 @@ pub struct BaseItemDto {
|
|||||||
#[serde(rename = "CurrentProgram")]
|
#[serde(rename = "CurrentProgram")]
|
||||||
pub current_program: Option<Box<BaseItemDto>>,
|
pub current_program: Option<Box<BaseItemDto>>,
|
||||||
}
|
}
|
||||||
/** 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.*/
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct BaseItemDtoImageBlurHashes {
|
pub struct BaseItemDtoImageBlurHashes {
|
||||||
@@ -1275,11 +1278,11 @@ pub struct DeviceOptionsDto {
|
|||||||
#[serde(rename = "CustomName")]
|
#[serde(rename = "CustomName")]
|
||||||
pub custom_name: Option<String>,
|
pub custom_name: 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.*/
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct DeviceProfile {
|
pub struct DeviceProfile {
|
||||||
@@ -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 {
|
||||||
@@ -1540,10 +1545,10 @@ pub struct ExternalIdInfo {
|
|||||||
/// Gets or sets the unique key for this id. This key should be unique across all providers.
|
/// Gets or sets the unique key for this id. This key should be unique across all providers.
|
||||||
#[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.
|
||||||
@@ -2438,8 +2443,8 @@ pub struct MediaSourceInfo {
|
|||||||
pub size: Option<i64>,
|
pub size: Option<i64>,
|
||||||
#[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")]
|
||||||
@@ -2500,8 +2505,8 @@ pub struct MediaSourceInfo {
|
|||||||
pub required_http_headers: Option<std::collections::HashMap<String, Option<String>>>,
|
pub required_http_headers: Option<std::collections::HashMap<String, Option<String>>>,
|
||||||
#[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")]
|
||||||
@@ -2651,9 +2656,9 @@ pub struct MediaStream {
|
|||||||
/// Gets or sets the real frame rate.
|
/// Gets or sets the real frame rate.
|
||||||
#[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.
|
||||||
@@ -2714,8 +2719,8 @@ pub struct MediaUpdateInfoPathDto {
|
|||||||
/// Gets or sets media path.
|
/// Gets or sets media path.
|
||||||
#[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>,
|
||||||
}
|
}
|
||||||
@@ -2963,8 +2968,8 @@ pub struct NetworkConfiguration {
|
|||||||
/// Gets or sets a value indicating whether the published server uri is based on information in HTTP requests.
|
/// Gets or sets a value indicating whether the published server uri is based on information in HTTP requests.
|
||||||
#[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" />.
|
||||||
@@ -3027,12 +3032,12 @@ pub struct OpenLiveStreamDto {
|
|||||||
/// Gets or sets a value indicating whether always burn in subtitles when transcoding.
|
/// Gets or sets a value indicating whether always burn in subtitles when transcoding.
|
||||||
#[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.
|
||||||
@@ -3067,8 +3072,8 @@ pub struct PackageInfo {
|
|||||||
/// Gets or sets the category.
|
/// Gets or sets the category.
|
||||||
#[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.
|
||||||
@@ -3186,12 +3191,12 @@ pub struct PlaybackInfoDto {
|
|||||||
/// Gets or sets the live stream id.
|
/// Gets or sets the live stream id.
|
||||||
#[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.
|
||||||
@@ -4036,7 +4041,7 @@ pub struct ScheduledTasksInfoMessage {
|
|||||||
#[serde(rename = "MessageType")]
|
#[serde(rename = "MessageType")]
|
||||||
pub message_type: SessionMessageType,
|
pub message_type: SessionMessageType,
|
||||||
}
|
}
|
||||||
/** Scheduled tasks info start message.
|
/** Scheduled tasks info start message.
|
||||||
Data is the timing data encoded as "$initialDelay,$interval" in ms.*/
|
Data is the timing data encoded as "$initialDelay,$interval" in ms.*/
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct ScheduledTasksInfoStartMessage {
|
pub struct ScheduledTasksInfoStartMessage {
|
||||||
@@ -4387,8 +4392,8 @@ pub struct ServerConfiguration {
|
|||||||
/// Gets or sets the last known version that was ran using the configuration.
|
/// Gets or sets the last known version that was ran using the configuration.
|
||||||
#[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.
|
||||||
@@ -4440,13 +4445,13 @@ pub struct ServerConfiguration {
|
|||||||
/// Gets or sets the remaining minutes of a book that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.
|
/// Gets or sets the remaining minutes of a book that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.
|
||||||
#[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.
|
||||||
@@ -4665,7 +4670,7 @@ pub struct SessionsMessage {
|
|||||||
#[serde(rename = "MessageType")]
|
#[serde(rename = "MessageType")]
|
||||||
pub message_type: SessionMessageType,
|
pub message_type: SessionMessageType,
|
||||||
}
|
}
|
||||||
/** Sessions start message.
|
/** Sessions start message.
|
||||||
Data is the timing data encoded as "$initialDelay,$interval" in ms.*/
|
Data is the timing data encoded as "$initialDelay,$interval" in ms.*/
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct SessionsStartMessage {
|
pub struct SessionsStartMessage {
|
||||||
@@ -5324,7 +5329,7 @@ pub struct TranscodingInfo {
|
|||||||
#[serde(rename = "TranscodeReasons")]
|
#[serde(rename = "TranscodeReasons")]
|
||||||
pub transcode_reasons: Vec<TranscodeReason>,
|
pub transcode_reasons: Vec<TranscodeReason>,
|
||||||
}
|
}
|
||||||
/** A class for transcoding profile information.
|
/** A class for transcoding profile information.
|
||||||
Note for client developers: Conditions defined in MediaBrowser.Model.Dlna.CodecProfile has higher priority and can override values defined here.*/
|
Note for client developers: Conditions defined in MediaBrowser.Model.Dlna.CodecProfile has higher priority and can override values defined here.*/
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct TranscodingProfile {
|
pub struct TranscodingProfile {
|
||||||
@@ -5414,8 +5419,8 @@ pub struct TrickplayOptions {
|
|||||||
/// Gets or sets a value indicating whether or not to use HW accelerated MJPEG encoding.
|
/// Gets or sets a value indicating whether or not to use HW accelerated MJPEG encoding.
|
||||||
#[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.
|
||||||
@@ -5706,8 +5711,8 @@ pub struct UserDto {
|
|||||||
/// Gets or sets the server identifier.
|
/// Gets or sets the server identifier.
|
||||||
#[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.
|
||||||
@@ -7009,7 +7014,7 @@ pub enum MediaSourceType {
|
|||||||
#[serde(rename = "Placeholder")]
|
#[serde(rename = "Placeholder")]
|
||||||
Placeholder,
|
Placeholder,
|
||||||
}
|
}
|
||||||
/** Media streaming protocol.
|
/** Media streaming protocol.
|
||||||
Lowercase for backwards compatibility.*/
|
Lowercase for backwards compatibility.*/
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum MediaStreamProtocol {
|
pub enum MediaStreamProtocol {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ impl JellyfinClient {
|
|||||||
|
|
||||||
pub fn pre_authenticated(token: impl AsRef<str>, config: JellyfinConfig) -> Result<Self> {
|
pub fn pre_authenticated(token: impl AsRef<str>, config: JellyfinConfig) -> Result<Self> {
|
||||||
let auth_header = core::iter::once((
|
let auth_header = core::iter::once((
|
||||||
reqwest::header::HeaderName::from_static("X-Emby-Authorization"),
|
reqwest::header::HeaderName::from_static("x-emby-authorization"),
|
||||||
reqwest::header::HeaderValue::from_str(&format!(
|
reqwest::header::HeaderValue::from_str(&format!(
|
||||||
"MediaBrowser Client=\"{}\", Device=\"{}\", DeviceId=\"{}\", Version=\"{}\"",
|
"MediaBrowser Client=\"{}\", Device=\"{}\", DeviceId=\"{}\", Version=\"{}\"",
|
||||||
config.client_name, config.device_name, config.device_id, config.version
|
config.client_name, config.device_name, config.device_id, config.version
|
||||||
|
|||||||
8
jello-types/Cargo.toml
Normal file
8
jello-types/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "jello-types"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
uuid = { version = "1.18.1", features = ["serde"] }
|
||||||
6
jello-types/src/lib.rs
Normal file
6
jello-types/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct User {
|
||||||
|
id: uuid::Uuid,
|
||||||
|
name: Option<String>,
|
||||||
|
primary_image_tag: Option<String>,
|
||||||
|
}
|
||||||
66
src/cli.rs
66
src/cli.rs
@@ -1,36 +1,38 @@
|
|||||||
#[derive(Debug, clap::Parser)]
|
#[derive(Debug, clap::Parser)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
#[clap(subcommand)]
|
// #[clap(subcommand)]
|
||||||
pub cmd: SubCommand,
|
// pub cmd: SubCommand,
|
||||||
|
#[command(flatten)]
|
||||||
|
pub verbosity: clap_verbosity_flag::Verbosity,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, clap::Subcommand)]
|
// #[derive(Debug, clap::Subcommand)]
|
||||||
pub enum SubCommand {
|
// pub enum SubCommand {
|
||||||
#[clap(name = "add")]
|
// #[clap(name = "add")]
|
||||||
Add(Add),
|
// Add(Add),
|
||||||
#[clap(name = "list")]
|
// #[clap(name = "list")]
|
||||||
List(List),
|
// List(List),
|
||||||
#[clap(name = "completions")]
|
// #[clap(name = "completions")]
|
||||||
Completions { shell: clap_complete::Shell },
|
// Completions { shell: clap_complete::Shell },
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
#[derive(Debug, clap::Args)]
|
// #[derive(Debug, clap::Args)]
|
||||||
pub struct Add {
|
// pub struct Add {
|
||||||
#[clap(short, long)]
|
// #[clap(short, long)]
|
||||||
pub name: String,
|
// pub name: String,
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
#[derive(Debug, clap::Args)]
|
// #[derive(Debug, clap::Args)]
|
||||||
pub struct List {}
|
// pub struct List {}
|
||||||
|
//
|
||||||
impl Cli {
|
// impl Cli {
|
||||||
pub fn completions(shell: clap_complete::Shell) {
|
// pub fn completions(shell: clap_complete::Shell) {
|
||||||
let mut command = <Cli as clap::CommandFactory>::command();
|
// let mut command = <Cli as clap::CommandFactory>::command();
|
||||||
clap_complete::generate(
|
// clap_complete::generate(
|
||||||
shell,
|
// shell,
|
||||||
&mut command,
|
// &mut command,
|
||||||
env!("CARGO_BIN_NAME"),
|
// env!("CARGO_BIN_NAME"),
|
||||||
&mut std::io::stdout(),
|
// &mut std::io::stdout(),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
|
mod cli;
|
||||||
mod errors;
|
mod errors;
|
||||||
use api::JellyfinConfig;
|
use api::JellyfinConfig;
|
||||||
use errors::*;
|
use errors::*;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
tracing_subscriber::fmt::init();
|
color_backtrace::install();
|
||||||
|
let args = <cli::Cli as clap::Parser>::parse();
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(args.verbosity)
|
||||||
|
.init();
|
||||||
ui_iced::ui().change_context(Error)?;
|
ui_iced::ui().change_context(Error)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
221
store/src/lib.rs
221
store/src/lib.rs
@@ -1,217 +1,10 @@
|
|||||||
use std::{
|
pub mod redb;
|
||||||
borrow::Borrow,
|
pub mod sqlite;
|
||||||
collections::VecDeque,
|
pub mod toml;
|
||||||
marker::PhantomData,
|
|
||||||
path::Path,
|
|
||||||
sync::{Arc, RwLock, atomic::AtomicBool},
|
|
||||||
};
|
|
||||||
|
|
||||||
use futures::task::AtomicWaker;
|
pub trait Store {
|
||||||
use redb::{Error, Key, ReadableDatabase, TableDefinition, Value};
|
fn image(&self, id: &str) -> Option<Vec<u8>>;
|
||||||
use serde::{Serialize, de::DeserializeOwned};
|
fn save_image(&mut self, id: &str, data: &[u8]);
|
||||||
|
|
||||||
const USERS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("users");
|
|
||||||
const SERVERS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("servers");
|
|
||||||
const SETTINGS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("settings");
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TableInner<T> {
|
|
||||||
db: Arc<T>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Clone for TableInner<T> {
|
pub struct Settings {}
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
db: Arc::clone(&self.db),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> TableInner<T> {
|
|
||||||
fn new(db: Arc<T>) -> Self {
|
|
||||||
Self { db }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TableInner<DatabaseHandle> {
|
|
||||||
async fn get<'a, K: Key, V: Serialize + DeserializeOwned>(
|
|
||||||
&self,
|
|
||||||
table: TableDefinition<'static, K, Vec<u8>>,
|
|
||||||
key: impl Borrow<K::SelfType<'a>>,
|
|
||||||
) -> Result<Option<V>> {
|
|
||||||
let db: &redb::Database = &self.db.as_ref().database;
|
|
||||||
let db_reader = db.begin_read()?;
|
|
||||||
let table = db_reader.open_table(table)?;
|
|
||||||
table
|
|
||||||
.get(key)?
|
|
||||||
.map(|value| bson::deserialize_from_slice(&value.value()))
|
|
||||||
.transpose()
|
|
||||||
.map_err(|e| redb::Error::Io(std::io::Error::other(e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert<'a, 'b, K: Key + Send, V: Serialize + DeserializeOwned + Send + 'a>(
|
|
||||||
&'b self,
|
|
||||||
table: TableDefinition<'static, K, Vec<u8>>,
|
|
||||||
key: impl Borrow<K::SelfType<'a>> + Send + 'b,
|
|
||||||
value: V,
|
|
||||||
) -> Result<Option<V>> {
|
|
||||||
let db: &redb::Database = &self.db.as_ref().database;
|
|
||||||
// self.db
|
|
||||||
// .writing
|
|
||||||
// .store(true, std::sync::atomic::Ordering::SeqCst);
|
|
||||||
|
|
||||||
let out = tokio::task::spawn_blocking(move || -> Result<Option<V>> {
|
|
||||||
let db_writer = db.begin_write()?;
|
|
||||||
let out = {
|
|
||||||
let mut table = db_writer.open_table(table)?;
|
|
||||||
let serialized_value = bson::serialize_to_vec(&value)
|
|
||||||
.map_err(|e| redb::Error::Io(std::io::Error::other(e)))?;
|
|
||||||
let previous = table.insert(key, &serialized_value)?;
|
|
||||||
let out = previous
|
|
||||||
.map(|value| bson::deserialize_from_slice(&value.value()))
|
|
||||||
.transpose()
|
|
||||||
.map_err(|e| redb::Error::Io(std::io::Error::other(e)));
|
|
||||||
out
|
|
||||||
};
|
|
||||||
db_writer.commit()?;
|
|
||||||
out
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("Failed to run blocking task")?;
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// impl<K: Key, V: Serialize + DeserializeOwned> Table<K, V> for TableInner {
|
|
||||||
// async fn get(&self, key: K) -> Result<Option<Value>> {}
|
|
||||||
// async fn insert(&self, key: K, value: V) -> Result<Option<Value>> {}
|
|
||||||
// async fn modify(&self, key: K, v: FnOnce(V) -> V) -> Result<bool> {}
|
|
||||||
// async fn remove(&self, key: K) -> Result<Option<Value>> {}
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Users<T>(TableInner<T>);
|
|
||||||
|
|
||||||
impl<T> Clone for Users<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self(self.0.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> Users<T> {
|
|
||||||
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = USERS;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Servers<T>(TableInner<T>);
|
|
||||||
impl<T> Clone for Servers<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self(self.0.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> Servers<T> {
|
|
||||||
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = SERVERS;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Settings<T>(TableInner<T>);
|
|
||||||
impl<T> Clone for Settings<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self(self.0.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> Settings<T> {
|
|
||||||
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = SETTINGS;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Database {
|
|
||||||
users: Users<DatabaseHandle>,
|
|
||||||
servers: Servers<DatabaseHandle>,
|
|
||||||
settings: Settings<DatabaseHandle>,
|
|
||||||
handle: Arc<DatabaseHandle>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct DatabaseHandle {
|
|
||||||
database: redb::Database,
|
|
||||||
writing: AtomicBool,
|
|
||||||
wakers: RwLock<VecDeque<AtomicWaker>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct DatabaseWriterGuard<'a> {
|
|
||||||
handle: &'a DatabaseHandle,
|
|
||||||
dropper: Arc<AtomicBool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// impl Drop for DatabaseWriterGuard<'_> {
|
|
||||||
// fn drop(&mut self) {
|
|
||||||
// self.handle
|
|
||||||
// .writing
|
|
||||||
// .store(false, std::sync::atomic::Ordering::SeqCst);
|
|
||||||
// let is_panicking = std::thread::panicking();
|
|
||||||
// let Ok(writer) = self.handle.wakers.write() else {
|
|
||||||
// if is_panicking {
|
|
||||||
// return;
|
|
||||||
// } else {
|
|
||||||
// panic!("Wakers lock poisoned");
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if let Some(waker) = (self.handle.wakers.write()).pop() {
|
|
||||||
// waker.wake();
|
|
||||||
// };
|
|
||||||
// // let mut wakers = self.handle.wakers.write().expect();
|
|
||||||
// // if let Some(waker) = self.handle.wakers.write().expect("Wakers lock poisoned").pop_front() {
|
|
||||||
// // waker.wake();
|
|
||||||
// // }
|
|
||||||
// // while let Some(waker) = wakers.pop_front() {
|
|
||||||
// // waker.wake();
|
|
||||||
// // }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
type Result<O, E = redb::Error> = core::result::Result<O, E>;
|
|
||||||
|
|
||||||
pub trait Table<K: Key> {
|
|
||||||
fn insert<V: Serialize + DeserializeOwned>(
|
|
||||||
&self,
|
|
||||||
key: K,
|
|
||||||
value: V,
|
|
||||||
) -> impl Future<Output = Result<Option<V>>> + Send;
|
|
||||||
fn modify<V: Serialize + DeserializeOwned, O: Serialize + DeserializeOwned>(
|
|
||||||
&self,
|
|
||||||
key: K,
|
|
||||||
v: impl FnOnce(V) -> O,
|
|
||||||
) -> impl Future<Output = Result<bool>> + Send;
|
|
||||||
fn remove<V: Serialize + DeserializeOwned>(
|
|
||||||
&self,
|
|
||||||
key: K,
|
|
||||||
) -> impl Future<Output = Result<Option<V>>> + Send;
|
|
||||||
fn get<V: Serialize + DeserializeOwned>(
|
|
||||||
&self,
|
|
||||||
key: K,
|
|
||||||
) -> impl Future<Output = Result<Option<V>>> + Send;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Database {
|
|
||||||
pub fn create(path: impl AsRef<Path>) -> Result<Self, Error> {
|
|
||||||
let writing = AtomicBool::new(false);
|
|
||||||
let wakers = RwLock::new(VecDeque::new());
|
|
||||||
let db = redb::Database::create(path)?;
|
|
||||||
let db = Arc::new(DatabaseHandle {
|
|
||||||
database: db,
|
|
||||||
writing,
|
|
||||||
wakers,
|
|
||||||
});
|
|
||||||
let table_inner = TableInner::new(Arc::clone(&db));
|
|
||||||
let users = Users(table_inner.clone());
|
|
||||||
let servers = Servers(table_inner.clone());
|
|
||||||
let settings = Settings(table_inner.clone());
|
|
||||||
Ok(Self {
|
|
||||||
servers,
|
|
||||||
users,
|
|
||||||
settings,
|
|
||||||
handle: db,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
226
store/src/redb.rs
Normal file
226
store/src/redb.rs
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
|
||||||
|
use std::{
|
||||||
|
borrow::Borrow,
|
||||||
|
collections::VecDeque,
|
||||||
|
marker::PhantomData,
|
||||||
|
path::Path,
|
||||||
|
sync::{Arc, RwLock, atomic::AtomicBool},
|
||||||
|
};
|
||||||
|
|
||||||
|
use futures::task::AtomicWaker;
|
||||||
|
use redb::{Error, Key, ReadableDatabase, TableDefinition, Value};
|
||||||
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
|
|
||||||
|
const USERS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("users");
|
||||||
|
const SERVERS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("servers");
|
||||||
|
const SETTINGS: TableDefinition<uuid::Uuid, Vec<u8>> = TableDefinition::new("settings");
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TableInner<T> {
|
||||||
|
db: Arc<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clone for TableInner<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
db: Arc::clone(&self.db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> TableInner<T> {
|
||||||
|
fn new(db: Arc<T>) -> Self {
|
||||||
|
Self { db }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableInner<DatabaseHandle> {
|
||||||
|
async fn get<'a, K: Key, V: Serialize + DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
table: TableDefinition<'static, K, Vec<u8>>,
|
||||||
|
key: impl Borrow<K::SelfType<'a>>,
|
||||||
|
) -> Result<Option<V>> {
|
||||||
|
let db: &redb::Database = &self.db.as_ref().database;
|
||||||
|
let db_reader = db.begin_read()?;
|
||||||
|
let table = db_reader.open_table(table)?;
|
||||||
|
table
|
||||||
|
.get(key)?
|
||||||
|
.map(|value| bson::deserialize_from_slice(&value.value()))
|
||||||
|
.transpose()
|
||||||
|
.map_err(|e| redb::Error::Io(std::io::Error::other(e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert<
|
||||||
|
'a,
|
||||||
|
'b,
|
||||||
|
K: Key + Send + Sync,
|
||||||
|
V: Serialize + DeserializeOwned + Send + Sync + 'a,
|
||||||
|
>(
|
||||||
|
&'b self,
|
||||||
|
table: TableDefinition<'static, K, Vec<u8>>,
|
||||||
|
key: impl Borrow<K::SelfType<'a>> + Send + 'b,
|
||||||
|
value: V,
|
||||||
|
) -> Result<Option<V>> {
|
||||||
|
let db: &redb::Database = &self.db.as_ref().database;
|
||||||
|
// self.db
|
||||||
|
// .writing
|
||||||
|
// .store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
|
||||||
|
// let out = tokio::task::spawn_blocking(move || -> Result<Option<V>>
|
||||||
|
|
||||||
|
let out = tokio::task::spawn_blocking(|| -> Result<Option<V>> {
|
||||||
|
let db_writer = db.begin_write()?;
|
||||||
|
let out = {
|
||||||
|
let mut table = db_writer.open_table(table)?;
|
||||||
|
let serialized_value = bson::serialize_to_vec(&value)
|
||||||
|
.map_err(|e| redb::Error::Io(std::io::Error::other(e)))?;
|
||||||
|
let previous = table.insert(key, &serialized_value)?;
|
||||||
|
let out = previous
|
||||||
|
.map(|value| bson::deserialize_from_slice(&value.value()))
|
||||||
|
.transpose()
|
||||||
|
.map_err(|e| redb::Error::Io(std::io::Error::other(e)));
|
||||||
|
out
|
||||||
|
};
|
||||||
|
db_writer.commit()?;
|
||||||
|
out
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Task panicked");
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl<K: Key, V: Serialize + DeserializeOwned> Table<K, V> for TableInner {
|
||||||
|
// async fn get(&self, key: K) -> Result<Option<Value>> {}
|
||||||
|
// async fn insert(&self, key: K, value: V) -> Result<Option<Value>> {}
|
||||||
|
// async fn modify(&self, key: K, v: FnOnce(V) -> V) -> Result<bool> {}
|
||||||
|
// async fn remove(&self, key: K) -> Result<Option<Value>> {}
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Users<T>(TableInner<T>);
|
||||||
|
|
||||||
|
impl<T> Clone for Users<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self(self.0.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> Users<T> {
|
||||||
|
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = USERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Servers<T>(TableInner<T>);
|
||||||
|
impl<T> Clone for Servers<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self(self.0.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> Servers<T> {
|
||||||
|
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = SERVERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Settings<T>(TableInner<T>);
|
||||||
|
impl<T> Clone for Settings<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self(self.0.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> Settings<T> {
|
||||||
|
const TABLE: TableDefinition<'static, uuid::Uuid, Vec<u8>> = SETTINGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Database {
|
||||||
|
users: Users<DatabaseHandle>,
|
||||||
|
servers: Servers<DatabaseHandle>,
|
||||||
|
settings: Settings<DatabaseHandle>,
|
||||||
|
handle: Arc<DatabaseHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DatabaseHandle {
|
||||||
|
database: redb::Database,
|
||||||
|
writing: AtomicBool,
|
||||||
|
wakers: RwLock<VecDeque<AtomicWaker>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DatabaseWriterGuard<'a> {
|
||||||
|
handle: &'a DatabaseHandle,
|
||||||
|
dropper: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl Drop for DatabaseWriterGuard<'_> {
|
||||||
|
// fn drop(&mut self) {
|
||||||
|
// self.handle
|
||||||
|
// .writing
|
||||||
|
// .store(false, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
// let is_panicking = std::thread::panicking();
|
||||||
|
// let Ok(writer) = self.handle.wakers.write() else {
|
||||||
|
// if is_panicking {
|
||||||
|
// return;
|
||||||
|
// } else {
|
||||||
|
// panic!("Wakers lock poisoned");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if let Some(waker) = (self.handle.wakers.write()).pop() {
|
||||||
|
// waker.wake();
|
||||||
|
// };
|
||||||
|
// // let mut wakers = self.handle.wakers.write().expect();
|
||||||
|
// // if let Some(waker) = self.handle.wakers.write().expect("Wakers lock poisoned").pop_front() {
|
||||||
|
// // waker.wake();
|
||||||
|
// // }
|
||||||
|
// // while let Some(waker) = wakers.pop_front() {
|
||||||
|
// // waker.wake();
|
||||||
|
// // }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
type Result<O, E = redb::Error> = core::result::Result<O, E>;
|
||||||
|
|
||||||
|
pub trait Table<K: Key> {
|
||||||
|
fn insert<V: Serialize + DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
key: K,
|
||||||
|
value: V,
|
||||||
|
) -> impl Future<Output = Result<Option<V>>> + Send;
|
||||||
|
fn modify<V: Serialize + DeserializeOwned, O: Serialize + DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
key: K,
|
||||||
|
v: impl FnOnce(V) -> O,
|
||||||
|
) -> impl Future<Output = Result<bool>> + Send;
|
||||||
|
fn remove<V: Serialize + DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
key: K,
|
||||||
|
) -> impl Future<Output = Result<Option<V>>> + Send;
|
||||||
|
fn get<V: Serialize + DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
key: K,
|
||||||
|
) -> impl Future<Output = Result<Option<V>>> + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
pub fn create(path: impl AsRef<Path>) -> Result<Self, Error> {
|
||||||
|
let writing = AtomicBool::new(false);
|
||||||
|
let wakers = RwLock::new(VecDeque::new());
|
||||||
|
let db = redb::Database::create(path)?;
|
||||||
|
let db = Arc::new(DatabaseHandle {
|
||||||
|
database: db,
|
||||||
|
writing,
|
||||||
|
wakers,
|
||||||
|
});
|
||||||
|
let table_inner = TableInner::new(Arc::clone(&db));
|
||||||
|
let users = Users(table_inner.clone());
|
||||||
|
let servers = Servers(table_inner.clone());
|
||||||
|
let settings = Settings(table_inner.clone());
|
||||||
|
Ok(Self {
|
||||||
|
servers,
|
||||||
|
users,
|
||||||
|
settings,
|
||||||
|
handle: db,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
0
store/src/sqlite.rs
Normal file
0
store/src/sqlite.rs
Normal file
0
store/src/toml.rs
Normal file
0
store/src/toml.rs
Normal file
@@ -1,4 +1,6 @@
|
|||||||
// mod settings;
|
mod settings;
|
||||||
|
mod video;
|
||||||
|
|
||||||
mod shared_string;
|
mod shared_string;
|
||||||
use iced_video_player::{Video, VideoPlayer};
|
use iced_video_player::{Video, VideoPlayer};
|
||||||
use shared_string::SharedString;
|
use shared_string::SharedString;
|
||||||
@@ -9,9 +11,9 @@ mod blur_hash;
|
|||||||
use blur_hash::BlurHash;
|
use blur_hash::BlurHash;
|
||||||
|
|
||||||
mod preview;
|
mod preview;
|
||||||
use preview::Preview;
|
// use preview::Preview;
|
||||||
|
|
||||||
use iced::{Alignment, Element, Length, Shadow, Task, widget::*};
|
use iced::{Alignment, Element, Length, Task, widget::*};
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -104,37 +106,57 @@ pub enum Screen {
|
|||||||
User,
|
User,
|
||||||
Video,
|
Video,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub server_url: Option<String>,
|
||||||
|
pub device_id: Option<String>,
|
||||||
|
pub device_name: Option<String>,
|
||||||
|
pub client_name: Option<String>,
|
||||||
|
pub version: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Config {
|
||||||
|
server_url: Some("http://localhost:8096".to_string()),
|
||||||
|
device_id: Some("jello-iced".to_string()),
|
||||||
|
device_name: Some("Jello Iced".to_string()),
|
||||||
|
client_name: Some("Jello".to_string()),
|
||||||
|
version: Some("0.1.0".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct State {
|
struct State {
|
||||||
loading: Option<Loading>,
|
loading: Option<Loading>,
|
||||||
current: Option<uuid::Uuid>,
|
current: Option<uuid::Uuid>,
|
||||||
cache: ItemCache,
|
cache: ItemCache,
|
||||||
jellyfin_client: api::JellyfinClient,
|
jellyfin_client: Option<api::JellyfinClient>,
|
||||||
messages: Vec<String>,
|
messages: Vec<String>,
|
||||||
history: Vec<Option<uuid::Uuid>>,
|
history: Vec<Option<uuid::Uuid>>,
|
||||||
query: Option<String>,
|
query: Option<String>,
|
||||||
screen: Screen,
|
screen: Screen,
|
||||||
// Login form state
|
settings: settings::SettingsState,
|
||||||
username_input: String,
|
|
||||||
password_input: String,
|
|
||||||
is_authenticated: bool,
|
is_authenticated: bool,
|
||||||
// Video
|
|
||||||
video: Option<Arc<Video>>,
|
video: Option<Arc<Video>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn new(jellyfin_client: api::JellyfinClient) -> Self {
|
pub fn new() -> Self {
|
||||||
State {
|
State {
|
||||||
loading: None,
|
loading: None,
|
||||||
current: None,
|
current: None,
|
||||||
cache: ItemCache::default(),
|
cache: ItemCache::default(),
|
||||||
jellyfin_client,
|
jellyfin_client: None,
|
||||||
messages: Vec::new(),
|
messages: Vec::new(),
|
||||||
history: Vec::new(),
|
history: Vec::new(),
|
||||||
query: None,
|
query: None,
|
||||||
screen: Screen::Home,
|
screen: Screen::Home,
|
||||||
username_input: String::new(),
|
settings: settings::SettingsState::default(),
|
||||||
password_input: String::new(),
|
// username_input: String::new(),
|
||||||
|
// password_input: String::new(),
|
||||||
is_authenticated: false,
|
is_authenticated: false,
|
||||||
video: None,
|
video: None,
|
||||||
}
|
}
|
||||||
@@ -143,8 +165,7 @@ impl State {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
OpenSettings,
|
Settings(settings::SettingsMessage),
|
||||||
CloseSettings,
|
|
||||||
Refresh,
|
Refresh,
|
||||||
Search,
|
Search,
|
||||||
SearchQueryChanged(String),
|
SearchQueryChanged(String),
|
||||||
@@ -154,95 +175,22 @@ pub enum Message {
|
|||||||
SetToken(String),
|
SetToken(String),
|
||||||
Back,
|
Back,
|
||||||
Home,
|
Home,
|
||||||
// Login-related messages
|
// Login {
|
||||||
UsernameChanged(String),
|
// username: String,
|
||||||
PasswordChanged(String),
|
// password: String,
|
||||||
Login,
|
// config: api::JellyfinConfig,
|
||||||
LoginSuccess(String),
|
// },
|
||||||
LoadedClient(api::JellyfinClient, bool),
|
// LoginSuccess(String),
|
||||||
Logout,
|
// LoadedClient(api::JellyfinClient, bool),
|
||||||
Video(VideoMessage),
|
// Logout,
|
||||||
}
|
Video(video::VideoMessage),
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum VideoMessage {
|
|
||||||
EndOfStream,
|
|
||||||
Open(url::Url),
|
|
||||||
Pause,
|
|
||||||
Play,
|
|
||||||
Seek(f64),
|
|
||||||
Stop,
|
|
||||||
Test,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(state: &mut State, message: Message) -> Task<Message> {
|
fn update(state: &mut State, message: Message) -> Task<Message> {
|
||||||
|
// if let Some(client) = state.jellyfin_client.clone() {
|
||||||
match message {
|
match message {
|
||||||
Message::OpenSettings => {
|
Message::Settings(msg) => settings::update(&mut state.settings, msg),
|
||||||
state.screen = Screen::Settings;
|
Message::OpenItem(id) if let Some(client) = state.jellyfin_client.clone() => {
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
Message::CloseSettings => {
|
|
||||||
state.screen = Screen::Home;
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
Message::UsernameChanged(username) => {
|
|
||||||
state.username_input = username;
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
Message::PasswordChanged(password) => {
|
|
||||||
state.password_input = password;
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
Message::Login => {
|
|
||||||
let username = state.username_input.clone();
|
|
||||||
let password = state.password_input.clone();
|
|
||||||
let config = (*state.jellyfin_client.config).clone();
|
|
||||||
|
|
||||||
Task::perform(
|
|
||||||
async move { api::JellyfinClient::authenticate(username, password, config).await },
|
|
||||||
|result| match result {
|
|
||||||
Ok(client) => Message::LoadedClient(client, true),
|
|
||||||
Err(e) => Message::Error(format!("Login failed: {}", e)),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Message::LoginSuccess(token) => {
|
|
||||||
state.jellyfin_client.set_token(token.clone());
|
|
||||||
state.is_authenticated = true;
|
|
||||||
state.password_input.clear();
|
|
||||||
state.messages.push("Login successful!".to_string());
|
|
||||||
state.screen = Screen::Home;
|
|
||||||
|
|
||||||
// Save token and refresh items
|
|
||||||
let client = state.jellyfin_client.clone();
|
|
||||||
Task::perform(
|
|
||||||
async move {
|
|
||||||
let _ = client.save_token(".session").await;
|
|
||||||
},
|
|
||||||
|_| Message::Refresh,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Message::LoadedClient(client, is_authenticated) => {
|
|
||||||
state.jellyfin_client = client;
|
|
||||||
state.is_authenticated = is_authenticated;
|
|
||||||
if is_authenticated {
|
|
||||||
Task::done(Message::Refresh)
|
|
||||||
} else {
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Message::Logout => {
|
|
||||||
state.is_authenticated = false;
|
|
||||||
state.jellyfin_client.set_token("");
|
|
||||||
state.cache = ItemCache::default();
|
|
||||||
state.current = None;
|
|
||||||
state.username_input.clear();
|
|
||||||
state.password_input.clear();
|
|
||||||
state.messages.push("Logged out successfully".to_string());
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
Message::OpenItem(id) => {
|
|
||||||
let client = state.jellyfin_client.clone();
|
|
||||||
use api::jellyfin::BaseItemKind::*;
|
use api::jellyfin::BaseItemKind::*;
|
||||||
if let Some(cached) = id.as_ref().and_then(|id| state.cache.get(id))
|
if let Some(cached) = id.as_ref().and_then(|id| state.cache.get(id))
|
||||||
&& matches!(cached._type, Video | Movie | Episode)
|
&& matches!(cached._type, Video | Movie | Episode)
|
||||||
@@ -250,7 +198,7 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
|
|||||||
let url = client
|
let url = client
|
||||||
.stream_url(id.expect("ID exists"))
|
.stream_url(id.expect("ID exists"))
|
||||||
.expect("Failed to get stream URL");
|
.expect("Failed to get stream URL");
|
||||||
Task::done(Message::Video(VideoMessage::Open(url)))
|
Task::done(Message::Video(video::VideoMessage::Open(url)))
|
||||||
} else {
|
} else {
|
||||||
Task::perform(
|
Task::perform(
|
||||||
async move {
|
async move {
|
||||||
@@ -273,9 +221,9 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
|
|||||||
state.current = id;
|
state.current = id;
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::Refresh => {
|
Message::Refresh if let Some(client) = state.jellyfin_client.clone() => {
|
||||||
// Handle refresh logic
|
// Handle refresh logic
|
||||||
let client = state.jellyfin_client.clone();
|
// let client = state.jellyfin_client.clone();
|
||||||
let current = state.current;
|
let current = state.current;
|
||||||
Task::perform(
|
Task::perform(
|
||||||
async move {
|
async move {
|
||||||
@@ -298,7 +246,10 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
|
|||||||
}
|
}
|
||||||
Message::SetToken(token) => {
|
Message::SetToken(token) => {
|
||||||
tracing::info!("Authenticated with token: {}", token);
|
tracing::info!("Authenticated with token: {}", token);
|
||||||
state.jellyfin_client.set_token(token);
|
state
|
||||||
|
.jellyfin_client
|
||||||
|
.as_mut()
|
||||||
|
.map(|mut client| client.set_token(token));
|
||||||
state.is_authenticated = true;
|
state.is_authenticated = true;
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
@@ -315,9 +266,9 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
|
|||||||
// Handle search query change
|
// Handle search query change
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::Search => {
|
Message::Search if let Some(client) = state.jellyfin_client.clone() => {
|
||||||
// Handle search action
|
// Handle search action
|
||||||
let client = state.jellyfin_client.clone();
|
// let client = state.jellyfin_client.clone();
|
||||||
let query = state.query.clone().unwrap_or_default();
|
let query = state.query.clone().unwrap_or_default();
|
||||||
Task::perform(async move { client.search(query).await }, |r| match r {
|
Task::perform(async move { client.search(query).await }, |r| match r {
|
||||||
Err(e) => Message::Error(format!("Search failed: {}", e)),
|
Err(e) => Message::Error(format!("Search failed: {}", e)),
|
||||||
@@ -327,63 +278,14 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Message::Video(msg) => match msg {
|
Message::Video(msg) => video::update(state, msg),
|
||||||
VideoMessage::EndOfStream => {
|
_ => todo!(),
|
||||||
state.video = None;
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
VideoMessage::Open(url) => {
|
|
||||||
state.video = Video::new(&url)
|
|
||||||
.inspect_err(|err| {
|
|
||||||
tracing::error!("Failed to play video at {}: {:?}", url, err);
|
|
||||||
})
|
|
||||||
.ok()
|
|
||||||
.map(Arc::new);
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
VideoMessage::Pause => {
|
|
||||||
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) {
|
|
||||||
video.set_paused(true);
|
|
||||||
}
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
VideoMessage::Play => {
|
|
||||||
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) {
|
|
||||||
video.set_paused(false);
|
|
||||||
}
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
VideoMessage::Seek(position) => {
|
|
||||||
// if let Some(ref video) = state.video {
|
|
||||||
// // video.seek(position, true);
|
|
||||||
// }
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
VideoMessage::Stop => {
|
|
||||||
state.video = None;
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
VideoMessage::Test => {
|
|
||||||
let url = url::Url::parse(
|
|
||||||
// "file:///home/servius/Projects/jello/crates/iced_video_player/.media/test.mp4",
|
|
||||||
"https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
state.video = Video::new(&url)
|
|
||||||
.inspect_err(|err| {
|
|
||||||
tracing::error!("{err:?}");
|
|
||||||
})
|
|
||||||
.ok()
|
|
||||||
.map(Arc::new);
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(state: &State) -> Element<'_, Message> {
|
fn view(state: &State) -> Element<'_, Message> {
|
||||||
match state.screen {
|
match state.screen {
|
||||||
// Screen::Settings => settings::settings(state),
|
Screen::Settings => settings::settings(state),
|
||||||
Screen::Home | _ => home(state),
|
Screen::Home | _ => home(state),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -395,25 +297,9 @@ fn home(state: &State) -> Element<'_, Message> {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn player(video: &Video) -> Element<'_, Message> {
|
|
||||||
container(
|
|
||||||
VideoPlayer::new(video)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.height(Length::Fill)
|
|
||||||
.content_fit(iced::ContentFit::Contain)
|
|
||||||
.on_end_of_stream(Message::Video(VideoMessage::EndOfStream)),
|
|
||||||
)
|
|
||||||
.style(|_| container::background(iced::Color::BLACK))
|
|
||||||
.width(Length::Fill)
|
|
||||||
.height(Length::Fill)
|
|
||||||
.align_x(Alignment::Center)
|
|
||||||
.align_y(Alignment::Center)
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn body(state: &State) -> Element<'_, Message> {
|
fn body(state: &State) -> Element<'_, Message> {
|
||||||
if let Some(ref video) = state.video {
|
if let Some(ref video) = state.video {
|
||||||
player(video)
|
video::player(video)
|
||||||
} else {
|
} else {
|
||||||
scrollable(
|
scrollable(
|
||||||
container(
|
container(
|
||||||
@@ -436,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)),
|
||||||
|
|||||||
@@ -1,8 +1,26 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
|
use iced::Element;
|
||||||
|
|
||||||
pub fn settings(state: &State) -> Element<'_, Message> {}
|
pub fn settings(state: &State) -> Element<'_, Message> {
|
||||||
|
empty()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
pub fn update(_state: &mut SettingsState, message: SettingsMessage) -> Task<Message> {
|
||||||
|
match message {
|
||||||
|
SettingsMessage::Open => {}
|
||||||
|
SettingsMessage::Close => {}
|
||||||
|
SettingsMessage::Select(screen) => {
|
||||||
|
tracing::trace!("Switching settings screen to {:?}", screen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn empty() -> Element<'static, Message> {
|
||||||
|
column([]).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct SettingsState {
|
pub struct SettingsState {
|
||||||
login_form: LoginForm,
|
login_form: LoginForm,
|
||||||
server_form: ServerForm,
|
server_form: ServerForm,
|
||||||
@@ -16,8 +34,9 @@ pub enum SettingsMessage {
|
|||||||
Select(SettingsScreen),
|
Select(SettingsScreen),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub enum SettingsScreen {
|
pub enum SettingsScreen {
|
||||||
|
#[default]
|
||||||
Main,
|
Main,
|
||||||
Users,
|
Users,
|
||||||
Servers,
|
Servers,
|
||||||
@@ -37,20 +56,27 @@ pub struct UserItem {
|
|||||||
pub name: SharedString,
|
pub name: SharedString,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct LoginForm {
|
pub struct LoginForm {
|
||||||
username: String,
|
username: Option<String>,
|
||||||
password: String,
|
password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct ServerForm {
|
pub struct ServerForm {
|
||||||
name: String,
|
name: Option<String>,
|
||||||
url: String,
|
url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
mod screens {
|
mod screens {
|
||||||
pub fn main(state: &State) -> Element<'_, Message> {}
|
use super::*;
|
||||||
pub fn server(state: &State) -> Element<'_, Message> {}
|
pub fn main(state: &State) -> Element<'_, Message> {
|
||||||
pub fn user(state: &State) -> Element<'_, Message> {}
|
empty()
|
||||||
|
}
|
||||||
|
pub fn server(state: &State) -> Element<'_, Message> {
|
||||||
|
empty()
|
||||||
|
}
|
||||||
|
pub fn user(state: &State) -> Element<'_, Message> {
|
||||||
|
empty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
85
ui-iced/src/video.rs
Normal file
85
ui-iced/src/video.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
use super::*;
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum VideoMessage {
|
||||||
|
EndOfStream,
|
||||||
|
Open(url::Url),
|
||||||
|
Pause,
|
||||||
|
Play,
|
||||||
|
Seek(f64),
|
||||||
|
Stop,
|
||||||
|
Test,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(state: &mut State, message: VideoMessage) -> Task<Message> {
|
||||||
|
match message {
|
||||||
|
VideoMessage::EndOfStream => {
|
||||||
|
state.video = None;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
VideoMessage::Open(url) => {
|
||||||
|
state.video = Video::new(&url)
|
||||||
|
.inspect_err(|err| {
|
||||||
|
tracing::error!("Failed to play video at {}: {:?}", url, err);
|
||||||
|
})
|
||||||
|
.inspect(|video| {
|
||||||
|
tracing::info!("Framerate {}", video.framerate());
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
.map(Arc::new);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
VideoMessage::Pause => {
|
||||||
|
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) {
|
||||||
|
video.set_paused(true);
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
VideoMessage::Play => {
|
||||||
|
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) {
|
||||||
|
video.set_paused(false);
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
VideoMessage::Seek(position) => {
|
||||||
|
// if let Some(ref video) = state.video {
|
||||||
|
// // video.seek(position, true);
|
||||||
|
// }
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
VideoMessage::Stop => {
|
||||||
|
state.video = None;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
VideoMessage::Test => {
|
||||||
|
let url = url::Url::parse(
|
||||||
|
// "file:///home/servius/Projects/jello/crates/iced_video_player/.media/test.mp4",
|
||||||
|
"https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm",
|
||||||
|
// "https://www.youtube.com/watch?v=QbUUaXGA3C4",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
state.video = Video::new(&url)
|
||||||
|
.inspect_err(|err| {
|
||||||
|
tracing::error!("{err:?}");
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
.map(Arc::new);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn player(video: &Video) -> Element<'_, Message> {
|
||||||
|
container(
|
||||||
|
VideoPlayer::new(video)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.content_fit(iced::ContentFit::Contain)
|
||||||
|
.on_end_of_stream(Message::Video(VideoMessage::EndOfStream)),
|
||||||
|
)
|
||||||
|
.style(|_| container::background(iced::Color::BLACK))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.align_x(Alignment::Center)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user