feat: Add batch PV conversion tool#63
Conversation
A new tool in the Tools page that bulk converts every PV (Promotional Video) under StreamingAssets/A###/MovieData in both directions: - USM/DAT -> MP4: writes a sibling .mp4 next to each source, original kept untouched - MP4 -> USM/DAT: writes to a temp file, validates non-empty, atomically swaps in the final .dat, then sends the source .mp4 to the recycle bin Implementation notes: - New endpoint VideoConvertToolController.BatchConvertPvTool, SSE with events Progress / FileError / Success / Cancelled / Error. - Progress payload is JSON (camelCase) with processed / total / fileProgress / fileName / failed. - All SSE frames go through a single-writer Channel<string> so that Xabe's synchronous OnProgress events from inside FFmpeg cannot interleave Response.WriteAsync calls. - Direct file system enumeration via StaticSettings.AssetsDirs rather than MovieDataMap, so files that share an ID across multiple asset dirs (or .mp4 + .dat siblings) are not silently skipped. - Cancellation is checked before destructive file ops; a partial output in temp is removed before throwing, so cancel cannot corrupt the source. - Sponsored feature, gated on IapManager.License == Active. - Final settings.ScanMovieData() in the finally block resynchronizes MovieDataMap with on-disk state. Frontend: src/views/Tools/BatchVideoConvertModal.tsx, a 3-step modal (Configure direction -> live Progress with overall + current-file bars and a collapsible per-file error list -> Done with summary). Uses fetchEventSource + AbortController, matching the existing single-file VideoConvertModal pattern. Surfaces 'no files' and 'needs sponsor' as friendly toasts instead of crash reports. Notes: - Locale.Designer.cs was updated by hand to expose the two new resource strings. - Front/src/client/apiGen.ts was NOT regenerated (requires a running backend on localhost:5181). The new endpoint is consumed directly via getUrl + fetchEventSource, matching the other SSE tool endpoints, so no client regen is strictly required, but a follow-up `pnpm genClient` is recommended after merging.
审阅者指南添加了一个新的赞助用户专用批量 PV 转换工具,该工具直接从 StreamingAssets 枚举 PV 文件,通过使用序列化的 Channel 写入器以 SSE 方式流式传输批处理进度,并在前端提供一个三步模态框,包含双进度条、取消功能、错误提示(toast)以及 i18n 支持。 批量 PV 转换 SSE 流程时序图sequenceDiagram
actor User
participant BatchVideoConvertModal
participant fetchEventSource
participant VideoConvertToolController
participant Channel_string as Channel_string
participant WriteSseFrames
participant VideoConvert
User ->> BatchVideoConvertModal: trigger()
BatchVideoConvertModal ->> fetchEventSource: fetchEventSource(BatchConvertPvToolApi)
fetchEventSource ->> VideoConvertToolController: BatchConvertPvTool(direction)
VideoConvertToolController ->> VideoConvertToolController: EnumerateMoviePvs()
VideoConvertToolController ->> WriteSseFrames: WriteSseFrames(ChannelReader, RequestAborted)
activate WriteSseFrames
loop each file
VideoConvertToolController ->> Channel_string: EnqueueProgress(Processed, Total, 0, FileName, Failed)
Channel_string ->> WriteSseFrames: ReadAllAsync()
WriteSseFrames ->> VideoConvertToolController: Response.WriteAsync(frame)
alt direction == UsmToMp4
VideoConvertToolController ->> VideoConvert: ConvertUsmToMp4(inputPath, outputPath, OnProgress)
else direction == Mp4ToUsm
VideoConvertToolController ->> VideoConvert: ConvertVideo(VideoConvertOptions)
end
Note over VideoConvertToolController,Channel_string: OnProgress -> EnqueueProgressFireAndForget
VideoConvertToolController ->> Channel_string: EnqueueProgress(Processed, Total, 100, FileName, Failed)
end
alt success
VideoConvertToolController ->> Channel_string: EnqueueEvent(Success, "processed/total|failed")
else cancelled
VideoConvertToolController ->> Channel_string: EnqueueEvent(Cancelled, "processed/total")
else error
VideoConvertToolController ->> Channel_string: EnqueueEvent(Error, Locale.ConvertFailed)
end
Channel_string ->> WriteSseFrames: Complete
deactivate WriteSseFrames
WriteSseFrames ->> fetchEventSource: SSE events (Progress, FileError, Success, Cancelled, Error)
fetchEventSource ->> BatchVideoConvertModal: onmessage(e)
BatchVideoConvertModal ->> BatchVideoConvertModal: update state / finishKind
User ->> BatchVideoConvertModal: cancel()
BatchVideoConvertModal ->> fetchEventSource: AbortController.abort()
fetchEventSource ->> VideoConvertToolController: RequestAborted
VideoConvertToolController ->> VideoConvertToolController: ThrowIfCancellationRequested()
VideoConvertToolController ->> Channel_string: EnqueueEvent(Cancelled, "processed/total")
文件级变更
提示与命令与 Sourcery 交互
自定义你的体验访问你的 控制面板 以:
获取帮助Original review guide in EnglishReviewer's GuideAdds a new sponsor-gated batch PV conversion tool that enumerates PV files directly from StreamingAssets, streams batch progress via SSE using a serialized Channel writer, and exposes a three-step front-end modal with dual progress bars, cancellation, error toasts, and i18n support. Sequence diagram for batch PV conversion SSE flowsequenceDiagram
actor User
participant BatchVideoConvertModal
participant fetchEventSource
participant VideoConvertToolController
participant Channel_string as Channel_string
participant WriteSseFrames
participant VideoConvert
User ->> BatchVideoConvertModal: trigger()
BatchVideoConvertModal ->> fetchEventSource: fetchEventSource(BatchConvertPvToolApi)
fetchEventSource ->> VideoConvertToolController: BatchConvertPvTool(direction)
VideoConvertToolController ->> VideoConvertToolController: EnumerateMoviePvs()
VideoConvertToolController ->> WriteSseFrames: WriteSseFrames(ChannelReader, RequestAborted)
activate WriteSseFrames
loop each file
VideoConvertToolController ->> Channel_string: EnqueueProgress(Processed, Total, 0, FileName, Failed)
Channel_string ->> WriteSseFrames: ReadAllAsync()
WriteSseFrames ->> VideoConvertToolController: Response.WriteAsync(frame)
alt direction == UsmToMp4
VideoConvertToolController ->> VideoConvert: ConvertUsmToMp4(inputPath, outputPath, OnProgress)
else direction == Mp4ToUsm
VideoConvertToolController ->> VideoConvert: ConvertVideo(VideoConvertOptions)
end
Note over VideoConvertToolController,Channel_string: OnProgress -> EnqueueProgressFireAndForget
VideoConvertToolController ->> Channel_string: EnqueueProgress(Processed, Total, 100, FileName, Failed)
end
alt success
VideoConvertToolController ->> Channel_string: EnqueueEvent(Success, "processed/total|failed")
else cancelled
VideoConvertToolController ->> Channel_string: EnqueueEvent(Cancelled, "processed/total")
else error
VideoConvertToolController ->> Channel_string: EnqueueEvent(Error, Locale.ConvertFailed)
end
Channel_string ->> WriteSseFrames: Complete
deactivate WriteSseFrames
WriteSseFrames ->> fetchEventSource: SSE events (Progress, FileError, Success, Cancelled, Error)
fetchEventSource ->> BatchVideoConvertModal: onmessage(e)
BatchVideoConvertModal ->> BatchVideoConvertModal: update state / finishKind
User ->> BatchVideoConvertModal: cancel()
BatchVideoConvertModal ->> fetchEventSource: AbortController.abort()
fetchEventSource ->> VideoConvertToolController: RequestAborted
VideoConvertToolController ->> VideoConvertToolController: ThrowIfCancellationRequested()
VideoConvertToolController ->> Channel_string: EnqueueEvent(Cancelled, "processed/total")
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - 我发现了两个问题,并给出了一些高层次的反馈:
- 在
BatchVideoConvertModal中,通用错误路径里的console.log(e)会把内部错误细节泄露到浏览器控制台;建议移除它,或者改为通过已有的globalCapture/日志系统上报,以保持与其他工具一致的行为。 finishSummary中的汇总解析逻辑(按/和|分割并计算成功/失败数量)在closeDone和renderDone之间是重复的;建议抽取一个小的辅助函数来集中这段解析逻辑,从而减少两处实现逐渐产生偏差的风险。
给 AI Agent 的提示词
Please address the comments from this code review:
## Overall Comments
- 在 `BatchVideoConvertModal` 中,通用错误路径里的 `console.log(e)` 会把内部错误细节泄露到浏览器控制台;建议移除它,或者改为通过已有的 `globalCapture`/日志系统上报,以保持与其他工具一致的行为。
- `finishSummary` 中的汇总解析逻辑(按 `/` 和 `|` 分割并计算成功/失败数量)在 `closeDone` 和 `renderDone` 之间是重复的;建议抽取一个小的辅助函数来集中这段解析逻辑,从而减少两处实现逐渐产生偏差的风险。
## Individual Comments
### Comment 1
<location path="MaiChartManager/Controllers/Tools/VideoConvertToolController.cs" line_range="158-164" />
<code_context>
+ var cancellationToken = HttpContext.RequestAborted;
+
+ // 单写者 Channel:所有 SSE 帧(不论来自循环还是 OnProgress)都进入这条队列
+ var sseChannel = Channel.CreateUnbounded<string>(new UnboundedChannelOptions
+ {
+ SingleReader = true,
+ SingleWriter = false,
+ });
+
+ var writer = WriteSseFrames(sseChannel.Reader, cancellationToken);
+
+ try
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 建议对 SSE 进度通道进行限界或节流,以避免在客户端较慢或进度事件非常频繁的情况下导致内存无限增长。
由于这里使用的是无界的 `Channel<string>`,并结合 `EnqueueProgressFireAndForget`,当客户端处理速度较慢或被阻塞、且进度回调很频繁时,尤其是在长时间运行的批量任务中,队列可能会无限增长。建议考虑使用有界 Channel,并对进度事件采用丢弃/覆盖策略,或者对进度更新进行节流/合并(例如只在进度变化 ≥1% 或按固定时间间隔入队),从而控制内存使用。
Suggested implementation:
```csharp
// 单写者 Channel:所有 SSE 帧(不论来自循环还是 OnProgress)都进入这条队列
// 使用有界 Channel,并在队列满时丢弃最旧的进度帧,防止在慢客户端/高频进度回调下无限占用内存
var sseChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(100)
{
SingleReader = true,
SingleWriter = false,
FullMode = BoundedChannelFullMode.DropOldest,
});
```
1. 如果 `EnqueueProgress`/`EnqueueProgressFireAndForget` 当前使用的是 `WriteAsync` 这类可能阻塞的写入方式,则需要改为使用 `TryWrite`,或对 `WriteAsync` 的失败/取消做容错处理,以适配 `BoundedChannelFullMode.DropOldest` 的策略。
2. 如果希望进一步节流/合并进度事件(例如仅在进度提升 ≥1% 或按固定时间间隔发送),可以在 `EnqueueProgress` 内部记录上一次发送的进度百分比/时间戳,在未满足阈值时直接返回而不写入 Channel。
</issue_to_address>
### Comment 2
<location path="MaiChartManager/Front/src/views/Tools/BatchVideoConvertModal.tsx" line_range="167-171" />
<code_context>
+ }
+ // 已知的友好错误(无文件 / 需要赞助):toast 提示并回到 Configure,不上报
+ const message: string = e?.message ?? '';
+ const friendlyMessages = [
+ t('tools.batchPv.noFiles'),
+ t('tools.batchPv.needLicense'),
+ ];
+ if (friendlyMessages.includes(message)) {
+ addToast({ message, type: 'warning' });
+ step.value = STEP.Configure;
</code_context>
<issue_to_address>
**issue (bug_risk):** 在服务器和客户端之间通过错误文案比对来判断逻辑是很脆弱的;更推荐使用结构化错误码或事件类型。
这里的 `friendlyMessages` 会将 `t('tools.batchPv.noFiles')` / `t('tools.batchPv.needLicense')` 与来自 SSE 层的 `e.message` 进行对比,而服务端则使用的是 `Locale.BatchConvertPvNoFiles` / `BatchConvertPvNeedLicense` 这两条文案。只要文案、标点或 `SanitizeSseLine` 的处理有任何差异,这种匹配就会失效,从而把已知情况降级为通用错误。建议改为由后端输出稳定的错误码或特定 SSE 事件类型(例如 `code: NO_FILES`),然后由前端根据这个错误码来选择 toast 内容和流程,而不是依赖本地化后的错误文案文本。
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会依据你的反馈改进后续的代码审查。
Original comment in English
Hey - I've found 2 issues, and left some high level feedback:
- In
BatchVideoConvertModal,console.log(e)in the generic error path will leak internal error details into the browser console; consider removing it or routing through your existingglobalCapture/logging instead to keep behavior consistent with other tools. - The summary parsing logic from
finishSummary(splitting on/and|and computing success/failed counts) is duplicated betweencloseDoneandrenderDone; consider extracting a small helper to centralize this parsing to reduce the chance of the two branches drifting apart.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `BatchVideoConvertModal`, `console.log(e)` in the generic error path will leak internal error details into the browser console; consider removing it or routing through your existing `globalCapture`/logging instead to keep behavior consistent with other tools.
- The summary parsing logic from `finishSummary` (splitting on `/` and `|` and computing success/failed counts) is duplicated between `closeDone` and `renderDone`; consider extracting a small helper to centralize this parsing to reduce the chance of the two branches drifting apart.
## Individual Comments
### Comment 1
<location path="MaiChartManager/Controllers/Tools/VideoConvertToolController.cs" line_range="158-164" />
<code_context>
+ var cancellationToken = HttpContext.RequestAborted;
+
+ // 单写者 Channel:所有 SSE 帧(不论来自循环还是 OnProgress)都进入这条队列
+ var sseChannel = Channel.CreateUnbounded<string>(new UnboundedChannelOptions
+ {
+ SingleReader = true,
+ SingleWriter = false,
+ });
+
+ var writer = WriteSseFrames(sseChannel.Reader, cancellationToken);
+
+ try
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider bounding or throttling the SSE progress channel to avoid unbounded memory growth under slow clients or very frequent progress events.
Because this is an unbounded `Channel<string>` combined with `EnqueueProgressFireAndForget`, a slow or blocked client plus frequent progress callbacks can cause unbounded queue growth, especially for long-running batch jobs. Consider either using a bounded channel with a drop/overwrite policy for progress events, or throttling/coalescing updates (e.g., only enqueue on ≥1% change or at a fixed interval) to keep memory usage controlled.
Suggested implementation:
```csharp
// 单写者 Channel:所有 SSE 帧(不论来自循环还是 OnProgress)都进入这条队列
// 使用有界 Channel,并在队列满时丢弃最旧的进度帧,防止在慢客户端/高频进度回调下无限占用内存
var sseChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(100)
{
SingleReader = true,
SingleWriter = false,
FullMode = BoundedChannelFullMode.DropOldest,
});
```
1. `EnqueueProgress`/`EnqueueProgressFireAndForget` 目前如果使用 `WriteAsync` 之类的阻塞写入,需要调整为使用 `TryWrite` 或对 `WriteAsync` 的失败/取消做容错处理,以配合 `BoundedChannelFullMode.DropOldest` 的策略。
2. 如果希望进一步节流/合并进度事件(例如仅在进度提升 ≥1% 或按固定时间间隔发送),可以在 `EnqueueProgress` 内部记录上一次发送的进度百分比/时间戳,在未满足阈值时直接返回而不写入 Channel。
</issue_to_address>
### Comment 2
<location path="MaiChartManager/Front/src/views/Tools/BatchVideoConvertModal.tsx" line_range="167-171" />
<code_context>
+ }
+ // 已知的友好错误(无文件 / 需要赞助):toast 提示并回到 Configure,不上报
+ const message: string = e?.message ?? '';
+ const friendlyMessages = [
+ t('tools.batchPv.noFiles'),
+ t('tools.batchPv.needLicense'),
+ ];
+ if (friendlyMessages.includes(message)) {
+ addToast({ message, type: 'warning' });
+ step.value = STEP.Configure;
</code_context>
<issue_to_address>
**issue (bug_risk):** Comparing error messages across server and client locales is fragile; prefer structured error codes or event types.
Here `friendlyMessages` compares `t('tools.batchPv.noFiles')` / `t('tools.batchPv.needLicense')` with `e.message` from the SSE layer, while the server uses separate `Locale.BatchConvertPvNoFiles` / `BatchConvertPvNeedLicense` strings. Any divergence in wording, punctuation, or sanitization (`SanitizeSseLine`) will break this match and downgrade known conditions to generic errors. Instead, have the backend emit a stable code or specific SSE event type (e.g., `code: NO_FILES`), and let the frontend choose the toast and flow based on that code rather than the localized message text.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| var sseChannel = Channel.CreateUnbounded<string>(new UnboundedChannelOptions | ||
| { | ||
| SingleReader = true, | ||
| SingleWriter = false, | ||
| }); | ||
|
|
||
| var writer = WriteSseFrames(sseChannel.Reader, cancellationToken); |
There was a problem hiding this comment.
suggestion (bug_risk): 建议对 SSE 进度通道进行限界或节流,以避免在客户端较慢或进度事件非常频繁的情况下导致内存无限增长。
由于这里使用的是无界的 Channel<string>,并结合 EnqueueProgressFireAndForget,当客户端处理速度较慢或被阻塞、且进度回调很频繁时,尤其是在长时间运行的批量任务中,队列可能会无限增长。建议考虑使用有界 Channel,并对进度事件采用丢弃/覆盖策略,或者对进度更新进行节流/合并(例如只在进度变化 ≥1% 或按固定时间间隔入队),从而控制内存使用。
Suggested implementation:
// 单写者 Channel:所有 SSE 帧(不论来自循环还是 OnProgress)都进入这条队列
// 使用有界 Channel,并在队列满时丢弃最旧的进度帧,防止在慢客户端/高频进度回调下无限占用内存
var sseChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(100)
{
SingleReader = true,
SingleWriter = false,
FullMode = BoundedChannelFullMode.DropOldest,
});- 如果
EnqueueProgress/EnqueueProgressFireAndForget当前使用的是WriteAsync这类可能阻塞的写入方式,则需要改为使用TryWrite,或对WriteAsync的失败/取消做容错处理,以适配BoundedChannelFullMode.DropOldest的策略。 - 如果希望进一步节流/合并进度事件(例如仅在进度提升 ≥1% 或按固定时间间隔发送),可以在
EnqueueProgress内部记录上一次发送的进度百分比/时间戳,在未满足阈值时直接返回而不写入 Channel。
Original comment in English
suggestion (bug_risk): Consider bounding or throttling the SSE progress channel to avoid unbounded memory growth under slow clients or very frequent progress events.
Because this is an unbounded Channel<string> combined with EnqueueProgressFireAndForget, a slow or blocked client plus frequent progress callbacks can cause unbounded queue growth, especially for long-running batch jobs. Consider either using a bounded channel with a drop/overwrite policy for progress events, or throttling/coalescing updates (e.g., only enqueue on ≥1% change or at a fixed interval) to keep memory usage controlled.
Suggested implementation:
// 单写者 Channel:所有 SSE 帧(不论来自循环还是 OnProgress)都进入这条队列
// 使用有界 Channel,并在队列满时丢弃最旧的进度帧,防止在慢客户端/高频进度回调下无限占用内存
var sseChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(100)
{
SingleReader = true,
SingleWriter = false,
FullMode = BoundedChannelFullMode.DropOldest,
});EnqueueProgress/EnqueueProgressFireAndForget目前如果使用WriteAsync之类的阻塞写入,需要调整为使用TryWrite或对WriteAsync的失败/取消做容错处理,以配合BoundedChannelFullMode.DropOldest的策略。- 如果希望进一步节流/合并进度事件(例如仅在进度提升 ≥1% 或按固定时间间隔发送),可以在
EnqueueProgress内部记录上一次发送的进度百分比/时间戳,在未满足阈值时直接返回而不写入 Channel。
| const friendlyMessages = [ | ||
| t('tools.batchPv.noFiles'), | ||
| t('tools.batchPv.needLicense'), | ||
| ]; | ||
| if (friendlyMessages.includes(message)) { |
There was a problem hiding this comment.
issue (bug_risk): 在服务器和客户端之间通过错误文案比对来判断逻辑是很脆弱的;更推荐使用结构化错误码或事件类型。
这里的 friendlyMessages 会将 t('tools.batchPv.noFiles') / t('tools.batchPv.needLicense') 与来自 SSE 层的 e.message 进行对比,而服务端则使用的是 Locale.BatchConvertPvNoFiles / BatchConvertPvNeedLicense 这两条文案。只要文案、标点或 SanitizeSseLine 的处理有任何差异,这种匹配就会失效,从而把已知情况降级为通用错误。建议改为由后端输出稳定的错误码或特定 SSE 事件类型(例如 code: NO_FILES),然后由前端根据这个错误码来选择 toast 内容和流程,而不是依赖本地化后的错误文案文本。
Original comment in English
issue (bug_risk): Comparing error messages across server and client locales is fragile; prefer structured error codes or event types.
Here friendlyMessages compares t('tools.batchPv.noFiles') / t('tools.batchPv.needLicense') with e.message from the SSE layer, while the server uses separate Locale.BatchConvertPvNoFiles / BatchConvertPvNeedLicense strings. Any divergence in wording, punctuation, or sanitization (SanitizeSseLine) will break this match and downgrade known conditions to generic errors. Instead, have the backend emit a stable code or specific SSE event type (e.g., code: NO_FILES), and let the frontend choose the toast and flow based on that code rather than the localized message text.
There was a problem hiding this comment.
1 issue found across 10 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="MaiChartManager/Front/src/views/Tools/BatchVideoConvertModal.tsx">
<violation number="1" location="MaiChartManager/Front/src/views/Tools/BatchVideoConvertModal.tsx:167">
P2: The `friendlyMessages` comparison will never match for the `needLicense` case (and `noFiles` in zh-TW) because the backend `Locale.*` strings differ from the frontend `t(...)` strings. For example, the backend sends `"Batch PV conversion requires sponsor activation"` while the frontend compares against `"This feature requires sponsor activation"`. This causes known-condition errors to fall through to `globalCapture` instead of showing a friendly toast.
Either align the strings exactly across backend `.resx` and frontend `.yaml`, or (more robustly) have the backend emit a stable error code/event type and let the frontend select the user-facing message based on that code.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| } | ||
| // 已知的友好错误(无文件 / 需要赞助):toast 提示并回到 Configure,不上报 | ||
| const message: string = e?.message ?? ''; | ||
| const friendlyMessages = [ |
There was a problem hiding this comment.
P2: The friendlyMessages comparison will never match for the needLicense case (and noFiles in zh-TW) because the backend Locale.* strings differ from the frontend t(...) strings. For example, the backend sends "Batch PV conversion requires sponsor activation" while the frontend compares against "This feature requires sponsor activation". This causes known-condition errors to fall through to globalCapture instead of showing a friendly toast.
Either align the strings exactly across backend .resx and frontend .yaml, or (more robustly) have the backend emit a stable error code/event type and let the frontend select the user-facing message based on that code.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At MaiChartManager/Front/src/views/Tools/BatchVideoConvertModal.tsx, line 167:
<comment>The `friendlyMessages` comparison will never match for the `needLicense` case (and `noFiles` in zh-TW) because the backend `Locale.*` strings differ from the frontend `t(...)` strings. For example, the backend sends `"Batch PV conversion requires sponsor activation"` while the frontend compares against `"This feature requires sponsor activation"`. This causes known-condition errors to fall through to `globalCapture` instead of showing a friendly toast.
Either align the strings exactly across backend `.resx` and frontend `.yaml`, or (more robustly) have the backend emit a stable error code/event type and let the frontend select the user-facing message based on that code.</comment>
<file context>
@@ -0,0 +1,325 @@
+ }
+ // 已知的友好错误(无文件 / 需要赞助):toast 提示并回到 Configure,不上报
+ const message: string = e?.message ?? '';
+ const friendlyMessages = [
+ t('tools.batchPv.noFiles'),
+ t('tools.batchPv.needLicense'),
</file context>
Per review feedback, batch PV conversion no longer scans the game's StreamingAssets/A###/MovieData. The user picks an arbitrary folder via the existing OpenFolderDialog endpoint and we convert every matching file in that folder in place. Backend (VideoConvertToolController.cs): - BatchConvertPvTool now takes folderPath in addition to direction. - Direct Directory.EnumerateFiles on the chosen folder, filtered by direction-derived source extensions. Removed the numeric-filename filter and EnumerateMoviePvs helper since arbitrary user folders contain arbitrarily named files. - Removed StaticSettings dependency: no AssetsDirs scan, no MovieDataMap mutation, no settings.ScanMovieData() call in finally. Constructor no longer takes StaticSettings. - Returns the new BatchConvertPvFolderNotFound error when the path is missing or does not exist. - All preserved: license gate, single-writer Channel SSE serialization, cancellation-before-delete, temp->validate->atomic-move, source MP4 to recycle bin, JSON camelCase payload. Frontend (BatchVideoConvertModal.tsx): - trigger() now opens the native folder picker via api.OpenFolderDialog (reusing the OobeController endpoint) before showing the modal. If the user cancels the picker, no modal is opened. - Configure step displays the selected path with a Change folder button that re-opens the picker. - start() includes folderPath as a query param when opening the SSE. - folderNotFound joins the friendly-error allowlist that surfaces as a toast instead of going through globalCapture. i18n: drop the 'same directory' wording from the direction hints, add selectFolder / changeFolder / selectedFolder / folderNotFound for zh, zh-TW, en. Backend Locale resx (and Designer.cs) gets the matching BatchConvertPvFolderNotFound entry.
|
📝 重构:批量 PV 不再扫描游戏目录,改为让用户选一个文件夹 按反馈调整:之前的实现扫描的是游戏的 用户流程
后端改动
前端改动
完整保留赞助门控、Channel 串行化 SSE 写入、删除前的取消检查、temp → 验证 → 原子 move、源 MP4 进回收站、camelCase JSON、所有进度/取消语义。 构建: |
There was a problem hiding this comment.
1 issue found across 9 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="MaiChartManager/Controllers/Tools/VideoConvertToolController.cs">
<violation number="1">
P2: Batch conversion no longer refreshes `MovieDataMap` after file changes, which can leave metadata out of sync with disk state until another scan runs.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| @@ -1,5 +1,8 @@ | |||
| using System.Text.Json; | |||
There was a problem hiding this comment.
P2: Batch conversion no longer refreshes MovieDataMap after file changes, which can leave metadata out of sync with disk state until another scan runs.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At MaiChartManager/Controllers/Tools/VideoConvertToolController.cs, line 11:
<comment>Batch conversion no longer refreshes `MovieDataMap` after file changes, which can leave metadata out of sync with disk state until another scan runs.</comment>
<file context>
@@ -8,7 +8,7 @@ namespace MaiChartManager.Controllers.Tools;
[ApiController]
[Route("MaiChartManagerServlet/[action]Api")]
-public class VideoConvertToolController(ILogger<VideoConvertToolController> logger, StaticSettings settings) : ControllerBase
+public class VideoConvertToolController(ILogger<VideoConvertToolController> logger) : ControllerBase
{
public enum VideoConvertEventType
</file context>
概要
工具页新增一个批量 PV 转换的入口,可以一次性把游戏内
StreamingAssets/A###/MovieData下的所有 PV 在两种格式之间互转:.mp4,源文件保留;.dat,源.mp4移入回收站(而非永久删除)。整体进度(已处理 / 总数)与当前文件进度通过 SSE 实时推送,配套有取消按钮(在当前文件完成后生效)。沿用现有
VideoConvertModal的fetchEventSource模式。改动
后端
Controllers/Tools/VideoConvertToolController.cs新增BatchConvertPvTool([FromQuery] BatchConvertPvDirection direction)StaticSettings.AssetsDirs直接枚举磁盘,避开MovieDataMap同 ID 去重导致的漏处理Channel<string>,避免 Xabe 同步进度事件并发触发Response.WriteAsync交织RequestAborted)发生时不会破坏源文件finally里再ScanMovieData()一次同步MovieDataMapIapManager.License == Active)JsonNamingPolicy.CamelCase,与前端命名习惯一致前端
Front/src/views/Tools/BatchVideoConvertModal.tsx:三步态 Modal(Configure → Progress → Done),双进度条 + 失败文件折叠列表 + 取消按钮Front/src/views/Tools/index.tsx增加第四张工具卡tools.batchPv.labelglobalCapturei18n
zh.yaml/zh-TW.yaml/en.yaml新增tools.batchPv.*键Locale.resx/Locale.zh-hans.resx/Locale.zh-hant.resx新增BatchConvertPvNeedLicense/BatchConvertPvNoFilesLocale.Designer.cs已手工同步对应的属性验证
pnpm build通过(rolldown 输出 wwwroot 资产无错)dotnet build在本环境无法完整跑(Linux 上缺 .NETFramework 4.7 / 4.8.1 reference assemblies,submodule 才需要),主项目代码已审阅并与现有VideoConvertToolController/MovieConvertController风格保持一致设计抉择 / 注意事项
RequestAborted.ThrowIfCancellationRequested()再覆盖 / 回收,所以中途断开不会损毁源 MP4Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(..., RecycleOption.SendToRecycleBin),保留误操作的恢复余地pnpm genClient(需要本地起后端),新端点直接用getUrl + fetchEventSource调用,与VideoConvertTool/ImageToAbTool行为一致,所以不阻塞合并;合并后建议跑一次pnpm genClient让apiGen.ts包含新签名由 Oracle 审过一轮,下列原 blocker 已全部修复:
Locale.Designer.cs同步、文件系统直接枚举、processed/failed 计数语义、取消时不再破坏源 MP4、SSE 写入串行化、MP4 改为送回收站。Summary by Sourcery
添加批量 PV 转换功能,可在确保文件安全和使用 SSE 驱动进度汇报的前提下,将所有 PV 文件在 USM/DAT 与 MP4 之间转换,并在 UI 中作为新工具对外提供。
New Features:
Enhancements:
Original summary in English
Summary by Sourcery
Add a batch PV conversion capability that converts all PV files between USM/DAT and MP4 with SSE-driven progress reporting and safe file handling, exposed as a new tool in the UI.
New Features:
Enhancements: