New and moved files Upsert themselves in FileManager.FilePathCache
This commit is contained in:
parent
18cca53968
commit
d0b78cc501
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user