Merge pull request #32 from Mbucari/master
Changes we discussed, plus a few more.
This commit is contained in:
commit
2146ebff29
@ -5,9 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="taglib-sharp">
|
||||
<HintPath>lib\taglib-sharp.dll</HintPath>
|
||||
</Reference>
|
||||
<PackageReference Include="TagLibSharp" Version="2.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -42,9 +40,6 @@
|
||||
<None Update="DecryptLib\swscale-5.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\taglib-sharp.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -10,51 +10,54 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public interface ISimpleAaxToM4bConverter
|
||||
public interface ISimpleAaxcToM4bConverter
|
||||
{
|
||||
event EventHandler<AaxcTagLibFile> RetrievedTags;
|
||||
event EventHandler<byte[]> RetrievedCoverArt;
|
||||
event EventHandler<TimeSpan> DecryptTimeRemaining;
|
||||
event EventHandler<int> DecryptProgressUpdate;
|
||||
bool Run();
|
||||
string AppName { get; set; }
|
||||
string outDir { get; }
|
||||
string outputFileName { get; }
|
||||
ChapterInfo chapters { get; }
|
||||
AaxcTagLibFile aaxcTagLib { get; }
|
||||
byte[] coverArt { get; }
|
||||
void SetCoverArt(byte[] coverArt);
|
||||
void SetOutputFilename(string outFileName);
|
||||
string Title { get; }
|
||||
string Author { get; }
|
||||
string Narrator { get; }
|
||||
byte[] CoverArt { get; }
|
||||
}
|
||||
public interface IAdvancedAaxcToM4bConverter : ISimpleAaxToM4bConverter
|
||||
public interface IAdvancedAaxcToM4bConverter : ISimpleAaxcToM4bConverter
|
||||
{
|
||||
void Cancel();
|
||||
bool Step1_CreateDir();
|
||||
bool Step2_DownloadAndCombine();
|
||||
bool Step3_RestoreMetadata();
|
||||
bool Step4_CreateCue();
|
||||
bool Step5_CreateNfo();
|
||||
bool Step2_GetMetadata();
|
||||
bool Step3_DownloadAndCombine();
|
||||
bool Step4_RestoreMetadata();
|
||||
bool Step5_CreateCue();
|
||||
bool Step6_CreateNfo();
|
||||
}
|
||||
public class AaxcDownloadConverter : IAdvancedAaxcToM4bConverter
|
||||
{
|
||||
public event EventHandler<AaxcTagLibFile> RetrievedTags;
|
||||
public event EventHandler<byte[]> RetrievedCoverArt;
|
||||
public event EventHandler<int> DecryptProgressUpdate;
|
||||
public event EventHandler<TimeSpan> DecryptTimeRemaining;
|
||||
public string AppName { get; set; } = nameof(AaxcDownloadConverter);
|
||||
public string outDir { get; private set; }
|
||||
public string outputFileName { get; private set; }
|
||||
public ChapterInfo chapters { get; private set; }
|
||||
public string Title => aaxcTagLib.Tag.Title.Replace(" (Unabridged)", "");
|
||||
public string Author => aaxcTagLib.Tag.FirstPerformer ?? "[unknown]";
|
||||
public string Narrator => aaxcTagLib.GetTag(TagLib.TagTypes.Apple).Narrator;
|
||||
public byte[] CoverArt => aaxcTagLib.Tag.Pictures.Length > 0 ? aaxcTagLib.Tag.Pictures[0].Data.Data : default;
|
||||
public AaxcTagLibFile aaxcTagLib { get; private set; }
|
||||
public byte[] coverArt { get; private set; }
|
||||
|
||||
private TagLib.Mpeg4.File aaxcTagLib { get; set; }
|
||||
private StepSequence steps { get; }
|
||||
private DownloadLicense downloadLicense { get; set; }
|
||||
private FFMpegAaxcProcesser aaxcProcesser;
|
||||
private bool isCanceled { get; set; }
|
||||
|
||||
public static async Task<AaxcDownloadConverter> CreateAsync(string outDirectory, DownloadLicense dlLic, ChapterInfo chapters = null)
|
||||
public static AaxcDownloadConverter Create(string outDirectory, DownloadLicense dlLic, ChapterInfo chapters = null)
|
||||
{
|
||||
var converter = new AaxcDownloadConverter(outDirectory, dlLic, chapters);
|
||||
await converter.prelimProcessing();
|
||||
converter.SetOutputFilename(Path.GetTempFileName());
|
||||
return converter;
|
||||
}
|
||||
|
||||
@ -72,35 +75,20 @@ namespace AaxDecrypter
|
||||
Name = "Convert Aax To M4b",
|
||||
|
||||
["Step 1: Create Dir"] = Step1_CreateDir,
|
||||
["Step 2: Download and Combine Audiobook"] = Step2_DownloadAndCombine,
|
||||
["Step 3: Restore Aaxc Metadata"] = Step3_RestoreMetadata,
|
||||
["Step 4: Create Cue"] = Step4_CreateCue,
|
||||
["Step 5: Create Nfo"] = Step5_CreateNfo,
|
||||
["Step 2: Get Aaxc Metadata"] = Step2_GetMetadata,
|
||||
["Step 3: Download Decrypted Audiobook"] = Step3_DownloadAndCombine,
|
||||
["Step 4: Restore Aaxc Metadata"] = Step4_RestoreMetadata,
|
||||
["Step 5: Create Cue"] = Step5_CreateCue,
|
||||
["Step 6: Create Nfo"] = Step6_CreateNfo,
|
||||
};
|
||||
|
||||
aaxcProcesser = new FFMpegAaxcProcesser(dlLic);
|
||||
aaxcProcesser.ProgressUpdate += AaxcProcesser_ProgressUpdate;
|
||||
|
||||
downloadLicense = dlLic;
|
||||
this.chapters = chapters;
|
||||
}
|
||||
|
||||
private async Task prelimProcessing()
|
||||
{
|
||||
//Get metadata from the file over http
|
||||
var client = new System.Net.Http.HttpClient();
|
||||
client.DefaultRequestHeaders.Add("User-Agent", downloadLicense.UserAgent);
|
||||
|
||||
var networkFile = await NetworkFileAbstraction.CreateAsync(client, new Uri(downloadLicense.DownloadUrl));
|
||||
|
||||
aaxcTagLib = await Task.Run(() => TagLib.File.Create(networkFile, "audio/mp4", TagLib.ReadStyle.Average) as TagLib.Mpeg4.File);
|
||||
|
||||
var defaultFilename = Path.Combine(
|
||||
outDir,
|
||||
PathLib.ToPathSafeString(aaxcTagLib.Tag.FirstPerformer??"[unknown]"),
|
||||
PathLib.ToPathSafeString(aaxcTagLib.Tag.Title.Replace(" (Unabridged)", "")) + ".m4b"
|
||||
);
|
||||
|
||||
SetOutputFilename(defaultFilename);
|
||||
}
|
||||
|
||||
public void SetOutputFilename(string outFileName)
|
||||
{
|
||||
outputFileName = PathLib.ReplaceExtension(outFileName, ".m4b");
|
||||
@ -110,6 +98,14 @@ namespace AaxDecrypter
|
||||
File.Delete(outputFileName);
|
||||
}
|
||||
|
||||
public void SetCoverArt(byte[] coverArt)
|
||||
{
|
||||
if (coverArt is null) return;
|
||||
|
||||
this.coverArt = coverArt;
|
||||
RetrievedCoverArt?.Invoke(this, coverArt);
|
||||
}
|
||||
|
||||
public bool Run()
|
||||
{
|
||||
var (IsSuccess, Elapsed) = steps.Run();
|
||||
@ -131,14 +127,32 @@ namespace AaxDecrypter
|
||||
ProcessRunner.WorkingDir = outDir;
|
||||
Directory.CreateDirectory(outDir);
|
||||
|
||||
return true;
|
||||
return !isCanceled;
|
||||
}
|
||||
|
||||
public bool Step2_DownloadAndCombine()
|
||||
public bool Step2_GetMetadata()
|
||||
{
|
||||
aaxcProcesser = new FFMpegAaxcProcesser(downloadLicense);
|
||||
aaxcProcesser.ProgressUpdate += AaxcProcesser_ProgressUpdate;
|
||||
//Get metadata from the file over http
|
||||
var client = new System.Net.Http.HttpClient();
|
||||
client.DefaultRequestHeaders.Add("User-Agent", downloadLicense.UserAgent);
|
||||
var networkFile = NetworkFileAbstraction.CreateAsync(client, new Uri(downloadLicense.DownloadUrl)).GetAwaiter().GetResult();
|
||||
|
||||
aaxcTagLib = new AaxcTagLibFile(networkFile);
|
||||
|
||||
if (coverArt is null && aaxcTagLib.AppleTags.Pictures.Length > 0)
|
||||
{
|
||||
coverArt = aaxcTagLib.AppleTags.Pictures[0].Data.Data;
|
||||
}
|
||||
|
||||
RetrievedTags?.Invoke(this, aaxcTagLib);
|
||||
RetrievedCoverArt?.Invoke(this, coverArt);
|
||||
|
||||
return !isCanceled;
|
||||
}
|
||||
|
||||
public bool Step3_DownloadAndCombine()
|
||||
{
|
||||
DecryptProgressUpdate?.Invoke(this, int.MaxValue);
|
||||
bool userSuppliedChapters = chapters != null;
|
||||
|
||||
string metadataPath = null;
|
||||
@ -165,115 +179,56 @@ namespace AaxDecrypter
|
||||
|
||||
DecryptProgressUpdate?.Invoke(this, 0);
|
||||
|
||||
return aaxcProcesser.Succeeded;
|
||||
return aaxcProcesser.Succeeded && !isCanceled;
|
||||
}
|
||||
|
||||
private void AaxcProcesser_ProgressUpdate(object sender, TimeSpan e)
|
||||
private void AaxcProcesser_ProgressUpdate(object sender, AaxcProcessUpdate e)
|
||||
{
|
||||
double averageRate = getAverageProcessRate(e);
|
||||
double remainingSecsToProcess = (aaxcTagLib.Properties.Duration - e).TotalSeconds;
|
||||
double estTimeRemaining = remainingSecsToProcess / averageRate;
|
||||
double remainingSecsToProcess = (aaxcTagLib.Properties.Duration - e.ProcessPosition).TotalSeconds;
|
||||
double estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
|
||||
|
||||
if (double.IsNormal(estTimeRemaining))
|
||||
DecryptTimeRemaining?.Invoke(this, TimeSpan.FromSeconds(estTimeRemaining));
|
||||
|
||||
double progressPercent = 100 * e.TotalSeconds / aaxcTagLib.Properties.Duration.TotalSeconds;
|
||||
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / aaxcTagLib.Properties.Duration.TotalSeconds;
|
||||
|
||||
DecryptProgressUpdate?.Invoke(this, (int)progressPercent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the average processing rate based on the last 2 to <see cref="MAX_NUM_AVERAGE"/> samples.
|
||||
/// </summary>
|
||||
/// <param name="lastProcessedPosition">Position in the audio file last processed</param>
|
||||
/// <returns>The average processing rate, in book_duration_seconds / second.</returns>
|
||||
private double getAverageProcessRate(TimeSpan lastProcessedPosition)
|
||||
{
|
||||
streamPositions.Enqueue(new StreamPosition
|
||||
{
|
||||
ProcessPosition = lastProcessedPosition,
|
||||
EventTime = DateTime.Now,
|
||||
});
|
||||
|
||||
if (streamPositions.Count < 2)
|
||||
return double.PositiveInfinity;
|
||||
|
||||
//Calculate the harmonic mean of the last 2 to MAX_NUM_AVERAGE progress updates
|
||||
//Units are Book_Duration_Seconds / second
|
||||
|
||||
var lastPos = streamPositions.Count > MAX_NUM_AVERAGE ? streamPositions.Dequeue() : null;
|
||||
|
||||
double harmonicDenominator = 0;
|
||||
int harmonicNumerator = 0;
|
||||
|
||||
foreach (var pos in streamPositions)
|
||||
{
|
||||
if (lastPos is null)
|
||||
{
|
||||
lastPos = pos;
|
||||
continue;
|
||||
}
|
||||
double dP = (pos.ProcessPosition - lastPos.ProcessPosition).TotalSeconds;
|
||||
double dT = (pos.EventTime - lastPos.EventTime).TotalSeconds;
|
||||
|
||||
harmonicDenominator += dT / dP;
|
||||
harmonicNumerator++;
|
||||
lastPos = pos;
|
||||
}
|
||||
|
||||
double harmonicMean = harmonicNumerator / harmonicDenominator;
|
||||
return harmonicMean;
|
||||
}
|
||||
|
||||
private const int MAX_NUM_AVERAGE = 15;
|
||||
private class StreamPosition
|
||||
{
|
||||
public TimeSpan ProcessPosition { get; set; }
|
||||
public DateTime EventTime { get; set; }
|
||||
}
|
||||
|
||||
private Queue<StreamPosition> streamPositions = new Queue<StreamPosition>();
|
||||
|
||||
/// <summary>
|
||||
/// Copy all aacx metadata to m4b file, including cover art.
|
||||
/// </summary>
|
||||
public bool Step3_RestoreMetadata()
|
||||
public bool Step4_RestoreMetadata()
|
||||
{
|
||||
var outFile = new TagLib.Mpeg4.File(outputFileName, TagLib.ReadStyle.Average);
|
||||
var outFile = new AaxcTagLibFile(outputFileName);
|
||||
outFile.CopyTagsFrom(aaxcTagLib);
|
||||
|
||||
var destTags = outFile.GetTag(TagLib.TagTypes.Apple) as TagLib.Mpeg4.AppleTag;
|
||||
destTags.Clear();
|
||||
|
||||
var sourceTag = aaxcTagLib.GetTag(TagLib.TagTypes.Apple) as TagLib.Mpeg4.AppleTag;
|
||||
|
||||
//copy all metadata fields in the source file, even those that TagLib doesn't
|
||||
//recognize, to the output file.
|
||||
//NOTE: Chapters aren't stored in MPEG-4 metadata. They are encoded as a Timed
|
||||
//Text Stream (MPEG-4 Part 17), so taglib doesn't read or write them.
|
||||
foreach (var stag in sourceTag)
|
||||
if (outFile.AppleTags.Pictures.Length == 0 && coverArt is not null)
|
||||
{
|
||||
destTags.SetData(stag.BoxType, stag.Children.Cast<TagLib.Mpeg4.AppleDataBox>().ToArray());
|
||||
outFile.AddPicture(coverArt);
|
||||
}
|
||||
|
||||
outFile.Save();
|
||||
|
||||
return true;
|
||||
return !isCanceled;
|
||||
}
|
||||
|
||||
public bool Step4_CreateCue()
|
||||
public bool Step5_CreateCue()
|
||||
{
|
||||
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(outputFileName), chapters));
|
||||
return true;
|
||||
return !isCanceled;
|
||||
}
|
||||
|
||||
public bool Step5_CreateNfo()
|
||||
public bool Step6_CreateNfo()
|
||||
{
|
||||
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".nfo"), NFO.CreateContents(AppName, aaxcTagLib, chapters));
|
||||
return true;
|
||||
return !isCanceled;
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
aaxcProcesser?.Cancel();
|
||||
isCanceled = true;
|
||||
aaxcProcesser.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
165
AaxDecrypter/AaxcTagLibFile.cs
Normal file
165
AaxDecrypter/AaxcTagLibFile.cs
Normal file
@ -0,0 +1,165 @@
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using TagLib;
|
||||
using TagLib.Mpeg4;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class AaxcTagLibFile : TagLib.Mpeg4.File
|
||||
{
|
||||
public AppleTag AppleTags => GetTag(TagTypes.Apple) as AppleTag;
|
||||
|
||||
private static ReadOnlyByteVector narratorType = new ReadOnlyByteVector(0xa9, (byte)'n', (byte)'r', (byte)'t');
|
||||
private static ReadOnlyByteVector descriptionType = new ReadOnlyByteVector(0xa9, (byte)'d', (byte)'e', (byte)'s');
|
||||
private static ReadOnlyByteVector publisherType = new ReadOnlyByteVector(0xa9, (byte)'p', (byte)'u', (byte)'b');
|
||||
|
||||
public string AsciiTitleSansUnabridged => TitleSansUnabridged is not null? unicodeToAscii(TitleSansUnabridged) : default;
|
||||
public string AsciiFirstAuthor => FirstAuthor is not null? unicodeToAscii(FirstAuthor) : default;
|
||||
public string AsciiNarrator => Narrator is not null ? unicodeToAscii(Narrator) : default;
|
||||
public string AsciiComment => Comment is not null ? unicodeToAscii(Comment) : default;
|
||||
public string AsciiLongDescription => LongDescription is not null ? unicodeToAscii(LongDescription) : default;
|
||||
|
||||
public string Comment => AppleTags.Comment;
|
||||
public string[] Authors => AppleTags.Performers;
|
||||
public string FirstAuthor => Authors?.Length > 0 ? Authors[0] : default;
|
||||
public string TitleSansUnabridged => AppleTags.Title?.Replace(" (Unabridged)", "");
|
||||
public string BookCopyright => _copyright is not null && _copyright.Length > 0 ? _copyright[0] : default;
|
||||
public string RecordingCopyright => _copyright is not null && _copyright.Length > 1 ? _copyright[1] : default;
|
||||
public string Narrator
|
||||
{
|
||||
get
|
||||
{
|
||||
string[] text = AppleTags.GetText(narratorType);
|
||||
return text.Length == 0 ? default : text[0];
|
||||
}
|
||||
}
|
||||
public string LongDescription
|
||||
{
|
||||
get
|
||||
{
|
||||
string[] text = AppleTags.GetText(descriptionType);
|
||||
return text.Length == 0 ? default : text[0];
|
||||
}
|
||||
}
|
||||
public string ReleaseDate
|
||||
{
|
||||
get
|
||||
{
|
||||
string[] text = AppleTags.GetText("rldt");
|
||||
return text.Length == 0 ? default : text[0];
|
||||
}
|
||||
}
|
||||
public string Publisher
|
||||
{
|
||||
get
|
||||
{
|
||||
string[] text = AppleTags.GetText(publisherType);
|
||||
return text.Length == 0 ? default : text[0];
|
||||
}
|
||||
}
|
||||
|
||||
private string[] _copyright => AppleTags.Copyright?.Replace("©", string.Empty)?.Replace("(P)", string.Empty)?.Split(';');
|
||||
public AaxcTagLibFile(IFileAbstraction abstraction)
|
||||
: base(abstraction, ReadStyle.Average)
|
||||
{
|
||||
}
|
||||
|
||||
public AaxcTagLibFile(string path)
|
||||
: this(new LocalFileAbstraction(path))
|
||||
{
|
||||
}
|
||||
/// <summary>
|
||||
/// Copy all metadata fields in the source file, even those that TagLib doesn't
|
||||
/// recognize, to the output file.
|
||||
/// NOTE: Chapters aren't stored in MPEG-4 metadata. They are encoded as a Timed
|
||||
/// Text Stream (MPEG-4 Part 17), so taglib doesn't read or write them.
|
||||
/// </summary>
|
||||
/// <param name="sourceFile">File from which tags will be coppied.</param>
|
||||
public void CopyTagsFrom(AaxcTagLibFile sourceFile)
|
||||
{
|
||||
AppleTags.Clear();
|
||||
|
||||
foreach (var stag in sourceFile.AppleTags)
|
||||
{
|
||||
AppleTags.SetData(stag.BoxType, stag.Children.Cast<AppleDataBox>().ToArray());
|
||||
}
|
||||
}
|
||||
public void AddPicture(byte[] coverArt)
|
||||
{
|
||||
AppleTags.SetData("covr", coverArt, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to convert unicode characters to an approximately equal ASCII character.
|
||||
/// </summary>
|
||||
private string unicodeToAscii(string unicodeStr)
|
||||
{
|
||||
//Accents
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[éèëêð]", "e");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ÉÈËÊ]", "E");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[àâä]", "a");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ÀÁÂÃÄÅ]", "A");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[àáâãäå]", "a");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ÙÚÛÜ]", "U");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ùúûüµ]", "u");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[òóôõöø]", "o");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ÒÓÔÕÖØ]", "O");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ìíîï]", "i");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ÌÍÎÏ]", "I");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[š]", "s");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[Š]", "S");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ñ]", "n");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[Ñ]", "N");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ç]", "c");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[Ç]", "C");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ÿ]", "y");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[Ÿ]", "Y");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ž]", "z");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[Ž]", "Z");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[Ð]", "D");
|
||||
|
||||
//Ligatures
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[œ]", "oe");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[Œ]", "Oe");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ꜳ]", "aa");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[Ꜳ]", "AA");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[æ]", "ae");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[Æ]", "AE");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ꜵ]", "ao");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[Ꜵ]", "AO");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ꜷ]", "au");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[Ꜷ]", "AU");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[«»ꜹꜻ]", "av");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[«»ꜸꜺ]", "AV");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[🙰]", "et");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ff]", "ff");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ffi]", "ffi");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ffl]", "ffl");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[fi]", "fi");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[fl]", "fl");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ƕ]", "hv");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[Ƕ]", "Hv");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[℔]", "lb");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ꝏ]", "oo");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[Ꝏ]", "OO");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[st]", "st");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ꜩ]", "tz");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[Ꜩ]", "TZ");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ᵫ]", "ue");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[ꭣ]", "uo");
|
||||
|
||||
//Punctuation
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[«»\u2018\u2019\u201A\u201B\u2032\u2035]", "\'");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[«»\u201C\u201D\u201E\u201F\u2033\u2036]", "\"");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[\u2026]", "...");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[\u1680]", "-");
|
||||
|
||||
//Spaces
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[«»\u00A0\u2000\u2002\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u200F\u205F\u3000]", " ");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[«»\u2001\u2003]", " ");
|
||||
unicodeStr = Regex.Replace(unicodeStr, "[«»\u180E\u200B\uFEFF]", "");
|
||||
|
||||
return unicodeStr;
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@ -7,13 +7,25 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
class AaxcProcessUpdate
|
||||
{
|
||||
public AaxcProcessUpdate(TimeSpan position, double speed)
|
||||
{
|
||||
ProcessPosition = position;
|
||||
ProcessSpeed = speed;
|
||||
EventTime = DateTime.Now;
|
||||
}
|
||||
public TimeSpan ProcessPosition { get; }
|
||||
public double ProcessSpeed { get; }
|
||||
public DateTime EventTime { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download audible aaxc, decrypt, and remux with chapters.
|
||||
/// </summary>
|
||||
class FFMpegAaxcProcesser
|
||||
{
|
||||
public event EventHandler<TimeSpan> ProgressUpdate;
|
||||
public event EventHandler<AaxcProcessUpdate> ProgressUpdate;
|
||||
public string FFMpegPath { get; }
|
||||
public DownloadLicense DownloadLicense { get; }
|
||||
public bool IsRunning { get; private set; }
|
||||
@ -24,9 +36,10 @@ namespace AaxDecrypter
|
||||
|
||||
private StringBuilder remuxerError = new StringBuilder();
|
||||
private StringBuilder downloaderError = new StringBuilder();
|
||||
private static Regex processedTimeRegex = new Regex("time=(\\d{2}):(\\d{2}):(\\d{2}).\\d{2}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static Regex processedTimeRegex = new Regex("time=(\\d{2}):(\\d{2}):(\\d{2}).\\d{2}.*speed=\\s{0,1}([0-9]*[.]?[0-9]+)(?:e\\+([0-9]+)){0,1}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private Process downloader;
|
||||
private Process remuxer;
|
||||
private bool isCanceled = false;
|
||||
|
||||
public FFMpegAaxcProcesser( DownloadLicense downloadLicense)
|
||||
{
|
||||
@ -60,6 +73,9 @@ namespace AaxDecrypter
|
||||
remuxer.Start();
|
||||
remuxer.BeginErrorReadLine();
|
||||
|
||||
//Thic check needs to be placed after remuxer has started
|
||||
if (isCanceled) return;
|
||||
|
||||
var pipedOutput = downloader.StandardOutput.BaseStream;
|
||||
var pipedInput = remuxer.StandardInput.BaseStream;
|
||||
|
||||
@ -94,8 +110,12 @@ namespace AaxDecrypter
|
||||
}
|
||||
public void Cancel()
|
||||
{
|
||||
isCanceled = true;
|
||||
|
||||
if (IsRunning && !remuxer.HasExited)
|
||||
remuxer.Kill();
|
||||
if (IsRunning && !downloader.HasExited)
|
||||
downloader.Kill();
|
||||
}
|
||||
private void Downloader_ErrorDataReceived(object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
@ -115,6 +135,7 @@ namespace AaxDecrypter
|
||||
if (processedTimeRegex.IsMatch(e.Data))
|
||||
{
|
||||
//get timestamp of of last processed audio stream position
|
||||
//and processing speed
|
||||
var match = processedTimeRegex.Match(e.Data);
|
||||
|
||||
int hours = int.Parse(match.Groups[1].Value);
|
||||
@ -123,7 +144,11 @@ namespace AaxDecrypter
|
||||
|
||||
var position = new TimeSpan(hours, minutes, seconds);
|
||||
|
||||
ProgressUpdate?.Invoke(sender, position);
|
||||
double speed = double.Parse(match.Groups[4].Value);
|
||||
int exp = match.Groups[5].Success ? int.Parse(match.Groups[5].Value) : 0;
|
||||
speed *= Math.Pow(10, exp);
|
||||
|
||||
ProgressUpdate?.Invoke(this, new AaxcProcessUpdate(position, speed));
|
||||
}
|
||||
|
||||
if (e.Data.Contains("aac bitstream error"))
|
||||
|
||||
@ -3,57 +3,50 @@ namespace AaxDecrypter
|
||||
{
|
||||
public static class NFO
|
||||
{
|
||||
public static string CreateContents(string ripper, TagLib.File aaxcTagLib, ChapterInfo chapters)
|
||||
public static string CreateContents(string ripper, AaxcTagLibFile aaxcTagLib, ChapterInfo chapters)
|
||||
{
|
||||
var tag = aaxcTagLib.GetTag(TagLib.TagTypes.Apple);
|
||||
|
||||
string narator = string.IsNullOrWhiteSpace(aaxcTagLib.Tag.FirstComposer) ? tag.Narrator : aaxcTagLib.Tag.FirstComposer;
|
||||
|
||||
var _hours = (int)aaxcTagLib.Properties.Duration.TotalHours;
|
||||
var myDuration
|
||||
= (_hours > 0 ? _hours + " hours, " : "")
|
||||
= (_hours > 0 ? _hours + " hours, " : string.Empty)
|
||||
+ aaxcTagLib.Properties.Duration.Minutes + " minutes, "
|
||||
+ aaxcTagLib.Properties.Duration.Seconds + " seconds";
|
||||
|
||||
var header
|
||||
var nfoString
|
||||
= "General Information\r\n"
|
||||
+ "===================\r\n"
|
||||
+ $" Title: {aaxcTagLib.Tag.Title.Replace(" (Unabridged)", "")}\r\n"
|
||||
+ $" Author: {aaxcTagLib.Tag.FirstPerformer ?? "[unknown]"}\r\n"
|
||||
+ $" Read By: {aaxcTagLib.GetTag(TagLib.TagTypes.Apple).Narrator??"[unknown]"}\r\n"
|
||||
+ $" Copyright: {aaxcTagLib.Tag.Year}\r\n"
|
||||
+ $" Audiobook Copyright: {aaxcTagLib.Tag.Year}\r\n";
|
||||
if (!string.IsNullOrEmpty(aaxcTagLib.Tag.FirstGenre))
|
||||
header += $" Genre: {aaxcTagLib.Tag.FirstGenre}\r\n";
|
||||
|
||||
var s
|
||||
= header
|
||||
+ $" Publisher: {tag.Publisher ?? ""}\r\n"
|
||||
+ "======================\r\n"
|
||||
+ $" Title: {aaxcTagLib.AsciiTitleSansUnabridged ?? "[unknown]"}\r\n"
|
||||
+ $" Author: {aaxcTagLib.AsciiFirstAuthor ?? "[unknown]"}\r\n"
|
||||
+ $" Read By: {aaxcTagLib.AsciiNarrator ?? "[unknown]"}\r\n"
|
||||
+ $" Release Date: {aaxcTagLib.ReleaseDate ?? "[unknown]"}\r\n"
|
||||
+ $" Book Copyright: {aaxcTagLib.BookCopyright ?? "[unknown]"}\r\n"
|
||||
+ $" Recording Copyright: {aaxcTagLib.RecordingCopyright ?? "[unknown]"}\r\n"
|
||||
+ $" Genre: {aaxcTagLib.AppleTags.FirstGenre ?? "[unknown]"}\r\n"
|
||||
+ $" Publisher: {aaxcTagLib.Publisher ?? "[unknown]"}\r\n"
|
||||
+ $" Duration: {myDuration}\r\n"
|
||||
+ $" Chapters: {chapters.Count}\r\n"
|
||||
+ "\r\n"
|
||||
+ "\r\n"
|
||||
+ "Media Information\r\n"
|
||||
+ "=================\r\n"
|
||||
+ "======================\r\n"
|
||||
+ " Source Format: Audible AAX\r\n"
|
||||
+ $" Source Sample Rate: {aaxcTagLib.Properties.AudioSampleRate} Hz\r\n"
|
||||
+ $" Source Channels: {aaxcTagLib.Properties.AudioChannels}\r\n"
|
||||
+ $" Source Bitrate: {aaxcTagLib.Properties.AudioBitrate} kbits\r\n"
|
||||
+ $" Source Bitrate: {aaxcTagLib.Properties.AudioBitrate} Kbps\r\n"
|
||||
+ "\r\n"
|
||||
+ " Lossless Encode: Yes\r\n"
|
||||
+ " Encoded Codec: AAC / M4B\r\n"
|
||||
+ $" Encoded Sample Rate: {aaxcTagLib.Properties.AudioSampleRate} Hz\r\n"
|
||||
+ $" Encoded Channels: {aaxcTagLib.Properties.AudioChannels}\r\n"
|
||||
+ $" Encoded Bitrate: {aaxcTagLib.Properties.AudioBitrate} kbits\r\n"
|
||||
+ $" Encoded Bitrate: {aaxcTagLib.Properties.AudioBitrate} Kbps\r\n"
|
||||
+ "\r\n"
|
||||
+ $" Ripper: {ripper}\r\n"
|
||||
+ "\r\n"
|
||||
+ "\r\n"
|
||||
+ "Book Description\r\n"
|
||||
+ "================\r\n"
|
||||
+ (!string.IsNullOrWhiteSpace(tag.LongDescription) ? tag.LongDescription : tag.Description);
|
||||
+ (!string.IsNullOrWhiteSpace(aaxcTagLib.LongDescription) ? aaxcTagLib.AsciiLongDescription : aaxcTagLib.AsciiComment);
|
||||
|
||||
return s;
|
||||
return nfoString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FileLiberator.AaxcDownloadDecrypt
|
||||
{
|
||||
public class DownloadBookDummy : DownloadableBase
|
||||
{
|
||||
public override Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook) => Task.FromResult(new StatusHandler());
|
||||
|
||||
public override bool Validate(LibraryBook libraryBook)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using FileLiberator.AaxcDownloadDecrypt;
|
||||
using FileLiberator;
|
||||
using FileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
@ -22,8 +22,6 @@ namespace FileLiberator
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<LibraryBook> Completed;
|
||||
|
||||
|
||||
public DownloadBookDummy DownloadBook { get; } = new DownloadBookDummy();
|
||||
public DownloadDecryptBook DecryptBook { get; } = new DownloadDecryptBook();
|
||||
public DownloadPdf DownloadPdf { get; } = new DownloadPdf();
|
||||
|
||||
@ -38,12 +36,6 @@ namespace FileLiberator
|
||||
|
||||
try
|
||||
{
|
||||
{
|
||||
var statusHandler = await DownloadBook.TryProcessAsync(libraryBook);
|
||||
if (statusHandler.HasErrors)
|
||||
return statusHandler;
|
||||
}
|
||||
|
||||
{
|
||||
var statusHandler = await DecryptBook.TryProcessAsync(libraryBook);
|
||||
if (statusHandler.HasErrors)
|
||||
|
||||
@ -11,10 +11,11 @@ using System.Threading.Tasks;
|
||||
using AaxDecrypter;
|
||||
using AudibleApi;
|
||||
|
||||
namespace FileLiberator.AaxcDownloadDecrypt
|
||||
namespace FileLiberator
|
||||
{
|
||||
public class DownloadDecryptBook : IDecryptable
|
||||
{
|
||||
public event EventHandler<Action<byte[]>> RequestCoverArt;
|
||||
public event EventHandler<LibraryBook> Begin;
|
||||
public event EventHandler<string> DecryptBegin;
|
||||
public event EventHandler<string> TitleDiscovered;
|
||||
@ -34,7 +35,6 @@ namespace FileLiberator.AaxcDownloadDecrypt
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
if (AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId))
|
||||
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
||||
|
||||
@ -57,7 +57,6 @@ namespace FileLiberator.AaxcDownloadDecrypt
|
||||
{
|
||||
Completed?.Invoke(this, libraryBook);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async Task<string> aaxToM4bConverterDecryptAsync(string destinationDir, LibraryBook libraryBook)
|
||||
@ -74,38 +73,30 @@ namespace FileLiberator.AaxcDownloadDecrypt
|
||||
|
||||
var aaxcDecryptDlLic = new DownloadLicense(dlLic.DownloadUrl, dlLic.AudibleKey, dlLic.AudibleIV, Resources.UserAgent);
|
||||
|
||||
var destinationDirectory = Path.GetDirectoryName(destinationDir);
|
||||
|
||||
if (Configuration.Instance.DownloadChapters)
|
||||
if (Configuration.Instance.AllowLibationFixup)
|
||||
{
|
||||
var contentMetadata = await api.GetLibraryBookMetadataAsync(libraryBook.Book.AudibleProductId);
|
||||
|
||||
var aaxcDecryptChapters = new ChapterInfo();
|
||||
|
||||
foreach (var chap in contentMetadata?.ChapterInfo?.Chapters)
|
||||
aaxcDecryptChapters.AddChapter(new Chapter(chap.Title, chap.StartOffsetMs, chap.LengthMs));
|
||||
|
||||
aaxcDownloader = await AaxcDownloadConverter.CreateAsync(destinationDirectory, aaxcDecryptDlLic, aaxcDecryptChapters);
|
||||
aaxcDownloader = AaxcDownloadConverter.Create(destinationDir, aaxcDecryptDlLic, aaxcDecryptChapters);
|
||||
}
|
||||
else
|
||||
{
|
||||
aaxcDownloader = await AaxcDownloadConverter.CreateAsync(destinationDirectory, aaxcDecryptDlLic);
|
||||
aaxcDownloader = AaxcDownloadConverter.Create(destinationDir, aaxcDecryptDlLic);
|
||||
}
|
||||
|
||||
aaxcDownloader.AppName = "Libation";
|
||||
|
||||
TitleDiscovered?.Invoke(this, aaxcDownloader.Title);
|
||||
AuthorsDiscovered?.Invoke(this, aaxcDownloader.Author);
|
||||
NarratorsDiscovered?.Invoke(this, aaxcDownloader.Narrator);
|
||||
|
||||
if (aaxcDownloader.CoverArt is not null)
|
||||
CoverImageFilepathDiscovered?.Invoke(this, aaxcDownloader.CoverArt);
|
||||
|
||||
// override default which was set in CreateAsync
|
||||
var proposedOutputFile = Path.Combine(destinationDir, $"{PathLib.ToPathSafeString(libraryBook.Book.Title)} [{libraryBook.Book.AudibleProductId}].m4b");
|
||||
aaxcDownloader.SetOutputFilename(proposedOutputFile);
|
||||
aaxcDownloader.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress);
|
||||
aaxcDownloader.DecryptTimeRemaining += (s, remaining) => UpdateRemainingTime?.Invoke(this, remaining);
|
||||
aaxcDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
|
||||
aaxcDownloader.RetrievedTags += aaxcDownloader_RetrievedTags;
|
||||
|
||||
// REAL WORK DONE HERE
|
||||
var success = await Task.Run(() => aaxcDownloader.Run());
|
||||
@ -122,6 +113,26 @@ namespace FileLiberator.AaxcDownloadDecrypt
|
||||
}
|
||||
}
|
||||
|
||||
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
|
||||
{
|
||||
if (e is null && Configuration.Instance.AllowLibationFixup)
|
||||
{
|
||||
RequestCoverArt?.Invoke(this, aaxcDownloader.SetCoverArt);
|
||||
}
|
||||
|
||||
if (e is not null)
|
||||
{
|
||||
CoverImageFilepathDiscovered?.Invoke(this, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void aaxcDownloader_RetrievedTags(object sender, AaxcTagLibFile e)
|
||||
{
|
||||
TitleDiscovered?.Invoke(this, e.TitleSansUnabridged);
|
||||
AuthorsDiscovered?.Invoke(this, e.FirstAuthor ?? "[unknown]");
|
||||
NarratorsDiscovered?.Invoke(this, e.Narrator ?? "[unknown]");
|
||||
}
|
||||
|
||||
private static string moveFilesToBooksDir(Book product, string outputAudioFilename)
|
||||
{
|
||||
// create final directory. move each file into it. MOVE AUDIO FILE LAST
|
||||
@ -6,6 +6,7 @@ namespace FileLiberator
|
||||
{
|
||||
event EventHandler<string> DecryptBegin;
|
||||
|
||||
event EventHandler<Action<byte[]>> RequestCoverArt;
|
||||
event EventHandler<string> TitleDiscovered;
|
||||
event EventHandler<string> AuthorsDiscovered;
|
||||
event EventHandler<string> NarratorsDiscovered;
|
||||
|
||||
@ -83,11 +83,11 @@ namespace FileManager
|
||||
set => persistentDictionary.Set(nameof(DecryptInProgressEnum), value);
|
||||
}
|
||||
|
||||
[Description("Download chapter titles from Audible?")]
|
||||
public bool DownloadChapters
|
||||
[Description("Allow Libation for fix up audiobook metadata?")]
|
||||
public bool AllowLibationFixup
|
||||
{
|
||||
get => persistentDictionary.Get<bool>(nameof(DownloadChapters));
|
||||
set => persistentDictionary.Set(nameof(DownloadChapters), value);
|
||||
get => persistentDictionary.Get<bool>(nameof(AllowLibationFixup));
|
||||
set => persistentDictionary.Set(nameof(AllowLibationFixup), value);
|
||||
}
|
||||
// note: any potential file manager static ctors can't compensate if storage dir is changed at run time via settings. this is partly bad architecture. but the side effect is desirable. if changing LibationFiles location: restart app
|
||||
|
||||
|
||||
@ -60,7 +60,7 @@ namespace LibationLauncher
|
||||
config.DownloadsInProgressEnum ??= "WinTemp";
|
||||
config.DecryptInProgressEnum ??= "WinTemp";
|
||||
config.Books ??= Configuration.AppDir;
|
||||
config.DownloadChapters = true;
|
||||
config.AllowLibationFixup = true;
|
||||
};
|
||||
// setupDialog.BasicBtn_Click += (_, __) => // no action needed
|
||||
setupDialog.AdvancedBtn_Click += (_, __) => isAdvanced = true;
|
||||
@ -235,9 +235,9 @@ namespace LibationLauncher
|
||||
{
|
||||
var persistentDictionary = new PersistentDictionary(Configuration.Instance.SettingsFilePath);
|
||||
|
||||
if (persistentDictionary.GetString("DownloadChapters") is null)
|
||||
if (persistentDictionary.GetString("AllowLibationFixup") is null)
|
||||
{
|
||||
persistentDictionary.Set("DownloadChapters", true);
|
||||
persistentDictionary.Set("AllowLibationFixup", true);
|
||||
}
|
||||
|
||||
if (!File.Exists(AudibleApiStorage.AccountsSettingsFile))
|
||||
|
||||
@ -56,15 +56,22 @@ namespace LibationWinForms.BookLiberation
|
||||
private void updateBookInfo()
|
||||
=> bookInfoLbl.UIThread(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}");
|
||||
|
||||
public void SetCoverImage(byte[] coverBytes)
|
||||
=> pictureBox1.UIThread(() => pictureBox1.Image = ImageReader.ToImage(coverBytes));
|
||||
public void SetCoverImage(System.Drawing.Image coverImage)
|
||||
=> pictureBox1.UIThread(() => pictureBox1.Image = coverImage);
|
||||
|
||||
public void UpdateProgress(int percentage)
|
||||
{
|
||||
if (percentage == 0)
|
||||
remainingTimeLbl.UIThread(() => remainingTimeLbl.Text = "ETA:\r\n0 sec");
|
||||
|
||||
progressBar1.UIThread(() => progressBar1.Value = percentage);
|
||||
if (percentage == int.MaxValue)
|
||||
progressBar1.UIThread(() => progressBar1.Style = ProgressBarStyle.Marquee);
|
||||
else
|
||||
progressBar1.UIThread(() =>
|
||||
{
|
||||
progressBar1.Value = percentage;
|
||||
progressBar1.Style = ProgressBarStyle.Blocks;
|
||||
});
|
||||
}
|
||||
|
||||
public void UpdateRemainingTime(TimeSpan remaining)
|
||||
|
||||
@ -25,7 +25,7 @@ namespace LibationWinForms.BookLiberation
|
||||
logMe.LogErrorString += (_, text) => Serilog.Log.Logger.Error(text);
|
||||
logMe.LogErrorString += (_, text) => form.WriteLine(text);
|
||||
|
||||
logMe.LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error");
|
||||
logMe.LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error");
|
||||
logMe.LogError += (_, tuple) =>
|
||||
{
|
||||
form.WriteLine(tuple.Item2 ?? "Automated backup: error");
|
||||
@ -70,8 +70,7 @@ namespace LibationWinForms.BookLiberation
|
||||
{
|
||||
var backupBook = new BackupBook();
|
||||
|
||||
backupBook.DownloadBook.Begin += (_, __) => wireUpEvents(backupBook.DownloadBook);
|
||||
backupBook.DecryptBook.Begin += (_, __) => wireUpEvents(backupBook.DecryptBook);
|
||||
backupBook.DecryptBook.Begin += (_, l) => wireUpEvents(backupBook.DecryptBook, l);
|
||||
backupBook.DownloadPdf.Begin += (_, __) => wireUpEvents(backupBook.DownloadPdf);
|
||||
|
||||
// must occur before completedAction. A common use case is:
|
||||
@ -80,13 +79,11 @@ namespace LibationWinForms.BookLiberation
|
||||
// completedAction is to refresh grid
|
||||
// - want to see that book disappear from grid
|
||||
// also for this to work, updateIsLiberated can NOT be async
|
||||
backupBook.DownloadBook.Completed += updateIsLiberated;
|
||||
backupBook.DecryptBook.Completed += updateIsLiberated;
|
||||
backupBook.DownloadPdf.Completed += updateIsLiberated;
|
||||
|
||||
if (completedAction != null)
|
||||
{
|
||||
backupBook.DownloadBook.Completed += completedAction;
|
||||
backupBook.DecryptBook.Completed += completedAction;
|
||||
backupBook.DownloadPdf.Completed += completedAction;
|
||||
}
|
||||
@ -104,9 +101,7 @@ namespace LibationWinForms.BookLiberation
|
||||
#endregion
|
||||
|
||||
#region define how model actions will affect form behavior
|
||||
void downloadBookBegin(object _, LibraryBook libraryBook) => logMe.Info($"Download Step, Begin: {libraryBook.Book}");
|
||||
void statusUpdate(object _, string str) => logMe.Info("- " + str);
|
||||
void downloadBookCompleted(object _, LibraryBook libraryBook) => logMe.Info($"Download Step, Completed: {libraryBook.Book}");
|
||||
void decryptBookBegin(object _, LibraryBook libraryBook) => logMe.Info($"Decrypt Step, Begin: {libraryBook.Book}");
|
||||
// extra line after book is completely finished
|
||||
void decryptBookCompleted(object _, LibraryBook libraryBook) => logMe.Info($"Decrypt Step, Completed: {libraryBook.Book}{Environment.NewLine}");
|
||||
@ -116,9 +111,6 @@ namespace LibationWinForms.BookLiberation
|
||||
#endregion
|
||||
|
||||
#region subscribe new form to model's events
|
||||
backupBook.DownloadBook.Begin += downloadBookBegin;
|
||||
backupBook.DownloadBook.StatusUpdate += statusUpdate;
|
||||
backupBook.DownloadBook.Completed += downloadBookCompleted;
|
||||
backupBook.DecryptBook.Begin += decryptBookBegin;
|
||||
backupBook.DecryptBook.StatusUpdate += statusUpdate;
|
||||
backupBook.DecryptBook.Completed += decryptBookCompleted;
|
||||
@ -131,9 +123,6 @@ namespace LibationWinForms.BookLiberation
|
||||
// unsubscribe so disposed forms aren't still trying to receive notifications
|
||||
automatedBackupsForm.FormClosing += (_, __) =>
|
||||
{
|
||||
backupBook.DownloadBook.Begin -= downloadBookBegin;
|
||||
backupBook.DownloadBook.StatusUpdate -= statusUpdate;
|
||||
backupBook.DownloadBook.Completed -= downloadBookCompleted;
|
||||
backupBook.DecryptBook.Begin -= decryptBookBegin;
|
||||
backupBook.DecryptBook.StatusUpdate -= statusUpdate;
|
||||
backupBook.DecryptBook.Completed -= decryptBookCompleted;
|
||||
@ -152,7 +141,7 @@ namespace LibationWinForms.BookLiberation
|
||||
|
||||
var downloadPdf = getWiredUpDownloadPdf(completedAction);
|
||||
|
||||
(AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(downloadPdf);
|
||||
(AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(downloadPdf);
|
||||
await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync();
|
||||
}
|
||||
|
||||
@ -250,23 +239,51 @@ namespace LibationWinForms.BookLiberation
|
||||
}
|
||||
|
||||
// subscribed to Begin event because a new form should be created+processed+closed on each iteration
|
||||
private static void wireUpEvents(IDecryptable decryptBook)
|
||||
private static void wireUpEvents(IDecryptable decryptBook, LibraryBook libraryBook)
|
||||
{
|
||||
#region create form
|
||||
var decryptDialog = new DecryptForm();
|
||||
#endregion
|
||||
|
||||
#region Set initially displayed book properties from library info.
|
||||
decryptDialog.SetTitle(libraryBook.Book.Title);
|
||||
decryptDialog.SetAuthorNames(string.Join(", ", libraryBook.Book.Authors));
|
||||
decryptDialog.SetNarratorNames(string.Join(", ", libraryBook.Book.NarratorNames));
|
||||
decryptDialog.SetCoverImage(
|
||||
WindowsDesktopUtilities.WinAudibleImageServer.GetImage(
|
||||
libraryBook.Book.PictureId,
|
||||
FileManager.PictureSize._80x80
|
||||
));
|
||||
#endregion
|
||||
|
||||
#region define how model actions will affect form behavior
|
||||
void decryptBegin(object _, string __) => decryptDialog.Show();
|
||||
|
||||
void titleDiscovered(object _, string title) => decryptDialog.SetTitle(title);
|
||||
void authorsDiscovered(object _, string authors) => decryptDialog.SetAuthorNames(authors);
|
||||
void narratorsDiscovered(object _, string narrators) => decryptDialog.SetNarratorNames(narrators);
|
||||
void coverImageFilepathDiscovered(object _, byte[] coverBytes) => decryptDialog.SetCoverImage(coverBytes);
|
||||
void coverImageFilepathDiscovered(object _, byte[] coverBytes) => decryptDialog.SetCoverImage(Dinah.Core.Drawing.ImageReader.ToImage(coverBytes));
|
||||
void updateProgress(object _, int percentage) => decryptDialog.UpdateProgress(percentage);
|
||||
void updateRemainingTime(object _, TimeSpan remaining) => decryptDialog.UpdateRemainingTime(remaining);
|
||||
|
||||
void decryptCompleted(object _, string __) => decryptDialog.Close();
|
||||
|
||||
void requestCoverArt(object _, Action<byte[]> setArt)
|
||||
{
|
||||
var picDef = new FileManager.PictureDefinition(libraryBook.Book.PictureId, FileManager.PictureSize._500x500);
|
||||
(bool isDefault, byte[] picture) = FileManager.PictureStorage.GetPicture(picDef);
|
||||
|
||||
if (isDefault)
|
||||
{
|
||||
void pictureCached(object _, string pictureId) => onPictureCached(libraryBook, pictureId, setArt, pictureCached);
|
||||
FileManager.PictureStorage.PictureCached += pictureCached;
|
||||
}
|
||||
else
|
||||
setArt(picture);
|
||||
}
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region subscribe new form to model's events
|
||||
@ -278,6 +295,7 @@ namespace LibationWinForms.BookLiberation
|
||||
decryptBook.CoverImageFilepathDiscovered += coverImageFilepathDiscovered;
|
||||
decryptBook.UpdateProgress += updateProgress;
|
||||
decryptBook.UpdateRemainingTime += updateRemainingTime;
|
||||
decryptBook.RequestCoverArt += requestCoverArt;
|
||||
|
||||
decryptBook.DecryptCompleted += decryptCompleted;
|
||||
#endregion
|
||||
@ -294,13 +312,25 @@ namespace LibationWinForms.BookLiberation
|
||||
decryptBook.CoverImageFilepathDiscovered -= coverImageFilepathDiscovered;
|
||||
decryptBook.UpdateProgress -= updateProgress;
|
||||
decryptBook.UpdateRemainingTime -= updateRemainingTime;
|
||||
decryptBook.RequestCoverArt -= requestCoverArt;
|
||||
|
||||
decryptBook.DecryptCompleted -= decryptCompleted;
|
||||
decryptBook.Cancel();
|
||||
};
|
||||
#endregion
|
||||
}
|
||||
private static void onPictureCached(LibraryBook libraryBook, string picId, Action<byte[]> setArt, EventHandler<string> pictureCacheDelegate)
|
||||
{
|
||||
if (picId == libraryBook.Book.PictureId)
|
||||
{
|
||||
FileManager.PictureStorage.PictureCached -= pictureCacheDelegate;
|
||||
|
||||
var picDef = new FileManager.PictureDefinition(libraryBook.Book.PictureId, FileManager.PictureSize._500x500);
|
||||
(_, byte[] picture) = FileManager.PictureStorage.GetPicture(picDef);
|
||||
|
||||
setArt(picture);
|
||||
}
|
||||
}
|
||||
private static (AutomatedBackupsForm, LogMe) attachToBackupsForm(IDownloadableProcessable downloadable)
|
||||
{
|
||||
#region create form and logger
|
||||
@ -418,23 +448,23 @@ Created new 'skip' file
|
||||
{
|
||||
private LibraryBook _libraryBook { get; }
|
||||
|
||||
protected override string SkipDialogText => @"
|
||||
protected override string SkipDialogText => @"
|
||||
An error occurred while trying to process this book. Skip this book permanently?
|
||||
|
||||
- Click YES to skip this book permanently.
|
||||
|
||||
- Click NO to skip the book this time only. We'll try again later.
|
||||
".Trim();
|
||||
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.YesNo;
|
||||
protected override DialogResult CreateSkipFileResult => DialogResult.Yes;
|
||||
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.YesNo;
|
||||
protected override DialogResult CreateSkipFileResult => DialogResult.Yes;
|
||||
|
||||
public BackupSingle(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm, LibraryBook libraryBook)
|
||||
public BackupSingle(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm, LibraryBook libraryBook)
|
||||
: base(logMe, processable, automatedBackupsForm)
|
||||
{
|
||||
_libraryBook = libraryBook;
|
||||
}
|
||||
|
||||
protected override async Task RunAsync()
|
||||
protected override async Task RunAsync()
|
||||
{
|
||||
if (_libraryBook is not null)
|
||||
await ProcessOneAsync(Processable.ProcessSingleAsync, _libraryBook);
|
||||
@ -442,7 +472,7 @@ An error occurred while trying to process this book. Skip this book permanently?
|
||||
}
|
||||
class BackupLoop : BackupRunner
|
||||
{
|
||||
protected override string SkipDialogText => @"
|
||||
protected override string SkipDialogText => @"
|
||||
An error occurred while trying to process this book
|
||||
|
||||
- ABORT: stop processing books.
|
||||
@ -461,20 +491,20 @@ An error occurred while trying to process this book
|
||||
{
|
||||
// support for 'skip this time only' requires state. iterators provide this state for free. therefore: use foreach/iterator here
|
||||
foreach (var libraryBook in Processable.GetValidLibraryBooks())
|
||||
{
|
||||
var keepGoing = await ProcessOneAsync(Processable.ProcessBookAsync_NoValidation, libraryBook);
|
||||
if (!keepGoing)
|
||||
return;
|
||||
{
|
||||
var keepGoing = await ProcessOneAsync(Processable.ProcessBookAsync_NoValidation, libraryBook);
|
||||
if (!keepGoing)
|
||||
return;
|
||||
|
||||
if (!AutomatedBackupsForm.KeepGoing)
|
||||
{
|
||||
if (AutomatedBackupsForm.KeepGoingVisible && !AutomatedBackupsForm.KeepGoingChecked)
|
||||
LogMe.Info("'Keep going' is unchecked");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!AutomatedBackupsForm.KeepGoing)
|
||||
{
|
||||
if (AutomatedBackupsForm.KeepGoingVisible && !AutomatedBackupsForm.KeepGoingChecked)
|
||||
LogMe.Info("'Keep going' is unchecked");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LogMe.Info("Done. All books have been processed");
|
||||
LogMe.Info("Done. All books have been processed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
20
LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
20
LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
@ -43,7 +43,7 @@
|
||||
this.saveBtn = new System.Windows.Forms.Button();
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.groupBox1 = new System.Windows.Forms.GroupBox();
|
||||
this.downloadChaptersCbox = new System.Windows.Forms.CheckBox();
|
||||
this.allowLibationFixupCbox = new System.Windows.Forms.CheckBox();
|
||||
this.downloadsInProgressGb.SuspendLayout();
|
||||
this.decryptInProgressGb.SuspendLayout();
|
||||
this.groupBox1.SuspendLayout();
|
||||
@ -215,7 +215,7 @@
|
||||
//
|
||||
// groupBox1
|
||||
//
|
||||
this.groupBox1.Controls.Add(this.downloadChaptersCbox);
|
||||
this.groupBox1.Controls.Add(this.allowLibationFixupCbox);
|
||||
this.groupBox1.Controls.Add(this.downloadsInProgressGb);
|
||||
this.groupBox1.Controls.Add(this.decryptInProgressGb);
|
||||
this.groupBox1.Location = new System.Drawing.Point(18, 61);
|
||||
@ -229,13 +229,13 @@
|
||||
//
|
||||
// downloadChaptersCbox
|
||||
//
|
||||
this.downloadChaptersCbox.AutoSize = true;
|
||||
this.downloadChaptersCbox.Location = new System.Drawing.Point(10, 24);
|
||||
this.downloadChaptersCbox.Name = "downloadChaptersCbox";
|
||||
this.downloadChaptersCbox.Size = new System.Drawing.Size(224, 19);
|
||||
this.downloadChaptersCbox.TabIndex = 6;
|
||||
this.downloadChaptersCbox.Text = "Download chapter titles from Audible";
|
||||
this.downloadChaptersCbox.UseVisualStyleBackColor = true;
|
||||
this.allowLibationFixupCbox.AutoSize = true;
|
||||
this.allowLibationFixupCbox.Location = new System.Drawing.Point(10, 24);
|
||||
this.allowLibationFixupCbox.Name = "downloadChaptersCbox";
|
||||
this.allowLibationFixupCbox.Size = new System.Drawing.Size(262, 19);
|
||||
this.allowLibationFixupCbox.TabIndex = 6;
|
||||
this.allowLibationFixupCbox.Text = "Allow Libation to fix up audiobook metadata";
|
||||
this.allowLibationFixupCbox.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// SettingsDialog
|
||||
//
|
||||
@ -284,6 +284,6 @@
|
||||
private System.Windows.Forms.Button saveBtn;
|
||||
private System.Windows.Forms.Button cancelBtn;
|
||||
private System.Windows.Forms.GroupBox groupBox1;
|
||||
private System.Windows.Forms.CheckBox downloadChaptersCbox;
|
||||
private System.Windows.Forms.CheckBox allowLibationFixupCbox;
|
||||
}
|
||||
}
|
||||
@ -36,7 +36,7 @@ namespace LibationWinForms.Dialogs
|
||||
? config.Books
|
||||
: Path.GetDirectoryName(Exe.FileLocationOnDisk);
|
||||
|
||||
downloadChaptersCbox.Checked = config.DownloadChapters;
|
||||
allowLibationFixupCbox.Checked = config.AllowLibationFixup;
|
||||
|
||||
switch (config.DownloadsInProgressEnum)
|
||||
{
|
||||
@ -73,7 +73,7 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
private void saveBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
config.DownloadChapters = downloadChaptersCbox.Checked;
|
||||
config.AllowLibationFixup = allowLibationFixupCbox.Checked;
|
||||
config.DownloadsInProgressEnum = downloadsInProgressLibationFilesRb.Checked ? "LibationFiles" : "WinTemp";
|
||||
config.DecryptInProgressEnum = decryptInProgressLibationFilesRb.Checked ? "LibationFiles" : "WinTemp";
|
||||
|
||||
|
||||
@ -81,15 +81,6 @@
|
||||
<metadata name="downloadsInProgressDescLbl.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="downloadsInProgressLibationFilesRb.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="downloadsInProgressWinTempRb.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="downloadsInProgressDescLbl.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="decryptInProgressGb.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
@ -102,15 +93,6 @@
|
||||
<metadata name="decryptInProgressDescLbl.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="decryptInProgressLibationFilesRb.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="decryptInProgressWinTempRb.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="decryptInProgressDescLbl.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="saveBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
@ -123,9 +105,6 @@
|
||||
<metadata name="downloadChaptersCbox.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="downloadChaptersCbox.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="$this.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user