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(); }