fix(player): simplify autoplay to redirect and ensure episodes are sorted
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -62,6 +63,10 @@ func (h *Handler) HandleWatchPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sort.Slice(episodes.Data, func(i, j int) bool {
|
||||||
|
return episodes.Data[i].MalID < episodes.Data[j].MalID
|
||||||
|
})
|
||||||
|
|
||||||
user, _ := r.Context().Value(ctxpkg.UserKey).(*database.User)
|
user, _ := r.Context().Value(ctxpkg.UserKey).(*database.User)
|
||||||
|
|
||||||
currentEpID := r.URL.Query().Get("ep")
|
currentEpID := r.URL.Query().Get("ep")
|
||||||
|
|||||||
149
static/player.ts
149
static/player.ts
@@ -134,9 +134,7 @@ const initPlayer = (): void => {
|
|||||||
let isScrubbing = false
|
let isScrubbing = false
|
||||||
let lastKnownVolume = 1
|
let lastKnownVolume = 1
|
||||||
let pendingSeekTime: number | null = null
|
let pendingSeekTime: number | null = null
|
||||||
let preloadedNextEpisodeData: EpisodeData | null = null
|
let activeSkipSegment: { type: string, start: number, end: number } | null = null
|
||||||
let preloadAttemptedForEpisode: number | null = null
|
|
||||||
let activeSkipSegment: { type: string, start: number, end: number } | null = null
|
|
||||||
let activeSubtitles: Array<{ start: number, end: number, text: string }> = []
|
let activeSubtitles: Array<{ start: number, end: number, text: string }> = []
|
||||||
let currentSubtitleTracks: Array<{ lang: string, label: string, url: string }> = []
|
let currentSubtitleTracks: Array<{ lang: string, label: string, url: string }> = []
|
||||||
|
|
||||||
@@ -803,39 +801,7 @@ const initPlayer = (): void => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const preloadNextEpisode = async (): Promise<void> => {
|
|
||||||
const currentEpNum = Number.parseInt(currentEpisode, 10)
|
|
||||||
if (Number.isNaN(currentEpNum)) return
|
|
||||||
|
|
||||||
const nextEpisode = currentEpNum + 1
|
|
||||||
if (totalEpisodes > 0 && nextEpisode > totalEpisodes) return
|
|
||||||
if (preloadAttemptedForEpisode === nextEpisode) return
|
|
||||||
|
|
||||||
preloadAttemptedForEpisode = nextEpisode
|
|
||||||
|
|
||||||
if (!Number.isInteger(malID) || malID <= 0) return
|
|
||||||
|
|
||||||
const url = `/api/watch/episode/${malID}/${nextEpisode}`
|
|
||||||
try {
|
|
||||||
const resp = await fetch(url)
|
|
||||||
if (resp.ok) {
|
|
||||||
const data = await resp.json() as EpisodeData
|
|
||||||
preloadedNextEpisodeData = data
|
|
||||||
|
|
||||||
// Prefetch the m3u8 playlist to the browser cache
|
|
||||||
const streamMode = data.initial_mode
|
|
||||||
const modeSource = data.mode_sources[streamMode]
|
|
||||||
const token = modeSource?.token || data.token
|
|
||||||
if (token) {
|
|
||||||
const streamURL = container.getAttribute('data-stream-url') || '/watch/proxy/stream'
|
|
||||||
const src = `${streamURL}?mode=${encodeURIComponent(streamMode)}&token=${encodeURIComponent(token)}`
|
|
||||||
fetch(src).catch(() => {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const goToNextEpisode = async (): Promise<void> => {
|
const goToNextEpisode = async (): Promise<void> => {
|
||||||
const currentEpNum = Number.parseInt(currentEpisode, 10)
|
const currentEpNum = Number.parseInt(currentEpisode, 10)
|
||||||
@@ -851,118 +817,13 @@ const initPlayer = (): void => {
|
|||||||
const nextEpisode = currentEpNum + 1
|
const nextEpisode = currentEpNum + 1
|
||||||
markEpisodeTransition(nextEpisode)
|
markEpisodeTransition(nextEpisode)
|
||||||
|
|
||||||
await loadNextEpisodeInPlace(Number(malID), nextEpisode)
|
sessionStorage.setItem('mal:autoplay-next', 'true')
|
||||||
}
|
|
||||||
|
|
||||||
const loadNextEpisodeInPlace = async (animeID: number, nextEpisode: number): Promise<void> => {
|
|
||||||
if (!Number.isInteger(animeID) || animeID <= 0) return
|
|
||||||
|
|
||||||
let data = preloadedNextEpisodeData
|
|
||||||
if (!data || preloadAttemptedForEpisode !== nextEpisode) {
|
|
||||||
const url = `/api/watch/episode/${animeID}/${nextEpisode}`
|
|
||||||
try {
|
|
||||||
const resp = await fetch(url)
|
|
||||||
if (!resp.ok) return
|
|
||||||
data = await resp.json() as EpisodeData
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) return
|
|
||||||
|
|
||||||
// Update URL without reloading
|
|
||||||
const newUrl = new URL(window.location.href)
|
const newUrl = new URL(window.location.href)
|
||||||
newUrl.searchParams.set('ep', String(nextEpisode))
|
newUrl.searchParams.set('ep', String(nextEpisode))
|
||||||
window.history.pushState({}, '', newUrl.toString())
|
window.location.href = newUrl.toString()
|
||||||
|
|
||||||
// Highlight the current episode in the UI
|
|
||||||
const episodeLinks = document.querySelectorAll('a[href*="ep="]')
|
|
||||||
episodeLinks.forEach(link => {
|
|
||||||
const linkUrl = new URL((link as HTMLAnchorElement).href)
|
|
||||||
if (linkUrl.searchParams.get('ep') === String(nextEpisode)) {
|
|
||||||
link.classList.add('ring-accent', 'ring-2')
|
|
||||||
} else {
|
|
||||||
link.classList.remove('ring-accent', 'ring-2')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const container = document.querySelector('[data-video-player]') as HTMLElement | null
|
|
||||||
if (!container) return
|
|
||||||
|
|
||||||
const video = container.querySelector('video') as HTMLVideoElement | null
|
|
||||||
if (!video) return
|
|
||||||
|
|
||||||
// Update component state
|
|
||||||
currentEpisode = String(nextEpisode)
|
|
||||||
totalEpisodes = data.total_episodes
|
|
||||||
|
|
||||||
// We must update the module level variables, not just the DOM attributes
|
|
||||||
modeSources = data.mode_sources
|
|
||||||
availableModes = data.available_modes || []
|
|
||||||
currentMode = availableModes.includes(data.initial_mode) ? data.initial_mode : (availableModes[0] || 'dub')
|
|
||||||
const fallbackMode = Object.keys(modeSources).find((m) => typeof modeSources[m]?.token === 'string' && modeSources[m].token !== '')
|
|
||||||
if ((!modeSources[currentMode] || !modeSources[currentMode].token) && fallbackMode) {
|
|
||||||
currentMode = fallbackMode
|
|
||||||
}
|
|
||||||
|
|
||||||
container.setAttribute('data-current-episode', currentEpisode)
|
|
||||||
container.setAttribute('data-mal-id', String(animeID))
|
|
||||||
container.setAttribute('data-total-episodes', String(totalEpisodes))
|
|
||||||
container.setAttribute('data-start-time-seconds', '0')
|
|
||||||
container.setAttribute('data-initial-mode', data.initial_mode)
|
|
||||||
container.setAttribute('data-stream-token', data.token)
|
|
||||||
container.setAttribute('data-available-modes', JSON.stringify(availableModes))
|
|
||||||
container.setAttribute('data-mode-sources', JSON.stringify(modeSources))
|
|
||||||
container.setAttribute('data-segments', JSON.stringify(data.segments))
|
|
||||||
|
|
||||||
// Reset preloader for next time
|
|
||||||
preloadedNextEpisodeData = null
|
|
||||||
preloadAttemptedForEpisode = null
|
|
||||||
|
|
||||||
const newStreamURL = container.getAttribute('data-stream-url') || '/watch/proxy/stream'
|
|
||||||
const modeSource = modeSources[currentMode]
|
|
||||||
|
|
||||||
if (modeSource?.token) {
|
|
||||||
video.src = `${newStreamURL}?mode=${encodeURIComponent(currentMode)}&token=${encodeURIComponent(modeSource.token)}`
|
|
||||||
} else if (data.token) {
|
|
||||||
video.src = `${newStreamURL}?mode=${encodeURIComponent(currentMode)}&token=${encodeURIComponent(data.token)}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
video.load()
|
|
||||||
if (isAutoplayEnabled()) {
|
|
||||||
video.play().catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedSegments = (data.segments || [])
|
|
||||||
.map((segment: SkipSegment) => {
|
|
||||||
const start = Number(segment.start || 0)
|
|
||||||
const end = Number(segment.end || 0)
|
|
||||||
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const rawType = String(segment.type || '').toLowerCase()
|
|
||||||
const type = rawType === 'ed' || rawType === 'outro' ? 'ed' : 'op'
|
|
||||||
return { type, start: Math.max(0, start), end: Math.max(0, end) }
|
|
||||||
})
|
|
||||||
.filter((s: unknown): s is { type: string, start: number, end: number } => s !== null)
|
|
||||||
.sort((a: { start: number }, b: { start: number }) => a.start - b.start)
|
|
||||||
|
|
||||||
activeSegments = []
|
|
||||||
resolveActiveSegments()
|
|
||||||
renderSegments()
|
|
||||||
updateSubtitleOptions()
|
|
||||||
updateModeButtons(data.initial_mode)
|
|
||||||
updateVideoOverlay(String(nextEpisode), data.episode_title)
|
|
||||||
|
|
||||||
const nextUrl = `/watch/${animeID}/${nextEpisode}`
|
|
||||||
window.history.replaceState(null, '', nextUrl)
|
|
||||||
|
|
||||||
const episodesList = document.getElementById('episodes-list')
|
|
||||||
if (episodesList) {
|
|
||||||
htmx.ajax('GET', `/api/anime/${animeID}/episodes?current=${nextEpisode}`, episodesList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const completeAnime = async (episodeNumber: number): Promise<void> => {
|
const completeAnime = async (episodeNumber: number): Promise<void> => {
|
||||||
if (completionSent) return
|
if (completionSent) return
|
||||||
|
|||||||
Reference in New Issue
Block a user