diff --git a/AaxDecrypter/AaxcDownloadConverter.cs b/AaxDecrypter/AaxcDownloadConverter.cs
index 2cbc8580..26a6abff 100644
--- a/AaxDecrypter/AaxcDownloadConverter.cs
+++ b/AaxDecrypter/AaxcDownloadConverter.cs
@@ -87,6 +87,30 @@ namespace AaxDecrypter
return success;
}
+ /*
+https://github.com/rmcrackan/Libation/pull/127#issuecomment-939088489
+
+If the chapter truly is empty, that is, 0 audio frames in length, then yes it is ignored.
+If the chapter is shorter than 3 seconds long but still has some audio frames, those frames are combined with the following chapter and not split into a new file.
+
+I also implemented file naming by chapter title. When 2 or more consecutive chapters are combined, the first of the combined chapter's title is used in the file name. For example, given an audiobook with the following chapters:
+
+00:00:00 - 00:00:02 | Part 1
+00:00:02 - 00:35:00 | Chapter 1
+00:35:02 - 01:02:00 | Chapter 2
+01:02:00 - 01:02:02 | Part 2
+01:02:02 - 01:41:00 | Chapter 3
+01:41:00 - 02:05:00 | Chapter 4
+
+The book will be split into the following files:
+
+00:00:00 - 00:35:00 | Book - 01 - Part 1.m4b
+00:35:00 - 01:02:00 | Book - 02 - Chapter 2.m4b
+01:02:00 - 01:41:00 | Book - 03 - Part 2.m4b
+01:41:00 - 02:05:00 | Book - 04 - Chapter 4.m4b
+
+That naming may not be desirable for everyone, but it's an easy change to instead use the last of the combined chapter's title in the file name.
+ */
private bool Step2_DownloadAudiobookAsMultipleFilesPerChapter()
{
var zeroProgress = Step2_Start();
diff --git a/AaxDecrypter/AudiobookDownloadBase.cs b/AaxDecrypter/AudiobookDownloadBase.cs
index bd89d4ca..22a523ee 100644
--- a/AaxDecrypter/AudiobookDownloadBase.cs
+++ b/AaxDecrypter/AudiobookDownloadBase.cs
@@ -104,7 +104,9 @@ namespace AaxDecrypter
// not a critical step. its failure should not prevent future steps from running
try
{
- File.WriteAllText(PathLib.ReplaceExtension(OutputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadLicense.ChapterInfo));
+ var path = PathLib.ReplaceExtension(OutputFileName, ".cue");
+ File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadLicense.ChapterInfo));
+ OnFileCreated(path);
}
catch (Exception ex)
{
diff --git a/AppScaffolding/AppScaffolding.csproj b/AppScaffolding/AppScaffolding.csproj
index d8a29a10..1b965e50 100644
--- a/AppScaffolding/AppScaffolding.csproj
+++ b/AppScaffolding/AppScaffolding.csproj
@@ -3,7 +3,7 @@
net5.0
- 6.2.4.0
+ 6.2.5.0
diff --git a/AppScaffolding/LibationScaffolding.cs b/AppScaffolding/LibationScaffolding.cs
index 41eb62bd..d4b39386 100644
--- a/AppScaffolding/LibationScaffolding.cs
+++ b/AppScaffolding/LibationScaffolding.cs
@@ -211,11 +211,11 @@ namespace AppScaffolding
config.InProgress,
- DownloadsInProgressDir = AudibleFileStorage.DownloadsInProgress,
- DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgress).Count(),
+ DownloadsInProgressDirectory = AudibleFileStorage.DownloadsInProgressDirectory,
+ DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgressDirectory).Count(),
- DecryptInProgressDir = AudibleFileStorage.DecryptInProgress,
- DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgress).Count(),
+ DecryptInProgressDirectory = AudibleFileStorage.DecryptInProgressDirectory,
+ DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgressDirectory).Count(),
});
}
diff --git a/FileLiberator/ConvertToMp3.cs b/FileLiberator/ConvertToMp3.cs
index abb1e6e2..b10dcd7b 100644
--- a/FileLiberator/ConvertToMp3.cs
+++ b/FileLiberator/ConvertToMp3.cs
@@ -55,7 +55,7 @@ namespace FileLiberator
var mp3Path = Mp3FileName(m4bPath);
FileExt.SafeMove(mp3File.Name, mp3Path);
- OnFileCreated(libraryBook.Book.AudibleProductId, FileManager.FileType.Audio, mp3Path);
+ OnFileCreated(libraryBook.Book.AudibleProductId, mp3Path);
var statusHandler = new StatusHandler();
diff --git a/FileLiberator/DownloadDecryptBook.cs b/FileLiberator/DownloadDecryptBook.cs
index b794157b..0bf31799 100644
--- a/FileLiberator/DownloadDecryptBook.cs
+++ b/FileLiberator/DownloadDecryptBook.cs
@@ -16,8 +16,27 @@ namespace FileLiberator
{
private AudiobookDownloadBase abDownloader;
+ public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists;
+
+ public override void Cancel() => abDownloader?.Cancel();
+
public override async Task ProcessAsync(LibraryBook libraryBook)
{
+ var entries = new List();
+ // these only work so minimally b/c CacheEntry is a record.
+ // in case of parallel decrypts, only capture the ones for this book id.
+ // if user somehow starts multiple decrypts of the same book in parallel: on their own head be it
+ void FilePathCache_Inserted(object sender, FilePathCache.CacheEntry e)
+ {
+ if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId))
+ entries.Add(e);
+ }
+ void FilePathCache_Removed(object sender, FilePathCache.CacheEntry e)
+ {
+ if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId))
+ entries.Remove(e);
+ }
+
OnBegin(libraryBook);
try
@@ -25,15 +44,19 @@ namespace FileLiberator
if (libraryBook.Book.Audio_Exists)
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
- var outputAudioFilename = await downloadAudiobookAsync(libraryBook);
+ FilePathCache.Inserted += FilePathCache_Inserted;
+ FilePathCache.Removed += FilePathCache_Removed;
+
+ var success = await downloadAudiobookAsync(libraryBook);
// decrypt failed
- if (outputAudioFilename is null)
+ if (!success)
return new StatusHandler { "Decrypt failed" };
// moves new files from temp dir to final dest
- var movedAudioFile = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
+ var movedAudioFile = moveFilesToBooksDir(libraryBook.Book, entries);
+ // decrypt failed
if (!movedAudioFile)
return new StatusHandler { "Cannot find final audio file after decryption" };
@@ -43,17 +66,20 @@ namespace FileLiberator
}
finally
{
+ FilePathCache.Inserted -= FilePathCache_Inserted;
+ FilePathCache.Removed -= FilePathCache_Removed;
+
OnCompleted(libraryBook);
}
}
- private async Task downloadAudiobookAsync(LibraryBook libraryBook)
+ private async Task downloadAudiobookAsync(LibraryBook libraryBook)
{
OnStreamingBegin($"Begin decrypting {libraryBook}");
try
{
- validate(libraryBook);
+ downloadValidation(libraryBook);
var api = await libraryBook.GetApiAsync();
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
@@ -82,9 +108,9 @@ namespace FileLiberator
audiobookDlLic.ChapterInfo.AddChapter(chap.Title, TimeSpan.FromMilliseconds(chap.LengthMs));
}
- var outFileName = Path.Combine(AudibleFileStorage.DecryptInProgress, $"{PathLib.ToPathSafeString(libraryBook.Book.Title)} [{libraryBook.Book.AudibleProductId}].{outputFormat.ToString().ToLower()}");
+ var outFileName = Path.Combine(AudibleFileStorage.DecryptInProgressDirectory, $"{PathLib.ToPathSafeString(libraryBook.Book.Title)} [{libraryBook.Book.AudibleProductId}].{outputFormat.ToString().ToLower()}");
- var cacheDir = AudibleFileStorage.DownloadsInProgress;
+ var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
abDownloader = contentLic.DrmType == AudibleApi.Common.DrmType.Adrm
? new AaxcDownloadConverter(outFileName, cacheDir, audiobookDlLic, outputFormat, Configuration.Instance.SplitFilesByChapter)
@@ -95,16 +121,12 @@ namespace FileLiberator
abDownloader.RetrievedAuthors += (_, authors) => OnAuthorsDiscovered(authors);
abDownloader.RetrievedNarrators += (_, narrators) => OnNarratorsDiscovered(narrators);
abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
- abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook.Book.AudibleProductId, FileType.Audio, path);
+ abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook.Book.AudibleProductId, path);
// REAL WORK DONE HERE
var success = await Task.Run(abDownloader.Run);
- // decrypt failed
- if (!success)
- return null;
-
- return outFileName;
+ return success;
}
finally
{
@@ -112,81 +134,7 @@ namespace FileLiberator
}
}
- private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
- {
- if (e is null && Configuration.Instance.AllowLibationFixup)
- {
- OnRequestCoverArt(abDownloader.SetCoverArt);
- }
-
- if (e is not null)
- {
- OnCoverImageDiscovered(e);
- }
- }
-
- /// Move new files to 'Books' directory
- /// True if audiobook file(s) were successfully created and can be located on disk. Else false.
- private static bool moveFilesToBooksDir(Book book, string outputAudioFilename)
- {
- // create final directory. move each file into it. MOVE AUDIO FILE LAST
- // new dir: safetitle_limit50char + " [" + productId + "]"
- // TODO make this method handle multiple audio files or a single audio file.
- var destinationDir = AudibleFileStorage.Audio.GetDestDir(book.Title, book.AudibleProductId);
- Directory.CreateDirectory(destinationDir);
-
- var sortedFiles = getProductFilesSorted(book, outputAudioFilename);
-
- var musicFileExt = Path.GetExtension(outputAudioFilename).Trim('.');
-
- // audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
- var audioFileName = FileUtility.GetValidFilename(destinationDir, book.Title, musicFileExt, book.AudibleProductId);
-
- bool movedAudioFile = false;
- foreach (var f in sortedFiles)
- {
- var isAudio = AudibleFileStorage.Audio.IsFileTypeMatch(f);
- var dest
- = isAudio
- ? Path.Join(destinationDir, f.Name)
- // non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext + "]." + non_audio_ext
- : FileUtility.GetValidFilename(destinationDir, book.Title, f.Extension, book.AudibleProductId, musicFileExt);
-
- if (Path.GetExtension(dest).Trim('.').ToLower() == "cue")
- Cue.UpdateFileName(f, audioFileName);
-
- File.Move(f.FullName, dest);
- if (isAudio)
- FilePathCache.Upsert(book.AudibleProductId, FileType.Audio, dest);
-
- movedAudioFile |= AudibleFileStorage.Audio.IsFileTypeMatch(f);
- }
-
- AudibleFileStorage.Audio.Refresh();
-
- return movedAudioFile;
- }
-
- private static List getProductFilesSorted(Book product, string outputAudioFilename)
- {
- // files are: temp path\author\[asin].ext
- var m4bDir = new FileInfo(outputAudioFilename).Directory;
- var files = m4bDir
- .EnumerateFiles()
- .Where(f => f.Name.ContainsInsensitive(product.AudibleProductId))
- .ToList();
-
- // move audio files to the end of the collection so these files are moved last
- var musicFiles = files.Where(f => AudibleFileStorage.Audio.IsFileTypeMatch(f));
- var sortedFiles = files
- .Except(musicFiles)
- .Concat(musicFiles)
- .ToList();
-
- return sortedFiles;
- }
-
- private static void validate(LibraryBook libraryBook)
+ private static void downloadValidation(LibraryBook libraryBook)
{
string errorString(string field)
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
@@ -208,11 +156,56 @@ namespace FileLiberator
throw new Exception(errorString("Locale"));
}
- public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists;
-
- public override void Cancel()
+ private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
{
- abDownloader?.Cancel();
+ if (e is null && Configuration.Instance.AllowLibationFixup)
+ OnRequestCoverArt(abDownloader.SetCoverArt);
+
+ if (e is not null)
+ OnCoverImageDiscovered(e);
+ }
+
+ /// Move new files to 'Books' directory
+ /// True if audiobook file(s) were successfully created and can be located on disk. Else false.
+ private static bool moveFilesToBooksDir(Book book, List entries)
+ {
+ // create final directory. move each file into it. MOVE AUDIO FILE LAST
+ // new dir: safetitle_limit50char + " [" + productId + "]"
+ // TODO make this method handle multiple audio files or a single audio file.
+ var destinationDir = AudibleFileStorage.Audio.GetDestDir(book.Title, book.AudibleProductId);
+ Directory.CreateDirectory(destinationDir);
+
+ var music = entries.FirstOrDefault(f => f.FileType == FileType.Audio);
+
+ if (music == default)
+ return false;
+
+ var musicFileExt = Path.GetExtension(music.Path).Trim('.');
+
+ // audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
+ var audioFileName = FileUtility.GetValidFilename(destinationDir, book.Title, musicFileExt, book.AudibleProductId);
+
+ foreach (var entry in entries)
+ {
+ var fileInfo = new FileInfo(entry.Path);
+
+ var isAudio = entry.FileType == FileType.Audio;
+ var dest
+ = isAudio
+ ? Path.Join(destinationDir, fileInfo.Name)
+ // non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext + "]." + non_audio_ext
+ : FileUtility.GetValidFilename(destinationDir, book.Title, fileInfo.Extension, book.AudibleProductId, musicFileExt);
+
+ if (Path.GetExtension(dest).Trim('.').ToLower() == "cue")
+ Cue.UpdateFileName(fileInfo, audioFileName);
+
+ File.Move(fileInfo.FullName, dest);
+ FilePathCache.Insert(book.AudibleProductId, dest);
+ }
+
+ AudibleFileStorage.Audio.Refresh();
+
+ return true;
}
}
}
diff --git a/FileLiberator/DownloadFile.cs b/FileLiberator/DownloadFile.cs
index 2decfd58..0ed0492b 100644
--- a/FileLiberator/DownloadFile.cs
+++ b/FileLiberator/DownloadFile.cs
@@ -20,7 +20,7 @@ namespace FileLiberator
try
{
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
- OnFileCreated("Upgrade", FileManager.FileType.Zip, actualDownloadedFilePath);
+ OnFileCreated("Upgrade", actualDownloadedFilePath);
return actualDownloadedFilePath;
}
finally
diff --git a/FileLiberator/DownloadPdf.cs b/FileLiberator/DownloadPdf.cs
index 2b4f4ff5..e286eff8 100644
--- a/FileLiberator/DownloadPdf.cs
+++ b/FileLiberator/DownloadPdf.cs
@@ -47,7 +47,7 @@ namespace FileLiberator
return Path.Combine(existingPath, Path.GetFileName(file));
var full = FileUtility.GetValidFilename(
- AudibleFileStorage.PdfStorageDirectory,
+ AudibleFileStorage.PdfDirectory,
libraryBook.Book.Title,
Path.GetExtension(file),
libraryBook.Book.AudibleProductId);
@@ -72,7 +72,7 @@ namespace FileLiberator
var client = new HttpClient();
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
- OnFileCreated(libraryBook.Book.AudibleProductId, FileType.PDF, actualDownloadedFilePath);
+ OnFileCreated(libraryBook.Book.AudibleProductId, actualDownloadedFilePath);
OnStatusUpdate(actualDownloadedFilePath);
return actualDownloadedFilePath;
diff --git a/FileLiberator/Streamable.cs b/FileLiberator/Streamable.cs
index d86377d0..52bf7cbf 100644
--- a/FileLiberator/Streamable.cs
+++ b/FileLiberator/Streamable.cs
@@ -10,7 +10,7 @@ namespace FileLiberator
public event EventHandler StreamingTimeRemaining;
public event EventHandler StreamingCompleted;
/// Fired when a file is successfully saved to disk
- public event EventHandler<(string id, FileManager.FileType type, string path)> FileCreated;
+ public event EventHandler<(string id, string path)> FileCreated;
protected void OnStreamingBegin(string filePath)
{
@@ -34,11 +34,11 @@ namespace FileLiberator
StreamingCompleted?.Invoke(this, filePath);
}
- protected void OnFileCreated(string productId, FileManager.FileType type, string path)
+ protected void OnFileCreated(string id, string path)
{
- Serilog.Log.Logger.Information("File created {@DebugInfo}", new { Name = nameof(FileCreated), productId, TypeId = (int)type, TypeName = type.ToString(), path });
- FileManager.FilePathCache.Upsert(productId, type, path);
- FileCreated?.Invoke(this, (productId, type, path));
+ Serilog.Log.Logger.Information("File created {@DebugInfo}", new { Name = nameof(FileCreated), id, path });
+ FileManager.FilePathCache.Insert(id, path);
+ FileCreated?.Invoke(this, (id, path));
}
}
}
diff --git a/FileManager/AudibleFileStorage.cs b/FileManager/AudibleFileStorage.cs
index 4f5457e8..5bbb30b5 100644
--- a/FileManager/AudibleFileStorage.cs
+++ b/FileManager/AudibleFileStorage.cs
@@ -3,28 +3,22 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
-using Dinah.Core;
-using Dinah.Core.Collections.Generic;
namespace FileManager
{
- public enum FileType { Unknown, Audio, AAXC, PDF, Zip }
+ public abstract class AudibleFileStorage
+ {
+ protected abstract string GetFilePathCustom(string productId);
- public abstract class AudibleFileStorage : Enumeration
- {
- protected abstract string[] Extensions { get; }
- public abstract string StorageDirectory { get; }
+ #region static
+ public static string DownloadsInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
+ public static string DecryptInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
+ public static string PdfDirectory => BooksDirectory;
- public static string DownloadsInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
- public static string DecryptInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
-
- public static string PdfStorageDirectory => BooksDirectory;
-
- private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage();
+ private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage();
public static bool AaxcExists(string productId) => AAXC.Exists(productId);
- #region static
- public static AudioFileStorage Audio { get; } = new AudioFileStorage();
+ public static AudioFileStorage Audio { get; } = new AudioFileStorage();
public static string BooksDirectory
{
@@ -35,69 +29,60 @@ namespace FileManager
return Directory.CreateDirectory(Configuration.Instance.Books).FullName;
}
}
-
- private static object bookDirectoryFilesLocker { get; } = new();
- internal static BackgroundFileSystem BookDirectoryFiles { get; set; }
#endregion
#region instance
- public FileType FileType => (FileType)Value;
+ private FileType FileType { get; }
+ private string regexTemplate { get; }
- protected IEnumerable extensions_noDots { get; }
- private string extAggr { get; }
+ protected AudibleFileStorage(FileType fileType)
+ {
+ FileType = fileType;
- protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString())
- {
- extensions_noDots = Extensions.Select(ext => ext.ToLower().Trim('.')).ToList();
- extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}");
- BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
+ var extAggr = FileTypes.GetExtensions(FileType).Aggregate((a, b) => $"{a}|{b}");
+ regexTemplate = $@"{{0}}.*?\.({extAggr})$";
}
protected string GetFilePath(string productId)
{
- var cachedFile = FilePathCache.GetPath(productId, FileType);
+ // primary lookup
+ var cachedFile = FilePathCache.GetFirstPath(productId, FileType);
if (cachedFile != null)
return cachedFile;
- var regex = new Regex($@"{productId}.*?\.({extAggr})$", RegexOptions.IgnoreCase);
+ // secondary lookup attempt
+ var firstOrNull = GetFilePathCustom(productId);
+ if (firstOrNull is not null)
+ FilePathCache.Insert(productId, firstOrNull);
- string firstOrNull;
+ return firstOrNull;
+ }
- if (StorageDirectory == BooksDirectory)
- {
- //If user changed the BooksDirectory, reinitialize.
- lock (bookDirectoryFilesLocker)
- if (StorageDirectory != BookDirectoryFiles.RootDirectory)
- BookDirectoryFiles = new BackgroundFileSystem(StorageDirectory, "*.*", SearchOption.AllDirectories);
-
- firstOrNull = BookDirectoryFiles.FindFile(regex);
- }
- else
- {
- firstOrNull =
- Directory
- .EnumerateFiles(StorageDirectory, "*.*", SearchOption.AllDirectories)
- .FirstOrDefault(s => regex.IsMatch(s));
- }
-
- if (firstOrNull is not null)
- FilePathCache.Upsert(productId, FileType, firstOrNull);
-
- return firstOrNull;
+ protected Regex GetBookSearchRegex(string productId)
+ {
+ var pattern = string.Format(regexTemplate, productId);
+ return new Regex(pattern, RegexOptions.IgnoreCase);
}
#endregion
}
public class AudioFileStorage : AudibleFileStorage
{
- protected override string[] Extensions { get; } = new[] { "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac" };
+ private static BackgroundFileSystem BookDirectoryFiles { get; set; }
+ private static object bookDirectoryFilesLocker { get; } = new();
+ protected override string GetFilePathCustom(string productId)
+ {
+ // If user changed the BooksDirectory: reinitialize
+ lock (bookDirectoryFilesLocker)
+ if (BooksDirectory != BookDirectoryFiles.RootDirectory)
+ BookDirectoryFiles = new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
- // we always want to use the latest config value, therefore
- // - DO use 'get' arrow "=>"
- // - do NOT use assign "="
- public override string StorageDirectory => BooksDirectory;
+ var regex = GetBookSearchRegex(productId);
+ return BookDirectoryFiles.FindFile(regex);
+ }
- public AudioFileStorage() : base(FileType.Audio) { }
+ internal AudioFileStorage() : base(FileType.Audio)
+ => BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
public void Refresh() => BookDirectoryFiles.RefreshFiles();
@@ -109,33 +94,25 @@ namespace FileManager
= underscoreIndex < 4
? title
: title.Substring(0, underscoreIndex);
- var finalDir = FileUtility.GetValidFilename(StorageDirectory, titleDir, null, asin);
+ var finalDir = FileUtility.GetValidFilename(BooksDirectory, titleDir, null, asin);
return finalDir;
}
- public bool IsFileTypeMatch(FileInfo fileInfo)
- => extensions_noDots.ContainsInsensative(fileInfo.Extension.Trim('.'));
-
public string GetPath(string productId) => GetFilePath(productId);
}
- public class AaxcFileStorage : AudibleFileStorage
+ internal class AaxcFileStorage : AudibleFileStorage
{
- protected override string[] Extensions { get; } = new[] { "aaxc" };
+ protected override string GetFilePathCustom(string productId)
+ {
+ var regex = GetBookSearchRegex(productId);
+ return Directory
+ .EnumerateFiles(DownloadsInProgressDirectory, "*.*", SearchOption.AllDirectories)
+ .FirstOrDefault(s => regex.IsMatch(s));
+ }
- // we always want to use the latest config value, therefore
- // - DO use 'get' arrow "=>"
- // - do NOT use assign "="
- public override string StorageDirectory => DownloadsInProgress;
+ internal AaxcFileStorage() : base(FileType.AAXC) { }
- public AaxcFileStorage() : base(FileType.AAXC) { }
-
- ///
- /// Example for full books:
- /// Search recursively in _books directory. Full book exists if either are true
- /// - a directory name has the product id and an audio file is immediately inside
- /// - any audio filename contains the product id
- ///
public bool Exists(string productId) => GetFilePath(productId) != null;
}
}
diff --git a/FileManager/BackgroundFileSystem.cs b/FileManager/BackgroundFileSystem.cs
index f999d4cb..663c702a 100644
--- a/FileManager/BackgroundFileSystem.cs
+++ b/FileManager/BackgroundFileSystem.cs
@@ -10,7 +10,7 @@ namespace FileManager
///
/// Tracks actual locations of files. This is especially useful for clicking button to navigate to the book's files.
///
- /// Note: this is no longer how Libation manages "Liberated" state. That is not statefully managed in the database.
+ /// Note: this is no longer how Libation manages "Liberated" state. That is now statefully managed in the database.
/// This paradigm is what allows users to manually choose to not download books. Also allows them to manually toggle
/// this state and download again.
///
diff --git a/FileManager/FilePathCache.cs b/FileManager/FilePathCache.cs
index c710740e..2d2dd5d6 100644
--- a/FileManager/FilePathCache.cs
+++ b/FileManager/FilePathCache.cs
@@ -8,14 +8,13 @@ using Newtonsoft.Json;
namespace FileManager
{
public static class FilePathCache
- {
+ {
+ public record CacheEntry(string Id, FileType FileType, string Path);
+
private const string FILENAME = "FileLocations.json";
- internal class CacheEntry
- {
- public string Id { get; set; }
- public FileType FileType { get; set; }
- public string Path { get; set; }
- }
+
+ public static event EventHandler Inserted;
+ public static event EventHandler Removed;
private static Cache cache { get; } = new Cache();
@@ -31,48 +30,51 @@ namespace FileManager
}
}
- public static bool Exists(string id, FileType type) => GetPath(id, type) != null;
+ public static bool Exists(string id, FileType type) => GetFirstPath(id, type) != null;
- public static string GetPath(string id, FileType type)
- {
- var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type);
+ public static List<(FileType fileType, string path)> GetFiles(string id)
+ => getEntries(entry => entry.Id == id)
+ .Select(entry => (entry.FileType, entry.Path))
+ .ToList();
- if (entry == null)
- return null;
+ public static string GetFirstPath(string id, FileType type)
+ => getEntries(entry => entry.Id == id && entry.FileType == type)
+ .FirstOrDefault()
+ ?.Path;
- if (!File.Exists(entry.Path))
- {
- remove(entry);
- return null;
- }
-
- return entry.Path;
- }
-
- private static void remove(CacheEntry entry)
+ private static List getEntries(Func predicate)
{
- cache.Remove(entry);
- save();
+ var entries = cache.Where(predicate).ToList();
+ if (entries is null || !entries.Any())
+ return null;
+
+ remove(entries.Where(e => !File.Exists(e.Path)).ToList());
+
+ return entries;
}
- public static void Upsert(string id, FileType type, string path)
+ private static void remove(List entries)
{
- if (!File.Exists(path))
+ if (entries is null)
+ return;
+
+ lock (locker)
{
- // file not found can happen after rapid move
- System.Threading.Thread.Sleep(100);
-
- if (!File.Exists(path))
- throw new FileNotFoundException($"Cannot add path to cache. File not found. Id={id} FileType={type}", path);
+ foreach (var entry in entries)
+ {
+ cache.Remove(entry);
+ Removed?.Invoke(null, entry);
+ }
+ save();
}
+ }
- var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type);
-
- if (entry is null)
- cache.Add(new CacheEntry { Id = id, FileType = type, Path = path });
- else
- entry.Path = path;
-
+ public static void Insert(string id, string path)
+ {
+ var type = FileTypes.GetFileTypeFromPath(path);
+ var entry = new CacheEntry(id, type, path);
+ cache.Add(entry);
+ Inserted?.Invoke(null, entry);
save();
}
diff --git a/FileManager/FileTypes.cs b/FileManager/FileTypes.cs
new file mode 100644
index 00000000..25609145
--- /dev/null
+++ b/FileManager/FileTypes.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace FileManager
+{
+ public enum FileType { Unknown, Audio, AAXC, PDF, Zip, Cue }
+
+ public static class FileTypes
+ {
+ private static Dictionary dic => new()
+ {
+ ["aaxc"] = FileType.AAXC,
+ ["cue"] = FileType.Cue,
+ ["pdf"] = FileType.PDF,
+ ["zip"] = FileType.Zip,
+
+ ["aac"] = FileType.Audio,
+ ["flac"] = FileType.Audio,
+ ["m4a"] = FileType.Audio,
+ ["m4b"] = FileType.Audio,
+ ["mp3"] = FileType.Audio,
+ ["mp4"] = FileType.Audio,
+ ["ogg"] = FileType.Audio,
+ };
+
+ public static FileType GetFileTypeFromPath(string path)
+ => dic.TryGetValue(Path.GetExtension(path).ToLower().Trim('.'), out var fileType)
+ ? fileType
+ : FileType.Unknown;
+
+ public static List GetExtensions(FileType fileType)
+ => dic
+ .Where(kvp => kvp.Value == fileType)
+ .Select(kvp => kvp.Key)
+ .ToList();
+ }
+}