feat: localize quality labels and add quality template token

This commit is contained in:
2026-05-01 07:16:03 +08:00
parent 0b8f2c9775
commit f0f9ed3456
9 changed files with 147 additions and 31 deletions
+22
View File
@@ -572,6 +572,28 @@ export const recordingTemplateLabelMap: Record<number, string> = {
2: "归档 TS" 2: "归档 TS"
}; };
export const qualityLabelMap: Record<string, string> = {
origin: "原画",
FULL_HD: "超清",
HD: "高清",
SD: "标清"
};
export const qualityOptionList = [
{ value: "origin", label: qualityLabelMap.origin },
{ value: "FULL_HD", label: qualityLabelMap.FULL_HD },
{ value: "HD", label: qualityLabelMap.HD },
{ value: "SD", label: qualityLabelMap.SD }
] as const;
export function formatQualityLabel(value?: string | null) {
if (!value) {
return "-";
}
return qualityLabelMap[value] ?? value;
}
export const autoStartDecisionLabelMap: Record<string, string> = { export const autoStartDecisionLabelMap: Record<string, string> = {
started: "已启动", started: "已启动",
skipped_disabled: "已禁用", skipped_disabled: "已禁用",
+78 -15
View File
@@ -6,9 +6,11 @@ import { useViewport } from "@/composables/useViewport";
import type { BatchLiveRoomsResult, ImportLiveRoomsRequest, ImportLiveRoomsResult, LiveRoom, RecordTask } from "@/types"; import type { BatchLiveRoomsResult, ImportLiveRoomsRequest, ImportLiveRoomsResult, LiveRoom, RecordTask } from "@/types";
import { import {
autoStartDecisionLabelMap, autoStartDecisionLabelMap,
formatQualityLabel,
availabilityLabelMap, availabilityLabelMap,
currentRecordingStateLabelMap, currentRecordingStateLabelMap,
outputFormatLabelMap, outputFormatLabelMap,
qualityOptionList,
recordingTemplateLabelMap, recordingTemplateLabelMap,
saveModeLabelMap saveModeLabelMap
} from "@/types"; } from "@/types";
@@ -89,7 +91,8 @@ const platformOptions = [
{ label: "虎牙", value: 3 } { label: "虎牙", value: 3 }
]; ];
const qualityOptions = ["origin", "FULL_HD", "HD", "SD"]; const qualityOptions = qualityOptionList;
const qualitySupportHint = "抖音和 Bilibili 支持按画质选流;虎牙暂未实现。若目标档位不可用,平台会自动回退到最接近的可用画质。";
const booleanOverrideOptions = [ const booleanOverrideOptions = [
{ label: "跟随全局", value: inheritValue }, { label: "跟随全局", value: inheritValue },
{ label: "开启", value: "true" }, { label: "开启", value: "true" },
@@ -143,6 +146,35 @@ function getRoomsTableScrollWrap() {
shell.querySelector<HTMLElement>(".el-table__body-wrapper"); shell.querySelector<HTMLElement>(".el-table__body-wrapper");
} }
function getRoomsTableContentWidth(wrap: HTMLElement) {
const shell = roomsTableShellRef.value;
if (!shell) {
return wrap.scrollWidth;
}
const widths = [
wrap.scrollWidth,
wrap.firstElementChild?.scrollWidth ?? 0,
shell.querySelector<HTMLElement>(".el-table__body table")?.scrollWidth ?? 0,
shell.querySelector<HTMLElement>(".el-table__header table")?.scrollWidth ?? 0
];
const wrapRect = wrap.getBoundingClientRect();
let maxRight = 0;
shell.querySelectorAll<HTMLElement>(
".el-table__header th, .el-table__body td, .room-actions-cell, .config-summary, .auto-start-cell"
).forEach((element) => {
const rect = element.getBoundingClientRect();
maxRight = Math.max(maxRight, rect.right - wrapRect.left + wrap.scrollLeft);
});
widths.push(Math.ceil(maxRight) + 24);
return Math.max(...widths);
}
function handleRoomsTableBodyScroll() { function handleRoomsTableBodyScroll() {
if (syncingRoomsTableProxy) { if (syncingRoomsTableProxy) {
return; return;
@@ -235,11 +267,11 @@ async function syncRoomsTableProxyScroll() {
return; return;
} }
const scrollWidth = Math.max(wrap.scrollWidth, wrap.firstElementChild?.scrollWidth ?? 0); const scrollWidth = getRoomsTableContentWidth(wrap);
const clientWidth = wrap.clientWidth; const clientWidth = wrap.clientWidth;
const canScrollHorizontally = scrollWidth > clientWidth + 1; const canScrollHorizontally = scrollWidth > clientWidth + 1;
roomsTableProxyInnerWidth.value = scrollWidth; roomsTableProxyInnerWidth.value = canScrollHorizontally ? scrollWidth : 0;
showRoomsTableProxyScroll.value = canScrollHorizontally; showRoomsTableProxyScroll.value = canScrollHorizontally;
if (canScrollHorizontally && proxy && Math.abs(proxy.scrollLeft - wrap.scrollLeft) > 1) { if (canScrollHorizontally && proxy && Math.abs(proxy.scrollLeft - wrap.scrollLeft) > 1) {
@@ -653,6 +685,10 @@ function getRoomAvatarText(room: LiveRoom) {
return source.slice(0, 1).toUpperCase(); return source.slice(0, 1).toUpperCase();
} }
function getQualityLabel(quality?: string | null) {
return formatQualityLabel(quality);
}
function autoStartDecisionLabel(code?: string) { function autoStartDecisionLabel(code?: string) {
if (!code) { if (!code) {
return "未记录"; return "未记录";
@@ -862,7 +898,7 @@ onBeforeUnmount(() => {
<div style="grid-column: 1 / -1;"> <div style="grid-column: 1 / -1;">
<dt>房间配置</dt> <dt>房间配置</dt>
<dd> <dd>
{{ row.effectiveSettings.preferredQuality }} · {{ getQualityLabel(row.effectiveSettings.preferredQuality) }} ·
{{ outputFormatLabelMap[row.effectiveSettings.outputFormat] }} · {{ outputFormatLabelMap[row.effectiveSettings.outputFormat] }} ·
{{ saveModeLabelMap[row.effectiveSettings.saveMode] }} · {{ saveModeLabelMap[row.effectiveSettings.saveMode] }} ·
{{ row.effectiveSettings.enableDanmakuRecording ? "弹幕开" : "弹幕关" }} {{ row.effectiveSettings.enableDanmakuRecording ? "弹幕开" : "弹幕关" }}
@@ -914,7 +950,7 @@ onBeforeUnmount(() => {
v-loading="loading" v-loading="loading"
:height="tableHeight" :height="tableHeight"
class="premium-table rooms-table" class="premium-table rooms-table"
table-layout="auto" table-layout="fixed"
row-key="id" row-key="id"
@selection-change="handleRoomSelectionChange" @selection-change="handleRoomSelectionChange"
> >
@@ -982,10 +1018,10 @@ onBeforeUnmount(() => {
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="房间配置" min-width="240"> <el-table-column label="房间配置" width="280">
<template #default="{ row }"> <template #default="{ row }">
<div class="config-summary"> <div class="config-summary">
<span>画质 {{ row.effectiveSettings.preferredQuality }}</span> <span>画质 {{ getQualityLabel(row.effectiveSettings.preferredQuality) }}</span>
<span>{{ outputFormatLabelMap[row.effectiveSettings.outputFormat] }}</span> <span>{{ outputFormatLabelMap[row.effectiveSettings.outputFormat] }}</span>
<span>{{ saveModeLabelMap[row.effectiveSettings.saveMode] }}</span> <span>{{ saveModeLabelMap[row.effectiveSettings.saveMode] }}</span>
<span>{{ row.effectiveSettings.enableDanmakuRecording ? "弹幕开" : "弹幕关" }}</span> <span>{{ row.effectiveSettings.enableDanmakuRecording ? "弹幕开" : "弹幕关" }}</span>
@@ -1012,7 +1048,7 @@ onBeforeUnmount(() => {
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="320"> <el-table-column label="操作" width="380">
<template #default="{ row }"> <template #default="{ row }">
<div class="room-actions-cell"> <div class="room-actions-cell">
<el-button size="small" @click="refreshRoom(row)">刷新</el-button> <el-button size="small" @click="refreshRoom(row)">刷新</el-button>
@@ -1172,10 +1208,17 @@ onBeforeUnmount(() => {
<el-form label-position="top"> <el-form label-position="top">
<el-form-item label="清晰度"> <el-form-item label="清晰度">
<el-select v-model="recordForm.preferredQuality"> <el-select v-model="recordForm.preferredQuality">
<el-option v-for="option in qualityOptions" :key="option" :label="option" :value="option" /> <el-option
v-for="option in qualityOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<div class="field-help">{{ qualitySupportHint }}</div>
<el-form-item label="输出格式"> <el-form-item label="输出格式">
<el-select v-model="recordForm.outputFormat"> <el-select v-model="recordForm.outputFormat">
<el-option <el-option
@@ -1241,10 +1284,17 @@ onBeforeUnmount(() => {
<el-form-item label="默认画质"> <el-form-item label="默认画质">
<el-select v-model="settingsForm.preferredQualityOverride"> <el-select v-model="settingsForm.preferredQualityOverride">
<el-option label="跟随全局" :value="inheritValue" /> <el-option label="跟随全局" :value="inheritValue" />
<el-option v-for="option in qualityOptions" :key="option" :label="option" :value="option" /> <el-option
v-for="option in qualityOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<div class="field-help field-help--full">{{ qualitySupportHint }}</div>
<el-form-item label="默认输出格式"> <el-form-item label="默认输出格式">
<el-select v-model="settingsForm.outputFormatOverride"> <el-select v-model="settingsForm.outputFormatOverride">
<el-option label="跟随全局" :value="inheritValue" /> <el-option label="跟随全局" :value="inheritValue" />
@@ -1454,7 +1504,7 @@ onBeforeUnmount(() => {
} }
.rooms-table { .rooms-table {
min-width: 1380px; min-width: 1920px;
} }
.rooms-table :deep(.el-table__cell) { .rooms-table :deep(.el-table__cell) {
@@ -1462,7 +1512,7 @@ onBeforeUnmount(() => {
} }
.rooms-table :deep(.cell) { .rooms-table :deep(.cell) {
overflow: visible; overflow: hidden;
} }
.room-avatar { .room-avatar {
@@ -1557,16 +1607,29 @@ onBeforeUnmount(() => {
} }
.room-actions-cell { .room-actions-cell {
display: flex; display: grid;
align-items: center; grid-template-columns: repeat(2, minmax(0, max-content));
gap: 8px; gap: 8px;
flex-wrap: wrap; justify-content: flex-start;
width: 100%;
} }
.room-actions-cell :deep(.el-button) { .room-actions-cell :deep(.el-button) {
margin: 0; margin: 0;
} }
.field-help {
margin-top: -8px;
margin-bottom: 14px;
color: var(--text-muted);
font-size: 12px;
line-height: 1.6;
}
.field-help--full {
grid-column: 1 / -1;
}
.settings-note { .settings-note {
display: grid; display: grid;
gap: 6px; gap: 6px;
@@ -12,6 +12,7 @@ import type {
RecordSessionTimelineSegment RecordSessionTimelineSegment
} from "@/types"; } from "@/types";
import { import {
formatQualityLabel,
logLevelLabelMap, logLevelLabelMap,
outputFormatLabelMap, outputFormatLabelMap,
platformLabelMap, platformLabelMap,
@@ -277,7 +278,7 @@ onMounted(loadDetail);
{{ outputFormatLabelMap[detail.session.outputFormat] }} {{ outputFormatLabelMap[detail.session.outputFormat] }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="清晰度"> <el-descriptions-item label="清晰度">
<span class="monospace">{{ detail.session.preferredQuality }}</span> <span>{{ formatQualityLabel(detail.session.preferredQuality) }}</span>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="分片数量"> <el-descriptions-item label="分片数量">
{{ detail.session.segmentCount }} {{ detail.session.segmentCount }}
+2 -1
View File
@@ -6,6 +6,7 @@ import apiClient, { getApiErrorMessage } from "@/api/client";
import { useViewport } from "@/composables/useViewport"; import { useViewport } from "@/composables/useViewport";
import type { RecordArtifactUploadItemResult, RecordPreviewTicket, RecordTaskDetail } from "@/types"; import type { RecordArtifactUploadItemResult, RecordPreviewTicket, RecordTaskDetail } from "@/types";
import { import {
formatQualityLabel,
logLevelLabelMap, logLevelLabelMap,
outputFormatLabelMap, outputFormatLabelMap,
sessionStatusLabelMap, sessionStatusLabelMap,
@@ -249,7 +250,7 @@ onMounted(loadDetailAndPreview);
<span class="monospace">#{{ detail.task.segmentIndex }}</span> <span class="monospace">#{{ detail.task.segmentIndex }}</span>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="RoomId">{{ detail.task.roomId }}</el-descriptions-item> <el-descriptions-item label="RoomId">{{ detail.task.roomId }}</el-descriptions-item>
<el-descriptions-item label="清晰度">{{ detail.task.preferredQuality }}</el-descriptions-item> <el-descriptions-item label="清晰度">{{ formatQualityLabel(detail.task.preferredQuality) }}</el-descriptions-item>
<el-descriptions-item label="输出格式"> <el-descriptions-item label="输出格式">
{{ outputFormatLabelMap[detail.task.outputFormat] }} {{ outputFormatLabelMap[detail.task.outputFormat] }}
</el-descriptions-item> </el-descriptions-item>
+3 -3
View File
@@ -17,6 +17,7 @@ import type {
RecordTask RecordTask
} from "@/types"; } from "@/types";
import { import {
formatQualityLabel,
outputFormatLabelMap, outputFormatLabelMap,
saveModeLabelMap, saveModeLabelMap,
sessionStatusLabelMap, sessionStatusLabelMap,
@@ -747,7 +748,7 @@ onBeforeUnmount(() => {
<div class="data-card__grid"> <div class="data-card__grid">
<div> <div>
<dt>清晰度</dt> <dt>清晰度</dt>
<dd class="monospace">{{ task.preferredQuality }}</dd> <dd>{{ formatQualityLabel(task.preferredQuality) }}</dd>
</div> </div>
<div> <div>
<dt>时长</dt> <dt>时长</dt>
@@ -905,7 +906,7 @@ onBeforeUnmount(() => {
<el-table-column label="清晰度" width="120"> <el-table-column label="清晰度" width="120">
<template #default="{ row }"> <template #default="{ row }">
<span class="monospace">{{ row.preferredQuality }}</span> <span>{{ formatQualityLabel(row.preferredQuality) }}</span>
</template> </template>
</el-table-column> </el-table-column>
@@ -1435,4 +1436,3 @@ onBeforeUnmount(() => {
} }
} }
</style> </style>
+24 -6
View File
@@ -8,7 +8,7 @@ import type {
SystemSettings, SystemSettings,
WebhookTestResult WebhookTestResult
} from "@/types"; } from "@/types";
import { outputFormatLabelMap, recordingTemplateLabelMap, saveModeLabelMap } from "@/types"; import { outputFormatLabelMap, qualityOptionList, recordingTemplateLabelMap, saveModeLabelMap } from "@/types";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { useViewport } from "@/composables/useViewport"; import { useViewport } from "@/composables/useViewport";
import { useUiPreferences } from "@/composables/useUiPreferences"; import { useUiPreferences } from "@/composables/useUiPreferences";
@@ -42,6 +42,8 @@ const webhookTestResult = ref<WebhookTestResult | null>(null);
const retentionCleanupResult = ref<RetentionCleanupResult | null>(null); const retentionCleanupResult = ref<RetentionCleanupResult | null>(null);
const loadError = ref(""); const loadError = ref("");
const activeSettingTab = ref("recording"); const activeSettingTab = ref("recording");
const qualitySupportHint = "抖音和 Bilibili 支持按画质选流;虎牙暂未实现。若目标档位不可用,平台会自动回退到最接近的可用画质。";
const qualityOptions = qualityOptionList;
const savebarStyle = computed(() => { const savebarStyle = computed(() => {
if (isMobile.value) { if (isMobile.value) {
@@ -63,7 +65,7 @@ const form = reactive<SystemSettings>({
ffmpegPath: "ffmpeg", ffmpegPath: "ffmpeg",
outputRoot: "records", outputRoot: "records",
outputDirectoryTemplate: "{platform}/{yyyy}/{MM}/{dd}/{anchor}", outputDirectoryTemplate: "{platform}/{yyyy}/{MM}/{dd}/{anchor}",
outputFileNameTemplate: "{HHmmss}_{anchor}_{title}_{roomId}{segmentSuffix}", outputFileNameTemplate: "{HHmmss}_{quality}_{anchor}_{title}_{roomId}{segmentSuffix}",
defaultQuality: "origin", defaultQuality: "origin",
defaultOutputFormat: 0, defaultOutputFormat: 0,
saveMode: 0, saveMode: 0,
@@ -187,6 +189,7 @@ const outputTemplateTokens = [
"{roomId}", "{roomId}",
"{anchor}", "{anchor}",
"{title}", "{title}",
"{quality}",
"{yyyy}", "{yyyy}",
"{MM}", "{MM}",
"{dd}", "{dd}",
@@ -266,12 +269,12 @@ const canSendTestWebhook = computed(() => Boolean(form.webhookUrl.trim()));
const segmentedExamplePath = computed(() => { const segmentedExamplePath = computed(() => {
const extension = form.defaultOutputFormat === 1 ? "ts" : "mp4"; const extension = form.defaultOutputFormat === 1 ? "ts" : "mp4";
return `Douyin/2026/04/15/主播名/221530_主播名_直播标题_123456789_00001.${extension}`; return `Douyin/2026/04/15/主播名/221530_origin_主播名_直播标题_123456789_00001.${extension}`;
}); });
const nestedSegmentedExamplePath = computed(() => { const nestedSegmentedExamplePath = computed(() => {
const extension = form.defaultOutputFormat === 1 ? "ts" : "mp4"; const extension = form.defaultOutputFormat === 1 ? "ts" : "mp4";
return `Douyin/2026/04/15/主播名/221530_主播名_直播标题_123456789/221530_主播名_直播标题_123456789_00001.${extension}`; return `Douyin/origin/2026/04/15/主播名/221530_origin_主播名_直播标题_123456789/221530_origin_主播名_直播标题_123456789_00001.${extension}`;
}); });
async function loadSettings() { async function loadSettings() {
@@ -599,7 +602,14 @@ onMounted(loadSettings);
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="默认清晰度"> <el-form-item label="默认清晰度">
<el-input v-model="form.defaultQuality" placeholder="origin / FULL_HD / HD / SD" /> <el-select v-model="form.defaultQuality">
<el-option
v-for="option in qualityOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
@@ -670,6 +680,10 @@ onMounted(loadSettings);
</el-col> </el-col>
</el-row> </el-row>
</el-form> </el-form>
<div class="helper-panel">
{{ qualitySupportHint }}
</div>
</el-card> </el-card>
<el-card class="surface-card settings-card" shadow="never"> <el-card class="surface-card settings-card" shadow="never">
@@ -815,7 +829,7 @@ onMounted(loadSettings);
v-model="form.outputFileNameTemplate" v-model="form.outputFileNameTemplate"
type="textarea" type="textarea"
:rows="3" :rows="3"
placeholder="{HHmmss}_{anchor}_{title}_{roomId}{segmentSuffix}" placeholder="{HHmmss}_{quality}_{anchor}_{title}_{roomId}{segmentSuffix}"
/> />
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -834,6 +848,10 @@ onMounted(loadSettings);
目录模板和文件名模板中的时间变量统一按北京时间UTC+8渲染示例路径也按北京时间展示 目录模板和文件名模板中的时间变量统一按北京时间UTC+8渲染示例路径也按北京时间展示
</div> </div>
<div class="helper-panel">
<code>{quality}</code> 输出稳定画质键例如 <code>origin</code><code>FULL_HD</code><code>HD</code><code>SD</code>适合放进目录名或文件名里做脚本处理
</div>
<div class="token-list"> <div class="token-list">
<span v-for="token in outputTemplateTokens" :key="token" class="token-chip">{{ token }}</span> <span v-for="token in outputTemplateTokens" :key="token" class="token-chip">{{ token }}</span>
</div> </div>
@@ -52,7 +52,7 @@ public sealed class SystemSettingsDto
public string OutputDirectoryTemplate { get; set; } = "{platform}/{yyyy}/{MM}/{dd}/{anchor}"; public string OutputDirectoryTemplate { get; set; } = "{platform}/{yyyy}/{MM}/{dd}/{anchor}";
public string OutputFileNameTemplate { get; set; } = "{HHmmss}_{anchor}_{title}_{roomId}"; public string OutputFileNameTemplate { get; set; } = "{HHmmss}_{quality}_{anchor}_{title}_{roomId}{segmentSuffix}";
public string DefaultQuality { get; set; } = "origin"; public string DefaultQuality { get; set; } = "origin";
@@ -233,7 +233,7 @@ public sealed class UpdateSystemSettingsRequest
public string OutputDirectoryTemplate { get; set; } = "{platform}/{yyyy}/{MM}/{dd}/{anchor}"; public string OutputDirectoryTemplate { get; set; } = "{platform}/{yyyy}/{MM}/{dd}/{anchor}";
public string OutputFileNameTemplate { get; set; } = "{HHmmss}_{anchor}_{title}_{roomId}"; public string OutputFileNameTemplate { get; set; } = "{HHmmss}_{quality}_{anchor}_{title}_{roomId}{segmentSuffix}";
public string DefaultQuality { get; set; } = "origin"; public string DefaultQuality { get; set; } = "origin";
@@ -284,6 +284,7 @@ public sealed class RecordService
liveRoom.RoomId, liveRoom.RoomId,
storageAnchorName, storageAnchorName,
liveRoom.Title, liveRoom.Title,
streamResult.SelectedQuality,
outputFormat, outputFormat,
saveMode, saveMode,
now); now);
@@ -770,6 +771,7 @@ public sealed class RecordService
string roomId, string roomId,
string? anchorName, string? anchorName,
string? title, string? title,
string selectedQuality,
RecordOutputFormat outputFormat, RecordOutputFormat outputFormat,
RecordSaveMode saveMode, RecordSaveMode saveMode,
DateTimeOffset now) DateTimeOffset now)
@@ -783,6 +785,7 @@ public sealed class RecordService
safeRoomId, safeRoomId,
anchorName, anchorName,
title, title,
selectedQuality,
localNow, localNow,
segmentSuffix: string.Empty); segmentSuffix: string.Empty);
var directoryPath = BuildDirectoryPath( var directoryPath = BuildDirectoryPath(
@@ -791,6 +794,7 @@ public sealed class RecordService
safeRoomId, safeRoomId,
anchorName, anchorName,
title, title,
selectedQuality,
localNow, localNow,
baseFileStem); baseFileStem);
var fileNameStem = BuildFileNameStem( var fileNameStem = BuildFileNameStem(
@@ -799,6 +803,7 @@ public sealed class RecordService
safeRoomId, safeRoomId,
anchorName, anchorName,
title, title,
selectedQuality,
localNow, localNow,
saveMode == RecordSaveMode.Segmented ? "_%05d" : string.Empty); saveMode == RecordSaveMode.Segmented ? "_%05d" : string.Empty);
var folder = Path.Combine(outputRoot, directoryPath); var folder = Path.Combine(outputRoot, directoryPath);
@@ -838,6 +843,7 @@ public sealed class RecordService
string roomId, string roomId,
string? anchorName, string? anchorName,
string? title, string? title,
string quality,
DateTimeOffset now, DateTimeOffset now,
string fileStem) string fileStem)
{ {
@@ -847,6 +853,7 @@ public sealed class RecordService
roomId, roomId,
anchorName, anchorName,
title, title,
quality,
now, now,
forPathSegment: true, forPathSegment: true,
fileStem, fileStem,
@@ -866,6 +873,7 @@ public sealed class RecordService
string roomId, string roomId,
string? anchorName, string? anchorName,
string? title, string? title,
string quality,
DateTimeOffset now, DateTimeOffset now,
string segmentSuffix) string segmentSuffix)
{ {
@@ -875,6 +883,7 @@ public sealed class RecordService
roomId, roomId,
anchorName, anchorName,
title, title,
quality,
now, now,
forPathSegment: false, forPathSegment: false,
fileStem: string.Empty, fileStem: string.Empty,
@@ -885,7 +894,7 @@ public sealed class RecordService
private static string EnsureSegmentSuffixTemplate(string template, RecordSaveMode saveMode) private static string EnsureSegmentSuffixTemplate(string template, RecordSaveMode saveMode)
{ {
var effectiveTemplate = string.IsNullOrWhiteSpace(template) var effectiveTemplate = string.IsNullOrWhiteSpace(template)
? "{HHmmss}_{anchor}_{title}_{roomId}{segmentSuffix}" ? "{HHmmss}_{quality}_{anchor}_{title}_{roomId}{segmentSuffix}"
: template.Trim(); : template.Trim();
if (saveMode != RecordSaveMode.Segmented) if (saveMode != RecordSaveMode.Segmented)
@@ -908,13 +917,14 @@ public sealed class RecordService
string roomId, string roomId,
string? anchorName, string? anchorName,
string? title, string? title,
string quality,
DateTimeOffset now, DateTimeOffset now,
bool forPathSegment, bool forPathSegment,
string fileStem, string fileStem,
string segmentSuffix) string segmentSuffix)
{ {
var effectiveTemplate = string.IsNullOrWhiteSpace(template) var effectiveTemplate = string.IsNullOrWhiteSpace(template)
? (forPathSegment ? "{platform}/{yyyy}/{MM}/{dd}/{anchor}" : "{HHmmss}_{anchor}_{title}_{roomId}{segmentSuffix}") ? (forPathSegment ? "{platform}/{yyyy}/{MM}/{dd}/{anchor}" : "{HHmmss}_{quality}_{anchor}_{title}_{roomId}{segmentSuffix}")
: template; : template;
// Path tokens must stay case-sensitive so {MM} (month) and {mm} (minute) don't collide. // Path tokens must stay case-sensitive so {MM} (month) and {mm} (minute) don't collide.
@@ -924,6 +934,7 @@ public sealed class RecordService
["roomId"] = roomId, ["roomId"] = roomId,
["anchor"] = NormalizeTokenValue(anchorName, "unknown-anchor"), ["anchor"] = NormalizeTokenValue(anchorName, "unknown-anchor"),
["title"] = NormalizeTokenValue(title, "untitled"), ["title"] = NormalizeTokenValue(title, "untitled"),
["quality"] = NormalizeTokenValue(quality, "origin"),
["fileStem"] = fileStem, ["fileStem"] = fileStem,
["segmentSuffix"] = segmentSuffix, ["segmentSuffix"] = segmentSuffix,
["yyyy"] = now.ToString("yyyy"), ["yyyy"] = now.ToString("yyyy"),
@@ -115,7 +115,7 @@ public sealed class SystemSettingsService : ISystemSettingsService
FfmpegPath = GetValue(lookup, FfmpegPathKey, "ffmpeg"), FfmpegPath = GetValue(lookup, FfmpegPathKey, "ffmpeg"),
OutputRoot = GetValue(lookup, OutputRootKey, "records"), OutputRoot = GetValue(lookup, OutputRootKey, "records"),
OutputDirectoryTemplate = GetValue(lookup, OutputDirectoryTemplateKey, "{platform}/{yyyy}/{MM}/{dd}/{anchor}"), OutputDirectoryTemplate = GetValue(lookup, OutputDirectoryTemplateKey, "{platform}/{yyyy}/{MM}/{dd}/{anchor}"),
OutputFileNameTemplate = GetValue(lookup, OutputFileNameTemplateKey, "{HHmmss}_{anchor}_{title}_{roomId}{segmentSuffix}"), OutputFileNameTemplate = GetValue(lookup, OutputFileNameTemplateKey, "{HHmmss}_{quality}_{anchor}_{title}_{roomId}{segmentSuffix}"),
DefaultQuality = GetValue(lookup, DefaultQualityKey, "origin"), DefaultQuality = GetValue(lookup, DefaultQualityKey, "origin"),
DefaultOutputFormat = Enum.TryParse(GetValue(lookup, DefaultOutputFormatKey, "Mp4"), true, out RecordOutputFormat outputFormat) DefaultOutputFormat = Enum.TryParse(GetValue(lookup, DefaultOutputFormatKey, "Mp4"), true, out RecordOutputFormat outputFormat)
? outputFormat ? outputFormat