Replaced another id dependency with cache. Now safe for multi-file audiobooks. Also safe for current session not trying to move files created in a previous session or a parallel session of a different title

This commit is contained in:
Robert McRackan 2021-10-08 21:34:42 -04:00
parent c9a6c8fd35
commit df90094884
13 changed files with 258 additions and 221 deletions

View file

@ -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();

View file

@ -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<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{
var entries = new List<FilePathCache.CacheEntry>();
// 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<string> downloadAudiobookAsync(LibraryBook libraryBook)
private async Task<bool> 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);
}
}
/// <summary>Move new files to 'Books' directory</summary>
/// <returns>True if audiobook file(s) were successfully created and can be located on disk. Else false.</returns>
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<FileInfo> 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);
}
/// <summary>Move new files to 'Books' directory</summary>
/// <returns>True if audiobook file(s) were successfully created and can be located on disk. Else false.</returns>
private static bool moveFilesToBooksDir(Book book, List<FilePathCache.CacheEntry> 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;
}
}
}

View file

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

View file

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

View file

@ -10,7 +10,7 @@ namespace FileLiberator
public event EventHandler<TimeSpan> StreamingTimeRemaining;
public event EventHandler<string> StreamingCompleted;
/// <summary>Fired when a file is successfully saved to disk</summary>
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));
}
}
}