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