From c8b189d0e7262434de4d401cd6db4f8a62455a94 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Fri, 15 May 2026 00:23:22 +0200 Subject: [PATCH] feat: preserve fullscreen when autoplaying next episode --- internal/playback/handler/handler.go | 60 ++++++++++++++++++++++++++++ static/player/episodes/nav.ts | 10 +++-- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/internal/playback/handler/handler.go b/internal/playback/handler/handler.go index 317f1b4..e9f1387 100644 --- a/internal/playback/handler/handler.go +++ b/internal/playback/handler/handler.go @@ -37,6 +37,7 @@ func (h *PlaybackHandler) Register(r *gin.Engine) { r.GET("/anime/:id/watch", h.HandleWatchPage) r.POST("/api/watch-progress", h.HandleSaveProgress) r.POST("/api/watch-complete", h.HandleWatchComplete) + r.GET("/api/watch/episode/:animeId/:episode", h.HandleEpisodeData) r.GET("/api/watch/thumbnails/:animeId", h.HandleEpisodeThumbnails) r.GET("/watch/proxy/stream", h.HandleProxyStream) r.GET("/watch/proxy/subtitle", h.HandleProxySubtitle) @@ -78,6 +79,65 @@ func (h *PlaybackHandler) HandleWatchPage(c *gin.Context) { c.HTML(http.StatusOK, "watch.gohtml", responseData) } +// HandleEpisodeData returns the minimal payload needed to advance to the next +// episode without a full page reload (preserves fullscreen). +func (h *PlaybackHandler) HandleEpisodeData(c *gin.Context) { + animeID, err := strconv.Atoi(c.Param("animeId")) + if err != nil || animeID <= 0 { + c.Status(http.StatusBadRequest) + return + } + + episode := c.Param("episode") + if episode == "" { + c.Status(http.StatusBadRequest) + return + } + + mode := c.DefaultQuery("mode", "sub") + + user, _ := c.Get("User") + userID := "" + if u, ok := user.(*domain.User); ok { + userID = u.ID + } + + data, err := h.svc.BuildWatchData(c.Request.Context(), animeID, []string{}, episode, mode, userID) + if err != nil { + c.Status(http.StatusInternalServerError) + return + } + + watchData, _ := data["WatchData"].(map[string]any) + if watchData == nil { + c.Status(http.StatusInternalServerError) + return + } + + modeSources := watchData["ModeSources"] + availableModes, _ := watchData["AvailableModes"].([]string) + segments := watchData["Segments"] + + // Try to resolve a title for this episode from the episode list. + episodeTitle := "" + if eps, ok := watchData["Episodes"].([]domain.EpisodeData); ok { + epNum, _ := strconv.Atoi(episode) + for _, e := range eps { + if e.MalID == epNum { + episodeTitle = e.Title + break + } + } + } + + c.JSON(http.StatusOK, gin.H{ + "mode_sources": modeSources, + "available_modes": availableModes, + "segments": segments, + "episode_title": episodeTitle, + }) +} + func (h *PlaybackHandler) HandleSaveProgress(c *gin.Context) { user, _ := c.Get("User") userID := "" diff --git a/static/player/episodes/nav.ts b/static/player/episodes/nav.ts index 94d2774..ce3f5b4 100644 --- a/static/player/episodes/nav.ts +++ b/static/player/episodes/nav.ts @@ -28,7 +28,9 @@ export const goToNextEpisode = async (): Promise => { markEpisodeTransition(nextEp); try { - const res = await fetch(`/api/watch/episode/${state.malID}/${nextEp}`); + const res = await fetch( + `/api/watch/episode/${state.malID}/${nextEp}?mode=${encodeURIComponent(state.currentMode)}` + ); if (!res.ok) { // fallback: full page navigation sessionStorage.setItem('mal:autoplay-next', 'true'); @@ -53,12 +55,14 @@ export const goToNextEpisode = async (): Promise => { return; } - // load new video - state.video.src = `${state.streamURL}?mode=${encodeURIComponent(fallback)}&token=${encodeURIComponent(state.modeSources[fallback].token)}`; + // load new video (keep preferences) + const preferredQuality = localStorage.getItem('mal:preferred-quality') || 'best'; + state.video.src = `${state.streamURL}?mode=${encodeURIComponent(fallback)}&token=${encodeURIComponent(state.modeSources[fallback].token)}${preferredQuality !== 'best' ? `&quality=${encodeURIComponent(preferredQuality)}` : ''}`; state.video.load(); if (!state.video.paused) state.video.play().catch(() => {}); state.currentEpisode = String(nextEp); + state.currentMode = fallback; state.pendingSeekTime = null; state.completionSent = false; state.completionAttempts = 0;