Refactor conversion job system.

This commit is contained in:
Adrien Allard 2015-08-18 23:06:51 +02:00
parent 890ed87cf8
commit c9d2c77340
7 changed files with 243 additions and 156 deletions

View File

@ -197,7 +197,9 @@ namespace FileConverter
for (int index = 0; index < filePaths.Count; index++)
{
string inputFilePath = filePaths[index];
ConversionJob conversionJob = new ConversionJob(conversionPreset, inputFilePath);
ConversionJob conversionJob = ConversionJobFactory.Create(conversionPreset);
conversionJob.PrepareConversion(inputFilePath);
this.conversionJobs.Add(conversionJob);
}

View File

@ -4,46 +4,29 @@ namespace FileConverter.ConversionJobs
{
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
public class ConversionJob : INotifyPropertyChanged
{
private readonly Regex durationRegex = new Regex(@"Duration:\s*([0-9][0-9]):([0-9][0-9]):([0-9][0-9])\.([0-9][0-9]),.*bitrate:\s*([0-9]+) kb\/s");
private readonly Regex progressRegex = new Regex(@"size=\s*([0-9]+)kB\s+time=([0-9][0-9]):([0-9][0-9]):([0-9][0-9]).([0-9][0-9])\s+bitrate=\s*([0-9]+.[0-9])kbits\/s");
private TimeSpan fileDuration;
private TimeSpan actualConvertedDuration;
private ProcessStartInfo ffmpegProcessStartInfo;
private float progress;
private ConversionState state;
private string exitingMessage;
private float progress = 0f;
private ConversionState state = ConversionState.Unknown;
private string errorMessage = string.Empty;
public ConversionJob()
{
this.ConversionPreset = null;
this.progress = 0f;
this.InputFilePath = string.Empty;
this.OutputFilePath = string.Empty;
this.State = ConversionState.Unknown;
this.ExitingMessage = string.Empty;
this.ConversionPreset = null;
this.InputFilePath = string.Empty;
}
public ConversionJob(ConversionPreset conversionPreset, string inputFilePath)
public ConversionJob(ConversionPreset conversionPreset) : this()
{
if (conversionPreset == null)
{
throw new ArgumentNullException("conversionPreset");
}
this.ConversionPreset = conversionPreset;
this.InputFilePath = inputFilePath;
string extension = System.IO.Path.GetExtension(inputFilePath);
this.OutputFilePath = inputFilePath.Substring(0, inputFilePath.Length - extension.Length) + "." + this.ConversionPreset.OutputType.ToString().ToLowerInvariant();
this.Initialize();
}
public event PropertyChangedEventHandler PropertyChanged;
@ -97,27 +80,47 @@ namespace FileConverter.ConversionJobs
return this.progress;
}
private set
protected set
{
this.progress = value;
this.NotifyPropertyChanged();
}
}
public string ExitingMessage
public string ErrorMessage
{
get
{
return this.exitingMessage;
return this.errorMessage;
}
set
private set
{
this.exitingMessage = value;
this.errorMessage = value;
this.NotifyPropertyChanged();
}
}
public void PrepareConversion(string inputFilePath)
{
if (string.IsNullOrEmpty(inputFilePath))
{
throw new ArgumentNullException("inputFilePath");
}
if (this.ConversionPreset == null)
{
throw new Exception("The conversion preset must be valid.");
}
this.InputFilePath = inputFilePath;
this.OutputFilePath = this.GenerateOutputPath(this.InputFilePath);
this.Initialize();
this.State = ConversionState.Ready;
}
public void StartConvertion()
{
if (this.ConversionPreset == null)
@ -125,140 +128,51 @@ namespace FileConverter.ConversionJobs
throw new Exception("The conversion preset must be valid.");
}
if (this.State != ConversionState.Ready)
{
throw new Exception("Invalid conversion state.");
}
Diagnostics.Log("Convert file {0} to {1}.", this.InputFilePath, this.OutputFilePath);
string arguments = string.Empty;
switch (this.ConversionPreset.OutputType)
{
case OutputType.Mp3:
arguments = string.Format("-n -stats -i \"{0}\" -qscale:a 2 \"{1}\"", this.InputFilePath, this.OutputFilePath);
break;
case OutputType.Ogg:
arguments = string.Format("-n -stats -i \"{0}\" -acodec libvorbis -qscale:a 2 \"{1}\"", this.InputFilePath, this.OutputFilePath);
break;
case OutputType.Flac:
case OutputType.Wav:
arguments = string.Format("-n -stats -i \"{0}\" \"{1}\"", this.InputFilePath, this.OutputFilePath);
break;
default:
throw new NotImplementedException("Converter not implemented for output file type " + this.ConversionPreset.OutputType);
}
if (string.IsNullOrEmpty(arguments) || this.ffmpegProcessStartInfo == null)
{
return;
}
this.State = ConversionState.InProgress;
try
this.Convert();
if (this.State != ConversionState.Failed)
{
Diagnostics.Log(string.Empty);
this.ffmpegProcessStartInfo.Arguments = arguments;
Diagnostics.Log("Execute command: {0} {1}.", this.ffmpegProcessStartInfo.FileName, this.ffmpegProcessStartInfo.Arguments);
using (Process exeProcess = Process.Start(this.ffmpegProcessStartInfo))
{
using (StreamReader reader = exeProcess.StandardError)
{
while (!reader.EndOfStream)
{
string result = reader.ReadLine();
this.ParseFFMPEGOutput(result);
Diagnostics.Log("ffmpeg output: {0}", result);
}
}
exeProcess.WaitForExit();
}
// Convertion succeed !
this.Progress = 1f;
this.State = ConversionState.Done;
Diagnostics.Log("\nDone!");
}
catch
{
this.State = ConversionState.Failed;
throw;
}
if (!string.IsNullOrEmpty(this.ExitingMessage))
{
this.State = ConversionState.Failed;
return;
}
this.Progress = 1f;
this.State = ConversionState.Done;
Diagnostics.Log("\nDone!");
}
private void Initialize()
protected virtual void Convert()
{
}
protected virtual void Initialize()
{
}
protected void ConvertionFailed(string exitingMessage)
{
this.State = ConversionState.Failed;
}
protected virtual string GenerateOutputPath(string inputFilePath)
{
if (this.ConversionPreset == null)
{
throw new Exception("The conversion preset must be valid.");
}
this.ffmpegProcessStartInfo = null;
string applicationDirectory = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
string ffmpegPath = string.Format("{0}\\ffmpeg.exe", applicationDirectory);
if (!System.IO.File.Exists(ffmpegPath))
{
Diagnostics.Log("Can't find ffmpeg executable ({0}). Try to reinstall the application.", ffmpegPath);
return;
}
this.ffmpegProcessStartInfo = new ProcessStartInfo(ffmpegPath);
this.ffmpegProcessStartInfo.CreateNoWindow = true;
this.ffmpegProcessStartInfo.UseShellExecute = false;
this.ffmpegProcessStartInfo.RedirectStandardOutput = true;
this.ffmpegProcessStartInfo.RedirectStandardError = true;
this.State = ConversionState.Ready;
string extension = System.IO.Path.GetExtension(inputFilePath);
return inputFilePath.Substring(0, inputFilePath.Length - extension.Length) + "." + this.ConversionPreset.OutputType.ToString().ToLowerInvariant();
}
private void ParseFFMPEGOutput(string input)
{
Match match = this.durationRegex.Match(input);
if (match.Success && match.Groups.Count >= 6)
{
int hours = int.Parse(match.Groups[1].Value);
int minutes = int.Parse(match.Groups[2].Value);
int seconds = int.Parse(match.Groups[3].Value);
int milliseconds = int.Parse(match.Groups[4].Value);
float bitrate = float.Parse(match.Groups[5].Value);
this.fileDuration = new TimeSpan(0, hours, minutes, seconds, milliseconds);
return;
}
match = this.progressRegex.Match(input);
if (match.Success && match.Groups.Count >= 7)
{
int size = int.Parse(match.Groups[1].Value);
int hours = int.Parse(match.Groups[2].Value);
int minutes = int.Parse(match.Groups[3].Value);
int seconds = int.Parse(match.Groups[4].Value);
int milliseconds = int.Parse(match.Groups[5].Value);
float bitrate = 0f;
float.TryParse(match.Groups[6].Value, out bitrate);
this.actualConvertedDuration = new TimeSpan(0, hours, minutes, seconds, milliseconds);
this.Progress = this.actualConvertedDuration.Ticks / (float)this.fileDuration.Ticks;
return;
}
if (input.Contains("Exiting."))
{
this.ExitingMessage = input;
}
}
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
if (this.PropertyChanged != null)
{

View File

@ -0,0 +1,14 @@
// <copyright file="ConversionJobFactory.cs" company="AAllard">License: http://www.gnu.org/licenses/gpl.html GPL version 3.</copyright>
namespace FileConverter.ConversionJobs
{
public static class ConversionJobFactory
{
public static ConversionJob Create(ConversionPreset conversionPreset)
{
ConversionJob conversionJob = new ConversionJob_FFMPEG(conversionPreset);
return conversionJob;
}
}
}

View File

@ -0,0 +1,154 @@
// <copyright file="ConversionJob_FFMPEG.cs" company="AAllard">License: http://www.gnu.org/licenses/gpl.html GPL version 3.</copyright>
namespace FileConverter.ConversionJobs
{
using System;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
public class ConversionJob_FFMPEG : ConversionJob
{
private readonly Regex durationRegex = new Regex(@"Duration:\s*([0-9][0-9]):([0-9][0-9]):([0-9][0-9])\.([0-9][0-9]),.*bitrate:\s*([0-9]+) kb\/s");
private readonly Regex progressRegex = new Regex(@"size=\s*([0-9]+)kB\s+time=([0-9][0-9]):([0-9][0-9]):([0-9][0-9]).([0-9][0-9])\s+bitrate=\s*([0-9]+.[0-9])kbits\/s");
private TimeSpan fileDuration;
private TimeSpan actualConvertedDuration;
private ProcessStartInfo ffmpegProcessStartInfo;
public ConversionJob_FFMPEG() : base()
{
}
public ConversionJob_FFMPEG(ConversionPreset conversionPreset) : base(conversionPreset)
{
}
protected override void Initialize()
{
if (this.ConversionPreset == null)
{
throw new Exception("The conversion preset must be valid.");
}
this.ffmpegProcessStartInfo = null;
string applicationDirectory = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
string ffmpegPath = string.Format("{0}\\ffmpeg.exe", applicationDirectory);
if (!System.IO.File.Exists(ffmpegPath))
{
Diagnostics.Log("Can't find ffmpeg executable ({0}). Try to reinstall the application.", ffmpegPath);
return;
}
this.ffmpegProcessStartInfo = new ProcessStartInfo(ffmpegPath);
this.ffmpegProcessStartInfo.CreateNoWindow = true;
this.ffmpegProcessStartInfo.UseShellExecute = false;
this.ffmpegProcessStartInfo.RedirectStandardOutput = true;
this.ffmpegProcessStartInfo.RedirectStandardError = true;
string arguments = string.Empty;
switch (this.ConversionPreset.OutputType)
{
case OutputType.Mp3:
arguments = string.Format("-n -stats -i \"{0}\" -qscale:a 2 \"{1}\"", this.InputFilePath, this.OutputFilePath);
break;
case OutputType.Ogg:
arguments = string.Format("-n -stats -i \"{0}\" -acodec libvorbis -qscale:a 2 \"{1}\"", this.InputFilePath, this.OutputFilePath);
break;
case OutputType.Flac:
case OutputType.Wav:
arguments = string.Format("-n -stats -i \"{0}\" \"{1}\"", this.InputFilePath, this.OutputFilePath);
break;
default:
throw new NotImplementedException("Converter not implemented for output file type " + this.ConversionPreset.OutputType);
}
if (string.IsNullOrEmpty(arguments))
{
throw new Exception("Invalid ffmpeg process arguments.");
}
this.ffmpegProcessStartInfo.Arguments = arguments;
}
protected override void Convert()
{
if (this.ConversionPreset == null)
{
throw new Exception("The conversion preset must be valid.");
}
Diagnostics.Log("Convert file {0} to {1}.", this.InputFilePath, this.OutputFilePath);
Diagnostics.Log(string.Empty);
Diagnostics.Log("Execute command: {0} {1}.", this.ffmpegProcessStartInfo.FileName, this.ffmpegProcessStartInfo.Arguments);
try
{
using (Process exeProcess = Process.Start(this.ffmpegProcessStartInfo))
{
using (StreamReader reader = exeProcess.StandardError)
{
while (!reader.EndOfStream)
{
string result = reader.ReadLine();
this.ParseFFMPEGOutput(result);
Diagnostics.Log("ffmpeg output: {0}", result);
}
}
exeProcess.WaitForExit();
}
}
catch
{
this.ConvertionFailed("Failed to launch FFMPEG process.");
throw;
}
}
private void ParseFFMPEGOutput(string input)
{
Match match = this.durationRegex.Match(input);
if (match.Success && match.Groups.Count >= 6)
{
int hours = int.Parse(match.Groups[1].Value);
int minutes = int.Parse(match.Groups[2].Value);
int seconds = int.Parse(match.Groups[3].Value);
int milliseconds = int.Parse(match.Groups[4].Value);
float bitrate = float.Parse(match.Groups[5].Value);
this.fileDuration = new TimeSpan(0, hours, minutes, seconds, milliseconds);
return;
}
match = this.progressRegex.Match(input);
if (match.Success && match.Groups.Count >= 7)
{
int size = int.Parse(match.Groups[1].Value);
int hours = int.Parse(match.Groups[2].Value);
int minutes = int.Parse(match.Groups[3].Value);
int seconds = int.Parse(match.Groups[4].Value);
int milliseconds = int.Parse(match.Groups[5].Value);
float bitrate = 0f;
float.TryParse(match.Groups[6].Value, out bitrate);
this.actualConvertedDuration = new TimeSpan(0, hours, minutes, seconds, milliseconds);
this.Progress = this.actualConvertedDuration.Ticks / (float)this.fileDuration.Ticks;
return;
}
if (input.Contains("Exiting."))
{
this.ConvertionFailed(input);
}
}
}
}

View File

@ -76,7 +76,8 @@
<Compile Include="Controls\EncodingQualitySliderControl.xaml.cs">
<DependentUpon>EncodingQualitySliderControl.xaml</DependentUpon>
</Compile>
<Compile Include="ConversionJobs\ConversionJob_MP3.cs" />
<Compile Include="ConversionJobs\ConversionJobFactory.cs" />
<Compile Include="ConversionJobs\ConversionJob_FFMPEG.cs" />
<Compile Include="EncodingMode.cs" />
<Compile Include="ValueConverters\ApplicationVersionToApplicationName.cs" />
<Compile Include="ConversionPreset.cs" />

View File

@ -12,5 +12,7 @@
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForOtherTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=MethodPropertyEvent/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -4,7 +4,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:FileConverter"
xmlns:valueConverters="clr-namespace:FileConverter.ValueConverters"
xmlns:ConversionJobs="clr-namespace:FileConverter.ConversionJobs" mc:Ignorable="d" x:Class="FileConverter.MainWindow"
xmlns:conversionJobs="clr-namespace:FileConverter.ConversionJobs" mc:Ignorable="d" x:Class="FileConverter.MainWindow"
x:Name="FileConverterMainWindow" Height="500" Width="950" MinHeight="250" MinWidth="450" WindowStartupLocation="CenterScreen" Icon="/FileConverter;component/Resources/ApplicationIcon-16x16.ico">
<Window.Resources>
<valueConverters:ApplicationVersionToApplicationName x:Key="ApplicationVersionToApplicationName"/>
@ -54,7 +54,7 @@
<TextBlock Text="Converted from " FontSize="11" FontStyle="Italic" />
<TextBlock Text="{Binding InputFilePath}" FontSize="11" FontStyle="Italic" />
</WrapPanel>
<TextBlock Text="{Binding ExitingMessage}" Foreground="{Binding State, ConverterParameter=Foreground, Converter={StaticResource ConversionStateToColor}}" />
<TextBlock Text="{Binding ErrorMessage}" Foreground="{Binding State, ConverterParameter=Foreground, Converter={StaticResource ConversionStateToColor}}" />
</StackPanel>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding State}" FontWeight="Bold" Foreground="{Binding State, ConverterParameter=Foreground, Converter={StaticResource ConversionStateToColor}}" />
@ -65,7 +65,7 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.DataContext>
<ConversionJobs:ConversionJob/>
<conversionJobs:ConversionJob/>
</ItemsControl.DataContext>
</ItemsControl>
</ScrollViewer>