diff --git a/AaxDecrypter/AaxDecrypter.csproj b/AaxDecrypter/AaxDecrypter.csproj index 903d32d3..7629f675 100644 --- a/AaxDecrypter/AaxDecrypter.csproj +++ b/AaxDecrypter/AaxDecrypter.csproj @@ -6,7 +6,11 @@ - + + + + + diff --git a/AaxDecrypter/AaxcDownloadConverter.cs b/AaxDecrypter/AaxcDownloadConverter.cs index 160d5ede..9cac8027 100644 --- a/AaxDecrypter/AaxcDownloadConverter.cs +++ b/AaxDecrypter/AaxcDownloadConverter.cs @@ -3,10 +3,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; using AAXClean; -using Dinah.Core; -using Dinah.Core.IO; using Dinah.Core.Net.Http; using Dinah.Core.StepRunner; +using FileManager; namespace AaxDecrypter { @@ -66,8 +65,7 @@ namespace AaxDecrypter { var zeroProgress = Step2_Start(); - if (File.Exists(OutputFileName)) - FileExt.SafeDelete(OutputFileName); + FileUtility.SafeDelete(OutputFileName); var outputFile = File.Open(OutputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite); @@ -185,12 +183,8 @@ That naming may not be desirable for everyone, but it's an easy change to instea { var chapterCount = 0; aaxFile.ConvertToMultiMp4a(splitChapters, newSplitCallback => - { - var fileName = GetMultipartFileName(++chapterCount, splitChapters.Count, newSplitCallback.Chapter.Title); - if (File.Exists(fileName)) - FileExt.SafeDelete(fileName); - newSplitCallback.OutputFile = File.Open(fileName, FileMode.OpenOrCreate); - }); + createOutputFileStream(++chapterCount, splitChapters, newSplitCallback) + ); } private void ConvertToMultiMp3(ChapterInfo splitChapters) @@ -198,37 +192,22 @@ That naming may not be desirable for everyone, but it's an easy change to instea var chapterCount = 0; aaxFile.ConvertToMultiMp3(splitChapters, newSplitCallback => { - var fileName = GetMultipartFileName(++chapterCount, splitChapters.Count, newSplitCallback.Chapter.Title); - if (File.Exists(fileName)) - FileExt.SafeDelete(fileName); - newSplitCallback.OutputFile = File.Open(fileName, FileMode.OpenOrCreate); + createOutputFileStream(++chapterCount, splitChapters, newSplitCallback); newSplitCallback.LameConfig.ID3.Track = chapterCount.ToString(); }); } - private string GetMultipartFileName(int chapterCount, int chaptersTotal, string chapterTitle) - { - const int MAX_FILENAME_LENGTH = 255; + private void createOutputFileStream(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback) + { + var fileName = FileUtility.GetMultipartFileName(OutputFileName, currentChapter, splitChapters.Count, newSplitCallback.Chapter.Title); + multiPartFilePaths.Add(fileName); - // 1-9 => 1-9 - // 10-99 => 01-99 - // 100-999 => 001-999 - var chapterCountLeadingZeros = chapterCount.ToString().PadLeft(chaptersTotal.ToString().Length, '0'); + FileUtility.SafeDelete(fileName); - string extension = Path.GetExtension(OutputFileName); + newSplitCallback.OutputFile = File.Open(fileName, FileMode.OpenOrCreate); + } - var filenameBase = $"{Path.GetFileNameWithoutExtension(OutputFileName)} - {chapterCountLeadingZeros} - {chapterTitle}"; - // Replace illegal path characters with spaces - var filenameBaseSafe = string.Join(" ", filenameBase.Split(Path.GetInvalidFileNameChars())); - var fileName = filenameBaseSafe.Truncate(MAX_FILENAME_LENGTH - extension.Length); - var path = Path.Combine(Path.GetDirectoryName(OutputFileName), fileName + extension); - - multiPartFilePaths.Add(path); - - return path; - } - - private void AaxFile_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e) + private void AaxFile_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e) { var duration = aaxFile.Duration; double remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds; diff --git a/AaxDecrypter/AudiobookDownloadBase.cs b/AaxDecrypter/AudiobookDownloadBase.cs index 22a523ee..270997fe 100644 --- a/AaxDecrypter/AudiobookDownloadBase.cs +++ b/AaxDecrypter/AudiobookDownloadBase.cs @@ -1,13 +1,9 @@ -using Dinah.Core; -using Dinah.Core.IO; +using System; +using System.IO; +using Dinah.Core; using Dinah.Core.Net.Http; using Dinah.Core.StepRunner; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using FileManager; namespace AaxDecrypter { @@ -24,11 +20,13 @@ namespace AaxDecrypter public event EventHandler FileCreated; protected bool IsCanceled { get; set; } - protected string OutputFileName { get; } + protected string OutputFileName { get; private set; } protected string CacheDir { get; } protected DownloadLicense DownloadLicense { get; } protected NetworkFileStream InputFileStream => (nfsPersister ??= OpenNetworkFileStream()).NetworkFileStream; + // Don't give the property a 'set'. This should have to be an obvious choice; not accidental + protected void SetOutputFileName(string newOutputFileName) => OutputFileName = newOutputFileName; protected abstract StepSequence Steps { get; } private NetworkFileStreamPersister nfsPersister; @@ -42,14 +40,14 @@ namespace AaxDecrypter var outDir = Path.GetDirectoryName(OutputFileName); if (!Directory.Exists(outDir)) - throw new ArgumentNullException(nameof(outDir), "Directory does not exist"); - if (File.Exists(OutputFileName)) - File.Delete(OutputFileName); + throw new DirectoryNotFoundException($"Directory does not exist: {nameof(outDir)}"); if (!Directory.Exists(cacheDirectory)) - throw new ArgumentNullException(nameof(cacheDirectory), "Directory does not exist"); + throw new DirectoryNotFoundException($"Directory does not exist: {nameof(cacheDirectory)}"); CacheDir = cacheDirectory; + // delete file after validation is complete + FileUtility.SafeDelete(OutputFileName); DownloadLicense = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic)); } @@ -105,6 +103,7 @@ namespace AaxDecrypter try { var path = PathLib.ReplaceExtension(OutputFileName, ".cue"); + path = FileUtility.GetValidFilename(path); File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadLicense.ChapterInfo)); OnFileCreated(path); } @@ -117,8 +116,8 @@ namespace AaxDecrypter protected bool Step4_Cleanup() { - FileExt.SafeDelete(jsonDownloadState); - FileExt.SafeDelete(tempFile); + FileUtility.SafeDelete(jsonDownloadState); + FileUtility.SafeDelete(tempFile); return !IsCanceled; } @@ -137,8 +136,8 @@ namespace AaxDecrypter } catch { - FileExt.SafeDelete(jsonDownloadState); - FileExt.SafeDelete(tempFile); + FileUtility.SafeDelete(jsonDownloadState); + FileUtility.SafeDelete(tempFile); return NewNetworkFilePersister(); } } diff --git a/AaxDecrypter/UnencryptedAudiobookDownloader.cs b/AaxDecrypter/UnencryptedAudiobookDownloader.cs index dcbb98a3..19fee327 100644 --- a/AaxDecrypter/UnencryptedAudiobookDownloader.cs +++ b/AaxDecrypter/UnencryptedAudiobookDownloader.cs @@ -1,10 +1,8 @@ -using Dinah.Core.IO; +using System; +using System.Threading; using Dinah.Core.Net.Http; using Dinah.Core.StepRunner; -using System; -using System.IO; -using System.Linq; -using System.Threading; +using FileManager; namespace AaxDecrypter { @@ -68,10 +66,8 @@ namespace AaxDecrypter CloseInputFileStream(); - if (File.Exists(OutputFileName)) - FileExt.SafeDelete(OutputFileName); - - FileExt.SafeMove(InputFileStream.SaveFilePath, OutputFileName); + var realOutputFileName = FileUtility.Move(InputFileStream.SaveFilePath, OutputFileName); + SetOutputFileName(realOutputFileName); OnFileCreated(OutputFileName); return !IsCanceled; diff --git a/AppScaffolding/AppScaffolding.csproj b/AppScaffolding/AppScaffolding.csproj index fc1c1dc7..a9612435 100644 --- a/AppScaffolding/AppScaffolding.csproj +++ b/AppScaffolding/AppScaffolding.csproj @@ -3,7 +3,7 @@ net5.0 - 6.2.6.4 + 6.2.6.7 diff --git a/DataLayer/DataLayer.csproj b/DataLayer/DataLayer.csproj index 7cb06c0b..3250720f 100644 --- a/DataLayer/DataLayer.csproj +++ b/DataLayer/DataLayer.csproj @@ -12,13 +12,13 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/FileLiberator/ConvertToMp3.cs b/FileLiberator/ConvertToMp3.cs index b4ef29de..42dc1570 100644 --- a/FileLiberator/ConvertToMp3.cs +++ b/FileLiberator/ConvertToMp3.cs @@ -1,13 +1,12 @@ using System; using System.IO; -using System.Linq; using System.Threading.Tasks; using AAXClean; using DataLayer; using Dinah.Core; using Dinah.Core.ErrorHandling; -using Dinah.Core.IO; using Dinah.Core.Net.Http; +using FileManager; using LibationFileManager; namespace FileLiberator @@ -52,10 +51,9 @@ namespace FileLiberator m4bBook.InputStream.Close(); mp3File.Close(); - var mp3Path = Mp3FileName(m4bPath); - - FileExt.SafeMove(mp3File.Name, mp3Path); - OnFileCreated(libraryBook.Book.AudibleProductId, mp3Path); + var proposedMp3Path = Mp3FileName(m4bPath); + var realMp3Path = FileUtility.Move(mp3File.Name, proposedMp3Path); + OnFileCreated(libraryBook, realMp3Path); var statusHandler = new StatusHandler(); diff --git a/FileLiberator/DownloadDecryptBook.cs b/FileLiberator/DownloadDecryptBook.cs index cf665dfe..3262919a 100644 --- a/FileLiberator/DownloadDecryptBook.cs +++ b/FileLiberator/DownloadDecryptBook.cs @@ -129,7 +129,7 @@ namespace FileLiberator abDownloader.RetrievedAuthors += (_, authors) => OnAuthorsDiscovered(authors); abDownloader.RetrievedNarrators += (_, narrators) => OnNarratorsDiscovered(narrators); abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt; - abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook.Book.AudibleProductId, path); + abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path); // REAL WORK DONE HERE var success = await Task.Run(abDownloader.Run); @@ -189,31 +189,26 @@ namespace FileLiberator var destinationDir = FileUtility.GetValidFilename(AudibleFileStorage.BooksDirectory, titleDir, null, asin); Directory.CreateDirectory(destinationDir); - var music = entries.FirstOrDefault(f => f.FileType == FileType.Audio); + FilePathCache.CacheEntry getFirstAudio() => entries.FirstOrDefault(f => f.FileType == FileType.Audio); - if (music == default) + if (getFirstAudio() == default) return false; - var musicFileExt = Path.GetExtension(music.Path).Trim('.'); - - var audioFileName = FileUtility.GetValidFilename(destinationDir, book.Title, musicFileExt, book.AudibleProductId); - - foreach (var entry in entries) + for (var i = 0; i < entries.Count; i++) { - var fileInfo = new FileInfo(entry.Path); + var entry = entries[i]; - var dest - = entry.FileType == FileType.Audio - ? Path.Join(destinationDir, fileInfo.Name) - : FileUtility.GetValidFilename(destinationDir, book.Title, fileInfo.Extension, book.AudibleProductId, musicFileExt); + var realDest = FileUtility.Move(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path))); + FilePathCache.Insert(book.AudibleProductId, realDest); - if (Path.GetExtension(dest).Trim('.').ToLower() == "cue") - Cue.UpdateFileName(fileInfo, audioFileName); - - File.Move(fileInfo.FullName, dest); - FilePathCache.Insert(book.AudibleProductId, dest); + // propogate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop) + entries[i] = entry with { Path = realDest }; } + var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue); + if (cue != default) + Cue.UpdateFileName(cue.Path, getFirstAudio().Path); + AudibleFileStorage.Audio.Refresh(); return true; diff --git a/FileLiberator/DownloadPdf.cs b/FileLiberator/DownloadPdf.cs index 8461dc4b..fe27d5ba 100644 --- a/FileLiberator/DownloadPdf.cs +++ b/FileLiberator/DownloadPdf.cs @@ -73,7 +73,7 @@ namespace FileLiberator var client = new HttpClient(); var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress); - OnFileCreated(libraryBook.Book.AudibleProductId, actualDownloadedFilePath); + OnFileCreated(libraryBook, actualDownloadedFilePath); OnStatusUpdate(actualDownloadedFilePath); return actualDownloadedFilePath; diff --git a/FileLiberator/Streamable.cs b/FileLiberator/Streamable.cs index 79cd4877..78f83d0a 100644 --- a/FileLiberator/Streamable.cs +++ b/FileLiberator/Streamable.cs @@ -34,6 +34,7 @@ namespace FileLiberator StreamingCompleted?.Invoke(this, filePath); } + protected void OnFileCreated(DataLayer.LibraryBook libraryBook, string path) => OnFileCreated(libraryBook.Book.AudibleProductId, path); protected void OnFileCreated(string id, string path) { Serilog.Log.Logger.Information("File created {@DebugInfo}", new { Name = nameof(FileCreated), id, path }); diff --git a/FileManager.Tests/FileManager.Tests.csproj b/FileManager.Tests/FileManager.Tests.csproj index 25bece55..2ae7e2bc 100644 --- a/FileManager.Tests/FileManager.Tests.csproj +++ b/FileManager.Tests/FileManager.Tests.csproj @@ -7,10 +7,13 @@ - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/FileManager/FileManager.csproj b/FileManager/FileManager.csproj index 7c7d0ea4..c908f72c 100644 --- a/FileManager/FileManager.csproj +++ b/FileManager/FileManager.csproj @@ -5,7 +5,7 @@ - + diff --git a/FileManager/FileUtility.cs b/FileManager/FileUtility.cs index 7e9fa9ea..a6122aa9 100644 --- a/FileManager/FileUtility.cs +++ b/FileManager/FileUtility.cs @@ -2,11 +2,15 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Dinah.Core; namespace FileManager { public static class FileUtility { + private const int MAX_FILENAME_LENGTH = 255; + private const int MAX_DIRECTORY_LENGTH = 247; + //public static string GetValidFilename(string template, Dictionary parameters) { } public static string GetValidFilename(string dirFullPath, string filename, string extension, params string[] metadataSuffixes) { @@ -15,11 +19,9 @@ namespace FileManager filename ??= ""; - // file max length = 255. dir max len = 247 - // sanitize. omit invalid characters. exception: colon => underscore filename = filename.Replace(':', '_'); - filename = Dinah.Core.PathLib.ToPathSafeString(filename); + filename = PathLib.ToPathSafeString(filename); // manage length if (filename.Length > 50) @@ -41,5 +43,89 @@ namespace FileManager return fullfilename; } + + public static string GetMultipartFileName(string originalPath, int partsPosition, int partsTotal, string suffix) + { + // 1-9 => 1-9 + // 10-99 => 01-99 + // 100-999 => 001-999 + var chapterCountLeadingZeros = partsPosition.ToString().PadLeft(partsTotal.ToString().Length, '0'); + + string extension = Path.GetExtension(originalPath); + + var filenameBase = $"{Path.GetFileNameWithoutExtension(originalPath)} - {chapterCountLeadingZeros} - {suffix}"; + + // Replace illegal path characters with spaces + var filenameBaseSafe = PathLib.ToPathSafeString(filenameBase, " "); + var fileName = filenameBaseSafe.Truncate(MAX_FILENAME_LENGTH - extension.Length); + var path = Path.Combine(Path.GetDirectoryName(originalPath), fileName + extension); + return path; + } + + public static string Move(string source, string destination) + { + // TODO: destination must be valid path. Use: " (#)" when needed + SafeMove(source, destination); + return destination; + } + + public static string GetValidFilename(string path) + { + // TODO: destination must be valid path. Use: " (#)" when needed + return path; + } + + /// Delete file. No error when file does not exist. + /// Exceptions are logged, not thrown. + /// File to delete + public static void SafeDelete(string source) + { + if (!File.Exists(source)) + return; + + while (true) + { + try + { + File.Delete(source); + Serilog.Log.Logger.Information($"File successfully deleted: {source}"); + break; + } + catch (Exception e) + { + System.Threading.Thread.Sleep(100); + Serilog.Log.Logger.Error(e, $"Failed to delete: {source}"); + } + } + } + + /// + /// Moves a specified file to a new location, providing the option to specify a newfile name. + /// Exceptions are logged, not thrown. + /// + /// The name of the file to move. Can include a relative or absolute path. + /// The new path and name for the file. + public static void SafeMove(string source, string target) + { + while (true) + { + try + { + if (File.Exists(source)) + { + SafeDelete(target); + File.Move(source, target); + Serilog.Log.Logger.Information($"File successfully moved from '{source}' to '{target}'"); + } + + break; + } + catch (Exception e) + { + System.Threading.Thread.Sleep(100); + Serilog.Log.Logger.Error(e, $"Failed to move '{source}' to '{target}'"); + } + } + } } } diff --git a/InternalUtilities/InternalUtilities.csproj b/InternalUtilities/InternalUtilities.csproj index fe68fb52..67020f0b 100644 --- a/InternalUtilities/InternalUtilities.csproj +++ b/InternalUtilities/InternalUtilities.csproj @@ -5,7 +5,7 @@ - + diff --git a/LibationWinForms/LibationWinForms.csproj b/LibationWinForms/LibationWinForms.csproj index 94221bf6..615a8b52 100644 --- a/LibationWinForms/LibationWinForms.csproj +++ b/LibationWinForms/LibationWinForms.csproj @@ -29,7 +29,7 @@ - +