diff --git a/EmbyToolbox/Behaviors/AvalonEditTextBindingBehavior.cs b/EmbyToolbox/Behaviors/AvalonEditTextBindingBehavior.cs new file mode 100644 index 0000000..7a833b0 --- /dev/null +++ b/EmbyToolbox/Behaviors/AvalonEditTextBindingBehavior.cs @@ -0,0 +1,40 @@ +using System.Windows; +using ICSharpCode.AvalonEdit; + +namespace EmbyToolbox.Behaviors; + +public static class AvalonEditTextBindingBehavior +{ + public static readonly DependencyProperty BoundTextProperty = + DependencyProperty.RegisterAttached( + "BoundText", + typeof(string), + typeof(AvalonEditTextBindingBehavior), + new PropertyMetadata(string.Empty, OnBoundTextChanged)); + + public static string GetBoundText(DependencyObject obj) + { + return (string)obj.GetValue(BoundTextProperty); + } + + public static void SetBoundText(DependencyObject obj, string value) + { + obj.SetValue(BoundTextProperty, value); + } + + private static void OnBoundTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not TextEditor editor) + { + return; + } + + var nextText = e.NewValue as string ?? string.Empty; + if (editor.Text == nextText) + { + return; + } + + editor.Text = nextText; + } +} diff --git a/EmbyToolbox/EmbyToolbox.csproj b/EmbyToolbox/EmbyToolbox.csproj index cc2e6b0..ffa3ed8 100644 --- a/EmbyToolbox/EmbyToolbox.csproj +++ b/EmbyToolbox/EmbyToolbox.csproj @@ -29,4 +29,8 @@ \ + + + + diff --git a/EmbyToolbox/MainWindow.xaml b/EmbyToolbox/MainWindow.xaml index 59a650b..14f6c95 100644 --- a/EmbyToolbox/MainWindow.xaml +++ b/EmbyToolbox/MainWindow.xaml @@ -7,6 +7,7 @@ xmlns:views="clr-namespace:EmbyToolbox.Views" xmlns:viewModels="clr-namespace:EmbyToolbox.ViewModels" xmlns:converters="clr-namespace:EmbyToolbox.Converters" + xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" @@ -472,21 +473,19 @@ - - - - - - - - - - - - - + + diff --git a/EmbyToolbox/Models/MediaAnalysisResult.cs b/EmbyToolbox/Models/MediaAnalysisResult.cs index e2178d7..59bc6b2 100644 --- a/EmbyToolbox/Models/MediaAnalysisResult.cs +++ b/EmbyToolbox/Models/MediaAnalysisResult.cs @@ -18,12 +18,12 @@ public sealed class MediaAnalysisResult public IReadOnlyList AllStreams { get; init; } = Array.Empty(); public long? SourceVideoBitrateBps { get; init; } - /// Основной видеопоток: самый крупный по площади кадра (не первый подряд в JSON — иначе cover/mjpeg может оказаться «основным»). + /// Основной видеопоток: default среди обычных video, затем самый крупный по площади кадра. public MediaStreamInfo? PrimaryVideo => VideoStreams .Where(static v => !v.IsAttachedPicture) - .OrderByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0)) - .ThenByDescending(static v => v.IsDefault ? 1 : 0) + .OrderByDescending(static v => v.IsDefault ? 1 : 0) + .ThenByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0)) .FirstOrDefault(); /// format.duration, иначе максимум duration среди streams (ffprobe). diff --git a/EmbyToolbox/Services/MediaAnalysisParser.cs b/EmbyToolbox/Services/MediaAnalysisParser.cs index 780463f..afb6a92 100644 --- a/EmbyToolbox/Services/MediaAnalysisParser.cs +++ b/EmbyToolbox/Services/MediaAnalysisParser.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.IO; using System.Linq; using System.Text.Json; using EmbyToolbox.Models; @@ -69,8 +70,8 @@ public static class MediaAnalysisParser var primaryVideoBitrate = streams .Where(x => x.Kind == MediaStreamKind.Video) .Where(x => !x.IsAttachedPicture) - .OrderByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0)) - .ThenByDescending(static v => v.IsDefault ? 1 : 0) + .OrderByDescending(static v => v.IsDefault ? 1 : 0) + .ThenByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0)) .Select(static v => v.BitRateBps) .FirstOrDefault(); @@ -100,6 +101,7 @@ public static class MediaAnalysisParser var t = (ct.GetString() ?? string.Empty).ToLowerInvariant(); if (t == "video") { + var isAttachedPicture = GetDisposition(s, "attached_pic", false) || IsCoverArtVideoStream(s); return new MediaStreamInfo { Index = GetInt(s, "index", -1), @@ -118,7 +120,7 @@ public static class MediaAnalysisParser ColorSpace = GetStr(s, "color_space", null), ColorPrimaries = GetStr(s, "color_primaries", null), ColorTransfer = GetStr(s, "color_transfer", null), - IsAttachedPicture = GetDisposition(s, "attached_pic", false), + IsAttachedPicture = isAttachedPicture, BitRateBps = ParseLong(s, "bit_rate") ?? ParseLong(s, "max_bit_rate"), DurationSeconds = streamDuration }; @@ -301,12 +303,58 @@ public static class MediaAnalysisParser return null; } - if (!tags.TryGetProperty(tagKey, out var val) || val.ValueKind != JsonValueKind.String) + foreach (var tag in tags.EnumerateObject()) { - return null; + if (string.Equals(tag.Name, tagKey, StringComparison.OrdinalIgnoreCase) && + tag.Value.ValueKind == JsonValueKind.String) + { + return tag.Value.GetString(); + } } - return val.GetString(); + return null; + } + + private static bool IsCoverArtVideoStream(JsonElement stream) + { + var mimeType = GetTagExact(stream, "mimetype"); + if (!string.IsNullOrWhiteSpace(mimeType) && + mimeType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + var fileName = GetTagExact(stream, "filename"); + if (string.IsNullOrWhiteSpace(fileName)) + { + return false; + } + + var codec = GetStr(stream, "codec_name", string.Empty) ?? string.Empty; + if (!IsStillImageCodec(codec)) + { + return false; + } + + var extension = Path.GetExtension(fileName); + return extension.Equals(".jpg", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".png", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".webp", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".bmp", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".gif", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".tif", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".tiff", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsStillImageCodec(string codec) + { + return codec.Equals("mjpeg", StringComparison.OrdinalIgnoreCase) + || codec.Equals("png", StringComparison.OrdinalIgnoreCase) + || codec.Equals("webp", StringComparison.OrdinalIgnoreCase) + || codec.Equals("bmp", StringComparison.OrdinalIgnoreCase) + || codec.Equals("gif", StringComparison.OrdinalIgnoreCase) + || codec.Equals("tiff", StringComparison.OrdinalIgnoreCase); } private static bool GetDisposition(JsonElement s, string d, bool def) diff --git a/EmbyToolbox/ViewModels/VideoInfoViewModel.cs b/EmbyToolbox/ViewModels/VideoInfoViewModel.cs index 66c0811..d439233 100644 --- a/EmbyToolbox/ViewModels/VideoInfoViewModel.cs +++ b/EmbyToolbox/ViewModels/VideoInfoViewModel.cs @@ -23,7 +23,6 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged private string _analysisStateText = string.Empty; private string _errorMessage = string.Empty; private string _formattedJson = string.Empty; - private IReadOnlyList _formattedJsonLines = Array.Empty(); private string _summaryText = string.Empty; private bool _isBusy; private bool _isVideoInfoDropHighlight; @@ -110,22 +109,6 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged } _formattedJson = value; - FormattedJsonLines = SplitTextLines(value); - OnPropertyChanged(); - } - } - - public IReadOnlyList FormattedJsonLines - { - get => _formattedJsonLines; - private set - { - if (ReferenceEquals(_formattedJsonLines, value)) - { - return; - } - - _formattedJsonLines = value; OnPropertyChanged(); } } @@ -496,14 +479,5 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged } } - private static IReadOnlyList SplitTextLines(string text) - { - if (string.IsNullOrEmpty(text)) - { - return Array.Empty(); - } - - return text.ReplaceLineEndings("\n").Split('\n'); - } }