diff --git a/internal/handler/watch_history.go b/internal/handler/watch_history.go index af94923..f4019ff 100644 --- a/internal/handler/watch_history.go +++ b/internal/handler/watch_history.go @@ -27,6 +27,10 @@ import ( // historyListHandler returns the caller's history rows joined with the // matching media in a single response. +// +// include_media=0 只返回历史行本身。客户端判断「继续播放」指向哪一集时只 +// 需要 media_id/position_ms;带上整条 media 会让 300 条的响应从 ~40KB 涨到 +// ~420KB,手机链路上光传输就要十几秒,详情页的续播解析会被它拖住。 func historyListHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { uid, _ := c.Get(middleware.CtxUserID) @@ -34,6 +38,12 @@ func historyListHandler(svc *service.Container) gin.HandlerFunc { if limit <= 0 || limit > 500 { limit = 50 } + includeMedia := true + if raw := strings.TrimSpace(c.Query("include_media")); raw != "" { + if parsed, err := strconv.ParseBool(raw); err == nil { + includeMedia = parsed + } + } items, err := svc.Playback.RecentHistory(c.Request.Context(), toString(uid), limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) @@ -42,7 +52,11 @@ func historyListHandler(svc *service.Container) gin.HandlerFunc { visibility := mediaVisibilityForRequest(c, svc) filtered := make([]service.HistoryItem, 0, len(items)) for _, item := range items { + // 可见性仍然按 media 判断,只是决定要不要把它写进响应。 if item.Media == nil || visibility.Allows(item.Media) { + if !includeMedia { + item.Media = nil + } filtered = append(filtered, item) } } diff --git a/internal/handler/watch_history_test.go b/internal/handler/watch_history_test.go index cf0d06a..41ebf1f 100644 --- a/internal/handler/watch_history_test.go +++ b/internal/handler/watch_history_test.go @@ -103,3 +103,75 @@ func TestHistoryContinueRemovesMissingMedia(t *testing.T) { t.Fatalf("stale history rows remaining = %d, want 0", staleCount) } } + +// include_media=0 只是不把 media 写进响应,行数和可见性判断都不变。 +func TestHistoryListCanOmitEmbeddedMedia(t *testing.T) { + gin.SetMode(gin.TestMode) + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatal(err) + } + if err := db.AutoMigrate(&model.PlaybackHistory{}, &model.Media{}); err != nil { + t.Fatal(err) + } + + media := model.Media{ + Base: model.Base{ID: "media-1"}, + Title: "影片", + Path: "/media/one.mkv", + } + if err := db.Create(&media).Error; err != nil { + t.Fatal(err) + } + if err := db.Create(&model.PlaybackHistory{ + Base: model.Base{ID: "history-1"}, + UserID: "user-1", + MediaID: media.ID, + PositionMs: 30_000, + DurationMs: 120_000, + WatchedAt: time.Now(), + }).Error; err != nil { + t.Fatal(err) + } + + repos := repository.New(db) + svc := &service.Container{ + Log: zap.NewNop(), + Repo: repos, + Playback: service.NewPlaybackService(zap.NewNop(), repos), + } + router := gin.New() + router.Use(func(c *gin.Context) { + c.Set(middleware.CtxUserID, "user-1") + c.Next() + }) + router.GET("/watch-history", historyListHandler(svc)) + + type listRow struct { + MediaID string `json:"media_id"` + Media *model.Media `json:"media"` + } + + full := httptest.NewRecorder() + router.ServeHTTP(full, httptest.NewRequest(http.MethodGet, "/watch-history?limit=10", nil)) + var fullRows []listRow + if err := json.Unmarshal(full.Body.Bytes(), &fullRows); err != nil { + t.Fatal(err) + } + if len(fullRows) != 1 || fullRows[0].Media == nil || fullRows[0].Media.ID != media.ID { + t.Fatalf("default response should embed media: %s", full.Body.String()) + } + + lean := httptest.NewRecorder() + router.ServeHTTP(lean, httptest.NewRequest(http.MethodGet, "/watch-history?limit=10&include_media=0", nil)) + var leanRows []listRow + if err := json.Unmarshal(lean.Body.Bytes(), &leanRows); err != nil { + t.Fatal(err) + } + if len(leanRows) != 1 || leanRows[0].MediaID != media.ID { + t.Fatalf("lean response should keep the row: %s", lean.Body.String()) + } + if leanRows[0].Media != nil { + t.Fatalf("include_media=0 must not embed media: %s", lean.Body.String()) + } +}