emby-toolbox/EmbyToolbox/Services/SidecarTitleResolver.cs
Emby Toolbox 6264b487fe Initial commit: Emby Toolbox (conversion scroll fix, bulk Del for tracks).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 21:33:47 +05:00

129 lines
3.7 KiB
C#

using System.IO;
using System.Text.RegularExpressions;
namespace EmbyToolbox.Services;
/// <summary>Распознаёт человекочитаемый title внешних audio/subtitle sidecar-файлов.</summary>
public sealed class SidecarTitleResolver
{
private static readonly char[] StartDelimiters = ['.', '_', '-', ' '];
private static readonly string[] TechnicalTailTokens = ["rus", "eng", "audio", "sub", "subtitle", "subtitles"];
public string ResolveExternalAudioTitle(
string videoPath,
string sidecarPath,
int streamIndex,
string? streamTags)
{
var tagTitle = NormalizeTitleOrNull(streamTags);
if (tagTitle is not null)
{
return tagTitle;
}
var byName = TryRecognizeSidecarTitle(videoPath, sidecarPath);
if (byName is not null)
{
return byName;
}
return BuildRusAudioFallback(streamIndex);
}
public string ResolveExternalSubtitleTitle(string videoPath, string sidecarPath, int index)
{
var byName = TryRecognizeSidecarTitle(videoPath, sidecarPath);
if (byName is not null)
{
return byName;
}
return BuildRusSubtitleFallback(index);
}
public string? TryRecognizeSidecarTitle(string videoPath, string sidecarPath)
{
if (string.IsNullOrWhiteSpace(videoPath) || string.IsNullOrWhiteSpace(sidecarPath))
{
return null;
}
var videoBase = Path.GetFileNameWithoutExtension(videoPath);
var sidecarBase = Path.GetFileNameWithoutExtension(sidecarPath);
if (string.IsNullOrWhiteSpace(videoBase) || string.IsNullOrWhiteSpace(sidecarBase))
{
return null;
}
if (!sidecarBase.StartsWith(videoBase, StringComparison.OrdinalIgnoreCase))
{
return null;
}
if (sidecarBase.Length <= videoBase.Length)
{
return null;
}
var delimiter = sidecarBase[videoBase.Length];
if (Array.IndexOf(StartDelimiters, delimiter) < 0)
{
return null;
}
var rawSuffix = sidecarBase[(videoBase.Length + 1)..];
return CleanupCandidate(rawSuffix);
}
private static string? CleanupCandidate(string? raw)
{
if (string.IsNullOrWhiteSpace(raw))
{
return null;
}
var cleaned = raw.Trim().Trim('.', '_', '-', ' ');
if (string.IsNullOrWhiteSpace(cleaned))
{
return null;
}
cleaned = cleaned.Replace('.', ' ')
.Replace('_', ' ')
.Replace('-', ' ');
cleaned = Regex.Replace(cleaned, "\\s+", " ").Trim();
if (string.IsNullOrWhiteSpace(cleaned))
{
return null;
}
var tokens = cleaned.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList();
while (tokens.Count > 0 && TechnicalTailTokens.Contains(tokens[^1], StringComparer.OrdinalIgnoreCase))
{
tokens.RemoveAt(tokens.Count - 1);
}
if (tokens.Count == 0)
{
return null;
}
cleaned = string.Join(' ', tokens).Trim();
return string.IsNullOrWhiteSpace(cleaned) ? null : cleaned;
}
public static string BuildRusAudioFallback(int index) => index <= 1 ? "RUS" : $"RUS {index}";
public static string BuildRusSubtitleFallback(int index) => index <= 0 ? "RUS" : $"RUS {index}";
public static string? NormalizeTitleOrNull(string? title)
{
if (string.IsNullOrWhiteSpace(title))
{
return null;
}
var normalized = title.Trim();
return normalized.Equals("unknown", StringComparison.OrdinalIgnoreCase) ? null : normalized;
}
}