aboutsummaryrefslogtreecommitdiff
path: root/server/src/routes/compat/jellyfin/mod.rs
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-04-27 19:25:11 +0200
committermetamuffin <metamuffin@disroot.org>2025-04-27 19:25:11 +0200
commit11a585b3dbe620dcc8772e713b22f1d9ba80d598 (patch)
tree44f8d97137412aefc79a2425a489c34fa3e5f6c5 /server/src/routes/compat/jellyfin/mod.rs
parentd871aa7c5bba49ff55170b5d2dac9cd440ae7170 (diff)
downloadjellything-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.rs877
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"
- }
- })
-}