Compare commits

...

2 Commits

Author SHA1 Message Date
Emby Toolbox
7f3c2ca999 Remove subtitle profile toggle from UI 2026-05-16 20:35:52 +05:00
Emby Toolbox
2e6c26178f Add effective profile editor for queued files 2026-05-16 20:28:49 +05:00
13 changed files with 650 additions and 188 deletions

View File

@ -973,19 +973,6 @@
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Субтитры" Width="95">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Subtitles}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding DataContext.ConversionYesNoOptions, ElementName=RootWindow}"
SelectedItem="{Binding Subtitles, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Внешние дорожки" Width="125">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>

View File

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

View File

@ -24,6 +24,7 @@ public sealed class ConversionQueueItem : INotifyPropertyChanged
private MediaAnalysisResult? _mediaAnalysis;
private IReadOnlyList<SidecarFile> _sidecars = System.Array.Empty<SidecarFile>();
private IReadOnlyList<ExternalAudioFile> _externalAudioFiles = System.Array.Empty<ExternalAudioFile>();
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();
}
}

View File

@ -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<string> ChangedFields { get; init; } = [];
public bool HasOverrides => ChangedFields.Count > 0;
}

View File

@ -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)}");

View File

@ -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<string>();
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<string> changed, string name, string? source, string? value)
{
if (!string.Equals(source?.Trim(), value?.Trim(), StringComparison.Ordinal))
{
changed.Add(name);
}
}
}

View File

@ -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))

View File

@ -15,6 +15,7 @@ public static class VideoBitratePolicy
Auto,
Source,
"2 Mbps",
"3 Mbps",
"4 Mbps",
"6 Mbps",
"8 Mbps",

View File

@ -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<AddFilesOptions> _onAdd;
private readonly Action _onCancel;
private bool _disableSubtitleDefault;
private bool _removeForeignAudioAndSubtitles;
private ConversionProfilePresetRow? _selectedProfile;
public AddFilesOptionsViewModel(
IReadOnlyList<ConversionProfilePresetRow> profiles,
string selectedProfileName,
bool disableSubtitleDefault,
bool removeForeignAudioAndSubtitles,
Action<AddFilesOptions> onAdd,
Action onCancel)
{
_onAdd = onAdd;
_onCancel = onCancel;
_disableSubtitleDefault = disableSubtitleDefault;
_removeForeignAudioAndSubtitles = removeForeignAudioAndSubtitles;
Profiles = new ObservableCollection<ConversionProfilePresetRow>(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<ConversionProfilePresetRow> 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));
}

View File

@ -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<ConversionProfilePresetRow> _profiles;
private readonly Action<AddFilesOptions> _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<ConversionProfilePresetRow> profiles,
ConversionFormOptions formOptions,
AddFilesSettingsState? previousState,
string fallbackProfileName,
bool fallbackDisableSubtitleDefault,
bool fallbackRemoveForeignTracks,
Action<AddFilesOptions> onAdd,
Action onCancel)
{
_profiles = profiles;
_onAdd = onAdd;
_onCancel = onCancel;
Profiles = new ObservableCollection<ConversionProfilePresetRow>(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<ConversionProfilePresetRow> Profiles { get; }
public IReadOnlyList<string> ContainerOptions { get; }
public IReadOnlyList<string> VideoCodecOptions { get; }
public IReadOnlyList<string> PixelFormatOptions { get; }
public IReadOnlyList<string> ResolutionOptions { get; }
public IReadOnlyList<string> FpsOptions { get; }
public IReadOnlyList<string> AudioCodecOptions { get; }
public IReadOnlyList<string> AudioBitrateOptions { get; }
public IReadOnlyList<string> VideoBitrateOptions { get; }
public IReadOnlyList<string> 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<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.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<string> 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; }
}

View File

@ -11,7 +11,8 @@ public sealed class ConversionFormOptions
public List<string> PixelFormatOptions { get; } = ["yuv420p", "yuv420p10le", "yuv422p", "yuv444p"];
public List<string> ResolutionOptions { get; } = ["Без изменений", "Максимум 2160p", "Максимум 1440p", "Максимум 1080p", "Максимум 720p"];
public List<string> FpsOptions { get; } = ["Без изменений", "Максимум 60", "Максимум 30", "Максимум 25", "Максимум 24"];
public List<string> AudioBitrateKbps { get; } = ["128 kbps", "192 kbps", "256 kbps", "320 kbps"];
public List<string> AudioCodecOptions { get; } = ["AAC", "AC3", "EAC3", "Opus", "MP3", "FLAC", "Copy"];
public List<string> AudioBitrateKbps { get; } = ["96 kbps", "128 kbps", "160 kbps", "192 kbps", "256 kbps", "320 kbps"];
public List<string> VideoBitrateModeOptions { get; } = VideoBitratePolicy.UiOptions.ToList();
internal void RestoreListsFromSerialized(IReadOnlyList<string>? containers,

View File

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

View File

@ -1,96 +1,181 @@
<Window x:Class="EmbyToolbox.Views.AddFilesOptionsDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:EmbyToolbox.Converters"
Title="Параметры добавления"
Width="520"
Height="230"
ResizeMode="NoResize"
Width="620"
Height="430"
MinWidth="600"
MinHeight="390"
ResizeMode="CanResize"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource Ui.Brush.Surface}">
<Grid Margin="14">
<Window.Resources>
<converters:BooleanToVisibilityConverter x:Key="BoolToVis" />
<Style x:Key="AddSettingsLabel" TargetType="TextBlock" BasedOn="{StaticResource UiTextCaption}">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="0,0,6,3" />
</Style>
<Style x:Key="AddSettingsCombo" TargetType="ComboBox" BasedOn="{StaticResource UiCombo}">
<Setter Property="Height" Value="26" />
<Setter Property="MinHeight" Value="26" />
<Setter Property="Margin" Value="0,0,8,6" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style x:Key="AddSettingsTextBox" TargetType="TextBox" BasedOn="{StaticResource UiTextInput}">
<Setter Property="Height" Value="26" />
<Setter Property="MinHeight" Value="26" />
<Setter Property="Margin" Value="0,0,8,6" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style x:Key="AddSettingsGroup" TargetType="GroupBox">
<Setter Property="Padding" Value="6,4,6,4" />
<Setter Property="Margin" Value="0,0,0,6" />
<Setter Property="BorderBrush" Value="{DynamicResource Ui.Brush.BorderSubtle}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FontSize" Value="11" />
</Style>
</Window.Resources>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="16" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid Grid.Row="0" Margin="0,0,0,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="220" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="Профиль"
Style="{StaticResource UiTextCaption}"
VerticalAlignment="Center"
Margin="0,0,12,0" />
<TextBlock Grid.Column="0" Text="Профиль:" Style="{StaticResource AddSettingsLabel}" Margin="0,0,8,0" />
<ComboBox Grid.Column="1"
Height="32"
Style="{StaticResource UiCombo}"
Style="{StaticResource AddSettingsCombo}"
Margin="0"
ItemsSource="{Binding Profiles}"
SelectedItem="{Binding SelectedProfile, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Profile, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
<TextBlock Text="{Binding Profile}" TextTrimming="CharacterEllipsis" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
<Grid Grid.Row="2"
Height="32"
VerticalAlignment="Top">
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel>
<GroupBox Header="Видео" Style="{StaticResource AddSettingsGroup}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Контейнер" Style="{StaticResource AddSettingsLabel}" />
<TextBlock Grid.Row="0" Grid.Column="1" Text="Видео codec" Style="{StaticResource AddSettingsLabel}" />
<TextBlock Grid.Row="0" Grid.Column="2" Text="Pixel format" Style="{StaticResource AddSettingsLabel}" />
<ComboBox Grid.Row="1" Grid.Column="0" Style="{StaticResource AddSettingsCombo}" ItemsSource="{Binding ContainerOptions}" SelectedItem="{Binding Container, Mode=TwoWay}" />
<ComboBox Grid.Row="1" Grid.Column="1" Style="{StaticResource AddSettingsCombo}" ItemsSource="{Binding VideoCodecOptions}" SelectedItem="{Binding Video, Mode=TwoWay}" />
<ComboBox Grid.Row="1" Grid.Column="2" Style="{StaticResource AddSettingsCombo}" ItemsSource="{Binding PixelFormatOptions}" SelectedItem="{Binding PixelFormat, Mode=TwoWay}" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="Resolution" Style="{StaticResource AddSettingsLabel}" />
<TextBlock Grid.Row="2" Grid.Column="1" Text="FPS" Style="{StaticResource AddSettingsLabel}" />
<TextBlock Grid.Row="2" Grid.Column="2" Text="Видеобитрейт" Style="{StaticResource AddSettingsLabel}" />
<ComboBox Grid.Row="3" Grid.Column="0" Style="{StaticResource AddSettingsCombo}" ItemsSource="{Binding ResolutionOptions}" SelectedItem="{Binding Resolution, Mode=TwoWay}" />
<ComboBox Grid.Row="3" Grid.Column="1" Style="{StaticResource AddSettingsCombo}" ItemsSource="{Binding FpsOptions}" SelectedItem="{Binding Fps, Mode=TwoWay}" />
<Grid Grid.Row="3" Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ComboBox Grid.Row="0" Style="{StaticResource AddSettingsCombo}" ItemsSource="{Binding VideoBitrateOptions}" SelectedItem="{Binding VideoBitrateMode, Mode=TwoWay}" />
<Grid Grid.Row="1" Visibility="{Binding IsVideoBitrateCustomVisible, Converter={StaticResource BoolToVis}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="90" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="Видеобитрейт (Mbps)"
Style="{StaticResource AddSettingsLabel}" />
<TextBox Grid.Column="1"
Style="{StaticResource AddSettingsTextBox}"
Text="{Binding VideoBitrateCustomMbps, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</Grid>
</Grid>
</GroupBox>
<GroupBox Header="Аудио" Style="{StaticResource AddSettingsGroup}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Audio codec" Style="{StaticResource AddSettingsLabel}" />
<TextBlock Grid.Row="0" Grid.Column="1" Text="Audio bitrate" Style="{StaticResource AddSettingsLabel}" />
<ComboBox Grid.Row="1" Grid.Column="0" Style="{StaticResource AddSettingsCombo}" ItemsSource="{Binding AudioCodecOptions}" SelectedItem="{Binding Audio, Mode=TwoWay}" />
<ComboBox Grid.Row="1" Grid.Column="1" Style="{StaticResource AddSettingsCombo}" ItemsSource="{Binding AudioBitrateOptions}" SelectedItem="{Binding AudioBitrate, Mode=TwoWay}" />
</Grid>
</GroupBox>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="8" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<GroupBox Grid.Column="0" Header="Внешние дорожки" Style="{StaticResource AddSettingsGroup}">
<WrapPanel>
<CheckBox Content="Внешние аудио" IsChecked="{Binding ExternalTracks, Mode=TwoWay}" Margin="0,0,14,4" />
<CheckBox Content="Внешние субтитры" IsChecked="{Binding ExternalSubtitles, Mode=TwoWay}" Margin="0,0,14,4" />
<CheckBox Content="Внешние шрифты" IsChecked="{Binding Fonts, Mode=TwoWay}" Margin="0,0,14,4" />
</WrapPanel>
</GroupBox>
<GroupBox Grid.Column="2" Header="Прочие настройки" Style="{StaticResource AddSettingsGroup}">
<WrapPanel>
<CheckBox Content="Удалять иностранные дорожки" IsChecked="{Binding RemoveForeignTracks, Mode=TwoWay}" Margin="0,0,14,4" />
<CheckBox Content="Отключать субтитры" IsChecked="{Binding DisableSubtitleDefault, Mode=TwoWay}" Margin="0,0,14,4" />
</WrapPanel>
</GroupBox>
</Grid>
</StackPanel>
</ScrollViewer>
<Grid Grid.Row="2" Margin="0,8,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="18" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="24" />
<ColumnDefinition Width="18" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0"
Width="18"
Height="18"
Margin="0"
VerticalAlignment="Center"
IsChecked="{Binding DisableSubtitleDefault, Mode=TwoWay}" />
<TextBlock Grid.Column="1"
Text="Отключать субтитры"
<TextBlock Grid.Column="0"
Text="{Binding ValidationMessage}"
Foreground="{DynamicResource Ui.Brush.ErrorText}"
VerticalAlignment="Center"
Margin="10,0,0,0"
FontSize="12" />
<CheckBox Grid.Column="3"
Width="18"
Height="18"
Margin="0"
VerticalAlignment="Center"
IsChecked="{Binding RemoveForeignAudioAndSubtitles, Mode=TwoWay}" />
<TextBlock Grid.Column="4"
Text="Удалять иностранные дорожки"
VerticalAlignment="Center"
Margin="10,0,0,0"
FontSize="12" />
Visibility="{Binding HasValidationMessage, Converter={StaticResource BoolToVis}}" />
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button MinWidth="96"
Margin="0,0,8,0"
Style="{StaticResource UiButtonPrimary}"
Foreground="White"
Content="Добавить"
IsDefault="True"
Command="{Binding AddCommand}" />
<Button MinWidth="96"
Style="{StaticResource UiButtonSecondary}"
Content="Отмена"
IsCancel="True"
Command="{Binding CancelCommand}" />
</StackPanel>
</Grid>
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
<Button MinWidth="100"
Margin="0,0,8,0"
Style="{StaticResource UiButtonPrimary}"
Foreground="White"
Content="Добавить"
IsDefault="True"
Command="{Binding AddCommand}" />
<Button MinWidth="100"
Style="{StaticResource UiButtonSecondary}"
Content="Отмена"
IsCancel="True"
Command="{Binding CancelCommand}" />
</StackPanel>
</Grid>
</Window>