diff --git a/Application/FileConverter/ConversionJobs/ConversionJob.cs b/Application/FileConverter/ConversionJobs/ConversionJob.cs index f2d0151..baf214a 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob.cs @@ -13,6 +13,7 @@ namespace FileConverter.ConversionJobs private ConversionState state = ConversionState.Unknown; private string errorMessage = string.Empty; private string initialInputPath = string.Empty; + private string userState = string.Empty; public ConversionJob() { @@ -75,6 +76,20 @@ namespace FileConverter.ConversionJobs } } + public string UserState + { + get + { + return this.userState; + } + + protected set + { + this.userState = value; + this.NotifyPropertyChanged(); + } + } + public float Progress { get @@ -103,7 +118,20 @@ namespace FileConverter.ConversionJobs } } - public void PrepareConversion(string inputFilePath) + protected virtual InputPostConversionAction InputPostConversionAction + { + get + { + if (this.ConversionPreset == null) + { + return InputPostConversionAction.None; + } + + return this.ConversionPreset.InputPostConversionAction; + } + } + + public void PrepareConversion(string inputFilePath, string outputFilePath = null) { if (string.IsNullOrEmpty(inputFilePath)) { @@ -120,17 +148,17 @@ namespace FileConverter.ConversionJobs string extensionCategory = PathHelpers.GetExtensionCategory(extension); if (!PathHelpers.IsOutputTypeCompatibleWithCategory(this.ConversionPreset.OutputType, extensionCategory)) { - this.ConvertionFailed("The input file type is not compatible with the output file type."); + this.ConversionFailed("The input file type is not compatible with the output file type."); return; } this.initialInputPath = inputFilePath; this.InputFilePath = inputFilePath; - this.OutputFilePath = this.ConversionPreset.GenerateOutputFilePath(inputFilePath); + this.OutputFilePath = outputFilePath ?? this.ConversionPreset.GenerateOutputFilePath(inputFilePath); if (!PathHelpers.IsPathValid(this.OutputFilePath)) { - this.ConvertionFailed("Invalid output path generated by output file path template."); + this.ConversionFailed("Invalid output path generated by output file path template."); Diagnostics.Log(string.Format("Invalid output path generated: {0} from input: {1}.", this.OutputFilePath, this.InputFilePath)); return; } @@ -164,7 +192,7 @@ namespace FileConverter.ConversionJobs } catch (Exception) { - this.ConvertionFailed("Invalid output path generated by output file path template."); + this.ConversionFailed("Invalid output path generated by output file path template."); Diagnostics.Log(string.Format("Can't create directories for path {0}", this.OutputFilePath)); return; } @@ -179,7 +207,7 @@ namespace FileConverter.ConversionJobs } catch (Exception exception) { - this.ConvertionFailed("Can't generate a valid output filename."); + this.ConversionFailed("Can't generate a valid output filename."); Diagnostics.Log(exception.Message); return; } @@ -190,6 +218,8 @@ namespace FileConverter.ConversionJobs { this.State = ConversionState.Ready; } + + this.UserState = "In Queue"; } public void StartConvertion() @@ -207,7 +237,7 @@ namespace FileConverter.ConversionJobs Diagnostics.Log("Convert file {0} to {1}.", this.InputFilePath, this.OutputFilePath); this.State = ConversionState.InProgress; - + this.Convert(); if (this.State != ConversionState.Failed) @@ -229,7 +259,7 @@ namespace FileConverter.ConversionJobs Diagnostics.Log("Conversion Succeed!"); // Apply the input post conversion action. - switch (this.ConversionPreset.InputPostConversionAction) + switch (this.InputPostConversionAction) { case InputPostConversionAction.None: break; @@ -258,12 +288,14 @@ namespace FileConverter.ConversionJobs this.Progress = 1f; this.State = ConversionState.Done; + this.UserState = "Done"; Diagnostics.Log("Conversion Done!"); } - protected void ConvertionFailed(string exitingMessage) + protected void ConversionFailed(string exitingMessage) { this.State = ConversionState.Failed; + this.UserState = "Failed"; this.ErrorMessage = exitingMessage; } diff --git a/Application/FileConverter/ConversionJobs/ConversionJobFactory.cs b/Application/FileConverter/ConversionJobs/ConversionJobFactory.cs index c919ca4..b63601c 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJobFactory.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJobFactory.cs @@ -4,9 +4,20 @@ namespace FileConverter.ConversionJobs { public static class ConversionJobFactory { - public static ConversionJob Create(ConversionPreset conversionPreset) + public static ConversionJob Create(ConversionPreset conversionPreset, string inputFilePath) { - ConversionJob conversionJob = new ConversionJob_FFMPEG(conversionPreset); + ConversionJob conversionJob = null; + + string extension = System.IO.Path.GetExtension(inputFilePath); + extension = extension.ToLowerInvariant().Substring(1, extension.Length - 1); + if (extension == "cda") + { + conversionJob = new ConversionJob_ExtractCDA(conversionPreset); + } + else + { + conversionJob = new ConversionJob_FFMPEG(conversionPreset); + } return conversionJob; } diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_ExtractCDA.cs b/Application/FileConverter/ConversionJobs/ConversionJob_ExtractCDA.cs new file mode 100644 index 0000000..9479eb2 --- /dev/null +++ b/Application/FileConverter/ConversionJobs/ConversionJob_ExtractCDA.cs @@ -0,0 +1,205 @@ +// License: http://www.gnu.org/licenses/gpl.html GPL version 3. + +namespace FileConverter.ConversionJobs +{ + using System; + using System.IO; + using System.Threading; + + using Ripper; + + using WaveLib; + using Yeti.MMedia; + + public class ConversionJob_ExtractCDA : ConversionJob + { + private Ripper.CDDrive cdDrive; + private int cdaTrackNumber = -1; + private WaveWriter waveWriter; + private string intermediateFilePath; + private ConversionJob compressionConversionJob; + private System.Threading.Thread compressionThread; + + public ConversionJob_ExtractCDA() : base() + { + } + + public ConversionJob_ExtractCDA(ConversionPreset conversionPreset) : base(conversionPreset) + { + } + + protected override InputPostConversionAction InputPostConversionAction + { + get + { + return InputPostConversionAction.None; + } + } + + protected override void Initialize() + { + base.Initialize(); + + if (this.ConversionPreset == null) + { + throw new Exception("The conversion preset must be valid."); + } + + // Retrieve and check drive letter. + string pathDriveLetter = PathHelpers.GetPathDriveLetter(this.InputFilePath); + if (pathDriveLetter.Length == 0) + { + this.ConversionFailed("Can't retrieve input path drive letter."); + return; + } + + char driveLetter = pathDriveLetter[0]; + + this.cdDrive = new Ripper.CDDrive(); + this.cdDrive.CDRemoved += new EventHandler(this.CdDriveCdRemoved); + + bool driveLetterFound = false; + char[] driveLetters = Ripper.CDDrive.GetCDDriveLetters(); + for (int index = 0; index < driveLetters.Length; index++) + { + driveLetterFound |= driveLetters[index] == driveLetter; + } + + if (!driveLetterFound) + { + this.ConversionFailed(string.Format("Invalid drive letter {0}.", driveLetter)); + return; + } + + // Retrieve and track number. + try + { + this.cdaTrackNumber = PathHelpers.GetCDATrackNumber(this.InputFilePath); + } + catch (Exception) + { + this.ConversionFailed(string.Format("Can't retrieve the track number from input path '{0}'.", this.InputFilePath)); + return; + } + + if (this.cdDrive.IsOpened) + { + this.ConversionFailed(string.Format("CD drive already used.")); + return; + } + + if (!this.cdDrive.Open(driveLetter)) + { + this.ConversionFailed(string.Format("Fail to open cd drive {0}.", driveLetter)); + return; + } + + // Generate intermediate file path. + string fileName = Path.GetFileName(this.OutputFilePath); + string tempPath = Path.GetTempPath(); + this.intermediateFilePath = PathHelpers.GenerateUniquePath(tempPath + fileName + ".wav"); + + // Sub conversion job (for compression). + this.compressionConversionJob = ConversionJobFactory.Create(this.ConversionPreset, this.intermediateFilePath); + this.compressionConversionJob.PrepareConversion(this.intermediateFilePath, this.OutputFilePath); + this.compressionThread = new Thread(this.CompressAsync); + } + + protected override void Convert() + { + if (this.ConversionPreset == null) + { + throw new Exception("The conversion preset must be valid."); + } + + Diagnostics.Log("Starting CDA extraction."); + + this.UserState = "Extraction"; + + if (!this.cdDrive.IsCDReady()) + { + this.ConversionFailed(string.Format("CD drive is not ready.")); + return; + } + + if (!this.cdDrive.Refresh()) + { + this.ConversionFailed(string.Format("Can't refresh CD drive data.")); + return; + } + + if (!this.cdDrive.LockCD()) + { + this.ConversionFailed(string.Format("Can't lock cd.")); + return; + } + + WaveFormat waveFormat = new WaveFormat(44100, 16, 2); + + using (Stream waveStream = new FileStream(this.intermediateFilePath, FileMode.Create, FileAccess.Write)) + using (this.waveWriter = new WaveWriter(waveStream, waveFormat, this.cdDrive.TrackSize(this.cdaTrackNumber))) + { + this.cdDrive.ReadTrack(this.cdaTrackNumber, this.WriteWaveData, this.CdReadProgress); + } + + this.waveWriter = null; + + this.cdDrive.UnLockCD(); + + this.cdDrive.Close(); + + if (!File.Exists(this.intermediateFilePath)) + { + this.ConversionFailed("Extraction failed."); + return; + } + + Diagnostics.Log("CDA extracted to {0}.", this.intermediateFilePath); + Diagnostics.Log(string.Empty); + Diagnostics.Log("Start compression."); + + this.UserState = "Conversion"; + + this.compressionThread.Start(); + + while (this.compressionConversionJob.State != ConversionState.Done && + this.compressionConversionJob.State != ConversionState.Failed) + { + this.Progress = this.compressionConversionJob.Progress; + } + + if (this.compressionConversionJob.State == ConversionState.Failed) + { + this.ConversionFailed(this.compressionConversionJob.ErrorMessage); + return; + } + + Diagnostics.Log(string.Empty); + Diagnostics.Log("Delete intermediate file {0}.", this.intermediateFilePath); + + File.Delete(this.intermediateFilePath); + } + + private void WriteWaveData(object sender, DataReadEventArgs eventArgs) + { + this.waveWriter?.Write(eventArgs.Data, 0, (int)eventArgs.DataSize); + } + + private void CdReadProgress(object sender, ReadProgressEventArgs eventArgs) + { + this.Progress = (float)eventArgs.BytesRead / (float)eventArgs.Bytes2Read; + + eventArgs.CancelRead |= this.State != ConversionState.InProgress; + } + + private void CdDriveCdRemoved(object sender, System.EventArgs eventArgs) + { + this.ConversionFailed("The CD has been ejected."); + } + + private void CompressAsync() + { + this.compressionConversionJob.StartConvertion(); + } + } +} diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.Converters.cs b/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.Converters.cs index 5e351fb..dc649d6 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.Converters.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.Converters.cs @@ -249,6 +249,5 @@ namespace FileConverter.ConversionJobs throw new Exception("Unknown VBR bitrate."); } - } } diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs b/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs index 06c54ac..fe3ba3e 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs @@ -40,7 +40,7 @@ namespace FileConverter.ConversionJobs string ffmpegPath = string.Format("{0}\\ffmpeg.exe", applicationDirectory); if (!System.IO.File.Exists(ffmpegPath)) { - this.ConvertionFailed("Can't find ffmpeg executable. You should try to reinstall the application."); + this.ConversionFailed("Can't find ffmpeg executable. You should try to reinstall the application."); Diagnostics.Log("Can't find ffmpeg executable ({0}). Try to reinstall the application.", ffmpegPath); return; } @@ -164,9 +164,10 @@ namespace FileConverter.ConversionJobs throw new Exception("The conversion preset must be valid."); } - Diagnostics.Log("Convert file {0} to {1}.", this.InputFilePath, this.OutputFilePath); - Diagnostics.Log(string.Empty); + this.UserState = "Conversion"; + Diagnostics.Log("Execute command: {0} {1}.", this.ffmpegProcessStartInfo.FileName, this.ffmpegProcessStartInfo.Arguments); + Diagnostics.Log(string.Empty); try { @@ -189,7 +190,7 @@ namespace FileConverter.ConversionJobs } catch { - this.ConvertionFailed("Failed to launch FFMPEG process."); + this.ConversionFailed("Failed to launch FFMPEG process."); throw; } } @@ -227,7 +228,7 @@ namespace FileConverter.ConversionJobs if (input.Contains("Exiting.")) { - this.ConvertionFailed(input); + this.ConversionFailed(input); } } } diff --git a/Application/FileConverter/FileConverter.csproj b/Application/FileConverter/FileConverter.csproj index 138a8fe..64a6a23 100644 --- a/Application/FileConverter/FileConverter.csproj +++ b/Application/FileConverter/FileConverter.csproj @@ -48,6 +48,10 @@ + + False + ..\..\Middleware\Ripper.dll + @@ -67,6 +71,9 @@ + + ..\..\Middleware\yeti.mmedia.dll + @@ -76,6 +83,7 @@ EncodingQualitySliderControl.xaml + diff --git a/Application/FileConverter/PathHelpers.cs b/Application/FileConverter/PathHelpers.cs index c92b6e9..b5e6110 100644 --- a/Application/FileConverter/PathHelpers.cs +++ b/Application/FileConverter/PathHelpers.cs @@ -8,6 +8,7 @@ namespace FileConverter public static class PathHelpers { private static Regex driveLetterRegex = new Regex(@"[a-zA-Z]:\\"); + private static Regex cdaTrackNumberRegex = new Regex(@"[a-zA-Z]:\\Track([0-9]+)\.cda"); private static Regex pathRegex = new Regex(@"^[a-zA-Z]:\\(?:[^\\/:*?""<>|\r\n]+\\)*[^\.\\/:*?""<>|\r\n][^\\/:*?""<>|\r\n]*$"); private static Regex filenameRegex = new Regex(@"[^\\]*", RegexOptions.RightToLeft); private static Regex directoryRegex = new Regex(@"(?:([^\\]*)\\)*"); @@ -17,6 +18,18 @@ namespace FileConverter return PathHelpers.driveLetterRegex.IsMatch(path); } + public static string GetPathDriveLetter(string path) + { + return PathHelpers.driveLetterRegex.Match(path).Groups[0].Value; + } + + public static int GetCDATrackNumber(string path) + { + Match match = PathHelpers.cdaTrackNumberRegex.Match(path); + string stringNumber = match.Groups[1].Value; + return int.Parse(stringNumber); + } + public static bool IsPathValid(string path) { return PathHelpers.pathRegex.IsMatch(path); @@ -88,7 +101,6 @@ namespace FileConverter case "png": case "tiff": return InputCategoryNames.Image; - } return InputCategoryNames.Misc; diff --git a/Application/FileConverter/Windows/MainWindow.xaml b/Application/FileConverter/Windows/MainWindow.xaml index 536f3ce..006ba1a 100644 --- a/Application/FileConverter/Windows/MainWindow.xaml +++ b/Application/FileConverter/Windows/MainWindow.xaml @@ -57,7 +57,7 @@ - + diff --git a/FileConverter.sln b/FileConverter.sln index 1697b2d..af7cc2b 100644 --- a/FileConverter.sln +++ b/FileConverter.sln @@ -1,6 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2012 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileConverter", "Application\FileConverter\FileConverter.csproj", "{D27A76D2-43E4-43CC-9DA3-334B0B46F4E5}" EndProject Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "Installer", "Installer\Installer.wixproj", "{F14673DF-DF38-44B4-AB1D-99A59182C24C}" @@ -9,32 +11,44 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileConverterExtension", "A EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D27A76D2-43E4-43CC-9DA3-334B0B46F4E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D27A76D2-43E4-43CC-9DA3-334B0B46F4E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {D27A76D2-43E4-43CC-9DA3-334B0B46F4E5}.Debug|x64.ActiveCfg = Debug|Any CPU {D27A76D2-43E4-43CC-9DA3-334B0B46F4E5}.Debug|x64.Build.0 = Debug|Any CPU {D27A76D2-43E4-43CC-9DA3-334B0B46F4E5}.Debug|x86.ActiveCfg = Debug|Any CPU {D27A76D2-43E4-43CC-9DA3-334B0B46F4E5}.Debug|x86.Build.0 = Debug|Any CPU + {D27A76D2-43E4-43CC-9DA3-334B0B46F4E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D27A76D2-43E4-43CC-9DA3-334B0B46F4E5}.Release|Any CPU.Build.0 = Release|Any CPU {D27A76D2-43E4-43CC-9DA3-334B0B46F4E5}.Release|x64.ActiveCfg = Release|Any CPU {D27A76D2-43E4-43CC-9DA3-334B0B46F4E5}.Release|x64.Build.0 = Release|Any CPU {D27A76D2-43E4-43CC-9DA3-334B0B46F4E5}.Release|x86.ActiveCfg = Release|Any CPU {D27A76D2-43E4-43CC-9DA3-334B0B46F4E5}.Release|x86.Build.0 = Release|Any CPU + {F14673DF-DF38-44B4-AB1D-99A59182C24C}.Debug|Any CPU.ActiveCfg = Debug|x86 {F14673DF-DF38-44B4-AB1D-99A59182C24C}.Debug|x64.ActiveCfg = Debug|x64 {F14673DF-DF38-44B4-AB1D-99A59182C24C}.Debug|x64.Build.0 = Debug|x64 {F14673DF-DF38-44B4-AB1D-99A59182C24C}.Debug|x86.ActiveCfg = Debug|x86 {F14673DF-DF38-44B4-AB1D-99A59182C24C}.Debug|x86.Build.0 = Debug|x86 + {F14673DF-DF38-44B4-AB1D-99A59182C24C}.Release|Any CPU.ActiveCfg = Release|x86 {F14673DF-DF38-44B4-AB1D-99A59182C24C}.Release|x64.ActiveCfg = Release|x64 {F14673DF-DF38-44B4-AB1D-99A59182C24C}.Release|x64.Build.0 = Release|x64 {F14673DF-DF38-44B4-AB1D-99A59182C24C}.Release|x86.ActiveCfg = Release|x86 {F14673DF-DF38-44B4-AB1D-99A59182C24C}.Release|x86.Build.0 = Release|x86 + {0C44CA69-42D6-4357-BDFD-83069D1ABA2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C44CA69-42D6-4357-BDFD-83069D1ABA2F}.Debug|Any CPU.Build.0 = Debug|Any CPU {0C44CA69-42D6-4357-BDFD-83069D1ABA2F}.Debug|x64.ActiveCfg = Debug|Any CPU {0C44CA69-42D6-4357-BDFD-83069D1ABA2F}.Debug|x64.Build.0 = Debug|Any CPU {0C44CA69-42D6-4357-BDFD-83069D1ABA2F}.Debug|x86.ActiveCfg = Debug|Any CPU {0C44CA69-42D6-4357-BDFD-83069D1ABA2F}.Debug|x86.Build.0 = Debug|Any CPU + {0C44CA69-42D6-4357-BDFD-83069D1ABA2F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C44CA69-42D6-4357-BDFD-83069D1ABA2F}.Release|Any CPU.Build.0 = Release|Any CPU {0C44CA69-42D6-4357-BDFD-83069D1ABA2F}.Release|x64.ActiveCfg = Release|Any CPU {0C44CA69-42D6-4357-BDFD-83069D1ABA2F}.Release|x64.Build.0 = Release|Any CPU {0C44CA69-42D6-4357-BDFD-83069D1ABA2F}.Release|x86.ActiveCfg = Release|Any CPU diff --git a/Installer/Product.wxs b/Installer/Product.wxs index e680539..10a4bf2 100644 --- a/Installer/Product.wxs +++ b/Installer/Product.wxs @@ -71,7 +71,7 @@ - + diff --git a/Middleware/Ripper.dll b/Middleware/Ripper.dll new file mode 100644 index 0000000..1049755 Binary files /dev/null and b/Middleware/Ripper.dll differ diff --git a/Middleware/yeti.mmedia.dll b/Middleware/yeti.mmedia.dll new file mode 100644 index 0000000..f216eef Binary files /dev/null and b/Middleware/yeti.mmedia.dll differ