fix(Remote playback): kludge to scrub after stream move
🏗️ Build and Test JellyTau / Run Tests (pull_request) Successful in 3m51s
Traceability Validation / Check Requirement Traces (pull_request) Successful in 20s
🏗️ Build and Test JellyTau / Build Android APK (pull_request) Successful in 18m18s

This commit is contained in:
2026-06-25 21:31:39 +02:00
parent 2811e1b7ca
commit 6836ce79c8
11 changed files with 227 additions and 40 deletions
+2 -2
View File
@@ -303,8 +303,8 @@ async playbackModeIsTransferring() : Promise<boolean> {
/**
* Transfer playback from local device to a remote Jellyfin session
*/
async playbackModeTransferToRemote(sessionId: string) : Promise<null> {
return await TAURI_INVOKE("playback_mode_transfer_to_remote", { sessionId });
async playbackModeTransferToRemote(sessionId: string, position: number | null) : Promise<null> {
return await TAURI_INVOKE("playback_mode_transfer_to_remote", { sessionId, position });
},
/**
* Get remote session status (for polling position/duration)
@@ -1,7 +1,9 @@
<!-- TRACES: UR-010 | JA-021, JA-025 | DR-037 -->
<script lang="ts">
import { get } from "svelte/store";
import { sessions, controllableSessions, selectedSession } from "$lib/stores";
import { playbackMode, isTransferring, transferError } from "$lib/stores/playbackMode";
import { playbackPosition } from "$lib/stores/player";
import type { Session } from "$lib/api/types";
interface Props {
@@ -14,8 +16,8 @@
async function handleSessionSelect(session: Session) {
try {
// Transfer playback to remote session
await playbackMode.transferToRemote(session.id);
// Transfer playback to remote session, resuming at our current position
await playbackMode.transferToRemote(session.id, get(playbackPosition));
if (onSelectSession) {
onSelectSession(session);
+10 -3
View File
@@ -10,7 +10,7 @@
import { type UnlistenFn } from "@tauri-apps/api/event";
import { commands, events, type PlayerStatusEvent, type SleepTimerMode } from "$lib/api/bindings";
import { player, playbackPosition } from "$lib/stores/player";
import { player, playbackPosition, currentMedia } from "$lib/stores/player";
import { queue, currentQueueItem } from "$lib/stores/queue";
import { playbackMode } from "$lib/stores/playbackMode";
import { sleepTimer } from "$lib/stores/sleepTimer";
@@ -163,9 +163,16 @@ async function handleStateChanged(state: string, _mediaId: string | null): Promi
}
if (state === "playing" && currentItem) {
// Use 0 for position/duration - will be updated by position_update events
// Preserve the current position when the same track is already loaded
// (e.g. resuming from pause, or a spurious PlaybackRestart). Only reset
// to 0 when switching to a different track. position_update events keep
// it fresh either way, but resetting unconditionally caused the time to
// flash to 0:00 on pause/resume.
const previous = get(currentMedia);
const isSameTrack = previous?.id === currentItem.id;
const startPosition = isSameTrack ? get(playbackPosition) : 0;
const initialDuration = currentItem.runTimeTicks ? currentItem.runTimeTicks / 10000000 : 0;
player.setPlaying(currentItem, 0, initialDuration);
player.setPlaying(currentItem, startPosition, initialDuration);
// Trigger preloading of upcoming tracks in the background
preloadUpcomingTracks().catch((e) => {
+14 -3
View File
@@ -175,10 +175,20 @@ function createMusicStore() {
const genres = await repo.getGenres(libraryId);
if (genres.length === 0) return;
const hasCounts = genres.some(g => g.albumCount != null);
// Counts are only *useful* if they actually differentiate genres. Some
// servers return Fields=ItemCounts but populate every genre with the same
// value (or 0), which leaves the list in its original alphabetical order
// after "ranking" — so diversity selection seeds on the first genre and we
// get a wall of A-genres ("avangard", "avan-gard", ...) with no Rock.
// Treat that as "no usable counts" and fall through to the probe path,
// which ranks by genres' real album counts instead.
const positiveCounts = genres
.map(g => g.albumCount)
.filter((c): c is number => c != null && c > 0);
const hasUsefulCounts = new Set(positiveCounts).size > 1;
let genreRows: GenreRow[];
if (hasCounts) {
if (hasUsefulCounts) {
// Rank by reported count, pick a diverse subset, then fetch only those.
const ranked = [...genres].sort(
(a, b) => (b.albumCount ?? 0) - (a.albumCount ?? 0)
@@ -188,7 +198,8 @@ function createMusicStore() {
row => row.items.length > 0
);
} else {
// No counts (offline, or a server that ignores Fields=ItemCounts).
// No usable counts (offline, a server that ignores Fields=ItemCounts,
// or one that returns uniform/zero counts — see hasUsefulCounts above).
// The genre list is alphabetical, so probing the first N would only
// ever surface A-genres. Sample at an even stride across the whole
// list instead, so the probe pool spans A→Z; then drop empties, rank
+1
View File
@@ -167,6 +167,7 @@ describe("playbackMode store", () => {
expect(mockInvoke).toHaveBeenCalledWith("playback_mode_transfer_to_remote", {
sessionId: "session-456",
position: null,
});
});
+13 -3
View File
@@ -73,7 +73,10 @@ function createPlaybackModeStore() {
* - Polls remote session until track loads
* - Stops local playback
*/
async function transferToRemote(sessionId: string | null | undefined): Promise<void> {
async function transferToRemote(
sessionId: string | null | undefined,
currentPosition?: number,
): Promise<void> {
console.log("[PlaybackMode] Transferring to remote session:", sessionId);
update((s) => ({ ...s, isTransferring: true, transferError: null }));
@@ -88,10 +91,17 @@ function createPlaybackModeStore() {
};
try {
// Pass the caller's current local position so the remote resumes where we
// are. The backend can't reliably read this itself: on Linux video plays in
// the HTML5 <video> element and the MPV backend reports 0. A null override
// falls back to the backend position (correct for Linux audio via MPV).
const positionOverride =
currentPosition !== undefined && currentPosition > 0 ? currentPosition : null;
// Rust handles everything - just wait for it to complete
// It includes its own 5-second timeout for track loading
console.log("[PlaybackMode] About to invoke playback_mode_transfer_to_remote with sessionId:", sessionId);
await commands.playbackModeTransferToRemote(sessionId ?? "");
console.log("[PlaybackMode] About to invoke playback_mode_transfer_to_remote with sessionId:", sessionId, "position:", positionOverride);
await commands.playbackModeTransferToRemote(sessionId ?? "", positionOverride);
console.log("[PlaybackMode] Invoke completed successfully");
if (aborted) {