New and moved files Upsert themselves in FileManager.FilePathCache

This commit is contained in:
Robert McRackan 2021-10-08 11:47:54 -04:00
parent 18cca53968
commit d0b78cc501
11 changed files with 149 additions and 144 deletions

View File

@ -1,11 +1,12 @@
using AAXClean; using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AAXClean;
using Dinah.Core; using Dinah.Core;
using Dinah.Core.IO; using Dinah.Core.IO;
using Dinah.Core.Net.Http; using Dinah.Core.Net.Http;
using Dinah.Core.StepRunner; using Dinah.Core.StepRunner;
using System;
using System.IO;
using System.Linq;
namespace AaxDecrypter namespace AaxDecrypter
{ {
@ -13,17 +14,19 @@ namespace AaxDecrypter
{ {
const int MAX_FILENAME_LENGTH = 255; const int MAX_FILENAME_LENGTH = 255;
private static readonly TimeSpan minChapterLength = TimeSpan.FromSeconds(3); private static readonly TimeSpan minChapterLength = TimeSpan.FromSeconds(3);
protected override StepSequence steps { get; } protected override StepSequence Steps { get; }
private AaxFile aaxFile; private AaxFile aaxFile;
private OutputFormat OutputFormat { get; } private OutputFormat OutputFormat { get; }
private List<string> multiPartFilePaths { get; } = new List<string>();
public AaxcDownloadConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat, bool splitFileByChapters) public AaxcDownloadConverter(string outFileName, string cacheDirectory, DownloadLicense dlLic, OutputFormat outputFormat, bool splitFileByChapters)
:base(outFileName, cacheDirectory, dlLic) : base(outFileName, cacheDirectory, dlLic)
{ {
OutputFormat = outputFormat; OutputFormat = outputFormat;
steps = new StepSequence Steps = new StepSequence
{ {
Name = "Download and Convert Aaxc To " + OutputFormat, Name = "Download and Convert Aaxc To " + OutputFormat,
@ -31,8 +34,8 @@ namespace AaxDecrypter
["Step 2: Download Decrypted Audiobook"] = splitFileByChapters ["Step 2: Download Decrypted Audiobook"] = splitFileByChapters
? Step2_DownloadAudiobookAsMultipleFilesPerChapter ? Step2_DownloadAudiobookAsMultipleFilesPerChapter
: Step2_DownloadAudiobookAsSingleFile, : Step2_DownloadAudiobookAsSingleFile,
["Step 3: Create Cue"] = splitFileByChapters ["Step 3: Create Cue"] = splitFileByChapters
? () => true ? () => true
: Step3_CreateCue, : Step3_CreateCue,
["Step 4: Cleanup"] = Step4_Cleanup, ["Step 4: Cleanup"] = Step4_Cleanup,
}; };
@ -49,7 +52,7 @@ namespace AaxDecrypter
} }
protected override bool Step1_GetMetadata() protected override bool Step1_GetMetadata()
{ {
aaxFile = new AaxFile(InputFileStream); aaxFile = new AaxFile(InputFileStream);
OnRetrievedTitle(aaxFile.AppleTags.TitleSansUnabridged); OnRetrievedTitle(aaxFile.AppleTags.TitleSansUnabridged);
@ -57,34 +60,38 @@ namespace AaxDecrypter
OnRetrievedNarrators(aaxFile.AppleTags.Narrator ?? "[unknown]"); OnRetrievedNarrators(aaxFile.AppleTags.Narrator ?? "[unknown]");
OnRetrievedCoverArt(aaxFile.AppleTags.Cover); OnRetrievedCoverArt(aaxFile.AppleTags.Cover);
return !isCanceled; return !IsCanceled;
} }
protected override bool Step2_DownloadAudiobookAsSingleFile() protected override bool Step2_DownloadAudiobookAsSingleFile()
{ {
var zeroProgress = Step2_Start(); var zeroProgress = Step2_Start();
if (File.Exists(outputFileName))
FileExt.SafeDelete(outputFileName);
var outputFile = File.Open(outputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite); if (File.Exists(OutputFileName))
FileExt.SafeDelete(OutputFileName);
var outputFile = File.Open(OutputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
aaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate; aaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
var decryptionResult = OutputFormat == OutputFormat.M4b ? aaxFile.ConvertToMp4a(outputFile, downloadLicense.ChapterInfo) : aaxFile.ConvertToMp3(outputFile); var decryptionResult = OutputFormat == OutputFormat.M4b ? aaxFile.ConvertToMp4a(outputFile, DownloadLicense.ChapterInfo) : aaxFile.ConvertToMp3(outputFile);
aaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate; aaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
downloadLicense.ChapterInfo = aaxFile.Chapters; DownloadLicense.ChapterInfo = aaxFile.Chapters;
Step2_End(zeroProgress); Step2_End(zeroProgress);
return decryptionResult == ConversionResult.NoErrorsDetected && !isCanceled; var success = decryptionResult == ConversionResult.NoErrorsDetected && !IsCanceled;
if (success)
base.OnFileCreated(OutputFileName);
return success;
} }
private bool Step2_DownloadAudiobookAsMultipleFilesPerChapter() private bool Step2_DownloadAudiobookAsMultipleFilesPerChapter()
{ {
var zeroProgress = Step2_Start(); var zeroProgress = Step2_Start();
var chapters = downloadLicense.ChapterInfo.Chapters.ToList(); var chapters = DownloadLicense.ChapterInfo.Chapters.ToList();
//Ensure split files are at least minChapterLength in duration. //Ensure split files are at least minChapterLength in duration.
var splitChapters = new ChapterInfo(); var splitChapters = new ChapterInfo();
@ -106,16 +113,25 @@ namespace AaxDecrypter
} }
} }
// reset, just in case
multiPartFilePaths.Clear();
aaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate; aaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
if(OutputFormat == OutputFormat.M4b) if (OutputFormat == OutputFormat.M4b)
ConvertToMultiMp4b(splitChapters); ConvertToMultiMp4b(splitChapters);
else else
ConvertToMultiMp3(splitChapters); ConvertToMultiMp3(splitChapters);
aaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate; aaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
Step2_End(zeroProgress); Step2_End(zeroProgress);
return !isCanceled; var success = !IsCanceled;
if (success)
foreach (var path in multiPartFilePaths)
OnFileCreated(path);
return success;
} }
private DownloadProgress Step2_Start() private DownloadProgress Step2_Start()
@ -129,10 +145,10 @@ namespace AaxDecrypter
OnDecryptProgressUpdate(zeroProgress); OnDecryptProgressUpdate(zeroProgress);
aaxFile.SetDecryptionKey(downloadLicense.AudibleKey, downloadLicense.AudibleIV); aaxFile.SetDecryptionKey(DownloadLicense.AudibleKey, DownloadLicense.AudibleIV);
return zeroProgress; return zeroProgress;
} }
private void Step2_End(DownloadProgress zeroProgress) private void Step2_End(DownloadProgress zeroProgress)
{ {
aaxFile.Close(); aaxFile.Close();
@ -147,19 +163,19 @@ namespace AaxDecrypter
var chapterCount = 0; var chapterCount = 0;
aaxFile.ConvertToMultiMp4a(splitChapters, newSplitCallback => aaxFile.ConvertToMultiMp4a(splitChapters, newSplitCallback =>
{ {
var fileName = GetMultipartFileName(outputFileName, ++chapterCount, newSplitCallback.Chapter.Title); var fileName = GetMultipartFileName(++chapterCount, newSplitCallback.Chapter.Title);
if (File.Exists(fileName)) if (File.Exists(fileName))
FileExt.SafeDelete(fileName); FileExt.SafeDelete(fileName);
newSplitCallback.OutputFile = File.Open(fileName, FileMode.OpenOrCreate); newSplitCallback.OutputFile = File.Open(fileName, FileMode.OpenOrCreate);
}); });
} }
private void ConvertToMultiMp3(ChapterInfo splitChapters) private void ConvertToMultiMp3(ChapterInfo splitChapters)
{ {
var chapterCount = 0; var chapterCount = 0;
aaxFile.ConvertToMultiMp3(splitChapters, newSplitCallback => aaxFile.ConvertToMultiMp3(splitChapters, newSplitCallback =>
{ {
var fileName = GetMultipartFileName(outputFileName, ++chapterCount, newSplitCallback.Chapter.Title); var fileName = GetMultipartFileName(++chapterCount, newSplitCallback.Chapter.Title);
if (File.Exists(fileName)) if (File.Exists(fileName))
FileExt.SafeDelete(fileName); FileExt.SafeDelete(fileName);
newSplitCallback.OutputFile = File.Open(fileName, FileMode.OpenOrCreate); newSplitCallback.OutputFile = File.Open(fileName, FileMode.OpenOrCreate);
@ -167,29 +183,21 @@ namespace AaxDecrypter
}); });
} }
private static string GetMultipartFileName(string baseFileName, int chapterCount, string chapterTitle) // return. cache name for event call at end of processing
private string GetMultipartFileName(int chapterCount, string chapterTitle)
{ {
string extension = Path.GetExtension(baseFileName); string extension = Path.GetExtension(OutputFileName);
var fileNameChars = $"{Path.GetFileNameWithoutExtension(baseFileName)} - {chapterCount:D2} - {chapterTitle}".ToCharArray(); var filenameBase = $"{Path.GetFileNameWithoutExtension(OutputFileName)} - {chapterCount:D2} - {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);
//Replace illegal path characters with spaces. multiPartFilePaths.Add(path);
for (int i = 0; i <fileNameChars.Length; i++)
{
foreach (var illegal in Path.GetInvalidFileNameChars())
{
if (fileNameChars[i] == illegal)
{
fileNameChars[i] = ' ';
break;
}
}
}
var fileName = new string(fileNameChars).Truncate(MAX_FILENAME_LENGTH - extension.Length); return path;
}
return Path.Combine(Path.GetDirectoryName(baseFileName), fileName + extension);
}
private void AaxFile_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e) private void AaxFile_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
{ {
@ -213,13 +221,10 @@ namespace AaxDecrypter
public override void Cancel() public override void Cancel()
{ {
isCanceled = true; IsCanceled = true;
aaxFile?.Cancel(); aaxFile?.Cancel();
aaxFile?.Dispose(); aaxFile?.Dispose();
CloseInputFileStream(); CloseInputFileStream();
} }
}
protected override int GetSpeedup(TimeSpan elapsed)
=> (int)(aaxFile.Duration.TotalSeconds / (long)elapsed.TotalSeconds);
}
} }

View File

@ -21,44 +21,39 @@ namespace AaxDecrypter
public event EventHandler<byte[]> RetrievedCoverArt; public event EventHandler<byte[]> RetrievedCoverArt;
public event EventHandler<DownloadProgress> DecryptProgressUpdate; public event EventHandler<DownloadProgress> DecryptProgressUpdate;
public event EventHandler<TimeSpan> DecryptTimeRemaining; public event EventHandler<TimeSpan> DecryptTimeRemaining;
public event EventHandler<string> FileCreated;
public string AppName { get; set; } protected bool IsCanceled { get; set; }
protected string OutputFileName { get; }
protected bool isCanceled { get; set; } protected string CacheDir { get; }
protected string outputFileName { get; } protected DownloadLicense DownloadLicense { get; }
protected string cacheDir { get; }
protected DownloadLicense downloadLicense { get; }
protected NetworkFileStream InputFileStream => (nfsPersister ??= OpenNetworkFileStream()).NetworkFileStream; protected NetworkFileStream InputFileStream => (nfsPersister ??= OpenNetworkFileStream()).NetworkFileStream;
protected abstract StepSequence steps { get; } protected abstract StepSequence Steps { get; }
private NetworkFileStreamPersister nfsPersister; private NetworkFileStreamPersister nfsPersister;
private string jsonDownloadState => Path.Combine(cacheDir, Path.GetFileNameWithoutExtension(outputFileName) + ".json"); private string jsonDownloadState => Path.Combine(CacheDir, Path.GetFileNameWithoutExtension(OutputFileName) + ".json");
private string tempFile => PathLib.ReplaceExtension(jsonDownloadState, ".tmp"); private string tempFile => PathLib.ReplaceExtension(jsonDownloadState, ".tmp");
public AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadLicense dlLic) public AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadLicense dlLic)
{ {
AppName = GetType().Name; OutputFileName = ArgumentValidator.EnsureNotNullOrWhiteSpace(outFileName, nameof(outFileName));
ArgumentValidator.EnsureNotNullOrWhiteSpace(outFileName, nameof(outFileName)); var outDir = Path.GetDirectoryName(OutputFileName);
outputFileName = outFileName;
var outDir = Path.GetDirectoryName(outputFileName);
if (!Directory.Exists(outDir)) if (!Directory.Exists(outDir))
throw new ArgumentNullException(nameof(outDir), "Directory does not exist"); throw new ArgumentNullException(nameof(outDir), "Directory does not exist");
if (File.Exists(outputFileName)) if (File.Exists(OutputFileName))
File.Delete(outputFileName); File.Delete(OutputFileName);
if (!Directory.Exists(cacheDirectory)) if (!Directory.Exists(cacheDirectory))
throw new ArgumentNullException(nameof(cacheDirectory), "Directory does not exist"); throw new ArgumentNullException(nameof(cacheDirectory), "Directory does not exist");
cacheDir = cacheDirectory; CacheDir = cacheDirectory;
downloadLicense = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic)); DownloadLicense = ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
} }
public abstract void Cancel(); public abstract void Cancel();
protected abstract int GetSpeedup(TimeSpan elapsed);
protected abstract bool Step2_DownloadAudiobookAsSingleFile(); protected abstract bool Step2_DownloadAudiobookAsSingleFile();
protected abstract bool Step1_GetMetadata(); protected abstract bool Step1_GetMetadata();
@ -72,7 +67,7 @@ namespace AaxDecrypter
public bool Run() public bool Run()
{ {
var (IsSuccess, Elapsed) = steps.Run(); var (IsSuccess, Elapsed) = Steps.Run();
if (!IsSuccess) if (!IsSuccess)
{ {
@ -80,7 +75,6 @@ namespace AaxDecrypter
return false; return false;
} }
//Serilog.Log.Logger.Information($"Speedup is {GetSpeedup(Elapsed)}x realtime.");
return true; return true;
} }
@ -94,8 +88,10 @@ namespace AaxDecrypter
=> RetrievedCoverArt?.Invoke(this, coverArt); => RetrievedCoverArt?.Invoke(this, coverArt);
protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress) protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress)
=> DecryptProgressUpdate?.Invoke(this, downloadProgress); => DecryptProgressUpdate?.Invoke(this, downloadProgress);
protected void OnDecryptTimeRemaining(TimeSpan timeRemaining) protected void OnDecryptTimeRemaining(TimeSpan timeRemaining)
=> DecryptTimeRemaining?.Invoke(this, timeRemaining); => DecryptTimeRemaining?.Invoke(this, timeRemaining);
protected void OnFileCreated(string path)
=> FileCreated?.Invoke(this, path);
protected void CloseInputFileStream() protected void CloseInputFileStream()
{ {
@ -108,57 +104,51 @@ namespace AaxDecrypter
// not a critical step. its failure should not prevent future steps from running // not a critical step. its failure should not prevent future steps from running
try try
{ {
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(outputFileName), downloadLicense.ChapterInfo)); File.WriteAllText(PathLib.ReplaceExtension(OutputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadLicense.ChapterInfo));
} }
catch (Exception ex) catch (Exception ex)
{ {
Serilog.Log.Logger.Error(ex, $"{nameof(Step3_CreateCue)}. FAILED"); Serilog.Log.Logger.Error(ex, $"{nameof(Step3_CreateCue)}. FAILED");
} }
return !isCanceled; return !IsCanceled;
} }
protected bool Step4_Cleanup() protected bool Step4_Cleanup()
{ {
FileExt.SafeDelete(jsonDownloadState); FileExt.SafeDelete(jsonDownloadState);
FileExt.SafeDelete(tempFile); FileExt.SafeDelete(tempFile);
return !isCanceled; return !IsCanceled;
} }
private NetworkFileStreamPersister OpenNetworkFileStream() private NetworkFileStreamPersister OpenNetworkFileStream()
{ {
NetworkFileStreamPersister nfsp; if (!File.Exists(jsonDownloadState))
return NewNetworkFilePersister();
if (File.Exists(jsonDownloadState)) try
{ {
try var nfsp = new NetworkFileStreamPersister(jsonDownloadState);
{ // If More than ~1 hour has elapsed since getting the download url, it will expire.
nfsp = new NetworkFileStreamPersister(jsonDownloadState); // The new url will be to the same file.
//If More than ~1 hour has elapsed since getting the download url, it will expire. nfsp.NetworkFileStream.SetUriForSameFile(new Uri(DownloadLicense.DownloadUrl));
//The new url will be to the same file. return nfsp;
nfsp.NetworkFileStream.SetUriForSameFile(new Uri(downloadLicense.DownloadUrl));
}
catch
{
FileExt.SafeDelete(jsonDownloadState);
FileExt.SafeDelete(tempFile);
nfsp = NewNetworkFilePersister();
}
} }
else catch
{ {
nfsp = NewNetworkFilePersister(); FileExt.SafeDelete(jsonDownloadState);
FileExt.SafeDelete(tempFile);
return NewNetworkFilePersister();
} }
return nfsp;
} }
private NetworkFileStreamPersister NewNetworkFilePersister() private NetworkFileStreamPersister NewNetworkFilePersister()
{ {
var headers = new System.Net.WebHeaderCollection var headers = new System.Net.WebHeaderCollection
{ {
{ "User-Agent", downloadLicense.UserAgent } { "User-Agent", DownloadLicense.UserAgent }
}; };
var networkFileStream = new NetworkFileStream(tempFile, new Uri(downloadLicense.DownloadUrl), 0, headers); var networkFileStream = new NetworkFileStream(tempFile, new Uri(DownloadLicense.DownloadUrl), 0, headers);
return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState); return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
} }
} }

View File

@ -10,13 +10,12 @@ namespace AaxDecrypter
{ {
public class UnencryptedAudiobookDownloader : AudiobookDownloadBase public class UnencryptedAudiobookDownloader : AudiobookDownloadBase
{ {
protected override StepSequence steps { get; } protected override StepSequence Steps { get; }
public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, DownloadLicense dlLic) public UnencryptedAudiobookDownloader(string outFileName, string cacheDirectory, DownloadLicense dlLic)
: base(outFileName, cacheDirectory, dlLic) : base(outFileName, cacheDirectory, dlLic)
{ {
Steps = new StepSequence
steps = new StepSequence
{ {
Name = "Download Mp3 Audiobook", Name = "Download Mp3 Audiobook",
@ -29,21 +28,15 @@ namespace AaxDecrypter
public override void Cancel() public override void Cancel()
{ {
isCanceled = true; IsCanceled = true;
CloseInputFileStream(); CloseInputFileStream();
} }
protected override int GetSpeedup(TimeSpan elapsed)
{
//Not implemented
return 0;
}
protected override bool Step1_GetMetadata() protected override bool Step1_GetMetadata()
{ {
OnRetrievedCoverArt(null); OnRetrievedCoverArt(null);
return !isCanceled; return !IsCanceled;
} }
protected override bool Step2_DownloadAudiobookAsSingleFile() protected override bool Step2_DownloadAudiobookAsSingleFile()
@ -75,12 +68,13 @@ namespace AaxDecrypter
CloseInputFileStream(); CloseInputFileStream();
if (File.Exists(outputFileName)) if (File.Exists(OutputFileName))
FileExt.SafeDelete(outputFileName); FileExt.SafeDelete(OutputFileName);
FileExt.SafeMove(InputFileStream.SaveFilePath, outputFileName); FileExt.SafeMove(InputFileStream.SaveFilePath, OutputFileName);
OnFileCreated(OutputFileName);
return !isCanceled; return !IsCanceled;
} }
} }
} }

View File

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<Version>6.2.3.0</Version> <Version>6.2.4.0</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -55,6 +55,7 @@ namespace FileLiberator
var mp3Path = Mp3FileName(m4bPath); var mp3Path = Mp3FileName(m4bPath);
FileExt.SafeMove(mp3File.Name, mp3Path); FileExt.SafeMove(mp3File.Name, mp3Path);
OnFileCreated(libraryBook.Book.AudibleProductId, FileManager.FileType.Audio, mp3Path);
var statusHandler = new StatusHandler(); var statusHandler = new StatusHandler();

View File

@ -25,16 +25,16 @@ namespace FileLiberator
if (libraryBook.Book.Audio_Exists) if (libraryBook.Book.Audio_Exists)
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" }; return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
var outputAudioFilename = await downloadAudiobookAsync(AudibleFileStorage.DownloadsInProgress, AudibleFileStorage.DecryptInProgress, libraryBook); var outputAudioFilename = await downloadAudiobookAsync(libraryBook);
// decrypt failed // decrypt failed
if (outputAudioFilename is null) if (outputAudioFilename is null)
return new StatusHandler { "Decrypt failed" }; return new StatusHandler { "Decrypt failed" };
// moves files and returns dest dir // moves new files from temp dir to final dest
var moveResults = MoveFilesToBooksDir(libraryBook.Book, outputAudioFilename); var movedAudioFile = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
if (!moveResults.movedAudioFile) if (!movedAudioFile)
return new StatusHandler { "Cannot find final audio file after decryption" }; return new StatusHandler { "Cannot find final audio file after decryption" };
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated; libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
@ -47,7 +47,7 @@ namespace FileLiberator
} }
} }
private async Task<string> downloadAudiobookAsync(string cacheDir, string destinationDir, LibraryBook libraryBook) private async Task<string> downloadAudiobookAsync(LibraryBook libraryBook)
{ {
OnStreamingBegin($"Begin decrypting {libraryBook}"); OnStreamingBegin($"Begin decrypting {libraryBook}");
@ -82,18 +82,20 @@ namespace FileLiberator
audiobookDlLic.ChapterInfo.AddChapter(chap.Title, TimeSpan.FromMilliseconds(chap.LengthMs)); audiobookDlLic.ChapterInfo.AddChapter(chap.Title, TimeSpan.FromMilliseconds(chap.LengthMs));
} }
var outFileName = Path.Combine(destinationDir, $"{PathLib.ToPathSafeString(libraryBook.Book.Title)} [{libraryBook.Book.AudibleProductId}].{outputFormat.ToString().ToLower()}"); var outFileName = Path.Combine(AudibleFileStorage.DecryptInProgress, $"{PathLib.ToPathSafeString(libraryBook.Book.Title)} [{libraryBook.Book.AudibleProductId}].{outputFormat.ToString().ToLower()}");
var cacheDir = AudibleFileStorage.DownloadsInProgress;
abDownloader = contentLic.DrmType == AudibleApi.Common.DrmType.Adrm abDownloader = contentLic.DrmType == AudibleApi.Common.DrmType.Adrm
? new AaxcDownloadConverter(outFileName, cacheDir, audiobookDlLic, outputFormat, Configuration.Instance.SplitFilesByChapter) ? new AaxcDownloadConverter(outFileName, cacheDir, audiobookDlLic, outputFormat, Configuration.Instance.SplitFilesByChapter)
: new UnencryptedAudiobookDownloader(outFileName, cacheDir, audiobookDlLic); : new UnencryptedAudiobookDownloader(outFileName, cacheDir, audiobookDlLic);
abDownloader.AppName = "Libation";
abDownloader.DecryptProgressUpdate += (_, progress) => OnStreamingProgressChanged(progress); abDownloader.DecryptProgressUpdate += (_, progress) => OnStreamingProgressChanged(progress);
abDownloader.DecryptTimeRemaining += (_, remaining) => OnStreamingTimeRemaining(remaining); abDownloader.DecryptTimeRemaining += (_, remaining) => OnStreamingTimeRemaining(remaining);
abDownloader.RetrievedTitle += (_, title) => OnTitleDiscovered(title); abDownloader.RetrievedTitle += (_, title) => OnTitleDiscovered(title);
abDownloader.RetrievedAuthors += (_, authors) => OnAuthorsDiscovered(authors); abDownloader.RetrievedAuthors += (_, authors) => OnAuthorsDiscovered(authors);
abDownloader.RetrievedNarrators += (_, narrators) => OnNarratorsDiscovered(narrators); abDownloader.RetrievedNarrators += (_, narrators) => OnNarratorsDiscovered(narrators);
abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt; abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook.Book.AudibleProductId, FileType.Audio, path);
// REAL WORK DONE HERE // REAL WORK DONE HERE
var success = await Task.Run(abDownloader.Run); var success = await Task.Run(abDownloader.Run);
@ -123,41 +125,46 @@ namespace FileLiberator
} }
} }
private static (string destinationDir, bool movedAudioFile) MoveFilesToBooksDir(Book product, string outputAudioFilename) /// <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 // create final directory. move each file into it. MOVE AUDIO FILE LAST
// new dir: safetitle_limit50char + " [" + productId + "]" // new dir: safetitle_limit50char + " [" + productId + "]"
// TODO make this method handle multiple audio files or a single audio file. // TODO make this method handle multiple audio files or a single audio file.
var destinationDir = AudibleFileStorage.Audio.GetDestDir(product.Title, product.AudibleProductId); var destinationDir = AudibleFileStorage.Audio.GetDestDir(book.Title, book.AudibleProductId);
Directory.CreateDirectory(destinationDir); Directory.CreateDirectory(destinationDir);
var sortedFiles = getProductFilesSorted(product, outputAudioFilename); var sortedFiles = getProductFilesSorted(book, outputAudioFilename);
var musicFileExt = Path.GetExtension(outputAudioFilename).Trim('.'); var musicFileExt = Path.GetExtension(outputAudioFilename).Trim('.');
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext // audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
var audioFileName = FileUtility.GetValidFilename(destinationDir, product.Title, musicFileExt, product.AudibleProductId); var audioFileName = FileUtility.GetValidFilename(destinationDir, book.Title, musicFileExt, book.AudibleProductId);
bool movedAudioFile = false; bool movedAudioFile = false;
foreach (var f in sortedFiles) foreach (var f in sortedFiles)
{ {
var isAudio = AudibleFileStorage.Audio.IsFileTypeMatch(f);
var dest var dest
= AudibleFileStorage.Audio.IsFileTypeMatch(f)//f.Extension.Equals($".{musicFileExt}", StringComparison.OrdinalIgnoreCase) = isAudio
? Path.Join(destinationDir, f.Name) ? Path.Join(destinationDir, f.Name)
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext + "]." + non_audio_ext // non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext + "]." + non_audio_ext
: FileUtility.GetValidFilename(destinationDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt); : FileUtility.GetValidFilename(destinationDir, book.Title, f.Extension, book.AudibleProductId, musicFileExt);
if (Path.GetExtension(dest).Trim('.').ToLower() == "cue") if (Path.GetExtension(dest).Trim('.').ToLower() == "cue")
Cue.UpdateFileName(f, audioFileName); Cue.UpdateFileName(f, audioFileName);
File.Move(f.FullName, dest); File.Move(f.FullName, dest);
if (isAudio)
FilePathCache.Upsert(book.AudibleProductId, FileType.Audio, dest);
movedAudioFile |= AudibleFileStorage.Audio.IsFileTypeMatch(f); movedAudioFile |= AudibleFileStorage.Audio.IsFileTypeMatch(f);
} }
AudibleFileStorage.Audio.Refresh(); AudibleFileStorage.Audio.Refresh();
return (destinationDir, movedAudioFile); return movedAudioFile;
} }
private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename) private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename)

View File

@ -20,6 +20,7 @@ namespace FileLiberator
try try
{ {
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress); var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
OnFileCreated("Upgrade", FileManager.FileType.Zip, actualDownloadedFilePath);
return actualDownloadedFilePath; return actualDownloadedFilePath;
} }
finally finally

View File

@ -72,6 +72,8 @@ namespace FileLiberator
var client = new HttpClient(); var client = new HttpClient();
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress); var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
OnFileCreated(libraryBook.Book.AudibleProductId, FileType.PDF, actualDownloadedFilePath);
OnStatusUpdate(actualDownloadedFilePath); OnStatusUpdate(actualDownloadedFilePath);
return actualDownloadedFilePath; return actualDownloadedFilePath;
} }

View File

@ -9,6 +9,8 @@ namespace FileLiberator
public event EventHandler<DownloadProgress> StreamingProgressChanged; public event EventHandler<DownloadProgress> StreamingProgressChanged;
public event EventHandler<TimeSpan> StreamingTimeRemaining; public event EventHandler<TimeSpan> StreamingTimeRemaining;
public event EventHandler<string> StreamingCompleted; 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;
protected void OnStreamingBegin(string filePath) protected void OnStreamingBegin(string filePath)
{ {
@ -30,8 +32,13 @@ namespace FileLiberator
{ {
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(StreamingCompleted), Message = filePath }); Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(StreamingCompleted), Message = filePath });
StreamingCompleted?.Invoke(this, filePath); StreamingCompleted?.Invoke(this, filePath);
}
//TODO: Update file cache protected void OnFileCreated(string productId, FileManager.FileType type, 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));
} }
} }
} }

View File

@ -8,7 +8,7 @@ using Dinah.Core.Collections.Generic;
namespace FileManager namespace FileManager
{ {
public enum FileType { Unknown, Audio, AAXC, PDF } public enum FileType { Unknown, Audio, AAXC, PDF, Zip }
public abstract class AudibleFileStorage : Enumeration<AudibleFileStorage> public abstract class AudibleFileStorage : Enumeration<AudibleFileStorage>
{ {
@ -66,16 +66,9 @@ namespace FileManager
if (StorageDirectory == BooksDirectory) if (StorageDirectory == BooksDirectory)
{ {
//If user changed the BooksDirectory, reinitialize. //If user changed the BooksDirectory, reinitialize.
if (StorageDirectory != BookDirectoryFiles.RootDirectory) lock (bookDirectoryFilesLocker)
{ if (StorageDirectory != BookDirectoryFiles.RootDirectory)
lock (bookDirectoryFilesLocker) BookDirectoryFiles = new BackgroundFileSystem(StorageDirectory, "*.*", SearchOption.AllDirectories);
{
if (StorageDirectory != BookDirectoryFiles.RootDirectory)
{
BookDirectoryFiles = new BackgroundFileSystem(StorageDirectory, "*.*", SearchOption.AllDirectories);
}
}
}
firstOrNull = BookDirectoryFiles.FindFile(regex); firstOrNull = BookDirectoryFiles.FindFile(regex);
} }
@ -87,10 +80,9 @@ namespace FileManager
.FirstOrDefault(s => regex.IsMatch(s)); .FirstOrDefault(s => regex.IsMatch(s));
} }
if (firstOrNull is null) if (firstOrNull is not null)
return null; FilePathCache.Upsert(productId, FileType, firstOrNull);
FilePathCache.Upsert(productId, FileType, firstOrNull);
return firstOrNull; return firstOrNull;
} }
#endregion #endregion

View File

@ -56,9 +56,15 @@ namespace FileManager
} }
public static void Upsert(string id, FileType type, string path) public static void Upsert(string id, FileType type, string path)
{ {
if (!File.Exists(path)) if (!File.Exists(path))
throw new FileNotFoundException("Cannot add path to cache. File not found"); {
// 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);
}
var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type); var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type);