Split software arch desc for easier manintenance. Many fixes related to next video playing and remote playback
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 12s
🏗️ Build and Test JellyTau / Build Android APK (push) Has been skipped
Traceability Validation / Check Requirement Traces (push) Failing after 1s

This commit is contained in:
2026-03-01 19:47:46 +01:00
parent 3a9c126dfe
commit 09780103a7
45 changed files with 5663 additions and 3332 deletions
+39 -16
View File
@@ -4,27 +4,27 @@
* Handles user interactions with the next episode popup.
* Backend manages countdown logic and autoplay decisions.
*
* Navigation uses goto() directly to load the next episode.
* The player page's $effect detects the URL param change and
* calls loadAndPlay for the new episode.
*
* TRACES: UR-023 | DR-047, DR-048
*/
import { cancelAutoplayCountdown, playNextEpisode } from "$lib/api/autoplay";
import { goto } from "$app/navigation";
import { cancelAutoplayCountdown } from "$lib/api/autoplay";
import { nextEpisode } from "$lib/stores/nextEpisode";
import type { MediaItem } from "$lib/api/types";
/** Guard against double-navigation */
let isNavigating = false;
/**
* Cleanup next episode state (called on unmount/destroy)
*/
export function cleanup() {
nextEpisode.reset();
}
/**
* Handle episode ended event
* Backend now handles autoplay decisions via on_playback_ended()
* This function is kept for backwards compatibility but does nothing
*/
export async function handleEpisodeEnded(media: any) {
// Backend now handles this - no action needed
// The backend will emit ShowNextEpisodePopup event
isNavigating = false;
}
/**
@@ -36,13 +36,36 @@ export async function cancelAutoPlay() {
nextEpisode.hidePopup();
}
/**
* Navigate to the next episode via goto().
* Uses replaceState to prevent history buildup when auto-advancing.
*/
function navigateToEpisode(episode: MediaItem) {
if (isNavigating) {
console.warn("[NextEpisode] Already navigating, skipping duplicate navigation to", episode.id);
return;
}
isNavigating = true;
console.log("[NextEpisode] Navigating to next episode:", episode.id, episode.name);
nextEpisode.hidePopup();
goto(`/player/${episode.id}`, { replaceState: true }).finally(() => {
isNavigating = false;
});
}
/**
* Manually play the next episode
* Called when user clicks "Play Now" button on next episode popup
*
* @param nextEpisodeItem - The next episode to play
*/
export async function watchNextManually(nextEpisodeItem: any) {
await playNextEpisode(nextEpisodeItem);
nextEpisode.hidePopup();
export async function watchNextManually(nextEpisodeItem: MediaItem) {
await cancelAutoplayCountdown();
navigateToEpisode(nextEpisodeItem);
}
/**
* Auto-play the next episode when countdown reaches 0
* Called by playerEvents when countdown_tick event has remaining_seconds: 0
*/
export function autoPlayNext(nextEpisodeItem: MediaItem) {
navigateToEpisode(nextEpisodeItem);
}
+11 -1
View File
@@ -14,7 +14,8 @@ import { player, playbackPosition } from "$lib/stores/player";
import { queue, currentQueueItem } from "$lib/stores/queue";
import { playbackMode } from "$lib/stores/playbackMode";
import { sleepTimer } from "$lib/stores/sleepTimer";
import { nextEpisode } from "$lib/stores/nextEpisode";
import { nextEpisode, nextEpisodeItem as nextEpisodeItemStore } from "$lib/stores/nextEpisode";
import { autoPlayNext } from "$lib/services/nextEpisodeService";
import { preloadUpcomingTracks } from "$lib/services/preload";
import type { MediaItem } from "$lib/api/types";
import { get } from "svelte/store";
@@ -320,8 +321,17 @@ function handleShowNextEpisodePopup(
/**
* Handle countdown tick event.
* When countdown reaches 0, automatically trigger playback of the next episode.
*/
function handleCountdownTick(remainingSeconds: number): void {
// Update next episode store with new countdown value
nextEpisode.updateCountdown(remainingSeconds);
// Auto-play when countdown reaches 0
if (remainingSeconds === 0) {
const episode = get(nextEpisodeItemStore);
if (episode) {
autoPlayNext(episode);
}
}
}
+33 -1
View File
@@ -28,7 +28,13 @@ export type SyncOperation =
| "unmark_favorite"
| "update_progress"
| "report_playback_start"
| "report_playback_stopped";
| "report_playback_stopped"
| "playlist_create"
| "playlist_delete"
| "playlist_rename"
| "playlist_add_items"
| "playlist_remove_items"
| "playlist_reorder_item";
/**
* Simplified sync service - handles offline mutation queueing
@@ -164,6 +170,32 @@ class SyncService {
return deleted;
}
// ===== Playlist sync operations =====
async queuePlaylistCreate(playlistId: string, name: string, itemIds: string[]): Promise<number> {
return this.queueMutation("playlist_create", playlistId, { name, itemIds });
}
async queuePlaylistDelete(playlistId: string): Promise<number> {
return this.queueMutation("playlist_delete", playlistId);
}
async queuePlaylistRename(playlistId: string, name: string): Promise<number> {
return this.queueMutation("playlist_rename", playlistId, { name });
}
async queuePlaylistAddItems(playlistId: string, itemIds: string[]): Promise<number> {
return this.queueMutation("playlist_add_items", playlistId, { itemIds });
}
async queuePlaylistRemoveItems(playlistId: string, entryIds: string[]): Promise<number> {
return this.queueMutation("playlist_remove_items", playlistId, { entryIds });
}
async queuePlaylistReorderItem(playlistId: string, itemId: string, newIndex: number): Promise<number> {
return this.queueMutation("playlist_reorder_item", playlistId, { itemId, newIndex });
}
/**
* Clear all sync operations for the current user (called during logout)
*