diff options
author | metamuffin <metamuffin@disroot.org> | 2025-02-03 22:42:13 +0100 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2025-02-03 22:42:13 +0100 |
commit | e43dc75e3cfb950ac0d0308900c20fa292de0c46 (patch) | |
tree | fce989ad0292328166efede8cfb2b769c370ab24 /server/src/routes/compat/jellyfin.rs | |
parent | 11c5be29987912b89fd6d351938d08fe6a561ad2 (diff) | |
download | jellything-e43dc75e3cfb950ac0d0308900c20fa292de0c46.tar jellything-e43dc75e3cfb950ac0d0308900c20fa292de0c46.tar.bz2 jellything-e43dc75e3cfb950ac0d0308900c20fa292de0c46.tar.zst |
some jellyfin api endpoints
Diffstat (limited to 'server/src/routes/compat/jellyfin.rs')
-rw-r--r-- | server/src/routes/compat/jellyfin.rs | 393 |
1 files changed, 393 insertions, 0 deletions
diff --git a/server/src/routes/compat/jellyfin.rs b/server/src/routes/compat/jellyfin.rs new file mode 100644 index 0000000..18f0242 --- /dev/null +++ b/server/src/routes/compat/jellyfin.rs @@ -0,0 +1,393 @@ +/* + 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::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<Value> { + r_jellyfin_system_info_public() +} +#[get("/system/info/public")] +pub fn r_jellyfin_system_info_public() -> Json<Value> { + 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<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": "/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<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("/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)?; + + 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<Value> { + Json(json!({ + "Items": [], + "TotalRecordCount": 0, + "StartIndex": 0 + })) +} + +#[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 + }))) +} + +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" + } + }) +} |