/* 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 */ 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 { r_jellyfin_system_info_public() } #[get("/system/info/public")] pub fn r_jellyfin_system_info_public() -> Json { 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 { Json(json!({ "LoginDisclaimer": format!("{} - {}", CONF.brand, CONF.slogan), "CustomCss": "", "SplashscreenEnabled": false, })) } #[get("/users/public")] pub fn r_jellyfin_users_public() -> Json { Json(json!([])) } #[get("/QuickConnect/Enabled")] pub fn r_jellyfin_quickconnect_enabled(_session: Session) -> Json { Json(json!(false)) } #[get("/System/Endpoint")] pub fn r_jellyfin_system_endpoint(_session: Session) -> Json { 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 { 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 { 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/")] pub fn r_jellyfin_users_id(session: Session, id: &str) -> Json { let _ = id; Json(user_object(session.user.name)) } #[get("/Items//Images/Primary?")] #[allow(non_snake_case)] pub fn r_jellyfin_items_image_primary( _session: Session, id: &str, fillWidth: Option, ) -> Redirect { Redirect::permanent(rocket::uri!(r_item_poster(id, fillWidth))) } #[get("/Users//Items/")] #[allow(private_interfaces)] pub fn r_jellyfin_users_items_item( session: Session, database: &State, uid: &str, id: &str, ) -> MyResult> { 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, #[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: Session, database: &State, uid: &str, query: JellyfinItemQuery, ) -> MyResult> { let _ = uid; r_jellyfin_items(session, database, query) } #[get("/Artists?")] #[allow(private_interfaces)] pub fn r_jellyfin_artists( session: Session, database: &State, mut query: JellyfinItemQuery, ) -> MyResult> { query.internal_artists = true; r_jellyfin_items(session, database, query)?; // TODO Ok(Json(json!({ "Items": [], "TotalRecordCount": 0, "StartIndex": 0 }))) } #[get("/Persons?")] #[allow(private_interfaces)] pub fn r_jellyfin_persons( session: Session, database: &State, mut query: JellyfinItemQuery, ) -> MyResult> { query.internal_persons = true; r_jellyfin_items(session, database, query)?; // TODO Ok(Json(json!({ "Items": [], "TotalRecordCount": 0, "StartIndex": 0 }))) } #[get("/Items?")] #[allow(private_interfaces)] pub fn r_jellyfin_items( session: Session, database: &State, query: JellyfinItemQuery, ) -> MyResult> { 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?")] #[allow(non_snake_case)] pub fn r_jellyfin_users_views( session: Session, database: &State, userId: &str, ) -> MyResult> { 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 { Json(json!({ "Items": [], "TotalRecordCount": 0, "StartIndex": 0 })) } #[get("/Users//Items//Intros")] pub fn r_jellyfin_items_intros(_session: Session, uid: &str, id: &str) -> Json { let _ = (uid, id); Json(json!({ "Items": [], "TotalRecordCount": 0, "StartIndex": 0 })) } #[post("/Items//PlaybackInfo")] pub fn r_jellyfin_items_playbackinfo( _session: Session, database: &State, id: &str, ) -> MyResult> { 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//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 = "")] #[allow(private_interfaces)] pub fn r_jellyfin_sessions_playing_progress( session: Session, database: &State, data: Json, ) -> 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?")] #[allow(non_snake_case)] pub fn r_jellyfin_playback_bitratetest(_session: Session, Size: usize) -> Vec { 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 = "")] #[allow(private_interfaces)] pub fn r_jellyfin_users_authenticatebyname( client_addr: IpAddr, database: &State, data: Json, ) -> MyResult> { 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, width: Option, average_frame_rate: Option, real_frame_rate: Option, reference_frame_rate: Option, 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, channels: Option, sample_rate: Option, 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, 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::>(), 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, overview: String, taglines: Vec, genres: Vec<()>, play_access: Option, 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, user_data: Value, display_preferences_id: String, primary_image_aspect_ratio: f64, collection_type: String, image_tags: BTreeMap, backdrop_image_tags: Vec<()>, location_type: Option, media_type: String, video_type: Option, container: Option, run_time_ticks: Option, media_sources: Option>, media_streams: Option>, path: Option, } 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" } }) }