/* 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::ui::{ account::{login_logic, session::Session}, assets::rocket_uri_macro_r_item_poster, error::MyResult, node::{aspect_class, DatabaseNodeUserDataExt}, }; use anyhow::Context; use jellybase::{database::Database, CONF}; use jellycommon::{user::WatchedState, NodeID, NodeKind}; use rocket::{get, post, response::Redirect, serde::json::Json, State}; use serde::Deserialize; use serde_json::{json, Value}; use std::net::IpAddr; const SERVER_ID: &'static str = "1694a95daf70708147f16103ce7b7566"; const USER_ID: &'static str = "33f772aae6c2495ca89fe00340dbd17c"; const VERSION: &'static str = "10.10.0"; #[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": "http://127.0.0.1:8000", "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": "/config", "WebPath": "/jellyfin/jellyfin-web", "ItemsByNamePath": "/var/lib/jellyfin/metadata", "CachePath": "/var/cache/jellyfin", "LogPath": "/config/log", "InternalMetadataPath": "/var/lib/jellyfin/metadata", "TranscodingTempPath": "/var/lib/jellyfin/transcodes", "CastReceiverApplications": [], "HasUpdateAvailable": false, "EncoderLocation": "System", "SystemArchitecture": "X64", "LocalAddress": "http://127.0.0.1:8000", "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("/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)?; items.push(json!({ "Name": n.title, "ServerId": SERVER_ID, "Id": n.slug.clone(), "Etag": "blob", "DateCreated": "0001-01-01T00:00:00.0000000Z", "CanDelete": false, "CanDownload": false, "PreferredMetadataLanguage": "", "PreferredMetadataCountryCode": "", "SortName": n.slug.clone(), "ForcedSortName": "", "ExternalUrls": [], "Path": "/why/does/it/matter", "EnableMediaSourceDisplay": true, "CustomRating": "", "ChannelId": null, "Overview": "", "Taglines": [], "Genres": [], "PlayAccess": "Full", "RemoteTrailers": [], "ProviderIds": {}, "IsFolder": true, "ParentId": "todo-parent", // TODO "Type": match n.kind { NodeKind::Collection | _ => "CollectionFolder", }, "People": [], "Studios": [], "GenreItems": [], "LocalTrailerCount": 0, "UserData": { "PlaybackPositionTicks": 0, "PlayCount": if ud.watched == WatchedState::Watched { 1 } else { 0 }, "IsFavorite": ud.rating > 0, "Played": ud.watched == WatchedState::Watched, "Key": "7a2175bc-cb1f-1a94-152c-bd2b2bae8f6d", "ItemId": "00000000000000000000000000000000" }, "ChildCount": 2, "SpecialFeatureCount": 0, "DisplayPreferencesId": n.slug.clone(), "Tags": [], "PrimaryImageAspectRatio": match aspect_class(n.kind) { "aspect-thumb" => 16. / 9., "aspect-land" => 2f32.sqrt(), "aspect-port" => 1. / 2f32.sqrt(), "aspect-square" | _ => 1., }, "CollectionType": "movies", "ImageTags": { "Primary": "the-image" }, "BackdropImageTags": [], "LocationType": "FileSystem", "MediaType": "Unknown", "LockedFields": [], "LockData": false })) } 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("/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 }))) } 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" } }) }