emby-toolbox/EmbyToolbox/Services/SnapshotScopePaths.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

109 lines
3.4 KiB
C#

using System.IO;
namespace EmbyToolbox.Services;
/// <summary>Область применения snapshot: каталог эпизода и опционально общий корень батча.</summary>
public static class SnapshotScopePaths
{
private static readonly StringComparer IC = StringComparer.OrdinalIgnoreCase;
/// <summary>Абсолютный путь без завершающего разделителя.</summary>
public static string NormalizeScopeDirectory(string directoryPath)
{
if (string.IsNullOrWhiteSpace(directoryPath))
{
return string.Empty;
}
return Path.TrimEndingDirectorySeparator(Path.GetFullPath(directoryPath.Trim()));
}
public static string NormalizeScopeBatchRootNullable(string? path) =>
string.IsNullOrWhiteSpace(path) ? string.Empty : NormalizeScopeDirectory(path);
public static string GetVideoScopeDirectory(string videoFileFullPath)
{
var dir = Path.GetDirectoryName(Path.GetFullPath(videoFileFullPath));
return dir is null ? string.Empty : NormalizeScopeDirectory(dir);
}
/// <summary>Узкий общий предок каталогов всех указанных видеофайлов (полные пути).</summary>
public static string? TryGetLowestCommonAncestorDirectory(IReadOnlyList<string> videoFileAbsolutePaths)
{
if (videoFileAbsolutePaths.Count == 0)
{
return null;
}
var dirs = videoFileAbsolutePaths
.Select(static p =>
{
var d = Path.GetDirectoryName(Path.GetFullPath(p));
return d is null ? string.Empty : NormalizeScopeDirectory(d);
})
.Where(static d => d.Length > 0)
.Distinct(IC)
.ToList();
if (dirs.Count == 0)
{
return null;
}
var acc = dirs[0];
for (var i = 1; i < dirs.Count; i++)
{
acc = LowestCommonAncestorOfTwoDirectories(acc, dirs[i]);
if (string.IsNullOrEmpty(acc))
{
return null;
}
}
return acc;
}
private static string LowestCommonAncestorOfTwoDirectories(string dirA, string dirB)
{
if (dirA.Length == 0 || dirB.Length == 0)
{
return string.Empty;
}
var ancestorsA = new HashSet<string>(EnumerateAncestorDirectoriesInclusive(dirA), IC);
foreach (var cand in EnumerateAncestorDirectoriesInclusive(dirB))
{
if (ancestorsA.Contains(cand))
{
return cand;
}
}
return string.Empty;
}
/// <summary>От указанной папки вверх к корню включая сам каталог.</summary>
private static IEnumerable<string> EnumerateAncestorDirectoriesInclusive(string normalizedAbsoluteDir)
{
var d = NormalizeScopeDirectory(normalizedAbsoluteDir);
while (!string.IsNullOrEmpty(d))
{
yield return d;
var parent = Path.GetDirectoryName(d);
if (string.IsNullOrEmpty(parent))
{
yield break;
}
parent = NormalizeScopeDirectory(parent);
if (string.Equals(parent, d, StringComparison.OrdinalIgnoreCase))
{
yield break;
}
d = parent;
}
}
}