From bf436d092684711759538950cb43f8b2dfa97d7a Mon Sep 17 00:00:00 2001 From: Alberto Balbo Date: Sun, 7 Sep 2025 22:42:36 +0200 Subject: [PATCH] Aggiunta gestione modelli di denominazione file video MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Modificato `MainWindow.xaml` per includere il binding del modello di denominazione. - Introdotta l'enumerazione `NamingPattern` in `VideoJob.cs`. - Aggiunti campi e proprietΓ  per gestire il modello di denominazione e il prefisso personalizzato. - Aggiornate le impostazioni predefinite per `FrameSize` e aggiunte nuove impostazioni per `DefaultNamingPattern` e `DefaultCustomPrefix`. - Aggiornato `VideoProcessingService.cs` per utilizzare il modello di denominazione e il prefisso personalizzato. - Modificato `JobConfigWindow.xaml` per aggiungere controlli per le impostazioni di denominazione. - Aggiunti controlli per le impostazioni di denominazione predefinite in `SettingsWindow.xaml`. - Creata la classe `NamingHelper` per la generazione dei nomi dei file. --- Ganimede/Ganimede/Helpers/NamingHelper.cs | 62 +++++++++++ Ganimede/Ganimede/MainWindow.xaml | 3 +- Ganimede/Ganimede/Models/VideoJob.cs | 52 ++++++++- .../Ganimede/Properties/Settings.Designer.cs | 26 ++++- .../Ganimede/Properties/Settings.settings | 8 +- .../Services/VideoProcessingService.cs | 103 ++++++++++++++---- .../Ganimede/Windows/JobConfigWindow.xaml | 47 ++++++-- .../Ganimede/Windows/JobConfigWindow.xaml.cs | 75 +++++++++++++ Ganimede/Ganimede/Windows/SettingsWindow.xaml | 49 +++++++-- .../Ganimede/Windows/SettingsWindow.xaml.cs | 19 ++++ 10 files changed, 402 insertions(+), 42 deletions(-) create mode 100644 Ganimede/Ganimede/Helpers/NamingHelper.cs diff --git a/Ganimede/Ganimede/Helpers/NamingHelper.cs b/Ganimede/Ganimede/Helpers/NamingHelper.cs new file mode 100644 index 0000000..d300f4c --- /dev/null +++ b/Ganimede/Ganimede/Helpers/NamingHelper.cs @@ -0,0 +1,62 @@ +using System; +using System.IO; +using System.ComponentModel; +using System.Reflection; +using Ganimede.Models; + +namespace Ganimede.Helpers +{ + public static class NamingHelper + { + public static string GenerateFileName(NamingPattern pattern, VideoJob job, int frameIndex, TimeSpan frameTime, string customPrefix = "") + { + var videoName = Path.GetFileNameWithoutExtension(job.VideoPath); + var progressiveNumber = frameIndex + 1; // Start from 1 instead of 0 + + return pattern switch + { + NamingPattern.VideoNameProgressive => + $"{videoName}_{progressiveNumber:D6}.png", + + NamingPattern.FrameProgressive => + $"frame_{progressiveNumber:D6}.png", + + NamingPattern.VideoNameTimestamp => + $"{videoName}_{(int)frameTime.TotalMilliseconds:D6}ms.png", + + NamingPattern.CustomProgressive => + $"{(string.IsNullOrEmpty(customPrefix) ? "custom" : customPrefix)}_{progressiveNumber:D6}.png", + + NamingPattern.TimestampOnly => + $"{(int)frameTime.Hours:D2}h{frameTime.Minutes:D2}m{frameTime.Seconds:D2}s{frameTime.Milliseconds:D3}ms.png", + + NamingPattern.VideoNameFrameProgressive => + $"{videoName}_frame_{progressiveNumber:D6}.png", + + _ => $"frame_{progressiveNumber:D6}.png" + }; + } + + public static string GetPatternDescription(NamingPattern pattern) + { + var field = pattern.GetType().GetField(pattern.ToString()); + var attribute = field?.GetCustomAttribute(); + return attribute?.Description ?? pattern.ToString(); + } + + public static string GetPatternExample(NamingPattern pattern, string videoName = "VID20250725", string customPrefix = "custom") + { + var job = new VideoJob + { + VideoPath = $"{videoName}.mp4", + OutputFolder = "", + CustomPrefix = customPrefix + }; + + var frameTime = new TimeSpan(0, 12, 34, 567); // 12:34.567 + var example = GenerateFileName(pattern, job, 0, frameTime, customPrefix); // frameIndex 0 will become 1 + + return example; + } + } +} \ No newline at end of file diff --git a/Ganimede/Ganimede/MainWindow.xaml b/Ganimede/Ganimede/MainWindow.xaml index b712776..4e0113f 100644 --- a/Ganimede/Ganimede/MainWindow.xaml +++ b/Ganimede/Ganimede/MainWindow.xaml @@ -141,10 +141,11 @@ - + + diff --git a/Ganimede/Ganimede/Models/VideoJob.cs b/Ganimede/Ganimede/Models/VideoJob.cs index 406c8c8..9cddb8b 100644 --- a/Ganimede/Ganimede/Models/VideoJob.cs +++ b/Ganimede/Ganimede/Models/VideoJob.cs @@ -19,6 +19,27 @@ namespace Ganimede.Models Overwrite } + public enum NamingPattern + { + [Description("VideoName + Progressive (VID20250725_000001)")] + VideoNameProgressive, + + [Description("Frame + Progressive (frame_000001)")] + FrameProgressive, + + [Description("VideoName + Timestamp (VID20250725_001234ms)")] + VideoNameTimestamp, + + [Description("Custom Prefix + Progressive (custom_000001)")] + CustomProgressive, + + [Description("Timestamp Only (00h12m34s567ms)")] + TimestampOnly, + + [Description("VideoName + Frame + Progressive (VID20250725_frame_000001)")] + VideoNameFrameProgressive + } + public class VideoJob : INotifyPropertyChanged { private JobStatus _status = JobStatus.Pending; @@ -28,6 +49,8 @@ namespace Ganimede.Models private string _customFrameSize = string.Empty; private OverwriteMode? _customOverwriteMode = null; private bool _customCreateSubfolder = true; + private NamingPattern? _customNamingPattern = null; + private string _customPrefix = string.Empty; public required string VideoPath { get; set; } public string VideoName => System.IO.Path.GetFileNameWithoutExtension(VideoPath); @@ -109,16 +132,43 @@ namespace Ganimede.Models } } + public NamingPattern? CustomNamingPattern + { + get => _customNamingPattern; + set + { + _customNamingPattern = value; + OnPropertyChanged(nameof(CustomNamingPattern)); + OnPropertyChanged(nameof(NamingPatternDisplay)); + } + } + + public string CustomPrefix + { + get => _customPrefix; + set + { + _customPrefix = value; + OnPropertyChanged(nameof(CustomPrefix)); + OnPropertyChanged(nameof(NamingPatternDisplay)); + } + } + // Display properties for UI public string OutputFolderDisplay => string.IsNullOrEmpty(CustomOutputFolder) ? "Default" : System.IO.Path.GetFileName(CustomOutputFolder) + (CustomCreateSubfolder ? "/??" : ""); public string FrameSizeDisplay => - string.IsNullOrEmpty(CustomFrameSize) ? "Default" : CustomFrameSize; + string.IsNullOrEmpty(CustomFrameSize) ? "Default" : + (CustomFrameSize == "original" ? "Original" : CustomFrameSize); public string OverwriteModeDisplay => CustomOverwriteMode?.ToString() ?? "Default"; + public string NamingPatternDisplay => + CustomNamingPattern?.ToString() ?? "Default" + + (!string.IsNullOrEmpty(CustomPrefix) ? $" ({CustomPrefix}_)" : ""); + public event PropertyChangedEventHandler? PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) diff --git a/Ganimede/Ganimede/Properties/Settings.Designer.cs b/Ganimede/Ganimede/Properties/Settings.Designer.cs index 3c8a691..497ea90 100644 --- a/Ganimede/Ganimede/Properties/Settings.Designer.cs +++ b/Ganimede/Ganimede/Properties/Settings.Designer.cs @@ -73,7 +73,7 @@ namespace Ganimede.Properties { [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("320,180")] + [global::System.Configuration.DefaultSettingValueAttribute("original")] public string FrameSize { get { return ((string)(this["FrameSize"])); @@ -94,5 +94,29 @@ namespace Ganimede.Properties { this["DefaultOverwriteMode"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("VideoNameProgressive")] + public string DefaultNamingPattern { + get { + return ((string)(this["DefaultNamingPattern"])); + } + set { + this["DefaultNamingPattern"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("custom")] + public string DefaultCustomPrefix { + get { + return ((string)(this["DefaultCustomPrefix"])); + } + set { + this["DefaultCustomPrefix"] = value; + } + } } } diff --git a/Ganimede/Ganimede/Properties/Settings.settings b/Ganimede/Ganimede/Properties/Settings.settings index 93d205c..43e163b 100644 --- a/Ganimede/Ganimede/Properties/Settings.settings +++ b/Ganimede/Ganimede/Properties/Settings.settings @@ -15,10 +15,16 @@ True - 320,180 + original Ask + + VideoNameProgressive + + + custom + \ No newline at end of file diff --git a/Ganimede/Ganimede/Services/VideoProcessingService.cs b/Ganimede/Ganimede/Services/VideoProcessingService.cs index f8694fd..3b114aa 100644 --- a/Ganimede/Ganimede/Services/VideoProcessingService.cs +++ b/Ganimede/Ganimede/Services/VideoProcessingService.cs @@ -7,6 +7,7 @@ using FFMpegCore; using System.IO; using Ganimede.Models; using Ganimede.Properties; +using Ganimede.Helpers; namespace Ganimede.Services { @@ -144,9 +145,11 @@ namespace Ganimede.Services var frameSize = GetFrameSize(job); var overwriteMode = GetOverwriteMode(job); + var namingPattern = GetNamingPattern(job); + var customPrefix = GetCustomPrefix(job); - // Check for existing files if needed - var existingFiles = Directory.GetFiles(job.OutputFolder, "frame_*.png"); + // Check for existing files if needed (using naming pattern) + var existingFiles = Directory.GetFiles(job.OutputFolder, "*.png"); if (existingFiles.Length > 0 && overwriteMode == OverwriteMode.Ask) { var dialogResult = System.Windows.Application.Current.Dispatcher.Invoke(() => @@ -176,7 +179,9 @@ namespace Ganimede.Services return; } - string framePath = Path.Combine(job.OutputFolder, $"frame_{i:D6}.png"); + var frameTime = TimeSpan.FromSeconds((double)i / frameRate); + var fileName = NamingHelper.GenerateFileName(namingPattern, job, i, frameTime, customPrefix); + string framePath = Path.Combine(job.OutputFolder, fileName); // Check if file exists and handle according to overwrite mode if (File.Exists(framePath) && overwriteMode == OverwriteMode.Skip) @@ -228,14 +233,14 @@ namespace Ganimede.Services // Use job-specific frame size if set, otherwise use default setting var frameSize = !string.IsNullOrEmpty(job.CustomFrameSize) ? job.CustomFrameSize : Settings.Default.FrameSize; - if (string.IsNullOrEmpty(frameSize)) - return (320, 180); + if (string.IsNullOrEmpty(frameSize) || frameSize == "original") + return (-1, -1); // Special value indicating original size var parts = frameSize.Split(','); if (parts.Length == 2 && int.TryParse(parts[0], out int width) && int.TryParse(parts[1], out int height)) return (width, height); - return (320, 180); + return (-1, -1); // Default to original size if parsing fails } private OverwriteMode GetOverwriteMode(VideoJob job) @@ -251,27 +256,77 @@ namespace Ganimede.Services return OverwriteMode.Ask; } + private NamingPattern GetNamingPattern(VideoJob job) + { + // Use job-specific naming pattern if set, otherwise use default setting + if (job.CustomNamingPattern.HasValue) + return job.CustomNamingPattern.Value; + + var defaultPattern = Settings.Default.DefaultNamingPattern; + if (Enum.TryParse(defaultPattern, out var pattern)) + return pattern; + + return NamingPattern.VideoNameProgressive; + } + + private string GetCustomPrefix(VideoJob job) + { + // Use job-specific custom prefix if set, otherwise use default setting + if (!string.IsNullOrEmpty(job.CustomPrefix)) + return job.CustomPrefix; + + return Settings.Default.DefaultCustomPrefix ?? "custom"; + } + private async Task ExtractFrameAsync(VideoJob job, int frameIndex, int frameRate, (int width, int height) frameSize, string framePath) { var frameTime = TimeSpan.FromSeconds((double)frameIndex / frameRate); try { - // Try with PNG codec first - await FFMpegArguments - .FromFileInput(job.VideoPath) - .OutputToFile(framePath, true, options => options - .Seek(frameTime) - .WithFrameOutputCount(1) - .WithVideoCodec("png") - .Resize(frameSize.width, frameSize.height)) - .ProcessAsynchronously(); - } - catch - { - // Fallback without codec specification + // Check if we should use original size + if (frameSize.width == -1 && frameSize.height == -1) + { + // Extract frame with original video size (no resize) + try + { + await FFMpegArguments + .FromFileInput(job.VideoPath) + .OutputToFile(framePath, true, options => options + .Seek(frameTime) + .WithFrameOutputCount(1) + .WithVideoCodec("png")) + .ProcessAsynchronously(); + return; + } + catch + { + // Fallback without codec specification + await FFMpegArguments + .FromFileInput(job.VideoPath) + .OutputToFile(framePath, true, options => options + .Seek(frameTime) + .WithFrameOutputCount(1)) + .ProcessAsynchronously(); + return; + } + } + + // Extract frame with specified resize try { + await FFMpegArguments + .FromFileInput(job.VideoPath) + .OutputToFile(framePath, true, options => options + .Seek(frameTime) + .WithFrameOutputCount(1) + .WithVideoCodec("png") + .Resize(frameSize.width, frameSize.height)) + .ProcessAsynchronously(); + } + catch + { + // Fallback without codec specification await FFMpegArguments .FromFileInput(job.VideoPath) .OutputToFile(framePath, true, options => options @@ -280,11 +335,11 @@ namespace Ganimede.Services .Resize(frameSize.width, frameSize.height)) .ProcessAsynchronously(); } - catch (Exception ex) - { - Debug.WriteLine($"[ERROR] Failed to extract frame {frameIndex} from {job.VideoName}: {ex.Message}"); - // Continue processing other frames even if this one fails - } + } + catch (Exception ex) + { + Debug.WriteLine($"[ERROR] Failed to extract frame {frameIndex} from {job.VideoName}: {ex.Message}"); + // Continue processing other frames even if this one fails } } } diff --git a/Ganimede/Ganimede/Windows/JobConfigWindow.xaml b/Ganimede/Ganimede/Windows/JobConfigWindow.xaml index 1de2e5e..e35392e 100644 --- a/Ganimede/Ganimede/Windows/JobConfigWindow.xaml +++ b/Ganimede/Ganimede/Windows/JobConfigWindow.xaml @@ -52,10 +52,11 @@ - - - - + + + + + @@ -68,12 +69,44 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Ganimede/Ganimede/Windows/JobConfigWindow.xaml.cs b/Ganimede/Ganimede/Windows/JobConfigWindow.xaml.cs index eb73700..69bcb33 100644 --- a/Ganimede/Ganimede/Windows/JobConfigWindow.xaml.cs +++ b/Ganimede/Ganimede/Windows/JobConfigWindow.xaml.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Windows; using System.Windows.Controls; using Ganimede.Models; +using Ganimede.Helpers; using WpfMessageBox = System.Windows.MessageBox; namespace Ganimede.Windows @@ -62,6 +63,21 @@ namespace Ganimede.Windows } } } + + // Naming settings + if (firstJob.CustomNamingPattern.HasValue) + { + UseCustomNamingCheckBox.IsChecked = true; + foreach (ComboBoxItem item in CustomNamingComboBox.Items) + { + if (item.Tag?.ToString() == firstJob.CustomNamingPattern.Value.ToString()) + { + CustomNamingComboBox.SelectedItem = item; + break; + } + } + CustomNamingPrefixTextBox.Text = firstJob.CustomPrefix ?? "custom"; + } } // Set default selections if nothing is selected @@ -70,6 +86,35 @@ namespace Ganimede.Windows if (CustomOverwriteComboBox.SelectedItem == null) CustomOverwriteComboBox.SelectedIndex = 0; + + if (CustomNamingComboBox.SelectedItem == null) + CustomNamingComboBox.SelectedIndex = 0; + + UpdateJobNamingPreview(); + } + + private void UpdateJobNamingPreview() + { + try + { + if (UseCustomNamingCheckBox.IsChecked == true && + CustomNamingComboBox.SelectedItem is ComboBoxItem selectedItem && + Enum.TryParse(selectedItem.Tag?.ToString(), out var pattern)) + { + var firstVideoName = _selectedJobs.FirstOrDefault()?.VideoName ?? "Video1"; + var customPrefix = string.IsNullOrWhiteSpace(CustomNamingPrefixTextBox.Text) ? "custom" : CustomNamingPrefixTextBox.Text; + var example = NamingHelper.GetPatternExample(pattern, firstVideoName, customPrefix); + JobNamingPreviewText.Text = example; + } + else + { + JobNamingPreviewText.Text = "Video1_000001.png (using default)"; + } + } + catch + { + JobNamingPreviewText.Text = "Video1_000001.png"; + } } private void UseCustomOutputCheckBox_CheckedChanged(object sender, RoutedEventArgs e) @@ -87,6 +132,21 @@ namespace Ganimede.Windows // Enable/disable controls handled by binding } + private void UseCustomNamingCheckBox_CheckedChanged(object sender, RoutedEventArgs e) + { + UpdateJobNamingPreview(); + } + + private void CustomNamingComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + UpdateJobNamingPreview(); + } + + private void CustomNamingPrefixTextBox_TextChanged(object sender, TextChangedEventArgs e) + { + UpdateJobNamingPreview(); + } + private void BrowseCustomOutputButton_Click(object sender, RoutedEventArgs e) { using var dialog = new System.Windows.Forms.FolderBrowserDialog @@ -149,6 +209,21 @@ namespace Ganimede.Windows { job.CustomOverwriteMode = null; } + + // Apply naming settings + if (UseCustomNamingCheckBox.IsChecked == true && CustomNamingComboBox.SelectedItem is ComboBoxItem namingItem) + { + if (Enum.TryParse(namingItem.Tag?.ToString(), out var namingPattern)) + { + job.CustomNamingPattern = namingPattern; + job.CustomPrefix = string.IsNullOrWhiteSpace(CustomNamingPrefixTextBox.Text) ? "custom" : CustomNamingPrefixTextBox.Text; + } + } + else + { + job.CustomNamingPattern = null; + job.CustomPrefix = string.Empty; + } } DialogResult = true; diff --git a/Ganimede/Ganimede/Windows/SettingsWindow.xaml b/Ganimede/Ganimede/Windows/SettingsWindow.xaml index 22fbd3e..e82dbad 100644 --- a/Ganimede/Ganimede/Windows/SettingsWindow.xaml +++ b/Ganimede/Ganimede/Windows/SettingsWindow.xaml @@ -62,20 +62,55 @@ - - - - + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Ganimede/Ganimede/Windows/SettingsWindow.xaml.cs b/Ganimede/Ganimede/Windows/SettingsWindow.xaml.cs index 7965d38..fa70809 100644 --- a/Ganimede/Ganimede/Windows/SettingsWindow.xaml.cs +++ b/Ganimede/Ganimede/Windows/SettingsWindow.xaml.cs @@ -42,6 +42,10 @@ namespace Ganimede.Windows } } + // TODO: Load naming pattern settings when controls are generated + // var namingPattern = Settings.Default.DefaultNamingPattern; + // CustomPrefixTextBox.Text = Settings.Default.DefaultCustomPrefix; + UpdateFFmpegStatus(); } @@ -111,6 +115,17 @@ namespace Ganimede.Windows } } + // TODO: Implement naming pattern event handlers when controls are generated + private void NamingPatternComboBox_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) + { + // UpdateNamingPreview(); + } + + private void CustomPrefixTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e) + { + // UpdateNamingPreview(); + } + private void SaveButton_Click(object sender, RoutedEventArgs e) { try @@ -125,6 +140,10 @@ namespace Ganimede.Windows var selectedOverwriteItem = OverwriteModeComboBox.SelectedItem as System.Windows.Controls.ComboBoxItem; Settings.Default.DefaultOverwriteMode = selectedOverwriteItem?.Tag?.ToString() ?? "Ask"; + + // TODO: Save naming settings when controls are available + // Settings.Default.DefaultNamingPattern = ... + // Settings.Default.DefaultCustomPrefix = ... Settings.Default.Save();