Compare commits

...

130 Commits

Author SHA1 Message Date
Robert McRackan
aeeba0d567 update ver. api bug fix 2021-07-08 10:54:38 -04:00
Robert McRackan
e2f919d625 "approval needed" ui improved wording 2021-07-07 21:34:05 -04:00
Robert McRackan
e821eea333 Bug: fix "approval" login step 2021-07-07 15:53:47 -04:00
Robert McRackan
8f487894f5 Fixed logging bug in single-book liberation 2021-07-04 16:10:11 -04:00
Robert McRackan
cd3e0dba68 Remove validation against 0-length chapters. It is evidently allowed 2021-07-04 16:08:30 -04:00
rmcrackan
6f31d97763 Merge pull request #47 from Mbucari/master
Addressed two issues and some minor fixed.
2021-07-04 09:27:58 -04:00
Mbucari
fa5637a340 Merge branch 'rmcrackan:master' into master 2021-07-03 22:07:34 -06:00
Michael Bucari-Tovo
7ab209171b Merge branch 'master' of https://github.com/Mbucari/Libation 2021-07-03 22:07:08 -06:00
Michael Bucari-Tovo
6d856f73e7 Reused yellow stoplight to indicate and interrupted and resumable download. 2021-07-03 22:06:56 -06:00
Michael Bucari-Tovo
05426eb618 Added uri refresh to step 2. 2021-07-03 21:54:28 -06:00
Michael Bucari-Tovo
d73701c939 Stop automatic processing if form is closed instead of crashing. 2021-07-03 20:34:50 -06:00
Michael Bucari-Tovo
f284f53edd Clicking on red stoplight now only decrypts that book with no conformation. 2021-07-03 20:21:11 -06:00
rmcrackan
17f3187748 Merge pull request #46 from Mbucari/master
Added resumable download support to FFMpegAaxcProcessor.
2021-07-03 21:06:25 -04:00
Mbucari
f55a41ac0a Merge branch 'rmcrackan:master' into master 2021-07-03 19:00:56 -06:00
Michael Bucari-Tovo
0be2a17537 Made FFMpegAaxcProcesser use NetworkFileStream. 2021-07-03 18:59:18 -06:00
rmcrackan
b417c5695e Merge pull request #45 from Mbucari/master
Fixed critical bug with Read stream not blocking.
2021-07-03 20:55:33 -04:00
Michael Bucari-Tovo
6efe064ca7 Added support for changing Uri to the same file in case iold one expires. 2021-07-03 17:15:35 -06:00
Michael Bucari-Tovo
da7af895fb Fixed possible hang issue. 2021-07-03 14:37:24 -06:00
Mbucari
1b39f30fd0 Merge branch 'rmcrackan:master' into master 2021-07-03 14:31:23 -06:00
Michael Bucari-Tovo
9cde6bddbd Fixed Read not blocking 2021-07-03 14:31:02 -06:00
rmcrackan
b21f257baa Merge pull request #43 from Mbucari/master
Modified NetworkFileStream to make it resumable.
2021-07-03 11:14:07 -04:00
Michael Bucari-Tovo
da68ddc9b8 Renamed property. 2021-07-03 06:13:38 -06:00
Michael Bucari-Tovo
9e15fde2e3 Modified NetworkFileStream to make is resumable. 2021-07-03 06:10:51 -06:00
rmcrackan
ef5b14a929 Merge pull request #40 from Mbucari/master
Addressed Issue #37 and minor corrections
2021-07-02 23:48:44 -04:00
Michael Bucari-Tovo
5df7d80aac Revert earlier. 2021-07-02 21:21:17 -06:00
Michael Bucari-Tovo
4b2c8ee513 Add any subtitle to the title. 2021-07-02 17:00:04 -06:00
Michael Bucari-Tovo
097bda2d25 Added null check. 2021-07-02 15:58:37 -06:00
Michael Bucari-Tovo
81195e382e Revert "Remove items from library."
This reverts commit 00f7e4b779.
2021-07-02 15:24:05 -06:00
Michael Bucari-Tovo
35fc3581b3 Revert "Added count of items removed from library."
This reverts commit 771d992da7.
2021-07-02 15:23:26 -06:00
Michael Bucari-Tovo
771d992da7 Added count of items removed from library. 2021-07-02 15:01:55 -06:00
Michael Bucari-Tovo
00f7e4b779 Remove items from library. 2021-07-02 14:07:42 -06:00
Michael Bucari-Tovo
5d4bcb2db0 Removed unnecessary conversion to List. 2021-07-02 13:51:32 -06:00
Mbucari
fbf92bf151 Merge branch 'rmcrackan:master' into master 2021-07-02 08:42:14 -06:00
Robert McRackan
2cb3e34d98 update 5.0.0.x ver # 2021-07-02 10:02:37 -04:00
rmcrackan
80589e3854 Update README.md
Add "Allow Libation to fix up audiobook metadata" to readme
2021-07-02 09:52:49 -04:00
Michael Bucari-Tovo
b9770220db Fixed stupid mistake. I need to go to ber. 2021-07-01 21:56:38 -06:00
rmcrackan
11128ffb1a Merge pull request #34 from Mbucari/master
Minor code fixes and simplification
2021-07-01 23:53:38 -04:00
Michael Bucari-Tovo
1d557d05c5 Merge branch 'master' of https://github.com/Mbucari/Libation 2021-07-01 21:44:12 -06:00
Michael Bucari-Tovo
d41fe0d3e6 Added log 2021-07-01 21:42:44 -06:00
Mbucari
17bd54a897 Merge branch 'rmcrackan:master' into master 2021-07-01 18:01:37 -06:00
Michael Bucari-Tovo
0d89c34107 Added try blocks. TODO: Add error logging. 2021-07-01 18:01:03 -06:00
Michael Bucari-Tovo
66bd18fdc5 Fixed typo. 2021-07-01 17:58:02 -06:00
Robert McRackan
7143104b40 git ignore windows shortcuts: *.lnk files 2021-07-01 16:11:52 -04:00
Michael Bucari-Tovo
729212a5d5 Simplified ffmpeg chapter parser by using json insterad of xml. 2021-07-01 13:16:54 -06:00
Michael Bucari-Tovo
6dafa03554 Replaced property name string with nameof. 2021-07-01 13:14:54 -06:00
Robert McRackan
08644fb937 Move UnicodeToAscii to Dinah.Core, minor refactoring 2021-07-01 11:46:23 -04:00
Robert McRackan
7ff4953f7b RetainAaxFiles no longer used 2021-07-01 11:19:16 -04:00
rmcrackan
797112740e Merge pull request #33 from Mbucari/master
Work with updated Api Dtos and some minor readability edits.
2021-07-01 10:52:15 -04:00
Michael Bucari-Tovo
36ab494b31 Minor refactoring. 2021-07-01 08:22:04 -06:00
Michael Bucari-Tovo
8c6ada8d20 Removed unused namespaces. 2021-07-01 08:17:10 -06:00
Michael Bucari-Tovo
41b0ace238 Code readability edit. 2021-07-01 08:10:28 -06:00
Michael Bucari-Tovo
c84f144274 Merge branch 'master' of https://github.com/Mbucari/Libation 2021-07-01 07:56:50 -06:00
Michael Bucari-Tovo
00f8a63781 Use new AudibleApi Dtos. 2021-07-01 07:52:28 -06:00
Michael Bucari-Tovo
25d89207bb Better ecapsulate requestCoverArt event handler 2021-07-01 07:48:20 -06:00
rmcrackan
2146ebff29 Merge pull request #32 from Mbucari/master
Changes we discussed, plus a few more.
2021-07-01 08:45:37 -04:00
Michael Bucari-Tovo
3aed3a5def Added "indeterminate" progress bar mode to beginning of Step3. 2021-06-30 21:55:22 -06:00
Michael Bucari-Tovo
1ee6f3b9f2 Improved cancellation routine. 2021-06-30 18:33:32 -06:00
Michael Bucari-Tovo
0c26c34bdd Fixed improper condition check. 2021-06-30 18:11:56 -06:00
Michael Bucari-Tovo
6696317ae6 Changed DownloadChapters setting to AllowLibationFixup. 2021-06-30 17:38:24 -06:00
Michael Bucari-Tovo
3af84af2e2 Added events to retrieve cover art upon request using PictureStorage. 2021-06-30 17:32:34 -06:00
Michael Bucari-Tovo
2955e8b464 Added method to set cover art in tags for writing. 2021-06-30 17:31:30 -06:00
Michael Bucari-Tovo
8d6b304a8b Moved Taglib loading into the step sequence and added event and method to request cover art. 2021-06-30 17:31:01 -06:00
Michael Bucari-Tovo
aa3c648c4c Refactored remaining time estimate. 2021-06-30 13:21:39 -06:00
Michael Bucari-Tovo
0da054ccea Taglib fixes. 2021-06-30 12:20:21 -06:00
Michael Bucari-Tovo
45080d1661 Refactored tags. 2021-06-30 12:05:29 -06:00
Michael Bucari-Tovo
d6b62c0521 Removed obsolete assembly reference and minor refactoring. 2021-06-30 01:12:34 -06:00
Michael Bucari-Tovo
bc3aa29175 Minor tag reading improvements. 2021-06-30 01:06:22 -06:00
Michael Bucari-Tovo
e958944466 Added TagLib through package manager and improved tag retreival. 2021-06-30 00:54:52 -06:00
Michael Bucari-Tovo
78f278121b Add book info to DecryptForm from LibraryBook on initialization. 2021-06-29 22:04:54 -06:00
Michael Bucari-Tovo
027cce2d99 Remove DownloadBook from BackupBook. 2021-06-29 22:03:36 -06:00
Michael Bucari-Tovo
9332a6f350 Remove downlaod book events. 2021-06-29 22:02:47 -06:00
Michael Bucari-Tovo
ac6a73d898 Removed DownloadBookDummy and related process steps. 2021-06-29 22:00:05 -06:00
Michael Bucari-Tovo
74f94fe17f Add book tag null checks. 2021-06-29 21:56:08 -06:00
Robert McRackan
120fb58da7 Add null check when displaying image in form 2021-06-29 13:57:18 -04:00
Robert McRackan
ef2adfd474 If a failure occurs before aaxcProcesser is init'd, this will give a null ref exception which can obscure the actual exception that we want to be visible 2021-06-29 13:08:21 -04:00
Robert McRackan
f3a746a852 Make path safe. Colon was breaking file path 2021-06-29 12:02:04 -04:00
Robert McRackan
dc8cea5355 fix ETA label anchor 2021-06-29 12:01:29 -04:00
Robert McRackan
83cb580db7 remove fragile broken tests 2021-06-29 10:29:31 -04:00
rmcrackan
491a5eba3a Merge pull request #30 from Mbucari/master
Complete overhaul of download and decrypt.
2021-06-29 09:35:19 -04:00
Michael Bucari-Tovo
15150a3633 Added UpdateRemainingTime event unsubscribe. 2021-06-28 15:46:24 -06:00
Michael Bucari-Tovo
82e3854c84 Fixed typo and minor formatting. 2021-06-28 15:35:43 -06:00
Michael Bucari-Tovo
f0eb57a40b Added Cancel method to stop download/decrypt and added estimated time remaining event. 2021-06-28 15:24:33 -06:00
Michael Bucari-Tovo
b65f9567e0 Added user option to download chapter titles from Audible. 2021-06-28 12:55:56 -06:00
Michael Bucari-Tovo
b5389c67ea Get chapter infor from file is no downloaded chapters were supplied. 2021-06-28 12:51:28 -06:00
Michael Bucari-Tovo
d564876eaa Removed unused option to retain aax files. 2021-06-28 11:24:33 -06:00
Michael Bucari-Tovo
258887152d Removed taglib assembly reference. 2021-06-28 11:21:54 -06:00
Michael Bucari-Tovo
87c3cac013 Fixed improper async usage. 2021-06-28 11:20:38 -06:00
Michael Bucari-Tovo
f148650e57 Fixed ffmpeg cover art not showing on android as discussed. 2021-06-28 11:19:03 -06:00
Michael Bucari-Tovo
b53aabe0e3 Modified ffmpeg arguments and added argument checks. 2021-06-27 18:24:44 -06:00
Michael Bucari-Tovo
e32a39085f Fixed narrator fields. 2021-06-26 02:09:03 -06:00
Michael Bucari-Tovo
4d743df643 Fixed ProcessBook overload to preserve chapters. 2021-06-26 01:58:23 -06:00
Michael Bucari-Tovo
6bd809c7c6 Minor edits and reorganizations. 2021-06-26 01:52:18 -06:00
Michael Bucari-Tovo
9930daa914 Made changes discussed in pull request. 2021-06-26 01:39:18 -06:00
Michael Bucari-Tovo
0475bd48b1 Typos and minor corrections. 2021-06-24 21:49:52 -06:00
Michael Bucari-Tovo
1b80f2ed28 Improve progress update. 2021-06-24 18:18:52 -06:00
Michael Bucari-Tovo
37ca9abd9d Merge branch 'master' of https://github.com/Mbucari/Libation 2021-06-24 18:13:11 -06:00
Michael Bucari-Tovo
0c159df6ca Simplified download decrypt progress update.
Removed unused fields and properties.
2021-06-24 18:13:00 -06:00
Michael Bucari-Tovo
31e24ad36c Removed unused fields and properties.
Removed unused fields.
2021-06-24 18:11:44 -06:00
Michael Bucari-Tovo
be41dca9e0 Sumplified download decrypt progress update. 2021-06-24 18:09:43 -06:00
Michael Bucari-Tovo
792207caee Fixed type. 2021-06-24 17:56:08 -06:00
Michael Bucari-Tovo
06549e5b4e Added AtomicParsley back in because ffmpeg cover art wasn't working. 2021-06-24 17:53:09 -06:00
Michael Bucari-Tovo
81d0f87b8a Removed unused classes. 2021-06-24 17:05:21 -06:00
Michael Bucari-Tovo
9550aac788 Merge branch 'master' of https://github.com/Mbucari/Libation 2021-06-24 16:57:51 -06:00
Michael Bucari-Tovo
54d650ea48 Decrypt libs and taglib-sharp reference 2021-06-24 16:57:12 -06:00
Michael Bucari-Tovo
1e88070f3a Completely redesigned donload and decrypt. 2021-06-24 16:48:29 -06:00
Robert McRackan
703e71ad74 Clean up minutia 2021-06-22 22:36:16 -04:00
Robert McRackan
ae6384486c All downloads are now aaxc, not aax 2021-06-22 22:26:39 -04:00
rmcrackan
8f8e0645a4 Merge pull request #29 from Mbucari/master
Add major version migration and hacked avformat-58
2021-06-22 21:37:07 -04:00
Michael Bucari-Tovo
d619c82fd8 Added progress bar to Chapterize step. 2021-06-22 17:30:48 -06:00
Michael Bucari-Tovo
919175cc10 Fixed migration logic. 2021-06-22 16:55:56 -06:00
Michael Bucari-Tovo
8d70d2a95f Fixed migration logic. 2021-06-22 16:53:02 -06:00
Michael Bucari-Tovo
e13dc2a48a Fixed migration logic. 2021-06-22 16:48:43 -06:00
Michael Bucari-Tovo
bb3baa6ce0 Added explanation of avformat-58 hack. 2021-06-22 16:36:23 -06:00
Mbucari
28731e51f5 Merge pull request #1 from rmcrackan/master
Pull in Robert's changes.
2021-06-22 15:15:53 -06:00
Michael Bucari-Tovo
dbcd124c1d Match version 2021-06-22 15:15:08 -06:00
Robert McRackan
305de34a76 db migration to remove license keys from Books table. They are not really data related to a book. Also, it was causing problems on update due to other persistence choices. For now, store decrypt keys along side of encrypted file instead. 2021-06-22 17:02:00 -04:00
Michael Bucari-Tovo
0034d51921 Add migration to re-register device fore new Identity properties. 2021-06-22 14:59:37 -06:00
Robert McRackan
b1a033e162 Keep download license details with aax file, not in db 2021-06-22 16:49:19 -04:00
Robert McRackan
9416f4e040 Persist keys to db. Necessary in the event that download succeeds then decrypt fails. 2021-06-22 11:25:18 -04:00
Michael Bucari-Tovo
344e675634 Hacked avformat-58.dll to ignore aac Scalable configurations flag. 2021-06-21 16:38:02 -06:00
Michael Bucari-Tovo
372e85d9af Merge branch 'master' of https://github.com/Mbucari/Libation 2021-06-21 16:29:51 -06:00
Michael Bucari-Tovo
c81788429b Revert "Hacked avformat-58 to ignore aac Scalable configurations error."
This reverts commit 26da307743.
2021-06-21 16:29:28 -06:00
Michael Bucari-Tovo
26da307743 Hacked avformat-58 to ignore aac Scalable configurations error. 2021-06-21 16:23:44 -06:00
Robert McRackan
0306c958d1 Remove filtering-out audible plus titles during library import. ie: they are now included 2021-06-21 15:10:40 -04:00
Robert McRackan
5ec6994da7 Remove outdated proof of concept demo programs 2021-06-21 14:24:54 -04:00
rmcrackan
c1f50a184a Merge pull request #27 from Mbucari/master
Add support for AXXC
2021-06-21 14:01:02 -04:00
Michael Bucari-Tovo
8231766d2c Fixed typo and removed unused method. 2021-06-21 10:07:30 -06:00
Michael Bucari-Tovo
eedc9bb34d Removed unused libraries and their references. 2021-06-21 10:04:48 -06:00
Michael Bucari-Tovo
310b90962c Download and decrypt AAXC files. Upgraded ffmpeg to 4.4-19. 2021-06-19 00:59:39 -06:00
Michael Bucari-Tovo
54c21e969e Added AAXC decryption keys to Book and created Migration. 2021-06-18 21:03:05 -06:00
106 changed files with 3158 additions and 2638 deletions

1
.gitignore vendored
View File

@@ -333,3 +333,4 @@ ASALocalRun/
# manually ignored files
/__TODO.txt
/DataLayer/LibationContext.db
*.lnk

View File

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

View File

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

View File

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

View 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();
}
}
}

View 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("&#169;", 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);
}
}
}

View File

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

View File

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

View File

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

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

@@ -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");
}
}
}

View 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;
}
}
}

View File

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

View 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;
}
}
}

View File

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

View 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();
}
}
}

View 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
}
}

View 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();
}
}

View File

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

View 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
}
}
}

View 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");
}
}
}

View 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
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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();
}
}
}

View File

@@ -1,5 +1,4 @@
using System;
using System.IO;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ApplicationServices;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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");
}
}
}

View File

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

View File

@@ -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");
}
}
}
}

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

Binary file not shown.

View File

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More