Migrate remaining legacy operator workflows

This commit is contained in:
2026-07-12 05:39:27 +09:00
parent ffb8f43c19
commit a01836a2d7
132 changed files with 20566 additions and 720 deletions

View File

@@ -15,6 +15,7 @@ internal sealed record ValidatedPlayoutOptions(
int? OutputChannel,
int LayoutIndex,
string? SceneDirectory,
string? OperatorBackgroundDirectory,
Regex? TestProcessWindowTitleRegex,
IReadOnlySet<string> TestSceneAllowlist,
bool TrustedLiveOutputEnabled,
@@ -59,6 +60,13 @@ internal sealed record ValidatedPlayoutOptions(
sceneDirectory = ValidateSceneDirectory(options.SceneDirectory);
}
var operatorBackgroundDirectory =
PlayoutSceneCompositionFactory.TryGetConfiguredBackgroundDirectory(
options,
out var configuredBackgroundDirectory)
? configuredBackgroundDirectory
: null;
RequireRange(options.QueueCapacity, 1, 4_096, nameof(options.QueueCapacity));
RequireRange(
options.ConnectTimeoutMilliseconds,
@@ -156,6 +164,7 @@ internal sealed record ValidatedPlayoutOptions(
options.OutputChannel,
options.LayoutIndex,
sceneDirectory,
operatorBackgroundDirectory,
titleRegex,
allowlist,
options.TrustedLiveOutputEnabled,
@@ -356,9 +365,14 @@ internal sealed record ValidatedPlayoutOptions(
return background;
case PlayoutSetBackgroundTexture texture:
ValidateMutationTiming(texture.Timing);
return texture with { AssetPath = ResolveAssetPath(texture.AssetPath) };
ValidateAssetRoot(texture.AssetRoot);
return texture with
{
AssetPath = ResolveAssetPath(texture.AssetPath, texture.AssetRoot)
};
case PlayoutSetBackgroundVideo video:
ValidateMutationTiming(video.Timing);
ValidateAssetRoot(video.AssetRoot);
// The legacy MainForm intentionally uses 2004 for the common studio
// background. Keep the input bounded while retaining that vendor value.
if (video.LoopCount is < 0 or > 10_000)
@@ -366,16 +380,28 @@ internal sealed record ValidatedPlayoutOptions(
throw new PlayoutRequestException("장면 배경 영상 반복 값이 올바르지 않습니다.");
}
return video with { AssetPath = ResolveAssetPath(video.AssetPath) };
return video with
{
AssetPath = ResolveAssetPath(video.AssetPath, video.AssetRoot)
};
default:
throw new PlayoutRequestException("지원하지 않는 장면 변경 항목입니다.");
}
}
private string ResolveAssetPath(string? relativePath)
=> ResolveAssetPath(relativePath, PlayoutAssetRoot.SceneDirectory);
private string ResolveAssetPath(string? relativePath, PlayoutAssetRoot assetRoot)
{
relativePath = relativePath?.Trim();
if (SceneDirectory is null || string.IsNullOrEmpty(relativePath) ||
var root = assetRoot switch
{
PlayoutAssetRoot.SceneDirectory => SceneDirectory,
PlayoutAssetRoot.OperatorBackgroundDirectory => OperatorBackgroundDirectory,
_ => null
};
if (root is null || string.IsNullOrEmpty(relativePath) ||
Path.IsPathRooted(relativePath) || relativePath.Contains("..", StringComparison.Ordinal) ||
relativePath.Any(char.IsControl))
{
@@ -393,12 +419,28 @@ internal sealed record ValidatedPlayoutOptions(
throw new PlayoutRequestException("지원하지 않는 장면 자산 형식입니다.");
}
var candidate = Path.GetFullPath(Path.Combine(SceneDirectory, relativePath));
var rootPrefix = SceneDirectory.EndsWith(Path.DirectorySeparatorChar)
? SceneDirectory
: SceneDirectory + Path.DirectorySeparatorChar;
if (assetRoot == PlayoutAssetRoot.OperatorBackgroundDirectory)
{
try
{
return TrustedPlayoutAssetPath.ResolveRelativeFile(
root,
relativePath,
new HashSet<string>(allowedExtensions, StringComparer.OrdinalIgnoreCase));
}
catch (TrustedPlayoutAssetException)
{
throw new PlayoutRequestException(
"신뢰 배경 파일을 확인할 수 없습니다.");
}
}
var candidate = Path.GetFullPath(Path.Combine(root, relativePath));
var rootPrefix = root.EndsWith(Path.DirectorySeparatorChar)
? root
: root + Path.DirectorySeparatorChar;
if (!candidate.StartsWith(rootPrefix, StringComparison.OrdinalIgnoreCase) ||
!File.Exists(candidate) || HasReparsePointBetween(SceneDirectory, candidate))
!File.Exists(candidate) || HasReparsePointBetween(root, candidate))
{
throw new PlayoutRequestException("허용된 장면 자산을 찾을 수 없습니다.");
}
@@ -469,6 +511,14 @@ internal sealed record ValidatedPlayoutOptions(
}
}
private static void ValidateAssetRoot(PlayoutAssetRoot assetRoot)
{
if (!Enum.IsDefined(assetRoot))
{
throw new PlayoutRequestException("배경 자산 루트가 올바르지 않습니다.");
}
}
private static bool IsByte(int value) => value is >= byte.MinValue and <= byte.MaxValue;
private static bool CanMatchProgramTitle(Regex regex)