Add JRay support
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 17m59s
Traceability Validation / Check Requirement Traces (push) Successful in 1m48s
🏗️ Build and Test JellyTau / Build Android APK (push) Has been cancelled

This commit is contained in:
2026-06-28 20:38:58 +02:00
parent 8eae4ae253
commit 0eae81ec59
8 changed files with 198 additions and 3 deletions
+16
View File
@@ -1068,6 +1068,14 @@ async repositoryGetItems(handle: string, parentId: string, options: GetItemsOpti
async repositoryGetItem(handle: string, itemId: string) : Promise<MediaItem> {
return await TAURI_INVOKE("repository_get_item", { handle, itemId });
},
/**
* 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.
*/
async repositoryJrayActorsAt(handle: string, itemId: string, t: number) : Promise<JRayActor[]> {
return await TAURI_INVOKE("repository_jray_actors_at", { handle, itemId, t });
},
/**
* Get latest items in a library
*/
@@ -1553,6 +1561,14 @@ export type ImageOptions = { maxWidth?: number | null; maxHeight?: number | null
* Image type
*/
export type ImageType = "Primary" | "Backdrop" | "Banner" | "Thumb" | "Logo"
/**
* 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 `""`.
*/
export type JRayActor = { name: string; imdb_id?: string; tmdb_id?: string; jellyfin_id?: string }
/**
* Library (media collection)
*/
+10
View File
@@ -3,6 +3,7 @@
// NO direct HTTP calls - everything routes through Rust backend
import { commands } from "./bindings";
import type { JRayActor } from "./bindings";
import type { QualityPreset } from "./quality-presets";
import type {
Library,
@@ -90,6 +91,15 @@ export class RepositoryClient {
return commands.repositoryGetItem(this.ensureHandle(), itemId);
}
/**
* Query the optional JRay plugin for the actors on screen at time `t`
* (seconds) in an item. Resolves to an empty array when JRay isn't installed
* or has no data for the item.
*/
async jrayActorsAt(itemId: string, t: number): Promise<JRayActor[]> {
return commands.repositoryJrayActorsAt(this.ensureHandle(), itemId, t);
}
async getLatestItems(parentId: string, limit?: number): Promise<MediaItem[]> {
return commands.repositoryGetLatestItems(this.ensureHandle(), parentId, limit ?? null);
}
@@ -1,7 +1,9 @@
<!-- TRACES: UR-003, UR-005, UR-020, UR-021, UR-026 | DR-010, DR-023, DR-024 -->
<script lang="ts">
import { onMount, onDestroy, untrack } from "svelte";
import { goto } from "$app/navigation";
import { commands } from "$lib/api/bindings";
import type { JRayActor } from "$lib/api/bindings";
import { listen } from "@tauri-apps/api/event";
import Hls from "hls.js";
import type { MediaItem } from "$lib/api/types";
@@ -750,6 +752,58 @@
}, 5000);
}
// ===== JRay "who's on screen" pause overlay (optional plugin) =====
// On pause, ask the JRay Jellyfin plugin which actors are on screen at the
// current timestamp and show them as a tappable overlay. If the plugin isn't
// installed (or has no data for this item), the call resolves to [] and the
// overlay simply doesn't render. Tapping an actor with a resolved Jellyfin
// Person id navigates to that person's library page.
let jrayActors = $state<JRayActor[]>([]);
// Monotonic token so a slow in-flight request can't overwrite a newer pause
// (or a resume that cleared the list).
let jrayRequestId = 0;
async function fetchJrayActors() {
const itemId = media?.id;
if (!itemId) return;
const token = ++jrayRequestId;
const t = currentTime;
try {
const actors = await auth.getRepository().jrayActorsAt(itemId, t);
// Discard if a newer pause/resume happened while we were waiting.
if (token === jrayRequestId) {
jrayActors = actors;
}
} catch (err) {
console.warn("[VideoPlayer] JRay lookup failed:", err);
if (token === jrayRequestId) jrayActors = [];
}
}
function clearJrayActors() {
jrayRequestId++; // invalidate any in-flight request
jrayActors = [];
}
function openJrayActor(actor: JRayActor) {
if (actor.jellyfin_id) {
goto(`/library/${actor.jellyfin_id}`);
}
}
// Drive the JRay overlay off the single `isPlaying` flag so it works for both
// the HTML5 <video> (desktop) and the native ExoPlayer path (Android, which
// updates isPlaying via the player://state-changed event). Fetch when we go
// paused, clear when we resume. untrack() keeps this from re-running on every
// currentTime tick — only isPlaying transitions matter.
$effect(() => {
if (isPlaying) {
untrack(clearJrayActors);
} else if (isMediaReady) {
untrack(fetchJrayActors);
}
});
function handlePlay() {
isPlaying = true;
startTimeUpdates(); // Start RAF loop for smooth time updates
@@ -1404,6 +1458,29 @@
</svg>
</button>
{/if}
<!-- JRay "who's on screen" overlay: shown while paused when the JRay plugin
returned actors for the current timestamp. Tapping an actor with a
resolved Jellyfin Person id opens their library page. -->
{#if !isPlaying && !isSeeking && jrayActors.length > 0}
<div class="absolute top-4 right-4 max-w-xs bg-black/70 rounded-lg p-3 backdrop-blur-sm pointer-events-auto">
<div class="text-white/60 text-xs font-medium uppercase tracking-wide mb-2">On screen</div>
<div class="flex flex-col gap-1">
{#each jrayActors as actor (actor.name + actor.jellyfin_id)}
{#if actor.jellyfin_id}
<button
class="text-left text-white text-sm hover:text-blue-300 transition-colors"
onclick={() => openJrayActor(actor)}
>
{actor.name}
</button>
{:else}
<span class="text-white/80 text-sm">{actor.name}</span>
{/if}
{/each}
</div>
</div>
{/if}
</div>
<!-- Controls -->