feat: Update to latest iced

This commit is contained in:
uttarayan21
2025-11-18 23:54:27 +05:30
parent a6ef6ba9c0
commit 3222c26bb6
15 changed files with 1231 additions and 4918 deletions

1435
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,14 +9,10 @@ license = "MIT"
[dependencies]
api = { version = "0.1.0", path = "api" }
blurhash = "0.2.3"
clap = { version = "4.5", features = ["derive"] }
clap_complete = "4.5"
dotenvy = "0.15.7"
error-stack = "0.6"
gpui = { version = "0.2.2", default-features = false, features = ["wayland"] }
image = "0.25.9"
tap = "1.0.1"
thiserror = "2.0"
tokio = { version = "1.43.1", features = ["macros", "rt-multi-thread"] }
tracing = "0.1"

View File

@@ -13,6 +13,7 @@ tap = "1.0.1"
thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["fs"] }
toml = "0.9.8"
tracing = "0.1.41"
uuid = { version = "1.18.1", features = ["serde"] }
[dev-dependencies]

View File

@@ -2,15 +2,23 @@ use api::*;
#[tokio::main]
pub async fn main() {
let config = std::fs::read_to_string("config.toml").expect("Config.toml");
let config: JellyfinConfig =
toml::from_str(&config).expect("Failed to parse config.toml");
let config: JellyfinConfig = toml::from_str(&config).expect("Failed to parse config.toml");
let mut jellyfin = JellyfinClient::new(config);
jellyfin.authenticate_with_cached_token(".session").await.expect("Auth");
let items = jellyfin.raw_items().await.expect("Items");
jellyfin
.authenticate_with_cached_token(".session")
.await
.expect("Auth");
let items = jellyfin.items(None).await.expect("Items");
std::fs::write(
"items.json",
serde_json::to_string_pretty(&items).expect("Serialize items"),
);
for item in items {
println!("{}: {:?}", item.id, item.name);
let items = jellyfin.items(item.id).await.expect("Items");
for item in items {
println!(" {}: {:?}", item.id, item.name);
}
}
}

View File

@@ -40,7 +40,7 @@ pub struct ActivityLogEntry {
pub item_id: Option<String>,
/// Gets or sets the date.
#[serde(rename = "Date")]
pub date: jiff::Zoned,
pub date: jiff::Timestamp,
/// Gets or sets the user identifier.
#[serde(rename = "UserId")]
pub user_id: uuid::Uuid,
@@ -130,7 +130,7 @@ pub struct AlbumInfo {
#[serde(rename = "ParentIndexNumber")]
pub parent_index_number: Option<i32>,
#[serde(rename = "PremiereDate")]
pub premiere_date: Option<jiff::Zoned>,
pub premiere_date: Option<jiff::Timestamp>,
#[serde(rename = "IsAutomated")]
pub is_automated: bool,
/// Gets or sets the album artist.
@@ -195,7 +195,7 @@ pub struct ArtistInfo {
#[serde(rename = "ParentIndexNumber")]
pub parent_index_number: Option<i32>,
#[serde(rename = "PremiereDate")]
pub premiere_date: Option<jiff::Zoned>,
pub premiere_date: Option<jiff::Timestamp>,
#[serde(rename = "IsAutomated")]
pub is_automated: bool,
#[serde(rename = "SongInfos")]
@@ -252,12 +252,12 @@ pub struct AuthenticationInfo {
pub is_active: bool,
/// Gets or sets the date created.
#[serde(rename = "DateCreated")]
pub date_created: jiff::Zoned,
pub date_created: jiff::Timestamp,
/// Gets or sets the date revoked.
#[serde(rename = "DateRevoked")]
pub date_revoked: Option<jiff::Zoned>,
pub date_revoked: Option<jiff::Timestamp>,
#[serde(rename = "DateLastActivity")]
pub date_last_activity: jiff::Zoned,
pub date_last_activity: jiff::Timestamp,
#[serde(rename = "UserName")]
pub user_name: Option<String>,
}
@@ -316,9 +316,9 @@ pub struct BaseItemDto {
pub playlist_item_id: Option<String>,
/// Gets or sets the date created.
#[serde(rename = "DateCreated")]
pub date_created: Option<jiff::Zoned>,
pub date_created: Option<jiff::Timestamp>,
#[serde(rename = "DateLastMediaAdded")]
pub date_last_media_added: Option<jiff::Zoned>,
pub date_last_media_added: Option<jiff::Timestamp>,
#[serde(rename = "ExtraType")]
pub extra_type: Option<ExtraType>,
#[serde(rename = "AirsBeforeSeasonNumber")]
@@ -351,7 +351,7 @@ pub struct BaseItemDto {
pub video3_d_format: Option<Video3DFormat>,
/// Gets or sets the premiere date.
#[serde(rename = "PremiereDate")]
pub premiere_date: Option<jiff::Zoned>,
pub premiere_date: Option<jiff::Timestamp>,
/// Gets or sets the external urls.
#[serde(rename = "ExternalUrls")]
pub external_urls: Option<Vec<ExternalUrl>>,
@@ -568,7 +568,7 @@ pub struct BaseItemDto {
/** Gets or sets the blurhashes for the image tags.
Maps image type to dictionary mapping image tag to blurhash value.*/
#[serde(rename = "ImageBlurHashes")]
pub image_blur_hashes: Option<std::collections::HashMap<String, serde_json::Value>>,
pub image_blur_hashes: BaseItemDtoImageBlurHashes,
/// Gets or sets the series studio.
#[serde(rename = "SeriesStudio")]
pub series_studio: Option<String>,
@@ -606,7 +606,7 @@ Maps image type to dictionary mapping image tag to blurhash value.*/
pub media_type: MediaType,
/// Gets or sets the end date.
#[serde(rename = "EndDate")]
pub end_date: Option<jiff::Zoned>,
pub end_date: Option<jiff::Timestamp>,
/// Gets or sets the locked fields.
#[serde(rename = "LockedFields")]
pub locked_fields: Option<Vec<MetadataField>>,
@@ -677,7 +677,7 @@ Maps image type to dictionary mapping image tag to blurhash value.*/
pub channel_primary_image_tag: Option<String>,
/// Gets or sets the start date of the recording, in UTC.
#[serde(rename = "StartDate")]
pub start_date: Option<jiff::Zoned>,
pub start_date: Option<jiff::Timestamp>,
/// Gets or sets the completion percentage.
#[serde(rename = "CompletionPercentage")]
pub completion_percentage: Option<f64>,
@@ -724,6 +724,37 @@ Maps image type to dictionary mapping image tag to blurhash value.*/
#[serde(rename = "CurrentProgram")]
pub current_program: Option<Box<BaseItemDto>>,
}
/** Gets or sets the blurhashes for the image tags.
Maps image type to dictionary mapping image tag to blurhash value.*/
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BaseItemDtoImageBlurHashes {
#[serde(rename = "Primary")]
pub primary: std::collections::HashMap<String, String>,
#[serde(rename = "Art")]
pub art: std::collections::HashMap<String, String>,
#[serde(rename = "Backdrop")]
pub backdrop: std::collections::HashMap<String, String>,
#[serde(rename = "Banner")]
pub banner: std::collections::HashMap<String, String>,
#[serde(rename = "Logo")]
pub logo: std::collections::HashMap<String, String>,
#[serde(rename = "Thumb")]
pub thumb: std::collections::HashMap<String, String>,
#[serde(rename = "Disc")]
pub disc: std::collections::HashMap<String, String>,
#[serde(rename = "Box")]
pub _box: std::collections::HashMap<String, String>,
#[serde(rename = "Screenshot")]
pub screenshot: std::collections::HashMap<String, String>,
#[serde(rename = "Menu")]
pub menu: std::collections::HashMap<String, String>,
#[serde(rename = "Chapter")]
pub chapter: std::collections::HashMap<String, String>,
#[serde(rename = "BoxRear")]
pub box_rear: std::collections::HashMap<String, String>,
#[serde(rename = "Profile")]
pub profile: std::collections::HashMap<String, String>,
}
/// Query result container.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BaseItemDtoQueryResult {
@@ -757,7 +788,37 @@ pub struct BaseItemPerson {
pub primary_image_tag: Option<String>,
/// Gets or sets the primary image blurhash.
#[serde(rename = "ImageBlurHashes")]
pub image_blur_hashes: Option<std::collections::HashMap<String, serde_json::Value>>,
pub image_blur_hashes: BaseItemPersonImageBlurHashes,
}
/// Gets or sets the primary image blurhash.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BaseItemPersonImageBlurHashes {
#[serde(rename = "Primary")]
pub primary: std::collections::HashMap<String, String>,
#[serde(rename = "Art")]
pub art: std::collections::HashMap<String, String>,
#[serde(rename = "Backdrop")]
pub backdrop: std::collections::HashMap<String, String>,
#[serde(rename = "Banner")]
pub banner: std::collections::HashMap<String, String>,
#[serde(rename = "Logo")]
pub logo: std::collections::HashMap<String, String>,
#[serde(rename = "Thumb")]
pub thumb: std::collections::HashMap<String, String>,
#[serde(rename = "Disc")]
pub disc: std::collections::HashMap<String, String>,
#[serde(rename = "Box")]
pub _box: std::collections::HashMap<String, String>,
#[serde(rename = "Screenshot")]
pub screenshot: std::collections::HashMap<String, String>,
#[serde(rename = "Menu")]
pub menu: std::collections::HashMap<String, String>,
#[serde(rename = "Chapter")]
pub chapter: std::collections::HashMap<String, String>,
#[serde(rename = "BoxRear")]
pub box_rear: std::collections::HashMap<String, String>,
#[serde(rename = "Profile")]
pub profile: std::collections::HashMap<String, String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BookInfo {
@@ -787,7 +848,7 @@ pub struct BookInfo {
#[serde(rename = "ParentIndexNumber")]
pub parent_index_number: Option<i32>,
#[serde(rename = "PremiereDate")]
pub premiere_date: Option<jiff::Zoned>,
pub premiere_date: Option<jiff::Timestamp>,
#[serde(rename = "IsAutomated")]
pub is_automated: bool,
#[serde(rename = "SeriesName")]
@@ -834,7 +895,7 @@ pub struct BoxSetInfo {
#[serde(rename = "ParentIndexNumber")]
pub parent_index_number: Option<i32>,
#[serde(rename = "PremiereDate")]
pub premiere_date: Option<jiff::Zoned>,
pub premiere_date: Option<jiff::Timestamp>,
#[serde(rename = "IsAutomated")]
pub is_automated: bool,
}
@@ -869,7 +930,7 @@ pub struct BrandingOptions {
pub struct BufferRequestDto {
/// Gets or sets when the request has been made by the client.
#[serde(rename = "When")]
pub when: jiff::Zoned,
pub when: jiff::Timestamp,
/// Gets or sets the position ticks.
#[serde(rename = "PositionTicks")]
pub position_ticks: i64,
@@ -958,7 +1019,7 @@ pub struct ChapterInfo {
#[serde(rename = "ImagePath")]
pub image_path: Option<String>,
#[serde(rename = "ImageDateModified")]
pub image_date_modified: jiff::Zoned,
pub image_date_modified: jiff::Timestamp,
#[serde(rename = "ImageTag")]
pub image_tag: Option<String>,
}
@@ -1183,7 +1244,7 @@ pub struct DeviceInfoDto {
pub last_user_id: Option<uuid::Uuid>,
/// Gets or sets the date last modified.
#[serde(rename = "DateLastActivity")]
pub date_last_activity: Option<jiff::Zoned>,
pub date_last_activity: Option<jiff::Timestamp>,
/// Gets or sets the capabilities.
#[serde(rename = "Capabilities")]
pub capabilities: ClientCapabilitiesDto,
@@ -1527,10 +1588,10 @@ pub struct FontFile {
pub size: i64,
/// Gets or sets the date created.
#[serde(rename = "DateCreated")]
pub date_created: jiff::Zoned,
pub date_created: jiff::Timestamp,
/// Gets or sets the date modified.
#[serde(rename = "DateModified")]
pub date_modified: jiff::Zoned,
pub date_modified: jiff::Timestamp,
}
/// Force keep alive websocket messages.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@@ -1569,7 +1630,7 @@ pub struct ForgotPasswordResult {
pub pin_file: Option<String>,
/// Gets or sets the pin expiration date.
#[serde(rename = "PinExpirationDate")]
pub pin_expiration_date: Option<jiff::Zoned>,
pub pin_expiration_date: Option<jiff::Timestamp>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct GeneralCommand {
@@ -1605,7 +1666,7 @@ pub struct GetProgramsDto {
pub user_id: Option<uuid::Uuid>,
/// Gets or sets the minimum premiere start date.
#[serde(rename = "MinStartDate")]
pub min_start_date: Option<jiff::Zoned>,
pub min_start_date: Option<jiff::Timestamp>,
/// Gets or sets filter by programs that have completed airing, or not.
#[serde(rename = "HasAired")]
pub has_aired: Option<bool>,
@@ -1614,13 +1675,13 @@ pub struct GetProgramsDto {
pub is_airing: Option<bool>,
/// Gets or sets the maximum premiere start date.
#[serde(rename = "MaxStartDate")]
pub max_start_date: Option<jiff::Zoned>,
pub max_start_date: Option<jiff::Timestamp>,
/// Gets or sets the minimum premiere end date.
#[serde(rename = "MinEndDate")]
pub min_end_date: Option<jiff::Zoned>,
pub min_end_date: Option<jiff::Timestamp>,
/// Gets or sets the maximum premiere end date.
#[serde(rename = "MaxEndDate")]
pub max_end_date: Option<jiff::Zoned>,
pub max_end_date: Option<jiff::Timestamp>,
/// Gets or sets filter for movies.
#[serde(rename = "IsMovie")]
pub is_movie: Option<bool>,
@@ -1696,7 +1757,7 @@ pub struct GroupInfoDto {
pub participants: Vec<String>,
/// Gets the date when this DTO has been created.
#[serde(rename = "LastUpdatedAt")]
pub last_updated_at: jiff::Zoned,
pub last_updated_at: jiff::Timestamp,
}
/// Class GroupUpdate.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@@ -1748,10 +1809,10 @@ pub struct GroupUpdate {
pub struct GuideInfo {
/// Gets or sets the start date.
#[serde(rename = "StartDate")]
pub start_date: jiff::Zoned,
pub start_date: jiff::Timestamp,
/// Gets or sets the end date.
#[serde(rename = "EndDate")]
pub end_date: jiff::Zoned,
pub end_date: jiff::Timestamp,
}
/// Class IgnoreWaitRequestDto.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@@ -2215,10 +2276,10 @@ pub struct LocalizationOption {
pub struct LogFile {
/// Gets or sets the date created.
#[serde(rename = "DateCreated")]
pub date_created: jiff::Zoned,
pub date_created: jiff::Timestamp,
/// Gets or sets the date modified.
#[serde(rename = "DateModified")]
pub date_modified: jiff::Zoned,
pub date_modified: jiff::Timestamp,
/// Gets or sets the size.
#[serde(rename = "Size")]
pub size: i64,
@@ -2755,7 +2816,7 @@ pub struct MovieInfo {
#[serde(rename = "ParentIndexNumber")]
pub parent_index_number: Option<i32>,
#[serde(rename = "PremiereDate")]
pub premiere_date: Option<jiff::Zoned>,
pub premiere_date: Option<jiff::Timestamp>,
#[serde(rename = "IsAutomated")]
pub is_automated: bool,
}
@@ -2800,7 +2861,7 @@ pub struct MusicVideoInfo {
#[serde(rename = "ParentIndexNumber")]
pub parent_index_number: Option<i32>,
#[serde(rename = "PremiereDate")]
pub premiere_date: Option<jiff::Zoned>,
pub premiere_date: Option<jiff::Timestamp>,
#[serde(rename = "IsAutomated")]
pub is_automated: bool,
#[serde(rename = "Artists")]
@@ -3070,7 +3131,7 @@ pub struct PersonLookupInfo {
#[serde(rename = "ParentIndexNumber")]
pub parent_index_number: Option<i32>,
#[serde(rename = "PremiereDate")]
pub premiere_date: Option<jiff::Zoned>,
pub premiere_date: Option<jiff::Timestamp>,
#[serde(rename = "IsAutomated")]
pub is_automated: bool,
}
@@ -3419,7 +3480,7 @@ pub struct PlayQueueUpdate {
pub reason: PlayQueueUpdateReason,
/// Gets the UTC time of the last change to the playing queue.
#[serde(rename = "LastUpdate")]
pub last_update: jiff::Zoned,
pub last_update: jiff::Timestamp,
/// Gets the playlist.
#[serde(rename = "Playlist")]
pub playlist: Vec<SyncPlayQueueItem>,
@@ -3729,14 +3790,14 @@ pub struct QuickConnectResult {
pub app_version: String,
/// Gets or sets the DateTime that this request was created.
#[serde(rename = "DateAdded")]
pub date_added: jiff::Zoned,
pub date_added: jiff::Timestamp,
}
/// Class ReadyRequest.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ReadyRequestDto {
/// Gets or sets when the request has been made by the client.
#[serde(rename = "When")]
pub when: jiff::Zoned,
pub when: jiff::Timestamp,
/// Gets or sets the position ticks.
#[serde(rename = "PositionTicks")]
pub position_ticks: i64,
@@ -3849,7 +3910,7 @@ pub struct RemoteSearchResult {
#[serde(rename = "ParentIndexNumber")]
pub parent_index_number: Option<i32>,
#[serde(rename = "PremiereDate")]
pub premiere_date: Option<jiff::Zoned>,
pub premiere_date: Option<jiff::Timestamp>,
#[serde(rename = "ImageUrl")]
pub image_url: Option<String>,
#[serde(rename = "SearchProviderName")]
@@ -3878,7 +3939,7 @@ pub struct RemoteSubtitleInfo {
#[serde(rename = "Comment")]
pub comment: Option<String>,
#[serde(rename = "DateCreated")]
pub date_created: Option<jiff::Zoned>,
pub date_created: Option<jiff::Timestamp>,
#[serde(rename = "CommunityRating")]
pub community_rating: Option<f32>,
#[serde(rename = "FrameRate")]
@@ -4051,10 +4112,10 @@ pub struct SearchHint {
pub media_type: MediaType,
/// Gets or sets the start date.
#[serde(rename = "StartDate")]
pub start_date: Option<jiff::Zoned>,
pub start_date: Option<jiff::Timestamp>,
/// Gets or sets the end date.
#[serde(rename = "EndDate")]
pub end_date: Option<jiff::Zoned>,
pub end_date: Option<jiff::Timestamp>,
/// Gets or sets the series.
#[serde(rename = "Series")]
pub series: Option<String>,
@@ -4117,7 +4178,7 @@ pub struct SendCommand {
pub playlist_item_id: uuid::Uuid,
/// Gets or sets the UTC time when to execute the command.
#[serde(rename = "When")]
pub when: jiff::Zoned,
pub when: jiff::Timestamp,
/// Gets the position ticks.
#[serde(rename = "PositionTicks")]
pub position_ticks: Option<i64>,
@@ -4126,7 +4187,7 @@ pub struct SendCommand {
pub command: SendCommandType,
/// Gets the UTC time when this command has been emitted.
#[serde(rename = "EmittedAt")]
pub emitted_at: jiff::Zoned,
pub emitted_at: jiff::Timestamp,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SeriesInfo {
@@ -4156,7 +4217,7 @@ pub struct SeriesInfo {
#[serde(rename = "ParentIndexNumber")]
pub parent_index_number: Option<i32>,
#[serde(rename = "PremiereDate")]
pub premiere_date: Option<jiff::Zoned>,
pub premiere_date: Option<jiff::Timestamp>,
#[serde(rename = "IsAutomated")]
pub is_automated: bool,
}
@@ -4238,10 +4299,10 @@ pub struct SeriesTimerInfoDto {
pub overview: Option<String>,
/// Gets or sets the start date of the recording, in UTC.
#[serde(rename = "StartDate")]
pub start_date: jiff::Zoned,
pub start_date: jiff::Timestamp,
/// Gets or sets the end date of the recording, in UTC.
#[serde(rename = "EndDate")]
pub end_date: jiff::Zoned,
pub end_date: jiff::Timestamp,
/// Gets or sets the name of the service.
#[serde(rename = "ServiceName")]
pub service_name: Option<String>,
@@ -4537,13 +4598,13 @@ pub struct SessionInfoDto {
pub client: Option<String>,
/// Gets or sets the last activity date.
#[serde(rename = "LastActivityDate")]
pub last_activity_date: jiff::Zoned,
pub last_activity_date: jiff::Timestamp,
/// Gets or sets the last playback check in.
#[serde(rename = "LastPlaybackCheckIn")]
pub last_playback_check_in: jiff::Zoned,
pub last_playback_check_in: jiff::Timestamp,
/// Gets or sets the last paused date.
#[serde(rename = "LastPausedDate")]
pub last_paused_date: Option<jiff::Zoned>,
pub last_paused_date: Option<jiff::Timestamp>,
/// Gets or sets the name of the device.
#[serde(rename = "DeviceName")]
pub device_name: Option<String>,
@@ -4699,7 +4760,7 @@ pub struct SongInfo {
#[serde(rename = "ParentIndexNumber")]
pub parent_index_number: Option<i32>,
#[serde(rename = "PremiereDate")]
pub premiere_date: Option<jiff::Zoned>,
pub premiere_date: Option<jiff::Timestamp>,
#[serde(rename = "IsAutomated")]
pub is_automated: bool,
#[serde(rename = "AlbumArtists")]
@@ -4992,10 +5053,10 @@ pub struct TaskInfo {
pub struct TaskResult {
/// Gets or sets the start time UTC.
#[serde(rename = "StartTimeUtc")]
pub start_time_utc: jiff::Zoned,
pub start_time_utc: jiff::Timestamp,
/// Gets or sets the end time UTC.
#[serde(rename = "EndTimeUtc")]
pub end_time_utc: jiff::Zoned,
pub end_time_utc: jiff::Timestamp,
/// Gets or sets the status.
#[serde(rename = "Status")]
pub status: TaskCompletionStatus,
@@ -5121,10 +5182,10 @@ pub struct TimerInfoDto {
pub overview: Option<String>,
/// Gets or sets the start date of the recording, in UTC.
#[serde(rename = "StartDate")]
pub start_date: jiff::Zoned,
pub start_date: jiff::Timestamp,
/// Gets or sets the end date of the recording, in UTC.
#[serde(rename = "EndDate")]
pub end_date: jiff::Zoned,
pub end_date: jiff::Timestamp,
/// Gets or sets the name of the service.
#[serde(rename = "ServiceName")]
pub service_name: Option<String>,
@@ -5208,7 +5269,7 @@ pub struct TrailerInfo {
#[serde(rename = "ParentIndexNumber")]
pub parent_index_number: Option<i32>,
#[serde(rename = "PremiereDate")]
pub premiere_date: Option<jiff::Zoned>,
pub premiere_date: Option<jiff::Timestamp>,
#[serde(rename = "IsAutomated")]
pub is_automated: bool,
}
@@ -5518,7 +5579,7 @@ pub struct UpdateUserItemDataDto {
pub likes: Option<bool>,
/// Gets or sets the last played date.
#[serde(rename = "LastPlayedDate")]
pub last_played_date: Option<jiff::Zoned>,
pub last_played_date: Option<jiff::Timestamp>,
/// Gets or sets a value indicating whether this MediaBrowser.Model.Dto.UserItemDataDto is played.
#[serde(rename = "Played")]
pub played: Option<bool>,
@@ -5674,10 +5735,10 @@ This is not used by the server and is for client-side usage only.*/
pub enable_auto_login: Option<bool>,
/// Gets or sets the last login date.
#[serde(rename = "LastLoginDate")]
pub last_login_date: Option<jiff::Zoned>,
pub last_login_date: Option<jiff::Timestamp>,
/// Gets or sets the last activity date.
#[serde(rename = "LastActivityDate")]
pub last_activity_date: Option<jiff::Zoned>,
pub last_activity_date: Option<jiff::Timestamp>,
/// Gets or sets the configuration.
#[serde(rename = "Configuration")]
pub configuration: Option<UserConfiguration>,
@@ -5714,7 +5775,7 @@ pub struct UserItemDataDto {
pub likes: Option<bool>,
/// Gets or sets the last played date.
#[serde(rename = "LastPlayedDate")]
pub last_played_date: Option<jiff::Zoned>,
pub last_played_date: Option<jiff::Timestamp>,
/// Gets or sets a value indicating whether this MediaBrowser.Model.Dto.UserItemDataDto is played.
#[serde(rename = "Played")]
pub played: bool,
@@ -5854,10 +5915,10 @@ pub struct UserUpdatedMessage {
pub struct UtcTimeResponse {
/// Gets the UTC time when request has been received.
#[serde(rename = "RequestReceptionTime")]
pub request_reception_time: jiff::Zoned,
pub request_reception_time: jiff::Timestamp,
/// Gets the UTC time when response has been sent.
#[serde(rename = "ResponseTransmissionTime")]
pub response_transmission_time: jiff::Zoned,
pub response_transmission_time: jiff::Timestamp,
}
/// Validate path object.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]

View File

@@ -1,5 +1,7 @@
pub mod jellyfin;
use std::sync::Arc;
use ::tap::*;
use reqwest::Method;
use serde::{Deserialize, Serialize};
@@ -12,6 +14,8 @@ pub enum JellyfinApiError {
SerdeError(#[from] serde_json::Error),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Unknown Jellyfin API error")]
Unknown,
}
type Result<T, E = JellyfinApiError> = std::result::Result<T, E>;
@@ -19,8 +23,8 @@ type Result<T, E = JellyfinApiError> = std::result::Result<T, E>;
#[derive(Debug, Clone)]
pub struct JellyfinClient {
client: reqwest::Client,
access_token: Option<String>,
config: JellyfinConfig,
access_token: Option<Arc<str>>,
config: Arc<JellyfinConfig>,
}
impl JellyfinClient {
@@ -28,13 +32,13 @@ impl JellyfinClient {
JellyfinClient {
client: reqwest::Client::new(),
access_token: None,
config,
config: Arc::new(config),
}
}
pub async fn save_token(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
if let Some(token) = &self.access_token {
tokio::fs::write(path, token).await
tokio::fs::write(path, &**token).await
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
@@ -43,10 +47,17 @@ impl JellyfinClient {
}
}
pub async fn load_token(&mut self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
pub async fn load_token(
&mut self,
path: impl AsRef<std::path::Path>,
) -> std::io::Result<String> {
let token = tokio::fs::read_to_string(path).await?;
self.access_token = Some(token);
Ok(())
self.access_token = Some(token.clone().into());
Ok(token)
}
pub async fn set_token(&mut self, token: impl AsRef<str>) {
self.access_token = Some(token.as_ref().into());
}
pub fn request_builder(
@@ -59,7 +70,7 @@ impl JellyfinClient {
.header("X-Emby-Authorization", format!("MediaBrowser Client=\"Jello\", Device=\"Jello\", DeviceId=\"{}\", Version=\"1.0.0\"", self.config.device_id))
.pipe(|builder| {
if let Some(token) = &self.access_token {
builder.header("X-MediaBrowser-Token", token)
builder.header("X-MediaBrowser-Token", &**token)
} else {
builder
}
@@ -118,20 +129,32 @@ impl JellyfinClient {
},
)
.await?;
self.access_token = auth_result.access_token.clone();
self.access_token = auth_result.access_token.clone().map(Into::into);
Ok(auth_result)
}
pub async fn authenticate_with_cached_token(
&mut self,
path: impl AsRef<std::path::Path>,
) -> Result<()> {
) -> Result<String> {
let path = path.as_ref();
if !self.load_token(path).await.is_ok() {
if let Ok(token) = self
.load_token(path)
.await
.inspect_err(|err| tracing::warn!("Failed to load cached token: {}", err))
{
self.authenticate().await?;
self.save_token(path).await?;
Ok(token)
} else {
let token = self
.authenticate()
.await?
.access_token
.ok_or_else(|| JellyfinApiError::Unknown)?;
self.save_token(path).await?;
Ok(token)
}
Ok(())
}
pub async fn raw_items(&self) -> Result<jellyfin::BaseItemDtoQueryResult> {
@@ -148,7 +171,7 @@ impl JellyfinClient {
pub async fn items(
&self,
root: impl Into<Option<String>>,
root: impl Into<Option<uuid::Uuid>>,
) -> Result<Vec<jellyfin::BaseItemDto>> {
let text = &self
.request_builder(Method::GET, "Items")

File diff suppressed because it is too large Load Diff

View File

@@ -43,7 +43,7 @@
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
name = cargoToml.package.name;
toolchain = pkgs.rust-bin.nightly.latest.default;
toolchain = pkgs.rust-bin.stable.latest.default;
toolchainWithLLvmTools = toolchain.override {
extensions = ["rust-src" "llvm-tools"];
};

View File

@@ -2,8 +2,7 @@ mod errors;
use api::{JellyfinClient, JellyfinConfig};
use errors::*;
#[tokio::main]
pub async fn main() -> Result<()> {
fn jellyfin_config_try() -> Result<JellyfinConfig> {
dotenvy::dotenv()
.change_context(Error)
.inspect_err(|err| {
@@ -16,16 +15,45 @@ pub async fn main() -> Result<()> {
std::env::var("JELLYFIN_SERVER_URL").change_context(Error)?,
"jello".to_string(),
);
let mut jellyfin = api::JellyfinClient::new(config);
jellyfin
.authenticate_with_cached_token(".session")
.await
.change_context(Error)?;
Ok(config)
}
#[cfg(feature = "iced")]
ui_iced::ui(jellyfin);
#[cfg(feature = "gpui")]
ui_gpui::ui(jellyfin);
fn jellyfin_config() -> JellyfinConfig {
jellyfin_config_try().unwrap_or_else(|err| {
eprintln!("Error loading Jellyfin configuration: {}", err);
std::process::exit(1);
})
}
fn main() -> Result<()> {
ui_iced::ui(jellyfin_config).change_context(Error)?;
Ok(())
}
// #[tokio::main]
// pub async fn main() -> Result<()> {
// dotenvy::dotenv()
// .change_context(Error)
// .inspect_err(|err| {
// eprintln!("Failed to load .env file: {}", err);
// })
// .ok();
// let config = JellyfinConfig::new(
// std::env::var("JELLYFIN_USERNAME").change_context(Error)?,
// std::env::var("JELLYFIN_PASSWORD").change_context(Error)?,
// std::env::var("JELLYFIN_SERVER_URL").change_context(Error)?,
// "jello".to_string(),
// );
// let mut jellyfin = api::JellyfinClient::new(config);
// jellyfin
// .authenticate_with_cached_token(".session")
// .await
// .change_context(Error)?;
//
// #[cfg(feature = "iced")]
// ui_iced::ui(jellyfin);
// #[cfg(feature = "gpui")]
// ui_gpui::ui(jellyfin);
//
// Ok(())
// }

View File

@@ -11,4 +11,4 @@ proc-macro2 = "1.0.103"
quote = "1.0.41"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
syn = { version = "2.0.108", features = ["full", "parsing"] }
syn = { version = "2.0.108", features = ["extra-traits", "full", "parsing"] }

View File

@@ -3,7 +3,7 @@ use indexmap::IndexMap;
const KEYWORDS: &[&str] = &[
"type", "match", "enum", "struct", "fn", "mod", "pub", "use", "crate", "self", "super", "as",
"in", "let", "mut", "ref", "static", "trait", "where",
"in", "let", "mut", "ref", "static", "trait", "where", "box",
];
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
@@ -15,6 +15,7 @@ pub struct JellyfinOpenapi {
pub struct Components {
schemas: indexmap::IndexMap<String, Schema>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
pub struct Schema {
#[serde(rename = "type")]
@@ -46,12 +47,15 @@ pub struct Property {
nullable: Option<bool>,
format: Option<String>,
items: Option<Box<Property>>,
properties: Option<indexmap::IndexMap<String, Property>>,
#[serde(rename = "additionalProperties")]
additional_properties: Option<Box<Property>>,
#[serde(rename = "enum")]
_enum: Option<Vec<String>>,
#[serde(rename = "allOf")]
all_of: Option<Vec<RefName>>,
#[serde(rename = "oneOf")]
one_of: Option<Vec<RefName>>,
description: Option<String>,
#[serde(rename = "$ref")]
_ref: Option<String>,
@@ -62,7 +66,7 @@ impl Property {
let out = match self._type {
Some(Types::String) => match self.format.as_deref() {
Some("uuid") => "uuid::Uuid".to_string(),
Some("date-time") => "jiff::Zoned".to_string(),
Some("date-time") => "jiff::Timestamp".to_string(),
_ => "String".to_string(),
},
Some(Types::Integer) => match self.format.as_deref() {
@@ -89,6 +93,8 @@ impl Property {
"std::collections::HashMap<String, {}>",
properties.to_string()
)
// } else if let Some(props) = &self.properties {
// todo!()
} else {
"std::collections::HashMap<String, serde_json::Value>".to_string()
}
@@ -115,7 +121,7 @@ impl Property {
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Types {
Object,
@@ -144,73 +150,10 @@ fn main() {
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let syn_structs: Vec<syn::ItemStruct> = structs
let syn_structs: Vec<syn::Item> = structs
.iter()
.map(|(key, value)| {
let fields = value
.properties
.as_ref()
.expect("Possible properties")
.iter()
.map(|(name, _type)| {
let og_name = name.clone();
let name = modify_keyword(&name.to_snake_case());
let _type_desc = _type.description();
let _type_desc = if let Some(desc) = &_type_desc {
Some(format!(" {}", desc))
} else {
None
};
let _type = _type.to_string();
let _type = if _type.contains(key) {
_type.replace(&format!("<{}>", key), format!("<Box<{}>>", key).as_str())
} else {
_type
};
syn::Field {
attrs: if let Some(desc) = _type_desc {
syn::parse_quote! {
#[doc = #desc]
#[serde(rename = #og_name)]
}
} else {
syn::parse_quote! {
#[serde(rename = #og_name)]
}
},
mutability: syn::FieldMutability::None,
vis: syn::Visibility::Public(syn::Token![pub](
proc_macro2::Span::call_site(),
)),
ident: Some(syn::Ident::new(&name, proc_macro2::Span::call_site())),
colon_token: Some(syn::token::Colon(proc_macro2::Span::call_site())),
ty: syn::parse_str(&_type).expect("Failed to parse type"),
}
})
.collect::<Vec<syn::Field>>();
let key = modify_keyword(key);
let desc = value.description.clone();
let key = syn::Ident::new(&key.to_pascal_case(), proc_macro2::Span::call_site());
let tokens = if let Some(desc) = desc {
let desc = format!(" {}", desc);
quote::quote! {
#[doc = #desc]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct #key {
#(#fields),*
}
}
} else {
quote::quote! {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct #key {
#(#fields),*
}
}
};
syn::parse2(tokens).expect("Failed to parse struct")
})
.map(|(key, value)| generate_struct(key, value))
.flatten()
.collect();
let syn_enums = enums
@@ -262,7 +205,6 @@ fn main() {
attrs: vec![],
items: syn_structs
.into_iter()
.map(syn::Item::Struct)
.chain(syn_enums.into_iter().map(syn::Item::Enum))
.collect(),
};
@@ -277,3 +219,104 @@ fn modify_keyword(name: &str) -> String {
name.to_string()
}
}
fn generate_struct(key: impl AsRef<str>, value: &Schema) -> Vec<syn::Item> {
let key = key.as_ref();
let extra_structs = value
.properties
.as_ref()
.expect("Possible properties")
.iter()
.filter_map(|(name, property)| {
if property._type == Some(Types::Object) && property.properties.is_some() {
Some(generate_struct(
&format!("{}_{}", key, name),
&Schema {
_type: Types::Object,
properties: property.properties.clone(),
one_of: None,
_enum: None,
description: property.description.clone(),
},
))
} else {
None
}
})
.flatten()
.collect::<Vec<syn::Item>>();
let fields = value
.properties
.as_ref()
.expect("Possible properties")
.iter()
.map(|(name, property)| {
let nested_struct =
property._type == Some(Types::Object) && property.properties.is_some();
let og_name = name.clone();
let name = modify_keyword(&name.to_snake_case());
let _type_desc = property.description();
let _type_desc = if let Some(desc) = &_type_desc {
Some(format!(" {}", desc))
} else {
None
};
let _type = if !nested_struct {
property.to_string()
} else {
format!("{}_{}", key, og_name).to_pascal_case()
};
let _type = if _type.contains(key) {
_type.replace(&format!("<{}>", key), format!("<Box<{}>>", key).as_str())
} else {
_type
};
syn::Field {
attrs: if let Some(desc) = _type_desc {
syn::parse_quote! {
#[doc = #desc]
#[serde(rename = #og_name)]
}
} else {
syn::parse_quote! {
#[serde(rename = #og_name)]
}
},
mutability: syn::FieldMutability::None,
vis: syn::Visibility::Public(syn::Token![pub](proc_macro2::Span::call_site())),
ident: Some(syn::Ident::new(&name, proc_macro2::Span::call_site())),
colon_token: Some(syn::token::Colon(proc_macro2::Span::call_site())),
ty: syn::parse_str(&_type).expect("Failed to parse type"),
}
})
.collect::<Vec<syn::Field>>();
let key = modify_keyword(key);
let desc = value.description.clone();
let key = syn::Ident::new(&key.to_pascal_case(), proc_macro2::Span::call_site());
let tokens = if let Some(desc) = desc {
let desc = format!(" {}", desc);
quote::quote! {
#[doc = #desc]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct #key {
#(#fields),*
}
}
} else {
quote::quote! {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct #key {
#(#fields),*
}
}
};
let mut out = syn::parse2::<syn::File>(tokens)
.expect("Failed to parse struct")
.items;
out.extend(extra_structs);
out
}

View File

@@ -6,3 +6,4 @@ edition = "2024"
[dependencies]
gpui = { version = "0.2.2", default-features = false, features = ["wayland"] }
tap = "1.0.1"
blurhash = "0.2.3"

View File

@@ -4,6 +4,13 @@ version = "0.1.0"
edition = "2024"
[dependencies]
api = { version = "0.1.0", path = "../api" }
gpui_util = "0.2.2"
iced = { version = "0.13.1", features = ["canvas", "image", "tokio"] }
iced = { git = "https://github.com/iced-rs/iced", features = [
"advanced",
"canvas",
"image",
"tokio",
] }
tracing = "0.1.41"
uuid = "1.18.1"

View File

@@ -1,33 +1,228 @@
use std::{collections::BTreeMap, sync::Arc};
type SharedString = Arc<str>;
mod shared_string;
use shared_string::SharedString;
use iced::{Element, Task};
struct State {
loading: Option<Loading>,
current: Screen,
cache: ItemCache,
}
pub struct ItemCache {
pub items: BTreeMap<uuid::Uuid, Item>,
}
#[derive(Clone, Debug)]
pub struct Item {
pub id: SharedString,
pub name: SharedString,
pub item_type: SharedString,
pub media_type: SharedString,
}
pub enum Screen {
Home,
Settings,
Profile,
}
use iced::{Alignment, Element, Length, Task, widget::*};
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub struct Loading {
to: Screen,
from: Screen,
}
#[derive(Default, Debug, Clone)]
pub struct ItemCache {
pub items: BTreeMap<uuid::Uuid, Item>,
}
impl ItemCache {
pub fn insert(&mut self, item: Item) {
self.items.insert(item.id, item);
}
pub fn extend<I: IntoIterator<Item = Item>>(&mut self, items: I) {
self.items
.extend(items.into_iter().map(|item| (item.id, item)));
}
}
#[derive(Clone, Debug)]
pub struct Image {
pub id: SharedString,
pub blur_hash: Option<SharedString>,
}
impl From<api::jellyfin::BaseItemDto> for Item {
fn from(dto: api::jellyfin::BaseItemDto) -> Self {
Item {
id: dto.id,
name: dto.name.map(Into::into),
picture: dto
.image_tags
.and_then(|tags| tags.get("Primary").cloned())
.map(|tag| Image {
id: tag.clone().into(),
blur_hash: dto
.image_blur_hashes
.primary
.get(&tag)
.map(|s| s.clone().into()),
}),
}
}
}
#[derive(Clone, Debug)]
pub struct Item {
pub id: uuid::Uuid,
pub name: Option<SharedString>,
pub picture: Option<Image>,
}
#[derive(Debug, Clone, Default)]
pub enum Screen {
#[default]
Home,
Settings,
Profile,
}
#[derive(Debug, Clone)]
struct State {
loading: Option<Loading>,
current: Option<uuid::Uuid>,
cache: ItemCache,
jellyfin_client: api::JellyfinClient,
}
impl State {
pub fn new(jellyfin_client: api::JellyfinClient) -> Self {
State {
loading: None,
current: None,
cache: ItemCache::default(),
jellyfin_client,
}
}
}
#[derive(Debug, Clone)]
pub enum Message {
OpenSettings,
Refresh,
OpenItem(uuid::Uuid),
LoadedItem(uuid::Uuid, Vec<Item>),
Error(String),
SetToken(String),
}
fn update(state: &mut State, message: Message) -> Task<Message> {
match message {
Message::OpenSettings => {
// Foo
Task::none()
}
Message::OpenItem(id) => {
let client = state.jellyfin_client.clone();
Task::perform(
async move {
let items: Result<Vec<Item>, api::JellyfinApiError> = client
.items(id)
.await
.map(|items| items.into_iter().map(Item::from).collect());
(id, items)
},
|(msg, items)| match items {
Err(e) => Message::Error(format!("Failed to load item: {}", e)),
Ok(items) => Message::LoadedItem(msg, items),
},
)
}
Message::LoadedItem(id, items) => {
state.cache.extend(items);
state.current = Some(id);
Task::none()
}
Message::Refresh => {
// Handle refresh logic
Task::none()
}
Message::Error(err) => {
tracing::error!("Error: {}", err);
Task::none()
}
Message::SetToken(token) => {
state.jellyfin_client.set_token(token);
Task::none()
}
}
}
fn view(state: &State) -> Element<'_, Message> {
column([header(), body(state)]).into()
}
fn body(state: &State) -> Element<'_, Message> {
container(Text::new("Home Screen"))
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.height(Length::Fill)
.width(Length::Fill)
.into()
}
fn header() -> Element<'static, Message> {
row([
container(
Text::new("Jello")
.width(Length::Fill)
.align_x(Alignment::Start),
)
.padding(10)
.width(Length::Fill)
.height(Length::Fill)
.align_x(Alignment::Start)
.align_y(Alignment::Center)
.style(container::rounded_box)
.into(),
container(
row([
button("Settings").on_press(Message::OpenSettings).into(),
button("Refresh").on_press(Message::Refresh).into(),
])
.spacing(10),
)
.padding(10)
.width(Length::Fill)
.height(Length::Fill)
.align_x(Alignment::End)
.align_y(Alignment::Center)
.style(container::rounded_box)
.into(),
])
.align_y(Alignment::Center)
.width(Length::Fill)
.height(50)
.into()
}
fn card(item: &Item) -> Element<'_, Message> {
let name = item
.name
.as_ref()
.map(|s| s.as_ref())
.unwrap_or("Unnamed Item");
container(
column([
Text::new(name).size(16).into(),
iced::widget::Image::new("placeholder.png")
.width(Length::Fill)
.height(150)
.into(),
])
.spacing(10),
)
.padding(10)
.width(Length::FillPortion(5))
.height(Length::FillPortion(5))
.style(container::rounded_box)
.into()
}
fn init(config: impl Fn() -> api::JellyfinConfig + 'static) -> impl Fn() -> (State, Task<Message>) {
move || {
let mut jellyfin = api::JellyfinClient::new(config());
(
State::new(jellyfin.clone()),
Task::perform(
async move { jellyfin.authenticate_with_cached_token(".session").await },
|token| match token {
Ok(token) => Message::SetToken(token),
Err(e) => Message::Error(format!("Authentication failed: {}", e)),
},
),
)
}
}
pub fn ui(config: impl Fn() -> api::JellyfinConfig + 'static) -> iced::Result {
iced::application(init(config), update, view).run()
}

View File

@@ -0,0 +1,68 @@
use std::{borrow::Cow, sync::Arc};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SharedString(ArcCow<'static, str>);
impl From<String> for SharedString {
fn from(s: String) -> Self {
SharedString(ArcCow::Owned(Arc::from(s)))
}
}
impl<'a> iced::advanced::text::IntoFragment<'a> for SharedString {
fn into_fragment(self) -> Cow<'a, str> {
match self.0 {
ArcCow::Borrowed(b) => Cow::Borrowed(b),
ArcCow::Owned(o) => Cow::Owned(o.as_ref().to_string()),
}
}
}
impl<'a> iced::advanced::text::IntoFragment<'a> for &SharedString {
fn into_fragment(self) -> Cow<'a, str> {
match &self.0 {
ArcCow::Borrowed(b) => Cow::Borrowed(b),
ArcCow::Owned(o) => Cow::Owned(o.as_ref().to_string()),
}
}
}
impl From<&'static str> for SharedString {
fn from(s: &'static str) -> Self {
SharedString(ArcCow::Borrowed(s))
}
}
impl AsRef<str> for SharedString {
fn as_ref(&self) -> &str {
match &self.0 {
ArcCow::Borrowed(b) => b,
ArcCow::Owned(o) => o.as_ref(),
}
}
}
impl std::ops::Deref for SharedString {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_ref()
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum ArcCow<'a, T: ?Sized> {
Borrowed(&'a T),
Owned(Arc<T>),
}
impl<'a, T> Clone for ArcCow<'a, T>
where
T: ?Sized,
{
fn clone(&self) -> Self {
match self {
ArcCow::Borrowed(b) => ArcCow::Borrowed(b),
ArcCow::Owned(o) => ArcCow::Owned(Arc::clone(o)),
}
}
}