Skip to content

修复在线/离线播放历史记录#2261

Merged
Predidit merged 5 commits into
Predidit:mainfrom
liangyuR:codex/history-online-offline-sync
Jun 24, 2026
Merged

修复在线/离线播放历史记录#2261
Predidit merged 5 commits into
Predidit:mainfrom
liangyuR:codex/history-online-offline-sync

Conversation

@liangyuR

@liangyuR liangyuR commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

#2261 的mini版本,仅针对 修复在线/离线播放历史记录 做的最小修复,同时考虑了后续统一数据模型的考量

后续约 3 ~ 4 个 PR

摘要

  • 将播放历史身份拆分为在线记录和离线记录,并使用带作用域的历史 key。
  • 将在线来源元数据与离线下载播放元数据分开保存。
  • 在 WebDAV 历史同步中传递 entryKind、剧集页面 URL,以及每集的播放进度时间戳。
  • 更新播放续播、历史卡片重新打开、离线路径映射、SyncPlay 弹幕剧集处理,以及回归测试。

根因

在线播放和已下载播放使用了相同的历史身份结构,因此本地存储和同步时,可能会将同一个 Bangumi 条目和插件下的记录合并或覆盖。

这也导致在在线历史和离线历史同时存在时,旧版 WebDAV 历史快照和删除标记变得含义不明确。

验证

  • git diff --cached --check
  • git diff --check
  • flutter test --no-pub test/history_repository_test.dart test/history_sync_test.dart test/resolved_episode_test.dart
flutter test test/history_repository_test.dart test/history_sync_test.dart test/resolved_episode_test.dart

手动后续验证

  • 验证在线历史和离线历史会显示为不同条目。
  • 验证在线播放仍然可以从在线历史中继续播放。
  • 验证离线播放可以重新打开已下载剧集,并从离线历史中继续播放。
  • 验证设备之间通过 WebDAV 同步时,在线 / 离线历史可以保持分离。

1. 为历史记录增加在线/离线身份并按 scoped key 存储。

2. 同步 WebDAV 历史事件、播放恢复和离线入口,避免记录互相覆盖。

3. 增加历史仓储、同步合并和集数解析回归测试。
@liangyuR liangyuR changed the title Fix online/offline playback history separation 修复在线/离线播放历史记录 Jun 24, 2026
@liangyuR liangyuR marked this pull request as ready for review June 24, 2026 05:41
Comment thread lib/pages/video/video_controller.dart Outdated
String urlItem = resolvedEpisode.episodePageUrl;
if (urlItem.contains(currentPlugin.baseUrl) ||
urlItem.contains(currentPlugin.baseUrl.replaceAll('https', 'http'))) {
urlItem = urlItem;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Dead no-op self-assignment.

When the original condition if (!contains && !contains) was inverted, the if branch was left with urlItem = urlItem;, which does nothing. The logic is still correct, but the empty branch is dead code. Consider collapsing this into a single guard:

if (!urlItem.contains(currentPlugin.baseUrl) &&
    !urlItem.contains(currentPlugin.baseUrl.replaceAll('https', 'http'))) {
  urlItem = currentPlugin.baseUrl + urlItem;
}

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

The SyncPlay episode round-trip flagged in the prior review is now resolved: the broadcast file name and the changeEpisode callback both use the 1-based playlist index (currentEpisode / resolvedEpisode.listIndex) consistently, so peers resolve to the correct episode. The previously noted dead no-op self-assignment has also been removed.

Files Reviewed (4 files)
  • lib/pages/player/controller/player_models.dart - added episode (playlist index) field; no issues
  • lib/pages/player/player_controller.dart - SyncPlay now uses currentEpisode consistently; round-trip regression fixed
  • lib/pages/video/video_controller.dart - removed dead getters/setters and unused helper; passes listIndex as episode; no issues
  • test/resolved_episode_test.dart - updated for new episode param; no issues
Previous Review Summaries (4 snapshots, latest commit a2f06be)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit a2f06be)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
lib/pages/player/player_controller.dart 38 SyncPlay now broadcasts the danmaku episode number in the file name, but the receiver feeds that value into changeEpisode as a 1-based playlist index. When a title's parsed number differs from its list position (e.g. split-cour roads), peers switch to the wrong episode or fail with 集数解析失败.
Files Reviewed (17 files)
  • lib/bean/card/bangumi_history_card.dart - online/offline playback split + source badge; no issues
  • lib/modules/history/history_module.dart - entryKind/scoped keys + Progress.updatedAtMs; no issues
  • lib/modules/history/history_module.g.dart - generated adapter fields; no issues
  • lib/modules/history/history_sync.dart - scoped-key canonicalization + legacy tombstone handling; no issues
  • lib/pages/history/history_controller.dart - identity-based update API; no issues
  • lib/pages/player/controller/player_models.dart - episode renamed to danmakuEpisodeNumber; no issues
  • lib/pages/player/player_controller.dart - 1 issue (SyncPlay episode round-trip)
  • lib/pages/player/player_item.dart - history identity recording; no issues
  • lib/pages/video/video_controller.dart - ResolvedEpisode + offline road snapshot; no issues
  • lib/pages/video/video_page.dart - road-name display + offline offset; no issues
  • lib/repositories/history_repository.dart - scoped lookup + legacy migration; no issues
  • lib/services/storage/storage.dart - temp box merge by scoped key; no issues
  • lib/services/sync/history_sync_service.dart - shared state-event builder; no issues
  • lib/services/sync/webdav.dart - delegates to shared builder; no issues
  • test/history_repository_test.dart - new tests; no issues
  • test/history_sync_test.dart - new tests; no issues
  • test/resolved_episode_test.dart - new tests; no issues

Fix these issues in Kilo Cloud

Previous review (commit ba1dd0c)

Status: No Issues Found | Recommendation: Merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 0

The latest commits refactor the history-card playback flow in lib/bean/card/bangumi_history_card.dart into testable top-level helpers (historySourceBadgeText, openHistoryPlaybackForEntry, HistoryPlaybackOpenResult) and add a _HistorySourceBadge widget that labels each entry as 在线/缓存. The online-failure path now returns a clear failure message instead of silently falling back to offline playback, which is covered by the new test/history_card_test.dart unit tests. All helpers correctly route through HistoryEntryKind.normalize, so legacy entry-kind values map to the online label. No new issues were found in the changed code.

Files Reviewed (2 files changed since last review)
  • lib/bean/card/bangumi_history_card.dart - playback refactor + source badge; no issues
  • test/history_card_test.dart - new tests for badge label and playback policy; no issues

Previous review (commit 08ea8ee)

Status: No Issues Found | Recommendation: Merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 0

The latest commit resolves the previously reported SUGGESTION in lib/pages/video/video_controller.dart: the dead no-op urlItem = urlItem; has been removed and the logic collapsed into a single inverted guard (if (!contains && !contains) { urlItem = baseUrl + urlItem; }). Behavior is preserved and no new issues were introduced.

Files Reviewed (1 file changed since last review)
  • lib/pages/video/video_controller.dart - previous dead no-op suggestion resolved; no remaining issues

Previous review (commit 19fceed)

Status: 1 Issue Found | Recommendation: Optional cleanup; safe to merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
lib/pages/video/video_controller.dart 467 Dead no-op urlItem = urlItem; left in the inverted if branch; collapse into a single guard.

The history online/offline split is implemented cleanly: scoped history keys (legacyKey::online/::offline), legacy-key migration on online upserts, per-progress updatedAtMs timestamps, and entryKind/episodePageUrl threaded through WebDAV sync events and snapshot canonicalization. The merge/tombstone canonicalization logic is well covered by the new tests in history_sync_test.dart, history_repository_test.dart, and resolved_episode_test.dart.

Files Reviewed (17 files)
  • lib/bean/card/bangumi_history_card.dart - online/offline reopen paths
  • lib/modules/history/history_module.dart - HistoryEntryKind, PlaybackHistoryIdentity, scoped keys
  • lib/modules/history/history_module.g.dart - Hive adapter fields 7/8 + updatedAtMs
  • lib/modules/history/history_sync.dart - key canonicalization, tombstone handling
  • lib/pages/history/history_controller.dart - identity-based API
  • lib/pages/player/controller/player_models.dart - danmakuEpisodeNumber rename
  • lib/pages/player/player_controller.dart - danmaku episode usage
  • lib/pages/player/player_item.dart - identity-based history recording
  • lib/pages/video/video_controller.dart - 1 issue (dead no-op)
  • lib/pages/video/video_page.dart - road name display + bounds guards
  • lib/repositories/history_repository.dart - scoped lookup + legacy migration
  • lib/services/storage/storage.dart - import dedup keying
  • lib/services/sync/history_sync_service.dart - buildStateEventsFromHistories
  • lib/services/sync/webdav.dart - delegates to shared event builder
  • test/history_repository_test.dart
  • test/history_sync_test.dart
  • test/resolved_episode_test.dart

Fix these issues in Kilo Cloud


Reviewed by claude-4.8-opus-20260528 · Input: 5.1K · Output: 8.2K · Cached: 583.8K

1. 将在线播放 URL 补全逻辑折回单 guard。

2. 移除 robot 指出的 urlItem 自赋值死代码。
@Predidit

Predidit commented Jun 24, 2026

Copy link
Copy Markdown
Owner

可以考虑在 history_card 上新增一个 badget 表示此条观看记录来自在线还是缓存,此外当在线源不可用时,不要试图 fallback 到离线源,直接弹出 toast 报错就可以了

@liangyuR

Copy link
Copy Markdown
Contributor Author
image

@Predidit

Copy link
Copy Markdown
Owner

不要给 widget 做测试,我了解这是最佳实践,但是此工程一直以来只测试逻辑,不测试UI,和之前保持一致,删除 history_card_test

@liangyuR

Copy link
Copy Markdown
Contributor Author

好的,但是为什么呢?这样做有什么风险?还是 test rule ?

- 1. 新增 `openHistoryPlaybackForEntry`,统一在线与缓存观看记录的播放入口处理。

- 2. 新增 `HistoryPlaybackOpenResult`,封装播放结果与失败提示。

- 3. 新增 `_HistorySourceBadge`,在历史卡片上标识在线或缓存来源。

- 4. 更新 `BangumiHistoryCardV`,使用新的播放处理并在标题旁展示来源标识。
@liangyuR liangyuR force-pushed the codex/history-online-offline-sync branch from f10c9fd to a2f06be Compare June 24, 2026 14:14
@Predidit

Copy link
Copy Markdown
Owner

没有风险,也不是明确的 test rule ,但这个工程从来没有考虑过 UI 可测试性的问题,大量的 widget 是不可测试的

现在添加 widget 测试很古怪,容易脆弱

Comment thread lib/pages/player/player_controller.dart Outdated
late final PlayerSyncPlayController syncplay = PlayerSyncPlayController(
bangumiId: () => bangumiId,
currentEpisode: () => currentEpisode,
currentEpisode: () => currentDanmakuEpisodeNumber,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: SyncPlay episode round-trip is now inconsistent.

The SyncPlay file name is broadcast as "$bangumiId[$currentDanmakuEpisodeNumber]" (see player_controller.dart:184 and player_syncplay_controller.dart:242), so this callback now exposes the danmaku episode number (parsed from the episode title via extractEpisodeNumber). However, the receiver parses that number out of the file name and feeds it straight into changeEpisode(episode, ...) (player_syncplay_controller.dart:159), where the first argument is treated as the 1-based playlist index (_resolveOnlineEpisode does index = episode - 1 into roadData.data).

Before this rename the broadcast value and changeEpisode both used the playlist index, so they matched. Now, whenever the title's parsed number differs from the list position (e.g. a split-cour road where 第13话 sits at position 1), peers will jump to the wrong position or hit 集数解析失败. Consider broadcasting/consuming the playlist index consistently, or mapping the received danmaku number back to a list index before calling changeEpisode.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已确认。这确实是一个 SyncPlay 往返回归问题,但不属于此 PR 的范围。我会在单独的 PR 中修复它,恢复 SyncPlay 中一致的播放列表索引处理。 @Predidit

@liangyuR

Copy link
Copy Markdown
Contributor Author

也许应该建一个 AGENTS.md 以及 docs 说明,比如在 test-rule.md 中将 “不考虑 UI 可测试性“ 这项规则加入

@Predidit

Copy link
Copy Markdown
Owner

有少量代码清洁性问题,部分死代码没删,例如

  1. lib/pages/video/video_controller.dart 里的 actualEpisodeNumberForSelection 函数
  2. lib/pages/video/video_controller.dart 里的 currentEpisode/currentRoad getter/setter

@Predidit

Copy link
Copy Markdown
Owner

上面bot的评论是需要更正的,syncplay 有些问题,虽然之前也有问题,但是现在语义上特别糟糕

发送的是 标题解析集数,接收端当成 播放列表位置

要么我们和之前一样两边均使用播放列表位置,并把问题留到下个PR

要么在此PR里进行更大的变更

liangyuR added 2 commits June 24, 2026 22:29
- 恢复 PlaybackInitParams 的播放列表位置字段,用于 SyncPlay 往返协议。

- 保留 danmakuEpisodeNumber 仅用于弹幕加载与历史身份。

- 补充测试覆盖播放列表位置和弹幕集数并存。
@Predidit Predidit merged commit 07a2c64 into Predidit:main Jun 24, 2026
9 checks passed
@liangyuR liangyuR deleted the codex/history-online-offline-sync branch June 24, 2026 15:48
ErBWs pushed a commit to ErBWs/Kazumi that referenced this pull request Jun 25, 2026
* ### fix: 分离在线与离线观看历史记录

1. 为历史记录增加在线/离线身份并按 scoped key 存储。

2. 同步 WebDAV 历史事件、播放恢复和离线入口,避免记录互相覆盖。

3. 增加历史仓储、同步合并和集数解析回归测试。

* ### fix: 清理视频 URL 构造 no-op 分支

1. 将在线播放 URL 补全逻辑折回单 guard。

2. 移除 robot 指出的 urlItem 自赋值死代码。

* ### Feat:增强观看历史卡片播放处理与来源标识

- 1. 新增 `openHistoryPlaybackForEntry`,统一在线与缓存观看记录的播放入口处理。

- 2. 新增 `HistoryPlaybackOpenResult`,封装播放结果与失败提示。

- 3. 新增 `_HistorySourceBadge`,在历史卡片上标识在线或缓存来源。

- 4. 更新 `BangumiHistoryCardV`,使用新的播放处理并在标题旁展示来源标识。

* Remove obsolete episode selection helpers

* ### Fix:恢复 SyncPlay 播放列表位置语义

- 恢复 PlaybackInitParams 的播放列表位置字段,用于 SyncPlay 往返协议。

- 保留 danmakuEpisodeNumber 仅用于弹幕加载与历史身份。

- 补充测试覆盖播放列表位置和弹幕集数并存。

---------

Co-authored-by: liangyu Ran <ranliangyu@enneagon.com>
(cherry picked from commit 07a2c64)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants