diff options
author | metamuffin <metamuffin@disroot.org> | 2025-04-27 19:25:11 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2025-04-27 19:25:11 +0200 |
commit | 11a585b3dbe620dcc8772e713b22f1d9ba80d598 (patch) | |
tree | 44f8d97137412aefc79a2425a489c34fa3e5f6c5 /server/src/routes/compat/jellyfin/mod.rs | |
parent | d871aa7c5bba49ff55170b5d2dac9cd440ae7170 (diff) | |
download | jellything-11a585b3dbe620dcc8772e713b22f1d9ba80d598.tar jellything-11a585b3dbe620dcc8772e713b22f1d9ba80d598.tar.bz2 jellything-11a585b3dbe620dcc8772e713b22f1d9ba80d598.tar.zst |
move files around
Diffstat (limited to 'server/src/routes/compat/jellyfin/mod.rs')
-rw-r--r-- | server/src/routes/compat/jellyfin/mod.rs | 877 |
1 files changed, 0 insertions, 877 deletions
diff --git a/server/src/routes/compat/jellyfin/mod.rs b/server/src/routes/compat/jellyfin/mod.rs deleted file mode 100644 index e37d7d1..0000000 --- a/server/src/routes/compat/jellyfin/mod.rs +++ /dev/null @@ -1,877 +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> -*/ -pub mod models; - -use crate::routes::ui::{ - account::{login_logic, session::Session}, - assets::{ - rocket_uri_macro_r_asset, rocket_uri_macro_r_item_backdrop, rocket_uri_macro_r_item_poster, - }, - error::MyResult, - node::{aspect_class, DatabaseNodeUserDataExt}, - sort::{filter_and_sort_nodes, FilterProperty, NodeFilterSort, SortOrder, SortProperty}, -}; -use anyhow::{anyhow, Context}; -use jellybase::{database::Database, CONF}; -use jellycommon::{ - stream::{StreamContainer, StreamSpec}, - user::{NodeUserData, WatchedState}, - MediaInfo, Node, NodeID, NodeKind, SourceTrack, SourceTrackKind, Visibility, -}; -use models::*; -use rocket::{ - get, - http::{Cookie, CookieJar}, - post, - response::Redirect, - serde::json::Json, - FromForm, State, -}; -use serde::Deserialize; -use serde_json::{json, Value}; -use std::{collections::BTreeMap, net::IpAddr}; - -const SERVER_ID: &str = "1694a95daf70708147f16103ce7b7566"; -const USER_ID: &str = "33f772aae6c2495ca89fe00340dbd17c"; -const VERSION: &str = "10.10.0"; -const LOCAL_ADDRESS: &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("/Branding/Css")] -pub fn r_jellyfin_branding_css() -> String { - "".to_string() -} - -#[get("/QuickConnect/Enabled")] -pub fn r_jellyfin_quickconnect_enabled() -> 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", - })) -} - -#[post("/DisplayPreferences/usersettings")] -pub fn r_jellyfin_displaypreferences_usersettings_post(_session: Session) {} - -#[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>&<tag>")] -#[allow(non_snake_case)] -pub fn r_jellyfin_items_image_primary( - _session: Session, - id: &str, - fillWidth: Option<usize>, - tag: String, -) -> Redirect { - if tag == "poster" { - Redirect::permanent(rocket::uri!(r_item_poster(id, fillWidth))) - } else { - Redirect::permanent(rocket::uri!(r_asset(tag, fillWidth))) - } -} - -#[get("/Items/<id>/Images/Backdrop/0?<maxWidth>")] -#[allow(non_snake_case)] -pub fn r_jellyfin_items_images_backdrop( - _session: Session, - id: &str, - maxWidth: Option<usize>, -) -> Redirect { - Redirect::permanent(rocket::uri!(r_item_backdrop(id, maxWidth))) -} - -#[get("/Items/<id>")] -#[allow(private_interfaces)] -pub fn r_jellyfin_items_item( - session: Session, - database: &State<Database>, - id: &str, -) -> MyResult<Json<JellyfinItem>> { - let (n, ud) = database.get_node_with_userdata(NodeID::from_slug(id), &session)?; - Ok(Json(item_object(&n, &ud))) -} -#[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; - r_jellyfin_items_item(session, database, id) -} - -#[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 (nodes, parent_kind) = if let Some(q) = query.search_term { - ( - database - .search(&q, query.limit, query.start_index.unwrap_or_default())? - .1, - None, - ) - } else if let Some(parent) = query.parent_id { - let parent = NodeID::from_slug(&parent); - ( - database - .get_node_children(parent)? - .into_iter() - .skip(query.start_index.unwrap_or_default()) - .take(query.limit) - .collect(), - database.get_node(parent)?.map(|n| n.kind), - ) - } else { - (vec![], None) - }; - - let filter_kind = query - .include_item_types - .map(|n| match n.as_str() { - "Movie" => vec![FilterProperty::KindMovie], - "Audio" => vec![FilterProperty::KindMusic], - "Video" => vec![FilterProperty::KindVideo], - "TvChannel" => vec![FilterProperty::KindChannel], - _ => vec![], - }) - .or(if query.internal_artists { - Some(vec![]) - } else { - None - }) - .or(if query.internal_persons { - Some(vec![]) - } else { - None - }); - - let mut nodes = nodes - .into_iter() - .map(|nid| database.get_node_with_userdata(nid, &session)) - .collect::<Result<Vec<_>, anyhow::Error>>()?; - - filter_and_sort_nodes( - &NodeFilterSort { - sort_by: None, - filter_kind, - sort_order: None, - }, - match parent_kind { - Some(NodeKind::Channel) => (SortProperty::ReleaseDate, SortOrder::Descending), - _ => (SortProperty::Title, SortOrder::Ascending), - }, - &mut nodes, - ); - - let items = nodes - .into_iter() - .filter(|(n, _)| n.visibility >= Visibility::Reduced) - .map(|(n, ud)| item_object(&n, &ud)) - .collect::<Vec<_>>(); - - 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 mut toplevel = database - .get_node_children(NodeID::from_slug("library")) - .context("root node missing")? - .into_iter() - .map(|nid| database.get_node_with_userdata(nid, &session)) - .collect::<Result<Vec<_>, anyhow::Error>>()?; - - toplevel.sort_by_key(|(n, _)| n.index.unwrap_or(usize::MAX)); - - let mut items = Vec::new(); - for (n, ud) in toplevel { - if n.visibility >= Visibility::Reduced { - items.push(item_object(&n, &ud)) - } - } - - Ok(Json(json!({ - "Items": items, - "TotalRecordCount": items.len(), - "StartIndex": 0 - }))) -} - -#[get("/Items/<id>/Similar")] -pub fn r_jellyfin_items_similar(_session: Session, id: &str) -> Json<Value> { - let _ = id; - Json(json!({ - "Items": [], - "TotalRecordCount": 0, - "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 - })) -} - -#[get("/Shows/NextUp")] -pub fn r_jellyfin_shows_nextup(_session: Session) -> Json<Value> { - 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.webm")] -pub fn r_jellyfin_video_stream( - _session: Session, - database: &State<Database>, - id: &str, -) -> MyResult<Redirect> { - 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 params = StreamSpec::Remux { - tracks: (0..media.tracks.len()).collect(), - container: StreamContainer::WebM, - } - .to_query(); - Ok(Redirect::temporary(format!("/n/{id}/stream{params}"))) -} - -#[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)] -} - -#[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_case( - client_addr: IpAddr, - database: &State<Database>, - data: Json<AuthData>, - jar: &CookieJar, -) -> MyResult<Json<Value>> { - r_jellyfin_users_authenticatebyname(client_addr, database, data, jar) -} - -#[post("/Users/authenticatebyname", data = "<data>")] -#[allow(private_interfaces)] -pub fn r_jellyfin_users_authenticatebyname( - client_addr: IpAddr, - database: &State<Database>, - data: Json<AuthData>, - jar: &CookieJar, -) -> MyResult<Json<Value>> { - let token = login_logic(database, &data.username, &data.pw, None, None)?; - - // setting the session cookie too because image requests carry no auth headers for some reason. - // TODO find alternative, non-web clients might not understand cookies - jar.add( - Cookie::build(("session", token.clone())) - .permanent() - .build(), - ); - - 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 - }))) -} - -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/{}.webm", node.slug), - r#type: JellyfinMediaSourceType::Default, - container: "webm".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, - } -} - -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_none(), - parent_id: "todo-parent".to_owned(), // TODO - r#type: match node.kind { - NodeKind::Movie | NodeKind::Video | NodeKind::ShortFormVideo => JellyfinItemType::Movie, - NodeKind::Collection => JellyfinItemType::CollectionFolder, - _ => JellyfinItemType::CollectionFolder, - }, - people: node - .people - .iter() - .flat_map(|(_pg, ps)| { - ps.iter().map(|p| JellyfinPerson { - name: p.person.name.clone(), - id: p.person.ids.tmdb.unwrap_or_default().to_string(), - primary_image_tag: p.person.headshot.clone().map(|a| a.0).unwrap_or_default(), - role: p.characters.join(","), - r#type: JellyfinPersonType::Actor, - }) - }) - .collect(), - 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., - _ => 1., - }, - collection_type: "unknown".to_owned(), - image_tags: BTreeMap::from_iter([("Primary".to_string(), "poster".to_string())]), - backdrop_image_tags: vec!["backdrop".to_string()], - 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(|_| "webm".to_owned()), - run_time_ticks: node - .media - .as_ref() - .map(|m| (m.duration * 10_000_000.) as i64), - 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/{}.webm", 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" - } - }) -} |