aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--base/src/database.rs4
-rw-r--r--server/src/routes/compat/jellyfin.rs332
-rw-r--r--server/src/routes/mod.rs20
-rw-r--r--server/src/routes/ui/search.rs2
4 files changed, 264 insertions, 94 deletions
diff --git a/base/src/database.rs b/base/src/database.rs
index 3c8bef4..90e42b9 100644
--- a/base/src/database.rs
+++ b/base/src/database.rs
@@ -286,7 +286,7 @@ impl Database {
drop(nodes);
Ok(i)
}
- pub fn search(&self, query: &str, page: usize) -> Result<(usize, Vec<NodeID>)> {
+ pub fn search(&self, query: &str, limit: usize, offset: usize) -> Result<(usize, Vec<NodeID>)> {
let query = QueryParser::for_index(
&self.text_search.index,
vec![self.text_search.title, self.text_search.description],
@@ -295,7 +295,7 @@ impl Database {
.context("parsing query")?;
let searcher = self.text_search.reader.searcher();
- let sres = searcher.search(&query, &TopDocs::with_limit(32).and_offset(page * 32))?;
+ let sres = searcher.search(&query, &TopDocs::with_limit(limit).and_offset(offset))?;
let scount = searcher.search(&query, &Count)?;
let mut results = Vec::new();
diff --git a/server/src/routes/compat/jellyfin.rs b/server/src/routes/compat/jellyfin.rs
index 7815c09..17ddcb6 100644
--- a/server/src/routes/compat/jellyfin.rs
+++ b/server/src/routes/compat/jellyfin.rs
@@ -19,7 +19,7 @@ use jellycommon::{
user::{NodeUserData, WatchedState},
MediaInfo, Node, NodeID, NodeKind, SourceTrack, SourceTrackKind, Visibility,
};
-use rocket::{get, post, response::Redirect, serde::json::Json, State};
+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};
@@ -150,46 +150,137 @@ pub fn r_jellyfin_items_image_primary(
}
#[get("/Users/<uid>/Items/<id>")]
-pub fn r_jellyfin_users_items(
+#[allow(private_interfaces)]
+pub fn r_jellyfin_users_items_item(
session: Session,
database: &State<Database>,
uid: &str,
id: &str,
-) -> MyResult<Json<Value>> {
+) -> MyResult<Json<JellyfinItem>> {
let _ = uid;
let (n, ud) = database.get_node_with_userdata(NodeID::from_slug(id), &session)?;
- Ok(Json(node_object(&n, &ud)))
+ Ok(Json(item_object(&n, &ud)))
}
-#[get("/Users/<uid>/Items?<StartIndex>&<ParentId>&<Limit>")]
-#[allow(non_snake_case)]
-pub fn r_jellyfin_items(
+#[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,
- StartIndex: Option<usize>,
- ParentId: &str,
- Limit: usize,
+ query: JellyfinItemQuery,
) -> MyResult<Json<Value>> {
let _ = uid;
- let children = database.get_node_children(NodeID::from_slug(ParentId))?;
+ 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 mut items = Vec::new();
- for nid in children
- .into_iter()
- .skip(StartIndex.unwrap_or_default())
- .take(Limit)
- {
+
+ 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(node_object(&n, &ud))
+ items.push(item_object(&n, &ud))
}
}
Ok(Json(json!({
"Items": items,
"TotalRecordCount": items.len(),
- "StartIndex": 0
+ "StartIndex": query.start_index.unwrap_or_default()
})))
}
@@ -210,7 +301,7 @@ pub fn r_jellyfin_users_views(
for nid in toplevel {
let (n, ud) = database.get_node_with_userdata(nid, &session)?;
if n.visibility >= Visibility::Reduced {
- items.push(node_object(&n, &ud))
+ items.push(item_object(&n, &ud))
}
}
@@ -386,14 +477,30 @@ pub fn r_jellyfin_users_authenticatebyname(
})))
}
-#[derive(Clone, Serialize)]
+#[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(Clone, Serialize)]
+#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "PascalCase")]
struct JellyfinMediaStream {
codec: String,
@@ -432,21 +539,21 @@ struct JellyfinMediaStream {
localized_external: String,
}
-#[derive(Clone, Serialize)]
+#[derive(Debug, Clone, Serialize)]
enum JellyfinMediaSourceProtocol {
File,
}
-#[derive(Clone, Serialize)]
+#[derive(Debug, Clone, Serialize)]
enum JellyfinMediaSourceType {
Default,
}
-#[derive(Clone, Serialize)]
+#[derive(Debug, Clone, Serialize)]
enum JellyfinVideoType {
VideoFile,
}
-#[derive(Clone, Serialize)]
+#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "PascalCase")]
struct JellyfinMediaSource {
protocol: JellyfinMediaSourceProtocol,
@@ -614,77 +721,136 @@ fn media_source_object(node: &Node, m: &MediaInfo) -> JellyfinMediaSource {
}
}
-fn node_object(node: &Node, userdata: &NodeUserData) -> Value {
+#[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<String>,
+ overview: String,
+ taglines: Vec<String>,
+ genres: Vec<()>,
+ play_access: Option<String>,
+ 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<String>,
+ user_data: Value,
+ display_preferences_id: String,
+ primary_image_aspect_ratio: f64,
+ collection_type: String,
+ image_tags: BTreeMap<String, String>,
+ backdrop_image_tags: Vec<()>,
+ location_type: Option<String>,
+ media_type: String,
+ video_type: Option<String>,
+ container: Option<String>,
+ run_time_ticks: Option<i64>,
+ media_sources: Option<Vec<JellyfinMediaSource>>,
+ media_streams: Option<Vec<JellyfinMediaStream>>,
+ path: Option<String>,
+}
+
+fn item_object(node: &Node, userdata: &NodeUserData) -> JellyfinItem {
let media_source = node.media.as_ref().map(|m| media_source_object(node, m));
- json!({
- "Name": node.title,
- "ServerId": SERVER_ID,
- "Id": node.slug.clone(),
- "Etag": "blob",
- "DateCreated": "0001-01-01T00:00:00.0000000Z",
- "CanDelete": false,
- "CanDownload": true,
- "PreferredMetadataLanguage": "",
- "PreferredMetadataCountryCode": "",
- "SortName": node.slug.clone(),
- "ForcedSortName": "",
- "ExternalUrls": [],
- "EnableMediaSourceDisplay": true,
- "CustomRating": "",
- "ChannelId": null,
- "Overview": "",
- "Taglines": [],
- "Genres": [],
- "PlayAccess": "Full",
- "RemoteTrailers": [],
- "ProviderIds": {},
- "IsFolder": !node.media.is_some(),
- "ParentId": "todo-parent", // TODO
- "Type": match node.kind {
- NodeKind::Movie | NodeKind::Video | NodeKind::ShortFormVideo => "Movie",
- NodeKind::Collection | _ => "CollectionFolder",
+ 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": [],
- "Studios": [],
- "GenreItems": [],
- "LocalTrailerCount": 0,
- "SpecialFeatureCount": 0,
- "ChildCount": 0,
- "LockedFields": [],
- "LockData": false,
- "Tags": [],
- "UserData": {
+ 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"
- },
- "DisplayPreferencesId": node.slug.clone(),
- "PrimaryImageAspectRatio": match aspect_class(node.kind) {
+ }),
+ display_preferences_id: node.slug.clone(),
+ primary_image_aspect_ratio: match aspect_class(node.kind) {
"aspect-thumb" => 16. / 9.,
- "aspect-land" => 2f32.sqrt(),
- "aspect-port" => 1. / 2f32.sqrt(),
+ "aspect-land" => 2f64.sqrt(),
+ "aspect-port" => 1. / 2f64.sqrt(),
"aspect-square" | _ => 1.,
},
- "CollectionType": "unknown",
- "ImageTags": {
- "Primary": "the-image"
+ 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()
},
- "BackdropImageTags": [],
- "LocationType": "FileSystem",
- "MediaType": if node.media.is_some() { "Video" } else { "Unknown" },
- "VideoType": node.media.as_ref().map(|_| "VideoFile"),
- "LocationType": node.media.as_ref().map(|_| "FileSystem"),
- "PlayAccess": node.media.as_ref().map(|_| "Full"),
- "Container": node.media.as_ref().map(|_| "mkv"),
- "RunTimeTicks": if let Some(m) = &node.media { Some((m.duration * 10_000_000.) as i64) } else { None },
- "MediaSources": media_source.as_ref().map(|s|vec![s.clone()]),
- "MediaStreams": media_source.as_ref().map(|s|s.media_streams.clone()),
- "Path": node.media.as_ref().map(|_|format!("/path/to/{}.mkv", node.slug)),
- })
+ 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 {
diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs
index 93a832f..373146b 100644
--- a/server/src/routes/mod.rs
+++ b/server/src/routes/mod.rs
@@ -9,16 +9,17 @@ use api::{r_api_account_login, r_api_asset_token_raw, r_api_root, r_api_version}
use base64::Engine;
use compat::{
jellyfin::{
- r_jellyfin_branding_configuration, r_jellyfin_branding_css,
+ r_jellyfin_artists, r_jellyfin_branding_configuration, r_jellyfin_branding_css,
r_jellyfin_displaypreferences_usersettings, r_jellyfin_items,
r_jellyfin_items_image_primary, r_jellyfin_items_intros, r_jellyfin_items_playbackinfo,
- r_jellyfin_livetv_programs_recommended, r_jellyfin_playback_bitratetest,
- r_jellyfin_quickconnect_enabled, r_jellyfin_sessions_capabilities_full,
- r_jellyfin_sessions_playing, r_jellyfin_sessions_playing_progress, r_jellyfin_socket,
- r_jellyfin_system_endpoint, r_jellyfin_system_info, r_jellyfin_system_info_public,
- r_jellyfin_system_info_public_case, r_jellyfin_users_authenticatebyname,
- r_jellyfin_users_id, r_jellyfin_users_items, r_jellyfin_users_public,
- r_jellyfin_users_views, r_jellyfin_video_stream,
+ r_jellyfin_livetv_programs_recommended, r_jellyfin_persons,
+ r_jellyfin_playback_bitratetest, r_jellyfin_quickconnect_enabled,
+ r_jellyfin_sessions_capabilities_full, r_jellyfin_sessions_playing,
+ r_jellyfin_sessions_playing_progress, r_jellyfin_socket, r_jellyfin_system_endpoint,
+ r_jellyfin_system_info, r_jellyfin_system_info_public, r_jellyfin_system_info_public_case,
+ r_jellyfin_users_authenticatebyname, r_jellyfin_users_id, r_jellyfin_users_items,
+ r_jellyfin_users_items_item, r_jellyfin_users_public, r_jellyfin_users_views,
+ r_jellyfin_video_stream,
},
youtube::{r_youtube_channel, r_youtube_watch},
};
@@ -177,7 +178,10 @@ pub fn build_rocket(database: Database, federation: Federation) -> Rocket<Build>
r_jellyfin_items_image_primary,
r_jellyfin_livetv_programs_recommended,
r_jellyfin_users_items,
+ r_jellyfin_users_items_item,
r_jellyfin_items,
+ r_jellyfin_persons,
+ r_jellyfin_artists,
r_jellyfin_items_intros,
r_jellyfin_items_playbackinfo,
r_jellyfin_video_stream,
diff --git a/server/src/routes/ui/search.rs b/server/src/routes/ui/search.rs
index 6b1504f..d020e2e 100644
--- a/server/src/routes/ui/search.rs
+++ b/server/src/routes/ui/search.rs
@@ -18,7 +18,7 @@ pub async fn r_search<'a>(
) -> MyResult<DynLayoutPage<'a>> {
let timing = Instant::now();
let results = if let Some(query) = query {
- let (count, ids) = db.search(query, page.unwrap_or_default())?;
+ let (count, ids) = db.search(query, 32, page.unwrap_or_default() * 32)?;
let mut nodes = ids
.into_iter()
.map(|id| db.get_node_with_userdata(id, &session))