Files
djk/internal/app/ui_playback_history.go

196 lines
6.5 KiB
Go

package app
import (
"errors"
"math"
"net/http"
"time"
)
type playbackHistoryProgress struct {
Run uint64 `json:"run"`
Sequence uint64 `json:"sequence"`
Episode int `json:"episode"`
Position float64 `json:"position"`
Duration float64 `json:"duration"`
Completed bool `json:"completed"`
}
type playbackHistoryRun struct {
episode int
duration float64
}
func (app *UIApp) handlePlaybackHistory(writer http.ResponseWriter, request *http.Request) {
if !playbackRequestAllowed(writer, request, http.MethodGet) {
return
}
viewer := requestViewer(writer, request)
if viewer == nil {
return
}
entries, err := viewer.playbackHistory().list()
if err != nil {
writeJSON(writer, http.StatusInternalServerError, map[string]string{"error": "无法读取观看记录:" + publicError(err).Error()})
return
}
visible := make([]playbackHistoryEntry, 0, len(entries))
for _, entry := range entries {
if dramaAllowed(request.Context(), entry.DramaID, entry.Source) {
visible = append(visible, entry)
}
}
writeJSON(writer, http.StatusOK, map[string]any{"data": visible, "limit": playbackHistoryLimit})
}
func (app *UIApp) handlePlaybackHistoryRemove(writer http.ResponseWriter, request *http.Request) {
var input struct {
DramaID string `json:"dramaId"`
All bool `json:"all"`
}
if !readPlaybackRequest(writer, request, &input) {
return
}
id, _, valid := playbackHistoryIdentity(input.DramaID)
if input.All && input.DramaID != "" || !input.All && !valid {
writeJSON(writer, http.StatusBadRequest, map[string]string{"error": "请选择要删除的观看记录"})
return
}
viewer := requestViewer(writer, request)
if viewer == nil {
return
}
if !input.All && !app.requireDramaSources(writer, request, []string{id}) {
return
}
if err := viewer.playbackHistory().removeForSources(request.Context(), id, input.All); err != nil {
writeJSON(writer, http.StatusInternalServerError, map[string]string{"error": "删除观看记录未能保存:" + publicError(err).Error()})
return
}
writeJSON(writer, http.StatusOK, map[string]bool{"ok": true})
}
func (app *UIApp) handlePlaybackProgress(writer http.ResponseWriter, request *http.Request) {
var input struct {
Session string `json:"session"`
Progress playbackHistoryProgress `json:"progress"`
}
if !readPlaybackRequest(writer, request, &input) {
return
}
if !app.requirePlaybackOwner(writer, request, input.Session) {
return
}
entry, saved, err := app.recordPlaybackProgress(contextViewer(request.Context()), input.Session, input.Progress)
if err != nil {
writeJSON(writer, http.StatusBadRequest, map[string]string{"error": publicError(err).Error()})
return
}
writeJSON(writer, http.StatusOK, map[string]any{"saved": saved, "entry": entry})
}
func (app *UIApp) recordPlaybackProgress(viewer *viewerRecords, id string, progress playbackHistoryProgress) (playbackHistoryEntry, bool, error) {
if progress.Run == 0 || progress.Sequence == 0 || !validPlaybackHistoryTime(progress.Position) || !validPlaybackHistoryTime(progress.Duration) {
return playbackHistoryEntry{}, false, errors.New("观看进度无效")
}
if viewer == nil {
return playbackHistoryEntry{}, false, errors.New("浏览器身份无效")
}
store := viewer.playbackHistory()
app.playbackMu.Lock()
session := app.playbacks[id]
if session == nil || progress.Episode < 1 || progress.Episode > len(session.tasks) {
app.playbackMu.Unlock()
return playbackHistoryEntry{}, false, nil
}
if session.viewer != viewer {
app.playbackMu.Unlock()
return playbackHistoryEntry{}, false, errors.New("播放会话已过期,请重新打开本剧")
}
run, known := session.historyRuns[progress.Run]
if !known || run.episode != progress.Episode || progress.Sequence <= session.historySequence {
app.playbackMu.Unlock()
return playbackHistoryEntry{}, false, nil
}
task := session.tasks[progress.Episode-1]
dramaID, source, valid := playbackHistoryIdentity(task.DramaID)
if !valid {
app.playbackMu.Unlock()
return playbackHistoryEntry{}, false, errors.New("观看记录的站源或剧集 ID 无效")
}
duration := progress.Duration
if run.duration > 0 && validPlaybackHistoryTime(run.duration) {
duration = run.duration
}
position := progress.Position
if duration > 0 {
if position > duration+2 {
app.playbackMu.Unlock()
return playbackHistoryEntry{}, false, errors.New("观看位置超过分集时长")
}
position = math.Min(position, duration)
}
completed := progress.Completed && duration > 0 && position >= math.Max(0, duration-2)
index := task.Index
if index < 1 {
index = progress.Episode
}
total := task.Total
if total < index {
total = index
}
if total < len(session.tasks) {
total = len(session.tasks)
}
entry := playbackHistoryEntry{DramaID: dramaID, Source: source, Title: task.DramaTitle, ChapterID: task.Chapter.ID, Episode: task.Chapter.EpisodeString(index), Index: index, Total: total, Position: math.Round(position*1000) / 1000, Duration: duration, Completed: completed, Mode: "online", WatchedAt: time.Now(), sessionID: id, sessionOpened: session.openedAt}
if len(session.downloadIDs) >= progress.Episode {
entry.Mode, entry.TaskID = "collection", session.downloadIDs[progress.Episode-1]
}
saved, err := store.record(entry, session.openedAt)
if err == nil {
session.historySequence = progress.Sequence
app.touchPlaybackLocked(session)
}
app.playbackMu.Unlock()
if err == nil && saved {
err = store.flush()
}
return entry, saved && err == nil, err
}
func playbackHistoryResume(entry playbackHistoryEntry, tasks []Task, fallback int) (int, float64, bool, string) {
matched := -1
for index, task := range tasks {
if entry.ChapterID != "" && entry.ChapterID == task.Chapter.ID {
matched = index
break
}
}
if matched < 0 {
for index, task := range tasks {
if task.Chapter.EpisodeString(task.Index) == entry.Episode {
matched = index
break
}
}
}
if matched < 0 {
return fallback, 0, false, "当前选集中没有上次观看的分集,请手动选集"
}
if entry.Completed {
if matched+1 < len(tasks) {
return matched + 2, 0, false, "已接续到下一集"
}
message := "已看至最新,可选集重看"
if entry.Index < entry.Total {
message = "已看完此合集当前可播放的分集,可更新合集或从剧库继续观看"
}
return matched + 1, 0, true, message
}
position := entry.Position
if entry.Duration > 0 && position >= entry.Duration {
position = math.Max(0, entry.Duration-1)
}
return matched + 1, position, false, "已恢复上次观看进度"
}