diff --git a/internal/handler/strm.go b/internal/handler/strm.go index 8f897fc..ead666f 100644 --- a/internal/handler/strm.go +++ b/internal/handler/strm.go @@ -28,17 +28,25 @@ type strmAccountView struct { model.StrmAccount HasCredential bool `json:"has_credential"` ProviderLabel string `json:"provider_label"` + // ProxyPlay 仅远程 Emby 挂载账号返回:播放流量是否经过 MMTL 代理(编辑回显用)。 + ProxyPlay *bool `json:"proxy_play,omitempty"` } -func strmAccountViews(accounts []model.StrmAccount) []strmAccountView { +func strmAccountViews(svc *service.Container, accounts []model.StrmAccount) []strmAccountView { out := make([]strmAccountView, 0, len(accounts)) for i := range accounts { a := accounts[i] - out = append(out, strmAccountView{ + view := strmAccountView{ StrmAccount: a, HasCredential: service.HasStrmAccountCredential(&a), ProviderLabel: providerLabelOf(a.Provider), - }) + } + if a.Provider == model.StrmProviderEmbyRemote && svc != nil && svc.EmbyRemote != nil { + if proxyPlay, err := svc.EmbyRemote.ProxyPlayOf(&a); err == nil { + view.ProxyPlay = &proxyPlay + } + } + out = append(out, view) } return out } @@ -58,7 +66,7 @@ func listStrmAccountsHandler(svc *service.Container) gin.HandlerFunc { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, strmAccountViews(accounts)) + c.JSON(http.StatusOK, strmAccountViews(svc, accounts)) } } @@ -74,7 +82,7 @@ func createStrmAccountHandler(svc *service.Container) gin.HandlerFunc { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - views := strmAccountViews([]model.StrmAccount{*acct}) + views := strmAccountViews(svc, []model.StrmAccount{*acct}) c.JSON(http.StatusOK, views[0]) } } @@ -92,7 +100,7 @@ func updateStrmAccountHandler(svc *service.Container) gin.HandlerFunc { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - views := strmAccountViews([]model.StrmAccount{*acct}) + views := strmAccountViews(svc, []model.StrmAccount{*acct}) c.JSON(http.StatusOK, views[0]) } } @@ -114,7 +122,7 @@ func testStrmAccountHandler(svc *service.Container) gin.HandlerFunc { c.JSON(http.StatusNotFound, gin.H{"error": "网盘账号不存在"}) return } - views := strmAccountViews([]model.StrmAccount{*acct}) + views := strmAccountViews(svc, []model.StrmAccount{*acct}) c.JSON(http.StatusOK, views[0]) } } diff --git a/internal/service/emby_remote.go b/internal/service/emby_remote.go index 45c66c7..924cb4c 100644 --- a/internal/service/emby_remote.go +++ b/internal/service/emby_remote.go @@ -272,6 +272,15 @@ func (r *EmbyRemoteService) TestConnection(ctx context.Context, acct *model.Strm return r.doGet(ctx, acct, cfg, "/System/Info", nil, &out) } +// ProxyPlayOf 返回账号是否配置了播放代理(供账号列表/编辑回显)。 +func (r *EmbyRemoteService) ProxyPlayOf(acct *model.StrmAccount) (bool, error) { + cfg, err := r.configOf(acct) + if err != nil { + return false, err + } + return cfg.ProxyPlay, nil +} + // ─── 元数据 / 目录聚合 ───────────────────────────────────────────────────────── // RemoteViews 拉取远程媒体库(View)列表,返回远程原始 view map(未重写)。 diff --git a/internal/service/strm_service.go b/internal/service/strm_service.go index b362f9a..994669e 100644 --- a/internal/service/strm_service.go +++ b/internal/service/strm_service.go @@ -237,6 +237,33 @@ func (s *StrmService) strmAccountConfig(acct *model.StrmAccount) (map[string]str return cfg, nil } +// mergeEmbyRemoteConfig 对远程 Emby 账号配置做合并式更新:config 中出现的键 +// 覆盖写入(敏感键按明文加密),未出现的键保留原密文;显式空字符串=清除。 +func (s *StrmService) mergeEmbyRemoteConfig(existing string, config map[string]string) (string, error) { + out := map[string]string{} + if strings.TrimSpace(existing) != "" { + if err := json.Unmarshal([]byte(existing), &out); err != nil { + return "", fmt.Errorf("decode account config: %w", err) + } + } + for k, v := range config { + if v == "" { + delete(out, k) + continue + } + if strmContains(StrmAccountSecretKeys, k) { + out[k] = s.crypto.Encrypt(v) + } else { + out[k] = v + } + } + data, err := json.Marshal(out) + if err != nil { + return "", err + } + return string(data), nil +} + // HasStrmAccountCredential 报告账号是否已配置核心凭据(用于前端展示)。 func HasStrmAccountCredential(acct *model.StrmAccount) bool { switch acct.Provider { @@ -292,7 +319,16 @@ func (s *StrmService) UpdateStrmAccount(ctx context.Context, id, name string, en acct.Enabled = *enabled } if len(config) > 0 { - enc, err := s.strmAccountConfigJSON(config, true) + var enc string + var err error + if acct.Provider == model.StrmProviderEmbyRemote { + // 远程 Emby 账号:合并式更新。config 中出现的键覆盖(敏感键按明文 + // 加密写入),未出现的键保留原密文——避免编辑「代理开关」时把已 + // 保存的地址与凭据清空。 + enc, err = s.mergeEmbyRemoteConfig(acct.Config, config) + } else { + enc, err = s.strmAccountConfigJSON(config, true) + } if err != nil { return nil, err } diff --git a/web/src/pages/StrmDialogs.tsx b/web/src/pages/StrmDialogs.tsx index 41d5121..a2f8bd8 100644 --- a/web/src/pages/StrmDialogs.tsx +++ b/web/src/pages/StrmDialogs.tsx @@ -111,7 +111,7 @@ export function StrmAccountDialog({ const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [token, setToken] = useState('') - const [proxyPlay, setProxyPlay] = useState(false) + const [proxyPlay, setProxyPlay] = useState(existing?.proxy_play ?? false) const [saving, setSaving] = useState(false) const buildConfig = (): Record => { diff --git a/web/src/types/strm.ts b/web/src/types/strm.ts index 42df572..709ce9d 100644 --- a/web/src/types/strm.ts +++ b/web/src/types/strm.ts @@ -21,6 +21,8 @@ export interface StrmAccount { last_test_ok: boolean has_credential: boolean provider_label: string + /** 仅远程 Emby 挂载账号返回:播放流量是否经过 MMTL 代理 */ + proxy_play?: boolean } export interface StrmAccountInput {