Add JRay support
This commit is contained in:
@@ -179,6 +179,23 @@ pub async fn repository_get_item(
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Query the optional JRay plugin for the actors on screen at time `t`
|
||||
/// (seconds) in an item. Returns an empty list when JRay isn't installed or
|
||||
/// has no data for the item, so the caller can render nothing without error.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn repository_jray_actors_at(
|
||||
manager: State<'_, RepositoryManagerWrapper>,
|
||||
handle: String,
|
||||
item_id: String,
|
||||
t: f64,
|
||||
) -> Result<Vec<crate::repository::JRayActor>, String> {
|
||||
let repo = manager.0.get(&handle).ok_or("Repository not found")?;
|
||||
repo.as_ref().get_jray_actors(&item_id, t)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
/// Get latest items in a library
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
|
||||
@@ -96,7 +96,7 @@ use commands::{
|
||||
storage_save_series_audio_preference, storage_get_series_audio_preference,
|
||||
// Repository commands
|
||||
repository_create, repository_destroy, repository_get_libraries, repository_get_items,
|
||||
repository_get_item, repository_get_latest_items, repository_get_resume_items,
|
||||
repository_get_item, repository_jray_actors_at, repository_get_latest_items, repository_get_resume_items,
|
||||
repository_get_next_up_episodes, repository_get_recently_played_audio, repository_get_resume_movies,
|
||||
repository_get_rediscover_albums,
|
||||
repository_get_genres, repository_search, repository_get_playback_info,
|
||||
@@ -634,6 +634,7 @@ fn specta_builder() -> Builder<tauri::Wry> {
|
||||
repository_get_libraries,
|
||||
repository_get_items,
|
||||
repository_get_item,
|
||||
repository_jray_actors_at,
|
||||
repository_get_latest_items,
|
||||
repository_get_resume_items,
|
||||
repository_get_next_up_episodes,
|
||||
@@ -676,7 +677,6 @@ fn specta_builder() -> Builder<tauri::Wry> {
|
||||
])
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
/// Configure GStreamer (the media backend behind WebKitGTK's HTML5 `<video>`
|
||||
/// element on Linux) to prefer hardware-accelerated VAAPI decoding when the
|
||||
/// host provides it, falling back to software decoding otherwise.
|
||||
@@ -752,6 +752,7 @@ fn set_env_if_unset(key: &str, value: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
// Initialize logger
|
||||
env_logger::Builder::from_default_env()
|
||||
|
||||
@@ -45,6 +45,12 @@ impl HybridRepository {
|
||||
self.online.download_bytes(url).await
|
||||
}
|
||||
|
||||
/// Query the JRay plugin for actors on screen at time `t`. Online-only
|
||||
/// (the plugin lives on the Jellyfin server); empty when JRay isn't present.
|
||||
pub async fn get_jray_actors(&self, item_id: &str, t: f64) -> Result<Vec<super::JRayActor>, RepoError> {
|
||||
self.online.get_jray_actors(item_id, t).await
|
||||
}
|
||||
|
||||
/// Get video stream URL with optional seeking support.
|
||||
/// This method is online-only since offline playback uses local file paths.
|
||||
pub async fn get_video_stream_url(
|
||||
|
||||
@@ -4,7 +4,7 @@ pub mod offline;
|
||||
pub mod hybrid;
|
||||
|
||||
pub use types::*;
|
||||
pub use online::OnlineRepository;
|
||||
pub use online::{OnlineRepository, JRayActor};
|
||||
pub use offline::OfflineRepository;
|
||||
pub use hybrid::HybridRepository;
|
||||
|
||||
|
||||
@@ -11,6 +11,30 @@ use crate::connectivity::ConnectivityReporter;
|
||||
use crate::jellyfin::HttpClient;
|
||||
use super::{MediaRepository, types::*};
|
||||
|
||||
/// A single actor returned by the JRay plugin's "context at time t" endpoint.
|
||||
///
|
||||
/// Mirrors the `actors[]` objects from `GET /Plugins/JRay/Items/{id}/jray?t=`.
|
||||
/// `jellyfin_id` (a Jellyfin Person item GUID) is preferred for navigation;
|
||||
/// the IMDb/TMDb ids are informational fallbacks. Unknown ids are `""`.
|
||||
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JRayActor {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub imdb_id: String,
|
||||
#[serde(default)]
|
||||
pub tmdb_id: String,
|
||||
#[serde(default)]
|
||||
pub jellyfin_id: String,
|
||||
}
|
||||
|
||||
/// Envelope returned by the JRay `jray?t=` endpoint. Extra keys (future
|
||||
/// `locations`, `trivia`, …) are ignored so the client tolerates schema growth.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct JRayContext {
|
||||
#[serde(default)]
|
||||
actors: Vec<JRayActor>,
|
||||
}
|
||||
|
||||
/// Online repository - fetches data from Jellyfin server via HTTP
|
||||
pub struct OnlineRepository {
|
||||
http_client: Arc<HttpClient>,
|
||||
@@ -104,6 +128,20 @@ impl OnlineRepository {
|
||||
.map_err(|e| format!("Failed to read bytes: {}", e))
|
||||
}
|
||||
|
||||
/// Query the JRay plugin for the actors on screen at time `t` (seconds) in
|
||||
/// the given item. Returns an empty list when the plugin isn't installed or
|
||||
/// has no truth data for the item (HTTP 404), so callers can treat "no JRay"
|
||||
/// and "nobody on screen" identically. Other failures propagate.
|
||||
pub async fn get_jray_actors(&self, item_id: &str, t: f64) -> Result<Vec<JRayActor>, RepoError> {
|
||||
let endpoint = format!("/Plugins/JRay/Items/{}/jray?t={}", item_id, t);
|
||||
match self.get_json::<JRayContext>(&endpoint).await {
|
||||
Ok(context) => Ok(context.actors),
|
||||
// No plugin / no truth data for this item — not an error to the user.
|
||||
Err(RepoError::NotFound { .. }) => Ok(Vec::new()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Make authenticated GET request
|
||||
async fn get_json<T: for<'de> Deserialize<'de>>(&self, endpoint: &str) -> Result<T, RepoError> {
|
||||
let result = self.get_json_inner(endpoint).await;
|
||||
@@ -1958,4 +1996,34 @@ mod tests {
|
||||
assert_eq!(urlencoding::encode("Star Wars"), "Star%20Wars");
|
||||
assert_eq!(urlencoding::encode("Tom & Jerry"), "Tom%20%26%20Jerry");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_jray_context_deserializes_actors() {
|
||||
// The jray?t= envelope as documented in the JRay truth file format.
|
||||
let json = r#"{
|
||||
"actors": [
|
||||
{ "name": "Tom Hanks", "imdb_id": "nm0000158", "tmdb_id": "31", "jellyfin_id": "abc123-guid" }
|
||||
]
|
||||
}"#;
|
||||
let ctx: JRayContext = serde_json::from_str(json).expect("should parse");
|
||||
assert_eq!(ctx.actors.len(), 1);
|
||||
assert_eq!(ctx.actors[0].name, "Tom Hanks");
|
||||
assert_eq!(ctx.actors[0].jellyfin_id, "abc123-guid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_jray_context_ignores_unknown_keys_and_missing_ids() {
|
||||
// Future fields (locations/trivia) must be ignored, and absent id keys
|
||||
// must default to "" rather than failing to parse.
|
||||
let json = r#"{
|
||||
"actors": [ { "name": "Extra" } ],
|
||||
"locations": ["Beach"],
|
||||
"trivia": "filmed in 1994"
|
||||
}"#;
|
||||
let ctx: JRayContext = serde_json::from_str(json).expect("should tolerate extra keys");
|
||||
assert_eq!(ctx.actors.len(), 1);
|
||||
assert_eq!(ctx.actors[0].name, "Extra");
|
||||
assert_eq!(ctx.actors[0].imdb_id, "");
|
||||
assert_eq!(ctx.actors[0].jellyfin_id, "");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user