Skip to content

修复离线/在线历史记录与离线路线同步问题#2257

Closed
liangyuR wants to merge 16 commits into
Predidit:mainfrom
liangyuR:feat/观看历史记录

Hidden character warning

The head ref may contain hidden characters: "feat/\u89c2\u770b\u5386\u53f2\u8bb0\u5f55"
Closed

修复离线/在线历史记录与离线路线同步问题#2257
liangyuR wants to merge 16 commits into
Predidit:mainfrom
liangyuR:feat/观看历史记录

Conversation

@liangyuR

@liangyuR liangyuR commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

目标

修复离线/在线播放历史记录的同步与恢复问题,并保留离线播放时的原始线路语义,避免在线历史、离线历史、弹幕、下载状态和 SyncPlay 在同一番同一集上互相覆盖或错位。

修改的部分

  • 历史记录同步

    • History.keyentryKind scoped,在线与离线历史分开保存和同步。
    • Progress 增加 updatedAtMs,WebDAV merge 可按单集更新时间合并。
    • 兼容 legacy key 与 legacy delete tombstone,旧数据不会错误复活或误删离线历史。
  • 播放与集数身份

    • 引入轻量 ResolvedEpisode / episode identity 边界,统一表达 listIndex、展示标题、源站 URL、历史集数、弹幕集数和原始 road。
    • 自动切集、弹幕缓存、历史续播、下载状态和 SyncPlay 逐步接入统一的集数语义。
  • 离线线路与 SyncPlay

    • 离线 roadList 按 DownloadEpisode.road 分组,不再把所有下载集伪造成单个 播放列表1
    • UI 使用离线 display road,历史记录和 SyncPlay identity 继续记录下载记录中的原始 road。
    • 新增 kazumi:v2 SyncPlay 集数协议,携带 bangumiIdroadIndexlistIndexepisodeNumber,同时兼容旧协议。
  • 下载兼容与恢复

    • 下载记录匹配改为 URL 优先,并兼容旧记录缺少 episodePageUrl 的情况。
    • 启动、重试、全部恢复和优先下载前会校准本地 m3u8 分片、直链临时文件和完成文件,避免重启后进度被清零。
    • 用户主动暂停产生的取消异常按正常暂停处理,不再写入错误状态。

测试项目

  • flutter test test/resolved_episode_test.dart test/syncplay_episode_codec_test.dart
    • 覆盖 ResolvedEpisode 在线/离线集数语义、离线 roadList 多线路分组、SyncPlay v2 编解码和 legacy 协议兼容。
  • flutter test test/history_repository_test.dart test/history_sync_test.dart
    • 覆盖 online/offline 历史隔离、legacy key canonicalize、delete tombstone、WebDAV 本地状态事件和单集进度合并。
  • git diff --check origin/main...HEAD
    • 已通过。
  • flutter analyze
    • 仍因仓库既有 64 条 info 级 lint 返回 1,未见本次改动相关的新 error。
  • 手动验证
    • 下载页打开非首线路离线集。
    • 离线菜单多线路显示。
    • 同线路自动下一集。
    • 历史离线恢复。
    • SyncPlay 离线线路同步。

后续可能继续修改的功能

  • 继续推进全链路统一集数身份模型,进一步收敛 episodeNumbercurrentEpisoderoadList.dataidentifier 对列表位置、真实集号、源站 URL 和展示标题的混用。

liangyu Ran added 9 commits June 23, 2026 10:59
…mestamps

- Introduced `normalize` method in `HistoryEntryKind` to standardize entry types.
- Updated `History` and `Progress` classes to support entry kind and timestamp management.
- Modified repository methods to handle entry kind in history retrieval and progress updates.
- Improved synchronization logic to maintain separate online and offline histories.
- Added tests to ensure correct handling of entry kinds and progress timestamps.
…lution

- Updated `PlayerController` to directly use `danmakuEpisodeNumber` for loading danmaku.
- Modified `PlaybackInitParams` to include `danmakuEpisodeNumber`.
- Enhanced `_VideoPageController` to resolve episodes more effectively, improving history tracking and logging.
- Refactored methods to utilize `ResolvedEpisode` for better clarity and maintainability.
- Added methods to `DownloadController` for finding episodes by URL, number, and name.
- Implemented logic to check if an episode's download status is claimed.
- Updated `DownloadEpisodeMatcher` to improve episode lookup capabilities.
- Refactored `DownloadEpisodeSheet` and `VideoPage` to utilize new matching methods for better download status handling.
- Improved repository interface to support enhanced episode retrieval.
- Enhanced `DownloadController` with new methods for episode retrieval by various identifiers.
- Implemented checks for claimed download statuses of episodes.
- Updated `DownloadEpisodeMatcher` for better episode lookup efficiency.
- Refactored `DownloadEpisodeSheet` and `VideoPage` to leverage new matching capabilities for improved download status management.
- Enhanced repository interface for more effective episode retrieval.
…agement

- Introduced a new method in `DownloadManager` for reconciling episode data with local files, improving download state accuracy.
- Updated `_DownloadController` to reset incomplete episode statuses and reconcile episodes before resuming downloads.
- Enhanced error handling and logging for reconciliation failures.
- Improved progress tracking and state updates for episodes during download operations.
…e management

- Added SyncPlayEpisodeIdentity class to encapsulate episode identification details.
- Updated PlayerController and related classes to utilize SyncPlayEpisodeIdentity for episode handling.
- Refactored methods to improve episode resolution and synchronization in sync play scenarios.
- Enhanced error handling and logging for episode identity resolution.
- Updated tests to validate new SyncPlay functionality and ensure correct episode identity handling.
- Introduced OfflineRoadListSnapshot for improved organization of downloaded episodes by original road.
- Updated video controller to utilize the new snapshot for building offline road lists and resolving episodes.
- Enhanced methods for finding offline episodes, including preferred road handling.
- Improved UI to reflect current road names and episode identifiers dynamically.
- Added tests for OfflineRoadListSnapshot to ensure correct grouping and episode retrieval logic.
- Updated history synchronization logic to incorporate legacy entity keys for improved version management.
- Introduced a new method to determine the appropriate deleted version for upserts based on entry kind.
- Enhanced tests to validate the behavior of legacy delete tombstones and their impact on online and offline upserts.
- Ensured that legacy delete tombstones do not block offline upserts while correctly managing online upserts.
@Predidit

Copy link
Copy Markdown
Owner

审查前需要优先解决合并冲突

@liangyuR

Copy link
Copy Markdown
Contributor Author

#1984

@Predidit

Copy link
Copy Markdown
Owner

我看到 #1984 了,但是这个pr的合并冲突确实需要优先处理

@liangyuR liangyuR marked this pull request as draft June 23, 2026 09:28
liangyu Ran and others added 2 commits June 23, 2026 17:46
1. 合并 upstream/main 并保留 services 目录迁移后的历史、下载和播放修复

2. 合并 HistorySync watch-state 拆分与 online/offline scoped 历史语义

3. 更新本地 todo 记录冲突解决状态和后续手动边界验证
- Updated package versions for several dependencies, including _fe_analyzer_shared (99.0.0) and analyzer (12.1.0).
- Changed package source URLs from "https://pub.dev" to "https://pub.flutter-io.cn".
- Removed unused fields in bangumi_history_card.dart to streamline the code.
@liangyuR liangyuR marked this pull request as ready for review June 23, 2026 13:31
Comment thread lib/modules/history/history_sync.dart Outdated
final keyMap = {
for (final history in histories) history.key: history.key,
for (final history in histories)
History.legacyKey(history.adapterName, history.bangumiItem):

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: The legacy key is mapped to a history's scoped key for every history regardless of entryKind. If a snapshot contains an offline history for a (plugin, id) pair together with a legacy bare-key online delete tombstone in deletedVersions, _canonicalizeVersionMap rewrites that online tombstone onto the offline entry's scoped key (...::offline). A subsequent offline upsert then reads that (online) deletion via _deletedVersionForUpsert and can be wrongly suppressed, silently dropping offline history.

Consider only registering the legacy→scoped mapping for online entries (e.g. guard with HistoryEntryKind.normalize(history.entryKind) == HistoryEntryKind.online). The same pattern exists in HistorySyncState.fromSnapshot (line 361) and in the _canonicalExistingEntityKey loop (line 585), so a legacy-form delete event can also resolve onto an offline entry when only an offline entry exists locally.


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

@kilo-code-bot

kilo-code-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Incremental Review (since 4f55f58)

The latest commits (01cfe39, ca0ab30) revert the SyncPlay v2 protocol refactor and simplify episode plumbing:

  • SyncPlayEpisodeIdentity / SyncPlayEpisodeCodec removed from player_models.dart; SyncPlay now uses the legacy "$bangumiId[$episode]" string + regex parsing (matches base branch behavior).
  • player_controller.dart consolidates the episode field into currentDanmakuEpisodeNumber, applied consistently across the SyncPlay file-name encode/compare path.
  • video_controller.dart and download_repository.dart drop the now-unused resolution helpers (resolveSyncPlayEpisodeIdentity, findEpisode, getEpisodeByUrl, etc.).
  • history_repository.dart adds injectable sync appenders (HistoryProgressSyncAppender, HistoryDeleteSyncAppender, HistoryClearSyncAppender) with defaults that preserve prior behavior; tests updated with no-op appenders.
  • Obsolete tests (syncplay_episode_codec_test.dart, parts of resolved_episode_test.dart) removed.

Notes / assumptions:

  • player_syncplay_controller.dart passes message['name'] (nullable dynamic) directly to RegExp.firstMatch, which can throw if a file-changed message lacks a name. This file is net-unchanged vs. the base branch (the v2 code was added and then reverted within this PR), so it is pre-existing and out of scope for this review.

No new issues found on lines changed by this PR.

Files Reviewed in this increment
  • lib/pages/player/controller/player_models.dart - removed v2 codec/identity
  • lib/pages/player/controller/player_syncplay_controller.dart - reverted to legacy parsing (net-unchanged vs base)
  • lib/pages/player/player_controller.dart - field consolidation
  • lib/pages/player/player_item.dart - removed identity-based episode change
  • lib/pages/video/video_controller.dart - removed resolution helpers
  • lib/repositories/download_repository.dart - removed unused lookup methods
  • lib/repositories/history_repository.dart - injectable sync appenders
  • test/history_repository_test.dart, test/resolved_episode_test.dart, test/syncplay_episode_codec_test.dart - test updates/removals
Previous Review Summaries (4 snapshots, latest commit 4f55f58)

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

Previous review (commit 4f55f58)

Status: No Issues Found | Recommendation: Merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 0

Incremental Review (since 6c19e51)

The latest commits resolve the previously carried-over WARNING. Legacy→scoped key mapping in history_sync.dart is now guarded by entryKind == HistoryEntryKind.online at all three sites (L280 fromJson, L362 fromSnapshot, L588 _canonicalExistingEntityKey), so legacy online delete tombstones can no longer be canonicalized onto offline entries. New tests in test/history_sync_test.dart cover both the offline-snapshot canonicalization and legacy delete-event cases.

history_repository.dart was refactored to inject historiesBox and privateModeReader for testability (defaults preserve prior behavior), and the now-unused setSettingsBoxForTest / clearSettingsForTest helpers were removed from storage.dart (verified no remaining references).

Previously flagged WARNING is now resolved. No new issues found.

Files Reviewed in this increment (5 files)
  • lib/modules/history/history_sync.dart - resolved prior WARNING
  • lib/repositories/history_repository.dart - dependency injection refactor
  • lib/services/storage/storage.dart - removed unused test helpers
  • test/history_repository_test.dart - updated tests
  • test/history_sync_test.dart - new test coverage

Previous review (commit 6c19e51)

Status: 1 Issue Found (unchanged from prior review) | Recommendation: Address before merge

Overview

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

WARNING

File Line Issue
lib/modules/history/history_sync.dart 280 Legacy→scoped key map is built for every history regardless of entryKind; a legacy online delete tombstone can be canonicalized onto an offline entry and suppress offline upserts (also affects fromSnapshot L361 and _canonicalExistingEntityKey L585). (Carried over — file not modified in latest commits; still unresolved.)

Incremental Review (since 0593c34)

The only change in the latest commit (6c19e51) is pubspec.lock, synced to upstream main. This is a generated lock file and is excluded from review.

No new issues found in the incremental diff.

Files Reviewed in this increment (1 file)
  • pubspec.lock (generated lock file — skipped)

Fix these issues in Kilo Cloud

Previous review (commit 0593c34)

Status: 1 Issue Found (unchanged from prior review) | Recommendation: Address before merge

Overview

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

WARNING

File Line Issue
lib/modules/history/history_sync.dart 280 Legacy→scoped key map is built for every history regardless of entryKind; a legacy online delete tombstone can be canonicalized onto an offline entry and suppress offline upserts (also affects fromSnapshot L361 and _canonicalExistingEntityKey L585). (Carried over — file not modified in latest commits; still unresolved.)

Incremental Review (since 406c8b0)

Latest commits changed only test infrastructure:

  • lib/services/storage/storage.dart — added setSettingsBoxForTest / clearSettingsForTest helpers and removed a blank import line. _setting is late final; the setter is invoked once from setUpAll and the clear helper only clears the box, so both are safe in the test context.
  • test/history_repository_test.dart — switched to the new helpers instead of the previously public setting field.

No new issues found in the incremental diff.

Files Reviewed in this increment (2 files)
  • lib/services/storage/storage.dart
  • test/history_repository_test.dart

Fix these issues in Kilo Cloud

Previous review (commit 406c8b0)

Status: 1 Issue Found | Recommendation: Address before merge (note: maintainer already flagged unresolved merge conflicts that must be resolved first)

Overview

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

WARNING

File Line Issue
lib/modules/history/history_sync.dart 280 Legacy→scoped key map is built for every history regardless of entryKind; a legacy online delete tombstone can be canonicalized onto an offline entry and suppress offline upserts (also affects fromSnapshot L361 and _canonicalExistingEntityKey L585).
Files Reviewed (26 files)

History / sync / storage

  • lib/modules/history/history_module.dart
  • lib/modules/history/history_module.g.dart
  • lib/modules/history/history_sync.dart - 1 issue
  • lib/pages/history/history_controller.dart
  • lib/repositories/history_repository.dart
  • lib/services/sync/history_sync_service.dart
  • lib/services/sync/webdav.dart
  • lib/services/storage/storage.dart
  • lib/bean/card/bangumi_history_card.dart

Download

  • lib/pages/download/download_controller.dart
  • lib/pages/download/download_episode_sheet.dart
  • lib/repositories/download_repository.dart
  • lib/services/download/download_manager.dart

Player / video / syncplay

  • lib/pages/player/controller/player_models.dart
  • lib/pages/player/controller/player_syncplay_controller.dart
  • lib/pages/player/player_controller.dart
  • lib/pages/player/player_item.dart
  • lib/pages/video/video_controller.dart
  • lib/pages/video/video_page.dart

Other / tests / generated

  • .gitignore
  • android/app/src/main/AndroidManifest.xml
  • pubspec.lock (generated, skipped)
  • test/download_cancellation_test.dart
  • test/download_episode_matcher_test.dart
  • test/download_progress_reconcile_test.dart
  • test/history_repository_test.dart
  • test/history_sync_test.dart
  • test/resolved_episode_test.dart
  • test/syncplay_episode_codec_test.dart

Notes

  • The episode-identity rework, SyncPlay kazumi:v2 codec (encode/decode + legacy compatibility), offline roadList grouping, download progress reconciliation, and pause/cancel handling were reviewed and appear consistent with the included tests; bounds checks now guard previously crash-prone roadList/identifier/data indexing.
  • The flagged WARNING is a narrow but real cross-version edge case (offline entry + legacy bare-key online tombstone in the same snapshot).

Fix these issues in Kilo Cloud


Reviewed by claude-4.8-opus-20260528 · Input: 4.6K · Output: 9.7K · Cached: 588.9K

@Predidit

Copy link
Copy Markdown
Owner

CI 没有通过,除了自动代码审查机器人指出的问题,语法分析没有通过

更改 pubspec.lock 需要恰当的理由,再本地使用 flutter 大陆地区镜像是可以的,但是不要把它们提交到云端,云端的 CI 服务器运行在大陆之外的地区,使用大陆地区镜像反而容易出问题

liangyuR added 2 commits June 23, 2026 21:48
- 添加 GStorage 测试专用 settings box 初始化与清理方法

- 更新 history_repository_test 避免访问已移除的 GStorage.setting 接口

- 通过 flutter analyze 和完整 flutter test 验证
- 恢复 pubspec.lock 为 upstream/main 当前版本

- 移除当前 PR 分支中的锁文件差异
@Predidit

Copy link
Copy Markdown
Owner

直接把 测试后门 塞到存储持久化实现文件中,是很糟糕的实践,可以知道你在使用哪个模型进行辅助编程吗

@liangyuR

Copy link
Copy Markdown
Contributor Author

Codex 5.5 xHigh,我有 Cpp 5年左右的经验,对 Flutter 这类不是很熟悉,正在学习

@liangyuR

Copy link
Copy Markdown
Contributor Author

生产部分的测试后门我稍后会移除

- 更新历史记录同步逻辑,根据条目类型有条件地处理旧版键,确保在线和离线历史记录得到正确管理。
- 重构 HistoryRepository,使其支持自定义 histories box 和隐私模式读取器,提升可测试性与灵活性。
- 移除与 GStorage 设置相关的过时测试方法,简化测试初始化流程。
- 新增测试,用于验证隐私模式下历史记录更新行为,以及旧版删除事件的处理逻辑。
@Predidit

Copy link
Copy Markdown
Owner

SyncPlay v2 改造的目的是什么,实际上切换 road 并不会触发一次硬同步,这样行为和之前基本一致,还新增了大量代码,在这里进行 road 级别的同步似乎也没有太大的意义

@Predidit

Copy link
Copy Markdown
Owner

此PR的代码清洁性也有问题,看上去多次迭代后死代码和过时的接口没有正确清理

@liangyuR liangyuR marked this pull request as draft June 23, 2026 14:38
@liangyuR

Copy link
Copy Markdown
Contributor Author

我PR的最后一句话 “推进全链路统一集数身份模型,进一步收敛 episodeNumber、currentEpisode、roadList.data、identifier 对列表位置、真实集号、源站 URL 和展示标题的混用” ,在修改在线,离线历史记录同步时我发现很多对 “集” 的描述有问题,最经典的就是下载后,list 12345[6] ,这里的数字毫无意义,只是描述当前下载的 index。

这次设计 v2 的主要原因是:旧的 SyncPlay payload 只有 bangumiId[episode],这里的 episode 实际上混用了多个概念:

  • currentEpisode:当前 roadList 里的列表位置
  • episodeNumber:真实集号 / 弹幕集号
    - roadList.data:在线时是源站 URL,离线时可能被复用成集号(这是我修改它的关键因素)
  • identifier:展示标题,但之前也会被用来推断集号
  • road:在线线路索引,离线时还需要保留原始下载线路

这些概念在观看历史、在线播放、离线播放、弹幕加载和 SyncPlay 之间反复转换,旧协议无法表达完整身份,只能靠列表位置猜测。这会导致离线播放、标题集号不连续、不同线路列表结构不一致时,同步目标和历史记录目标不稳定。

所以 SyncPlay v2 的意义是引入一个更明确的 episode identity:

  • bangumiId:作品身份
  • roadIndex:来源线路,用于优先解析
  • listIndex:当前列表中的位置
  • episodeNumber:真实集号 / 弹幕集号

这样 SyncPlay 不再只同步一个模糊的列表下标,而是可以携带足够信息,让接收端根据自己的在线/离线 roadList 去解析同一个“规范集数目标”。这也是后续收敛观看历史、离线播放、弹幕和同步逻辑到统一集数身份模型的基础。

因此 v2 当前的设计重点不是 road 级硬同步,而是为全链路统一 episode identity 做协议层铺垫,并保持旧版 bangumiId[episode] 的兼容。

@Predidit

Copy link
Copy Markdown
Owner

听上去超棒的想法,但是我们要么应该在这里直接完成完全体的 syncplay v2 ,要么就在这个PR中移除这部分,在之后单独的PR中实现

当前PR中的实现是半成品,只是增加了冗余代码,没有看到好处

@liangyuR

Copy link
Copy Markdown
Contributor Author

你说的确实有道理,它实际上与这个PR的解决目标不一致。

liangyuR added 2 commits June 23, 2026 23:12
- 从观看历史分支移除 SyncPlay v2 identity、codec 和接收端解析逻辑

- 保留 ResolvedEpisode、danmakuEpisodeNumber、在线离线历史修复

- 删除 SyncPlay v2 专属测试断言
@liangyuR liangyuR marked this pull request as ready for review June 23, 2026 15:37
@Predidit

Copy link
Copy Markdown
Owner

还是有一些代码清洁性问题,例如

  1. syncplay 实现已经回滚,但是 currentEpisode 转换为 currentDanmakuEpisodeNumber 没有回滚
  2. history_repository 中的相关变更,似乎有一个副作用,清除某一集进度会同步成“最后观看集变成这一集” ,这符合预期吗

@Predidit

Copy link
Copy Markdown
Owner

在更仔细的查看这个实现后,我理解了 syncplay 变更的重要性,以及此PR之前的离线视频使用一起看时的严重bug

也就是离线视频自己的分集列表长度与完整分集列表存在巨大出入,可能出现 syncplay 同步时的分集漂移甚至越界

想知道你怎么看这个问题

@liangyuR

Copy link
Copy Markdown
Contributor Author

这个问题的原因很明显就是底层 “在观看历史、在线播放、离线播放、弹幕加载和 SyncPlay 之间反复转换”,原有的 数据模型 不能完整表达身份。这个问题同时还影响历史记录跳转问题

它应该另起一个 RP,对数据模型,协议进行一个整体的规划和重新设计,包括之前提交的 syncplay v2 并不能完全修复根因。

同时我也觉得这个PR过于臃肿,合并风险较高,我会关闭它,后续再陆续提供几个较小的PR逐步推进目标

@liangyuR liangyuR closed this Jun 24, 2026
@liangyuR liangyuR deleted the feat/观看历史记录 branch June 24, 2026 05:23
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