diff options
| author | metamuffin <metamuffin@disroot.org> | 2025-02-04 14:12:58 +0100 |
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2025-02-04 14:12:58 +0100 |
| commit | 8118667fc80bdf0246b16dff08f5a522efea27cf (patch) | |
| tree | 47d8bf8815ecc553b6fcd84aaeb1b5eda7c8777f /server/src/routes/compat/jellyfin.rs | |
| parent | a506544a748bbbb133fbe3743a686878d9fbcef1 (diff) | |
| download | jellything-8118667fc80bdf0246b16dff08f5a522efea27cf.tar jellything-8118667fc80bdf0246b16dff08f5a522efea27cf.tar.bz2 jellything-8118667fc80bdf0246b16dff08f5a522efea27cf.tar.zst | |
jellyfin to multiple files
Diffstat (limited to 'server/src/routes/compat/jellyfin.rs')
| -rw-r--r-- | server/src/routes/compat/jellyfin.rs | 929 |
1 files changed, 0 insertions, 929 deletions
diff --git a/server/src/routes/compat/jellyfin.rs b/server/src/routes/compat/jellyfin.rs deleted file mode 100644 index 17ddcb6..0000000 --- a/server/src/routes/compat/jellyfin.rs +++ /dev/null @@ -1,929 +0,0 @@ -/* - This file is part of jellything (https://codeberg.org/metamuffin/jellything) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2025 metamuffin <metamuffin.org> -*/ -use crate::routes::{ - stream::rocket_uri_macro_r_stream, - ui::{ - account::{login_logic, session::Session}, - assets::rocket_uri_macro_r_item_poster, - error::MyResult, - node::{aspect_class, DatabaseNodeUserDataExt}, - }, -}; -use anyhow::{anyhow, Context}; -use jellybase::{database::Database, CONF}; -use jellycommon::{ - stream::{StreamFormat, StreamSpec}, - user::{NodeUserData, WatchedState}, - MediaInfo, Node, NodeID, NodeKind, SourceTrack, SourceTrackKind, Visibility, -}; -use rocket::{get, post, response::Redirect, serde::json::Json, FromForm, State}; -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; -use std::{collections::BTreeMap, net::IpAddr}; - -const SERVER_ID: &'static str = "1694a95daf70708147f16103ce7b7566"; -const USER_ID: &'static str = "33f772aae6c2495ca89fe00340dbd17c"; -const VERSION: &'static str = "10.10.0"; -const LOCAL_ADDRESS: &'static str = "http://127.0.0.1:8000"; - -#[get("/System/Info/Public")] -pub fn r_jellyfin_system_info_public_case() -> Json<Value> { - r_jellyfin_system_info_public() -} - -#[get("/system/info/public")] -pub fn r_jellyfin_system_info_public() -> Json<Value> { - Json(json!({ - "LocalAddress": LOCAL_ADDRESS, - "ServerName": CONF.brand.clone(), - "Version": VERSION, - "ProductName": "Jellything", - "OperatingSystem": "", - "Id": SERVER_ID, - "StartupWizardCompleted": true, - })) -} - -#[get("/Branding/Configuration")] -pub fn r_jellyfin_branding_configuration() -> Json<Value> { - Json(json!({ - "LoginDisclaimer": format!("{} - {}", CONF.brand, CONF.slogan), - "CustomCss": "", - "SplashscreenEnabled": false, - })) -} - -#[get("/users/public")] -pub fn r_jellyfin_users_public() -> Json<Value> { - Json(json!([])) -} - -#[get("/QuickConnect/Enabled")] -pub fn r_jellyfin_quickconnect_enabled(_session: Session) -> Json<Value> { - Json(json!(false)) -} - -#[get("/System/Endpoint")] -pub fn r_jellyfin_system_endpoint(_session: Session) -> Json<Value> { - Json(json!({ - "IsLocal": false, - "IsInNetwork": false, - })) -} - -use rocket_ws::{Message, Stream, WebSocket}; -#[get("/socket")] -pub fn r_jellyfin_socket(_session: Session, ws: WebSocket) -> Stream!['static] { - Stream! { ws => - for await message in ws { - eprintln!("{message:?}"); - } - yield Message::Text("test".to_string()) - } -} - -#[get("/System/Info")] -pub fn r_jellyfin_system_info(_session: Session) -> Json<Value> { - Json(json!({ - "OperatingSystemDisplayName": "", - "HasPendingRestart": false, - "IsShuttingDown": false, - "SupportsLibraryMonitor": true, - "WebSocketPortNumber": 8096, - "CompletedInstallations": [], - "CanSelfRestart": true, - "CanLaunchWebBrowser": false, - "ProgramDataPath": "/path/to/data", - "WebPath": "/path/to/web", - "ItemsByNamePath": "/path/to/items", - "CachePath": "/path/to/cache", - "LogPath": "/path/to/log", - "InternalMetadataPath": "/path/to/metadata", - "TranscodingTempPath": "/path/to/transcodes", - "CastReceiverApplications": [], - "HasUpdateAvailable": false, - "EncoderLocation": "System", - "SystemArchitecture": "X64", - "LocalAddress": LOCAL_ADDRESS, - "ServerName": CONF.brand, - "Version": VERSION, - "OperatingSystem": "", - "Id": SERVER_ID - })) -} - -#[get("/DisplayPreferences/usersettings")] -pub fn r_jellyfin_displaypreferences_usersettings(_session: Session) -> Json<Value> { - Json(json!({ - "Id": "3ce5b65d-e116-d731-65d1-efc4a30ec35c", - "SortBy": "SortName", - "RememberIndexing": false, - "PrimaryImageHeight": 250, - "PrimaryImageWidth": 250, - "CustomPrefs": false, - "ScrollDirection": "Horizontal", - "ShowBackdrop": true, - "RememberSorting": false, - "SortOrder": "Ascending", - "ShowSidebar": false, - "Client": "emby", - })) -} - -#[get("/Users/<id>")] -pub fn r_jellyfin_users_id(session: Session, id: &str) -> Json<Value> { - let _ = id; - Json(user_object(session.user.name)) -} - -#[get("/Items/<id>/Images/Primary?<fillWidth>")] -#[allow(non_snake_case)] -pub fn r_jellyfin_items_image_primary( - _session: Session, - id: &str, - fillWidth: Option<usize>, -) -> Redirect { - Redirect::permanent(rocket::uri!(r_item_poster(id, fillWidth))) -} - -#[get("/Users/<uid>/Items/<id>")] -#[allow(private_interfaces)] -pub fn r_jellyfin_users_items_item( - session: Session, - database: &State<Database>, - uid: &str, - id: &str, -) -> MyResult<Json<JellyfinItem>> { - let _ = uid; - let (n, ud) = database.get_node_with_userdata(NodeID::from_slug(id), &session)?; - Ok(Json(item_object(&n, &ud))) -} - -#[derive(Debug, FromForm)] -struct JellyfinItemQuery { - #[field(name = uncased("searchterm"))] - search_term: Option<String>, - #[field(name = uncased("limit"))] - limit: usize, - #[field(name = uncased("parentid"))] - parent_id: Option<String>, - #[field(name = uncased("startindex"))] - start_index: Option<usize>, - #[field(name = uncased("includeitemtypes"))] - include_item_types: Option<String>, - - internal_artists: bool, - internal_persons: bool, -} - -#[get("/Users/<uid>/Items?<query..>")] -#[allow(private_interfaces)] -pub fn r_jellyfin_users_items( - session: Session, - database: &State<Database>, - uid: &str, - query: JellyfinItemQuery, -) -> MyResult<Json<Value>> { - let _ = uid; - r_jellyfin_items(session, database, query) -} - -#[get("/Artists?<query..>")] -#[allow(private_interfaces)] -pub fn r_jellyfin_artists( - session: Session, - database: &State<Database>, - mut query: JellyfinItemQuery, -) -> MyResult<Json<Value>> { - query.internal_artists = true; - r_jellyfin_items(session, database, query)?; // TODO - Ok(Json(json!({ - "Items": [], - "TotalRecordCount": 0, - "StartIndex": 0 - }))) -} - -#[get("/Persons?<query..>")] -#[allow(private_interfaces)] -pub fn r_jellyfin_persons( - session: Session, - database: &State<Database>, - mut query: JellyfinItemQuery, -) -> MyResult<Json<Value>> { - query.internal_persons = true; - r_jellyfin_items(session, database, query)?; // TODO - Ok(Json(json!({ - "Items": [], - "TotalRecordCount": 0, - "StartIndex": 0 - }))) -} - -#[get("/Items?<query..>")] -#[allow(private_interfaces)] -pub fn r_jellyfin_items( - session: Session, - database: &State<Database>, - query: JellyfinItemQuery, -) -> MyResult<Json<Value>> { - let mut items = Vec::new(); - - let nodes = if let Some(q) = query.search_term { - database - .search(&q, query.limit, query.start_index.unwrap_or_default())? - .1 - } else if let Some(parent) = query.parent_id { - database - .get_node_children(NodeID::from_slug(&parent))? - .into_iter() - .skip(query.start_index.unwrap_or_default()) - .take(query.limit) - .collect() - } else { - vec![] - }; - - let filter_kind = query - .include_item_types - .map(|n| match n.as_str() { - "Movie" => NodeKind::Movie, - "Audio" => NodeKind::Music, - "Video" => NodeKind::Video, - _ => NodeKind::Unknown, - }) - .or(if query.internal_artists { - Some(NodeKind::Channel) - } else { - None - }) - .or(if query.internal_persons { - Some(NodeKind::Channel) - } else { - None - }); - for nid in nodes { - let (n, ud) = database.get_node_with_userdata(nid, &session)?; - if let Some(fk) = filter_kind { - if n.kind != fk { - continue; - } - } - if n.visibility >= Visibility::Reduced { - items.push(item_object(&n, &ud)) - } - } - - Ok(Json(json!({ - "Items": items, - "TotalRecordCount": items.len(), - "StartIndex": query.start_index.unwrap_or_default() - }))) -} - -#[get("/UserViews?<userId>")] -#[allow(non_snake_case)] -pub fn r_jellyfin_users_views( - session: Session, - database: &State<Database>, - userId: &str, -) -> MyResult<Json<Value>> { - let _ = userId; - - let toplevel = database - .get_node_children(NodeID::from_slug("library")) - .context("root node missing")?; - - let mut items = Vec::new(); - for nid in toplevel { - let (n, ud) = database.get_node_with_userdata(nid, &session)?; - if n.visibility >= Visibility::Reduced { - items.push(item_object(&n, &ud)) - } - } - - Ok(Json(json!({ - "Items": items, - "TotalRecordCount": items.len(), - "StartIndex": 0 - }))) -} - -#[get("/LiveTv/Programs/Recommended")] -pub fn r_jellyfin_livetv_programs_recommended(_session: Session) -> Json<Value> { - Json(json!({ - "Items": [], - "TotalRecordCount": 0, - "StartIndex": 0 - })) -} - -#[get("/Users/<uid>/Items/<id>/Intros")] -pub fn r_jellyfin_items_intros(_session: Session, uid: &str, id: &str) -> Json<Value> { - let _ = (uid, id); - Json(json!({ - "Items": [], - "TotalRecordCount": 0, - "StartIndex": 0 - })) -} - -#[post("/Items/<id>/PlaybackInfo")] -pub fn r_jellyfin_items_playbackinfo( - _session: Session, - database: &State<Database>, - id: &str, -) -> MyResult<Json<Value>> { - let node = database - .get_node_slug(id)? - .ok_or(anyhow!("node does not exist"))?; - let media = node.media.as_ref().ok_or(anyhow!("node has no media"))?; - let ms = media_source_object(&node, media); - Ok(Json(json!({ - "MediaSources": [ms], - "PlaySessionId": "why do we need this id?" - }))) -} - -#[get("/Videos/<id>/stream.mkv")] -pub fn r_jellyfin_video_stream(_session: Session, id: &str) -> Redirect { - Redirect::temporary(rocket::uri!(r_stream( - id, - StreamSpec { - format: StreamFormat::Matroska, - webm: Some(true), - track: vec![0, 1], - index: None, - profile: None, - } - ))) -} - -#[derive(Deserialize)] -#[serde(rename_all = "PascalCase")] -struct JellyfinProgressData { - item_id: String, - position_ticks: f64, -} -#[post("/Sessions/Playing/Progress", data = "<data>")] -#[allow(private_interfaces)] -pub fn r_jellyfin_sessions_playing_progress( - session: Session, - database: &State<Database>, - data: Json<JellyfinProgressData>, -) -> MyResult<()> { - let position = data.position_ticks / 10_000_000.; - database.update_node_udata( - NodeID::from_slug(&data.item_id), - &session.user.name, - |udata| { - udata.watched = match udata.watched { - WatchedState::None | WatchedState::Pending | WatchedState::Progress(_) => { - WatchedState::Progress(position) - } - WatchedState::Watched => WatchedState::Watched, - }; - Ok(()) - }, - )?; - Ok(()) -} - -#[post("/Sessions/Playing")] -pub fn r_jellyfin_sessions_playing(_session: Session) {} - -#[get("/Playback/BitrateTest?<Size>")] -#[allow(non_snake_case)] -pub fn r_jellyfin_playback_bitratetest(_session: Session, Size: usize) -> Vec<u8> { - vec![0; Size.min(1_000_000)] -} - -#[get("/Branding/Css")] -pub fn r_jellyfin_branding_css(_session: Session) -> String { - "".to_string() -} - -#[post("/Sessions/Capabilities/Full")] -pub fn r_jellyfin_sessions_capabilities_full(_session: Session) -> () { - () -} - -#[derive(Deserialize)] -#[serde(rename_all = "PascalCase")] -struct AuthData { - pw: String, - username: String, -} - -#[post("/Users/authenticatebyname", data = "<data>")] -#[allow(private_interfaces)] -pub fn r_jellyfin_users_authenticatebyname( - client_addr: IpAddr, - database: &State<Database>, - data: Json<AuthData>, -) -> MyResult<Json<Value>> { - let token = login_logic(database, &data.username, &data.pw, None, None)?; - - Ok(Json(json!({ - "User": user_object(data.username.clone()), - "SessionInfo": { - "PlayState": { - "CanSeek": false, - "IsPaused": false, - "IsMuted": false, - "RepeatMode": "RepeatNone", - "PlaybackOrder": "Default" - }, - "AdditionalUsers": [], - "Capabilities": { - "PlayableMediaTypes": [ - "Audio", - "Video" - ], - "SupportedCommands": [], - // "MoveUp", "MoveDown", "MoveLeft", "MoveRight", "PageUp", "PageDown", "PreviousLetter", "NextLetter", "ToggleOsd", "ToggleContextMenu", "Select", "Back", "SendKey", "SendString", "GoHome", "GoToSettings", "VolumeUp", "VolumeDown", "Mute", "Unmute", "ToggleMute", "SetVolume", "SetAudioStreamIndex", "SetSubtitleStreamIndex", "DisplayContent", "GoToSearch", "DisplayMessage", "SetRepeatMode", "SetShuffleQueue", "ChannelUp", "ChannelDown", "PlayMediaSource", "PlayTrailers" - "SupportsMediaControl": true, - "SupportsPersistentIdentifier": false - }, - "RemoteEndPoint": client_addr, - "PlayableMediaTypes": [ - "Audio", - "Video" - ], - "Id": "6e05fbb4fe33477b991455c97a57e25d", - "UserId": USER_ID, - "UserName": data.username.clone(), - "Client": "Jellyfin Web", - "LastActivityDate": "0001-01-01T00:00:00.0000000Z", - "LastPlaybackCheckIn": "0001-01-01T00:00:00.0000000Z", - "DeviceName": "blub blub blub", - "DeviceId": "wagening", - "ApplicationVersion": VERSION, - "IsActive": true, - "SupportsMediaControl": false, - "SupportsRemoteControl": false, - "NowPlayingQueue": [], - "NowPlayingQueueFullItems": [], - "HasCustomDeviceName": false, - "ServerId": SERVER_ID, - "SupportedCommands": [] - // "MoveUp", "MoveDown", "MoveLeft", "MoveRight", "PageUp", "PageDown", "PreviousLetter", "NextLetter", "ToggleOsd", "ToggleContextMenu", "Select", "Back", "SendKey", "SendString", "GoHome", "GoToSettings", "VolumeUp", "VolumeDown", "Mute", "Unmute", "ToggleMute", "SetVolume", "SetAudioStreamIndex", "SetSubtitleStreamIndex", "DisplayContent", "GoToSearch", "DisplayMessage", "SetRepeatMode", "SetShuffleQueue", "ChannelUp", "ChannelDown", "PlayMediaSource", "PlayTrailers" - }, - "AccessToken": token, - "ServerId": SERVER_ID - }))) -} - -#[derive(Debug, Serialize, Deserialize)] -enum JellyfinItemType { - AudioBook, - Movie, - BoxSet, - Book, - Photo, - PhotoAlbum, - TvChannel, - LiveTvProgram, - Video, - Audio, - MusicAlbum, - CollectionFolder, -} - -#[derive(Debug, Clone, Serialize)] -enum JellyfinMediaStreamType { - Video, - Audio, - Subtitle, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "PascalCase")] -struct JellyfinMediaStream { - codec: String, - time_base: String, - video_range: String, - video_range_type: String, - audio_spatial_format: String, - display_title: String, - is_interlaced: bool, - is_avc: bool, - bit_rate: usize, - bit_depth: usize, - ref_frames: usize, - is_default: bool, - is_forced: bool, - is_hearing_impaired: bool, - height: Option<u64>, - width: Option<u64>, - average_frame_rate: Option<f64>, - real_frame_rate: Option<f64>, - reference_frame_rate: Option<f64>, - profile: String, - r#type: JellyfinMediaStreamType, - aspect_ratio: String, - index: usize, - is_external: bool, - is_text_subtitle_stream: bool, - supports_external_stream: bool, - pixel_format: String, - level: usize, - is_anamorphic: bool, - channel_layout: Option<String>, - channels: Option<usize>, - sample_rate: Option<f64>, - localized_default: String, - localized_external: String, -} - -#[derive(Debug, Clone, Serialize)] -enum JellyfinMediaSourceProtocol { - File, -} -#[derive(Debug, Clone, Serialize)] -enum JellyfinMediaSourceType { - Default, -} - -#[derive(Debug, Clone, Serialize)] -enum JellyfinVideoType { - VideoFile, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "PascalCase")] -struct JellyfinMediaSource { - protocol: JellyfinMediaSourceProtocol, - id: String, - path: String, - r#type: JellyfinMediaSourceType, - container: String, - size: usize, - name: String, - is_remote: bool, - e_tag: String, - run_time_ticks: f64, - read_at_native_framerate: bool, - ignore_dts: bool, - ignore_index: bool, - gen_pts_input: bool, - supports_transcoding: bool, - supports_direct_stream: bool, - supports_direct_play: bool, - is_infinite_stream: bool, - use_most_compatible_transcoding_profile: bool, - requires_opening: bool, - requires_closing: bool, - requires_looping: bool, - supports_probing: bool, - video_type: JellyfinVideoType, - media_streams: Vec<JellyfinMediaStream>, - media_attachments: Vec<()>, - formats: Vec<()>, - bitrate: usize, - required_http_headers: BTreeMap<(), ()>, - transcoding_sub_protocol: String, - default_audio_stream_index: usize, - default_subtitle_stream_index: usize, - has_segments: bool, -} - -fn track_object(index: usize, track: &SourceTrack) -> JellyfinMediaStream { - let fr = if let SourceTrackKind::Video { fps, .. } = &track.kind { - Some(fps.unwrap_or_default()) - } else { - None - }; - JellyfinMediaStream { - codec: match track.codec.as_str() { - "V_HEVC" => "hevc", - "V_AV1" => "av1", - "V_VP8" => "vp8", - "V_VP9" => "vp9", - "A_AAC" => "aac", - "A_OPUS" => "opus", - _ => "unknown", - } - .to_string(), - time_base: "1/1000".to_string(), // TODO unsure what that means - video_range: if track.kind.letter() == 'v' { - "SDR" - } else { - "Unknown" - } - .to_string(), - video_range_type: if track.kind.letter() == 'v' { - "SDR" - } else { - "Unknown" - } - .to_string(), - audio_spatial_format: "None".to_string(), - display_title: track.to_string(), - is_interlaced: false, // TODO assuming that - is_avc: track.codec.as_str() == "V_AVC", - bit_rate: 5_000_000, // TODO totally - bit_depth: 8, - ref_frames: 1, - is_default: true, - is_forced: false, - is_hearing_impaired: false, - height: if let SourceTrackKind::Video { height, .. } = &track.kind { - Some(*height) - } else { - None - }, - width: if let SourceTrackKind::Video { width, .. } = &track.kind { - Some(*width) - } else { - None - }, - average_frame_rate: fr, - real_frame_rate: fr, - reference_frame_rate: fr, - profile: "Main".to_string(), - r#type: match track.kind { - SourceTrackKind::Audio { .. } => JellyfinMediaStreamType::Audio, - SourceTrackKind::Video { .. } => JellyfinMediaStreamType::Video, - SourceTrackKind::Subtitles => JellyfinMediaStreamType::Subtitle, - }, - aspect_ratio: "1:1".to_string(), // TODO aaa - index, - is_external: false, - is_text_subtitle_stream: false, - supports_external_stream: false, - pixel_format: "yuv420p".to_string(), - level: 150, // TODO what this mean? - is_anamorphic: false, - channel_layout: if let SourceTrackKind::Audio { .. } = &track.kind { - Some("5.1".to_string()) // TODO aaa - } else { - None - }, - channels: if let SourceTrackKind::Audio { channels, .. } = &track.kind { - Some(*channels) - } else { - None - }, - sample_rate: if let SourceTrackKind::Audio { sample_rate, .. } = &track.kind { - Some(*sample_rate) - } else { - None - }, - localized_default: "Default".to_string(), - localized_external: "External".to_string(), - } -} - -fn media_source_object(node: &Node, m: &MediaInfo) -> JellyfinMediaSource { - JellyfinMediaSource { - protocol: JellyfinMediaSourceProtocol::File, - id: node.slug.clone(), - path: format!("/path/to/{}.mkv", node.slug), - r#type: JellyfinMediaSourceType::Default, - container: "mkv".to_string(), - size: 1_000_000_000, - name: node.slug.clone(), - is_remote: false, - e_tag: "blub".to_string(), - run_time_ticks: m.duration * 10_000_000., - read_at_native_framerate: false, - ignore_dts: false, - ignore_index: false, - gen_pts_input: false, - supports_transcoding: true, - supports_direct_stream: true, - supports_direct_play: true, - is_infinite_stream: false, - use_most_compatible_transcoding_profile: false, - requires_opening: false, - requires_closing: false, - requires_looping: false, - supports_probing: true, - video_type: JellyfinVideoType::VideoFile, - media_streams: m - .tracks - .iter() - .enumerate() - .map(|(i, t)| track_object(i, t)) - .collect::<Vec<_>>(), - media_attachments: Vec::new(), - formats: Vec::new(), - bitrate: 10_000_000, - required_http_headers: BTreeMap::new(), - transcoding_sub_protocol: "http".to_string(), - default_audio_stream_index: 1, // TODO - default_subtitle_stream_index: 2, // TODO - has_segments: false, - } -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "PascalCase")] -struct JellyfinItem { - name: String, - server_id: String, - id: String, - e_tag: String, - date_created: String, - can_delete: bool, - can_download: bool, - preferred_metadata_language: String, - preferred_metadata_country_code: String, - sort_name: String, - forced_sort_name: String, - external_urls: Vec<()>, - enable_media_source_display: bool, - custom_rating: String, - channel_id: Option<String>, - overview: String, - taglines: Vec<String>, - genres: Vec<()>, - play_access: Option<String>, - remote_trailers: Vec<()>, - provider_ids: BTreeMap<(), ()>, - is_folder: bool, - parent_id: String, - r#type: JellyfinItemType, - people: Vec<()>, - studios: Vec<()>, - genre_items: Vec<()>, - local_trailer_count: usize, - special_feature_count: usize, - child_count: usize, - locked_fields: Vec<()>, - lock_data: bool, - tags: Vec<String>, - user_data: Value, - display_preferences_id: String, - primary_image_aspect_ratio: f64, - collection_type: String, - image_tags: BTreeMap<String, String>, - backdrop_image_tags: Vec<()>, - location_type: Option<String>, - media_type: String, - video_type: Option<String>, - container: Option<String>, - run_time_ticks: Option<i64>, - media_sources: Option<Vec<JellyfinMediaSource>>, - media_streams: Option<Vec<JellyfinMediaStream>>, - path: Option<String>, -} - -fn item_object(node: &Node, userdata: &NodeUserData) -> JellyfinItem { - let media_source = node.media.as_ref().map(|m| media_source_object(node, m)); - - JellyfinItem { - name: node.title.clone().unwrap_or_default(), - server_id: SERVER_ID.to_owned(), - id: node.slug.clone(), - e_tag: "blob".to_owned(), - date_created: "0001-01-01T00:00:00.0000000Z".to_owned(), - can_delete: false, - can_download: true, - preferred_metadata_language: "".to_owned(), - preferred_metadata_country_code: "".to_owned(), - sort_name: node.slug.clone(), - forced_sort_name: "".to_owned(), - external_urls: vec![], - enable_media_source_display: true, - custom_rating: "".to_owned(), - channel_id: None, - overview: node.description.clone().unwrap_or_default(), - taglines: vec![node.tagline.clone().unwrap_or_default()], - genres: vec![], - remote_trailers: vec![], - provider_ids: BTreeMap::new(), - is_folder: !node.media.is_some(), - parent_id: "todo-parent".to_owned(), // TODO - r#type: match node.kind { - NodeKind::Movie | NodeKind::Video | NodeKind::ShortFormVideo => JellyfinItemType::Movie, - NodeKind::Collection | _ => JellyfinItemType::CollectionFolder, - }, - people: vec![], - studios: vec![], - genre_items: vec![], - local_trailer_count: 0, - special_feature_count: 0, - child_count: 0, - locked_fields: vec![], - lock_data: false, - tags: vec![], - user_data: json!({ - "PlaybackPositionTicks": 0, - "PlayCount": if userdata.watched == WatchedState::Watched { 1 } else { 0 }, - "IsFavorite": userdata.rating > 0, - "Played": userdata.watched == WatchedState::Watched, - "Key": "7a2175bc-cb1f-1a94-152c-bd2b2bae8f6d", - "ItemId": "00000000000000000000000000000000" - }), - display_preferences_id: node.slug.clone(), - primary_image_aspect_ratio: match aspect_class(node.kind) { - "aspect-thumb" => 16. / 9., - "aspect-land" => 2f64.sqrt(), - "aspect-port" => 1. / 2f64.sqrt(), - "aspect-square" | _ => 1., - }, - collection_type: "unknown".to_owned(), - image_tags: BTreeMap::from_iter([("Primary".to_string(), "the-image".to_string())]), - backdrop_image_tags: vec![], - media_type: if node.media.is_some() { - "Video".to_owned() - } else { - "Unknown".to_owned() - }, - video_type: node.media.as_ref().map(|_| "VideoFile".to_owned()), - location_type: node.media.as_ref().map(|_| "FileSystem".to_owned()), - play_access: node.media.as_ref().map(|_| "Full".to_owned()), - container: node.media.as_ref().map(|_| "mkv".to_owned()), - run_time_ticks: if let Some(m) = &node.media { - Some((m.duration * 10_000_000.) as i64) - } else { - None - }, - media_sources: media_source.as_ref().map(|s| vec![s.clone()]), - media_streams: media_source.as_ref().map(|s| s.media_streams.clone()), - path: node - .media - .as_ref() - .map(|_| format!("/path/to/{}.mkv", node.slug)), - } -} - -fn user_object(username: String) -> Value { - json!({ - "Name": username, - "ServerId": SERVER_ID, - "Id": USER_ID, - "HasPassword": true, - "HasConfiguredPassword": true, - "HasConfiguredEasyPassword": false, - "EnableAutoLogin": false, - "LastLoginDate": "0001-01-01T00:00:00.0000000Z", - "LastActivityDate": "0001-01-01T00:00:00.0000000Z", - "Configuration": { - "PlayDefaultAudioTrack": true, - "SubtitleLanguagePreference": "", - "DisplayMissingEpisodes": false, - "GroupedFolders": [], - "SubtitleMode": "Default", - "DisplayCollectionsView": false, - "EnableLocalPassword": false, - "OrderedViews": [], - "LatestItemsExcludes": [], - "MyMediaExcludes": [], - "HidePlayedInLatest": true, - "RememberAudioSelections": true, - "RememberSubtitleSelections": true, - "EnableNextEpisodeAutoPlay": true, - "CastReceiverId": "F007D354" - }, - "Policy": { - "IsAdministrator": false, - "IsHidden": true, - "EnableCollectionManagement": false, - "EnableSubtitleManagement": false, - "EnableLyricManagement": false, - "IsDisabled": false, - "BlockedTags": [], - "AllowedTags": [], - "EnableUserPreferenceAccess": true, - "AccessSchedules": [], - "BlockUnratedItems": [], - "EnableRemoteControlOfOtherUsers": false, - "EnableSharedDeviceControl": false, - "EnableRemoteAccess": true, - "EnableLiveTvManagement": false, - "EnableLiveTvAccess": true, - "EnableMediaPlayback": true, - "EnableAudioPlaybackTranscoding": true, - "EnableVideoPlaybackTranscoding": true, - "EnablePlaybackRemuxing": true, - "ForceRemoteSourceTranscoding": false, - "EnableContentDeletion": false, - "EnableContentDeletionFromFolders": [], - "EnableContentDownloading": true, - "EnableSyncTranscoding": true, - "EnableMediaConversion": true, - "EnabledDevices": [], - "EnableAllDevices": true, - "EnabledChannels": [], - "EnableAllChannels": false, - "EnabledFolders": [], - "EnableAllFolders": true, - "InvalidLoginAttemptCount": 0, - "LoginAttemptsBeforeLockout": -1, - "MaxActiveSessions": 0, - "EnablePublicSharing": true, - "BlockedMediaFolders": [], - "BlockedChannels": [], - "RemoteClientBitrateLimit": 0, - "AuthenticationProviderId": "Jellyfin.Server.Implementations.Users.DefaultAuthenticationProvider", - "PasswordResetProviderId": "Jellyfin.Server.Implementations.Users.DefaultPasswordResetProvider", - "SyncPlayAccess": "CreateAndJoinGroups" - } - }) -} |