diff --git a/EmbyToolbox/App.xaml.cs b/EmbyToolbox/App.xaml.cs
index 83a43ed..7d973a5 100644
--- a/EmbyToolbox/App.xaml.cs
+++ b/EmbyToolbox/App.xaml.cs
@@ -8,9 +8,9 @@ public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
+ _ = ToastShortcutRegistration.TryEnsureStartMenuShortcut(NotificationService.ToastAppUserModelId);
_ = AppUserModelIdRegistration.TryRegister(NotificationService.ToastAppUserModelId);
base.OnStartup(e);
}
}
-
diff --git a/EmbyToolbox/Interop/ToastShortcutRegistration.cs b/EmbyToolbox/Interop/ToastShortcutRegistration.cs
new file mode 100644
index 0000000..ebe05db
--- /dev/null
+++ b/EmbyToolbox/Interop/ToastShortcutRegistration.cs
@@ -0,0 +1,155 @@
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+
+namespace EmbyToolbox.Interop;
+
+internal static class ToastShortcutRegistration
+{
+ private const int StgmReadwrite = 0x00000002;
+
+ private static readonly PropertyKey AppUserModelIdKey = new(
+ new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"),
+ 5);
+
+ public static string? LastDiagnostics { get; private set; }
+
+ public static bool TryEnsureStartMenuShortcut(string appUserModelId)
+ {
+ LastDiagnostics = null;
+
+ if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 10240))
+ {
+ LastDiagnostics = "требуется Windows 10 (10240) или новее.";
+ return false;
+ }
+
+ try
+ {
+ var exePath = Environment.ProcessPath;
+ if (string.IsNullOrWhiteSpace(exePath) || !File.Exists(exePath))
+ {
+ LastDiagnostics = "не удалось определить путь к exe.";
+ return false;
+ }
+
+ var programs = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu);
+ var shortcutPath = Path.Combine(programs, "Programs", "Emby Toolbox.lnk");
+ Directory.CreateDirectory(Path.GetDirectoryName(shortcutPath)!);
+
+ var shellLinkObject = (object)new CShellLink();
+ var shellLink = (IShellLinkW)shellLinkObject;
+ shellLink.SetPath(exePath);
+ shellLink.SetArguments(string.Empty);
+ shellLink.SetWorkingDirectory(Path.GetDirectoryName(exePath) ?? AppContext.BaseDirectory);
+ shellLink.SetDescription("Emby Toolbox");
+
+ if (File.Exists(Path.Combine(AppContext.BaseDirectory, "Resources", "AppIcon.ico")))
+ {
+ shellLink.SetIconLocation(Path.Combine(AppContext.BaseDirectory, "Resources", "AppIcon.ico"), 0);
+ }
+ else
+ {
+ shellLink.SetIconLocation(exePath, 0);
+ }
+
+ using var appId = PropVariant.FromString(appUserModelId);
+ var propertyStore = (IPropertyStore)shellLink;
+ propertyStore.SetValue(AppUserModelIdKey, appId);
+ propertyStore.Commit();
+
+ var persistFile = (IPersistFile)shellLink;
+ persistFile.Save(shortcutPath, true);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ LastDiagnostics = $"{ex.GetType().Name}: {ex.Message}";
+ return false;
+ }
+ }
+
+ [ComImport]
+ [Guid("00021401-0000-0000-C000-000000000046")]
+ private sealed class CShellLink
+ {
+ }
+
+ [ComImport]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ [Guid("000214F9-0000-0000-C000-000000000046")]
+ private interface IShellLinkW
+ {
+ void GetPath(IntPtr pszFile, int cchMaxPath, IntPtr pfd, uint fFlags);
+ void GetIDList(out IntPtr ppidl);
+ void SetIDList(IntPtr pidl);
+ void GetDescription(IntPtr pszName, int cchMaxName);
+ void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
+ void GetWorkingDirectory(IntPtr pszDir, int cchMaxPath);
+ void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
+ void GetArguments(IntPtr pszArgs, int cchMaxPath);
+ void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
+ void GetHotkey(out short pwHotkey);
+ void SetHotkey(short wHotkey);
+ void GetShowCmd(out int piShowCmd);
+ void SetShowCmd(int iShowCmd);
+ void GetIconLocation(IntPtr pszIconPath, int cchIconPath, out int piIcon);
+ void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
+ void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, uint dwReserved);
+ void Resolve(IntPtr hwnd, uint fFlags);
+ void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
+ }
+
+ [ComImport]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ [Guid("00000138-0000-0000-C000-000000000046")]
+ private interface IPropertyStore
+ {
+ void GetCount(out uint cProps);
+ void GetAt(uint iProp, out PropertyKey pkey);
+ void GetValue(ref PropertyKey key, out PropVariant pv);
+ void SetValue(in PropertyKey key, in PropVariant pv);
+ void Commit();
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 4)]
+ private readonly struct PropertyKey(Guid formatId, int propertyId)
+ {
+ private readonly Guid _formatId = formatId;
+ private readonly int _propertyId = propertyId;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private sealed class PropVariant : IDisposable
+ {
+ private ushort _valueType;
+ private ushort _reserved1;
+ private ushort _reserved2;
+ private ushort _reserved3;
+ private IntPtr _value;
+ private IntPtr _reserved4;
+
+ public static PropVariant FromString(string value)
+ {
+ return new PropVariant
+ {
+ _valueType = 31, // VT_LPWSTR
+ _value = Marshal.StringToCoTaskMemUni(value)
+ };
+ }
+
+ public void Dispose()
+ {
+ PropVariantClear(this);
+ GC.SuppressFinalize(this);
+ }
+
+ ~PropVariant()
+ {
+ PropVariantClear(this);
+ }
+
+ [DllImport("ole32.dll")]
+ private static extern int PropVariantClear([In, Out] PropVariant pvar);
+ }
+}
diff --git a/EmbyToolbox/MainWindow.xaml b/EmbyToolbox/MainWindow.xaml
index 98f65e8..59a650b 100644
--- a/EmbyToolbox/MainWindow.xaml
+++ b/EmbyToolbox/MainWindow.xaml
@@ -368,7 +368,6 @@
-
@@ -412,37 +411,6 @@
-
-
-
-
-
-
-
-
-
@@ -463,43 +431,6 @@
-
-
-
@@ -541,9 +472,21 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
@@ -596,40 +539,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
+
-
+
+
-
@@ -645,107 +645,139 @@
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
+
+
+
+
+
+
-
+
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/EmbyToolbox/MainWindow.xaml.cs b/EmbyToolbox/MainWindow.xaml.cs
index 8acdfce..811ee44 100644
--- a/EmbyToolbox/MainWindow.xaml.cs
+++ b/EmbyToolbox/MainWindow.xaml.cs
@@ -1,7 +1,4 @@
using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Input;
-using System.Windows.Media;
using EmbyToolbox.ViewModels;
namespace EmbyToolbox;
@@ -13,19 +10,4 @@ public partial class MainWindow
InitializeComponent();
DataContext = new MainWindowViewModel();
}
-
- private void OnJsonTreeItemPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
- {
- var dependencyObject = e.OriginalSource as DependencyObject;
- while (dependencyObject is not null && dependencyObject is not TreeViewItem)
- {
- dependencyObject = VisualTreeHelper.GetParent(dependencyObject);
- }
-
- if (dependencyObject is TreeViewItem item)
- {
- item.IsSelected = true;
- item.Focus();
- }
- }
}
diff --git a/EmbyToolbox/Models/AddFilesOptions.cs b/EmbyToolbox/Models/AddFilesOptions.cs
index 15d4736..a40a293 100644
--- a/EmbyToolbox/Models/AddFilesOptions.cs
+++ b/EmbyToolbox/Models/AddFilesOptions.cs
@@ -5,4 +5,5 @@ public sealed class AddFilesOptions
public string Profile { get; init; } = "Emby";
public bool DisableSubtitleDefault { get; init; }
public bool RemoveForeignAudioAndSubtitles { get; init; }
+ public EffectiveProfileSettings EffectiveSettings { get; init; } = new();
}
diff --git a/EmbyToolbox/Models/ConversionQueueItem.cs b/EmbyToolbox/Models/ConversionQueueItem.cs
index f247af8..74d4993 100644
--- a/EmbyToolbox/Models/ConversionQueueItem.cs
+++ b/EmbyToolbox/Models/ConversionQueueItem.cs
@@ -24,6 +24,7 @@ public sealed class ConversionQueueItem : INotifyPropertyChanged
private MediaAnalysisResult? _mediaAnalysis;
private IReadOnlyList _sidecars = System.Array.Empty();
private IReadOnlyList _externalAudioFiles = System.Array.Empty();
+ private EffectiveProfileSettings? _effectiveProfileSettings;
private ConversionPlan? _lastPlan;
private bool _isProcessed;
private bool _processedInCurrentRun;
@@ -133,6 +134,21 @@ public sealed class ConversionQueueItem : INotifyPropertyChanged
public ConversionTaskOverride TaskOverride { get; } = new();
+ public EffectiveProfileSettings? EffectiveProfileSettings
+ {
+ get => _effectiveProfileSettings;
+ set
+ {
+ if (ReferenceEquals(_effectiveProfileSettings, value))
+ {
+ return;
+ }
+
+ _effectiveProfileSettings = value;
+ OnPropertyChanged();
+ }
+ }
+
public ConversionPlan? LastPlan
{
get => _lastPlan;
@@ -484,6 +500,7 @@ public sealed class ConversionQueueItem : INotifyPropertyChanged
}
_profile = value;
+ EffectiveProfileSettings = null;
OnPropertyChanged();
}
}
diff --git a/EmbyToolbox/Models/EffectiveProfileSettings.cs b/EmbyToolbox/Models/EffectiveProfileSettings.cs
new file mode 100644
index 0000000..2b3f3a2
--- /dev/null
+++ b/EmbyToolbox/Models/EffectiveProfileSettings.cs
@@ -0,0 +1,12 @@
+using EmbyToolbox.Services;
+
+namespace EmbyToolbox.Models;
+
+public sealed class EffectiveProfileSettings
+{
+ public string SourceProfileName { get; init; } = "Emby";
+ public ConversionProfileSettingsEntry Profile { get; init; } = new();
+ public IReadOnlyList ChangedFields { get; init; } = [];
+
+ public bool HasOverrides => ChangedFields.Count > 0;
+}
diff --git a/EmbyToolbox/Services/ConversionExecutionService.cs b/EmbyToolbox/Services/ConversionExecutionService.cs
index 811f5ee..fecd9e9 100644
--- a/EmbyToolbox/Services/ConversionExecutionService.cs
+++ b/EmbyToolbox/Services/ConversionExecutionService.cs
@@ -103,7 +103,9 @@ public sealed class ConversionExecutionService
item.ErrorDetails = null;
}).ConfigureAwait(false);
- var profile = resolveProfile(item.Profile) ?? ConversionProfileMapping.EmbyFallback;
+ var profile = item.EffectiveProfileSettings?.Profile
+ ?? resolveProfile(item.Profile)
+ ?? ConversionProfileMapping.EmbyFallback;
targetContainer = ResolveTargetContainer(item, profile);
finalPath = BuildFinalPath(item.FullPath, targetContainer);
tempOut = Path.Combine(tempRoot, $"{Path.GetFileNameWithoutExtension(item.FileName)}.__processing__{GetOutputExtension(targetContainer)}");
diff --git a/EmbyToolbox/Services/NotificationService.cs b/EmbyToolbox/Services/NotificationService.cs
index 9dad4d5..8db34f5 100644
--- a/EmbyToolbox/Services/NotificationService.cs
+++ b/EmbyToolbox/Services/NotificationService.cs
@@ -35,6 +35,13 @@ public sealed class NotificationService
private void LogAppUserModelRegistrationState()
{
+ if (!string.IsNullOrWhiteSpace(ToastShortcutRegistration.LastDiagnostics))
+ {
+ _logging.Warning(
+ $"Windows toast: ярлык Start Menu не подготовлен ({ToastShortcutRegistration.LastDiagnostics})",
+ "notify");
+ }
+
if (string.Equals(AppUserModelIdRegistration.LastRegisteredId, ToastAppUserModelId, StringComparison.Ordinal))
{
_logging.Info($"App User Model ID: {ToastAppUserModelId}", "notify");
diff --git a/EmbyToolbox/Services/ProfileOverrideBuilder.cs b/EmbyToolbox/Services/ProfileOverrideBuilder.cs
new file mode 100644
index 0000000..e4f9cec
--- /dev/null
+++ b/EmbyToolbox/Services/ProfileOverrideBuilder.cs
@@ -0,0 +1,65 @@
+using EmbyToolbox.Models;
+
+namespace EmbyToolbox.Services;
+
+public sealed class ProfileOverrideBuilder
+{
+ public EffectiveProfileSettings Build(
+ ConversionProfileSettingsEntry source,
+ ConversionProfileSettingsEntry formValues,
+ string sourceProfileName)
+ {
+ var changed = new List();
+ AddIfChanged(changed, "Container", source.Container, formValues.Container);
+ AddIfChanged(changed, "Video", source.Video, formValues.Video);
+ AddIfChanged(changed, "PixelFormat", source.PixelFormat, formValues.PixelFormat);
+ AddIfChanged(changed, "Resolution", source.Resolution, formValues.Resolution);
+ AddIfChanged(changed, "Fps", source.Fps, formValues.Fps);
+ AddIfChanged(changed, "Audio", source.Audio, formValues.Audio);
+ AddIfChanged(changed, "Bitrate", source.Bitrate, formValues.Bitrate);
+ AddIfChanged(changed, "VideoBitrateMode", source.VideoBitrateMode, formValues.VideoBitrateMode);
+ if (source.VideoBitrateMbps != formValues.VideoBitrateMbps)
+ {
+ changed.Add("VideoBitrateMbps");
+ }
+
+ AddIfChanged(changed, "Subtitles", source.Subtitles, formValues.Subtitles);
+ AddIfChanged(changed, "ExternalTracks", source.ExternalTracks, formValues.ExternalTracks);
+ AddIfChanged(changed, "ExternalSubtitles", source.ExternalSubtitles, formValues.ExternalSubtitles);
+ AddIfChanged(changed, "Fonts", source.Fonts, formValues.Fonts);
+
+ return new EffectiveProfileSettings
+ {
+ SourceProfileName = sourceProfileName,
+ Profile = Clone(formValues, sourceProfileName),
+ ChangedFields = changed
+ };
+ }
+
+ public static ConversionProfileSettingsEntry Clone(ConversionProfileSettingsEntry source, string? profileName = null) =>
+ new()
+ {
+ Profile = string.IsNullOrWhiteSpace(profileName) ? source.Profile : profileName!,
+ Container = source.Container,
+ Video = source.Video,
+ PixelFormat = source.PixelFormat,
+ Resolution = source.Resolution,
+ Fps = source.Fps,
+ Audio = source.Audio,
+ Bitrate = source.Bitrate,
+ VideoBitrateMode = source.VideoBitrateMode,
+ VideoBitrateMbps = source.VideoBitrateMbps,
+ Subtitles = source.Subtitles,
+ ExternalTracks = source.ExternalTracks,
+ ExternalSubtitles = source.ExternalSubtitles,
+ Fonts = source.Fonts
+ };
+
+ private static void AddIfChanged(List changed, string name, string? source, string? value)
+ {
+ if (!string.Equals(source?.Trim(), value?.Trim(), StringComparison.Ordinal))
+ {
+ changed.Add(name);
+ }
+ }
+}
diff --git a/EmbyToolbox/Services/QueueAnalysisService.cs b/EmbyToolbox/Services/QueueAnalysisService.cs
index 1f866e4..b88b8c0 100644
--- a/EmbyToolbox/Services/QueueAnalysisService.cs
+++ b/EmbyToolbox/Services/QueueAnalysisService.cs
@@ -102,7 +102,7 @@ public sealed class QueueAnalysisService
await uiInvoke(
() =>
{
- var profile = _profile.GetProfile(item.Profile) ?? ConversionProfileMapping.EmbyFallback;
+ var profile = ResolveEffectiveProfile(item);
item.TaskOverride.TrackOverrides.Clear();
TrackOverrideSeeder.EnsureDefaults(
item.TaskOverride,
@@ -297,7 +297,7 @@ public sealed class QueueAnalysisService
var audio = FfprobeAudioInfoParser.TryParse(result.Json) ?? new FfprobeAudioInfo(0, null, true);
var side = discovery.Sidecars;
- var profile = _profile.GetProfile(item.Profile) ?? ConversionProfileMapping.EmbyFallback;
+ var profile = ResolveEffectiveProfile(item);
// При повторном анализе sidecar-набор может измениться (добавили/удалили внешние файлы).
// Пересобираем список дорожек, чтобы не держать устаревшие external entries.
item.TaskOverride.TrackOverrides.Clear();
@@ -419,6 +419,11 @@ public sealed class QueueAnalysisService
public int ErrorCount;
}
+ private ConversionProfileSettingsEntry ResolveEffectiveProfile(ConversionQueueItem item) =>
+ item.EffectiveProfileSettings?.Profile
+ ?? _profile.GetProfile(item.Profile)
+ ?? ConversionProfileMapping.EmbyFallback;
+
private static bool IsForeignLanguageForAutoRemove(string? language)
{
if (string.IsNullOrWhiteSpace(language))
diff --git a/EmbyToolbox/Services/VideoBitratePolicy.cs b/EmbyToolbox/Services/VideoBitratePolicy.cs
index 4dbc702..69e4c24 100644
--- a/EmbyToolbox/Services/VideoBitratePolicy.cs
+++ b/EmbyToolbox/Services/VideoBitratePolicy.cs
@@ -15,6 +15,7 @@ public static class VideoBitratePolicy
Auto,
Source,
"2 Mbps",
+ "3 Mbps",
"4 Mbps",
"6 Mbps",
"8 Mbps",
diff --git a/EmbyToolbox/ViewModels/AddFilesOptionsViewModel.cs b/EmbyToolbox/ViewModels/AddFilesOptionsViewModel.cs
deleted file mode 100644
index 6823e6e..0000000
--- a/EmbyToolbox/ViewModels/AddFilesOptionsViewModel.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-using System.ComponentModel;
-using System.Collections.ObjectModel;
-using System.Runtime.CompilerServices;
-using EmbyToolbox.Models;
-
-namespace EmbyToolbox.ViewModels;
-
-public sealed class AddFilesOptionsViewModel : INotifyPropertyChanged
-{
- private readonly Action _onAdd;
- private readonly Action _onCancel;
-
- private bool _disableSubtitleDefault;
- private bool _removeForeignAudioAndSubtitles;
- private ConversionProfilePresetRow? _selectedProfile;
-
- public AddFilesOptionsViewModel(
- IReadOnlyList profiles,
- string selectedProfileName,
- bool disableSubtitleDefault,
- bool removeForeignAudioAndSubtitles,
- Action onAdd,
- Action onCancel)
- {
- _onAdd = onAdd;
- _onCancel = onCancel;
- _disableSubtitleDefault = disableSubtitleDefault;
- _removeForeignAudioAndSubtitles = removeForeignAudioAndSubtitles;
- Profiles = new ObservableCollection(profiles);
- _selectedProfile = Profiles.FirstOrDefault(p => p.Profile.Equals(selectedProfileName, StringComparison.OrdinalIgnoreCase))
- ?? Profiles.FirstOrDefault(p => p.Profile.Equals("Emby", StringComparison.OrdinalIgnoreCase))
- ?? Profiles.FirstOrDefault();
- AddCommand = new RelayCommand(ExecuteAdd);
- CancelCommand = new RelayCommand(() => _onCancel());
- }
-
- public bool DisableSubtitleDefault
- {
- get => _disableSubtitleDefault;
- set
- {
- if (_disableSubtitleDefault == value)
- {
- return;
- }
-
- _disableSubtitleDefault = value;
- OnPropertyChanged();
- }
- }
-
- public bool RemoveForeignAudioAndSubtitles
- {
- get => _removeForeignAudioAndSubtitles;
- set
- {
- if (_removeForeignAudioAndSubtitles == value)
- {
- return;
- }
-
- _removeForeignAudioAndSubtitles = value;
- OnPropertyChanged();
- }
- }
-
- public ObservableCollection Profiles { get; }
-
- public ConversionProfilePresetRow? SelectedProfile
- {
- get => _selectedProfile;
- set
- {
- if (ReferenceEquals(_selectedProfile, value))
- {
- return;
- }
-
- _selectedProfile = value;
- OnPropertyChanged();
- }
- }
-
- public RelayCommand AddCommand { get; }
- public RelayCommand CancelCommand { get; }
-
- private void ExecuteAdd()
- {
- _onAdd(
- new AddFilesOptions
- {
- Profile = string.IsNullOrWhiteSpace(_selectedProfile?.Profile) ? "Emby" : _selectedProfile.Profile,
- DisableSubtitleDefault = _disableSubtitleDefault,
- RemoveForeignAudioAndSubtitles = _removeForeignAudioAndSubtitles
- });
- }
-
- public event PropertyChangedEventHandler? PropertyChanged;
-
- private void OnPropertyChanged([CallerMemberName] string? name = null) =>
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
-}
diff --git a/EmbyToolbox/ViewModels/AddFilesSettingsViewModel.cs b/EmbyToolbox/ViewModels/AddFilesSettingsViewModel.cs
new file mode 100644
index 0000000..56d4032
--- /dev/null
+++ b/EmbyToolbox/ViewModels/AddFilesSettingsViewModel.cs
@@ -0,0 +1,351 @@
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using EmbyToolbox.Models;
+using EmbyToolbox.Services;
+
+namespace EmbyToolbox.ViewModels;
+
+public sealed class AddFilesSettingsViewModel : INotifyPropertyChanged
+{
+ private readonly IReadOnlyList _profiles;
+ private readonly Action _onAdd;
+ private readonly Action _onCancel;
+ private readonly ProfileOverrideBuilder _builder = new();
+ private ConversionProfileSettingsEntry _sourceProfile = new();
+ private ConversionProfilePresetRow? _selectedProfile;
+ private string _container = string.Empty;
+ private string _video = string.Empty;
+ private string _pixelFormat = string.Empty;
+ private string _resolution = string.Empty;
+ private string _fps = string.Empty;
+ private string _videoBitrateMode = VideoBitratePolicy.Auto;
+ private string _videoBitrateCustomMbps = string.Empty;
+ private string _audio = string.Empty;
+ private string _audioBitrate = string.Empty;
+ private bool _externalTracks;
+ private bool _externalSubtitles;
+ private bool _fonts;
+ private bool _removeForeignTracks;
+ private bool _disableSubtitleDefault;
+ private string _validationMessage = string.Empty;
+ private bool _isValid;
+
+ public AddFilesSettingsViewModel(
+ IReadOnlyList profiles,
+ ConversionFormOptions formOptions,
+ AddFilesSettingsState? previousState,
+ string fallbackProfileName,
+ bool fallbackDisableSubtitleDefault,
+ bool fallbackRemoveForeignTracks,
+ Action onAdd,
+ Action onCancel)
+ {
+ _profiles = profiles;
+ _onAdd = onAdd;
+ _onCancel = onCancel;
+ Profiles = new ObservableCollection(profiles);
+ ContainerOptions = formOptions.ContainerOptions;
+ VideoCodecOptions = formOptions.VideoCodecOptions;
+ PixelFormatOptions = formOptions.PixelFormatOptions;
+ ResolutionOptions = formOptions.ResolutionOptions;
+ FpsOptions = formOptions.FpsOptions;
+ AudioCodecOptions = formOptions.AudioCodecOptions;
+ AudioBitrateOptions = formOptions.AudioBitrateKbps;
+ VideoBitrateOptions = VideoBitratePolicy.UiOptions;
+ YesNoOptions = ["Да", "Нет"];
+ AddCommand = new RelayCommand(ExecuteAdd, CanAdd);
+ CancelCommand = new RelayCommand(() => _onCancel());
+
+ var profileName = string.IsNullOrWhiteSpace(previousState?.ProfileName)
+ ? fallbackProfileName
+ : previousState!.ProfileName;
+ _selectedProfile = Profiles.FirstOrDefault(p => p.Profile.Equals(profileName, StringComparison.OrdinalIgnoreCase))
+ ?? Profiles.FirstOrDefault(p => p.Profile.Equals("Emby", StringComparison.OrdinalIgnoreCase))
+ ?? Profiles.FirstOrDefault();
+
+ if (_selectedProfile is not null)
+ {
+ LoadProfile(_selectedProfile.ToSettingsEntry(), resetUserValues: true);
+ }
+
+ if (previousState is not null)
+ {
+ ApplyState(previousState);
+ }
+ else
+ {
+ _disableSubtitleDefault = fallbackDisableSubtitleDefault;
+ _removeForeignTracks = fallbackRemoveForeignTracks;
+ }
+
+ Validate();
+ }
+
+ public ObservableCollection Profiles { get; }
+ public IReadOnlyList ContainerOptions { get; }
+ public IReadOnlyList VideoCodecOptions { get; }
+ public IReadOnlyList PixelFormatOptions { get; }
+ public IReadOnlyList ResolutionOptions { get; }
+ public IReadOnlyList FpsOptions { get; }
+ public IReadOnlyList AudioCodecOptions { get; }
+ public IReadOnlyList AudioBitrateOptions { get; }
+ public IReadOnlyList VideoBitrateOptions { get; }
+ public IReadOnlyList YesNoOptions { get; }
+
+ public RelayCommand AddCommand { get; }
+ public RelayCommand CancelCommand { get; }
+
+ public ConversionProfilePresetRow? SelectedProfile
+ {
+ get => _selectedProfile;
+ set
+ {
+ if (ReferenceEquals(_selectedProfile, value))
+ {
+ return;
+ }
+
+ _selectedProfile = value;
+ OnPropertyChanged();
+ if (value is not null)
+ {
+ LoadProfile(value.ToSettingsEntry(), resetUserValues: true);
+ }
+ }
+ }
+
+ public string Container { get => _container; set => SetField(ref _container, value); }
+ public string Video { get => _video; set => SetField(ref _video, value); }
+ public string PixelFormat { get => _pixelFormat; set => SetField(ref _pixelFormat, value); }
+ public string Resolution { get => _resolution; set => SetField(ref _resolution, value); }
+ public string Fps { get => _fps; set => SetField(ref _fps, value); }
+ public string VideoBitrateMode
+ {
+ get => _videoBitrateMode;
+ set
+ {
+ if (SetField(ref _videoBitrateMode, string.IsNullOrWhiteSpace(value) ? VideoBitratePolicy.Auto : value))
+ {
+ OnPropertyChanged(nameof(IsVideoBitrateCustomVisible));
+ }
+ }
+ }
+
+ public string VideoBitrateCustomMbps { get => _videoBitrateCustomMbps; set => SetField(ref _videoBitrateCustomMbps, value); }
+ public string Audio { get => _audio; set => SetField(ref _audio, value); }
+ public string AudioBitrate { get => _audioBitrate; set => SetField(ref _audioBitrate, value); }
+ public bool ExternalTracks { get => _externalTracks; set => SetField(ref _externalTracks, value); }
+ public bool ExternalSubtitles { get => _externalSubtitles; set => SetField(ref _externalSubtitles, value); }
+ public bool Fonts { get => _fonts; set => SetField(ref _fonts, value); }
+ public bool RemoveForeignTracks { get => _removeForeignTracks; set => SetField(ref _removeForeignTracks, value); }
+ public bool DisableSubtitleDefault { get => _disableSubtitleDefault; set => SetField(ref _disableSubtitleDefault, value); }
+ public bool IsVideoBitrateCustomVisible => string.Equals(VideoBitrateMode, VideoBitratePolicy.Custom, StringComparison.Ordinal);
+ public bool HasValidationMessage => !string.IsNullOrWhiteSpace(ValidationMessage);
+
+ public string ValidationMessage
+ {
+ get => _validationMessage;
+ private set
+ {
+ if (_validationMessage == value)
+ {
+ return;
+ }
+
+ _validationMessage = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(HasValidationMessage));
+ }
+ }
+
+ public AddFilesSettingsState CaptureState() =>
+ new AddFilesSettingsState
+ {
+ ProfileName = SelectedProfile?.Profile ?? "Emby",
+ ChangedFields = BuildEffectiveSettings().ChangedFields.ToArray(),
+ Container = Container,
+ Video = Video,
+ PixelFormat = PixelFormat,
+ Resolution = Resolution,
+ Fps = Fps,
+ VideoBitrateMode = VideoBitrateMode,
+ VideoBitrateCustomMbps = VideoBitrateCustomMbps,
+ Audio = Audio,
+ AudioBitrate = AudioBitrate,
+ ExternalTracks = ExternalTracks,
+ ExternalSubtitles = ExternalSubtitles,
+ Fonts = Fonts,
+ RemoveForeignTracks = RemoveForeignTracks,
+ DisableSubtitleDefault = DisableSubtitleDefault
+ };
+
+ private void LoadProfile(ConversionProfileSettingsEntry profile, bool resetUserValues)
+ {
+ _sourceProfile = ProfileOverrideBuilder.Clone(profile);
+ if (!resetUserValues)
+ {
+ return;
+ }
+
+ Container = profile.Container;
+ Video = profile.Video;
+ PixelFormat = profile.PixelFormat;
+ Resolution = profile.Resolution;
+ Fps = profile.Fps;
+ VideoBitrateMode = VideoBitratePolicy.NormalizeMode(profile.VideoBitrateMode);
+ VideoBitrateCustomMbps = profile.VideoBitrateMbps?.ToString("0.###", CultureInfo.InvariantCulture) ?? string.Empty;
+ Audio = profile.Audio;
+ AudioBitrate = profile.Bitrate;
+ ExternalTracks = IsYes(profile.ExternalTracks);
+ ExternalSubtitles = IsYes(profile.ExternalSubtitles);
+ Fonts = IsYes(profile.Fonts);
+ }
+
+ private void ApplyState(AddFilesSettingsState state)
+ {
+ var changed = state.ChangedFields.ToHashSet(StringComparer.Ordinal);
+ if (changed.Contains("Container")) Container = state.Container;
+ if (changed.Contains("Video")) Video = state.Video;
+ if (changed.Contains("PixelFormat")) PixelFormat = state.PixelFormat;
+ if (changed.Contains("Resolution")) Resolution = state.Resolution;
+ if (changed.Contains("Fps")) Fps = state.Fps;
+ if (changed.Contains("VideoBitrateMode")) VideoBitrateMode = state.VideoBitrateMode;
+ if (changed.Contains("VideoBitrateMbps")) VideoBitrateCustomMbps = state.VideoBitrateCustomMbps;
+ if (changed.Contains("Audio")) Audio = state.Audio;
+ if (changed.Contains("Bitrate")) AudioBitrate = state.AudioBitrate;
+ if (changed.Contains("ExternalTracks")) ExternalTracks = state.ExternalTracks;
+ if (changed.Contains("ExternalSubtitles")) ExternalSubtitles = state.ExternalSubtitles;
+ if (changed.Contains("Fonts")) Fonts = state.Fonts;
+ RemoveForeignTracks = state.RemoveForeignTracks;
+ DisableSubtitleDefault = state.DisableSubtitleDefault;
+ }
+
+ private bool CanAdd() => _isValid;
+
+ private void ExecuteAdd()
+ {
+ if (!Validate())
+ {
+ return;
+ }
+
+ var effective = BuildEffectiveSettings();
+ _onAdd(
+ new AddFilesOptions
+ {
+ Profile = effective.SourceProfileName,
+ DisableSubtitleDefault = DisableSubtitleDefault,
+ RemoveForeignAudioAndSubtitles = RemoveForeignTracks,
+ EffectiveSettings = effective
+ });
+ }
+
+ private EffectiveProfileSettings BuildEffectiveSettings()
+ {
+ var form = new ConversionProfileSettingsEntry
+ {
+ Profile = SelectedProfile?.Profile ?? "Emby",
+ Container = Container,
+ Video = Video,
+ PixelFormat = PixelFormat,
+ Resolution = Resolution,
+ Fps = Fps,
+ Audio = Audio,
+ Bitrate = AudioBitrate,
+ VideoBitrateMode = VideoBitrateMode,
+ VideoBitrateMbps = IsVideoBitrateCustomVisible && TryParseCustomVideoBitrate(VideoBitrateCustomMbps, out var mbps) ? mbps : null,
+ Subtitles = _sourceProfile.Subtitles,
+ ExternalTracks = ToYesNo(ExternalTracks),
+ ExternalSubtitles = ToYesNo(ExternalSubtitles),
+ Fonts = ToYesNo(Fonts)
+ };
+
+ return _builder.Build(_sourceProfile, form, SelectedProfile?.Profile ?? "Emby");
+ }
+
+ private bool Validate()
+ {
+ if (SelectedProfile is null)
+ {
+ ValidationMessage = "Выберите профиль.";
+ _isValid = false;
+ AddCommand.RaiseCanExecuteChanged();
+ return false;
+ }
+
+ if (IsVideoBitrateCustomVisible && !TryParseCustomVideoBitrate(VideoBitrateCustomMbps, out _))
+ {
+ ValidationMessage = "Видеобитрейт должен быть числом больше 0.";
+ _isValid = false;
+ AddCommand.RaiseCanExecuteChanged();
+ return false;
+ }
+
+ ValidationMessage = string.Empty;
+ _isValid = true;
+ AddCommand.RaiseCanExecuteChanged();
+ return true;
+ }
+
+ private bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(field, value))
+ {
+ return false;
+ }
+
+ field = value;
+ OnPropertyChanged(propertyName);
+ Validate();
+ return true;
+ }
+
+ private static bool TryParseCustomVideoBitrate(string? raw, out double mbps)
+ {
+ mbps = 0;
+ if (string.IsNullOrWhiteSpace(raw))
+ {
+ return false;
+ }
+
+ return double.TryParse(raw.Trim().Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out mbps)
+ && mbps > 0;
+ }
+
+ private static bool IsYes(string? value) =>
+ value is not null
+ && (value.Equals("Да", StringComparison.OrdinalIgnoreCase)
+ || value.Equals("Yes", StringComparison.OrdinalIgnoreCase)
+ || value.Equals("true", StringComparison.OrdinalIgnoreCase));
+
+ private static string ToYesNo(bool value) => value ? "Да" : "Нет";
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
+
+public sealed class AddFilesSettingsState
+{
+ public string ProfileName { get; init; } = "Emby";
+ public IReadOnlyList ChangedFields { get; init; } = [];
+ public string Container { get; init; } = string.Empty;
+ public string Video { get; init; } = string.Empty;
+ public string PixelFormat { get; init; } = string.Empty;
+ public string Resolution { get; init; } = string.Empty;
+ public string Fps { get; init; } = string.Empty;
+ public string VideoBitrateMode { get; init; } = VideoBitratePolicy.Auto;
+ public string VideoBitrateCustomMbps { get; init; } = string.Empty;
+ public string Audio { get; init; } = string.Empty;
+ public string AudioBitrate { get; init; } = string.Empty;
+ public bool ExternalTracks { get; init; }
+ public bool ExternalSubtitles { get; init; }
+ public bool Fonts { get; init; }
+ public bool RemoveForeignTracks { get; init; }
+ public bool DisableSubtitleDefault { get; init; }
+}
diff --git a/EmbyToolbox/ViewModels/ConversionFormOptions.cs b/EmbyToolbox/ViewModels/ConversionFormOptions.cs
index fcbed94..97c7e87 100644
--- a/EmbyToolbox/ViewModels/ConversionFormOptions.cs
+++ b/EmbyToolbox/ViewModels/ConversionFormOptions.cs
@@ -11,7 +11,8 @@ public sealed class ConversionFormOptions
public List PixelFormatOptions { get; } = ["yuv420p", "yuv420p10le", "yuv422p", "yuv444p"];
public List ResolutionOptions { get; } = ["Без изменений", "Максимум 2160p", "Максимум 1440p", "Максимум 1080p", "Максимум 720p"];
public List FpsOptions { get; } = ["Без изменений", "Максимум 60", "Максимум 30", "Максимум 25", "Максимум 24"];
- public List AudioBitrateKbps { get; } = ["128 kbps", "192 kbps", "256 kbps", "320 kbps"];
+ public List AudioCodecOptions { get; } = ["AAC", "AC3", "EAC3", "Opus", "MP3", "FLAC", "Copy"];
+ public List AudioBitrateKbps { get; } = ["96 kbps", "128 kbps", "160 kbps", "192 kbps", "256 kbps", "320 kbps"];
public List VideoBitrateModeOptions { get; } = VideoBitratePolicy.UiOptions.ToList();
internal void RestoreListsFromSerialized(IReadOnlyList? containers,
diff --git a/EmbyToolbox/ViewModels/ConversionViewModel.cs b/EmbyToolbox/ViewModels/ConversionViewModel.cs
index 95b8525..21953bd 100644
--- a/EmbyToolbox/ViewModels/ConversionViewModel.cs
+++ b/EmbyToolbox/ViewModels/ConversionViewModel.cs
@@ -3,6 +3,7 @@ using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
+using System.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
@@ -44,6 +45,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
private bool _copyPreviousTrackSettings;
private bool _disableSubtitleDefault;
private bool _removeForeignTracksByDefault;
+ private AddFilesSettingsState? _lastAddFilesSettingsState;
private ConversionProfilePresetRow? _selectedDefaultProfile;
private bool _isQueueDropHighlight;
private bool _isExecutionRunning;
@@ -611,7 +613,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
return;
}
- var prof = _profile.GetProfile(item.Profile) ?? ConversionProfileMapping.EmbyFallback;
+ var prof = ResolveEffectiveProfile(item);
TrackOverrideSeeder.SyncTargetFieldsFromProfile(item.TaskOverride, prof);
var plan = _planService.Build(item.MediaAnalysis, item.Sidecars, prof, item.TaskOverride, item.ExternalAudioFiles);
item.SetPlan(plan);
@@ -630,7 +632,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
&& (string.Equals(item.Status, ConversionQueueStatus.Pending, StringComparison.Ordinal)
|| string.Equals(item.Status, ConversionQueueStatus.Ready, StringComparison.Ordinal)))
{
- var prof = _profile.GetProfile(item.Profile) ?? ConversionProfileMapping.EmbyFallback;
+ var prof = ResolveEffectiveProfile(item);
// Keep per-file manual overrides intact; only untouched tasks follow updated profile targets.
if (!item.IsManuallyEdited)
{
@@ -890,7 +892,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
var affected = 0;
foreach (var item in analysis.MajorityItems)
{
- var prof = _profile.GetProfile(item.Profile) ?? ConversionProfileMapping.EmbyFallback;
+ var prof = ResolveEffectiveProfile(item);
var plan = _planService.Build(item.MediaAnalysis!, item.Sidecars, prof, item.TaskOverride, item.ExternalAudioFiles);
item.IsManuallyEdited = true;
item.SetPlan(plan);
@@ -1378,7 +1380,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
t.FfprobeAudioSizeMb,
t.FfprobeAudioSizePartial);
- var prof = _profile.GetProfile(item.Profile) ?? ConversionProfileMapping.EmbyFallback;
+ var prof = ResolveEffectiveProfile(item);
var plan = _planService.Build(item.MediaAnalysis!, sidecars, prof, item.TaskOverride, ext);
item.SetPlan(plan);
item.Status = NormalizeLoadedExecutionStatus(t.Status);
@@ -1556,6 +1558,15 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
}
var profile = string.IsNullOrWhiteSpace(addOptions.Profile) ? "Emby" : addOptions.Profile.Trim();
+ var effectiveSettings = addOptions.EffectiveSettings.Profile.Profile.Length > 0
+ ? addOptions.EffectiveSettings
+ : new EffectiveProfileSettings
+ {
+ SourceProfileName = profile,
+ Profile = _profile.GetProfile(profile) ?? ConversionProfileMapping.EmbyFallback,
+ ChangedFields = []
+ };
+ LogAddFilesEffectiveSettings(effectiveSettings, addOptions);
var added = 0;
var dups = 0;
var newBatch = new List();
@@ -1600,8 +1611,10 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
Status = ConversionQueueStatus.Analyzing,
Progress = 0,
Profile = profile,
+ EffectiveProfileSettings = effectiveSettings,
PlanSummary = "Анализ…"
};
+ TrackOverrideSeeder.SyncTargetFieldsFromProfile(item.TaskOverride, effectiveSettings.Profile);
QueueTasks.Add(item);
newBatch.Add(item);
added++;
@@ -1624,6 +1637,27 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
private string CurrentProfileNameForNewTasks() =>
string.IsNullOrWhiteSpace(_defaultQueueProfile) ? "Emby" : _defaultQueueProfile;
+ private ConversionProfileSettingsEntry ResolveEffectiveProfile(ConversionQueueItem item) =>
+ item.EffectiveProfileSettings?.Profile
+ ?? _profile.GetProfile(item.Profile)
+ ?? ConversionProfileMapping.EmbyFallback;
+
+ private void LogAddFilesEffectiveSettings(EffectiveProfileSettings effectiveSettings, AddFilesOptions addOptions)
+ {
+ var p = effectiveSettings.Profile;
+ var changed = effectiveSettings.ChangedFields.Count == 0
+ ? "none"
+ : string.Join(", ", effectiveSettings.ChangedFields);
+ _logging.Info(
+ "AddFiles effective settings:" + Environment.NewLine
+ + $"Profile={effectiveSettings.SourceProfileName}" + Environment.NewLine
+ + $"Overrides={changed}" + Environment.NewLine
+ + $"Container={p.Container}; Video={p.Video}; PixelFormat={p.PixelFormat}; Resolution={p.Resolution}; FPS={p.Fps}; VideoBitrate={p.VideoBitrateMode}; VideoBitrateMbps={p.VideoBitrateMbps?.ToString("0.###", CultureInfo.InvariantCulture) ?? "-"}" + Environment.NewLine
+ + $"Audio={p.Audio}; AudioBitrate={p.Bitrate}; Subtitles={p.Subtitles}; ExternalAudio={p.ExternalTracks}; ExternalSubtitles={p.ExternalSubtitles}; ExternalFonts={p.Fonts}" + Environment.NewLine
+ + $"ForeignTracks={(addOptions.RemoveForeignAudioAndSubtitles ? "Remove" : "Keep")}; DisableSubtitleDefault={addOptions.DisableSubtitleDefault}",
+ "conversion.queue");
+ }
+
private void RenumberQueue()
{
for (var i = 0; i < QueueTasks.Count; i++)
@@ -1769,8 +1803,10 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
};
AddFilesOptions? selected = null;
- var vm = new AddFilesOptionsViewModel(
+ var vm = new AddFilesSettingsViewModel(
_presetRowsForSetup(),
+ FormOptions,
+ _lastAddFilesSettingsState,
CurrentProfileNameForNewTasks(),
DisableSubtitleDefault,
RemoveForeignTracksByDefault,
@@ -1780,6 +1816,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
_defaultQueueProfile = options.Profile;
DisableSubtitleDefault = options.DisableSubtitleDefault;
RemoveForeignTracksByDefault = options.RemoveForeignAudioAndSubtitles;
+ _lastAddFilesSettingsState = ((AddFilesSettingsViewModel)dialog.DataContext).CaptureState();
SyncDefaultProfileFromList(_presetRowsForSetup());
dialog.DialogResult = true;
dialog.Close();
@@ -2151,7 +2188,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
}
}
- var profile = _profile.GetProfile(item.Profile) ?? ConversionProfileMapping.EmbyFallback;
+ var profile = ResolveEffectiveProfile(item);
var plan = _planService.Build(item.MediaAnalysis, item.Sidecars, profile, item.TaskOverride, item.ExternalAudioFiles);
item.SetPlan(plan);
}
diff --git a/EmbyToolbox/ViewModels/VideoInfoViewModel.cs b/EmbyToolbox/ViewModels/VideoInfoViewModel.cs
index 92d747c..66c0811 100644
--- a/EmbyToolbox/ViewModels/VideoInfoViewModel.cs
+++ b/EmbyToolbox/ViewModels/VideoInfoViewModel.cs
@@ -1,4 +1,3 @@
-using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json;
@@ -23,7 +22,8 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
private string _selectedFilePath = string.Empty;
private string _analysisStateText = string.Empty;
private string _errorMessage = string.Empty;
- private string _rawJson = string.Empty;
+ private string _formattedJson = string.Empty;
+ private IReadOnlyList _formattedJsonLines = Array.Empty();
private string _summaryText = string.Empty;
private bool _isBusy;
private bool _isVideoInfoDropHighlight;
@@ -44,41 +44,16 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
SelectFileCommand = new RelayCommand(ExecuteSelectFile);
SelectSummaryFilesCommand = new RelayCommand(ExecuteSelectSummaryFiles);
- ExpandAllCommand = new RelayCommand(ExecuteExpandAll);
- CollapseAllCommand = new RelayCommand(ExecuteCollapseAll);
- CopyJsonCommand = new RelayCommand(ExecuteCopyJson, () => !string.IsNullOrWhiteSpace(_rawJson));
- SaveJsonCommand = new RelayCommand(ExecuteSaveJson, () => !string.IsNullOrWhiteSpace(_rawJson));
CopySummaryCommand = new RelayCommand(ExecuteCopySummary, () => !string.IsNullOrWhiteSpace(SummaryText) && SummaryText != "Файл не проанализирован");
SaveSummaryCommand = new RelayCommand(ExecuteSaveSummary, () => !string.IsNullOrWhiteSpace(SummaryText) && SummaryText != "Файл не проанализирован");
- CopyNodeValueCommand = new RelayCommand(ExecuteCopyNodeValue, CanCopyNodeValue);
- CopyNodeLineCommand = new RelayCommand(ExecuteCopyNodeLine, CanCopyNodeLine);
- CopyNodeWithChildrenCommand = new RelayCommand(ExecuteCopyNodeWithChildren, CanCopyNodeWithChildren);
- CopyNodePathCommand = new RelayCommand(ExecuteCopyNodePath, CanCopyNodePath);
}
- public ObservableCollection JsonNodes { get; } = new();
-
public ICommand SelectFileCommand { get; }
public ICommand SelectSummaryFilesCommand { get; }
- public ICommand ExpandAllCommand { get; }
-
- public ICommand CollapseAllCommand { get; }
-
- public ICommand CopyJsonCommand { get; }
-
- public ICommand SaveJsonCommand { get; }
public ICommand CopySummaryCommand { get; }
public ICommand SaveSummaryCommand { get; }
- public ICommand CopyNodeValueCommand { get; }
-
- public ICommand CopyNodeLineCommand { get; }
-
- public ICommand CopyNodeWithChildrenCommand { get; }
-
- public ICommand CopyNodePathCommand { get; }
-
public string SelectedFilePath
{
get => _selectedFilePath;
@@ -124,6 +99,37 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
}
}
+ public string FormattedJson
+ {
+ get => _formattedJson;
+ private set
+ {
+ if (_formattedJson == value)
+ {
+ return;
+ }
+
+ _formattedJson = value;
+ FormattedJsonLines = SplitTextLines(value);
+ OnPropertyChanged();
+ }
+ }
+
+ public IReadOnlyList FormattedJsonLines
+ {
+ get => _formattedJsonLines;
+ private set
+ {
+ if (ReferenceEquals(_formattedJsonLines, value))
+ {
+ return;
+ }
+
+ _formattedJsonLines = value;
+ OnPropertyChanged();
+ }
+ }
+
public bool IsBusy
{
get => _isBusy;
@@ -307,9 +313,8 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
private async Task AnalyzeAsync()
{
- JsonNodes.Clear();
ErrorMessage = string.Empty;
- _rawJson = string.Empty;
+ FormattedJson = string.Empty;
RaiseCommandStates();
if (string.IsNullOrWhiteSpace(SelectedFilePath))
@@ -335,8 +340,7 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
try
{
- _rawJson = result.Json;
- BuildTree(_rawJson);
+ FormattedJson = FormatJsonOrFallback(result.Json);
AnalysisStateText = "Готово";
RaiseCommandStates();
_logging.Info($"ffprobe завершен: {Path.GetFileName(SelectedFilePath)}", "video-info.ffprobe", command: result.Command, stdout: result.StdOut, stderr: result.StdErr);
@@ -427,125 +431,6 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
RaiseCommandStates();
}
- private void BuildTree(string json)
- {
- using var doc = JsonDocument.Parse(json);
- JsonNodes.Clear();
-
- if (doc.RootElement.ValueKind == JsonValueKind.Object)
- {
- foreach (var property in doc.RootElement.EnumerateObject())
- {
- JsonNodes.Add(CreateNode(property.Name, property.Value, null));
- }
- }
- else
- {
- JsonNodes.Add(CreateNode("root", doc.RootElement, null));
- }
- }
-
- private static JsonTreeNodeViewModel CreateNode(string name, JsonElement element, JsonTreeNodeViewModel? parent)
- {
- var node = new JsonTreeNodeViewModel(name, GetPreviewValue(element), element.GetRawText(), parent);
-
- switch (element.ValueKind)
- {
- case JsonValueKind.Object:
- foreach (var prop in element.EnumerateObject())
- {
- node.Children.Add(CreateNode(prop.Name, prop.Value, node));
- }
- break;
- case JsonValueKind.Array:
- var index = 0;
- foreach (var item in element.EnumerateArray())
- {
- node.Children.Add(CreateNode($"[{index}]", item, node));
- index++;
- }
- break;
- }
-
- return node;
- }
-
- private static string GetPreviewValue(JsonElement element)
- {
- return element.ValueKind switch
- {
- JsonValueKind.Object => "{...}",
- JsonValueKind.Array => "[...]",
- JsonValueKind.String => element.GetString() ?? string.Empty,
- JsonValueKind.Number => element.GetRawText(),
- JsonValueKind.True => "true",
- JsonValueKind.False => "false",
- JsonValueKind.Null => "null",
- _ => element.GetRawText()
- };
- }
-
- private void ExecuteExpandAll()
- {
- SetExpandedState(JsonNodes, true);
- }
-
- private void ExecuteCollapseAll()
- {
- SetExpandedState(JsonNodes, false);
- }
-
- private static void SetExpandedState(IEnumerable nodes, bool isExpanded)
- {
- foreach (var node in nodes)
- {
- node.IsExpanded = isExpanded;
- SetExpandedState(node.Children, isExpanded);
- }
- }
-
- private void ExecuteCopyJson()
- {
- if (string.IsNullOrWhiteSpace(_rawJson))
- {
- return;
- }
-
- Clipboard.SetText(_rawJson);
- _logging.Info("JSON скопирован в буфер обмена", "video-info.copy");
- }
-
- private void ExecuteSaveJson()
- {
- if (string.IsNullOrWhiteSpace(_rawJson))
- {
- return;
- }
-
- var defaultName = string.IsNullOrWhiteSpace(SelectedFilePath)
- ? "ffprobe.json"
- : $"{Path.GetFileNameWithoutExtension(SelectedFilePath)}.ffprobe.json";
-
- var dialog = new SaveFileDialog
- {
- Title = "Сохранить JSON ffprobe",
- Filter = "JSON (*.json)|*.json|Все файлы|*.*",
- FileName = defaultName,
- InitialDirectory = _recentPaths.GetInitialDirectory(RecentPathScenario.SettingsOutputFolder),
- };
-
- if (dialog.ShowDialog() != true)
- {
- return;
- }
-
- File.WriteAllText(dialog.FileName, _rawJson);
- _recentPaths.RememberChosenFolder(
- RecentPathScenario.SettingsOutputFolder,
- Path.GetDirectoryName(dialog.FileName) ?? dialog.FileName);
- _logging.Info($"JSON сохранен: {dialog.FileName}", "video-info.save");
- }
-
private void ExecuteCopySummary()
{
if (string.IsNullOrWhiteSpace(SummaryText) || SummaryText == "Файл не проанализирован")
@@ -594,105 +479,8 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
private void RaiseCommandStates()
{
- (CopyJsonCommand as RelayCommand)?.RaiseCanExecuteChanged();
- (SaveJsonCommand as RelayCommand)?.RaiseCanExecuteChanged();
(CopySummaryCommand as RelayCommand)?.RaiseCanExecuteChanged();
(SaveSummaryCommand as RelayCommand)?.RaiseCanExecuteChanged();
- (CopyNodeValueCommand as RelayCommand)?.RaiseCanExecuteChanged();
- (CopyNodeLineCommand as RelayCommand)?.RaiseCanExecuteChanged();
- (CopyNodeWithChildrenCommand as RelayCommand)?.RaiseCanExecuteChanged();
- (CopyNodePathCommand as RelayCommand)?.RaiseCanExecuteChanged();
- }
-
- private static JsonTreeNodeViewModel? AsNode(object? parameter)
- {
- return parameter as JsonTreeNodeViewModel;
- }
-
- private bool CanCopyNodeValue(object? parameter)
- {
- var node = AsNode(parameter);
- return node is not null && node.Children.Count == 0;
- }
-
- private bool CanCopyNodeLine(object? parameter)
- {
- return AsNode(parameter) is not null;
- }
-
- private bool CanCopyNodeWithChildren(object? parameter)
- {
- return AsNode(parameter) is not null;
- }
-
- private bool CanCopyNodePath(object? parameter)
- {
- return AsNode(parameter) is not null;
- }
-
- private void ExecuteCopyNodeValue(object? parameter)
- {
- var node = AsNode(parameter);
- if (node is null || node.Children.Count > 0)
- {
- return;
- }
-
- Clipboard.SetText(node.Value);
- _logging.Info("узел JSON скопирован", "video-info.copy");
- }
-
- private void ExecuteCopyNodeLine(object? parameter)
- {
- var node = AsNode(parameter);
- if (node is null)
- {
- return;
- }
-
- Clipboard.SetText($"{node.Name}: {node.Value}");
- _logging.Info("узел JSON скопирован", "video-info.copy");
- }
-
- private void ExecuteCopyNodeWithChildren(object? parameter)
- {
- var node = AsNode(parameter);
- if (node is null)
- {
- return;
- }
-
- var formatted = FormatJsonOrFallback(node.SubtreeJson);
- Clipboard.SetText(formatted);
- _logging.Info("узел JSON скопирован", "video-info.copy");
- }
-
- private void ExecuteCopyNodePath(object? parameter)
- {
- var node = AsNode(parameter);
- if (node is null)
- {
- return;
- }
-
- Clipboard.SetText(BuildNodePath(node));
- _logging.Info("узел JSON скопирован", "video-info.copy");
- }
-
- private static string BuildNodePath(JsonTreeNodeViewModel node)
- {
- if (node.Parent is null)
- {
- return node.Name;
- }
-
- var parentPath = BuildNodePath(node.Parent);
- if (node.Name.StartsWith("[", StringComparison.Ordinal))
- {
- return $"{parentPath}{node.Name}";
- }
-
- return string.IsNullOrWhiteSpace(parentPath) ? node.Name : $"{parentPath}.{node.Name}";
}
private static string FormatJsonOrFallback(string json)
@@ -707,5 +495,15 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
return json;
}
}
+
+ private static IReadOnlyList SplitTextLines(string text)
+ {
+ if (string.IsNullOrEmpty(text))
+ {
+ return Array.Empty();
+ }
+
+ return text.ReplaceLineEndings("\n").Split('\n');
+ }
}
diff --git a/EmbyToolbox/Views/AddFilesOptionsDialog.xaml b/EmbyToolbox/Views/AddFilesOptionsDialog.xaml
index b095dbd..54083cf 100644
--- a/EmbyToolbox/Views/AddFilesOptionsDialog.xaml
+++ b/EmbyToolbox/Views/AddFilesOptionsDialog.xaml
@@ -1,96 +1,178 @@
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
-
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
-
-
-
-
-
-
+ Visibility="{Binding HasValidationMessage, Converter={StaticResource BoolToVis}}" />
+
+
+
+
-
-
-
-
-