package app import ( "errors" "fmt" "net/http" "strconv" "strings" "time" "unicode/utf8" ) type followingView struct { followingEntry TotalEpisode int `json:"totalEpisode"` NewEpisodes int `json:"newEpisodes"` } func followingEpisodeCount(drama Drama) int { for _, value := range []any{drama.TotalEpisode, drama.TotalEpisodeSnake, drama.ChapterCount, drama.ChapterCountSnake, drama.EpisodeCount, drama.EpisodeCountSnake, drama.Total, drama.Episodes} { count, err := strconv.Atoi(strings.TrimSpace(fmt.Sprint(value))) if err == nil && count > 0 && count <= 10000 { return count } } return 0 } func followingDramaEntry(drama Drama) followingEntry { id, source, valid := playbackHistoryIdentity(drama.ID) if !valid { return followingEntry{} } return followingEntry{DramaID: id, Source: source, Title: followingText(drama.DisplayTitle(), 4096), Category: followingText(firstNonEmpty(drama.CategoryName, drama.CategoryNameSnake, drama.Category, drama.TypeName, drama.SortName), 512), KnownEpisodes: followingEpisodeCount(drama)} } func followingText(value string, limit int) string { value = strings.ToValidUTF8(strings.TrimSpace(value), "") if len(value) > limit { value = value[:limit] for !utf8.ValidString(value) { value = value[:len(value)-1] } } return value } func (app *UIApp) followingMetadata(ids []string) map[string]followingEntry { result := make(map[string]followingEntry, len(ids)) if len(ids) == 0 { return result } wanted := make(map[string]bool, len(ids)) for _, id := range ids { wanted[id] = true } selected := make([]Drama, 0, len(ids)) app.mu.Lock() for _, drama := range app.dramas { if wanted[drama.ID] { selected = append(selected, drama) } } app.mu.Unlock() for _, drama := range selected { entry := followingDramaEntry(drama) if entry.DramaID != "" { result[entry.DramaID] = entry } } return result } func (app *UIApp) handleFollowing(writer http.ResponseWriter, request *http.Request) { if request.Method == http.MethodPost { app.handleFollowingUpdate(writer, request) return } if !playbackRequestAllowed(writer, request, http.MethodGet) { return } viewer := requestViewer(writer, request) if viewer == nil { return } entries, err := viewer.followingStore().list() if err != nil { writeJSON(writer, http.StatusInternalServerError, map[string]string{"error": "无法读取追剧清单:" + publicError(err).Error()}) return } ids := make([]string, 0, len(entries)) for _, entry := range entries { ids = append(ids, entry.DramaID) } known := app.followingMetadata(ids) result := make([]followingView, 0, len(entries)) for _, entry := range entries { if !dramaAllowed(request.Context(), entry.DramaID, entry.Source) { continue } view := followingView{followingEntry: entry, TotalEpisode: entry.KnownEpisodes} if current, exists := known[entry.DramaID]; exists { view.Title = current.Title view.Category = current.Category if current.KnownEpisodes > 0 { view.TotalEpisode = current.KnownEpisodes if entry.KnownEpisodes > 0 && current.KnownEpisodes > entry.KnownEpisodes { view.NewEpisodes = current.KnownEpisodes - entry.KnownEpisodes } } } result = append(result, view) } writeJSON(writer, http.StatusOK, map[string]any{"data": result, "limit": followingLimit}) } func (app *UIApp) handleFollowingUpdate(writer http.ResponseWriter, request *http.Request) { var input struct { DramaID string `json:"dramaId"` Saved *bool `json:"saved"` Completed *bool `json:"completed"` Acknowledge bool `json:"acknowledge"` } if !readPlaybackRequest(writer, request, &input) { return } id, _, valid := playbackHistoryIdentity(input.DramaID) if !valid || input.Saved == nil && input.Completed == nil && !input.Acknowledge { writeJSON(writer, http.StatusBadRequest, map[string]string{"error": "追剧操作或剧集 ID 无效"}) return } viewer := requestViewer(writer, request) if viewer == nil { return } if !app.requireDramaSources(writer, request, []string{id}) { return } metadata := app.followingMetadata([]string{id})[id] if metadata.DramaID == "" { if history, exists := viewer.playbackHistory().get(id); exists { metadata = followingEntry{DramaID: id, Source: history.Source, Title: followingText(history.Title, 4096), KnownEpisodes: history.Total} } } entry, err := viewer.followingStore().update(id, func(previous followingEntry, exists bool) (followingEntry, error) { if !exists { if metadata.DramaID == "" { return followingEntry{}, errFollowingUnknown } previous = metadata previous.AddedAt = time.Now() } if metadata.DramaID != "" { previous.Title, previous.Category = metadata.Title, metadata.Category if metadata.KnownEpisodes > 0 && (input.Acknowledge || input.Completed != nil && *input.Completed || previous.KnownEpisodes == 0) { previous.KnownEpisodes = metadata.KnownEpisodes } } if input.Saved != nil { previous.Saved = *input.Saved } if input.Completed != nil { previous.Completed = *input.Completed } previous.UpdatedAt = time.Now() return previous, nil }) if err != nil { status := http.StatusInternalServerError if errors.Is(err, errFollowingUnknown) { status = http.StatusNotFound } else if errors.Is(err, errFollowingLimit) { status = http.StatusConflict } writeJSON(writer, status, map[string]string{"error": "追剧操作未保存:" + publicError(err).Error()}) return } app.notifyEmbySync() writeJSON(writer, http.StatusOK, map[string]any{"ok": true, "entry": entry}) }