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