129 lines
3.7 KiB
C#
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;
|
|
}
|
|
}
|