using System.IO; using System.Text.RegularExpressions; namespace EmbyToolbox.Services; /// Распознаёт человекочитаемый title внешних audio/subtitle sidecar-файлов. 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; } }