From 83deffcf4510cc7917e584a5b9be4c4f0cf84872 Mon Sep 17 00:00:00 2001 From: truewhile <779943132@qq.com> Date: Tue, 15 Sep 2026 15:21:43 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/emby_auth.go | 7 ++ internal/handler/emby_items_handlers.go | 41 ++----- internal/handler/emby_playback.go | 10 +- internal/handler/emby_playstate_handlers.go | 10 +- internal/handler/emby_views_handlers.go | 5 +- internal/service/emby_compat.go | 4 + internal/service/emby_items_detail.go | 26 +++-- internal/service/emby_user_data.go | 118 +++++++++++++------- internal/service/emby_user_data_test.go | 101 ++++++++++++++++- internal/service/favorites_sync.go | 32 +----- 10 files changed, 217 insertions(+), 137 deletions(-) diff --git a/internal/handler/emby_auth.go b/internal/handler/emby_auth.go index f960355..9ab16a6 100644 --- a/internal/handler/emby_auth.go +++ b/internal/handler/emby_auth.go @@ -26,6 +26,13 @@ func embyUserID(c *gin.Context) string { return "" } +// embyEffectiveUserID 返回当前鉴权用户。收藏、续播、播放进度等用户态数据必须 +// 绑定到登录身份;路径/Query 里的 UserId 只是 Emby URL 形态兼容,不能覆盖鉴权结果, +// 否则多用户会串到同一个 UserId 上。 +func embyEffectiveUserID(c *gin.Context) string { + return embyUserID(c) +} + const embyCompatSessionTTL = 30 * time.Minute type embyCompatSession struct { diff --git a/internal/handler/emby_items_handlers.go b/internal/handler/emby_items_handlers.go index 55d2bda..f2b2f61 100644 --- a/internal/handler/emby_items_handlers.go +++ b/internal/handler/emby_items_handlers.go @@ -13,13 +13,7 @@ import ( func parseEmbyItemsParams(c *gin.Context) service.ItemsParams { limit, _ := strconv.Atoi(embyFirstNonEmptyString(firstQueryValue(c, "Limit", "limit"), "50")) offset, _ := strconv.Atoi(embyFirstNonEmptyString(firstQueryValue(c, "StartIndex", "startIndex", "startindex"), "0")) - uid := c.Param("userId") - if uid == "" { - uid = firstQueryValue(c, "UserId", "userId", "userid") - } - if uid == "" { - uid = embyUserID(c) - } + uid := embyEffectiveUserID(c) splitOpt := func(s string) []string { if s == "" { return nil @@ -73,10 +67,7 @@ func embyItemsHandler(svc *service.Container) gin.HandlerFunc { func embyItemByIDHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") - uid := c.Param("userId") - if uid == "" { - uid = embyUserID(c) - } + uid := embyEffectiveUserID(c) out, err := svc.Emby.Item(c.Request.Context(), id, uid) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) @@ -106,13 +97,7 @@ func embyUserItemByIDHandler(svc *service.Container) gin.HandlerFunc { func embyLatestItemsHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { - uid := c.Param("userId") - if uid == "" { - uid = firstQueryValue(c, "UserId", "userId", "userid") - } - if uid == "" { - uid = embyUserID(c) - } + uid := embyEffectiveUserID(c) limit, _ := strconv.Atoi(embyFirstNonEmptyString(firstQueryValue(c, "Limit", "limit"), "20")) out, err := svc.Emby.LatestItems(c.Request.Context(), uid, firstQueryValue(c, "ParentId", "parentId", "parentid"), limit) if err != nil { @@ -126,13 +111,7 @@ func embyLatestItemsHandler(svc *service.Container) gin.HandlerFunc { func embyResumeItemsHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { - uid := c.Param("userId") - if uid == "" { - uid = firstQueryValue(c, "UserId", "userId", "userid") - } - if uid == "" { - uid = embyUserID(c) - } + uid := embyEffectiveUserID(c) limit, _ := strconv.Atoi(embyFirstNonEmptyString(firstQueryValue(c, "Limit", "limit"), "20")) out, err := svc.Emby.ResumeItems(c.Request.Context(), uid, limit) if err != nil { @@ -147,13 +126,7 @@ func embyResumeItemsHandler(svc *service.Container) gin.HandlerFunc { func embyItemsCountsHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { if svc != nil && svc.Emby != nil { - uid := firstQueryValue(c, "UserId", "userId") - if uid == "" { - uid = c.Param("userId") - } - if uid == "" { - uid = embyUserID(c) - } + uid := embyEffectiveUserID(c) out, err := svc.Emby.ItemCounts(c.Request.Context(), uid) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) @@ -209,7 +182,7 @@ func embySaveDisplayPreferencesHandler(_ *service.Container) gin.HandlerFunc { func embyShowSeasonsHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { params := service.ItemsParams{ - UserID: firstQueryValue(c, "UserId", "userId"), + UserID: embyEffectiveUserID(c), ParentID: c.Param("id"), Limit: 500, } @@ -230,7 +203,7 @@ func embyShowEpisodesHandler(svc *service.Container) gin.HandlerFunc { parentID = c.Param("id") } params := service.ItemsParams{ - UserID: firstQueryValue(c, "UserId", "userId"), + UserID: embyEffectiveUserID(c), ParentID: parentID, IncludeItemTypes: []string{"Episode"}, Recursive: true, diff --git a/internal/handler/emby_playback.go b/internal/handler/emby_playback.go index af26ab2..5102ce2 100644 --- a/internal/handler/emby_playback.go +++ b/internal/handler/emby_playback.go @@ -13,10 +13,7 @@ import ( func embyPlaybackInfoHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { - uid := c.Param("userId") - if uid == "" { - uid = embyUserID(c) - } + uid := embyEffectiveUserID(c) out, err := svc.Emby.PlaybackInfo(c.Request.Context(), c.Param("id"), uid) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) @@ -47,10 +44,7 @@ func embySubtitleStreamHandler(svc *service.Container) gin.HandlerFunc { } return } - uid := c.Param("userId") - if uid == "" { - uid = embyUserID(c) - } + uid := embyEffectiveUserID(c) ctx := c.Request.Context() // The official-format route carries a :format suffix (Stream.ass / // Stream.vtt); prefer it for the Content-Type when present, otherwise diff --git a/internal/handler/emby_playstate_handlers.go b/internal/handler/emby_playstate_handlers.go index f3bb1e7..70affd7 100644 --- a/internal/handler/emby_playstate_handlers.go +++ b/internal/handler/emby_playstate_handlers.go @@ -92,10 +92,7 @@ func embyPlayingProgressHandler(svc *service.Container) gin.HandlerFunc { func embyFavoriteHandler(svc *service.Container, fav bool) gin.HandlerFunc { return func(c *gin.Context) { - uid := c.Param("userId") - if uid == "" { - uid = embyUserID(c) - } + uid := embyEffectiveUserID(c) mid := c.Param("itemId") if uid == "" || mid == "" { c.Status(http.StatusBadRequest) @@ -116,10 +113,7 @@ func embyFavoriteHandler(svc *service.Container, fav bool) gin.HandlerFunc { func embyMarkPlayedHandler(svc *service.Container, played bool) gin.HandlerFunc { return func(c *gin.Context) { - uid := c.Param("userId") - if uid == "" { - uid = embyUserID(c) - } + uid := embyEffectiveUserID(c) mid := c.Param("itemId") if uid == "" || mid == "" { c.Status(http.StatusBadRequest) diff --git a/internal/handler/emby_views_handlers.go b/internal/handler/emby_views_handlers.go index 093963b..2296b1d 100644 --- a/internal/handler/emby_views_handlers.go +++ b/internal/handler/emby_views_handlers.go @@ -10,10 +10,7 @@ import ( func embyViewsHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { - uid := c.Param("userId") - if uid == "" { - uid = embyUserID(c) - } + uid := embyEffectiveUserID(c) out, err := svc.Emby.Views(c.Request.Context(), uid) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) diff --git a/internal/service/emby_compat.go b/internal/service/emby_compat.go index 78b1f0c..a6204dd 100644 --- a/internal/service/emby_compat.go +++ b/internal/service/emby_compat.go @@ -205,6 +205,10 @@ func (e *EmbyService) Items(ctx context.Context, p ItemsParams) (map[string]any, if containsEmbyFilter(p.Filters, "IsFavorite") { return e.favoriteItems(ctx, p) } + // 继续观看必须走 MeBox 本地 PlaybackHistory,不能转发到远程共用账号。 + if containsEmbyFilter(p.Filters, "IsResumable") { + return e.resumableItems(ctx, p) + } mountID, _, _ := DecodeEmbyRemoteID(p.ParentID) mount, acct, _ := e.remote.ResolveMount(ctx, mountID) if mount == nil || acct == nil { diff --git a/internal/service/emby_items_detail.go b/internal/service/emby_items_detail.go index 13e894c..6748dcd 100644 --- a/internal/service/emby_items_detail.go +++ b/internal/service/emby_items_detail.go @@ -38,14 +38,6 @@ func (e *EmbyService) Item(ctx context.Context, mediaID, userID string) (map[str if err := e.mergeRemoteUserData(ctx, userID, out); err != nil { return nil, err } - if favorite, _ := IsUserFavorite(ctx, e.repo, userID, mediaID); favorite { - userData, _ := out["UserData"].(map[string]any) - if userData == nil { - userData = map[string]any{} - out["UserData"] = userData - } - userData["IsFavorite"] = true - } return out, nil } if lib, err := e.repo.Library.FindByID(ctx, mediaID); err != nil { @@ -466,12 +458,26 @@ func (e *EmbyService) resumableItems(ctx context.Context, p ItemsParams) (map[st return map[string]any{"Items": []any{}, "TotalRecordCount": int64(0), "StartIndex": p.StartIndex}, nil } + mediaIDs := make([]string, 0, len(hist)) localIDs := make([]string, 0, len(hist)) for _, h := range hist { + mediaIDs = append(mediaIDs, h.MediaID) if !IsEmbyRemoteID(h.MediaID) { localIDs = append(localIDs, h.MediaID) } } + favSet := map[string]bool{} + if len(mediaIDs) > 0 { + var favs []model.Favorite + if err := e.repo.DB.WithContext(ctx). + Where("user_id = ? AND media_id IN ?", p.UserID, mediaIDs). + Find(&favs).Error; err != nil { + return nil, err + } + for _, fav := range favs { + favSet[fav.MediaID] = true + } + } byID := map[string]*model.Media{} if len(localIDs) > 0 { var medias []model.Media @@ -498,7 +504,7 @@ func (e *EmbyService) resumableItems(ctx context.Context, p ItemsParams) (map[st continue } localTotal++ - slots = append(slots, resumeSlot{item: e.itemPayload(ctx, m, false, h.PositionMs, false)}) + slots = append(slots, resumeSlot{item: e.itemPayload(ctx, m, favSet[h.MediaID], h.PositionMs, false)}) continue } if e.remote == nil || !IsEmbyRemoteID(h.MediaID) { @@ -559,7 +565,7 @@ func (e *EmbyService) resumableItems(ctx context.Context, p ItemsParams) (map[st continue } } - item["UserData"] = mergedRemoteUserData(item["UserData"], &f.hist) + item["UserData"] = applyMeBoxUserData(item["UserData"], &f.hist, favSet[f.hist.MediaID]) slots[i].item = item } } diff --git a/internal/service/emby_user_data.go b/internal/service/emby_user_data.go index 1851945..6c3c981 100644 --- a/internal/service/emby_user_data.go +++ b/internal/service/emby_user_data.go @@ -11,8 +11,8 @@ import ( "github.com/truewhile/MeBox/internal/model" ) -// SetFavorite 把 mediaID 标为 userID 的收藏。挂载的远程 Emby 条目会同时写入 -// 本地 favourites 表并透传到对应远程服务器,保证网页与第三方 Emby 客户端一致。 +// SetFavorite 把 mediaID 标为 userID 的收藏。只写入 MeBox 本地 favourites 表, +// 按 user_id 隔离;挂载远程 Emby 共用账号,不能再透传收藏以免串用户。 func (e *EmbyService) SetFavorite(ctx context.Context, userID, mediaID string, favorite bool) error { if err := SyncUserFavorite(ctx, e.repo, e.remote, userID, mediaID, favorite); err != nil { return err @@ -22,15 +22,8 @@ func (e *EmbyService) SetFavorite(ctx context.Context, userID, mediaID string, f } // MarkPlayed 把 mediaID 标为已看(写一个 100% 进度的 history 行)。 -// 远程 Emby 条目直接透传到对应服务器(本地不落库)。 +// 远程挂载条目同样只落本地 PlaybackHistory,按 MeBox 用户隔离。 func (e *EmbyService) MarkPlayed(ctx context.Context, userID, mediaID string, played bool) error { - if e.remote != nil && IsEmbyRemoteID(mediaID) { - acctID, remoteID, _ := DecodeEmbyRemoteID(mediaID) - if err := e.ProxyRemoteSetPlayed(ctx, acctID, remoteID, played); err != nil { - return err - } - return nil - } if !played { err := e.repo.DB.WithContext(ctx). Where("user_id = ? AND media_id = ?", userID, mediaID). @@ -40,15 +33,20 @@ func (e *EmbyService) MarkPlayed(ctx context.Context, userID, mediaID string, pl } return err } - m, err := e.repo.Media.FindByID(ctx, mediaID) - if err != nil || m == nil { - return errors.New("media not found") + dur := int64(0) + if IsEmbyRemoteID(mediaID) { + dur = remoteItemDurationMs(ctx, e, mediaID) + } else { + m, err := e.repo.Media.FindByID(ctx, mediaID) + if err != nil || m == nil { + return errors.New("media not found") + } + dur = int64(m.DurationSec) * 1000 } - dur := int64(m.DurationSec) * 1000 if dur <= 0 { dur = 1 } - err = e.repo.History.Upsert(ctx, &model.PlaybackHistory{ + err := e.repo.History.Upsert(ctx, &model.PlaybackHistory{ UserID: userID, MediaID: mediaID, PositionMs: dur, @@ -62,6 +60,36 @@ func (e *EmbyService) MarkPlayed(ctx context.Context, userID, mediaID string, pl return err } +func remoteItemDurationMs(ctx context.Context, e *EmbyService, mediaID string) int64 { + if e == nil || e.remote == nil || !IsEmbyRemoteID(mediaID) { + return 0 + } + mountID, remoteID, _ := DecodeEmbyRemoteID(mediaID) + mount, acct, err := e.remote.ResolveMount(ctx, mountID) + if err != nil || mount == nil || acct == nil { + return 0 + } + item, err := e.remote.RemoteItem(ctx, mount, acct, remoteID) + if err != nil || item == nil { + return 0 + } + switch ticks := item["RunTimeTicks"].(type) { + case float64: + if ticks > 0 { + return int64(ticks) / 10_000 + } + case int64: + if ticks > 0 { + return ticks / 10_000 + } + case int: + if ticks > 0 { + return int64(ticks) / 10_000 + } + } + return 0 +} + // RecordProgress 记录播放进度(来自 Emby 客户端的 /Sessions/Playing/Progress)。 // 不携带 PlaySessionId 的旧调用仍保持兼容。 func (e *EmbyService) RecordProgress(ctx context.Context, userID, mediaID string, positionTicks, runtimeTicks int64) error { @@ -138,8 +166,9 @@ func embyPlaySessionStartedAtMs(playSessionID string) int64 { return value } -// mergeRemoteUserData applies the current MeBox user's locally recorded playback -// and favourite state to remote Emby payloads. +// mergeRemoteUserData overlays the current MeBox user's locally recorded +// playback and favourite state onto remote Emby payloads. Remote mounts share +// one upstream Emby account, so upstream UserData must never leak across MeBox users. func (e *EmbyService) mergeRemoteUserData(ctx context.Context, userID string, payload any) error { if strings.TrimSpace(userID) == "" || payload == nil { return nil @@ -178,18 +207,7 @@ func (e *EmbyService) mergeRemoteUserData(ctx context.Context, userID string, pa } for _, item := range items { id, _ := item["Id"].(string) - userData, _ := item["UserData"].(map[string]any) - if h := byMediaID[id]; h != nil { - item["UserData"] = mergedRemoteUserData(userData, h) - userData, _ = item["UserData"].(map[string]any) - } - if favSet[id] { - if userData == nil { - userData = map[string]any{} - item["UserData"] = userData - } - userData["IsFavorite"] = true - } + item["UserData"] = applyMeBoxUserData(item["UserData"], byMediaID[id], favSet[id]) } return nil } @@ -221,12 +239,37 @@ func remoteItemMaps(payload any) []map[string]any { } func mergedRemoteUserData(raw any, history *model.PlaybackHistory) map[string]any { + favorite := false + if existing, ok := raw.(map[string]any); ok { + if v, ok := existing["IsFavorite"].(bool); ok { + favorite = v + } + } + return applyMeBoxUserData(raw, history, favorite) +} + +// applyMeBoxUserData rebuilds UserData for a remote item using only MeBox-local +// per-user state. Shared upstream Emby favourite/progress fields are discarded. +func applyMeBoxUserData(raw any, history *model.PlaybackHistory, favorite bool) map[string]any { userData := map[string]any{} if existing, ok := raw.(map[string]any); ok { for key, value := range existing { - userData[key] = value + switch key { + case "IsFavorite", "PlaybackPositionTicks", "Played", "PlayedPercentage", "PlayCount", "LastPlayedDate": + continue + default: + userData[key] = value + } } } + userData["IsFavorite"] = favorite + if history == nil { + userData["PlaybackPositionTicks"] = int64(0) + userData["Played"] = false + userData["PlayedPercentage"] = float64(0) + userData["PlayCount"] = 0 + return userData + } duration := history.DurationMs position := history.PositionMs percentage := float64(0) @@ -237,18 +280,9 @@ func mergedRemoteUserData(raw any, history *model.PlaybackHistory) map[string]an userData["Played"] = history.Completed userData["PlayedPercentage"] = percentage if history.Completed { - playCount := 0 - switch value := userData["PlayCount"].(type) { - case int: - playCount = value - case int64: - playCount = int(value) - case float64: - playCount = int(value) - } - if playCount < 1 { - userData["PlayCount"] = 1 - } + userData["PlayCount"] = 1 + } else { + userData["PlayCount"] = 0 } return userData } diff --git a/internal/service/emby_user_data_test.go b/internal/service/emby_user_data_test.go index 372026b..2ec1486 100644 --- a/internal/service/emby_user_data_test.go +++ b/internal/service/emby_user_data_test.go @@ -18,16 +18,18 @@ func TestMergedRemoteUserData(t *testing.T) { preserve any }{ { - name: "in-progress preserves remote fields", + name: "in-progress preserves non-user remote fields only", raw: map[string]any{ - "PlayCount": 2, - "Custom": "remote-value", + "PlayCount": 2, + "IsFavorite": true, + "PlaybackPositionTicks": int64(999), + "Custom": "remote-value", }, history: model.PlaybackHistory{PositionMs: 25_000, DurationMs: 100_000}, position: 250_000_000, played: false, percent: 25, - count: 2, + count: 0, preserve: "remote-value", }, { @@ -59,10 +61,47 @@ func TestMergedRemoteUserData(t *testing.T) { if tt.preserve != nil && out["Custom"] != tt.preserve { t.Fatalf("Custom = %#v, want %#v", out["Custom"], tt.preserve) } + if out["IsFavorite"] != false && out["IsFavorite"] != true { + t.Fatalf("IsFavorite missing: %#v", out) + } }) } } +func TestApplyMeBoxUserDataClearsSharedRemoteState(t *testing.T) { + out := applyMeBoxUserData(map[string]any{ + "IsFavorite": true, + "PlaybackPositionTicks": int64(42_000_000), + "Played": true, + "PlayedPercentage": 80.0, + "PlayCount": 3, + "Key": "keep", + }, nil, false) + if out["IsFavorite"] != false { + t.Fatalf("IsFavorite = %#v, want false", out["IsFavorite"]) + } + if out["PlaybackPositionTicks"] != int64(0) { + t.Fatalf("PlaybackPositionTicks = %#v, want 0", out["PlaybackPositionTicks"]) + } + if out["Played"] != false { + t.Fatalf("Played = %#v, want false", out["Played"]) + } + if out["PlayedPercentage"] != float64(0) { + t.Fatalf("PlayedPercentage = %#v, want 0", out["PlayedPercentage"]) + } + if out["PlayCount"] != 0 { + t.Fatalf("PlayCount = %#v, want 0", out["PlayCount"]) + } + if out["Key"] != "keep" { + t.Fatalf("Key = %#v, want keep", out["Key"]) + } + + fav := applyMeBoxUserData(map[string]any{"IsFavorite": false}, nil, true) + if fav["IsFavorite"] != true { + t.Fatalf("favorite overlay IsFavorite = %#v, want true", fav["IsFavorite"]) + } +} + func TestRemoteItemMapsFindsEnvelopeItems(t *testing.T) { remoteID := EncodeEmbyRemoteID("mount-1", "item-1") payload := map[string]any{ @@ -108,3 +147,57 @@ func TestRecordProgressFallbacksToExistingHistoryDuration(t *testing.T) { t.Fatalf("expected 95%% progress to be completed") } } + +func TestMarkPlayedStoresRemoteItemLocallyPerUser(t *testing.T) { + svc := newTestEmbyService(t) + remoteID := EncodeEmbyRemoteID("mount-test", "item-played") + alice := &model.User{Username: "alice_played", Role: "user", Tier: "free", IsActive: true} + bob := &model.User{Username: "bob_played", Role: "user", Tier: "free", IsActive: true} + if err := svc.repo.User.Create(t.Context(), alice); err != nil { + t.Fatalf("create alice: %v", err) + } + if err := svc.repo.User.Create(t.Context(), bob); err != nil { + t.Fatalf("create bob: %v", err) + } + + if err := svc.MarkPlayed(t.Context(), alice.ID, remoteID, true); err != nil { + t.Fatalf("mark played: %v", err) + } + + var aliceRows, bobRows int64 + _ = svc.repo.DB.Model(&model.PlaybackHistory{}).Where("user_id = ? AND media_id = ?", alice.ID, remoteID).Count(&aliceRows) + _ = svc.repo.DB.Model(&model.PlaybackHistory{}).Where("user_id = ? AND media_id = ?", bob.ID, remoteID).Count(&bobRows) + if aliceRows != 1 { + t.Fatalf("alice history rows = %d, want 1", aliceRows) + } + if bobRows != 0 { + t.Fatalf("bob should not see alice remote played state, rows=%d", bobRows) + } + + payload := map[string]any{ + "Id": remoteID, + "UserData": map[string]any{ + "IsFavorite": true, + "PlaybackPositionTicks": int64(50_000_000), + "Played": true, + }, + } + if err := svc.mergeRemoteUserData(t.Context(), bob.ID, payload); err != nil { + t.Fatalf("merge for bob: %v", err) + } + bobData := payload["UserData"].(map[string]any) + if bobData["IsFavorite"] != false { + t.Fatalf("bob IsFavorite leaked: %#v", bobData) + } + if bobData["Played"] != false || bobData["PlaybackPositionTicks"] != int64(0) { + t.Fatalf("bob playback leaked: %#v", bobData) + } + + if err := svc.mergeRemoteUserData(t.Context(), alice.ID, payload); err != nil { + t.Fatalf("merge for alice: %v", err) + } + aliceData := payload["UserData"].(map[string]any) + if aliceData["Played"] != true { + t.Fatalf("alice Played = %#v, want true", aliceData["Played"]) + } +} diff --git a/internal/service/favorites_sync.go b/internal/service/favorites_sync.go index eb6c069..cc72d5d 100644 --- a/internal/service/favorites_sync.go +++ b/internal/service/favorites_sync.go @@ -10,21 +10,14 @@ import ( "github.com/truewhile/MeBox/internal/repository" ) -// SyncUserFavorite keeps favourite state aligned across the local favourites table -// and the upstream remote Emby server for mounted items. -func SyncUserFavorite(ctx context.Context, repo *repository.Container, remote *EmbyRemoteService, userID, mediaID string, favorite bool) error { +// SyncUserFavorite keeps favourite state in the local favourites table, keyed +// by MeBox user_id. Remote Emby mounts share one upstream account, so favourite +// state is intentionally not proxied upstream. +func SyncUserFavorite(ctx context.Context, repo *repository.Container, _ *EmbyRemoteService, userID, mediaID string, favorite bool) error { if repo == nil || userID == "" || mediaID == "" { return errors.New("missing favourite sync inputs") } - if err := setLocalFavorite(ctx, repo, userID, mediaID, favorite); err != nil { - return err - } - if favorite || IsEmbyRemoteID(mediaID) { - if err := proxyRemoteFavorite(ctx, remote, mediaID, favorite); err != nil { - return err - } - } - return nil + return setLocalFavorite(ctx, repo, userID, mediaID, favorite) } // IsUserFavorite reports whether the user has favourited mediaID locally. @@ -57,18 +50,3 @@ func setLocalFavorite(ctx context.Context, repo *repository.Container, userID, m Where("user_id = ? AND media_id = ?", userID, mediaID). Delete(&model.Favorite{}).Error } - -func proxyRemoteFavorite(ctx context.Context, remote *EmbyRemoteService, mediaID string, favorite bool) error { - if remote == nil || !IsEmbyRemoteID(mediaID) { - return nil - } - mountID, remoteItemID, ok := DecodeEmbyRemoteID(mediaID) - if !ok { - return nil - } - _, acct, err := remote.ResolveMount(ctx, mountID) - if err != nil { - return err - } - return remote.ProxySetFavorite(ctx, acct, remoteItemID, favorite) -}