Skip to content

Commit fe4e433

Browse files
committed
fix: 保留视频转换失败的完整日志
1 parent 3b21c83 commit fe4e433

12 files changed

Lines changed: 273 additions & 34 deletions

File tree

MaiChartManager/Controllers/Music/MovieConvertController.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using MaiChartManager.Utils;
1+
using System.Text.Json;
2+
using MaiChartManager.Utils;
23
using Microsoft.AspNetCore.Mvc;
34

45
namespace MaiChartManager.Controllers.Music;
@@ -7,6 +8,13 @@ namespace MaiChartManager.Controllers.Music;
78
[Route("MaiChartManagerServlet/[action]Api/{assetDir}/{id:int}")]
89
public class MovieConvertController(ILogger<MovieConvertController> logger) : ControllerBase
910
{
11+
private static readonly JsonSerializerOptions JsonOptions = new()
12+
{
13+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
14+
};
15+
16+
private record SseErrorPayload(string Message, string? Detail);
17+
1018
public enum SetMovieEventType
1119
{
1220
Progress,
@@ -73,7 +81,7 @@ await VideoConvert.ConvertVideo(new VideoConvert.VideoConvertOptions
7381
{
7482
logger.LogError(e, "Failed to convert video");
7583
SentrySdk.CaptureException(e);
76-
await Response.WriteAsync($"event: {SetMovieEventType.Error}\ndata: 转换失败:{e.Message}\n\n");
84+
await WriteError(e);
7785
await Response.Body.FlushAsync();
7886
}
7987
finally
@@ -89,4 +97,13 @@ await VideoConvert.ConvertVideo(new VideoConvert.VideoConvertOptions
8997
}
9098
}
9199
}
92-
}
100+
101+
private async Task WriteError(Exception exception)
102+
{
103+
var detail = exception is VideoConversionException videoConversionException
104+
? videoConversionException.Detail
105+
: exception.ToString();
106+
var payload = JsonSerializer.Serialize(new SseErrorPayload(exception.Message, detail), JsonOptions);
107+
await Response.WriteAsync($"event: {SetMovieEventType.Error}\ndata: {payload}\n\n");
108+
}
109+
}

MaiChartManager/Front/src/locales/en.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ common:
2121
dismiss: Dismiss
2222
longMusic: LongMusic
2323
error: Error
24+
detail: Detail
2425
assetDir:
2526
title: Opt Management
2627
create: Create Directory

MaiChartManager/Front/src/locales/zh-TW.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ common:
2121
dismiss: 放棄
2222
longMusic: LongMusic
2323
error: 錯誤
24+
detail: 詳情
2425
assetDir:
2526
title: Opt 管理
2627
create: 建立目錄

MaiChartManager/Front/src/locales/zh.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ common:
2121
dismiss: 放弃
2222
longMusic: 长乐曲
2323
error: 错误
24+
detail: 详情
2425
assetDir:
2526
title: Opt 管理
2627
create: 创建目录
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
export type StructuredError = {
2+
readonly message: string,
3+
readonly detail?: string,
4+
}
5+
6+
const isRecord = (value: unknown): value is Record<string, unknown> =>
7+
typeof value === 'object' && value !== null;
8+
9+
export const parseStructuredError = (value: unknown): StructuredError => {
10+
if (value instanceof Error) {
11+
const parsed = parseStructuredErrorText(value.message);
12+
return parsed ?? { message: value.message };
13+
}
14+
15+
if (isRecord(value)) {
16+
const errorValue = value["error"];
17+
if (typeof errorValue === 'string') {
18+
const parsed = parseStructuredErrorText(errorValue);
19+
if (parsed) return parsed;
20+
}
21+
22+
const messageValue = value["message"];
23+
const detailValue = value["detail"];
24+
if (typeof messageValue === 'string') {
25+
return {
26+
message: messageValue,
27+
detail: typeof detailValue === 'string' ? detailValue : undefined,
28+
};
29+
}
30+
}
31+
32+
return { message: String(value) };
33+
};
34+
35+
export const parseStructuredErrorText = (value: string): StructuredError | undefined => {
36+
try {
37+
const parsed: unknown = JSON.parse(value);
38+
if (!isRecord(parsed)) return undefined;
39+
40+
const message = parsed["message"];
41+
if (typeof message !== 'string') return undefined;
42+
43+
const detail = parsed["detail"];
44+
return {
45+
message,
46+
detail: typeof detail === 'string' ? detail : undefined,
47+
};
48+
} catch {
49+
return undefined;
50+
}
51+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default async (dir: FileSystemDirectoryHandle, file: string): Promise<File | undefined> => {
2+
try {
3+
const handle = await dir.getFileHandle(file);
4+
return await handle.getFile();
5+
} catch {
6+
return undefined;
7+
}
8+
};

MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/ImportAlert.tsx

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { computed, defineComponent, PropType } from "vue";
1+
import { computed, defineComponent, PropType, ref } from "vue";
22
import { MessageLevel, ShiftMethod } from "@/client/apiGen";
33
import { ImportChartMessageEx, TempOptions } from "./types";
44
import { showNeedPurchaseDialog } from "@/store/refs";
55
import { useI18n } from 'vue-i18n';
6+
import { Button, Modal } from "@munet/ui";
67

78
export default defineComponent({
89
props: {
@@ -11,6 +12,7 @@ export default defineComponent({
1112
},
1213
setup(props) {
1314
const {t} = useI18n();
15+
const detail = ref<string>();
1416

1517
const i18nPostfix = computed(() => {
1618
switch (props.tempOptions.shift) {
@@ -19,8 +21,9 @@ export default defineComponent({
1921
}
2022
})
2123

22-
return () => <div class="of-y-auto cst max-h-20vh">
23-
<div class="flex flex-col gap-2">
24+
return () => <>
25+
<div class="of-y-auto cst max-h-20vh">
26+
<div class="flex flex-col gap-2">
2427
{
2528
props.errors.map((error, i) => {
2629
if ('first' in error) {
@@ -65,17 +68,34 @@ export default defineComponent({
6568
break;
6669
}
6770
return <div key={i} class={`p-3 rounded border ${borderColor} ${bgColor} ${error.isPaid && 'cursor-pointer'}`}
68-
// @ts-ignore
69-
onClick={() => error.isPaid && (showNeedPurchaseDialog.value = true)}
71+
onClick={() => {
72+
if (error.isPaid) showNeedPurchaseDialog.value = true;
73+
}}
7074
>
71-
<div class="font-bold mb-1">{error.name}</div>
75+
<div class="flex items-center gap-2 mb-1">
76+
<div class="font-bold w-0 grow">{error.name}</div>
77+
{error.detail && <span onClick={(event: MouseEvent) => event.stopPropagation()}>
78+
<Button size="small" onClick={() => {
79+
detail.value = error.detail;
80+
}}>{t('common.detail')}</Button>
81+
</span>}
82+
</div>
7283
<div class="whitespace-pre-wrap">
7384
{error.message}
7485
</div>
7586
</div>
7687
})
7788
}
89+
</div>
7890
</div>
79-
</div>
91+
<Modal
92+
width="min(70vw,70em)"
93+
title={t('common.detail')}
94+
show={!!detail.value}
95+
onUpdateShow={() => detail.value = undefined}
96+
>
97+
<pre class="max-h-60vh of-auto cst whitespace-pre-wrap break-words text-xs font-mono">{detail.value}</pre>
98+
</Modal>
99+
</>
80100
}
81101
})
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { MessageLevel } from "@/client/apiGen";
2+
import { parseStructuredError } from "@/utils/structuredError";
3+
import type { ImportChartMessageEx } from "./types";
4+
5+
export const createVideoConvertWarning = (
6+
errorValue: unknown,
7+
musicName: string,
8+
fallbackMessage: string,
9+
unknownMessage: string,
10+
): ImportChartMessageEx => {
11+
const error = parseStructuredError(errorValue);
12+
return {
13+
level: MessageLevel.Warning,
14+
message: `${fallbackMessage}: ${error.message || unknownMessage}`,
15+
detail: error.detail,
16+
name: musicName,
17+
};
18+
};
19+
20+
export const createImportFatal = (errorValue: unknown, musicName: string): ImportChartMessageEx => {
21+
const error = parseStructuredError(errorValue);
22+
return {
23+
level: MessageLevel.Fatal,
24+
message: error.message,
25+
detail: error.detail,
26+
name: musicName,
27+
};
28+
};
29+
30+
export const getCaptureTarget = (errorValue: unknown): unknown => {
31+
if (typeof errorValue !== "object" || errorValue === null) return errorValue;
32+
if (!("error" in errorValue)) return errorValue;
33+
return errorValue.error;
34+
};
35+
36+
export const isAbortError = (errorValue: unknown): boolean =>
37+
errorValue instanceof DOMException && errorValue.name === "AbortError" ||
38+
errorValue instanceof Error && errorValue.name === "AbortError";

MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/index.tsx

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,13 @@ import { handleSseOpen } from "@/utils/sseOpen";
1515
import { defaultSavedOptions, defaultTempOptions, dummyMeta, IMPORT_STEP, ImportChartMessageEx, ImportMeta, STEP } from "./types";
1616
import getNextUnusedMusicId from "@/utils/getNextUnusedMusicId";
1717
import { useI18n } from 'vue-i18n';
18+
import { createImportFatal, createVideoConvertWarning, getCaptureTarget, isAbortError } from "./importErrors";
19+
import tryGetFile from "@/utils/tryGetFile";
1820

19-
const tryGetFile = async (dir: FileSystemDirectoryHandle, file: string) => {
20-
try {
21-
const handle = await dir.getFileHandle(file);
22-
return await handle.getFile();
23-
} catch (e) {
24-
return;
25-
}
26-
}
27-
28-
export let startProcess = (dir?: FileSystemDirectoryHandle | FileSystemDirectoryHandle[]) => { }
21+
export let startProcess = (_dir?: FileSystemDirectoryHandle | FileSystemDirectoryHandle[]) => { }
2922

3023
export default defineComponent({
31-
setup(props) {
24+
setup() {
3225
const savedOptions = useStorage('importMusicOptions', defaultSavedOptions, undefined, { mergeDefaults: true });
3326
const tempOptions = ref({ ...defaultTempOptions });
3427
const step = ref(STEP.none);
@@ -186,24 +179,24 @@ export default defineComponent({
186179
music.importStep = IMPORT_STEP.movie;
187180
try {
188181
await uploadMovie(music.id, music.movie, audioPadding);
189-
} catch (e: any) {
190-
errors.value.push({ level: MessageLevel.Warning, message: t('chart.import.error.videoConvertFailed') + `: ${e.error?.message || e.error?.detail || e?.message || e?.toString() || t('error.unknown')}`, name: music.name });
182+
} catch (e) {
183+
errors.value.push(createVideoConvertWarning(e, music.name, t('chart.import.error.videoConvertFailed'), t('error.unknown')));
191184
}
192185
}
193186

194187
music.importStep = IMPORT_STEP.jacket;
195188
if (music.bg) await api.SetMusicJacket(music.id, selectedADir.value, { file: music.bg });
196189

197190
music.importStep = IMPORT_STEP.finish;
198-
} catch (e: any) {
191+
} catch (e) {
199192
console.log(music, e)
200-
captureException(e.error || e, {
193+
captureException(getCaptureTarget(e), {
201194
tags: {
202195
context: t('chart.import.error.importError'),
203196
step: music.importStep,
204197
}
205198
})
206-
errors.value.push({ level: MessageLevel.Fatal, message: e.error?.message || e.error?.detail || e.message || e.toString(), name: music.name });
199+
errors.value.push(createImportFatal(e, music.name));
207200
if (music.importStep !== IMPORT_STEP.create) {
208201
// 如果是在创建乐曲这步就挂了,说明乐曲XML没有创建成功,则不需要删除乐曲。
209202
// 否则,在ID冲突的情况下,会把原本的乐曲给删除掉,见 https://github.com/MuNET-OSS/MaiChartManager/issues/34
@@ -269,8 +262,8 @@ export default defineComponent({
269262
if (errors.value.length) {
270263
step.value = STEP.showResultError
271264
}
272-
} catch (e: any) {
273-
if (e.name === 'AbortError') return
265+
} catch (e) {
266+
if (isAbortError(e)) return
274267
console.log(e)
275268
globalCapture(e, t('chart.import.error.importErrorGlobal'))
276269
} finally {

MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export type ImportMeta = {
3434
}
3535

3636
export type FirstPaddingMessage = { first: number, chartPaddings: ImportChartCheckResult['chartPaddings']}
37-
export type ImportChartMessageEx = (ImportChartMessage | FirstPaddingMessage) & { name: string, isPaid?: boolean }
37+
export type ImportChartMessageEx = (ImportChartMessage | FirstPaddingMessage) & { name: string, isPaid?: boolean, detail?: string }
3838

3939
export const dummyMeta = {name: '', importStep: IMPORT_STEP.start} as ImportMeta
4040

0 commit comments

Comments
 (0)