From 065652d75894efd839510071dcb5778bd29d0224 Mon Sep 17 00:00:00 2001
From: truewhile <62226914+truewhile@users.noreply.github.com>
Date: Thu, 24 Sep 2026 22:27:09 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
internal/handler/danmaku_settings_test.go | 42 ++++++++-----
internal/model/user.go | 4 +-
internal/service/danmaku_service.go | 45 +++----------
internal/service/danmaku_service_test.go | 2 -
web/src/api/danmaku.ts | 2 -
web/src/components/ExternalPlayerButton.tsx | 2 +-
web/src/components/PlayerControls.tsx | 28 ++-------
web/src/pages/MediaDetailAdminPanel.tsx | 54 +++++++++-------
web/src/pages/MediaDetailMetadata.tsx | 70 ++++++++++++++-------
web/src/pages/MediaDetailPageSections.tsx | 54 ++++++++--------
web/src/pages/PlayerPage.tsx | 49 +++------------
web/src/pages/PlayerVideoStage.tsx | 3 -
web/src/types/auth.ts | 2 -
web/src/utils/playbackRate.ts | 3 +
14 files changed, 161 insertions(+), 199 deletions(-)
diff --git a/internal/handler/danmaku_settings_test.go b/internal/handler/danmaku_settings_test.go
index cb0422c..9a0158c 100644
--- a/internal/handler/danmaku_settings_test.go
+++ b/internal/handler/danmaku_settings_test.go
@@ -112,8 +112,7 @@ func TestGetDanmakuConfigIncludesPerUserMergePreference(t *testing.T) {
t.Fatalf("status = %d, want 200", w.Code)
}
var cfg struct {
- MergeSources bool `json:"merge_sources"`
- PlaybackRate float64 `json:"playback_rate"`
+ MergeSources bool `json:"merge_sources"`
}
if err := json.Unmarshal(w.Body.Bytes(), &cfg); err != nil {
t.Fatalf("decode: %v", err)
@@ -121,9 +120,6 @@ func TestGetDanmakuConfigIncludesPerUserMergePreference(t *testing.T) {
if cfg.MergeSources {
t.Fatal("default merge preference should be false")
}
- if cfg.PlaybackRate != 1 {
- t.Fatalf("default playback rate = %v, want 1", cfg.PlaybackRate)
- }
if err := svc.Danmaku.SetMergeSources(t.Context(), "user-1", true); err != nil {
t.Fatalf("set: %v", err)
@@ -141,7 +137,7 @@ func TestGetDanmakuConfigIncludesPerUserMergePreference(t *testing.T) {
func TestUpdateDanmakuSettingsPersistsAllPlayerPreferences(t *testing.T) {
svc := newDanmakuSettingsService(t)
- body := "{\"enabled\":false,\"opacity\":0.6,\"font_size\":32,\"area\":0.7,\"merge_sources\":true,\"volume\":0.35,\"playback_rate\":1.5,\"source\":\"https://dm.example/base/\",\"app_id\":\"my-app-id\",\"app_key\":\"my-app-secret\"}"
+ body := "{\"enabled\":false,\"opacity\":0.6,\"font_size\":32,\"area\":0.7,\"merge_sources\":true,\"volume\":0.35,\"source\":\"https://dm.example/base/\",\"app_id\":\"my-app-id\",\"app_key\":\"my-app-secret\"}"
c, w := newDanmakuSettingsContext(t, svc, http.MethodPut, "/danmaku/settings", body, "user-1")
updateDanmakuSettingsHandler(svc)(c)
@@ -158,7 +154,7 @@ func TestUpdateDanmakuSettingsPersistsAllPlayerPreferences(t *testing.T) {
if cfg.Enabled || cfg.Opacity != "0.6" || cfg.FontSize != "32" || cfg.Area != "0.7" {
t.Fatalf("unexpected render config: %+v", cfg)
}
- if !cfg.MergeSources || cfg.Volume != 0.35 || cfg.PlaybackRate != 1.5 {
+ if !cfg.MergeSources || cfg.Volume != 0.35 {
t.Fatalf("unexpected user preferences: %+v", cfg)
}
if cfg.Source != "https://dm.example/base" || cfg.AppID != "my-app-id" || !cfg.AppKeyConfigured {
@@ -169,7 +165,7 @@ func TestUpdateDanmakuSettingsPersistsAllPlayerPreferences(t *testing.T) {
if err != nil || user == nil {
t.Fatalf("read persisted user: %v", err)
}
- if user.DanmakuAppKey != "my-app-secret" || user.PlayerVolume != 0.35 || user.PlayerPlaybackRate != 1.5 || user.DanmakuSource != "https://dm.example/base" {
+ if user.DanmakuAppKey != "my-app-secret" || user.PlayerVolume != 0.35 || user.DanmakuSource != "https://dm.example/base" {
t.Fatalf("preferences not persisted: %+v", user)
}
}
@@ -207,22 +203,36 @@ func TestUpdateDanmakuSettingsPersistsVr360GuideSeen(t *testing.T) {
}
}
-func TestUpdateDanmakuSettingsRejectsInvalidPlaybackRate(t *testing.T) {
+// 播放倍速只作用于当前视频:接口既不下发也不保存它。老客户端仍可能带上
+// playback_rate,服务端应当忽略而不是报错。
+func TestDanmakuSettingsIgnorePlaybackRate(t *testing.T) {
svc := newDanmakuSettingsService(t)
- c, w := newDanmakuSettingsContext(t, svc, http.MethodPut, "/danmaku/settings",
- `{"playback_rate":4}`, "user-1")
- updateDanmakuSettingsHandler(svc)(c)
+ c, w := newDanmakuSettingsContext(t, svc, http.MethodGet, "/danmaku/config", "", "user-1")
+ getDanmakuConfigHandler(svc)(c)
+ if w.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200 (body=%s)", w.Code, w.Body.String())
+ }
+ if bytes.Contains(w.Body.Bytes(), []byte("playback_rate")) {
+ t.Fatalf("config must not expose playback rate: %s", w.Body.String())
+ }
- if w.Code != http.StatusBadRequest {
- t.Fatalf("status = %d, want 400 (body=%s)", w.Code, w.Body.String())
+ c2, w2 := newDanmakuSettingsContext(t, svc, http.MethodPut, "/danmaku/settings",
+ `{"volume":0.5,"playback_rate":1.5}`, "user-1")
+ updateDanmakuSettingsHandler(svc)(c2)
+
+ if w2.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200 (body=%s)", w2.Code, w2.Body.String())
+ }
+ if bytes.Contains(w2.Body.Bytes(), []byte("playback_rate")) {
+ t.Fatalf("settings response must not expose playback rate: %s", w2.Body.String())
}
user, err := svc.Repo.User.FindByID(t.Context(), "user-1")
if err != nil || user == nil {
t.Fatalf("read user: %v", err)
}
- if user.PlayerPlaybackRate != 1 {
- t.Fatalf("invalid playback rate was persisted: %v", user.PlayerPlaybackRate)
+ if user.PlayerVolume != 0.5 {
+ t.Fatalf("volume not persisted: %v", user.PlayerVolume)
}
}
diff --git a/internal/model/user.go b/internal/model/user.go
index e1f5e0a..bbfac11 100644
--- a/internal/model/user.go
+++ b/internal/model/user.go
@@ -37,8 +37,8 @@ type User struct {
// original / simplified / traditional。
SubtitleChineseMode string `gorm:"size:16;not null;default:original" json:"subtitle_chinese_mode"`
// 网页播放器偏好按用户存储,切换媒体对象后继续沿用。
- PlayerVolume float64 `gorm:"not null;default:1" json:"player_volume"`
- PlayerPlaybackRate float64 `gorm:"not null;default:1" json:"player_playback_rate"`
+ // 播放倍速不在此列:它只作用于当前视频,不进数据库。
+ PlayerVolume float64 `gorm:"not null;default:1" json:"player_volume"`
// PlayerVr360GuideSeen 记录用户是否已经看过 VR 全景播放的首次操作说明,
// 按用户保存:看过一次之后不再弹出。
PlayerVr360GuideSeen bool `gorm:"not null;default:false" json:"player_vr360_guide_seen"`
diff --git a/internal/service/danmaku_service.go b/internal/service/danmaku_service.go
index 2aba865..0744f37 100644
--- a/internal/service/danmaku_service.go
+++ b/internal/service/danmaku_service.go
@@ -49,12 +49,6 @@ const (
// dandanplay protocol (search/episodes + comment/{episodeId}) may be used.
const DanmakuDefaultSource = "https://api.dandanplay.net"
-const (
- minPlayerPlaybackRate = 0.5
- maxPlayerPlaybackRate = 3.0
- defaultPlayerPlaybackRate = 1.0
-)
-
// danmakuOfficialBase is where identification (/api/v2/match) and the
// comment/search fallback always go, regardless of the configured source.
// A package var (not a const) so tests can point it at a local server.
@@ -69,8 +63,6 @@ type DanmakuRenderConfig struct {
FontSize string `json:"font_size"`
Area string `json:"area"`
Volume float64 `json:"volume"`
- // PlaybackRate 是当前用户的播放倍速偏好(按用户存储)。
- PlaybackRate float64 `json:"playback_rate"`
// Vr360GuideSeen 表示当前用户是否已经看过 VR 全景播放的首次操作说明。
Vr360GuideSeen bool `json:"vr360_guide_seen"`
// MergeSources 是当前用户的弹幕合并偏好(按用户存储)。
@@ -99,7 +91,6 @@ type DanmakuSettingsPatch struct {
Area *float64 `json:"area"`
MergeSources *bool `json:"merge_sources"`
Volume *float64 `json:"volume"`
- PlaybackRate *float64 `json:"playback_rate"`
// Vr360GuideSeen 标记 VR 全景播放的首次操作说明是否已经看过。
Vr360GuideSeen *bool `json:"vr360_guide_seen"`
}
@@ -358,11 +349,10 @@ func cloneDanmakuAnimeList(in []DanmakuAnime) []DanmakuAnime {
// Config reads danmaku settings from the runtime settings table.
func (s *DanmakuService) Config(ctx context.Context) DanmakuRenderConfig {
cfg := DanmakuRenderConfig{
- Opacity: "1",
- FontSize: "24",
- Area: "1",
- Volume: 1,
- PlaybackRate: defaultPlayerPlaybackRate,
+ Opacity: "1",
+ FontSize: "24",
+ Area: "1",
+ Volume: 1,
}
if s == nil || s.repo == nil || s.repo.Setting == nil {
return cfg
@@ -414,12 +404,11 @@ func (s *DanmakuService) findUser(ctx context.Context, userID string) (*model.Us
func danmakuConfigFromUser(user *model.User) DanmakuRenderConfig {
if user == nil {
return DanmakuRenderConfig{
- Enabled: true,
- Opacity: "1",
- FontSize: "24",
- Area: "1",
- Volume: 1,
- PlaybackRate: defaultPlayerPlaybackRate,
+ Enabled: true,
+ Opacity: "1",
+ FontSize: "24",
+ Area: "1",
+ Volume: 1,
}
}
opacity := user.DanmakuOpacity
@@ -438,10 +427,6 @@ func danmakuConfigFromUser(user *model.User) DanmakuRenderConfig {
if volume < 0 || volume > 1 {
volume = 1
}
- playbackRate := user.PlayerPlaybackRate
- if playbackRate < minPlayerPlaybackRate || playbackRate > maxPlayerPlaybackRate {
- playbackRate = defaultPlayerPlaybackRate
- }
return DanmakuRenderConfig{
Enabled: user.DanmakuEnabled,
Source: strings.TrimSpace(user.DanmakuSource),
@@ -450,7 +435,6 @@ func danmakuConfigFromUser(user *model.User) DanmakuRenderConfig {
FontSize: strconv.Itoa(fontSize),
Area: strconv.FormatFloat(area, 'f', -1, 64),
Volume: volume,
- PlaybackRate: playbackRate,
Vr360GuideSeen: user.PlayerVr360GuideSeen,
MergeSources: user.DanmakuMergeSources,
AppKeyConfigured: strings.TrimSpace(user.DanmakuAppKey) != "",
@@ -522,17 +506,6 @@ func (s *DanmakuService) UpdateUserSettings(ctx context.Context, userID string,
}
updates["player_volume"] = *patch.Volume
}
- if patch.PlaybackRate != nil {
- if *patch.PlaybackRate < minPlayerPlaybackRate || *patch.PlaybackRate > maxPlayerPlaybackRate {
- return DanmakuRenderConfig{}, fmt.Errorf(
- "%w: playback_rate must be between %.2f and %.2f",
- ErrInvalidDanmakuSettings,
- minPlayerPlaybackRate,
- maxPlayerPlaybackRate,
- )
- }
- updates["player_playback_rate"] = *patch.PlaybackRate
- }
if patch.Vr360GuideSeen != nil {
updates["player_vr360_guide_seen"] = *patch.Vr360GuideSeen
}
diff --git a/internal/service/danmaku_service_test.go b/internal/service/danmaku_service_test.go
index e4ca045..c863251 100644
--- a/internal/service/danmaku_service_test.go
+++ b/internal/service/danmaku_service_test.go
@@ -422,7 +422,6 @@ func TestDanmakuFetchUsesPerUserSourceAndCredentials(t *testing.T) {
user.DanmakuFontSize = 30
user.DanmakuArea = 0.7
user.PlayerVolume = 0.42
- user.PlayerPlaybackRate = 1.25
require.NoError(t, svc.repo.User.Create(ctx, &user))
seedDanmakuMedia(t, svc, "per-user-media", "测试动画", "", 0)
@@ -431,7 +430,6 @@ func TestDanmakuFetchUsesPerUserSourceAndCredentials(t *testing.T) {
require.Contains(t, res.Raw, "弹幕A")
require.Equal(t, srv.URL(), res.Source)
require.Equal(t, 0.42, res.Volume)
- require.Equal(t, 1.25, res.PlaybackRate)
require.Equal(t, "0.65", res.Opacity)
require.Equal(t, "30", res.FontSize)
require.Equal(t, srv.lastHeaders.Get("X-AppId"), "user-app-id")
diff --git a/web/src/api/danmaku.ts b/web/src/api/danmaku.ts
index eb81c10..c13d58f 100644
--- a/web/src/api/danmaku.ts
+++ b/web/src/api/danmaku.ts
@@ -71,7 +71,6 @@ export interface DanmakuConfig {
font_size: string
area: string
volume: number
- playback_rate: number
/** 当前用户是否已经看过 VR 全景播放的首次操作说明(按用户存储)。 */
vr360_guide_seen: boolean
/** Per-user preference: merge the same episode's multiple sources. */
@@ -90,7 +89,6 @@ export interface DanmakuSettingsPatch {
area?: number
merge_sources?: boolean
volume?: number
- playback_rate?: number
/** 看过 VR 操作说明后置为 true,之后不再弹出。 */
vr360_guide_seen?: boolean
}
diff --git a/web/src/components/ExternalPlayerButton.tsx b/web/src/components/ExternalPlayerButton.tsx
index 20ebf50..e6c2242 100644
--- a/web/src/components/ExternalPlayerButton.tsx
+++ b/web/src/components/ExternalPlayerButton.tsx
@@ -52,7 +52,7 @@ export function ExternalPlayerButton({
className={
compact
? 'shrink-0 inline-flex items-center rounded-lg border border-primary-400/35 bg-white px-2 py-1 text-xs font-semibold text-brand-500 hover:bg-primary-400/10 disabled:opacity-50 transition-colors whitespace-nowrap'
- : 'btn-outline border-brand-500/30 px-5 text-[#c9954a] hover:border-brand-500 hover:bg-brand-50'
+ : 'btn-outline h-11 border-brand-500/30 px-5 text-[#c9954a] hover:border-brand-500 hover:bg-brand-50'
}
>
系统后台高级控制面板
- {isEpisodeArtworkTarget(media) && ( -