using System.Globalization;
using System.IO;
using System.Text;
using EmbyToolbox.Models;
namespace EmbyToolbox.Services;
/// Имена выходных файлов и аргументы ffmpeg для извлечения аудио/субтитров/вложений (stream copy).
public sealed class ExtractCommandBuilder
{
///
/// Безопасный фрагмент имени без расширения для префикса выходных файлов (Movie из Movie.mkv).
///
public static string SanitizeSourceFileStem(string? fileNameWithoutExtension)
{
if (string.IsNullOrWhiteSpace(fileNameWithoutExtension))
{
return "media";
}
var sb = new StringBuilder(fileNameWithoutExtension.Trim().Length);
foreach (var ch in fileNameWithoutExtension.Trim())
{
sb.Append(ch is < '\u0020' or '"' or '*' or ':' or '<' or '>' or '?' or '\\' or '/' or '|' ? '_' : ch);
}
var s = sb.ToString().Trim('.', ' ');
return string.IsNullOrEmpty(s) ? "media" : s[..Math.Min(s.Length, 200)];
}
/// Базовое имя файла (без пути): все результаты в общих каталогах, префикс — имя исходника без расширения.
public string ResolveOutputBaseFileName(string sourceStemSanitized, MediaStreamInfo stream, int ordinalInKind)
{
return stream.Kind switch
{
MediaStreamKind.Audio =>
$"{sourceStemSanitized}_audio_{ordinalInKind:D2}_{SanitizeLangSegment(stream.Language)}.{GetAudioExtension(stream.CodecName)}",
MediaStreamKind.Subtitle =>
$"{sourceStemSanitized}_subtitle_{ordinalInKind:D2}_{SanitizeLangSegment(stream.Language)}.{GetSubtitleExtension(stream.CodecName)}",
MediaStreamKind.Attachment =>
$"{sourceStemSanitized}_attachment_{ordinalInKind:D2}_{ResolveAttachmentLeafFileName(stream)}",
MediaStreamKind.Video or MediaStreamKind.Data =>
throw new InvalidOperationException("Видео и data-потоки не извлекаются."),
_ =>
throw new InvalidOperationException($"Неподдерживаемый тип потока: {stream.Kind}"),
};
}
public IReadOnlyList BuildFfmpegArgumentList(string inputPath, MediaStreamInfo stream, string destinationFullPath)
{
return new[]
{
"-hide_banner",
"-loglevel",
"error",
"-y",
"-i",
inputPath,
"-map",
$"0:{stream.Index}",
"-c",
"copy",
destinationFullPath,
};
}
private static string GetAudioExtension(string codecRaw)
{
var c = NormalizeCodec(codecRaw);
return c switch
{
"aac" => "aac",
"ac3" => "ac3",
"dts" => "dts",
"flac" => "flac",
"truehd" => "thd",
"eac3" => "eac3",
"opus" => "opus",
"mp3" => "mp3",
"vorbis" => "ogg",
_ => "bin",
};
}
private static string GetSubtitleExtension(string codecRaw)
{
var c = NormalizeCodec(codecRaw);
return c switch
{
"subrip" or "srt" => "srt",
"ass" or "ssa" => "ass",
"mov_text" or "text" or "tx3g" => "txt",
"hdmv_pgs_subtitle" or "pgssub" => "sup",
"dvd_subtitle" or "vobsub" => "sub",
_ => "bin",
};
}
private static string ResolveAttachmentLeafFileName(MediaStreamInfo stream)
{
var declared = stream.AttachmentDeclaredFileName?.Trim();
if (string.IsNullOrEmpty(declared))
{
declared = $"blob_{stream.Index}";
}
declared = SanitizeDeclaredAttachmentFileName(declared);
if (!Path.HasExtension(declared))
{
var mimeExt = MimeToExtension(stream.AttachmentDeclaredMimeType);
if (!string.IsNullOrEmpty(mimeExt))
{
declared += mimeExt.StartsWith('.') ? mimeExt : "." + mimeExt;
}
}
return declared;
}
private static string? MimeToExtension(string? mimeRaw)
{
if (string.IsNullOrWhiteSpace(mimeRaw))
{
return null;
}
var m = mimeRaw.Trim().ToLowerInvariant();
return m switch
{
"font/ttf" or "application/x-truetype-font" => ".ttf",
"font/otf" or "application/font-sfnt" => ".otf",
"application/vnd.ms-opentype" => ".otf",
_ => null,
};
}
private static string SanitizeDeclaredAttachmentFileName(string declared)
{
var cleaned = declared.Replace('\\', '_').Replace('/', '_');
cleaned = Path.GetFileName(cleaned);
var sb = new StringBuilder(cleaned.Length);
foreach (var ch in cleaned)
{
sb.Append(ch is < '\u0020' or '"' or '*' or ':' or '<' or '>' or '?' or '|' ? '_' : ch);
}
cleaned = sb.ToString().Trim('.', '_');
return string.IsNullOrEmpty(cleaned) ? "attachment" : cleaned;
}
private static string SanitizeLangSegment(string? lang)
{
if (string.IsNullOrWhiteSpace(lang))
{
return "und";
}
var s = lang.Trim().ToLowerInvariant();
Span buffer = stackalloc char[s.Length];
for (var i = 0; i < s.Length; i++)
{
var ch = s[i];
buffer[i] = ch is '_' or '-' or >= 'a' and <= 'z' ? ch : '_';
}
var span = new string(buffer).Trim('_');
return string.IsNullOrEmpty(span) ? "und" : span[..Math.Min(span.Length, 24)];
}
private static string NormalizeCodec(string? codecRaw) =>
string.IsNullOrWhiteSpace(codecRaw) ? string.Empty : codecRaw.Trim().ToLowerInvariant();
}