mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-02 02:48:17 -05:00
Compare commits
130 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aeeba0d567 | ||
|
|
e2f919d625 | ||
|
|
e821eea333 | ||
|
|
8f487894f5 | ||
|
|
cd3e0dba68 | ||
|
|
6f31d97763 | ||
|
|
fa5637a340 | ||
|
|
7ab209171b | ||
|
|
6d856f73e7 | ||
|
|
05426eb618 | ||
|
|
d73701c939 | ||
|
|
f284f53edd | ||
|
|
17f3187748 | ||
|
|
f55a41ac0a | ||
|
|
0be2a17537 | ||
|
|
b417c5695e | ||
|
|
6efe064ca7 | ||
|
|
da7af895fb | ||
|
|
1b39f30fd0 | ||
|
|
9cde6bddbd | ||
|
|
b21f257baa | ||
|
|
da68ddc9b8 | ||
|
|
9e15fde2e3 | ||
|
|
ef5b14a929 | ||
|
|
5df7d80aac | ||
|
|
4b2c8ee513 | ||
|
|
097bda2d25 | ||
|
|
81195e382e | ||
|
|
35fc3581b3 | ||
|
|
771d992da7 | ||
|
|
00f7e4b779 | ||
|
|
5d4bcb2db0 | ||
|
|
fbf92bf151 | ||
|
|
2cb3e34d98 | ||
|
|
80589e3854 | ||
|
|
b9770220db | ||
|
|
11128ffb1a | ||
|
|
1d557d05c5 | ||
|
|
d41fe0d3e6 | ||
|
|
17bd54a897 | ||
|
|
0d89c34107 | ||
|
|
66bd18fdc5 | ||
|
|
7143104b40 | ||
|
|
729212a5d5 | ||
|
|
6dafa03554 | ||
|
|
08644fb937 | ||
|
|
7ff4953f7b | ||
|
|
797112740e | ||
|
|
36ab494b31 | ||
|
|
8c6ada8d20 | ||
|
|
41b0ace238 | ||
|
|
c84f144274 | ||
|
|
00f8a63781 | ||
|
|
25d89207bb | ||
|
|
2146ebff29 | ||
|
|
3aed3a5def | ||
|
|
1ee6f3b9f2 | ||
|
|
0c26c34bdd | ||
|
|
6696317ae6 | ||
|
|
3af84af2e2 | ||
|
|
2955e8b464 | ||
|
|
8d6b304a8b | ||
|
|
aa3c648c4c | ||
|
|
0da054ccea | ||
|
|
45080d1661 | ||
|
|
d6b62c0521 | ||
|
|
bc3aa29175 | ||
|
|
e958944466 | ||
|
|
78f278121b | ||
|
|
027cce2d99 | ||
|
|
9332a6f350 | ||
|
|
ac6a73d898 | ||
|
|
74f94fe17f | ||
|
|
120fb58da7 | ||
|
|
ef2adfd474 | ||
|
|
f3a746a852 | ||
|
|
dc8cea5355 | ||
|
|
83cb580db7 | ||
|
|
491a5eba3a | ||
|
|
15150a3633 | ||
|
|
82e3854c84 | ||
|
|
f0eb57a40b | ||
|
|
b65f9567e0 | ||
|
|
b5389c67ea | ||
|
|
d564876eaa | ||
|
|
258887152d | ||
|
|
87c3cac013 | ||
|
|
f148650e57 | ||
|
|
b53aabe0e3 | ||
|
|
e32a39085f | ||
|
|
4d743df643 | ||
|
|
6bd809c7c6 | ||
|
|
9930daa914 | ||
|
|
0475bd48b1 | ||
|
|
1b80f2ed28 | ||
|
|
37ca9abd9d | ||
|
|
0c159df6ca | ||
|
|
31e24ad36c | ||
|
|
be41dca9e0 | ||
|
|
792207caee | ||
|
|
06549e5b4e | ||
|
|
81d0f87b8a | ||
|
|
9550aac788 | ||
|
|
54d650ea48 | ||
|
|
1e88070f3a | ||
|
|
703e71ad74 | ||
|
|
ae6384486c | ||
|
|
8f8e0645a4 | ||
|
|
d619c82fd8 | ||
|
|
919175cc10 | ||
|
|
8d70d2a95f | ||
|
|
e13dc2a48a | ||
|
|
bb3baa6ce0 | ||
|
|
28731e51f5 | ||
|
|
dbcd124c1d | ||
|
|
305de34a76 | ||
|
|
0034d51921 | ||
|
|
b1a033e162 | ||
|
|
9416f4e040 | ||
|
|
344e675634 | ||
|
|
372e85d9af | ||
|
|
c81788429b | ||
|
|
26da307743 | ||
|
|
0306c958d1 | ||
|
|
5ec6994da7 | ||
|
|
c1f50a184a | ||
|
|
8231766d2c | ||
|
|
eedc9bb34d | ||
|
|
310b90962c | ||
|
|
54c21e969e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -333,3 +333,4 @@ ASALocalRun/
|
||||
# manually ignored files
|
||||
/__TODO.txt
|
||||
/DataLayer/LibationContext.db
|
||||
*.lnk
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Dinah.Core.Diagnostics;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class AAXChapters : Chapters
|
||||
{
|
||||
public AAXChapters(string file)
|
||||
{
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = DecryptSupportLibraries.ffprobePath,
|
||||
Arguments = "-loglevel panic -show_chapters -print_format xml \"" + file + "\""
|
||||
};
|
||||
var xml = info.RunHidden().Output;
|
||||
|
||||
var xmlDocument = new System.Xml.XmlDocument();
|
||||
xmlDocument.LoadXml(xml);
|
||||
var chaptersXml = xmlDocument.SelectNodes("/ffprobe/chapters/chapter")
|
||||
.Cast<System.Xml.XmlNode>()
|
||||
.Where(n => n.Name == "chapter");
|
||||
|
||||
foreach (var cnode in chaptersXml)
|
||||
{
|
||||
double startTime = double.Parse(cnode.Attributes["start_time"].Value.Replace(",", "."), CultureInfo.InvariantCulture);
|
||||
double endTime = double.Parse(cnode.Attributes["end_time"].Value.Replace(",", "."), CultureInfo.InvariantCulture);
|
||||
|
||||
string chapterTitle = cnode.ChildNodes
|
||||
.Cast<System.Xml.XmlNode>()
|
||||
.Where(childnode => childnode.Attributes["key"].Value == "title")
|
||||
.Select(childnode => childnode.Attributes["value"].Value)
|
||||
.FirstOrDefault();
|
||||
|
||||
AddChapter(new Chapter(startTime, endTime, chapterTitle));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -15,40 +13,19 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="DecryptLib\AtomicParsley.exe">
|
||||
<None Update="DecryptLib\avcodec-58.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\avcodec-57.dll">
|
||||
<None Update="DecryptLib\avdevice-58.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\avdevice-57.dll">
|
||||
<None Update="DecryptLib\avfilter-7.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\avfilter-6.dll">
|
||||
<None Update="DecryptLib\avformat-58.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\avformat-57.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\avutil-55.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\cygcrypto-1.0.0.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\cyggcc_s-1.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\cygmp4v2-2.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\cygstdc++-6.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\cygwin1.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\cygz.dll">
|
||||
<None Update="DecryptLib\avutil-56.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\ffmpeg.exe">
|
||||
@@ -57,19 +34,10 @@
|
||||
<None Update="DecryptLib\ffprobe.exe">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\mp4trackdump.exe">
|
||||
<None Update="DecryptLib\swresample-3.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\postproc-54.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\swresample-2.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\swscale-4.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DecryptLib\taglib-sharp.dll">
|
||||
<None Update="DecryptLib\swscale-5.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,339 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Diagnostics;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.StepRunner;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public interface ISimpleAaxToM4bConverter
|
||||
{
|
||||
event EventHandler<int> DecryptProgressUpdate;
|
||||
|
||||
bool Run();
|
||||
|
||||
string AppName { get; set; }
|
||||
string inputFileName { get; }
|
||||
byte[] coverBytes { get; }
|
||||
string outDir { get; }
|
||||
string outputFileName { get; }
|
||||
|
||||
Chapters chapters { get; }
|
||||
Tags tags { get; }
|
||||
EncodingInfo encodingInfo { get; }
|
||||
|
||||
void SetOutputFilename(string outFileName);
|
||||
}
|
||||
public interface IAdvancedAaxToM4bConverter : ISimpleAaxToM4bConverter
|
||||
{
|
||||
bool Step1_CreateDir();
|
||||
bool Step2_DecryptAax();
|
||||
bool Step3_Chapterize();
|
||||
bool Step4_InsertCoverArt();
|
||||
bool Step5_Cleanup();
|
||||
bool Step6_AddTags();
|
||||
bool End_CreateCue();
|
||||
bool End_CreateNfo();
|
||||
}
|
||||
/// <summary>full c# app. integrated logging. no UI</summary>
|
||||
public class AaxToM4bConverter : IAdvancedAaxToM4bConverter
|
||||
{
|
||||
public event EventHandler<int> DecryptProgressUpdate;
|
||||
|
||||
public string inputFileName { get; }
|
||||
public string decryptKey { get; private set; }
|
||||
|
||||
private StepSequence steps { get; }
|
||||
public byte[] coverBytes { get; private set; }
|
||||
|
||||
public string AppName { get; set; } = nameof(AaxToM4bConverter);
|
||||
|
||||
public string outDir { get; private set; }
|
||||
public string outputFileName { get; private set; }
|
||||
|
||||
public Chapters chapters { get; private set; }
|
||||
public Tags tags { get; private set; }
|
||||
public EncodingInfo encodingInfo { get; private set; }
|
||||
|
||||
private Func<Task<string>> getKeyFuncAsync { get; }
|
||||
|
||||
public static async Task<AaxToM4bConverter> CreateAsync(string inputFile, string decryptKey, Func<Task<string>> getKeyFunc, Chapters chapters = null)
|
||||
{
|
||||
var converter = new AaxToM4bConverter(inputFile, decryptKey, getKeyFunc);
|
||||
converter.chapters = chapters ?? new AAXChapters(inputFile);
|
||||
await converter.prelimProcessing();
|
||||
converter.printPrelim();
|
||||
|
||||
return converter;
|
||||
}
|
||||
private AaxToM4bConverter(string inputFile, string decryptKey, Func<Task<string>> getKeyFunc)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(inputFile, nameof(inputFile));
|
||||
if (!File.Exists(inputFile))
|
||||
throw new ArgumentNullException(nameof(inputFile), "File does not exist");
|
||||
|
||||
steps = new StepSequence
|
||||
{
|
||||
Name = "Convert Aax To M4b",
|
||||
|
||||
["Step 1: Create Dir"] = Step1_CreateDir,
|
||||
["Step 2: Decrypt Aax"] = Step2_DecryptAax,
|
||||
["Step 3: Chapterize and tag"] = Step3_Chapterize,
|
||||
["Step 4: Insert Cover Art"] = Step4_InsertCoverArt,
|
||||
["Step 5: Cleanup"] = Step5_Cleanup,
|
||||
["Step 6: Add Tags"] = Step6_AddTags,
|
||||
["End: Create Cue"] = End_CreateCue,
|
||||
["End: Create Nfo"] = End_CreateNfo
|
||||
};
|
||||
|
||||
inputFileName = inputFile;
|
||||
this.decryptKey = decryptKey;
|
||||
this.getKeyFuncAsync = getKeyFunc;
|
||||
}
|
||||
|
||||
private async Task prelimProcessing()
|
||||
{
|
||||
tags = new Tags(inputFileName);
|
||||
encodingInfo = new EncodingInfo(inputFileName);
|
||||
|
||||
var defaultFilename = Path.Combine(
|
||||
Path.GetDirectoryName(inputFileName),
|
||||
PathLib.ToPathSafeString(tags.author),
|
||||
PathLib.ToPathSafeString(tags.title) + ".m4b"
|
||||
);
|
||||
|
||||
// set default name
|
||||
SetOutputFilename(defaultFilename);
|
||||
|
||||
await Task.Run(() => saveCover(inputFileName));
|
||||
}
|
||||
|
||||
private void saveCover(string aaxFile)
|
||||
{
|
||||
using var file = TagLib.File.Create(aaxFile, "audio/mp4", TagLib.ReadStyle.Average);
|
||||
coverBytes = file.Tag.Pictures[0].Data.Data;
|
||||
}
|
||||
|
||||
private void printPrelim()
|
||||
{
|
||||
Console.WriteLine($"Audible Book ID = {tags.id}");
|
||||
|
||||
Console.WriteLine($"Book: {tags.title}");
|
||||
Console.WriteLine($"Author: {tags.author}");
|
||||
Console.WriteLine($"Narrator: {tags.narrator}");
|
||||
Console.WriteLine($"Year: {tags.year}");
|
||||
Console.WriteLine($"Total Time: {tags.duration.GetTotalTimeFormatted()} in {chapters.Count} chapters");
|
||||
Console.WriteLine($"WARNING-Source is {encodingInfo.originalBitrate} kbits @ {encodingInfo.sampleRate}Hz, {encodingInfo.channels} channels");
|
||||
}
|
||||
|
||||
public bool Run()
|
||||
{
|
||||
var (IsSuccess, Elapsed) = steps.Run();
|
||||
|
||||
if (!IsSuccess)
|
||||
{
|
||||
Console.WriteLine("WARNING-Conversion failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
var speedup = (int)(tags.duration.TotalSeconds / (long)Elapsed.TotalSeconds);
|
||||
Console.WriteLine("Speedup is " + speedup + "x realtime.");
|
||||
Console.WriteLine("Done");
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetOutputFilename(string outFileName)
|
||||
{
|
||||
outputFileName = PathLib.ReplaceExtension(outFileName, ".m4b");
|
||||
outDir = Path.GetDirectoryName(outputFileName);
|
||||
|
||||
if (File.Exists(outputFileName))
|
||||
File.Delete(outputFileName);
|
||||
}
|
||||
|
||||
private string outputFileWithNewExt(string extension) => PathLib.ReplaceExtension(outputFileName, extension);
|
||||
|
||||
public bool Step1_CreateDir()
|
||||
{
|
||||
ProcessRunner.WorkingDir = outDir;
|
||||
Directory.CreateDirectory(outDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Step2_DecryptAax()
|
||||
{
|
||||
DecryptProgressUpdate?.Invoke(this, 0);
|
||||
|
||||
var tempRipFile = Path.Combine(outDir, "funny.aac");
|
||||
|
||||
var fail = "WARNING-Decrypt failure. ";
|
||||
|
||||
int returnCode;
|
||||
if (string.IsNullOrWhiteSpace(decryptKey))
|
||||
{
|
||||
returnCode = getKey_decrypt(tempRipFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
returnCode = decrypt(tempRipFile);
|
||||
if (returnCode == -99)
|
||||
{
|
||||
Console.WriteLine($"{fail}Incorrect decrypt key: {decryptKey}");
|
||||
decryptKey = null;
|
||||
returnCode = getKey_decrypt(tempRipFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (returnCode == 100)
|
||||
Console.WriteLine($"{fail}Thread completed without changing return code. This shouldn't be possible");
|
||||
else if (returnCode == 0)
|
||||
{
|
||||
// success!
|
||||
FileExt.SafeMove(tempRipFile, outputFileWithNewExt(".mp4"));
|
||||
DecryptProgressUpdate?.Invoke(this, 100);
|
||||
return true;
|
||||
}
|
||||
else if (returnCode == -99)
|
||||
Console.WriteLine($"{fail}Incorrect decrypt key: {decryptKey}");
|
||||
else // any other returnCode
|
||||
Console.WriteLine($"{fail}Unknown failure code: {returnCode}");
|
||||
|
||||
FileExt.SafeDelete(tempRipFile);
|
||||
DecryptProgressUpdate?.Invoke(this, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
private int getKey_decrypt(string tempRipFile)
|
||||
{
|
||||
decryptKey = getKey();
|
||||
return decrypt(tempRipFile);
|
||||
}
|
||||
|
||||
// I am NOT happy about doing async this way. Async needs to be added to Step framework
|
||||
string getKey() => getKeyFuncAsync().GetAwaiter().GetResult();
|
||||
|
||||
private int decrypt(string tempRipFile)
|
||||
{
|
||||
FileExt.SafeDelete(tempRipFile);
|
||||
|
||||
Console.WriteLine("Decrypting with key " + decryptKey);
|
||||
|
||||
var returnCode = 100;
|
||||
var thread = new Thread(() => returnCode = ngDecrypt());
|
||||
thread.Start();
|
||||
|
||||
double fileLen = new FileInfo(inputFileName).Length;
|
||||
while (thread.IsAlive && returnCode == 100)
|
||||
{
|
||||
Thread.Sleep(500);
|
||||
if (File.Exists(tempRipFile))
|
||||
{
|
||||
double tempLen = new FileInfo(tempRipFile).Length;
|
||||
var percentProgress = tempLen / fileLen * 100.0;
|
||||
DecryptProgressUpdate?.Invoke(this, (int)percentProgress);
|
||||
}
|
||||
}
|
||||
|
||||
return returnCode;
|
||||
}
|
||||
|
||||
private int ngDecrypt()
|
||||
{
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = DecryptSupportLibraries.mp4trackdumpPath,
|
||||
Arguments = "-c " + encodingInfo.channels + " -r " + encodingInfo.sampleRate + " \"" + inputFileName + "\""
|
||||
};
|
||||
info.EnvironmentVariables["VARIABLE"] = decryptKey;
|
||||
|
||||
var result = info.RunHidden();
|
||||
|
||||
// bad checksum -- bad decrypt key
|
||||
if (result.Output.Contains("checksums mismatch, aborting!"))
|
||||
return -99;
|
||||
|
||||
return result.ExitCode;
|
||||
}
|
||||
|
||||
// temp file names for steps 3, 4, 5
|
||||
string tempChapsGuid { get; } = Guid.NewGuid().ToString().ToUpper().Replace("-", "");
|
||||
string tempChapsPath => Path.Combine(outDir, $"tempChaps_{tempChapsGuid}.mp4");
|
||||
string mp4_file => outputFileWithNewExt(".mp4");
|
||||
string ff_txt_file => mp4_file + ".ff.txt";
|
||||
|
||||
public bool Step3_Chapterize()
|
||||
{
|
||||
var str1 = "";
|
||||
if (chapters.FirstChapter.StartTime != 0.0)
|
||||
{
|
||||
str1 = " -ss " + chapters.FirstChapter.StartTime.ToString("0.000", CultureInfo.InvariantCulture) + " -t " + chapters.LastChapter.EndTime.ToString("0.000", CultureInfo.InvariantCulture) + " ";
|
||||
}
|
||||
|
||||
var ffmpegTags = tags.GenerateFfmpegTags();
|
||||
var ffmpegChapters = chapters.GenerateFfmpegChapters();
|
||||
File.WriteAllText(ff_txt_file, ffmpegTags + ffmpegChapters);
|
||||
|
||||
var tagAndChapterInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = DecryptSupportLibraries.ffmpegPath,
|
||||
Arguments = "-y -i \"" + mp4_file + "\" -f ffmetadata -i \"" + ff_txt_file + "\" -map_metadata 1 -bsf:a aac_adtstoasc -c:a copy" + str1 + " -map 0 \"" + tempChapsPath + "\""
|
||||
};
|
||||
tagAndChapterInfo.RunHidden();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Step4_InsertCoverArt()
|
||||
{
|
||||
// save cover image as temp file
|
||||
var coverPath = Path.Combine(outDir, "cover-" + Guid.NewGuid() + ".jpg");
|
||||
FileExt.CreateFile(coverPath, coverBytes);
|
||||
|
||||
var insertCoverArtInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = DecryptSupportLibraries.atomicParsleyPath,
|
||||
Arguments = "\"" + tempChapsPath + "\" --encodingTool \"" + AppName + "\" --artwork \"" + coverPath + "\" --overWrite"
|
||||
};
|
||||
insertCoverArtInfo.RunHidden();
|
||||
|
||||
// delete temp file
|
||||
FileExt.SafeDelete(coverPath);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Step5_Cleanup()
|
||||
{
|
||||
FileExt.SafeDelete(mp4_file);
|
||||
FileExt.SafeDelete(ff_txt_file);
|
||||
FileExt.SafeMove(tempChapsPath, outputFileName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Step6_AddTags()
|
||||
{
|
||||
tags.AddAppleTags(outputFileName);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool End_CreateCue()
|
||||
{
|
||||
File.WriteAllText(outputFileWithNewExt(".cue"), Cue.CreateContents(Path.GetFileName(outputFileName), chapters));
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool End_CreateNfo()
|
||||
{
|
||||
File.WriteAllText(outputFileWithNewExt(".nfo"), NFO.CreateContents(AppName, tags, encodingInfo, chapters));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
298
AaxDecrypter/AaxcDownloadConverter.cs
Normal file
298
AaxDecrypter/AaxcDownloadConverter.cs
Normal file
@@ -0,0 +1,298 @@
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Diagnostics;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.StepRunner;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
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; }
|
||||
DownloadLicense downloadLicense { get; }
|
||||
AaxcTagLibFile aaxcTagLib { get; }
|
||||
byte[] coverArt { get; }
|
||||
void SetCoverArt(byte[] coverArt);
|
||||
void SetOutputFilename(string outFileName);
|
||||
}
|
||||
public interface IAdvancedAaxcToM4bConverter : ISimpleAaxcToM4bConverter
|
||||
{
|
||||
void Cancel();
|
||||
bool Step1_CreateDir();
|
||||
bool Step2_GetMetadata();
|
||||
bool Step3_DownloadAndCombine();
|
||||
bool Step4_RestoreMetadata();
|
||||
bool Step5_CreateCue();
|
||||
bool Step6_CreateNfo();
|
||||
bool Step7_Cleanup();
|
||||
}
|
||||
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 cacheDir { get; private set; }
|
||||
public string outputFileName { get; private set; }
|
||||
public DownloadLicense downloadLicense { get; private set; }
|
||||
public AaxcTagLibFile aaxcTagLib { get; private set; }
|
||||
public byte[] coverArt { get; private set; }
|
||||
|
||||
private StepSequence steps { get; }
|
||||
private FFMpegAaxcProcesser aaxcProcesser;
|
||||
private bool isCanceled { get; set; }
|
||||
private string jsonDownloadState => Path.Combine(cacheDir, Path.GetFileNameWithoutExtension(outputFileName) + ".json");
|
||||
private string tempFile => PathLib.ReplaceExtension(jsonDownloadState, ".aaxc");
|
||||
|
||||
public static AaxcDownloadConverter Create(string cacheDirectory, string outDirectory, DownloadLicense dlLic)
|
||||
{
|
||||
var converter = new AaxcDownloadConverter(cacheDirectory, outDirectory, dlLic);
|
||||
converter.SetOutputFilename(Path.GetTempFileName());
|
||||
return converter;
|
||||
}
|
||||
|
||||
private AaxcDownloadConverter(string cacheDirectory, string outDirectory, DownloadLicense dlLic)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(outDirectory, nameof(outDirectory));
|
||||
ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
|
||||
|
||||
if (!Directory.Exists(outDirectory))
|
||||
throw new ArgumentNullException(nameof(cacheDirectory), "Directory does not exist");
|
||||
if (!Directory.Exists(outDirectory))
|
||||
throw new ArgumentNullException(nameof(outDirectory), "Directory does not exist");
|
||||
|
||||
cacheDir = cacheDirectory;
|
||||
outDir = outDirectory;
|
||||
|
||||
steps = new StepSequence
|
||||
{
|
||||
Name = "Download and Convert Aaxc To M4b",
|
||||
|
||||
["Step 1: Create Dir"] = Step1_CreateDir,
|
||||
["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,
|
||||
["Step 7: Cleanup"] = Step7_Cleanup,
|
||||
};
|
||||
|
||||
aaxcProcesser = new FFMpegAaxcProcesser(dlLic);
|
||||
aaxcProcesser.ProgressUpdate += AaxcProcesser_ProgressUpdate;
|
||||
|
||||
downloadLicense = dlLic;
|
||||
}
|
||||
|
||||
public void SetOutputFilename(string outFileName)
|
||||
{
|
||||
outputFileName = PathLib.ReplaceExtension(outFileName, ".m4b");
|
||||
outDir = Path.GetDirectoryName(outputFileName);
|
||||
|
||||
if (File.Exists(outputFileName))
|
||||
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();
|
||||
|
||||
if (!IsSuccess)
|
||||
{
|
||||
Console.WriteLine("WARNING-Conversion failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
var speedup = (int)(aaxcTagLib.Properties.Duration.TotalSeconds / (long)Elapsed.TotalSeconds);
|
||||
Console.WriteLine("Speedup is " + speedup + "x realtime.");
|
||||
Console.WriteLine("Done");
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Step1_CreateDir()
|
||||
{
|
||||
ProcessRunner.WorkingDir = outDir;
|
||||
Directory.CreateDirectory(outDir);
|
||||
|
||||
return !isCanceled;
|
||||
}
|
||||
|
||||
public bool Step2_GetMetadata()
|
||||
{
|
||||
//Get metadata from the file over http
|
||||
|
||||
NetworkFileStreamPersister nfsPersister;
|
||||
if (File.Exists(jsonDownloadState))
|
||||
{
|
||||
nfsPersister = new NetworkFileStreamPersister(jsonDownloadState);
|
||||
//If More thaan ~1 hour has elapsed since getting the download url, it will expire.
|
||||
//The new url will be to the same file.
|
||||
nfsPersister.NetworkFileStream.SetUriForSameFile(new Uri(downloadLicense.DownloadUrl));
|
||||
}
|
||||
else
|
||||
{
|
||||
var headers = new System.Net.WebHeaderCollection();
|
||||
headers.Add("User-Agent", downloadLicense.UserAgent);
|
||||
|
||||
NetworkFileStream networkFileStream = new NetworkFileStream(tempFile, new Uri(downloadLicense.DownloadUrl), 0, headers);
|
||||
nfsPersister = new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
|
||||
}
|
||||
|
||||
var networkFile = new NetworkFileAbstraction(nfsPersister.NetworkFileStream);
|
||||
aaxcTagLib = new AaxcTagLibFile(networkFile);
|
||||
nfsPersister.Dispose();
|
||||
|
||||
|
||||
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);
|
||||
|
||||
NetworkFileStreamPersister nfsPersister;
|
||||
if (File.Exists(jsonDownloadState))
|
||||
{
|
||||
nfsPersister = new NetworkFileStreamPersister(jsonDownloadState);
|
||||
//If More thaan ~1 hour has elapsed since getting the download url, it will expire.
|
||||
//The new url will be to the same file.
|
||||
nfsPersister.NetworkFileStream.SetUriForSameFile(new Uri(downloadLicense.DownloadUrl));
|
||||
}
|
||||
else
|
||||
{
|
||||
var headers = new System.Net.WebHeaderCollection();
|
||||
headers.Add("User-Agent", downloadLicense.UserAgent);
|
||||
|
||||
NetworkFileStream networkFileStream = new NetworkFileStream(tempFile, new Uri(downloadLicense.DownloadUrl), 0, headers);
|
||||
nfsPersister = new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
|
||||
}
|
||||
|
||||
string metadataPath = Path.Combine(outDir, Path.GetFileName(outputFileName) + ".ffmeta");
|
||||
|
||||
if (downloadLicense.ChapterInfo is null)
|
||||
{
|
||||
//If we want to keep the original chapters, we need to get them from the url.
|
||||
//Ffprobe needs to seek to find metadata and it can't seek a pipe. Also, there's
|
||||
//no guarantee that enough of the file will have been downloaded at this point
|
||||
//to be able to use the cache file.
|
||||
downloadLicense.ChapterInfo = new ChapterInfo(downloadLicense.DownloadUrl);
|
||||
}
|
||||
|
||||
//Only write chapters to the metadata file. All other aaxc metadata will be
|
||||
//wiped out but is restored in Step 3.
|
||||
File.WriteAllText(metadataPath, downloadLicense.ChapterInfo.ToFFMeta(true));
|
||||
|
||||
|
||||
aaxcProcesser.ProcessBook(
|
||||
nfsPersister.NetworkFileStream,
|
||||
outputFileName,
|
||||
metadataPath)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
|
||||
nfsPersister.NetworkFileStream.Close();
|
||||
nfsPersister.Dispose();
|
||||
|
||||
FileExt.SafeDelete(metadataPath);
|
||||
|
||||
DecryptProgressUpdate?.Invoke(this, 0);
|
||||
|
||||
return aaxcProcesser.Succeeded && !isCanceled;
|
||||
}
|
||||
|
||||
private void AaxcProcesser_ProgressUpdate(object sender, AaxcProcessUpdate e)
|
||||
{
|
||||
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.ProcessPosition.TotalSeconds / aaxcTagLib.Properties.Duration.TotalSeconds;
|
||||
|
||||
DecryptProgressUpdate?.Invoke(this, (int)progressPercent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy all aacx metadata to m4b file, including cover art.
|
||||
/// </summary>
|
||||
public bool Step4_RestoreMetadata()
|
||||
{
|
||||
var outFile = new AaxcTagLibFile(outputFileName);
|
||||
outFile.CopyTagsFrom(aaxcTagLib);
|
||||
|
||||
if (outFile.AppleTags.Pictures.Length == 0 && coverArt is not null)
|
||||
{
|
||||
outFile.AddPicture(coverArt);
|
||||
}
|
||||
|
||||
outFile.Save();
|
||||
|
||||
return !isCanceled;
|
||||
}
|
||||
|
||||
public bool Step5_CreateCue()
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(outputFileName), downloadLicense.ChapterInfo));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, $"{nameof(Step5_CreateCue)}. FAILED");
|
||||
}
|
||||
return !isCanceled;
|
||||
}
|
||||
|
||||
public bool Step6_CreateNfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".nfo"), NFO.CreateContents(AppName, aaxcTagLib, downloadLicense.ChapterInfo));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, $"{nameof(Step6_CreateNfo)}. FAILED");
|
||||
}
|
||||
return !isCanceled;
|
||||
}
|
||||
|
||||
public bool Step7_Cleanup()
|
||||
{
|
||||
FileExt.SafeDelete(jsonDownloadState);
|
||||
FileExt.SafeDelete(tempFile);
|
||||
return !isCanceled;
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
isCanceled = true;
|
||||
aaxcProcesser.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
75
AaxDecrypter/AaxcTagLibFile.cs
Normal file
75
AaxDecrypter/AaxcTagLibFile.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using TagLib;
|
||||
using TagLib.Mpeg4;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class AaxcTagLibFile : TagLib.Mpeg4.File
|
||||
{
|
||||
// ©
|
||||
private const byte COPYRIGHT = 0xa9;
|
||||
|
||||
private static ReadOnlyByteVector narratorType { get; } = new ReadOnlyByteVector(COPYRIGHT, (byte)'n', (byte)'r', (byte)'t');
|
||||
private static ReadOnlyByteVector descriptionType { get; } = new ReadOnlyByteVector(COPYRIGHT, (byte)'d', (byte)'e', (byte)'s');
|
||||
private static ReadOnlyByteVector publisherType { get; } = new ReadOnlyByteVector(COPYRIGHT, (byte)'p', (byte)'u', (byte)'b');
|
||||
|
||||
public string AsciiTitleSansUnabridged => TitleSansUnabridged?.UnicodeToAscii();
|
||||
public string AsciiFirstAuthor => FirstAuthor?.UnicodeToAscii();
|
||||
public string AsciiNarrator => Narrator?.UnicodeToAscii();
|
||||
public string AsciiComment => Comment?.UnicodeToAscii();
|
||||
public string AsciiLongDescription => LongDescription?.UnicodeToAscii();
|
||||
|
||||
public AppleTag AppleTags => GetTag(TagTypes.Apple) as AppleTag;
|
||||
|
||||
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;
|
||||
private string[] _copyright => AppleTags.Copyright?.Replace("©", string.Empty)?.Replace("(P)", string.Empty)?.Split(';');
|
||||
|
||||
public string Narrator => getAppleTagsText(narratorType);
|
||||
public string LongDescription => getAppleTagsText(descriptionType);
|
||||
public string ReleaseDate => getAppleTagsText("rldt");
|
||||
public string Publisher => getAppleTagsText(publisherType);
|
||||
private string getAppleTagsText(ByteVector byteVector)
|
||||
{
|
||||
string[] text = AppleTags.GetText(byteVector);
|
||||
return text.Length == 0 ? default : text[0];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class Chapter
|
||||
{
|
||||
public Chapter(double startTime, double endTime, string title)
|
||||
{
|
||||
StartTime = startTime;
|
||||
EndTime = endTime;
|
||||
Title = title;
|
||||
}
|
||||
/// <summary>
|
||||
/// Chapter start time, in seconds.
|
||||
/// </summary>
|
||||
public double StartTime { get; private set; }
|
||||
/// <summary>
|
||||
/// Chapter end time, in seconds.
|
||||
/// </summary>
|
||||
public double EndTime { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,86 @@
|
||||
using System;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Diagnostics;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public abstract class Chapters
|
||||
public class ChapterInfo
|
||||
{
|
||||
private List<Chapter> _chapterList = new();
|
||||
private List<Chapter> _chapterList = new List<Chapter>();
|
||||
public IEnumerable<Chapter> Chapters => _chapterList.AsEnumerable();
|
||||
public int Count => _chapterList.Count;
|
||||
public Chapter FirstChapter => _chapterList[0];
|
||||
public Chapter LastChapter => _chapterList[Count - 1];
|
||||
public IEnumerable<Chapter> ChapterList => _chapterList.AsEnumerable();
|
||||
public IEnumerable<TimeSpan> GetBeginningTimes() => ChapterList.Select(c => TimeSpan.FromSeconds(c.StartTime));
|
||||
protected void AddChapter(Chapter chapter)
|
||||
{
|
||||
_chapterList.Add(chapter);
|
||||
}
|
||||
protected void AddChapters(IEnumerable<Chapter> chapters)
|
||||
public ChapterInfo() { }
|
||||
public ChapterInfo(string audiobookFile)
|
||||
{
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = DecryptSupportLibraries.ffprobePath,
|
||||
Arguments = "-loglevel panic -show_chapters -print_format json \"" + audiobookFile + "\""
|
||||
};
|
||||
|
||||
var jString = info.RunHidden().Output;
|
||||
var chapterJObject = JObject.Parse(jString);
|
||||
var chapters = chapterJObject["chapters"]
|
||||
.Select(c => new Chapter(
|
||||
c["tags"]?["title"]?.Value<string>(),
|
||||
c["start_time"].Value<double>(),
|
||||
c["end_time"].Value<double>()
|
||||
));
|
||||
|
||||
_chapterList.AddRange(chapters);
|
||||
}
|
||||
public string GenerateFfmpegChapters()
|
||||
public void AddChapter(Chapter chapter)
|
||||
{
|
||||
var stringBuilder = new StringBuilder();
|
||||
ArgumentValidator.EnsureNotNull(chapter, nameof(chapter));
|
||||
_chapterList.Add(chapter);
|
||||
}
|
||||
public string ToFFMeta(bool includeFFMetaHeader)
|
||||
{
|
||||
var ffmetaChapters = new StringBuilder();
|
||||
|
||||
foreach (Chapter c in ChapterList)
|
||||
if (includeFFMetaHeader)
|
||||
ffmetaChapters.AppendLine(";FFMETADATA1");
|
||||
|
||||
foreach (var c in Chapters)
|
||||
{
|
||||
stringBuilder.Append("[CHAPTER]\n");
|
||||
stringBuilder.Append("TIMEBASE=1/1000\n");
|
||||
stringBuilder.Append("START=" + c.StartTime * 1000 + "\n");
|
||||
stringBuilder.Append("END=" + c.EndTime * 1000 + "\n");
|
||||
stringBuilder.Append("title=" + c.Title + "\n");
|
||||
ffmetaChapters.AppendLine(c.ToFFMeta());
|
||||
}
|
||||
return ffmetaChapters.ToString();
|
||||
}
|
||||
}
|
||||
public class Chapter
|
||||
{
|
||||
public string Title { get; }
|
||||
public TimeSpan StartOffset { get; }
|
||||
public TimeSpan EndOffset { get; }
|
||||
public Chapter(string title, long startOffsetMs, long lengthMs)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrEmpty(title, nameof(title));
|
||||
ArgumentValidator.EnsureGreaterThan(startOffsetMs, nameof(startOffsetMs), -1);
|
||||
|
||||
return stringBuilder.ToString();
|
||||
// do not validate lengthMs for '> 0'. It is valid to set sections this way. eg: 11-22-63 [B005UR3VFO] by Stephen King
|
||||
|
||||
Title = title;
|
||||
StartOffset = TimeSpan.FromMilliseconds(startOffsetMs);
|
||||
EndOffset = StartOffset + TimeSpan.FromMilliseconds(lengthMs);
|
||||
}
|
||||
public Chapter(string title, double startTimeSec, double endTimeSec)
|
||||
: this(title, (long)(startTimeSec * 1000), (long)((endTimeSec - startTimeSec) * 1000))
|
||||
{
|
||||
}
|
||||
|
||||
public string ToFFMeta()
|
||||
{
|
||||
return "[CHAPTER]\n" +
|
||||
"TIMEBASE=1/1000\n" +
|
||||
"START=" + StartOffset.TotalMilliseconds + "\n" +
|
||||
"END=" + EndOffset.TotalMilliseconds + "\n" +
|
||||
"title=" + Title;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Dinah.Core;
|
||||
|
||||
@@ -8,21 +7,20 @@ namespace AaxDecrypter
|
||||
{
|
||||
public static class Cue
|
||||
{
|
||||
public static string CreateContents(string filePath, Chapters chapters)
|
||||
public static string CreateContents(string filePath, ChapterInfo chapters)
|
||||
{
|
||||
var stringBuilder = new StringBuilder();
|
||||
|
||||
stringBuilder.AppendLine(GetFileLine(filePath, "MP3"));
|
||||
|
||||
var trackCount = 0;
|
||||
foreach (Chapter c in chapters.ChapterList)
|
||||
foreach (var c in chapters.Chapters)
|
||||
{
|
||||
trackCount++;
|
||||
var startTime = TimeSpan.FromSeconds(c.StartTime);
|
||||
|
||||
stringBuilder.AppendLine($"TRACK {trackCount} AUDIO");
|
||||
stringBuilder.AppendLine($" TITLE \"{c.Title}\"");
|
||||
stringBuilder.AppendLine($" INDEX 01 {(int)startTime.TotalMinutes}:{startTime:ss\\:ff}");
|
||||
stringBuilder.AppendLine($" INDEX 01 {(int)c.StartOffset.TotalMinutes}:{c.StartOffset:ss\\:ff}");
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/avdevice-58.dll
Normal file
BIN
AaxDecrypter/DecryptLib/avdevice-58.dll
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/avformat-58.dll
Normal file
BIN
AaxDecrypter/DecryptLib/avformat-58.dll
Normal file
Binary file not shown.
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/avutil-56.dll
Normal file
BIN
AaxDecrypter/DecryptLib/avutil-56.dll
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/swresample-3.dll
Normal file
BIN
AaxDecrypter/DecryptLib/swresample-3.dll
Normal file
Binary file not shown.
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/swscale-5.dll
Normal file
BIN
AaxDecrypter/DecryptLib/swscale-5.dll
Normal file
Binary file not shown.
Binary file not shown.
@@ -6,16 +6,11 @@ namespace AaxDecrypter
|
||||
{
|
||||
// OTHER EXTERNAL DEPENDENCIES
|
||||
// ffprobe has these pre-req.s as I'm using it:
|
||||
// avcodec-57.dll, avdevice-57.dll, avfilter-6.dll, avformat-57.dll, avutil-55.dll, postproc-54.dll, swresample-2.dll, swscale-4.dll, taglib-sharp.dll
|
||||
//
|
||||
// something else needs the cygwin files (cyg*.dll)
|
||||
// avcodec-58.dll, avdevice-58.dll, avfilter-7.dll, avformat-58.dll, avutil-56.dll, swresample-3.dll, swscale-5.dll, taglib-sharp.dll
|
||||
|
||||
private static string appPath_ { get; } = Path.GetDirectoryName(Dinah.Core.Exe.FileLocationOnDisk);
|
||||
private static string decryptLib_ { get; } = Path.Combine(appPath_, "DecryptLib");
|
||||
|
||||
public static string ffmpegPath { get; } = Path.Combine(decryptLib_, "ffmpeg.exe");
|
||||
public static string ffprobePath { get; } = Path.Combine(decryptLib_, "ffprobe.exe");
|
||||
public static string atomicParsleyPath { get; } = Path.Combine(decryptLib_, "AtomicParsley.exe");
|
||||
public static string mp4trackdumpPath { get; } = Path.Combine(decryptLib_, "mp4trackdump.exe");
|
||||
}
|
||||
}
|
||||
}
|
||||
26
AaxDecrypter/DownloadLicense.cs
Normal file
26
AaxDecrypter/DownloadLicense.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Dinah.Core;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class DownloadLicense
|
||||
{
|
||||
public string DownloadUrl { get; }
|
||||
public string AudibleKey { get; }
|
||||
public string AudibleIV { get; }
|
||||
public string UserAgent { get; }
|
||||
public ChapterInfo ChapterInfo { get; set; }
|
||||
|
||||
public DownloadLicense(string downloadUrl, string audibleKey, string audibleIV, string userAgent)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
|
||||
ArgumentValidator.EnsureNotNullOrEmpty(audibleKey, nameof(audibleKey));
|
||||
ArgumentValidator.EnsureNotNullOrEmpty(audibleIV, nameof(audibleIV));
|
||||
ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
|
||||
|
||||
DownloadUrl = downloadUrl;
|
||||
AudibleKey = audibleKey;
|
||||
AudibleIV = audibleIV;
|
||||
UserAgent = userAgent;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Dinah.Core.Diagnostics;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class EncodingInfo
|
||||
{
|
||||
public int sampleRate { get; } = 44100;
|
||||
public int channels { get; } = 2;
|
||||
public int originalBitrate { get; }
|
||||
|
||||
public EncodingInfo(string file)
|
||||
{
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = DecryptSupportLibraries.ffprobePath,
|
||||
Arguments = "-loglevel panic -show_streams -print_format flat \"" + file + "\""
|
||||
};
|
||||
var end = info.RunHidden().Output;
|
||||
|
||||
foreach (string str2 in end.Split('\n'))
|
||||
{
|
||||
string[] strArray = str2.Split('=');
|
||||
switch (strArray[0])
|
||||
{
|
||||
case "streams.stream.0.channels":
|
||||
this.channels = int.Parse(strArray[1].Replace("\"", "").TrimEnd('\r', '\n'));
|
||||
break;
|
||||
case "streams.stream.0.sample_rate":
|
||||
this.sampleRate = int.Parse(strArray[1].Replace("\"", "").TrimEnd('\r', '\n'));
|
||||
break;
|
||||
case "streams.stream.0.bit_rate":
|
||||
string s = strArray[1].Replace("\"", "").TrimEnd('\r', '\n');
|
||||
this.originalBitrate = (int)Math.Round(double.Parse(s) / 1000.0, MidpointRounding.AwayFromZero);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
254
AaxDecrypter/FFMpegAaxcProcesser.cs
Normal file
254
AaxDecrypter/FFMpegAaxcProcesser.cs
Normal file
@@ -0,0 +1,254 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
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<AaxcProcessUpdate> ProgressUpdate;
|
||||
public string FFMpegPath { get; }
|
||||
public DownloadLicense DownloadLicense { get; }
|
||||
public bool IsRunning { get; private set; }
|
||||
public bool Succeeded { get; private set; }
|
||||
public string FFMpegRemuxerStandardError => remuxerError.ToString();
|
||||
public string FFMpegDecrypterStandardError => decrypterError.ToString();
|
||||
|
||||
|
||||
private StringBuilder remuxerError { get; } = new StringBuilder();
|
||||
private StringBuilder decrypterError { get; } = new StringBuilder();
|
||||
private static Regex processedTimeRegex { get; } = 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 decrypter;
|
||||
private Process remuxer;
|
||||
private Stream inputFile;
|
||||
private bool isCanceled = false;
|
||||
|
||||
public FFMpegAaxcProcesser(DownloadLicense downloadLicense)
|
||||
{
|
||||
FFMpegPath = DecryptSupportLibraries.ffmpegPath;
|
||||
DownloadLicense = downloadLicense;
|
||||
}
|
||||
|
||||
public async Task ProcessBook(Stream inputFile, string outputFile, string ffmetaChaptersPath)
|
||||
{
|
||||
this.inputFile = inputFile;
|
||||
|
||||
//This process gets the aaxc from the url and streams the decrypted
|
||||
//aac stream to standard output
|
||||
decrypter = new Process
|
||||
{
|
||||
StartInfo = getDownloaderStartInfo()
|
||||
};
|
||||
|
||||
//This process retreves an aac stream from standard input and muxes
|
||||
// it into an m4b along with the cover art and metadata.
|
||||
remuxer = new Process
|
||||
{
|
||||
StartInfo = getRemuxerStartInfo(outputFile, ffmetaChaptersPath)
|
||||
};
|
||||
|
||||
IsRunning = true;
|
||||
|
||||
decrypter.ErrorDataReceived += Downloader_ErrorDataReceived;
|
||||
decrypter.Start();
|
||||
decrypter.BeginErrorReadLine();
|
||||
|
||||
remuxer.ErrorDataReceived += Remuxer_ErrorDataReceived;
|
||||
remuxer.Start();
|
||||
remuxer.BeginErrorReadLine();
|
||||
|
||||
//Thic check needs to be placed after remuxer has started.
|
||||
if (isCanceled) return;
|
||||
|
||||
var decrypterInput = decrypter.StandardInput.BaseStream;
|
||||
var decrypterOutput = decrypter.StandardOutput.BaseStream;
|
||||
var remuxerInput = remuxer.StandardInput.BaseStream;
|
||||
|
||||
//Read inputFile into decrypter stdin in the background
|
||||
var t = new Thread(() => CopyStream(inputFile, decrypterInput, decrypter));
|
||||
t.Start();
|
||||
|
||||
//All the work done here. Copy download standard output into
|
||||
//remuxer standard input
|
||||
await Task.Run(() => CopyStream(decrypterOutput, remuxerInput, remuxer));
|
||||
|
||||
//If the remuxer exited due to failure, downloader will still have
|
||||
//data in the pipe. Force kill downloader to continue.
|
||||
if (remuxer.HasExited && !decrypter.HasExited)
|
||||
decrypter.Kill();
|
||||
|
||||
remuxer.WaitForExit();
|
||||
decrypter.WaitForExit();
|
||||
|
||||
IsRunning = false;
|
||||
Succeeded = decrypter.ExitCode == 0 && remuxer.ExitCode == 0;
|
||||
}
|
||||
|
||||
private void CopyStream(Stream inputStream, Stream outputStream, Process returnOnProcExit)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] buffer = new byte[32 * 1024];
|
||||
int lastRead;
|
||||
do
|
||||
{
|
||||
lastRead = inputStream.Read(buffer, 0, buffer.Length);
|
||||
outputStream.Write(buffer, 0, lastRead);
|
||||
} while (lastRead > 0 && !returnOnProcExit.HasExited);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
//There is no way to tell if the process closed the input stream
|
||||
//before trying to write to it. If it did close, throws IOException.
|
||||
isCanceled = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
outputStream.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
isCanceled = true;
|
||||
|
||||
if (IsRunning && !remuxer.HasExited)
|
||||
remuxer.Kill();
|
||||
if (IsRunning && !decrypter.HasExited)
|
||||
decrypter.Kill();
|
||||
inputFile?.Close();
|
||||
}
|
||||
private void Downloader_ErrorDataReceived(object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrEmpty(e.Data))
|
||||
return;
|
||||
|
||||
decrypterError.AppendLine(e.Data);
|
||||
}
|
||||
|
||||
private void Remuxer_ErrorDataReceived(object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrEmpty(e.Data))
|
||||
return;
|
||||
|
||||
remuxerError.AppendLine(e.Data);
|
||||
|
||||
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);
|
||||
int minutes = int.Parse(match.Groups[2].Value);
|
||||
int seconds = int.Parse(match.Groups[3].Value);
|
||||
|
||||
var position = new TimeSpan(hours, minutes, seconds);
|
||||
|
||||
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"))
|
||||
{
|
||||
//This happens if input is corrupt (should never happen) or if caller
|
||||
//supplied wrong key/iv
|
||||
var process = sender as Process;
|
||||
process.Kill();
|
||||
}
|
||||
}
|
||||
|
||||
private ProcessStartInfo getDownloaderStartInfo() =>
|
||||
new ProcessStartInfo
|
||||
{
|
||||
FileName = FFMpegPath,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = Path.GetDirectoryName(FFMpegPath),
|
||||
ArgumentList ={
|
||||
"-audible_key",
|
||||
DownloadLicense.AudibleKey,
|
||||
"-audible_iv",
|
||||
DownloadLicense.AudibleIV,
|
||||
"-f",
|
||||
"mp4",
|
||||
"-i",
|
||||
"pipe:",
|
||||
"-c:a", //audio codec
|
||||
"copy", //copy stream
|
||||
"-f", //force output format: adts
|
||||
"adts",
|
||||
"pipe:" //pipe output to stdout
|
||||
}
|
||||
};
|
||||
|
||||
private ProcessStartInfo getRemuxerStartInfo(string outputFile, string ffmetaChaptersPath = null)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = FFMpegPath,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = Path.GetDirectoryName(FFMpegPath),
|
||||
};
|
||||
|
||||
startInfo.ArgumentList.Add("-thread_queue_size");
|
||||
startInfo.ArgumentList.Add("1024");
|
||||
startInfo.ArgumentList.Add("-f"); //force input format: aac
|
||||
startInfo.ArgumentList.Add("aac");
|
||||
startInfo.ArgumentList.Add("-i"); //read input from stdin
|
||||
startInfo.ArgumentList.Add("pipe:");
|
||||
|
||||
//copy metadata from supplied metadata file
|
||||
startInfo.ArgumentList.Add("-f");
|
||||
startInfo.ArgumentList.Add("ffmetadata");
|
||||
startInfo.ArgumentList.Add("-i");
|
||||
startInfo.ArgumentList.Add(ffmetaChaptersPath);
|
||||
|
||||
startInfo.ArgumentList.Add("-map"); //map file 0 (aac audio stream)
|
||||
startInfo.ArgumentList.Add("0");
|
||||
startInfo.ArgumentList.Add("-map_chapters"); //copy chapter data from file metadata file
|
||||
startInfo.ArgumentList.Add("1");
|
||||
startInfo.ArgumentList.Add("-c"); //copy all mapped streams
|
||||
startInfo.ArgumentList.Add("copy");
|
||||
startInfo.ArgumentList.Add("-f"); //force output format: mp4
|
||||
startInfo.ArgumentList.Add("mp4");
|
||||
startInfo.ArgumentList.Add("-movflags");
|
||||
startInfo.ArgumentList.Add("disable_chpl"); //Disable Nero chapters format
|
||||
startInfo.ArgumentList.Add(outputFile);
|
||||
startInfo.ArgumentList.Add("-y"); //overwrite existing
|
||||
|
||||
return startInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,52 @@
|
||||
namespace AaxDecrypter
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public static class NFO
|
||||
{
|
||||
public static string CreateContents(string ripper, Tags tags, EncodingInfo encodingInfo, Chapters chapters)
|
||||
public static string CreateContents(string ripper, AaxcTagLibFile aaxcTagLib, ChapterInfo chapters)
|
||||
{
|
||||
var _hours = (int)tags.duration.TotalHours;
|
||||
var _hours = (int)aaxcTagLib.Properties.Duration.TotalHours;
|
||||
var myDuration
|
||||
= (_hours > 0 ? _hours + " hours, " : "")
|
||||
+ tags.duration.Minutes + " minutes, "
|
||||
+ tags.duration.Seconds + " seconds";
|
||||
= (_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: {tags.title}\r\n"
|
||||
+ $" Author: {tags.author}\r\n"
|
||||
+ $" Read By: {tags.narrator}\r\n"
|
||||
+ $" Copyright: {tags.year}\r\n"
|
||||
+ $" Audiobook Copyright: {tags.year}\r\n";
|
||||
if (tags.genre != "")
|
||||
header += $" Genre: {tags.genre}\r\n";
|
||||
|
||||
var s
|
||||
= header
|
||||
+ $" Publisher: {tags.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: {encodingInfo.sampleRate} Hz\r\n"
|
||||
+ $" Source Channels: {encodingInfo.channels}\r\n"
|
||||
+ $" Source Bitrate: {encodingInfo.originalBitrate} kbits\r\n"
|
||||
+ $" Source Sample Rate: {aaxcTagLib.Properties.AudioSampleRate} Hz\r\n"
|
||||
+ $" Source Channels: {aaxcTagLib.Properties.AudioChannels}\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: {encodingInfo.sampleRate} Hz\r\n"
|
||||
+ $" Encoded Channels: {encodingInfo.channels}\r\n"
|
||||
+ $" Encoded Bitrate: {encodingInfo.originalBitrate} kbits\r\n"
|
||||
+ $" Encoded Sample Rate: {aaxcTagLib.Properties.AudioSampleRate} Hz\r\n"
|
||||
+ $" Encoded Channels: {aaxcTagLib.Properties.AudioChannels}\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"
|
||||
+ tags.comments;
|
||||
+ (!string.IsNullOrWhiteSpace(aaxcTagLib.LongDescription) ? aaxcTagLib.AsciiLongDescription : aaxcTagLib.AsciiComment);
|
||||
|
||||
return s;
|
||||
return nfoString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
AaxDecrypter/NetworkFileAbstraction.cs
Normal file
31
AaxDecrypter/NetworkFileAbstraction.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a <see cref="TagLib.File.IFileAbstraction"/> for a file over Http.
|
||||
/// </summary>
|
||||
class NetworkFileAbstraction : TagLib.File.IFileAbstraction
|
||||
{
|
||||
private NetworkFileStream aaxNetworkStream;
|
||||
|
||||
public NetworkFileAbstraction( NetworkFileStream networkFileStream)
|
||||
{
|
||||
Name = networkFileStream.SaveFilePath;
|
||||
aaxNetworkStream = networkFileStream;
|
||||
}
|
||||
public string Name { get; private set; }
|
||||
|
||||
public Stream ReadStream => aaxNetworkStream;
|
||||
|
||||
public Stream WriteStream => throw new NotImplementedException();
|
||||
|
||||
public void CloseStream(Stream stream)
|
||||
{
|
||||
aaxNetworkStream.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
455
AaxDecrypter/NetworkFileStream.cs
Normal file
455
AaxDecrypter/NetworkFileStream.cs
Normal file
@@ -0,0 +1,455 @@
|
||||
using Dinah.Core;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="CookieContainer"/> for a single Uri.
|
||||
/// </summary>
|
||||
public class SingleUriCookieContainer : CookieContainer
|
||||
{
|
||||
private Uri baseAddress;
|
||||
public Uri Uri
|
||||
{
|
||||
get => baseAddress;
|
||||
set
|
||||
{
|
||||
baseAddress = new UriBuilder(value.Scheme, value.Host).Uri;
|
||||
}
|
||||
}
|
||||
|
||||
public CookieCollection GetCookies()
|
||||
{
|
||||
return base.GetCookies(Uri);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A resumable, simultaneous file downloader and reader.
|
||||
/// </summary>
|
||||
public class NetworkFileStream : Stream, IUpdatable
|
||||
{
|
||||
public event EventHandler Updated;
|
||||
|
||||
#region Public Properties
|
||||
|
||||
/// <summary>
|
||||
/// Location to save the downloaded data.
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public string SaveFilePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Http(s) address of the file to download.
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public Uri Uri { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// All cookies set by caller or by the remote server.
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public SingleUriCookieContainer CookieContainer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Http headers to be sent to the server with the request.
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public WebHeaderCollection RequestHeaders { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The position in <see cref="SaveFilePath"/> that has been written and flushed to disk.
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public long WritePosition { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total length of the <see cref="Uri"/> file to download.
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public long ContentLength { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Properties
|
||||
|
||||
private HttpWebRequest HttpRequest { get; set; }
|
||||
private FileStream _writeFile { get; }
|
||||
private FileStream _readFile { get; }
|
||||
private Stream _networkStream { get; set; }
|
||||
private bool hasBegunDownloading { get; set; }
|
||||
private bool isCancelled { get; set; }
|
||||
private bool finishedDownloading { get; set; }
|
||||
private Action downloadThreadCompleteCallback { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constants
|
||||
|
||||
//Download buffer size
|
||||
private const int DOWNLOAD_BUFF_SZ = 4 * 1024;
|
||||
|
||||
//NetworkFileStream will flush all data in _writeFile to disk after every
|
||||
//DATA_FLUSH_SZ bytes are written to the file stream.
|
||||
private const int DATA_FLUSH_SZ = 1024 * 1024;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
/// <summary>
|
||||
/// A resumable, simultaneous file downloader and reader.
|
||||
/// </summary>
|
||||
/// <param name="saveFilePath">Path to a location on disk to save the downloaded data from <paramref name="uri"/></param>
|
||||
/// <param name="uri">Http(s) address of the file to download.</param>
|
||||
/// <param name="writePosition">The position in <paramref name="uri"/> to begin downloading.</param>
|
||||
/// <param name="requestHeaders">Http headers to be sent to the server with the <see cref="HttpWebRequest"/>.</param>
|
||||
/// <param name="cookies">A <see cref="SingleUriCookieContainer"/> with cookies to send with the <see cref="HttpWebRequest"/>. It will also be populated with any cookies set by the server. </param>
|
||||
public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, WebHeaderCollection requestHeaders = null, SingleUriCookieContainer cookies = null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath));
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri));
|
||||
ArgumentValidator.EnsureGreaterThan(writePosition, nameof(writePosition), -1);
|
||||
|
||||
if (!Directory.Exists(Path.GetDirectoryName(saveFilePath)))
|
||||
throw new ArgumentException($"Specified {nameof(saveFilePath)} directory \"{Path.GetDirectoryName(saveFilePath)}\" does not exist.");
|
||||
|
||||
SaveFilePath = saveFilePath;
|
||||
Uri = uri;
|
||||
WritePosition = writePosition;
|
||||
RequestHeaders = requestHeaders ?? new WebHeaderCollection();
|
||||
CookieContainer = cookies ?? new SingleUriCookieContainer { Uri = uri };
|
||||
|
||||
_writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)
|
||||
{
|
||||
Position = WritePosition
|
||||
};
|
||||
|
||||
_readFile = new FileStream(SaveFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
|
||||
SetUriForSameFile(uri);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Downloader
|
||||
|
||||
/// <summary>
|
||||
/// Update the <see cref="JsonFilePersister"/>.
|
||||
/// </summary>
|
||||
private void Update()
|
||||
{
|
||||
RequestHeaders = HttpRequest.Headers;
|
||||
Updated?.Invoke(this, new EventArgs());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a different <see cref="System.Uri"/> to the same file targeted by this instance of <see cref="NetworkFileStream"/>
|
||||
/// </summary>
|
||||
/// <param name="uriToSameFile">New <see cref="System.Uri"/> host must match existing host.</param>
|
||||
public void SetUriForSameFile(Uri uriToSameFile)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(uriToSameFile?.AbsoluteUri, nameof(uriToSameFile));
|
||||
|
||||
if (uriToSameFile.Host != Uri.Host)
|
||||
throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}");
|
||||
if (hasBegunDownloading && !finishedDownloading)
|
||||
throw new Exception("Cannot change Uri during a download operation.");
|
||||
|
||||
Uri = uriToSameFile;
|
||||
HttpRequest = WebRequest.CreateHttp(Uri);
|
||||
|
||||
HttpRequest.CookieContainer = CookieContainer;
|
||||
HttpRequest.Headers = RequestHeaders;
|
||||
//If NetworkFileStream is resuming, Header will already contain a range.
|
||||
HttpRequest.Headers.Remove("Range");
|
||||
HttpRequest.AddRange(WritePosition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread.
|
||||
/// </summary>
|
||||
private void BeginDownloading()
|
||||
{
|
||||
if (ContentLength != 0 && WritePosition == ContentLength)
|
||||
{
|
||||
hasBegunDownloading = true;
|
||||
finishedDownloading = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ContentLength != 0 && WritePosition > ContentLength)
|
||||
throw new Exception($"Specified write position (0x{WritePosition:X10}) is larger than the file size.");
|
||||
|
||||
var response = HttpRequest.GetResponse() as HttpWebResponse;
|
||||
|
||||
if (response.StatusCode != HttpStatusCode.PartialContent)
|
||||
throw new Exception($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}.");
|
||||
|
||||
if (response.Headers.GetValues("Accept-Ranges").FirstOrDefault(r => r.EqualsInsensitive("bytes")) is null)
|
||||
throw new Exception($"Server at {Uri.Host} does not support Http ranges");
|
||||
|
||||
//Content length is the length of the range request, and it is only equal
|
||||
//to the complete file length if requesting Range: bytes=0-
|
||||
if (WritePosition == 0)
|
||||
ContentLength = response.ContentLength;
|
||||
|
||||
_networkStream = response.GetResponseStream();
|
||||
|
||||
//Download the file in the background.
|
||||
Thread downloadThread = new Thread(() => DownloadFile());
|
||||
downloadThread.Start();
|
||||
|
||||
hasBegunDownloading = true;
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downlod <see cref="Uri"/> to <see cref="SaveFilePath"/>.
|
||||
/// </summary>
|
||||
private void DownloadFile()
|
||||
{
|
||||
long downloadPosition = WritePosition;
|
||||
long nextFlush = downloadPosition + DATA_FLUSH_SZ;
|
||||
|
||||
byte[] buff = new byte[DOWNLOAD_BUFF_SZ];
|
||||
do
|
||||
{
|
||||
int bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
|
||||
_writeFile.Write(buff, 0, bytesRead);
|
||||
|
||||
downloadPosition += bytesRead;
|
||||
|
||||
if (downloadPosition > nextFlush)
|
||||
{
|
||||
_writeFile.Flush();
|
||||
WritePosition = downloadPosition;
|
||||
Update();
|
||||
nextFlush = downloadPosition + DATA_FLUSH_SZ;
|
||||
}
|
||||
|
||||
} while (downloadPosition < ContentLength && !isCancelled);
|
||||
|
||||
_writeFile.Close();
|
||||
WritePosition = downloadPosition;
|
||||
Update();
|
||||
_networkStream.Close();
|
||||
|
||||
if (!isCancelled && WritePosition < ContentLength)
|
||||
throw new Exception("File download ended before finishing.");
|
||||
|
||||
if (WritePosition > ContentLength)
|
||||
throw new Exception("Downloaded file is larger than expected.");
|
||||
|
||||
finishedDownloading = true;
|
||||
downloadThreadCompleteCallback?.Invoke();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Json Connverters
|
||||
|
||||
public static JsonSerializerSettings GetJsonSerializerSettings()
|
||||
{
|
||||
var settings = new JsonSerializerSettings();
|
||||
settings.Converters.Add(new CookieContainerConverter());
|
||||
settings.Converters.Add(new WebHeaderCollectionConverter());
|
||||
return settings;
|
||||
}
|
||||
|
||||
internal class CookieContainerConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
=> objectType == typeof(SingleUriCookieContainer);
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var jObj = JObject.Load(reader);
|
||||
|
||||
var result = new SingleUriCookieContainer()
|
||||
{
|
||||
Uri = new Uri(jObj["Uri"].Value<string>()),
|
||||
Capacity = jObj["Capacity"].Value<int>(),
|
||||
MaxCookieSize = jObj["MaxCookieSize"].Value<int>(),
|
||||
PerDomainCapacity = jObj["PerDomainCapacity"].Value<int>()
|
||||
};
|
||||
|
||||
var cookieList = jObj["Cookies"].ToList();
|
||||
|
||||
foreach (var cookie in cookieList)
|
||||
{
|
||||
result.Add(
|
||||
new Cookie
|
||||
{
|
||||
Comment = cookie["Comment"].Value<string>(),
|
||||
HttpOnly = cookie["HttpOnly"].Value<bool>(),
|
||||
Discard = cookie["Discard"].Value<bool>(),
|
||||
Domain = cookie["Domain"].Value<string>(),
|
||||
Expired = cookie["Expired"].Value<bool>(),
|
||||
Expires = cookie["Expires"].Value<DateTime>(),
|
||||
Name = cookie["Name"].Value<string>(),
|
||||
Path = cookie["Path"].Value<string>(),
|
||||
Port = cookie["Port"].Value<string>(),
|
||||
Secure = cookie["Secure"].Value<bool>(),
|
||||
Value = cookie["Value"].Value<string>(),
|
||||
Version = cookie["Version"].Value<int>(),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
var cookies = value as SingleUriCookieContainer;
|
||||
var obj = (JObject)JToken.FromObject(value);
|
||||
var container = cookies.GetCookies();
|
||||
var propertyNames = container.Select(c => JToken.FromObject(c));
|
||||
obj.AddFirst(new JProperty("Cookies", new JArray(propertyNames)));
|
||||
obj.WriteTo(writer);
|
||||
}
|
||||
}
|
||||
|
||||
internal class WebHeaderCollectionConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
=> objectType == typeof(WebHeaderCollection);
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var jObj = JObject.Load(reader);
|
||||
var result = new WebHeaderCollection();
|
||||
|
||||
foreach (var kvp in jObj)
|
||||
{
|
||||
result.Add(kvp.Key, kvp.Value.Value<string>());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
JObject jObj = new JObject();
|
||||
Type type = value.GetType();
|
||||
var headers = value as WebHeaderCollection;
|
||||
var jHeaders = headers.AllKeys.Select(k => new JProperty(k, headers[k]));
|
||||
jObj.Add(jHeaders);
|
||||
jObj.WriteTo(writer);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Download Stream Reader
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool CanRead => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool CanSeek => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool CanWrite => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override long Length => ContentLength;
|
||||
|
||||
[JsonIgnore]
|
||||
public override long Position { get => _readFile.Position; set => Seek(value, SeekOrigin.Begin); }
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool CanTimeout => base.CanTimeout;
|
||||
|
||||
[JsonIgnore]
|
||||
public override int ReadTimeout { get => base.ReadTimeout; set => base.ReadTimeout = value; }
|
||||
|
||||
[JsonIgnore]
|
||||
public override int WriteTimeout { get => base.WriteTimeout; set => base.WriteTimeout = value; }
|
||||
|
||||
public override void Flush() => throw new NotImplementedException();
|
||||
public override void SetLength(long value) => throw new NotImplementedException();
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (!hasBegunDownloading)
|
||||
BeginDownloading();
|
||||
|
||||
long toRead = Math.Min(count, Length - Position);
|
||||
long requiredPosition = Position + toRead;
|
||||
|
||||
//read operation will block until file contains enough data
|
||||
//to fulfil the request, or until cancelled.
|
||||
while (requiredPosition > WritePosition && !isCancelled)
|
||||
Thread.Sleep(0);
|
||||
|
||||
return _readFile.Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
long newPosition;
|
||||
|
||||
switch (origin)
|
||||
{
|
||||
case SeekOrigin.Current:
|
||||
newPosition = Position + offset;
|
||||
break;
|
||||
case SeekOrigin.End:
|
||||
newPosition = ContentLength + offset;
|
||||
break;
|
||||
default:
|
||||
newPosition = offset;
|
||||
break;
|
||||
}
|
||||
ReadToPosition(newPosition);
|
||||
|
||||
_readFile.Position = newPosition;
|
||||
return newPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the file has downloaded to at least <paramref name="neededPosition"/>, then returns.
|
||||
/// </summary>
|
||||
/// <param name="neededPosition">The minimum required data length in <see cref="SaveFilePath"/>.</param>
|
||||
private void ReadToPosition(long neededPosition)
|
||||
{
|
||||
byte[] buff = new byte[DOWNLOAD_BUFF_SZ];
|
||||
do
|
||||
{
|
||||
Read(buff, 0, DOWNLOAD_BUFF_SZ);
|
||||
} while (neededPosition > WritePosition);
|
||||
}
|
||||
public override void Close()
|
||||
{
|
||||
isCancelled = true;
|
||||
downloadThreadCompleteCallback = CloseAction;
|
||||
|
||||
//ensure that close will run even if called after callback was fired.
|
||||
if (finishedDownloading)
|
||||
CloseAction();
|
||||
|
||||
}
|
||||
private void CloseAction()
|
||||
{
|
||||
_readFile.Close();
|
||||
_writeFile.Close();
|
||||
_networkStream?.Close();
|
||||
Update();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
23
AaxDecrypter/NetworkFileStreamPersister.cs
Normal file
23
AaxDecrypter/NetworkFileStreamPersister.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Dinah.Core.IO;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
internal class NetworkFileStreamPersister : JsonFilePersister<NetworkFileStream>
|
||||
{
|
||||
|
||||
/// <summary>Alias for Target </summary>
|
||||
public NetworkFileStream NetworkFileStream => Target;
|
||||
|
||||
/// <summary>uses path. create file if doesn't yet exist</summary>
|
||||
public NetworkFileStreamPersister(NetworkFileStream networkFileStream, string path, string jsonPath = null)
|
||||
: base(networkFileStream, path, jsonPath) { }
|
||||
|
||||
/// <summary>load from existing file</summary>
|
||||
public NetworkFileStreamPersister(string path, string jsonPath = null)
|
||||
: base(path, jsonPath) { }
|
||||
|
||||
protected override JsonSerializerSettings GetSerializerSettings() => NetworkFileStream.GetJsonSerializerSettings();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using System;
|
||||
using TagLib;
|
||||
using TagLib.Mpeg4;
|
||||
using Dinah.Core;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class Tags
|
||||
{
|
||||
public string title { get; }
|
||||
public string album { get; }
|
||||
public string author { get; }
|
||||
public string comments { get; }
|
||||
public string narrator { get; }
|
||||
public string year { get; }
|
||||
public string publisher { get; }
|
||||
public string id { get; }
|
||||
public string genre { get; }
|
||||
public TimeSpan duration { get; }
|
||||
|
||||
// input file
|
||||
public Tags(string file)
|
||||
{
|
||||
using var tagLibFile = TagLib.File.Create(file, "audio/mp4", ReadStyle.Average);
|
||||
title = tagLibFile.Tag.Title.Replace(" (Unabridged)", "");
|
||||
album = tagLibFile.Tag.Album.Replace(" (Unabridged)", "");
|
||||
author = tagLibFile.Tag.FirstPerformer ?? "[unknown]";
|
||||
year = tagLibFile.Tag.Year.ToString();
|
||||
comments = tagLibFile.Tag.Comment ?? "";
|
||||
duration = tagLibFile.Properties.Duration;
|
||||
genre = tagLibFile.Tag.FirstGenre ?? "";
|
||||
|
||||
var tag = tagLibFile.GetTag(TagTypes.Apple, true);
|
||||
publisher = tag.Publisher ?? "";
|
||||
narrator = string.IsNullOrWhiteSpace(tagLibFile.Tag.FirstComposer) ? tag.Narrator : tagLibFile.Tag.FirstComposer;
|
||||
comments = !string.IsNullOrWhiteSpace(tag.LongDescription) ? tag.LongDescription : tag.Description;
|
||||
id = tag.AudibleCDEK;
|
||||
}
|
||||
|
||||
// my best guess of what this step is doing:
|
||||
// re-publish the data we read from the input file => output file
|
||||
public void AddAppleTags(string file)
|
||||
{
|
||||
using var tagLibFile = TagLib.File.Create(file, "audio/mp4", ReadStyle.Average);
|
||||
var tag = (AppleTag)tagLibFile.GetTag(TagTypes.Apple, true);
|
||||
tag.Publisher = publisher;
|
||||
tag.LongDescription = comments;
|
||||
tag.Description = comments;
|
||||
tagLibFile.Save();
|
||||
}
|
||||
|
||||
public string GenerateFfmpegTags()
|
||||
=> $";FFMETADATA1"
|
||||
+ $"\nmajor_brand=aax"
|
||||
+ $"\nminor_version=1"
|
||||
+ $"\ncompatible_brands=aax M4B mp42isom"
|
||||
+ $"\ndate={year}"
|
||||
+ $"\ngenre={genre}"
|
||||
+ $"\ntitle={title}"
|
||||
+ $"\nartist={author}"
|
||||
+ $"\nalbum={album}"
|
||||
+ $"\ncomposer={narrator}"
|
||||
+ $"\ncomment={comments.Truncate(254)}"
|
||||
+ $"\ndescription={comments}"
|
||||
+ $"\n";
|
||||
}
|
||||
}
|
||||
387
DataLayer/Migrations/20210619030017_AddAaxcDecryptionKeys.Designer.cs
generated
Normal file
387
DataLayer/Migrations/20210619030017_AddAaxcDecryptionKeys.Designer.cs
generated
Normal file
@@ -0,0 +1,387 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DataLayer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LibationContext))]
|
||||
[Migration("20210619030017_AddAaxcDecryptionKeys")]
|
||||
partial class AddAaxcDecryptionKeys
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "5.0.5");
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleIV")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AudibleKey")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AudibleProductId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DatePublished")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsAbridged")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("LengthInMinutes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Locale")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PictureId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.HasIndex("AudibleProductId");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.ToTable("Books");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ContributorId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte>("Order")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("BookId", "ContributorId", "Role");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("ContributorId");
|
||||
|
||||
b.ToTable("BookContributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.Property<int>("CategoryId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleCategoryId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("ParentCategoryCategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("CategoryId");
|
||||
|
||||
b.HasIndex("AudibleCategoryId");
|
||||
|
||||
b.HasIndex("ParentCategoryCategoryId");
|
||||
|
||||
b.ToTable("Categories");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
CategoryId = -1,
|
||||
AudibleCategoryId = "",
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Property<int>("ContributorId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleContributorId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ContributorId");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.ToTable("Contributors");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
ContributorId = -1,
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Account")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.ToTable("Library");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleSeriesId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SeriesId");
|
||||
|
||||
b.HasIndex("AudibleSeriesId");
|
||||
|
||||
b.ToTable("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float?>("Index")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("SeriesId", "BookId");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("SeriesBook");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<float>("OverallRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.Property<float>("PerformanceRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.Property<float>("StoryRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("Books");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("BookId");
|
||||
});
|
||||
|
||||
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
|
||||
{
|
||||
b1.Property<int>("SupplementId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Url")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("SupplementId");
|
||||
|
||||
b1.HasIndex("BookId");
|
||||
|
||||
b1.ToTable("Supplement");
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.Navigation("Book");
|
||||
});
|
||||
|
||||
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Tags")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("UserDefinedItem");
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
|
||||
{
|
||||
b2.Property<int>("UserDefinedItemBookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b2.Property<float>("OverallRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.Property<float>("PerformanceRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.Property<float>("StoryRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.HasKey("UserDefinedItemBookId");
|
||||
|
||||
b2.ToTable("UserDefinedItem");
|
||||
|
||||
b2.WithOwner()
|
||||
.HasForeignKey("UserDefinedItemBookId");
|
||||
});
|
||||
|
||||
b1.Navigation("Book");
|
||||
|
||||
b1.Navigation("Rating");
|
||||
});
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("Rating");
|
||||
|
||||
b.Navigation("Supplements");
|
||||
|
||||
b.Navigation("UserDefinedItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("ContributorsLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Contributor", "Contributor")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("ContributorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Contributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "ParentCategory")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentCategoryCategoryId");
|
||||
|
||||
b.Navigation("ParentCategory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithOne()
|
||||
.HasForeignKey("DataLayer.LibraryBook", "BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("SeriesLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Series", "Series")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Navigation("ContributorsLink");
|
||||
|
||||
b.Navigation("SeriesLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
33
DataLayer/Migrations/20210619030017_AddAaxcDecryptionKeys.cs
Normal file
33
DataLayer/Migrations/20210619030017_AddAaxcDecryptionKeys.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
public partial class AddAaxcDecryptionKeys : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AudibleIV",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AudibleKey",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AudibleIV",
|
||||
table: "Books");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AudibleKey",
|
||||
table: "Books");
|
||||
}
|
||||
}
|
||||
}
|
||||
381
DataLayer/Migrations/20210622205558_RemoveAaxcDecryptionKeys.Designer.cs
generated
Normal file
381
DataLayer/Migrations/20210622205558_RemoveAaxcDecryptionKeys.Designer.cs
generated
Normal file
@@ -0,0 +1,381 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DataLayer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LibationContext))]
|
||||
[Migration("20210622205558_RemoveAaxcDecryptionKeys")]
|
||||
partial class RemoveAaxcDecryptionKeys
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "5.0.5");
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleProductId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DatePublished")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsAbridged")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("LengthInMinutes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Locale")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PictureId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.HasIndex("AudibleProductId");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.ToTable("Books");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ContributorId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte>("Order")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("BookId", "ContributorId", "Role");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("ContributorId");
|
||||
|
||||
b.ToTable("BookContributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.Property<int>("CategoryId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleCategoryId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("ParentCategoryCategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("CategoryId");
|
||||
|
||||
b.HasIndex("AudibleCategoryId");
|
||||
|
||||
b.HasIndex("ParentCategoryCategoryId");
|
||||
|
||||
b.ToTable("Categories");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
CategoryId = -1,
|
||||
AudibleCategoryId = "",
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Property<int>("ContributorId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleContributorId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ContributorId");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.ToTable("Contributors");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
ContributorId = -1,
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Account")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.ToTable("Library");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleSeriesId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SeriesId");
|
||||
|
||||
b.HasIndex("AudibleSeriesId");
|
||||
|
||||
b.ToTable("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float?>("Index")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("SeriesId", "BookId");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("SeriesBook");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<float>("OverallRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.Property<float>("PerformanceRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.Property<float>("StoryRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("Books");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("BookId");
|
||||
});
|
||||
|
||||
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
|
||||
{
|
||||
b1.Property<int>("SupplementId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Url")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("SupplementId");
|
||||
|
||||
b1.HasIndex("BookId");
|
||||
|
||||
b1.ToTable("Supplement");
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.Navigation("Book");
|
||||
});
|
||||
|
||||
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Tags")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("UserDefinedItem");
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
|
||||
{
|
||||
b2.Property<int>("UserDefinedItemBookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b2.Property<float>("OverallRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.Property<float>("PerformanceRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.Property<float>("StoryRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.HasKey("UserDefinedItemBookId");
|
||||
|
||||
b2.ToTable("UserDefinedItem");
|
||||
|
||||
b2.WithOwner()
|
||||
.HasForeignKey("UserDefinedItemBookId");
|
||||
});
|
||||
|
||||
b1.Navigation("Book");
|
||||
|
||||
b1.Navigation("Rating");
|
||||
});
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("Rating");
|
||||
|
||||
b.Navigation("Supplements");
|
||||
|
||||
b.Navigation("UserDefinedItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("ContributorsLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Contributor", "Contributor")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("ContributorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Contributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "ParentCategory")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentCategoryCategoryId");
|
||||
|
||||
b.Navigation("ParentCategory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithOne()
|
||||
.HasForeignKey("DataLayer.LibraryBook", "BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("SeriesLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Series", "Series")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Navigation("ContributorsLink");
|
||||
|
||||
b.Navigation("SeriesLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
public partial class RemoveAaxcDecryptionKeys : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AudibleIV",
|
||||
table: "Books");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AudibleKey",
|
||||
table: "Books");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AudibleIV",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AudibleKey",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace DataLayer.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "3.1.7");
|
||||
.HasAnnotation("ProductVersion", "5.0.5");
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
@@ -244,6 +244,8 @@ namespace DataLayer.Migrations
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.Navigation("Book");
|
||||
});
|
||||
|
||||
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
||||
@@ -282,7 +284,19 @@ namespace DataLayer.Migrations
|
||||
b2.WithOwner()
|
||||
.HasForeignKey("UserDefinedItemBookId");
|
||||
});
|
||||
|
||||
b1.Navigation("Book");
|
||||
|
||||
b1.Navigation("Rating");
|
||||
});
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("Rating");
|
||||
|
||||
b.Navigation("Supplements");
|
||||
|
||||
b.Navigation("UserDefinedItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
@@ -298,6 +312,10 @@ namespace DataLayer.Migrations
|
||||
.HasForeignKey("ContributorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Contributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
@@ -305,6 +323,8 @@ namespace DataLayer.Migrations
|
||||
b.HasOne("DataLayer.Category", "ParentCategory")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentCategoryCategoryId");
|
||||
|
||||
b.Navigation("ParentCategory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
@@ -314,6 +334,8 @@ namespace DataLayer.Migrations
|
||||
.HasForeignKey("DataLayer.LibraryBook", "BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
@@ -329,6 +351,27 @@ namespace DataLayer.Migrations
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Navigation("ContributorsLink");
|
||||
|
||||
b.Navigation("SeriesLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
|
||||
@@ -67,6 +67,9 @@ namespace DtoImporterService
|
||||
{
|
||||
var item = importItem.DtoItem;
|
||||
|
||||
//Add any subtitle after the title title.
|
||||
var title = item.Title + (!string.IsNullOrWhiteSpace(item.Subtitle) ? $": {item.Subtitle}" : "");
|
||||
|
||||
// absence of authors is very rare, but possible
|
||||
if (!item.Authors?.Any() ?? true)
|
||||
item.Authors = new[] { new Person { Name = "", Asin = null } };
|
||||
@@ -102,7 +105,7 @@ namespace DtoImporterService
|
||||
|
||||
var book = DbContext.Books.Add(new Book(
|
||||
new AudibleProductId(item.ProductId),
|
||||
item.Title,
|
||||
title,
|
||||
item.Description,
|
||||
item.LengthInMinutes,
|
||||
authors,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
@@ -21,8 +20,7 @@ namespace FileLiberator
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<LibraryBook> Completed;
|
||||
|
||||
public DownloadBook DownloadBook { get; } = new DownloadBook();
|
||||
public DecryptBook DecryptBook { get; } = new DecryptBook();
|
||||
public DownloadDecryptBook DecryptBook { get; } = new DownloadDecryptBook();
|
||||
public DownloadPdf DownloadPdf { get; } = new DownloadPdf();
|
||||
|
||||
public bool Validate(LibraryBook libraryBook)
|
||||
@@ -36,12 +34,6 @@ namespace FileLiberator
|
||||
|
||||
try
|
||||
{
|
||||
{
|
||||
var statusHandler = await DownloadBook.TryProcessAsync(libraryBook);
|
||||
if (statusHandler.HasErrors)
|
||||
return statusHandler;
|
||||
}
|
||||
|
||||
{
|
||||
var statusHandler = await DecryptBook.TryProcessAsync(libraryBook);
|
||||
if (statusHandler.HasErrors)
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AaxDecrypter;
|
||||
using AudibleApi;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using FileManager;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
/// <summary>
|
||||
/// Decrypt audiobook files
|
||||
///
|
||||
/// Processes:
|
||||
/// Download: download aax file: the DRM encrypted audiobook
|
||||
/// Decrypt: remove DRM encryption from audiobook. Store final book
|
||||
/// Backup: perform all steps (downloaded, decrypt) still needed to get final book
|
||||
/// </summary>
|
||||
public class DecryptBook : IDecryptable
|
||||
{
|
||||
public event EventHandler<LibraryBook> Begin;
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<string> DecryptBegin;
|
||||
|
||||
public event EventHandler<string> TitleDiscovered;
|
||||
public event EventHandler<string> AuthorsDiscovered;
|
||||
public event EventHandler<string> NarratorsDiscovered;
|
||||
public event EventHandler<byte[]> CoverImageFilepathDiscovered;
|
||||
public event EventHandler<int> UpdateProgress;
|
||||
|
||||
public event EventHandler<string> DecryptCompleted;
|
||||
public event EventHandler<LibraryBook> Completed;
|
||||
|
||||
public bool Validate(LibraryBook libraryBook)
|
||||
=> AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId)
|
||||
&& !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
|
||||
|
||||
// do NOT use ConfigureAwait(false) on ProcessAsync()
|
||||
// often calls events which prints to forms in the UI context
|
||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
Begin?.Invoke(this, libraryBook);
|
||||
|
||||
try
|
||||
{
|
||||
var aaxFilename = AudibleFileStorage.AAX.GetPath(libraryBook.Book.AudibleProductId);
|
||||
|
||||
if (aaxFilename == null)
|
||||
return new StatusHandler { "aaxFilename parameter is null" };
|
||||
if (!File.Exists(aaxFilename))
|
||||
return new StatusHandler { $"Cannot find AAX file: {aaxFilename}" };
|
||||
if (AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId))
|
||||
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
||||
|
||||
var api = await AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale);
|
||||
|
||||
var chapters = await downloadChapterNames(libraryBook, api);
|
||||
|
||||
var outputAudioFilename = await aaxToM4bConverterDecrypt(aaxFilename, libraryBook, chapters, api);
|
||||
|
||||
// decrypt failed
|
||||
if (outputAudioFilename == null)
|
||||
return new StatusHandler { "Decrypt failed" };
|
||||
|
||||
var destinationDir = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
|
||||
|
||||
var config = Configuration.Instance;
|
||||
if (config.RetainAaxFiles)
|
||||
{
|
||||
var newAaxFilename = FileUtility.GetValidFilename(
|
||||
destinationDir,
|
||||
Path.GetFileNameWithoutExtension(aaxFilename),
|
||||
"aax");
|
||||
File.Move(aaxFilename, newAaxFilename);
|
||||
}
|
||||
else
|
||||
{
|
||||
Dinah.Core.IO.FileExt.SafeDelete(aaxFilename);
|
||||
}
|
||||
|
||||
var finalAudioExists = AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
|
||||
if (!finalAudioExists)
|
||||
return new StatusHandler { "Cannot find final audio file after decryption" };
|
||||
|
||||
return new StatusHandler();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Completed?.Invoke(this, libraryBook);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Chapters> downloadChapterNames(LibraryBook libraryBook, Api api)
|
||||
{
|
||||
try
|
||||
{
|
||||
var contentMetadata = await api.GetLibraryBookMetadataAsync(libraryBook.Book.AudibleProductId);
|
||||
if (contentMetadata?.ChapterInfo is null)
|
||||
return null;
|
||||
|
||||
return new DownloadedChapters(contentMetadata.ChapterInfo);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> aaxToM4bConverterDecrypt(string aaxFilename, LibraryBook libraryBook, Chapters chapters, Api api)
|
||||
{
|
||||
DecryptBegin?.Invoke(this, $"Begin decrypting {aaxFilename}");
|
||||
|
||||
try
|
||||
{
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
|
||||
var account = persister
|
||||
.AccountsSettings
|
||||
.GetAccount(libraryBook.Account, libraryBook.Book.Locale);
|
||||
|
||||
var converter = await AaxToM4bConverter.CreateAsync(aaxFilename, account.DecryptKey, api.GetActivationBytesAsync, chapters);
|
||||
converter.AppName = "Libation";
|
||||
|
||||
TitleDiscovered?.Invoke(this, converter.tags.title);
|
||||
AuthorsDiscovered?.Invoke(this, converter.tags.author);
|
||||
NarratorsDiscovered?.Invoke(this, converter.tags.narrator);
|
||||
CoverImageFilepathDiscovered?.Invoke(this, converter.coverBytes);
|
||||
|
||||
// override default which was set in CreateAsync
|
||||
var proposedOutputFile = Path.Combine(AudibleFileStorage.DecryptInProgress, $"[{libraryBook.Book.AudibleProductId}].m4b");
|
||||
converter.SetOutputFilename(proposedOutputFile);
|
||||
converter.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress);
|
||||
|
||||
// REAL WORK DONE HERE
|
||||
var success = await Task.Run(() => converter.Run());
|
||||
|
||||
// decrypt failed
|
||||
if (!success)
|
||||
return null;
|
||||
|
||||
account.DecryptKey = converter.decryptKey;
|
||||
|
||||
return converter.outputFileName;
|
||||
}
|
||||
finally
|
||||
{
|
||||
DecryptCompleted?.Invoke(this, $"Completed decrypting {aaxFilename}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string moveFilesToBooksDir(Book product, string outputAudioFilename)
|
||||
{
|
||||
// create final directory. move each file into it. MOVE AUDIO FILE LAST
|
||||
// new dir: safetitle_limit50char + " [" + productId + "]"
|
||||
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestDir(product.Title, product.AudibleProductId);
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
|
||||
var sortedFiles = getProductFilesSorted(product, outputAudioFilename);
|
||||
|
||||
var musicFileExt = Path.GetExtension(outputAudioFilename).Trim('.');
|
||||
|
||||
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
|
||||
var audioFileName = FileUtility.GetValidFilename(destinationDir, product.Title, musicFileExt, product.AudibleProductId);
|
||||
|
||||
foreach (var f in sortedFiles)
|
||||
{
|
||||
var dest
|
||||
= AudibleFileStorage.Audio.IsFileTypeMatch(f)
|
||||
? audioFileName
|
||||
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext +"]." + non_audio_ext
|
||||
: FileUtility.GetValidFilename(destinationDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt);
|
||||
|
||||
if (Path.GetExtension(dest).Trim('.').ToLower() == "cue")
|
||||
Cue.UpdateFileName(f, audioFileName);
|
||||
|
||||
File.Move(f.FullName, dest);
|
||||
}
|
||||
|
||||
return destinationDir;
|
||||
}
|
||||
|
||||
private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename)
|
||||
{
|
||||
// files are: temp path\author\[asin].ext
|
||||
var m4bDir = new FileInfo(outputAudioFilename).Directory;
|
||||
var files = m4bDir
|
||||
.EnumerateFiles()
|
||||
.Where(f => f.Name.ContainsInsensitive(product.AudibleProductId))
|
||||
.ToList();
|
||||
|
||||
// move audio files to the end of the collection so these files are moved last
|
||||
var musicFiles = files.Where(f => AudibleFileStorage.Audio.IsFileTypeMatch(f));
|
||||
var sortedFiles = files
|
||||
.Except(musicFiles)
|
||||
.Concat(musicFiles)
|
||||
.ToList();
|
||||
|
||||
return sortedFiles;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using FileManager;
|
||||
using InternalUtilities;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
/// <summary>
|
||||
/// Download DRM book
|
||||
///
|
||||
/// Processes:
|
||||
/// Download: download aax file: the DRM encrypted audiobook
|
||||
/// Decrypt: remove DRM encryption from audiobook. Store final book
|
||||
/// Backup: perform all steps (downloaded, decrypt) still needed to get final book
|
||||
/// </summary>
|
||||
public class DownloadBook : DownloadableBase
|
||||
{
|
||||
private const string SERVICE_UNAVAILABLE = "Content Delivery Companion Service is not available.";
|
||||
|
||||
public override bool Validate(LibraryBook libraryBook)
|
||||
=> !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId)
|
||||
&& !AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId);
|
||||
|
||||
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var tempAaxFilename = getDownloadPath(libraryBook);
|
||||
var actualFilePath = await downloadBookAsync(libraryBook, tempAaxFilename);
|
||||
moveBook(libraryBook, actualFilePath);
|
||||
return verifyDownload(libraryBook);
|
||||
}
|
||||
|
||||
private static string getDownloadPath(LibraryBook libraryBook)
|
||||
=> FileUtility.GetValidFilename(
|
||||
AudibleFileStorage.DownloadsInProgress,
|
||||
libraryBook.Book.Title,
|
||||
"aax",
|
||||
libraryBook.Book.AudibleProductId);
|
||||
|
||||
private async Task<string> downloadBookAsync(LibraryBook libraryBook, string tempAaxFilename)
|
||||
{
|
||||
validate(libraryBook);
|
||||
|
||||
var api = await GetApiAsync(libraryBook);
|
||||
|
||||
var actualFilePath = await PerformDownloadAsync(
|
||||
tempAaxFilename,
|
||||
(p) => api.DownloadAaxWorkaroundAsync(libraryBook.Book.AudibleProductId, tempAaxFilename, p));
|
||||
|
||||
System.Threading.Thread.Sleep(100);
|
||||
// if bad file download, a 0-33 byte file will be created
|
||||
// if service unavailable, a 52 byte string will be saved as file
|
||||
var length = new FileInfo(actualFilePath).Length;
|
||||
|
||||
if (length > 100)
|
||||
return actualFilePath;
|
||||
|
||||
var contents = File.ReadAllText(actualFilePath);
|
||||
File.Delete(actualFilePath);
|
||||
|
||||
var exMsg = contents.StartsWithInsensitive(SERVICE_UNAVAILABLE)
|
||||
? SERVICE_UNAVAILABLE
|
||||
: "Error downloading file";
|
||||
|
||||
var ex = new Exception(exMsg);
|
||||
Serilog.Log.Logger.Error(ex, "Download error {@DebugInfo}", new
|
||||
{
|
||||
libraryBook.Book.Title,
|
||||
libraryBook.Book.AudibleProductId,
|
||||
libraryBook.Book.Locale,
|
||||
Account = libraryBook.Account?.ToMask() ?? "[empty]",
|
||||
tempAaxFilename,
|
||||
actualFilePath,
|
||||
length,
|
||||
contents
|
||||
});
|
||||
throw ex;
|
||||
}
|
||||
|
||||
private static void validate(LibraryBook libraryBook)
|
||||
{
|
||||
string errorString(string field)
|
||||
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
|
||||
|
||||
string errorTitle()
|
||||
{
|
||||
var title
|
||||
= (libraryBook.Book.Title.Length > 53)
|
||||
? $"{libraryBook.Book.Title.Truncate(50)}..."
|
||||
: libraryBook.Book.Title;
|
||||
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
|
||||
return errorBookTitle;
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(libraryBook.Account))
|
||||
throw new Exception(errorString("Account"));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
|
||||
throw new Exception(errorString("Locale"));
|
||||
}
|
||||
|
||||
private void moveBook(LibraryBook libraryBook, string actualFilePath)
|
||||
{
|
||||
var newAaxFilename = FileUtility.GetValidFilename(
|
||||
AudibleFileStorage.DownloadsFinal,
|
||||
libraryBook.Book.Title,
|
||||
"aax",
|
||||
libraryBook.Book.AudibleProductId);
|
||||
File.Move(actualFilePath, newAaxFilename);
|
||||
Invoke_StatusUpdate($"Successfully downloaded. Moved to: {newAaxFilename}");
|
||||
}
|
||||
|
||||
private static StatusHandler verifyDownload(LibraryBook libraryBook)
|
||||
=> !AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId)
|
||||
? new StatusHandler { "Downloaded AAX file cannot be found" }
|
||||
: new StatusHandler();
|
||||
}
|
||||
}
|
||||
223
FileLiberator/DownloadDecryptBook.cs
Normal file
223
FileLiberator/DownloadDecryptBook.cs
Normal file
@@ -0,0 +1,223 @@
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using FileManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AaxDecrypter;
|
||||
using AudibleApi;
|
||||
|
||||
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;
|
||||
public event EventHandler<string> AuthorsDiscovered;
|
||||
public event EventHandler<string> NarratorsDiscovered;
|
||||
public event EventHandler<byte[]> CoverImageFilepathDiscovered;
|
||||
public event EventHandler<int> UpdateProgress;
|
||||
public event EventHandler<TimeSpan> UpdateRemainingTime;
|
||||
public event EventHandler<string> DecryptCompleted;
|
||||
public event EventHandler<LibraryBook> Completed;
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
|
||||
private AaxcDownloadConverter aaxcDownloader;
|
||||
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
Begin?.Invoke(this, libraryBook);
|
||||
|
||||
try
|
||||
{
|
||||
if (AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId))
|
||||
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
||||
|
||||
var outputAudioFilename = await aaxToM4bConverterDecryptAsync(AudibleFileStorage.DownloadsInProgress, AudibleFileStorage.DecryptInProgress, libraryBook);
|
||||
|
||||
// decrypt failed
|
||||
if (outputAudioFilename is null)
|
||||
return new StatusHandler { "Decrypt failed" };
|
||||
|
||||
// moves files and returns dest dir
|
||||
_ = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
|
||||
|
||||
var finalAudioExists = AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
|
||||
if (!finalAudioExists)
|
||||
return new StatusHandler { "Cannot find final audio file after decryption" };
|
||||
|
||||
return new StatusHandler();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Completed?.Invoke(this, libraryBook);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> aaxToM4bConverterDecryptAsync(string cacheDir, string destinationDir, LibraryBook libraryBook)
|
||||
{
|
||||
DecryptBegin?.Invoke(this, $"Begin decrypting {libraryBook}");
|
||||
|
||||
try
|
||||
{
|
||||
validate(libraryBook);
|
||||
|
||||
var api = await InternalUtilities.AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale);
|
||||
|
||||
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
|
||||
|
||||
var aaxcDecryptDlLic = new DownloadLicense
|
||||
(
|
||||
contentLic.ContentMetadata?.ContentUrl?.OfflineUrl,
|
||||
contentLic.Voucher?.Key,
|
||||
contentLic.Voucher?.Iv,
|
||||
Resources.UserAgent
|
||||
);
|
||||
|
||||
if (Configuration.Instance.AllowLibationFixup)
|
||||
{
|
||||
aaxcDecryptDlLic.ChapterInfo = new ChapterInfo();
|
||||
|
||||
foreach (var chap in contentLic.ContentMetadata?.ChapterInfo?.Chapters)
|
||||
aaxcDecryptDlLic.ChapterInfo.AddChapter(
|
||||
new Chapter(
|
||||
chap.Title,
|
||||
chap.StartOffsetMs,
|
||||
chap.LengthMs
|
||||
));
|
||||
}
|
||||
|
||||
aaxcDownloader = AaxcDownloadConverter.Create(cacheDir, destinationDir, aaxcDecryptDlLic);
|
||||
|
||||
aaxcDownloader.AppName = "Libation";
|
||||
|
||||
// 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());
|
||||
|
||||
// decrypt failed
|
||||
if (!success)
|
||||
return null;
|
||||
|
||||
return aaxcDownloader.outputFileName;
|
||||
}
|
||||
finally
|
||||
{
|
||||
DecryptCompleted?.Invoke(this, $"Completed downloading and decrypting {libraryBook.Book.Title}");
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// new dir: safetitle_limit50char + " [" + productId + "]"
|
||||
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestDir(product.Title, product.AudibleProductId);
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
|
||||
var sortedFiles = getProductFilesSorted(product, outputAudioFilename);
|
||||
|
||||
var musicFileExt = Path.GetExtension(outputAudioFilename).Trim('.');
|
||||
|
||||
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
|
||||
var audioFileName = FileUtility.GetValidFilename(destinationDir, product.Title, musicFileExt, product.AudibleProductId);
|
||||
|
||||
foreach (var f in sortedFiles)
|
||||
{
|
||||
var dest
|
||||
= AudibleFileStorage.Audio.IsFileTypeMatch(f)
|
||||
? audioFileName
|
||||
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext +"]." + non_audio_ext
|
||||
: FileUtility.GetValidFilename(destinationDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt);
|
||||
|
||||
if (Path.GetExtension(dest).Trim('.').ToLower() == "cue")
|
||||
Cue.UpdateFileName(f, audioFileName);
|
||||
|
||||
File.Move(f.FullName, dest);
|
||||
}
|
||||
|
||||
return destinationDir;
|
||||
}
|
||||
|
||||
private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename)
|
||||
{
|
||||
// files are: temp path\author\[asin].ext
|
||||
var m4bDir = new FileInfo(outputAudioFilename).Directory;
|
||||
var files = m4bDir
|
||||
.EnumerateFiles()
|
||||
.Where(f => f.Name.ContainsInsensitive(product.AudibleProductId))
|
||||
.ToList();
|
||||
|
||||
// move audio files to the end of the collection so these files are moved last
|
||||
var musicFiles = files.Where(f => AudibleFileStorage.Audio.IsFileTypeMatch(f));
|
||||
var sortedFiles = files
|
||||
.Except(musicFiles)
|
||||
.Concat(musicFiles)
|
||||
.ToList();
|
||||
|
||||
return sortedFiles;
|
||||
}
|
||||
|
||||
private static void validate(LibraryBook libraryBook)
|
||||
{
|
||||
string errorString(string field)
|
||||
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
|
||||
|
||||
string errorTitle()
|
||||
{
|
||||
var title
|
||||
= (libraryBook.Book.Title.Length > 53)
|
||||
? $"{libraryBook.Book.Title.Truncate(50)}..."
|
||||
: libraryBook.Book.Title;
|
||||
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
|
||||
return errorBookTitle;
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(libraryBook.Account))
|
||||
throw new Exception(errorString("Account"));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
|
||||
throw new Exception(errorString("Locale"));
|
||||
}
|
||||
|
||||
public bool Validate(LibraryBook libraryBook)
|
||||
=> !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
aaxcDownloader?.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using AaxDecrypter;
|
||||
using AudibleApiDTOs;
|
||||
using Dinah.Core.Diagnostics;
|
||||
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public class DownloadedChapters : Chapters
|
||||
{
|
||||
public DownloadedChapters(ChapterInfo chapterInfo)
|
||||
{
|
||||
AddChapters(chapterInfo.Chapters
|
||||
.Select(c => new AaxDecrypter.Chapter(c.StartOffsetMs / 1000d, (c.StartOffsetMs + c.LengthMs) / 1000d, c.Title)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,15 @@ namespace FileLiberator
|
||||
{
|
||||
event EventHandler<string> DecryptBegin;
|
||||
|
||||
event EventHandler<Action<byte[]>> RequestCoverArt;
|
||||
event EventHandler<string> TitleDiscovered;
|
||||
event EventHandler<string> AuthorsDiscovered;
|
||||
event EventHandler<string> NarratorsDiscovered;
|
||||
event EventHandler<byte[]> CoverImageFilepathDiscovered;
|
||||
event EventHandler<int> UpdateProgress;
|
||||
event EventHandler<TimeSpan> UpdateRemainingTime;
|
||||
|
||||
event EventHandler<string> DecryptCompleted;
|
||||
void Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
|
||||
@@ -9,7 +9,7 @@ using Dinah.Core.Collections.Generic;
|
||||
namespace FileManager
|
||||
{
|
||||
// could add images here, but for now images are stored in a well-known location
|
||||
public enum FileType { Unknown, Audio, AAX, PDF }
|
||||
public enum FileType { Unknown, Audio, AAXC, PDF }
|
||||
|
||||
/// <summary>
|
||||
/// Files are large. File contents are never read by app.
|
||||
@@ -25,7 +25,7 @@ namespace FileManager
|
||||
|
||||
#region static
|
||||
public static AudioFileStorage Audio { get; } = new AudioFileStorage();
|
||||
public static AudibleFileStorage AAX { get; } = new AaxFileStorage();
|
||||
public static AudibleFileStorage AAXC { get; } = new AaxcFileStorage();
|
||||
public static AudibleFileStorage PDF { get; } = new PdfFileStorage();
|
||||
|
||||
public static string DownloadsInProgress
|
||||
@@ -77,7 +77,7 @@ namespace FileManager
|
||||
public FileType FileType => (FileType)Value;
|
||||
|
||||
private IEnumerable<string> extensions_noDots { get; }
|
||||
private string extAggr { get; }
|
||||
private string extAggr { get; }
|
||||
|
||||
protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString())
|
||||
{
|
||||
@@ -153,16 +153,16 @@ namespace FileManager
|
||||
}
|
||||
}
|
||||
|
||||
public class AaxFileStorage : AudibleFileStorage
|
||||
public class AaxcFileStorage : AudibleFileStorage
|
||||
{
|
||||
public override string[] Extensions { get; } = new[] { "aax" };
|
||||
public override string[] Extensions { get; } = new[] { "aaxc" };
|
||||
|
||||
// we always want to use the latest config value, therefore
|
||||
// - DO use 'get' arrow "=>"
|
||||
// - do NOT use assign "="
|
||||
public override string StorageDirectory => DownloadsFinal;
|
||||
public override string StorageDirectory => DownloadsInProgress;
|
||||
|
||||
public AaxFileStorage() : base(FileType.AAX) { }
|
||||
public AaxcFileStorage() : base(FileType.AAXC) { }
|
||||
}
|
||||
|
||||
public class PdfFileStorage : AudibleFileStorage
|
||||
|
||||
@@ -83,13 +83,12 @@ namespace FileManager
|
||||
set => persistentDictionary.Set(nameof(DecryptInProgressEnum), value);
|
||||
}
|
||||
|
||||
[Description("Retain .aax files after decrypting?")]
|
||||
public bool RetainAaxFiles
|
||||
[Description("Allow Libation for fix up audiobook metadata?")]
|
||||
public bool AllowLibationFixup
|
||||
{
|
||||
get => persistentDictionary.Get<bool>(nameof(RetainAaxFiles));
|
||||
set => persistentDictionary.Set(nameof(RetainAaxFiles), 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
|
||||
|
||||
// singleton stuff
|
||||
|
||||
@@ -60,8 +60,8 @@ namespace InternalUtilities
|
||||
{
|
||||
var items = await api.GetAllLibraryItemsAsync();
|
||||
|
||||
// remove episode parents and 'audible plus' check-outs
|
||||
items.RemoveAll(i => i.IsEpisodes || i.IsNonLibraryAudiblePlus);
|
||||
// remove episode parents
|
||||
items.RemoveAll(i => i.IsEpisodes);
|
||||
|
||||
#region // episode handling. doesn't quite work
|
||||
// // add individual/children episodes
|
||||
|
||||
14
Libation.sln
14
Libation.sln
@@ -54,10 +54,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationWinForms", "Libatio
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinFormsDesigner", "WinFormsDesigner\WinFormsDesigner.csproj", "{0807616A-A77A-4B08-A65A-1582B09E114B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ffmpeg decrypt", "_Demos\ffmpeg decrypt\ffmpeg decrypt.csproj", "{DF72740C-900A-45DA-A3A6-4DDD68F286F2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "inAudibleLite", "_Demos\inAudibleLite\inAudibleLite.csproj", "{74D02251-898E-4CAF-80C7-801820622903}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core", "..\Dinah.Core\Dinah.Core\Dinah.Core.csproj", "{9E951521-2587-4FC6-AD26-FAA9179FB6C4}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.EntityFrameworkCore", "..\Dinah.Core\Dinah.EntityFrameworkCore\Dinah.EntityFrameworkCore.csproj", "{1255D9BA-CE6E-42E4-A253-6376540B9661}"
|
||||
@@ -152,14 +148,6 @@ Global
|
||||
{0807616A-A77A-4B08-A65A-1582B09E114B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0807616A-A77A-4B08-A65A-1582B09E114B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0807616A-A77A-4B08-A65A-1582B09E114B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DF72740C-900A-45DA-A3A6-4DDD68F286F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DF72740C-900A-45DA-A3A6-4DDD68F286F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DF72740C-900A-45DA-A3A6-4DDD68F286F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DF72740C-900A-45DA-A3A6-4DDD68F286F2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{74D02251-898E-4CAF-80C7-801820622903}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{74D02251-898E-4CAF-80C7-801820622903}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{74D02251-898E-4CAF-80C7-801820622903}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{74D02251-898E-4CAF-80C7-801820622903}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -239,8 +227,6 @@ Global
|
||||
{111420E2-D4F0-4068-B46A-C4B6DCC823DC} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{0807616A-A77A-4B08-A65A-1582B09E114B} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{DF72740C-900A-45DA-A3A6-4DDD68F286F2} = {F61184E7-2426-4A13-ACEF-5689928E2CE2}
|
||||
{74D02251-898E-4CAF-80C7-801820622903} = {F61184E7-2426-4A13-ACEF-5689928E2CE2}
|
||||
{9E951521-2587-4FC6-AD26-FAA9179FB6C4} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
|
||||
{1255D9BA-CE6E-42E4-A253-6376540B9661} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
|
||||
{35803735-B669-4090-9681-CC7F7FABDC71} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<!-- <PublishSingleFile>true</PublishSingleFile> -->
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
|
||||
<Version>4.4.0.5</Version>
|
||||
<Version>5.1.3.2</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using AudibleApi;
|
||||
using AudibleApi.Authorization;
|
||||
using FileManager;
|
||||
using InternalUtilities;
|
||||
using LibationWinForms;
|
||||
@@ -28,8 +29,8 @@ namespace LibationLauncher
|
||||
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
|
||||
migrate_to_v4_0_0();
|
||||
migrate_to_v4_0_3(); // add setting for whether to delete/retain aax
|
||||
|
||||
migrate_to_v5_0_0();
|
||||
|
||||
ensureLoggingConfig();
|
||||
ensureSerilogConfig();
|
||||
configureLogging();
|
||||
@@ -58,6 +59,7 @@ namespace LibationLauncher
|
||||
config.DownloadsInProgressEnum ??= "WinTemp";
|
||||
config.DecryptInProgressEnum ??= "WinTemp";
|
||||
config.Books ??= Configuration.AppDir;
|
||||
config.AllowLibationFixup = true;
|
||||
};
|
||||
// setupDialog.BasicBtn_Click += (_, __) => // no action needed
|
||||
setupDialog.AdvancedBtn_Click += (_, __) => isAdvanced = true;
|
||||
@@ -194,7 +196,7 @@ namespace LibationLauncher
|
||||
|
||||
var jLocale = jObj.Property("LocaleCountryCode");
|
||||
var jDecryptKey = jObj.Property("DecryptKey");
|
||||
|
||||
|
||||
jDecryptKey?.Remove();
|
||||
jLocale?.Remove();
|
||||
|
||||
@@ -206,28 +208,56 @@ namespace LibationLauncher
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region migrate_to_v4_0_3 add setting for whether to delete/retain aax
|
||||
private static void migrate_to_v4_0_3()
|
||||
#region migrate_to_v5_0_0 re-gegister device if device info not in settings
|
||||
private static void migrate_to_v5_0_0()
|
||||
{
|
||||
if (!File.Exists(Configuration.Instance.SettingsFilePath))
|
||||
var persistentDictionary = new PersistentDictionary(Configuration.Instance.SettingsFilePath);
|
||||
|
||||
var config = Configuration.Instance;
|
||||
if (persistentDictionary.GetString(nameof(config.AllowLibationFixup)) is null)
|
||||
{
|
||||
persistentDictionary.Set(nameof(config.AllowLibationFixup), true);
|
||||
}
|
||||
|
||||
if (!File.Exists(AudibleApiStorage.AccountsSettingsFile))
|
||||
return;
|
||||
|
||||
// use JObject to remove decrypt key and locale from Settings.json
|
||||
var settingsContents = File.ReadAllText(Configuration.Instance.SettingsFilePath);
|
||||
var jObj = JObject.Parse(settingsContents);
|
||||
var accountsPersister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
|
||||
var jRetainAaxFiles = jObj.Property("RetainAaxFiles");
|
||||
if (jRetainAaxFiles is null)
|
||||
var accounts = accountsPersister?.AccountsSettings?.Accounts;
|
||||
if (accounts is null)
|
||||
return;
|
||||
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
jObj.Add("RetainAaxFiles", false);
|
||||
|
||||
var newContents = jObj.ToString(Formatting.Indented);
|
||||
File.WriteAllText(Configuration.Instance.SettingsFilePath, newContents);
|
||||
var identity = account?.IdentityTokens;
|
||||
|
||||
if (identity is null)
|
||||
continue;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(identity.DeviceType) &&
|
||||
!string.IsNullOrWhiteSpace(identity.DeviceSerialNumber) &&
|
||||
!string.IsNullOrWhiteSpace(identity.AmazonAccountId))
|
||||
continue;
|
||||
|
||||
var authorize = new Authorize(identity.Locale);
|
||||
|
||||
try
|
||||
{
|
||||
authorize.DeregisterAsync(identity.ExistingAccessToken, identity.Cookies.ToKeyValuePair()).GetAwaiter().GetResult();
|
||||
identity.Invalidate();
|
||||
|
||||
var api = AudibleApiActions.GetApiAsync(new LibationWinForms.Login.WinformResponder(account), account).GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Don't care if it fails
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
private static string defaultLoggingLevel { get; } = "Information";
|
||||
private static string defaultLoggingLevel { get; } = "Information";
|
||||
private static void ensureLoggingConfig()
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
|
||||
@@ -24,13 +24,18 @@ namespace LibationWinForms.BookLiberation
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public void WriteLine(string text)
|
||||
=> logTb.UIThread(() => logTb.AppendText($"{DateTime.Now} {text}{Environment.NewLine}"));
|
||||
public void WriteLine(string text)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
logTb.UIThread(() => logTb.AppendText($"{DateTime.Now} {text}{Environment.NewLine}"));
|
||||
}
|
||||
|
||||
public void FinalizeUI()
|
||||
{
|
||||
keepGoingCb.Enabled = false;
|
||||
logTb.AppendText("");
|
||||
|
||||
if (!IsDisposed)
|
||||
logTb.AppendText("");
|
||||
}
|
||||
|
||||
private void AutomatedBackupsForm_FormClosing(object sender, FormClosingEventArgs e) => keepGoingCb.Checked = false;
|
||||
|
||||
136
LibationWinForms/BookLiberation/DecryptForm.Designer.cs
generated
136
LibationWinForms/BookLiberation/DecryptForm.Designer.cs
generated
@@ -28,68 +28,85 @@
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.pictureBox1 = new System.Windows.Forms.PictureBox();
|
||||
this.bookInfoLbl = new System.Windows.Forms.Label();
|
||||
this.progressBar1 = new System.Windows.Forms.ProgressBar();
|
||||
this.rtbLog = new System.Windows.Forms.RichTextBox();
|
||||
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// pictureBox1
|
||||
//
|
||||
this.pictureBox1.Location = new System.Drawing.Point(12, 12);
|
||||
this.pictureBox1.Name = "pictureBox1";
|
||||
this.pictureBox1.Size = new System.Drawing.Size(100, 100);
|
||||
this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
|
||||
this.pictureBox1.TabIndex = 0;
|
||||
this.pictureBox1.TabStop = false;
|
||||
//
|
||||
// bookInfoLbl
|
||||
//
|
||||
this.bookInfoLbl.AutoSize = true;
|
||||
this.bookInfoLbl.Location = new System.Drawing.Point(118, 12);
|
||||
this.bookInfoLbl.Name = "bookInfoLbl";
|
||||
this.bookInfoLbl.Size = new System.Drawing.Size(100, 13);
|
||||
this.bookInfoLbl.TabIndex = 0;
|
||||
this.bookInfoLbl.Text = "[multi-line book info]";
|
||||
//
|
||||
// progressBar1
|
||||
//
|
||||
this.progressBar1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
|
||||
this.pictureBox1 = new System.Windows.Forms.PictureBox();
|
||||
this.bookInfoLbl = new System.Windows.Forms.Label();
|
||||
this.progressBar1 = new System.Windows.Forms.ProgressBar();
|
||||
this.rtbLog = new System.Windows.Forms.RichTextBox();
|
||||
this.remainingTimeLbl = new System.Windows.Forms.Label();
|
||||
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// pictureBox1
|
||||
//
|
||||
this.pictureBox1.Location = new System.Drawing.Point(14, 14);
|
||||
this.pictureBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.pictureBox1.Name = "pictureBox1";
|
||||
this.pictureBox1.Size = new System.Drawing.Size(117, 115);
|
||||
this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
|
||||
this.pictureBox1.TabIndex = 0;
|
||||
this.pictureBox1.TabStop = false;
|
||||
//
|
||||
// bookInfoLbl
|
||||
//
|
||||
this.bookInfoLbl.AutoSize = true;
|
||||
this.bookInfoLbl.Location = new System.Drawing.Point(138, 14);
|
||||
this.bookInfoLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.bookInfoLbl.Name = "bookInfoLbl";
|
||||
this.bookInfoLbl.Size = new System.Drawing.Size(121, 15);
|
||||
this.bookInfoLbl.TabIndex = 0;
|
||||
this.bookInfoLbl.Text = "[multi-line book info]";
|
||||
//
|
||||
// progressBar1
|
||||
//
|
||||
this.progressBar1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.progressBar1.Location = new System.Drawing.Point(12, 526);
|
||||
this.progressBar1.Name = "progressBar1";
|
||||
this.progressBar1.Size = new System.Drawing.Size(582, 23);
|
||||
this.progressBar1.TabIndex = 2;
|
||||
//
|
||||
// rtbLog
|
||||
//
|
||||
this.rtbLog.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
this.progressBar1.Location = new System.Drawing.Point(14, 607);
|
||||
this.progressBar1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.progressBar1.Name = "progressBar1";
|
||||
this.progressBar1.Size = new System.Drawing.Size(611, 27);
|
||||
this.progressBar1.TabIndex = 2;
|
||||
//
|
||||
// rtbLog
|
||||
//
|
||||
this.rtbLog.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.rtbLog.Location = new System.Drawing.Point(12, 118);
|
||||
this.rtbLog.Name = "rtbLog";
|
||||
this.rtbLog.Size = new System.Drawing.Size(582, 402);
|
||||
this.rtbLog.TabIndex = 1;
|
||||
this.rtbLog.Text = "";
|
||||
//
|
||||
// DecryptForm
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(606, 561);
|
||||
this.Controls.Add(this.rtbLog);
|
||||
this.Controls.Add(this.progressBar1);
|
||||
this.Controls.Add(this.bookInfoLbl);
|
||||
this.Controls.Add(this.pictureBox1);
|
||||
this.Name = "DecryptForm";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "DecryptForm";
|
||||
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.DecryptForm_FormClosing);
|
||||
this.Load += new System.EventHandler(this.DecryptForm_Load);
|
||||
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
this.rtbLog.Location = new System.Drawing.Point(14, 136);
|
||||
this.rtbLog.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.rtbLog.Name = "rtbLog";
|
||||
this.rtbLog.Size = new System.Drawing.Size(678, 463);
|
||||
this.rtbLog.TabIndex = 1;
|
||||
this.rtbLog.Text = "";
|
||||
//
|
||||
// remainingTimeLbl
|
||||
//
|
||||
this.remainingTimeLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.remainingTimeLbl.Location = new System.Drawing.Point(632, 607);
|
||||
this.remainingTimeLbl.Name = "remainingTimeLbl";
|
||||
this.remainingTimeLbl.Size = new System.Drawing.Size(60, 31);
|
||||
this.remainingTimeLbl.TabIndex = 3;
|
||||
this.remainingTimeLbl.Text = "ETA:\r\n";
|
||||
this.remainingTimeLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
|
||||
//
|
||||
// DecryptForm
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(707, 647);
|
||||
this.Controls.Add(this.remainingTimeLbl);
|
||||
this.Controls.Add(this.rtbLog);
|
||||
this.Controls.Add(this.progressBar1);
|
||||
this.Controls.Add(this.bookInfoLbl);
|
||||
this.Controls.Add(this.pictureBox1);
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.Name = "DecryptForm";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "DecryptForm";
|
||||
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.DecryptForm_FormClosing);
|
||||
this.Load += new System.EventHandler(this.DecryptForm_Load);
|
||||
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
@@ -99,5 +116,6 @@
|
||||
private System.Windows.Forms.Label bookInfoLbl;
|
||||
private System.Windows.Forms.ProgressBar progressBar1;
|
||||
private System.Windows.Forms.RichTextBox rtbLog;
|
||||
private System.Windows.Forms.Label remainingTimeLbl;
|
||||
}
|
||||
}
|
||||
@@ -56,9 +56,27 @@ 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) => progressBar1.UIThread(() => progressBar1.Value = percentage);
|
||||
public void UpdateProgress(int percentage)
|
||||
{
|
||||
if (percentage == 0)
|
||||
remainingTimeLbl.UIThread(() => remainingTimeLbl.Text = "ETA:\r\n0 sec");
|
||||
|
||||
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)
|
||||
{
|
||||
remainingTimeLbl.UIThread(() => remainingTimeLbl.Text = $"ETA:\r\n{(int)remaining.TotalSeconds} sec");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
|
||||
@@ -15,21 +15,28 @@ namespace LibationWinForms.BookLiberation
|
||||
public event EventHandler<string> LogErrorString;
|
||||
public event EventHandler<(Exception, string)> LogError;
|
||||
|
||||
public static LogMe RegisterForm(AutomatedBackupsForm form)
|
||||
private LogMe()
|
||||
{
|
||||
LogInfo += (_, text) => Serilog.Log.Logger.Information($"Automated backup: {text}");
|
||||
LogErrorString += (_, text) => Serilog.Log.Logger.Error(text);
|
||||
LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error");
|
||||
}
|
||||
|
||||
public static LogMe RegisterForm(AutomatedBackupsForm form = null)
|
||||
{
|
||||
var logMe = new LogMe();
|
||||
|
||||
logMe.LogInfo += (_, text) => Serilog.Log.Logger.Information($"Automated backup: {text}");
|
||||
logMe.LogInfo += (_, text) => form.WriteLine(text);
|
||||
if (form is null)
|
||||
return logMe;
|
||||
|
||||
logMe.LogErrorString += (_, text) => Serilog.Log.Logger.Error(text);
|
||||
logMe.LogErrorString += (_, text) => form.WriteLine(text);
|
||||
logMe.LogInfo += (_, text) => form?.WriteLine(text);
|
||||
|
||||
logMe.LogErrorString += (_, text) => form?.WriteLine(text);
|
||||
|
||||
logMe.LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error");
|
||||
logMe.LogError += (_, tuple) =>
|
||||
{
|
||||
form.WriteLine(tuple.Item2 ?? "Automated backup: error");
|
||||
form.WriteLine("ERROR: " + tuple.Item1.Message);
|
||||
form?.WriteLine(tuple.Item2 ?? "Automated backup: error");
|
||||
form?.WriteLine("ERROR: " + tuple.Item1.Message);
|
||||
};
|
||||
|
||||
return logMe;
|
||||
@@ -47,13 +54,14 @@ namespace LibationWinForms.BookLiberation
|
||||
Serilog.Log.Logger.Information("Begin " + nameof(BackupSingleBookAsync) + " {@DebugInfo}", new { productId });
|
||||
|
||||
var backupBook = getWiredUpBackupBook(completedAction);
|
||||
|
||||
(AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(backupBook);
|
||||
automatedBackupsForm.KeepGoingVisible = false;
|
||||
|
||||
(Action unsubscribeEvents, LogMe logMe) = attachToBackupsForm(backupBook);
|
||||
|
||||
var libraryBook = IProcessableExt.GetSingleLibraryBook(productId);
|
||||
// continue even if libraryBook is null. we'll display even that in the processing box
|
||||
await new BackupSingle(logMe, backupBook, automatedBackupsForm, libraryBook).RunBackupAsync();
|
||||
await new BackupSingle(logMe, backupBook, libraryBook).RunBackupAsync();
|
||||
|
||||
unsubscribeEvents();
|
||||
}
|
||||
|
||||
public static async Task BackupAllBooksAsync(EventHandler<LibraryBook> completedAction = null)
|
||||
@@ -61,17 +69,20 @@ namespace LibationWinForms.BookLiberation
|
||||
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync));
|
||||
|
||||
var backupBook = getWiredUpBackupBook(completedAction);
|
||||
var automatedBackupsForm = new AutomatedBackupsForm();
|
||||
|
||||
(Action unsubscribeEvents, LogMe logMe) = attachToBackupsForm(backupBook, automatedBackupsForm);
|
||||
|
||||
(AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(backupBook);
|
||||
await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync();
|
||||
|
||||
unsubscribeEvents();
|
||||
}
|
||||
|
||||
private static BackupBook getWiredUpBackupBook(EventHandler<LibraryBook> completedAction)
|
||||
{
|
||||
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 +91,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;
|
||||
}
|
||||
@@ -96,17 +105,14 @@ namespace LibationWinForms.BookLiberation
|
||||
|
||||
private static void updateIsLiberated(object sender, LibraryBook e) => ApplicationServices.SearchEngineCommands.UpdateIsLiberated(e.Book);
|
||||
|
||||
private static (AutomatedBackupsForm, LogMe) attachToBackupsForm(BackupBook backupBook)
|
||||
private static (Action unsubscribeEvents, LogMe) attachToBackupsForm(BackupBook backupBook, AutomatedBackupsForm automatedBackupsForm = null)
|
||||
{
|
||||
#region create form and logger
|
||||
var automatedBackupsForm = new AutomatedBackupsForm();
|
||||
#region create logger
|
||||
var logMe = LogMe.RegisterForm(automatedBackupsForm);
|
||||
#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 +122,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;
|
||||
@@ -129,11 +132,8 @@ namespace LibationWinForms.BookLiberation
|
||||
|
||||
#region when form closes, unsubscribe from model's events
|
||||
// unsubscribe so disposed forms aren't still trying to receive notifications
|
||||
automatedBackupsForm.FormClosing += (_, __) =>
|
||||
Action unsubscribe = () =>
|
||||
{
|
||||
backupBook.DownloadBook.Begin -= downloadBookBegin;
|
||||
backupBook.DownloadBook.StatusUpdate -= statusUpdate;
|
||||
backupBook.DownloadBook.Completed -= downloadBookCompleted;
|
||||
backupBook.DecryptBook.Begin -= decryptBookBegin;
|
||||
backupBook.DecryptBook.StatusUpdate -= statusUpdate;
|
||||
backupBook.DecryptBook.Completed -= decryptBookCompleted;
|
||||
@@ -143,7 +143,7 @@ namespace LibationWinForms.BookLiberation
|
||||
};
|
||||
#endregion
|
||||
|
||||
return (automatedBackupsForm, logMe);
|
||||
return (unsubscribe, logMe);
|
||||
}
|
||||
|
||||
public static async Task BackupAllPdfsAsync(EventHandler<LibraryBook> completedAction = null)
|
||||
@@ -152,7 +152,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,22 +250,58 @@ 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[]> setCoverArtDelegate)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (pictureId == libraryBook.Book.PictureId)
|
||||
{
|
||||
FileManager.PictureStorage.PictureCached -= pictureCached;
|
||||
|
||||
var picDef = new FileManager.PictureDefinition(libraryBook.Book.PictureId, FileManager.PictureSize._500x500);
|
||||
(_, picture) = FileManager.PictureStorage.GetPicture(picDef);
|
||||
|
||||
setCoverArtDelegate(picture);
|
||||
}
|
||||
};
|
||||
FileManager.PictureStorage.PictureCached += pictureCached;
|
||||
}
|
||||
else
|
||||
setCoverArtDelegate(picture);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region subscribe new form to model's events
|
||||
@@ -276,6 +312,8 @@ namespace LibationWinForms.BookLiberation
|
||||
decryptBook.NarratorsDiscovered += narratorsDiscovered;
|
||||
decryptBook.CoverImageFilepathDiscovered += coverImageFilepathDiscovered;
|
||||
decryptBook.UpdateProgress += updateProgress;
|
||||
decryptBook.UpdateRemainingTime += updateRemainingTime;
|
||||
decryptBook.RequestCoverArt += requestCoverArt;
|
||||
|
||||
decryptBook.DecryptCompleted += decryptCompleted;
|
||||
#endregion
|
||||
@@ -291,12 +329,15 @@ namespace LibationWinForms.BookLiberation
|
||||
decryptBook.NarratorsDiscovered -= narratorsDiscovered;
|
||||
decryptBook.CoverImageFilepathDiscovered -= coverImageFilepathDiscovered;
|
||||
decryptBook.UpdateProgress -= updateProgress;
|
||||
decryptBook.UpdateRemainingTime -= updateRemainingTime;
|
||||
decryptBook.RequestCoverArt -= requestCoverArt;
|
||||
|
||||
decryptBook.DecryptCompleted -= decryptCompleted;
|
||||
decryptBook.Cancel();
|
||||
};
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
private static (AutomatedBackupsForm, LogMe) attachToBackupsForm(IDownloadableProcessable downloadable)
|
||||
{
|
||||
#region create form and logger
|
||||
@@ -337,7 +378,7 @@ namespace LibationWinForms.BookLiberation
|
||||
protected IProcessable Processable { get; }
|
||||
protected AutomatedBackupsForm AutomatedBackupsForm { get; }
|
||||
|
||||
protected BackupRunner(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm)
|
||||
protected BackupRunner(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm = null)
|
||||
{
|
||||
LogMe = logMe;
|
||||
Processable = processable;
|
||||
@@ -352,7 +393,7 @@ namespace LibationWinForms.BookLiberation
|
||||
|
||||
public async Task RunBackupAsync()
|
||||
{
|
||||
AutomatedBackupsForm.Show();
|
||||
AutomatedBackupsForm?.Show();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -363,7 +404,7 @@ namespace LibationWinForms.BookLiberation
|
||||
LogMe.Error(ex);
|
||||
}
|
||||
|
||||
AutomatedBackupsForm.FinalizeUI();
|
||||
AutomatedBackupsForm?.FinalizeUI();
|
||||
LogMe.Info("DONE");
|
||||
}
|
||||
|
||||
@@ -414,23 +455,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)
|
||||
: base(logMe, processable, automatedBackupsForm)
|
||||
public BackupSingle(LogMe logMe, IProcessable processable, LibraryBook libraryBook)
|
||||
: base(logMe, processable)
|
||||
{
|
||||
_libraryBook = libraryBook;
|
||||
}
|
||||
|
||||
protected override async Task RunAsync()
|
||||
protected override async Task RunAsync()
|
||||
{
|
||||
if (_libraryBook is not null)
|
||||
await ProcessOneAsync(Processable.ProcessSingleAsync, _libraryBook);
|
||||
@@ -438,7 +479,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.
|
||||
@@ -457,20 +498,23 @@ 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.IsDisposed)
|
||||
break;
|
||||
|
||||
LogMe.Info("Done. All books have been processed");
|
||||
if (!AutomatedBackupsForm.KeepGoing)
|
||||
{
|
||||
if (AutomatedBackupsForm.KeepGoingVisible && !AutomatedBackupsForm.KeepGoingChecked)
|
||||
LogMe.Info("'Keep going' is unchecked");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LogMe.Info("Done. All books have been processed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,9 +34,11 @@
|
||||
//
|
||||
// approvedBtn
|
||||
//
|
||||
this.approvedBtn.Location = new System.Drawing.Point(15, 25);
|
||||
this.approvedBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.approvedBtn.Location = new System.Drawing.Point(18, 75);
|
||||
this.approvedBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.approvedBtn.Name = "approvedBtn";
|
||||
this.approvedBtn.Size = new System.Drawing.Size(79, 23);
|
||||
this.approvedBtn.Size = new System.Drawing.Size(92, 27);
|
||||
this.approvedBtn.TabIndex = 1;
|
||||
this.approvedBtn.Text = "Approved";
|
||||
this.approvedBtn.UseVisualStyleBackColor = true;
|
||||
@@ -45,27 +47,30 @@
|
||||
// label1
|
||||
//
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Location = new System.Drawing.Point(12, 9);
|
||||
this.label1.Location = new System.Drawing.Point(14, 10);
|
||||
this.label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(104, 13);
|
||||
this.label1.Size = new System.Drawing.Size(314, 45);
|
||||
this.label1.TabIndex = 0;
|
||||
this.label1.Text = "Click after approving";
|
||||
this.label1.Text = "Amazon is sending you an email.\r\n\r\nPlease press this button after you approve the" +
|
||||
" notification.";
|
||||
//
|
||||
// ApprovalNeededDialog
|
||||
//
|
||||
this.AcceptButton = this.approvedBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(149, 60);
|
||||
this.ClientSize = new System.Drawing.Size(345, 115);
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.approvedBtn);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "ApprovalNeededDialog";
|
||||
this.ShowIcon = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Approval Needed";
|
||||
this.Text = "Approval Alert Detected";
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
|
||||
60
LibationWinForms/Dialogs/Login/ApprovalNeededDialog.resx
Normal file
60
LibationWinForms/Dialogs/Login/ApprovalNeededDialog.resx
Normal file
@@ -0,0 +1,60 @@
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
445
LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
445
LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
@@ -28,211 +28,243 @@
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.booksLocationLbl = new System.Windows.Forms.Label();
|
||||
this.booksLocationTb = new System.Windows.Forms.TextBox();
|
||||
this.booksLocationSearchBtn = new System.Windows.Forms.Button();
|
||||
this.booksLocationDescLbl = new System.Windows.Forms.Label();
|
||||
this.downloadsInProgressGb = new System.Windows.Forms.GroupBox();
|
||||
this.downloadsInProgressLibationFilesRb = new System.Windows.Forms.RadioButton();
|
||||
this.downloadsInProgressWinTempRb = new System.Windows.Forms.RadioButton();
|
||||
this.downloadsInProgressDescLbl = new System.Windows.Forms.Label();
|
||||
this.decryptInProgressGb = new System.Windows.Forms.GroupBox();
|
||||
this.decryptInProgressLibationFilesRb = new System.Windows.Forms.RadioButton();
|
||||
this.decryptInProgressWinTempRb = new System.Windows.Forms.RadioButton();
|
||||
this.decryptInProgressDescLbl = new System.Windows.Forms.Label();
|
||||
this.saveBtn = new System.Windows.Forms.Button();
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.groupBox1 = new System.Windows.Forms.GroupBox();
|
||||
this.downloadsInProgressGb.SuspendLayout();
|
||||
this.decryptInProgressGb.SuspendLayout();
|
||||
this.groupBox1.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// booksLocationLbl
|
||||
//
|
||||
this.booksLocationLbl.AutoSize = true;
|
||||
this.booksLocationLbl.Location = new System.Drawing.Point(12, 17);
|
||||
this.booksLocationLbl.Name = "booksLocationLbl";
|
||||
this.booksLocationLbl.Size = new System.Drawing.Size(77, 13);
|
||||
this.booksLocationLbl.TabIndex = 0;
|
||||
this.booksLocationLbl.Text = "Books location";
|
||||
//
|
||||
// booksLocationTb
|
||||
//
|
||||
this.booksLocationTb.Location = new System.Drawing.Point(95, 14);
|
||||
this.booksLocationTb.Name = "booksLocationTb";
|
||||
this.booksLocationTb.Size = new System.Drawing.Size(652, 20);
|
||||
this.booksLocationTb.TabIndex = 1;
|
||||
//
|
||||
// booksLocationSearchBtn
|
||||
//
|
||||
this.booksLocationSearchBtn.Location = new System.Drawing.Point(753, 12);
|
||||
this.booksLocationSearchBtn.Name = "booksLocationSearchBtn";
|
||||
this.booksLocationSearchBtn.Size = new System.Drawing.Size(35, 23);
|
||||
this.booksLocationSearchBtn.TabIndex = 2;
|
||||
this.booksLocationSearchBtn.Text = "...";
|
||||
this.booksLocationSearchBtn.UseVisualStyleBackColor = true;
|
||||
this.booksLocationSearchBtn.Click += new System.EventHandler(this.booksLocationSearchBtn_Click);
|
||||
//
|
||||
// booksLocationDescLbl
|
||||
//
|
||||
this.booksLocationDescLbl.AutoSize = true;
|
||||
this.booksLocationDescLbl.Location = new System.Drawing.Point(92, 37);
|
||||
this.booksLocationDescLbl.Name = "booksLocationDescLbl";
|
||||
this.booksLocationDescLbl.Size = new System.Drawing.Size(36, 13);
|
||||
this.booksLocationDescLbl.TabIndex = 3;
|
||||
this.booksLocationDescLbl.Text = "[desc]";
|
||||
//
|
||||
// downloadsInProgressGb
|
||||
//
|
||||
this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressLibationFilesRb);
|
||||
this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressWinTempRb);
|
||||
this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressDescLbl);
|
||||
this.downloadsInProgressGb.Location = new System.Drawing.Point(15, 19);
|
||||
this.downloadsInProgressGb.Name = "downloadsInProgressGb";
|
||||
this.downloadsInProgressGb.Size = new System.Drawing.Size(758, 117);
|
||||
this.downloadsInProgressGb.TabIndex = 4;
|
||||
this.downloadsInProgressGb.TabStop = false;
|
||||
this.downloadsInProgressGb.Text = "Downloads in progress";
|
||||
//
|
||||
// downloadsInProgressLibationFilesRb
|
||||
//
|
||||
this.downloadsInProgressLibationFilesRb.AutoSize = true;
|
||||
this.downloadsInProgressLibationFilesRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft;
|
||||
this.downloadsInProgressLibationFilesRb.Location = new System.Drawing.Point(9, 81);
|
||||
this.downloadsInProgressLibationFilesRb.Name = "downloadsInProgressLibationFilesRb";
|
||||
this.downloadsInProgressLibationFilesRb.Size = new System.Drawing.Size(193, 30);
|
||||
this.downloadsInProgressLibationFilesRb.TabIndex = 2;
|
||||
this.downloadsInProgressLibationFilesRb.TabStop = true;
|
||||
this.downloadsInProgressLibationFilesRb.Text = "[desc]\r\n[libationFiles\\DownloadsInProgress]";
|
||||
this.downloadsInProgressLibationFilesRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// downloadsInProgressWinTempRb
|
||||
//
|
||||
this.downloadsInProgressWinTempRb.AutoSize = true;
|
||||
this.downloadsInProgressWinTempRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft;
|
||||
this.downloadsInProgressWinTempRb.Location = new System.Drawing.Point(9, 45);
|
||||
this.downloadsInProgressWinTempRb.Name = "downloadsInProgressWinTempRb";
|
||||
this.downloadsInProgressWinTempRb.Size = new System.Drawing.Size(182, 30);
|
||||
this.downloadsInProgressWinTempRb.TabIndex = 1;
|
||||
this.downloadsInProgressWinTempRb.TabStop = true;
|
||||
this.downloadsInProgressWinTempRb.Text = "[desc]\r\n[winTemp\\DownloadsInProgress]";
|
||||
this.downloadsInProgressWinTempRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// downloadsInProgressDescLbl
|
||||
//
|
||||
this.downloadsInProgressDescLbl.AutoSize = true;
|
||||
this.downloadsInProgressDescLbl.Location = new System.Drawing.Point(6, 16);
|
||||
this.downloadsInProgressDescLbl.Name = "downloadsInProgressDescLbl";
|
||||
this.downloadsInProgressDescLbl.Size = new System.Drawing.Size(38, 26);
|
||||
this.downloadsInProgressDescLbl.TabIndex = 0;
|
||||
this.downloadsInProgressDescLbl.Text = "[desc]\r\n[line 2]";
|
||||
//
|
||||
// decryptInProgressGb
|
||||
//
|
||||
this.decryptInProgressGb.Controls.Add(this.decryptInProgressLibationFilesRb);
|
||||
this.decryptInProgressGb.Controls.Add(this.decryptInProgressWinTempRb);
|
||||
this.decryptInProgressGb.Controls.Add(this.decryptInProgressDescLbl);
|
||||
this.decryptInProgressGb.Location = new System.Drawing.Point(9, 144);
|
||||
this.decryptInProgressGb.Name = "decryptInProgressGb";
|
||||
this.decryptInProgressGb.Size = new System.Drawing.Size(758, 117);
|
||||
this.decryptInProgressGb.TabIndex = 5;
|
||||
this.decryptInProgressGb.TabStop = false;
|
||||
this.decryptInProgressGb.Text = "Decrypt in progress";
|
||||
//
|
||||
// decryptInProgressLibationFilesRb
|
||||
//
|
||||
this.decryptInProgressLibationFilesRb.AutoSize = true;
|
||||
this.decryptInProgressLibationFilesRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft;
|
||||
this.decryptInProgressLibationFilesRb.Location = new System.Drawing.Point(6, 81);
|
||||
this.decryptInProgressLibationFilesRb.Name = "decryptInProgressLibationFilesRb";
|
||||
this.decryptInProgressLibationFilesRb.Size = new System.Drawing.Size(177, 30);
|
||||
this.decryptInProgressLibationFilesRb.TabIndex = 2;
|
||||
this.decryptInProgressLibationFilesRb.TabStop = true;
|
||||
this.decryptInProgressLibationFilesRb.Text = "[desc]\r\n[libationFiles\\DecryptInProgress]";
|
||||
this.decryptInProgressLibationFilesRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// decryptInProgressWinTempRb
|
||||
//
|
||||
this.decryptInProgressWinTempRb.AutoSize = true;
|
||||
this.decryptInProgressWinTempRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft;
|
||||
this.decryptInProgressWinTempRb.Location = new System.Drawing.Point(6, 45);
|
||||
this.decryptInProgressWinTempRb.Name = "decryptInProgressWinTempRb";
|
||||
this.decryptInProgressWinTempRb.Size = new System.Drawing.Size(166, 30);
|
||||
this.decryptInProgressWinTempRb.TabIndex = 1;
|
||||
this.decryptInProgressWinTempRb.TabStop = true;
|
||||
this.decryptInProgressWinTempRb.Text = "[desc]\r\n[winTemp\\DecryptInProgress]";
|
||||
this.decryptInProgressWinTempRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// decryptInProgressDescLbl
|
||||
//
|
||||
this.decryptInProgressDescLbl.AutoSize = true;
|
||||
this.decryptInProgressDescLbl.Location = new System.Drawing.Point(6, 16);
|
||||
this.decryptInProgressDescLbl.Name = "decryptInProgressDescLbl";
|
||||
this.decryptInProgressDescLbl.Size = new System.Drawing.Size(38, 26);
|
||||
this.decryptInProgressDescLbl.TabIndex = 0;
|
||||
this.decryptInProgressDescLbl.Text = "[desc]\r\n[line 2]";
|
||||
//
|
||||
// saveBtn
|
||||
//
|
||||
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.saveBtn.Location = new System.Drawing.Point(612, 328);
|
||||
this.saveBtn.Name = "saveBtn";
|
||||
this.saveBtn.Size = new System.Drawing.Size(75, 23);
|
||||
this.saveBtn.TabIndex = 7;
|
||||
this.saveBtn.Text = "Save";
|
||||
this.saveBtn.UseVisualStyleBackColor = true;
|
||||
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
|
||||
//
|
||||
// cancelBtn
|
||||
//
|
||||
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
this.cancelBtn.Location = new System.Drawing.Point(713, 328);
|
||||
this.cancelBtn.Name = "cancelBtn";
|
||||
this.cancelBtn.Size = new System.Drawing.Size(75, 23);
|
||||
this.cancelBtn.TabIndex = 8;
|
||||
this.cancelBtn.Text = "Cancel";
|
||||
this.cancelBtn.UseVisualStyleBackColor = true;
|
||||
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
|
||||
//
|
||||
// groupBox1
|
||||
//
|
||||
this.groupBox1.Controls.Add(this.downloadsInProgressGb);
|
||||
this.groupBox1.Controls.Add(this.decryptInProgressGb);
|
||||
this.groupBox1.Location = new System.Drawing.Point(15, 53);
|
||||
this.groupBox1.Name = "groupBox1";
|
||||
this.groupBox1.Size = new System.Drawing.Size(773, 269);
|
||||
this.groupBox1.TabIndex = 6;
|
||||
this.groupBox1.TabStop = false;
|
||||
this.groupBox1.Text = "Advanced settings for control freaks";
|
||||
//
|
||||
// SettingsDialog
|
||||
//
|
||||
this.AcceptButton = this.saveBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.CancelButton = this.cancelBtn;
|
||||
this.ClientSize = new System.Drawing.Size(800, 363);
|
||||
this.Controls.Add(this.groupBox1);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
this.Controls.Add(this.saveBtn);
|
||||
this.Controls.Add(this.booksLocationDescLbl);
|
||||
this.Controls.Add(this.booksLocationSearchBtn);
|
||||
this.Controls.Add(this.booksLocationTb);
|
||||
this.Controls.Add(this.booksLocationLbl);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
|
||||
this.Name = "SettingsDialog";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Edit Settings";
|
||||
this.Load += new System.EventHandler(this.SettingsDialog_Load);
|
||||
this.downloadsInProgressGb.ResumeLayout(false);
|
||||
this.downloadsInProgressGb.PerformLayout();
|
||||
this.decryptInProgressGb.ResumeLayout(false);
|
||||
this.decryptInProgressGb.PerformLayout();
|
||||
this.groupBox1.ResumeLayout(false);
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
this.booksLocationLbl = new System.Windows.Forms.Label();
|
||||
this.booksLocationTb = new System.Windows.Forms.TextBox();
|
||||
this.booksLocationSearchBtn = new System.Windows.Forms.Button();
|
||||
this.booksLocationDescLbl = new System.Windows.Forms.Label();
|
||||
this.downloadsInProgressGb = new System.Windows.Forms.GroupBox();
|
||||
this.downloadsInProgressLibationFilesRb = new System.Windows.Forms.RadioButton();
|
||||
this.downloadsInProgressWinTempRb = new System.Windows.Forms.RadioButton();
|
||||
this.downloadsInProgressDescLbl = new System.Windows.Forms.Label();
|
||||
this.decryptInProgressGb = new System.Windows.Forms.GroupBox();
|
||||
this.decryptInProgressLibationFilesRb = new System.Windows.Forms.RadioButton();
|
||||
this.decryptInProgressWinTempRb = new System.Windows.Forms.RadioButton();
|
||||
this.decryptInProgressDescLbl = new System.Windows.Forms.Label();
|
||||
this.saveBtn = new System.Windows.Forms.Button();
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.groupBox1 = new System.Windows.Forms.GroupBox();
|
||||
this.allowLibationFixupCbox = new System.Windows.Forms.CheckBox();
|
||||
this.downloadsInProgressGb.SuspendLayout();
|
||||
this.decryptInProgressGb.SuspendLayout();
|
||||
this.groupBox1.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// booksLocationLbl
|
||||
//
|
||||
this.booksLocationLbl.AutoSize = true;
|
||||
this.booksLocationLbl.Location = new System.Drawing.Point(14, 20);
|
||||
this.booksLocationLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.booksLocationLbl.Name = "booksLocationLbl";
|
||||
this.booksLocationLbl.Size = new System.Drawing.Size(85, 15);
|
||||
this.booksLocationLbl.TabIndex = 0;
|
||||
this.booksLocationLbl.Text = "Books location";
|
||||
//
|
||||
// booksLocationTb
|
||||
//
|
||||
this.booksLocationTb.Location = new System.Drawing.Point(111, 16);
|
||||
this.booksLocationTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.booksLocationTb.Name = "booksLocationTb";
|
||||
this.booksLocationTb.Size = new System.Drawing.Size(760, 23);
|
||||
this.booksLocationTb.TabIndex = 1;
|
||||
//
|
||||
// booksLocationSearchBtn
|
||||
//
|
||||
this.booksLocationSearchBtn.Location = new System.Drawing.Point(878, 14);
|
||||
this.booksLocationSearchBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.booksLocationSearchBtn.Name = "booksLocationSearchBtn";
|
||||
this.booksLocationSearchBtn.Size = new System.Drawing.Size(41, 27);
|
||||
this.booksLocationSearchBtn.TabIndex = 2;
|
||||
this.booksLocationSearchBtn.Text = "...";
|
||||
this.booksLocationSearchBtn.UseVisualStyleBackColor = true;
|
||||
this.booksLocationSearchBtn.Click += new System.EventHandler(this.booksLocationSearchBtn_Click);
|
||||
//
|
||||
// booksLocationDescLbl
|
||||
//
|
||||
this.booksLocationDescLbl.AutoSize = true;
|
||||
this.booksLocationDescLbl.Location = new System.Drawing.Point(107, 43);
|
||||
this.booksLocationDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.booksLocationDescLbl.Name = "booksLocationDescLbl";
|
||||
this.booksLocationDescLbl.Size = new System.Drawing.Size(39, 15);
|
||||
this.booksLocationDescLbl.TabIndex = 3;
|
||||
this.booksLocationDescLbl.Text = "[desc]";
|
||||
//
|
||||
// downloadsInProgressGb
|
||||
//
|
||||
this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressLibationFilesRb);
|
||||
this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressWinTempRb);
|
||||
this.downloadsInProgressGb.Controls.Add(this.downloadsInProgressDescLbl);
|
||||
this.downloadsInProgressGb.Location = new System.Drawing.Point(10, 49);
|
||||
this.downloadsInProgressGb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.downloadsInProgressGb.Name = "downloadsInProgressGb";
|
||||
this.downloadsInProgressGb.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.downloadsInProgressGb.Size = new System.Drawing.Size(884, 135);
|
||||
this.downloadsInProgressGb.TabIndex = 4;
|
||||
this.downloadsInProgressGb.TabStop = false;
|
||||
this.downloadsInProgressGb.Text = "Downloads in progress";
|
||||
//
|
||||
// downloadsInProgressLibationFilesRb
|
||||
//
|
||||
this.downloadsInProgressLibationFilesRb.AutoSize = true;
|
||||
this.downloadsInProgressLibationFilesRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft;
|
||||
this.downloadsInProgressLibationFilesRb.Location = new System.Drawing.Point(10, 93);
|
||||
this.downloadsInProgressLibationFilesRb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.downloadsInProgressLibationFilesRb.Name = "downloadsInProgressLibationFilesRb";
|
||||
this.downloadsInProgressLibationFilesRb.Size = new System.Drawing.Size(215, 34);
|
||||
this.downloadsInProgressLibationFilesRb.TabIndex = 2;
|
||||
this.downloadsInProgressLibationFilesRb.TabStop = true;
|
||||
this.downloadsInProgressLibationFilesRb.Text = "[desc]\r\n[libationFiles\\DownloadsInProgress]";
|
||||
this.downloadsInProgressLibationFilesRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// downloadsInProgressWinTempRb
|
||||
//
|
||||
this.downloadsInProgressWinTempRb.AutoSize = true;
|
||||
this.downloadsInProgressWinTempRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft;
|
||||
this.downloadsInProgressWinTempRb.Location = new System.Drawing.Point(10, 52);
|
||||
this.downloadsInProgressWinTempRb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.downloadsInProgressWinTempRb.Name = "downloadsInProgressWinTempRb";
|
||||
this.downloadsInProgressWinTempRb.Size = new System.Drawing.Size(200, 34);
|
||||
this.downloadsInProgressWinTempRb.TabIndex = 1;
|
||||
this.downloadsInProgressWinTempRb.TabStop = true;
|
||||
this.downloadsInProgressWinTempRb.Text = "[desc]\r\n[winTemp\\DownloadsInProgress]";
|
||||
this.downloadsInProgressWinTempRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// downloadsInProgressDescLbl
|
||||
//
|
||||
this.downloadsInProgressDescLbl.AutoSize = true;
|
||||
this.downloadsInProgressDescLbl.Location = new System.Drawing.Point(7, 18);
|
||||
this.downloadsInProgressDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.downloadsInProgressDescLbl.Name = "downloadsInProgressDescLbl";
|
||||
this.downloadsInProgressDescLbl.Size = new System.Drawing.Size(43, 30);
|
||||
this.downloadsInProgressDescLbl.TabIndex = 0;
|
||||
this.downloadsInProgressDescLbl.Text = "[desc]\r\n[line 2]";
|
||||
//
|
||||
// decryptInProgressGb
|
||||
//
|
||||
this.decryptInProgressGb.Controls.Add(this.decryptInProgressLibationFilesRb);
|
||||
this.decryptInProgressGb.Controls.Add(this.decryptInProgressWinTempRb);
|
||||
this.decryptInProgressGb.Controls.Add(this.decryptInProgressDescLbl);
|
||||
this.decryptInProgressGb.Location = new System.Drawing.Point(10, 193);
|
||||
this.decryptInProgressGb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.decryptInProgressGb.Name = "decryptInProgressGb";
|
||||
this.decryptInProgressGb.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.decryptInProgressGb.Size = new System.Drawing.Size(884, 135);
|
||||
this.decryptInProgressGb.TabIndex = 5;
|
||||
this.decryptInProgressGb.TabStop = false;
|
||||
this.decryptInProgressGb.Text = "Decrypt in progress";
|
||||
//
|
||||
// decryptInProgressLibationFilesRb
|
||||
//
|
||||
this.decryptInProgressLibationFilesRb.AutoSize = true;
|
||||
this.decryptInProgressLibationFilesRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft;
|
||||
this.decryptInProgressLibationFilesRb.Location = new System.Drawing.Point(7, 93);
|
||||
this.decryptInProgressLibationFilesRb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.decryptInProgressLibationFilesRb.Name = "decryptInProgressLibationFilesRb";
|
||||
this.decryptInProgressLibationFilesRb.Size = new System.Drawing.Size(197, 34);
|
||||
this.decryptInProgressLibationFilesRb.TabIndex = 2;
|
||||
this.decryptInProgressLibationFilesRb.TabStop = true;
|
||||
this.decryptInProgressLibationFilesRb.Text = "[desc]\r\n[libationFiles\\DecryptInProgress]";
|
||||
this.decryptInProgressLibationFilesRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// decryptInProgressWinTempRb
|
||||
//
|
||||
this.decryptInProgressWinTempRb.AutoSize = true;
|
||||
this.decryptInProgressWinTempRb.CheckAlign = System.Drawing.ContentAlignment.TopLeft;
|
||||
this.decryptInProgressWinTempRb.Location = new System.Drawing.Point(7, 52);
|
||||
this.decryptInProgressWinTempRb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.decryptInProgressWinTempRb.Name = "decryptInProgressWinTempRb";
|
||||
this.decryptInProgressWinTempRb.Size = new System.Drawing.Size(182, 34);
|
||||
this.decryptInProgressWinTempRb.TabIndex = 1;
|
||||
this.decryptInProgressWinTempRb.TabStop = true;
|
||||
this.decryptInProgressWinTempRb.Text = "[desc]\r\n[winTemp\\DecryptInProgress]";
|
||||
this.decryptInProgressWinTempRb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// decryptInProgressDescLbl
|
||||
//
|
||||
this.decryptInProgressDescLbl.AutoSize = true;
|
||||
this.decryptInProgressDescLbl.Location = new System.Drawing.Point(7, 18);
|
||||
this.decryptInProgressDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.decryptInProgressDescLbl.Name = "decryptInProgressDescLbl";
|
||||
this.decryptInProgressDescLbl.Size = new System.Drawing.Size(43, 30);
|
||||
this.decryptInProgressDescLbl.TabIndex = 0;
|
||||
this.decryptInProgressDescLbl.Text = "[desc]\r\n[line 2]";
|
||||
//
|
||||
// saveBtn
|
||||
//
|
||||
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.saveBtn.Location = new System.Drawing.Point(714, 401);
|
||||
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.saveBtn.Name = "saveBtn";
|
||||
this.saveBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.saveBtn.TabIndex = 7;
|
||||
this.saveBtn.Text = "Save";
|
||||
this.saveBtn.UseVisualStyleBackColor = true;
|
||||
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
|
||||
//
|
||||
// cancelBtn
|
||||
//
|
||||
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
this.cancelBtn.Location = new System.Drawing.Point(832, 401);
|
||||
this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.cancelBtn.Name = "cancelBtn";
|
||||
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.cancelBtn.TabIndex = 8;
|
||||
this.cancelBtn.Text = "Cancel";
|
||||
this.cancelBtn.UseVisualStyleBackColor = true;
|
||||
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
|
||||
//
|
||||
// groupBox1
|
||||
//
|
||||
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);
|
||||
this.groupBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.groupBox1.Name = "groupBox1";
|
||||
this.groupBox1.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.groupBox1.Size = new System.Drawing.Size(902, 334);
|
||||
this.groupBox1.TabIndex = 6;
|
||||
this.groupBox1.TabStop = false;
|
||||
this.groupBox1.Text = "Advanced settings for control freaks";
|
||||
//
|
||||
// downloadChaptersCbox
|
||||
//
|
||||
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
|
||||
//
|
||||
this.AcceptButton = this.saveBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.CancelButton = this.cancelBtn;
|
||||
this.ClientSize = new System.Drawing.Size(933, 442);
|
||||
this.Controls.Add(this.groupBox1);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
this.Controls.Add(this.saveBtn);
|
||||
this.Controls.Add(this.booksLocationDescLbl);
|
||||
this.Controls.Add(this.booksLocationSearchBtn);
|
||||
this.Controls.Add(this.booksLocationTb);
|
||||
this.Controls.Add(this.booksLocationLbl);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.Name = "SettingsDialog";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Edit Settings";
|
||||
this.Load += new System.EventHandler(this.SettingsDialog_Load);
|
||||
this.downloadsInProgressGb.ResumeLayout(false);
|
||||
this.downloadsInProgressGb.PerformLayout();
|
||||
this.decryptInProgressGb.ResumeLayout(false);
|
||||
this.decryptInProgressGb.PerformLayout();
|
||||
this.groupBox1.ResumeLayout(false);
|
||||
this.groupBox1.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
@@ -252,5 +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 allowLibationFixupCbox;
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,8 @@ namespace LibationWinForms.Dialogs
|
||||
? config.Books
|
||||
: Path.GetDirectoryName(Exe.FileLocationOnDisk);
|
||||
|
||||
allowLibationFixupCbox.Checked = config.AllowLibationFixup;
|
||||
|
||||
switch (config.DownloadsInProgressEnum)
|
||||
{
|
||||
case "LibationFiles":
|
||||
@@ -71,6 +73,7 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
private void saveBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
config.AllowLibationFixup = allowLibationFixupCbox.Checked;
|
||||
config.DownloadsInProgressEnum = downloadsInProgressLibationFilesRb.Checked ? "LibationFiles" : "WinTemp";
|
||||
config.DecryptInProgressEnum = decryptInProgressLibationFilesRb.Checked ? "LibationFiles" : "WinTemp";
|
||||
|
||||
|
||||
@@ -1,64 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
@@ -117,4 +57,55 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="booksLocationLbl.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="booksLocationTb.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="booksLocationSearchBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="booksLocationDescLbl.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="downloadsInProgressGb.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>
|
||||
<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>
|
||||
<metadata name="cancelBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="groupBox1.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>
|
||||
</root>
|
||||
@@ -117,7 +117,7 @@ namespace LibationWinForms
|
||||
{
|
||||
if (AudibleFileStorage.Audio.Exists(productId))
|
||||
return AudioFileState.full;
|
||||
if (AudibleFileStorage.AAX.Exists(productId))
|
||||
if (AudibleFileStorage.AAXC.Exists(productId))
|
||||
return AudioFileState.aax;
|
||||
return AudioFileState.none;
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ namespace LibationWinForms
|
||||
[Browsable(false)]
|
||||
public IEnumerable<string> TagsEnumerated => book.UserDefinedItem.TagsEnumerated;
|
||||
|
||||
public enum LiberatedState { NotDownloaded, DRM, Liberated }
|
||||
public enum LiberatedState { NotDownloaded, PartialDownload, Liberated }
|
||||
[Browsable(false)]
|
||||
public LiberatedState Liberated_Status
|
||||
=> FileManager.AudibleFileStorage.Audio.Exists(book.AudibleProductId) ? LiberatedState.Liberated
|
||||
: FileManager.AudibleFileStorage.AAX.Exists(book.AudibleProductId) ? LiberatedState.DRM
|
||||
: FileManager.AudibleFileStorage.AAXC.Exists(book.AudibleProductId) ? LiberatedState.PartialDownload
|
||||
: LiberatedState.NotDownloaded;
|
||||
|
||||
public enum PdfState { NoPdf, Downloaded, NotDownloaded }
|
||||
|
||||
@@ -126,7 +126,7 @@ namespace LibationWinForms
|
||||
var libState = liberatedStatus switch
|
||||
{
|
||||
GridEntry.LiberatedState.Liberated => "Liberated",
|
||||
GridEntry.LiberatedState.DRM => "Downloaded but needs DRM removed",
|
||||
GridEntry.LiberatedState.PartialDownload => "File has been at least\r\npartially downloaded",
|
||||
GridEntry.LiberatedState.NotDownloaded => "Book NOT downloaded",
|
||||
_ => throw new Exception("Unexpected liberation state")
|
||||
};
|
||||
@@ -142,7 +142,7 @@ namespace LibationWinForms
|
||||
var text = libState + pdfState;
|
||||
|
||||
if (liberatedStatus == GridEntry.LiberatedState.NotDownloaded ||
|
||||
liberatedStatus == GridEntry.LiberatedState.DRM ||
|
||||
liberatedStatus == GridEntry.LiberatedState.PartialDownload ||
|
||||
pdfStatus == GridEntry.PdfState.NotDownloaded)
|
||||
text += "\r\nClick to complete";
|
||||
|
||||
@@ -154,7 +154,7 @@ namespace LibationWinForms
|
||||
{
|
||||
var image_lib
|
||||
= liberatedStatus == GridEntry.LiberatedState.NotDownloaded ? "red"
|
||||
: liberatedStatus == GridEntry.LiberatedState.DRM ? "yellow"
|
||||
: liberatedStatus == GridEntry.LiberatedState.PartialDownload ? "yellow"
|
||||
: liberatedStatus == GridEntry.LiberatedState.Liberated ? "green"
|
||||
: throw new Exception("Unexpected liberation state");
|
||||
var image_pdf
|
||||
@@ -182,15 +182,7 @@ namespace LibationWinForms
|
||||
return;
|
||||
}
|
||||
|
||||
// not liberated: liberate
|
||||
var msg
|
||||
= "Liberate entire library instead?"
|
||||
+ "\r\n\r\nClick Yes to begin liberating your entire library"
|
||||
+ "\r\n\r\nClick No to liberate this book only";
|
||||
if (MessageBox.Show(msg, "Liberate entire library?", MessageBoxButtons.YesNo) == DialogResult.Yes)
|
||||
await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync((_, libraryBook) => RefreshRow(libraryBook.Book.AudibleProductId));
|
||||
else
|
||||
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(productId, (_, __) => RefreshRow(productId));
|
||||
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(productId, (_, __) => RefreshRow(productId));
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -231,3 +231,7 @@ To make upgrades and reinstalls easier, Libation separates all of its responsibi
|
||||
* In a separate folder, Libation keeps track of all of the files it creates like settings and downloaded images. After an upgrade, Libation might think that's its being run for the first time. Just click ADVANCED SETUP and point to this folder. Libation will reload your library and settings.
|
||||
|
||||
* The last important folder is the "books location." This is where Libation looks for your downloaded and decrypted books. This is how it knows which books still need to be downloaded. The Audible id must be somewhere in the book's file or folder name for Libation to detect your downloaded book.
|
||||
|
||||
### Settings
|
||||
|
||||
* Allow Libation to fix up audiobook metadata. After decrypting a title, Libation attempts to fix details like chapters and cover art. Some power users and/or control freaks prefer to manage this themselves. By unchecking this setting, Libation will only decrypt the book and will leave metadata as-is, warts and all.
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
//
|
||||
// approvedBtn
|
||||
//
|
||||
this.approvedBtn.Location = new System.Drawing.Point(15, 25);
|
||||
this.approvedBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.approvedBtn.Location = new System.Drawing.Point(18, 75);
|
||||
this.approvedBtn.Name = "approvedBtn";
|
||||
this.approvedBtn.Size = new System.Drawing.Size(79, 23);
|
||||
this.approvedBtn.TabIndex = 1;
|
||||
@@ -46,16 +47,17 @@
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Location = new System.Drawing.Point(12, 9);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(104, 13);
|
||||
this.label1.Size = new System.Drawing.Size(280, 39);
|
||||
this.label1.TabIndex = 0;
|
||||
this.label1.Text = "Click after approving";
|
||||
this.label1.Text = "Amazon is sending you an email.\r\n\r\nPlease press this button after you approve the" +
|
||||
" notification.";
|
||||
//
|
||||
// ApprovalNeededDialog
|
||||
//
|
||||
this.AcceptButton = this.approvedBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(149, 60);
|
||||
this.ClientSize = new System.Drawing.Size(345, 115);
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.approvedBtn);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
@@ -64,7 +66,7 @@
|
||||
this.Name = "ApprovalNeededDialog";
|
||||
this.ShowIcon = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Approval Needed";
|
||||
this.Text = "Approval Alert Detected";
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
|
||||
120
WinFormsDesigner/Dialogs/Login/ApprovalNeededDialog.resx
Normal file
120
WinFormsDesigner/Dialogs/Login/ApprovalNeededDialog.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -182,6 +182,9 @@
|
||||
<EmbeddedResource Include="Dialogs\LibationFilesDialog.resx">
|
||||
<DependentUpon>LibationFilesDialog.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Dialogs\Login\ApprovalNeededDialog.resx">
|
||||
<DependentUpon>ApprovalNeededDialog.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Dialogs\ScanAccountsDialog.resx">
|
||||
<DependentUpon>ScanAccountsDialog.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ffmpeg_decrypt
|
||||
{
|
||||
// from: http://csharptest.net/529/how-to-correctly-escape-command-line-arguments-in-c/index.html
|
||||
public static class Escaper
|
||||
{
|
||||
/// <summary>
|
||||
/// Quotes all arguments that contain whitespace, or begin with a quote and returns a single
|
||||
/// argument string for use with Process.Start().
|
||||
/// </summary>
|
||||
/// <param name="args">A list of strings for arguments, may not contain null, '\0', '\r', or '\n'</param>
|
||||
/// <returns>The combined list of escaped/quoted strings</returns>
|
||||
/// <exception cref="System.ArgumentNullException">Raised when one of the arguments is null</exception>
|
||||
/// <exception cref="System.ArgumentOutOfRangeException">Raised if an argument contains '\0', '\r', or '\n'</exception>
|
||||
public static string EscapeArguments(params string[] args)
|
||||
{
|
||||
var arguments = new System.Text.StringBuilder();
|
||||
var invalidChar = new Regex("[\x00\x0a\x0d]");// these can not be escaped
|
||||
var needsQuotes = new Regex(@"\s|""");// contains whitespace or two quote characters
|
||||
var escapeQuote = new Regex(@"(\\*)(""|$)");// one or more '\' followed with a quote or end of string
|
||||
|
||||
for (int carg = 0; args != null && carg < args.Length; carg++)
|
||||
{
|
||||
if (args[carg] == null)
|
||||
throw new ArgumentNullException("args[" + carg + "]");
|
||||
|
||||
if (invalidChar.IsMatch(args[carg]))
|
||||
throw new ArgumentOutOfRangeException("args[" + carg + "]");
|
||||
|
||||
if (args[carg] == string.Empty)
|
||||
arguments.Append("\"\"");
|
||||
else if (!needsQuotes.IsMatch(args[carg]))
|
||||
arguments.Append(args[carg]);
|
||||
else
|
||||
{
|
||||
arguments.Append('"');
|
||||
arguments.Append(escapeQuote.Replace(args[carg], m =>
|
||||
m.Groups[1].Value + m.Groups[1].Value +
|
||||
(m.Groups[2].Value == "\"" ? "\\\"" : "")
|
||||
));
|
||||
arguments.Append('"');
|
||||
}
|
||||
|
||||
if (carg + 1 < args.Length)
|
||||
arguments.Append(' ');
|
||||
}
|
||||
return arguments.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace ffmpeg_decrypt
|
||||
{
|
||||
public static class StringExt
|
||||
{
|
||||
public static string SurroundWithQuotes(this string str) => "\"" + str + "\"";
|
||||
|
||||
public static string ExtractString(this string haystack, string before, int needleLength)
|
||||
{
|
||||
var index = haystack.IndexOf(before);
|
||||
var needle = haystack.Substring(index + before.Length, needleLength);
|
||||
|
||||
return needle;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ControlExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the Action asynchronously on the UI thread, does not block execution on the calling thread.
|
||||
/// </summary>
|
||||
/// <param name="control"></param>
|
||||
/// <param name="code"></param>
|
||||
public static void UIThread(this Control control, Action code)
|
||||
{
|
||||
if (control.InvokeRequired)
|
||||
control.BeginInvoke(code);
|
||||
else
|
||||
code.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
293
_Demos/ffmpeg decrypt/Form1.Designer.cs
generated
293
_Demos/ffmpeg decrypt/Form1.Designer.cs
generated
@@ -1,293 +0,0 @@
|
||||
namespace ffmpeg_decrypt
|
||||
{
|
||||
partial class Form1
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
|
||||
this.inpbutton = new System.Windows.Forms.Button();
|
||||
this.outpbutton = new System.Windows.Forms.Button();
|
||||
this.inputdisplay = new System.Windows.Forms.TextBox();
|
||||
this.outputdisplay = new System.Windows.Forms.TextBox();
|
||||
this.convertbutton = new System.Windows.Forms.Button();
|
||||
this.txtConsole = new System.Windows.Forms.TextBox();
|
||||
this.qualityCombo = new System.Windows.Forms.ComboBox();
|
||||
this.setQualityLbl = new System.Windows.Forms.Label();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
this.statuslbl = new System.Windows.Forms.Label();
|
||||
this.rmp3 = new System.Windows.Forms.RadioButton();
|
||||
this.raac = new System.Windows.Forms.RadioButton();
|
||||
this.rflac = new System.Windows.Forms.RadioButton();
|
||||
this.inputPnl = new System.Windows.Forms.Panel();
|
||||
this.convertGb = new System.Windows.Forms.GroupBox();
|
||||
this.decryptConvertPnl = new System.Windows.Forms.Panel();
|
||||
this.convertRb = new System.Windows.Forms.RadioButton();
|
||||
this.decryptRb = new System.Windows.Forms.RadioButton();
|
||||
this.inputPnl.SuspendLayout();
|
||||
this.convertGb.SuspendLayout();
|
||||
this.decryptConvertPnl.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// inpbutton
|
||||
//
|
||||
this.inpbutton.Location = new System.Drawing.Point(306, 0);
|
||||
this.inpbutton.Name = "inpbutton";
|
||||
this.inpbutton.Size = new System.Drawing.Size(99, 23);
|
||||
this.inpbutton.TabIndex = 0;
|
||||
this.inpbutton.Text = "Choose .aax ...";
|
||||
this.inpbutton.UseVisualStyleBackColor = true;
|
||||
this.inpbutton.Click += new System.EventHandler(this.inpbutton_Click);
|
||||
//
|
||||
// outpbutton
|
||||
//
|
||||
this.outpbutton.Location = new System.Drawing.Point(306, 29);
|
||||
this.outpbutton.Name = "outpbutton";
|
||||
this.outpbutton.Size = new System.Drawing.Size(99, 23);
|
||||
this.outpbutton.TabIndex = 1;
|
||||
this.outpbutton.Text = "Set extract dir...";
|
||||
this.outpbutton.UseVisualStyleBackColor = true;
|
||||
this.outpbutton.Click += new System.EventHandler(this.outpbutton_Click);
|
||||
//
|
||||
// inputdisplay
|
||||
//
|
||||
this.inputdisplay.Location = new System.Drawing.Point(0, 2);
|
||||
this.inputdisplay.Name = "inputdisplay";
|
||||
this.inputdisplay.Size = new System.Drawing.Size(300, 20);
|
||||
this.inputdisplay.TabIndex = 2;
|
||||
//
|
||||
// outputdisplay
|
||||
//
|
||||
this.outputdisplay.Location = new System.Drawing.Point(0, 31);
|
||||
this.outputdisplay.Name = "outputdisplay";
|
||||
this.outputdisplay.Size = new System.Drawing.Size(300, 20);
|
||||
this.outputdisplay.TabIndex = 3;
|
||||
//
|
||||
// convertbutton
|
||||
//
|
||||
this.convertbutton.Enabled = false;
|
||||
this.convertbutton.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
|
||||
this.convertbutton.Location = new System.Drawing.Point(306, 58);
|
||||
this.convertbutton.Name = "convertbutton";
|
||||
this.convertbutton.Size = new System.Drawing.Size(99, 107);
|
||||
this.convertbutton.TabIndex = 6;
|
||||
this.convertbutton.Text = "Convert Audible Audio File";
|
||||
this.convertbutton.UseVisualStyleBackColor = true;
|
||||
this.convertbutton.Click += new System.EventHandler(this.convertbutton_Click);
|
||||
//
|
||||
// txtConsole
|
||||
//
|
||||
this.txtConsole.BackColor = System.Drawing.Color.Black;
|
||||
this.txtConsole.ForeColor = System.Drawing.Color.White;
|
||||
this.txtConsole.Location = new System.Drawing.Point(12, 199);
|
||||
this.txtConsole.Multiline = true;
|
||||
this.txtConsole.Name = "txtConsole";
|
||||
this.txtConsole.ReadOnly = true;
|
||||
this.txtConsole.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
|
||||
this.txtConsole.Size = new System.Drawing.Size(405, 184);
|
||||
this.txtConsole.TabIndex = 7;
|
||||
//
|
||||
// qualityCombo
|
||||
//
|
||||
this.qualityCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.qualityCombo.FormattingEnabled = true;
|
||||
this.qualityCombo.Items.AddRange(new object[] {
|
||||
"32",
|
||||
"80",
|
||||
"96",
|
||||
"128",
|
||||
"160",
|
||||
"192",
|
||||
"256",
|
||||
"320"});
|
||||
this.qualityCombo.Location = new System.Drawing.Point(148, 41);
|
||||
this.qualityCombo.Name = "qualityCombo";
|
||||
this.qualityCombo.Size = new System.Drawing.Size(146, 21);
|
||||
this.qualityCombo.TabIndex = 9;
|
||||
//
|
||||
// setQualityLbl
|
||||
//
|
||||
this.setQualityLbl.AutoSize = true;
|
||||
this.setQualityLbl.Location = new System.Drawing.Point(145, 21);
|
||||
this.setQualityLbl.Name = "setQualityLbl";
|
||||
this.setQualityLbl.Size = new System.Drawing.Size(149, 13);
|
||||
this.setQualityLbl.TabIndex = 10;
|
||||
this.setQualityLbl.Text = "Set MP3/M4B Quality (kbit/s):";
|
||||
//
|
||||
// label1
|
||||
//
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Location = new System.Drawing.Point(12, 183);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(40, 13);
|
||||
this.label1.TabIndex = 11;
|
||||
this.label1.Text = "Status:";
|
||||
//
|
||||
// statuslbl
|
||||
//
|
||||
this.statuslbl.AutoSize = true;
|
||||
this.statuslbl.Location = new System.Drawing.Point(58, 183);
|
||||
this.statuslbl.Name = "statuslbl";
|
||||
this.statuslbl.Size = new System.Drawing.Size(51, 13);
|
||||
this.statuslbl.TabIndex = 12;
|
||||
this.statuslbl.Text = "[statuslbl]";
|
||||
//
|
||||
// rmp3
|
||||
//
|
||||
this.rmp3.AutoSize = true;
|
||||
this.rmp3.Location = new System.Drawing.Point(6, 19);
|
||||
this.rmp3.Name = "rmp3";
|
||||
this.rmp3.Size = new System.Drawing.Size(76, 17);
|
||||
this.rmp3.TabIndex = 13;
|
||||
this.rmp3.Text = "MP3 audio";
|
||||
this.rmp3.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// raac
|
||||
//
|
||||
this.raac.AutoSize = true;
|
||||
this.raac.Checked = true;
|
||||
this.raac.Location = new System.Drawing.Point(6, 42);
|
||||
this.raac.Name = "raac";
|
||||
this.raac.Size = new System.Drawing.Size(125, 17);
|
||||
this.raac.TabIndex = 14;
|
||||
this.raac.TabStop = true;
|
||||
this.raac.Text = "AAC M4B Audiobook";
|
||||
this.raac.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// rflac
|
||||
//
|
||||
this.rflac.AutoSize = true;
|
||||
this.rflac.Location = new System.Drawing.Point(6, 65);
|
||||
this.rflac.Name = "rflac";
|
||||
this.rflac.Size = new System.Drawing.Size(99, 17);
|
||||
this.rflac.TabIndex = 15;
|
||||
this.rflac.Text = "FLAC HQ audio";
|
||||
this.rflac.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// inputPnl
|
||||
//
|
||||
this.inputPnl.Controls.Add(this.convertGb);
|
||||
this.inputPnl.Controls.Add(this.decryptConvertPnl);
|
||||
this.inputPnl.Controls.Add(this.inputdisplay);
|
||||
this.inputPnl.Controls.Add(this.inpbutton);
|
||||
this.inputPnl.Controls.Add(this.outpbutton);
|
||||
this.inputPnl.Controls.Add(this.outputdisplay);
|
||||
this.inputPnl.Controls.Add(this.convertbutton);
|
||||
this.inputPnl.Location = new System.Drawing.Point(12, 12);
|
||||
this.inputPnl.Name = "inputPnl";
|
||||
this.inputPnl.Size = new System.Drawing.Size(405, 168);
|
||||
this.inputPnl.TabIndex = 16;
|
||||
//
|
||||
// convertGb
|
||||
//
|
||||
this.convertGb.Controls.Add(this.rmp3);
|
||||
this.convertGb.Controls.Add(this.setQualityLbl);
|
||||
this.convertGb.Controls.Add(this.rflac);
|
||||
this.convertGb.Controls.Add(this.qualityCombo);
|
||||
this.convertGb.Controls.Add(this.raac);
|
||||
this.convertGb.Location = new System.Drawing.Point(0, 80);
|
||||
this.convertGb.Name = "convertGb";
|
||||
this.convertGb.Size = new System.Drawing.Size(300, 85);
|
||||
this.convertGb.TabIndex = 14;
|
||||
this.convertGb.TabStop = false;
|
||||
this.convertGb.Text = "Convert options";
|
||||
//
|
||||
// decryptConvertPnl
|
||||
//
|
||||
this.decryptConvertPnl.Controls.Add(this.convertRb);
|
||||
this.decryptConvertPnl.Controls.Add(this.decryptRb);
|
||||
this.decryptConvertPnl.Location = new System.Drawing.Point(0, 58);
|
||||
this.decryptConvertPnl.Name = "decryptConvertPnl";
|
||||
this.decryptConvertPnl.Size = new System.Drawing.Size(266, 16);
|
||||
this.decryptConvertPnl.TabIndex = 13;
|
||||
//
|
||||
// convertRb
|
||||
//
|
||||
this.convertRb.AutoSize = true;
|
||||
this.convertRb.Location = new System.Drawing.Point(88, 0);
|
||||
this.convertRb.Name = "convertRb";
|
||||
this.convertRb.Size = new System.Drawing.Size(62, 17);
|
||||
this.convertRb.TabIndex = 1;
|
||||
this.convertRb.Text = "Convert";
|
||||
this.convertRb.UseVisualStyleBackColor = true;
|
||||
this.convertRb.CheckedChanged += new System.EventHandler(this.decryptConvertRb_CheckedChanged);
|
||||
//
|
||||
// decryptRb
|
||||
//
|
||||
this.decryptRb.AutoSize = true;
|
||||
this.decryptRb.Location = new System.Drawing.Point(0, 0);
|
||||
this.decryptRb.Name = "decryptRb";
|
||||
this.decryptRb.Size = new System.Drawing.Size(82, 17);
|
||||
this.decryptRb.TabIndex = 0;
|
||||
this.decryptRb.Text = "Just decrypt";
|
||||
this.decryptRb.UseVisualStyleBackColor = true;
|
||||
this.decryptRb.CheckedChanged += new System.EventHandler(this.decryptConvertRb_CheckedChanged);
|
||||
//
|
||||
// Form1
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(429, 395);
|
||||
this.Controls.Add(this.inputPnl);
|
||||
this.Controls.Add(this.txtConsole);
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.statuslbl);
|
||||
this.Name = "Form1";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "Open Source Audible Converter";
|
||||
this.inputPnl.ResumeLayout(false);
|
||||
this.inputPnl.PerformLayout();
|
||||
this.convertGb.ResumeLayout(false);
|
||||
this.convertGb.PerformLayout();
|
||||
this.decryptConvertPnl.ResumeLayout(false);
|
||||
this.decryptConvertPnl.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Button inpbutton;
|
||||
private System.Windows.Forms.Button outpbutton;
|
||||
private System.Windows.Forms.TextBox inputdisplay;
|
||||
private System.Windows.Forms.TextBox outputdisplay;
|
||||
private System.Windows.Forms.Button convertbutton;
|
||||
private System.Windows.Forms.TextBox txtConsole;
|
||||
private System.Windows.Forms.ComboBox qualityCombo;
|
||||
private System.Windows.Forms.Label setQualityLbl;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.Label statuslbl;
|
||||
private System.Windows.Forms.RadioButton rmp3;
|
||||
private System.Windows.Forms.RadioButton raac;
|
||||
private System.Windows.Forms.RadioButton rflac;
|
||||
private System.Windows.Forms.Panel inputPnl;
|
||||
private System.Windows.Forms.GroupBox convertGb;
|
||||
private System.Windows.Forms.Panel decryptConvertPnl;
|
||||
private System.Windows.Forms.RadioButton convertRb;
|
||||
private System.Windows.Forms.RadioButton decryptRb;
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace ffmpeg_decrypt
|
||||
{
|
||||
public partial class Form1 : Form
|
||||
{
|
||||
public Form1()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
qualityCombo.SelectedIndex = 1; // value == 80
|
||||
statuslbl.Text = "";
|
||||
decryptRb.Checked = true;
|
||||
}
|
||||
|
||||
private void inpbutton_Click(object sender, EventArgs e)
|
||||
{
|
||||
using var ofd = new OpenFileDialog { Filter = "Audible Audio Files|*.aax", Title = "Select an Audible Audio File", FileName = "" };
|
||||
if (ofd.ShowDialog() == DialogResult.OK)
|
||||
{
|
||||
inputdisplay.Text = ofd.FileName;
|
||||
outputdisplay.Text = Path.GetDirectoryName(ofd.FileName);
|
||||
convertbutton.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void outpbutton_Click(object sender, EventArgs e)
|
||||
{
|
||||
using var fbd = new FolderBrowserDialog();
|
||||
if (fbd.ShowDialog() == DialogResult.OK && !string.IsNullOrWhiteSpace(fbd.SelectedPath))
|
||||
outputdisplay.Text = fbd.SelectedPath;
|
||||
}
|
||||
|
||||
private async void convertbutton_Click(object sender, EventArgs e)
|
||||
{
|
||||
//var sw = Stopwatch.StartNew();
|
||||
|
||||
// disable UI
|
||||
inputPnl.Enabled = false;
|
||||
|
||||
statuslbl.Text = "Getting File Hash...";
|
||||
var checksum = await RCrack.GetChecksum(inputdisplay.Text);
|
||||
|
||||
statuslbl.Text = "Cracking Activation Bytes...";
|
||||
var activation_bytes = await RCrack.GetActivationBytes(checksum);
|
||||
|
||||
statuslbl.Text = "Converting File...";
|
||||
var encodeTo
|
||||
= decryptRb.Checked ? EncodeTo.DecryptOnly
|
||||
: rmp3.Checked ? EncodeTo.Mp3
|
||||
: raac.Checked ? EncodeTo.M4b
|
||||
: rflac.Checked ? EncodeTo.Flac
|
||||
: throw new NotImplementedException();
|
||||
await decryptAndSaveFile(activation_bytes, inputdisplay.Text, outputdisplay.Text, txtConsole, encodeTo, int.Parse(qualityCombo.Text));
|
||||
|
||||
// re-enable UI
|
||||
inputPnl.Enabled = true;
|
||||
|
||||
//sw.Stop();
|
||||
//var total = (int)sw.Elapsed.TotalSeconds;
|
||||
|
||||
statuslbl.Text = "Conversion Complete!";
|
||||
}
|
||||
|
||||
// ProcessStartInfo.Arguments: use Escaper.EscapeArguments instead of .SurroundWithQuotes()
|
||||
|
||||
// see also: https://stackoverflow.com/questions/4291912/process-start-how-to-get-the-output
|
||||
// top 2 answers show: easy, sync, async
|
||||
|
||||
enum EncodeTo
|
||||
{
|
||||
/// <summary>Decrypt only. Retain original encoding</summary>
|
||||
DecryptOnly,
|
||||
/// <summary>LAME MP3</summary>
|
||||
Mp3,
|
||||
/// <summary>M4B AAC</summary>
|
||||
M4b,
|
||||
/// <summary>FLAC HD</summary>
|
||||
Flac
|
||||
}
|
||||
|
||||
private static async Task decryptAndSaveFile(string activation_bytes, string inputPath, string outputPath, TextBoxBase debugWindow, EncodeTo encodeTo, int encodeQuality = 80)
|
||||
{
|
||||
Resources.Extract("ffmpeg.exe");
|
||||
|
||||
var fileBase = Path.Combine(outputPath, Path.GetFileNameWithoutExtension(inputPath));
|
||||
|
||||
string arguments;
|
||||
|
||||
// only decrypt. no re-encoding
|
||||
if (encodeTo == EncodeTo.DecryptOnly)
|
||||
{
|
||||
var fileout = fileBase + ".m4b";
|
||||
arguments = $"-activation_bytes {activation_bytes} -i {inputPath.SurroundWithQuotes()} -vn -c:a copy {fileout.SurroundWithQuotes()}";
|
||||
}
|
||||
// re-encode. encoding will be determined by file extension
|
||||
else // if (convertRb.Checked)
|
||||
{
|
||||
var fileout = fileBase + "." + encodeTo.ToString().ToLower();
|
||||
arguments = $"-y -activation_bytes {activation_bytes} -i {inputPath.SurroundWithQuotes()} -ab {encodeQuality} -map_metadata 0 -id3v2_version 3 -vn {fileout.SurroundWithQuotes()}";
|
||||
}
|
||||
|
||||
// nothing in stdout. progress/debug info is in stderr
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Path.Combine(Resources.resdir, "ffmpeg.exe"),
|
||||
Arguments = arguments,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = Directory.GetCurrentDirectory()
|
||||
};
|
||||
|
||||
using var ffm = new Process { StartInfo = startInfo, EnableRaisingEvents = true };
|
||||
ffm.ErrorDataReceived += (s, ea) => debugWindow.UIThread(() => debugWindow.AppendText($"DEBUG: {ea.Data}\r\n"));
|
||||
|
||||
ffm.Start();
|
||||
ffm.BeginErrorReadLine();
|
||||
await Task.Run(() => ffm.WaitForExit());
|
||||
ffm.Close();
|
||||
}
|
||||
|
||||
private void decryptConvertRb_CheckedChanged(object sender, EventArgs e) => convertGb.Enabled = convertRb.Checked;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace ffmpeg_decrypt
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
Application.SetHighDpiMode(HighDpiMode.SystemAware);
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new Form1());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ffmpeg_decrypt
|
||||
{
|
||||
public static class RCrack
|
||||
{
|
||||
public static async Task<string> GetChecksum(string aaxPath)
|
||||
{
|
||||
Resources.Extract("ffprobe.exe");
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Path.Combine(Resources.resdir, "ffprobe.exe"),
|
||||
Arguments = aaxPath.SurroundWithQuotes(),
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = Directory.GetCurrentDirectory()
|
||||
};
|
||||
|
||||
using var ffp = new Process { StartInfo = startInfo };
|
||||
ffp.Start();
|
||||
|
||||
// checksum is in the debug info. ffprobe's debug info is written to stderr, not stdout
|
||||
var ffprobeStderr = ffp.StandardError.ReadToEnd();
|
||||
|
||||
await ffp.WaitForExitAsync();
|
||||
|
||||
ffp.Close();
|
||||
|
||||
// example checksum line:
|
||||
// ... [aax] file checksum == 0c527840c4f18517157eb0b4f9d6f9317ce60cd1
|
||||
var checksum = ffprobeStderr.ExtractString("file checksum == ", 40);
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// <summary>use checksum to get activation bytes. activation bytes are unique per audible customer. only have to do this 1x/customer</summary>
|
||||
public static async Task<string> GetActivationBytes(string checksum)
|
||||
{
|
||||
Resources.Extract("rcrack.exe");
|
||||
|
||||
Resources.Extract("alglib1.dll");
|
||||
// RainbowCrack files to recover your own Audible activation data (activation_bytes) in an offline manner
|
||||
Resources.Extract("audible_byte#4-4_0_10000x789935_0.rtc");
|
||||
Resources.Extract("audible_byte#4-4_1_10000x791425_0.rtc");
|
||||
Resources.Extract("audible_byte#4-4_2_10000x790991_0.rtc");
|
||||
Resources.Extract("audible_byte#4-4_3_10000x792120_0.rtc");
|
||||
Resources.Extract("audible_byte#4-4_4_10000x790743_0.rtc");
|
||||
Resources.Extract("audible_byte#4-4_5_10000x790568_0.rtc");
|
||||
Resources.Extract("audible_byte#4-4_6_10000x791458_0.rtc");
|
||||
Resources.Extract("audible_byte#4-4_7_10000x791707_0.rtc");
|
||||
Resources.Extract("audible_byte#4-4_8_10000x790202_0.rtc");
|
||||
Resources.Extract("audible_byte#4-4_9_10000x791022_0.rtc");
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Path.Combine(Resources.resdir, "rcrack.exe"),
|
||||
Arguments = @". -h " + checksum,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = Directory.GetCurrentDirectory()
|
||||
};
|
||||
|
||||
using var rcr = new Process { StartInfo = startInfo };
|
||||
rcr.Start();
|
||||
|
||||
var rcrackStdout = rcr.StandardOutput.ReadToEnd();
|
||||
|
||||
await rcr.WaitForExitAsync();
|
||||
rcr.Close();
|
||||
|
||||
// example result
|
||||
// 0c527840c4f18517157eb0b4f9d6f9317ce60cd1 \xbd\x89X\x09 hex:bd895809
|
||||
var activation_bytes = rcrackStdout.ExtractString("hex:", 8);
|
||||
|
||||
return activation_bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace ffmpeg_decrypt
|
||||
{
|
||||
public static class Resources
|
||||
{
|
||||
public static string resdir { get; } = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "res");
|
||||
|
||||
/// <summary>extract embedded resource to file if it doesn't already exist</summary>
|
||||
public static void Extract(string resourceName)
|
||||
{
|
||||
// first determine whether files exist already in res dir
|
||||
if (File.Exists(Path.Combine(resdir, resourceName)))
|
||||
return;
|
||||
|
||||
// extract embedded resource
|
||||
// this technique works but there are easier ways:
|
||||
// https://stackoverflow.com/questions/13031778/how-can-i-extract-a-file-from-an-embedded-resource-and-save-it-to-disk
|
||||
Directory.CreateDirectory(resdir);
|
||||
using var resource = System.Reflection.Assembly.GetCallingAssembly().GetManifestResourceStream($"{nameof(ffmpeg_decrypt)}.res." + resourceName);
|
||||
using var reader = new BinaryReader(resource);
|
||||
using var file = new FileStream(Path.Combine(resdir, resourceName), FileMode.OpenOrCreate);
|
||||
using var writer = new BinaryWriter(file);
|
||||
writer.Write(reader.ReadBytes((int)resource.Length));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<RootNamespace>ffmpeg_decrypt</RootNamespace>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="res\alglib1.dll" />
|
||||
<None Remove="res\ffmpeg.exe" />
|
||||
<None Remove="res\ffprobe.exe" />
|
||||
<None Remove="res\rcrack.exe" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="res\alglib1.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="res\ffmpeg.exe">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="res\ffprobe.exe">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="res\rcrack.exe">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="res\audible_byte#4-4_0_10000x789935_0.rtc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="res\audible_byte#4-4_1_10000x791425_0.rtc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="res\audible_byte#4-4_2_10000x790991_0.rtc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="res\audible_byte#4-4_3_10000x792120_0.rtc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="res\audible_byte#4-4_4_10000x790743_0.rtc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="res\audible_byte#4-4_5_10000x790568_0.rtc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="res\audible_byte#4-4_6_10000x791458_0.rtc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="res\audible_byte#4-4_7_10000x791707_0.rtc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="res\audible_byte#4-4_8_10000x790202_0.rtc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="res\audible_byte#4-4_9_10000x791022_0.rtc">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user