/* 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 */ pub mod models; use crate::{helper::A, ui::error::MyResult}; use anyhow::anyhow; use jellycommon::{ api::{NodeFilterSort, SortOrder, SortProperty}, routes::{u_asset, u_node_slug_backdrop, u_node_slug_poster}, stream::{StreamContainer, StreamSpec}, user::{NodeUserData, WatchedState}, MediaInfo, Node, NodeID, NodeKind, SourceTrack, SourceTrackKind, Visibility, }; use jellylogic::{ login::login_logic, node::{get_node, update_node_userdata_watched_progress}, search::search, session::Session, }; use jellyui::{get_brand, get_slogan, node_page::aspect_class}; use log::warn; use models::*; use rocket::{ get, http::{Cookie, CookieJar}, post, response::Redirect, serde::json::Json, FromForm, }; use serde::Deserialize; use serde_json::{json, Value}; use std::{collections::BTreeMap, net::IpAddr}; // these are both random values. idk what they are for 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"; // TODO #[get("/System/Info/Public")] pub fn r_jellyfin_system_info_public_case() -> Json { r_jellyfin_system_info_public() } #[get("/system/info/public")] pub fn r_jellyfin_system_info_public() -> Json { Json(json!({ "LocalAddress": LOCAL_ADDRESS, "ServerName": get_brand(), "Version": VERSION, "ProductName": "Jellything", "OperatingSystem": "", "Id": SERVER_ID, "StartupWizardCompleted": true, })) } #[get("/Branding/Configuration")] pub fn r_jellyfin_branding_configuration() -> Json { Json(json!({ "LoginDisclaimer": format!("{} - {}", get_brand(), get_slogan()), "CustomCss": "", "SplashscreenEnabled": false, })) } #[get("/users/public")] pub fn r_jellyfin_users_public() -> Json { Json(json!([])) } #[get("/Branding/Css")] pub fn r_jellyfin_branding_css() -> String { "".to_string() } #[get("/QuickConnect/Enabled")] pub fn r_jellyfin_quickconnect_enabled() -> Json { Json(json!(false)) } #[get("/System/Endpoint")] pub fn r_jellyfin_system_endpoint(_session: A) -> Json { Json(json!({ "IsLocal": false, "IsInNetwork": false, })) } use rocket_ws::{Message, Stream, WebSocket}; #[get("/socket")] pub fn r_jellyfin_socket(_session: A, 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: A) -> Json { 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": get_brand(), "Version": VERSION, "OperatingSystem": "", "Id": SERVER_ID })) } #[get("/DisplayPreferences/usersettings")] pub fn r_jellyfin_displaypreferences_usersettings(_session: A) -> Json { 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: A) {} #[get("/Users/")] pub fn r_jellyfin_users_id(session: A, id: &str) -> Json { let _ = id; Json(user_object(session.0.user.name)) } #[get("/Items//Images/Primary?&")] #[allow(non_snake_case)] pub fn r_jellyfin_items_image_primary( _session: A, id: &str, fillWidth: Option, tag: String, ) -> Redirect { if tag == "poster" { Redirect::permanent(u_node_slug_poster(id, fillWidth.unwrap_or(1024))) } else { Redirect::permanent(u_asset(&tag, fillWidth.unwrap_or(1024))) } } #[get("/Items//Images/Backdrop/0?")] #[allow(non_snake_case)] pub fn r_jellyfin_items_images_backdrop( _session: A, id: &str, maxWidth: Option, ) -> Redirect { Redirect::permanent(u_node_slug_backdrop(id, maxWidth.unwrap_or(1024))) } #[get("/Items/")] #[allow(private_interfaces)] pub fn r_jellyfin_items_item(session: A, id: &str) -> MyResult> { let r = get_node( &session.0, NodeID::from_slug(id), false, false, NodeFilterSort::default(), )?; Ok(Json(item_object(&r.node, &r.userdata))) } #[get("/Users//Items/")] #[allow(private_interfaces)] pub fn r_jellyfin_users_items_item( session: A, uid: &str, id: &str, ) -> MyResult> { let _ = uid; r_jellyfin_items_item(session, id) } #[derive(Debug, FromForm)] #[allow(unused)] // TODO struct JellyfinItemQuery { #[field(name = uncased("searchterm"))] search_term: Option, #[field(name = uncased("limit"))] limit: usize, #[field(name = uncased("parentid"))] parent_id: Option, #[field(name = uncased("startindex"))] start_index: Option, #[field(name = uncased("includeitemtypes"))] include_item_types: Option, internal_artists: bool, internal_persons: bool, } #[get("/Users//Items?")] #[allow(private_interfaces)] pub fn r_jellyfin_users_items( session: A, uid: &str, query: JellyfinItemQuery, ) -> MyResult> { let _ = uid; r_jellyfin_items(session, query) } #[get("/Artists?")] #[allow(private_interfaces)] pub fn r_jellyfin_artists( session: A, mut query: JellyfinItemQuery, ) -> MyResult> { query.internal_artists = true; r_jellyfin_items(session, query)?; // TODO Ok(Json(JellyfinItemsResponse::default())) } #[get("/Persons?")] #[allow(private_interfaces)] pub fn r_jellyfin_persons( session: A, mut query: JellyfinItemQuery, ) -> MyResult> { query.internal_persons = true; r_jellyfin_items(session, query)?; // TODO Ok(Json(JellyfinItemsResponse::default())) } #[get("/Items?")] #[allow(private_interfaces)] pub fn r_jellyfin_items( session: A, query: JellyfinItemQuery, ) -> MyResult> { // 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.0)) // .collect::, 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 nodes = if let Some(q) = query.search_term { search(&session.0, &q, query.start_index.map(|x| x / 50))?.results // TODO } else if let Some(parent) = query.parent_id { get_node( &session.0, NodeID::from_slug(&parent), true, false, NodeFilterSort::default(), )? .children } else { warn!("unknown items request"); vec![] }; // TODO reimplemnt filter behaviour let items = nodes .into_iter() .filter(|(n, _)| n.visibility >= Visibility::Reduced) .map(|(n, ud)| item_object(&n, &ud)) .collect::>(); Ok(Json(JellyfinItemsResponse { total_record_count: items.len(), start_index: query.start_index.unwrap_or_default(), items, })) } #[get("/UserViews?")] #[allow(non_snake_case)] pub fn r_jellyfin_users_views(session: A, userId: &str) -> MyResult> { let _ = userId; let items = get_node( &session.0, NodeID::from_slug("library"), false, true, NodeFilterSort { sort_by: Some(SortProperty::Index), sort_order: Some(SortOrder::Ascending), filter_kind: None, }, )? .children .into_iter() .map(|(node, udata)| item_object(&node, &udata)) .collect::>(); Ok(Json(json!({ "Items": items, "TotalRecordCount": items.len(), "StartIndex": 0 }))) } #[get("/Items//Similar")] pub fn r_jellyfin_items_similar(_session: A, id: &str) -> Json { let _ = id; Json(json!({ "Items": [], "TotalRecordCount": 0, "StartIndex": 0 })) } #[get("/LiveTv/Programs/Recommended")] pub fn r_jellyfin_livetv_programs_recommended(_session: A) -> Json { Json(json!({ "Items": [], "TotalRecordCount": 0, "StartIndex": 0 })) } #[get("/Users//Items//Intros")] pub fn r_jellyfin_items_intros(_session: A, uid: &str, id: &str) -> Json { let _ = (uid, id); Json(json!({ "Items": [], "TotalRecordCount": 0, "StartIndex": 0 })) } #[get("/Shows/NextUp")] pub fn r_jellyfin_shows_nextup(_session: A) -> Json { Json(json!({ "Items": [], "TotalRecordCount": 0, "StartIndex": 0 })) } #[post("/Items//PlaybackInfo")] pub fn r_jellyfin_items_playbackinfo(session: A, id: &str) -> MyResult> { let node = get_node( &session.0, NodeID::from_slug(id), false, false, NodeFilterSort::default(), )? .node; 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//stream.webm")] pub fn r_jellyfin_video_stream(session: A, id: &str) -> MyResult { let node = get_node( &session.0, NodeID::from_slug(id), false, false, NodeFilterSort::default(), )? .node; 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 = "")] #[allow(private_interfaces)] pub fn r_jellyfin_sessions_playing_progress( session: A, data: Json, ) -> MyResult<()> { let position = data.position_ticks / 10_000_000.; update_node_userdata_watched_progress(&session.0, NodeID::from_slug(&data.item_id), position)?; Ok(()) } #[post("/Sessions/Playing")] pub fn r_jellyfin_sessions_playing(_session: A) {} #[get("/Playback/BitrateTest?")] #[allow(non_snake_case)] pub fn r_jellyfin_playback_bitratetest(_session: A, Size: usize) -> Vec { vec![0; Size.min(1_000_000)] } #[post("/Sessions/Capabilities/Full")] pub fn r_jellyfin_sessions_capabilities_full(_session: A) {} #[derive(Deserialize)] #[serde(rename_all = "PascalCase")] struct AuthData { pw: String, username: String, } #[post("/Users/AuthenticateByName", data = "")] #[allow(private_interfaces)] pub fn r_jellyfin_users_authenticatebyname_case( client_addr: IpAddr, data: Json, jar: &CookieJar, ) -> MyResult> { r_jellyfin_users_authenticatebyname(client_addr, data, jar) } #[post("/Users/authenticatebyname", data = "")] #[allow(private_interfaces)] pub fn r_jellyfin_users_authenticatebyname( client_addr: IpAddr, data: Json, jar: &CookieJar, ) -> MyResult> { let token = login_logic(&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::Subtitle => 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::>(), 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" } }) }