mirror of
https://github.com/rmcrackan/Libation.git
synced 2025-12-31 01:48:39 -05:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2f919d625 | ||
|
|
e821eea333 | ||
|
|
8f487894f5 | ||
|
|
cd3e0dba68 | ||
|
|
6f31d97763 | ||
|
|
fa5637a340 | ||
|
|
7ab209171b | ||
|
|
6d856f73e7 | ||
|
|
05426eb618 | ||
|
|
d73701c939 | ||
|
|
f284f53edd | ||
|
|
17f3187748 | ||
|
|
f55a41ac0a | ||
|
|
0be2a17537 | ||
|
|
b417c5695e | ||
|
|
6efe064ca7 | ||
|
|
da7af895fb | ||
|
|
1b39f30fd0 | ||
|
|
9cde6bddbd | ||
|
|
b21f257baa | ||
|
|
da68ddc9b8 | ||
|
|
9e15fde2e3 | ||
|
|
ef5b14a929 | ||
|
|
5df7d80aac | ||
|
|
4b2c8ee513 | ||
|
|
097bda2d25 | ||
|
|
81195e382e | ||
|
|
35fc3581b3 | ||
|
|
771d992da7 | ||
|
|
00f7e4b779 | ||
|
|
5d4bcb2db0 | ||
|
|
fbf92bf151 | ||
|
|
b9770220db |
@@ -32,6 +32,7 @@ namespace AaxDecrypter
|
||||
bool Step4_RestoreMetadata();
|
||||
bool Step5_CreateCue();
|
||||
bool Step6_CreateNfo();
|
||||
bool Step7_Cleanup();
|
||||
}
|
||||
public class AaxcDownloadConverter : IAdvancedAaxcToM4bConverter
|
||||
{
|
||||
@@ -41,6 +42,7 @@ namespace AaxDecrypter
|
||||
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; }
|
||||
@@ -49,26 +51,32 @@ namespace AaxDecrypter
|
||||
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 outDirectory, DownloadLicense dlLic)
|
||||
public static AaxcDownloadConverter Create(string cacheDirectory, string outDirectory, DownloadLicense dlLic)
|
||||
{
|
||||
var converter = new AaxcDownloadConverter(outDirectory, dlLic);
|
||||
var converter = new AaxcDownloadConverter(cacheDirectory, outDirectory, dlLic);
|
||||
converter.SetOutputFilename(Path.GetTempFileName());
|
||||
return converter;
|
||||
}
|
||||
|
||||
private AaxcDownloadConverter(string outDirectory, DownloadLicense dlLic)
|
||||
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 = "Convert Aax To M4b",
|
||||
Name = "Download and Convert Aaxc To M4b",
|
||||
|
||||
["Step 1: Create Dir"] = Step1_CreateDir,
|
||||
["Step 2: Get Aaxc Metadata"] = Step2_GetMetadata,
|
||||
@@ -76,6 +84,7 @@ namespace AaxDecrypter
|
||||
["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);
|
||||
@@ -128,11 +137,28 @@ namespace AaxDecrypter
|
||||
public bool Step2_GetMetadata()
|
||||
{
|
||||
//Get metadata from the file over http
|
||||
var client = new System.Net.Http.HttpClient();
|
||||
client.DefaultRequestHeaders.Add("User-Agent", downloadLicense.UserAgent);
|
||||
var networkFile = NetworkFileAbstraction.CreateAsync(client, new Uri(downloadLicense.DownloadUrl)).GetAwaiter().GetResult();
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -149,29 +175,50 @@ namespace AaxDecrypter
|
||||
{
|
||||
DecryptProgressUpdate?.Invoke(this, int.MaxValue);
|
||||
|
||||
bool userSuppliedChapters = downloadLicense.ChapterInfo != null;
|
||||
|
||||
string metadataPath = null;
|
||||
|
||||
if (userSuppliedChapters)
|
||||
NetworkFileStreamPersister nfsPersister;
|
||||
if (File.Exists(jsonDownloadState))
|
||||
{
|
||||
//Only write chaopters to the metadata file. All other aaxc metadata will be
|
||||
//wiped out but is restored in Step 3.
|
||||
metadataPath = Path.Combine(outDir, Path.GetFileName(outputFileName) + ".ffmeta");
|
||||
File.WriteAllText(metadataPath, downloadLicense.ChapterInfo.ToFFMeta(true));
|
||||
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();
|
||||
|
||||
if (!userSuppliedChapters && aaxcProcesser.Succeeded)
|
||||
downloadLicense.ChapterInfo = new ChapterInfo(outputFileName);
|
||||
nfsPersister.NetworkFileStream.Close();
|
||||
nfsPersister.Dispose();
|
||||
|
||||
if (userSuppliedChapters)
|
||||
FileExt.SafeDelete(metadataPath);
|
||||
FileExt.SafeDelete(metadataPath);
|
||||
|
||||
DecryptProgressUpdate?.Invoke(this, 0);
|
||||
|
||||
@@ -230,11 +277,18 @@ namespace AaxDecrypter
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, $"{nameof(Step5_CreateCue)}. FAILED");
|
||||
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;
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace AaxDecrypter
|
||||
public IEnumerable<Chapter> Chapters => _chapterList.AsEnumerable();
|
||||
public int Count => _chapterList.Count;
|
||||
public ChapterInfo() { }
|
||||
public ChapterInfo(string audiobookFile)
|
||||
public ChapterInfo(string audiobookFile)
|
||||
{
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
@@ -27,7 +27,7 @@ namespace AaxDecrypter
|
||||
var chapterJObject = JObject.Parse(jString);
|
||||
var chapters = chapterJObject["chapters"]
|
||||
.Select(c => new Chapter(
|
||||
c["tags"]?["title"]?.Value<string>(),
|
||||
c["tags"]?["title"]?.Value<string>(),
|
||||
c["start_time"].Value<double>(),
|
||||
c["end_time"].Value<double>()
|
||||
));
|
||||
@@ -44,7 +44,7 @@ namespace AaxDecrypter
|
||||
var ffmetaChapters = new StringBuilder();
|
||||
|
||||
if (includeFFMetaHeader)
|
||||
ffmetaChapters.AppendLine(";FFMETADATA1\n");
|
||||
ffmetaChapters.AppendLine(";FFMETADATA1");
|
||||
|
||||
foreach (var c in Chapters)
|
||||
{
|
||||
@@ -62,14 +62,15 @@ namespace AaxDecrypter
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrEmpty(title, nameof(title));
|
||||
ArgumentValidator.EnsureGreaterThan(startOffsetMs, nameof(startOffsetMs), -1);
|
||||
ArgumentValidator.EnsureGreaterThan(lengthMs, nameof(lengthMs), 0);
|
||||
|
||||
// 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);
|
||||
EndOffset = StartOffset + TimeSpan.FromMilliseconds(lengthMs);
|
||||
}
|
||||
public Chapter(string title, double startTimeSec, double endTimeSec)
|
||||
:this(title, (long)(startTimeSec * 1000), (long)((endTimeSec - startTimeSec) * 1000))
|
||||
: this(title, (long)(startTimeSec * 1000), (long)((endTimeSec - startTimeSec) * 1000))
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AaxDecrypter
|
||||
@@ -31,27 +32,30 @@ namespace AaxDecrypter
|
||||
public bool IsRunning { get; private set; }
|
||||
public bool Succeeded { get; private set; }
|
||||
public string FFMpegRemuxerStandardError => remuxerError.ToString();
|
||||
public string FFMpegDownloaderStandardError => downloaderError.ToString();
|
||||
public string FFMpegDecrypterStandardError => decrypterError.ToString();
|
||||
|
||||
|
||||
private StringBuilder remuxerError { get; } = new StringBuilder();
|
||||
private StringBuilder downloaderError { 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 downloader;
|
||||
private Process decrypter;
|
||||
private Process remuxer;
|
||||
private Stream inputFile;
|
||||
private bool isCanceled = false;
|
||||
|
||||
public FFMpegAaxcProcesser( DownloadLicense downloadLicense)
|
||||
public FFMpegAaxcProcesser(DownloadLicense downloadLicense)
|
||||
{
|
||||
FFMpegPath = DecryptSupportLibraries.ffmpegPath;
|
||||
DownloadLicense = downloadLicense;
|
||||
}
|
||||
|
||||
public async Task ProcessBook(string outputFile, string ffmetaChaptersPath = null)
|
||||
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
|
||||
downloader = new Process
|
||||
decrypter = new Process
|
||||
{
|
||||
StartInfo = getDownloaderStartInfo()
|
||||
};
|
||||
@@ -65,64 +69,81 @@ namespace AaxDecrypter
|
||||
|
||||
IsRunning = true;
|
||||
|
||||
downloader.ErrorDataReceived += Downloader_ErrorDataReceived;
|
||||
downloader.Start();
|
||||
downloader.BeginErrorReadLine();
|
||||
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
|
||||
//Thic check needs to be placed after remuxer has started.
|
||||
if (isCanceled) return;
|
||||
|
||||
var pipedOutput = downloader.StandardOutput.BaseStream;
|
||||
var pipedInput = remuxer.StandardInput.BaseStream;
|
||||
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(() =>
|
||||
{
|
||||
int lastRead = 0;
|
||||
byte[] buffer = new byte[32 * 1024];
|
||||
|
||||
do
|
||||
{
|
||||
lastRead = pipedOutput.Read(buffer, 0, buffer.Length);
|
||||
pipedInput.Write(buffer, 0, lastRead);
|
||||
} while (lastRead > 0 && !remuxer.HasExited);
|
||||
});
|
||||
|
||||
//Closing input stream terminates remuxer
|
||||
pipedInput.Close();
|
||||
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 && !downloader.HasExited)
|
||||
downloader.Kill();
|
||||
if (remuxer.HasExited && !decrypter.HasExited)
|
||||
decrypter.Kill();
|
||||
|
||||
remuxer.WaitForExit();
|
||||
downloader.WaitForExit();
|
||||
decrypter.WaitForExit();
|
||||
|
||||
IsRunning = false;
|
||||
Succeeded = downloader.ExitCode == 0 && remuxer.ExitCode == 0;
|
||||
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 && !downloader.HasExited)
|
||||
downloader.Kill();
|
||||
if (IsRunning && !decrypter.HasExited)
|
||||
decrypter.Kill();
|
||||
inputFile?.Close();
|
||||
}
|
||||
private void Downloader_ErrorDataReceived(object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrEmpty(e.Data))
|
||||
return;
|
||||
|
||||
downloaderError.AppendLine(e.Data);
|
||||
decrypterError.AppendLine(e.Data);
|
||||
}
|
||||
|
||||
private void Remuxer_ErrorDataReceived(object sender, DataReceivedEventArgs e)
|
||||
@@ -165,21 +186,21 @@ namespace AaxDecrypter
|
||||
{
|
||||
FileName = FFMpegPath,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = Path.GetDirectoryName(FFMpegPath),
|
||||
ArgumentList ={
|
||||
"-nostdin",
|
||||
"-audible_key",
|
||||
DownloadLicense.AudibleKey,
|
||||
"-audible_iv",
|
||||
DownloadLicense.AudibleIV,
|
||||
"-user_agent",
|
||||
DownloadLicense.UserAgent, //user-agent is requied for CDN to serve the file
|
||||
"-f",
|
||||
"mp4",
|
||||
"-i",
|
||||
DownloadLicense.DownloadUrl,
|
||||
"pipe:",
|
||||
"-c:a", //audio codec
|
||||
"copy", //copy stream
|
||||
"-f", //force output format: adts
|
||||
@@ -208,26 +229,15 @@ namespace AaxDecrypter
|
||||
startInfo.ArgumentList.Add("-i"); //read input from stdin
|
||||
startInfo.ArgumentList.Add("pipe:");
|
||||
|
||||
if (ffmetaChaptersPath is null)
|
||||
{
|
||||
//copy metadata from aaxc file.
|
||||
startInfo.ArgumentList.Add("-user_agent");
|
||||
startInfo.ArgumentList.Add(DownloadLicense.UserAgent);
|
||||
startInfo.ArgumentList.Add("-i");
|
||||
startInfo.ArgumentList.Add(DownloadLicense.DownloadUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
//copy metadata from supplied metadata file
|
||||
startInfo.ArgumentList.Add("-f");
|
||||
startInfo.ArgumentList.Add("ffmetadata");
|
||||
startInfo.ArgumentList.Add("-i");
|
||||
startInfo.ArgumentList.Add(ffmetaChaptersPath);
|
||||
}
|
||||
//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 1 (either metadata file or aaxc file)
|
||||
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");
|
||||
|
||||
@@ -12,26 +12,10 @@ namespace AaxDecrypter
|
||||
{
|
||||
private NetworkFileStream aaxNetworkStream;
|
||||
|
||||
public static async Task<NetworkFileAbstraction> CreateAsync(HttpClient client, Uri webFileUri)
|
||||
public NetworkFileAbstraction( NetworkFileStream networkFileStream)
|
||||
{
|
||||
var response = await client.GetAsync(webFileUri, HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
if (response.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
throw new Exception("Can't read file from client.");
|
||||
|
||||
var contentLength = response.Content.Headers.ContentLength ?? 0;
|
||||
|
||||
var networkStream = await response.Content.ReadAsStreamAsync();
|
||||
|
||||
var networkFile = new NetworkFileAbstraction(Path.GetFileName(webFileUri.LocalPath), networkStream, contentLength);
|
||||
|
||||
return networkFile;
|
||||
}
|
||||
|
||||
private NetworkFileAbstraction(string fileName, Stream netStream, long contentLength)
|
||||
{
|
||||
Name = fileName;
|
||||
aaxNetworkStream = new NetworkFileStream(netStream, contentLength);
|
||||
Name = networkFileStream.SaveFilePath;
|
||||
aaxNetworkStream = networkFileStream;
|
||||
}
|
||||
public string Name { get; private set; }
|
||||
|
||||
@@ -43,93 +27,5 @@ namespace AaxDecrypter
|
||||
{
|
||||
aaxNetworkStream.Close();
|
||||
}
|
||||
|
||||
private class NetworkFileStream : Stream
|
||||
{
|
||||
private const int BUFF_SZ = 2 * 1024;
|
||||
|
||||
private FileStream _fileBacker;
|
||||
|
||||
private Stream _networkStream;
|
||||
|
||||
private long networkBytesRead = 0;
|
||||
|
||||
private long _contentLength;
|
||||
public NetworkFileStream(Stream netStream, long contentLength)
|
||||
{
|
||||
_networkStream = netStream;
|
||||
_contentLength = contentLength;
|
||||
_fileBacker = File.Create(Path.GetTempFileName(), BUFF_SZ, FileOptions.DeleteOnClose);
|
||||
}
|
||||
public override bool CanRead => true;
|
||||
|
||||
public override bool CanSeek => true;
|
||||
|
||||
public override bool CanWrite => false;
|
||||
|
||||
public override long Length => _contentLength;
|
||||
|
||||
public override long Position { get => _fileBacker.Position; set => Seek(value, 0); }
|
||||
|
||||
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)
|
||||
{
|
||||
long requiredLength = Position + count;
|
||||
|
||||
if (requiredLength > networkBytesRead)
|
||||
readWebFileToPosition(requiredLength);
|
||||
|
||||
return _fileBacker.Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
long newPosition = (long)origin + offset;
|
||||
|
||||
if (newPosition > networkBytesRead)
|
||||
readWebFileToPosition(newPosition);
|
||||
|
||||
_fileBacker.Position = newPosition;
|
||||
return newPosition;
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
_fileBacker.Close();
|
||||
_networkStream.Close();
|
||||
}
|
||||
/// <summary>
|
||||
/// Read more data from <see cref="_networkStream"/> into <see cref="_fileBacker"/> as needed.
|
||||
/// </summary>
|
||||
/// <param name="requiredLength">Length of strem required for the operation.</param>
|
||||
private void readWebFileToPosition(long requiredLength)
|
||||
{
|
||||
byte[] buff = new byte[BUFF_SZ];
|
||||
|
||||
long backerPosition = _fileBacker.Position;
|
||||
|
||||
_fileBacker.Position = networkBytesRead;
|
||||
|
||||
while (networkBytesRead < requiredLength)
|
||||
{
|
||||
int bytesRead = _networkStream.Read(buff, 0, BUFF_SZ);
|
||||
_fileBacker.Write(buff, 0, bytesRead);
|
||||
networkBytesRead += bytesRead;
|
||||
}
|
||||
|
||||
_fileBacker.Position = backerPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
455
AaxDecrypter/NetworkFileStream.cs
Normal file
455
AaxDecrypter/NetworkFileStream.cs
Normal file
@@ -0,0 +1,455 @@
|
||||
using Dinah.Core;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="CookieContainer"/> for a single Uri.
|
||||
/// </summary>
|
||||
public class SingleUriCookieContainer : CookieContainer
|
||||
{
|
||||
private Uri baseAddress;
|
||||
public Uri Uri
|
||||
{
|
||||
get => baseAddress;
|
||||
set
|
||||
{
|
||||
baseAddress = new UriBuilder(value.Scheme, value.Host).Uri;
|
||||
}
|
||||
}
|
||||
|
||||
public CookieCollection GetCookies()
|
||||
{
|
||||
return base.GetCookies(Uri);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A resumable, simultaneous file downloader and reader.
|
||||
/// </summary>
|
||||
public class NetworkFileStream : Stream, IUpdatable
|
||||
{
|
||||
public event EventHandler Updated;
|
||||
|
||||
#region Public Properties
|
||||
|
||||
/// <summary>
|
||||
/// Location to save the downloaded data.
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public string SaveFilePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Http(s) address of the file to download.
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public Uri Uri { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// All cookies set by caller or by the remote server.
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public SingleUriCookieContainer CookieContainer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Http headers to be sent to the server with the request.
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public WebHeaderCollection RequestHeaders { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The position in <see cref="SaveFilePath"/> that has been written and flushed to disk.
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public long WritePosition { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total length of the <see cref="Uri"/> file to download.
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public long ContentLength { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Properties
|
||||
|
||||
private HttpWebRequest HttpRequest { get; set; }
|
||||
private FileStream _writeFile { get; }
|
||||
private FileStream _readFile { get; }
|
||||
private Stream _networkStream { get; set; }
|
||||
private bool hasBegunDownloading { get; set; }
|
||||
private bool isCancelled { get; set; }
|
||||
private bool finishedDownloading { get; set; }
|
||||
private Action downloadThreadCompleteCallback { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constants
|
||||
|
||||
//Download buffer size
|
||||
private const int DOWNLOAD_BUFF_SZ = 4 * 1024;
|
||||
|
||||
//NetworkFileStream will flush all data in _writeFile to disk after every
|
||||
//DATA_FLUSH_SZ bytes are written to the file stream.
|
||||
private const int DATA_FLUSH_SZ = 1024 * 1024;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
/// <summary>
|
||||
/// A resumable, simultaneous file downloader and reader.
|
||||
/// </summary>
|
||||
/// <param name="saveFilePath">Path to a location on disk to save the downloaded data from <paramref name="uri"/></param>
|
||||
/// <param name="uri">Http(s) address of the file to download.</param>
|
||||
/// <param name="writePosition">The position in <paramref name="uri"/> to begin downloading.</param>
|
||||
/// <param name="requestHeaders">Http headers to be sent to the server with the <see cref="HttpWebRequest"/>.</param>
|
||||
/// <param name="cookies">A <see cref="SingleUriCookieContainer"/> with cookies to send with the <see cref="HttpWebRequest"/>. It will also be populated with any cookies set by the server. </param>
|
||||
public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, WebHeaderCollection requestHeaders = null, SingleUriCookieContainer cookies = null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath));
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri));
|
||||
ArgumentValidator.EnsureGreaterThan(writePosition, nameof(writePosition), -1);
|
||||
|
||||
if (!Directory.Exists(Path.GetDirectoryName(saveFilePath)))
|
||||
throw new ArgumentException($"Specified {nameof(saveFilePath)} directory \"{Path.GetDirectoryName(saveFilePath)}\" does not exist.");
|
||||
|
||||
SaveFilePath = saveFilePath;
|
||||
Uri = uri;
|
||||
WritePosition = writePosition;
|
||||
RequestHeaders = requestHeaders ?? new WebHeaderCollection();
|
||||
CookieContainer = cookies ?? new SingleUriCookieContainer { Uri = uri };
|
||||
|
||||
_writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)
|
||||
{
|
||||
Position = WritePosition
|
||||
};
|
||||
|
||||
_readFile = new FileStream(SaveFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
|
||||
SetUriForSameFile(uri);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Downloader
|
||||
|
||||
/// <summary>
|
||||
/// Update the <see cref="JsonFilePersister"/>.
|
||||
/// </summary>
|
||||
private void Update()
|
||||
{
|
||||
RequestHeaders = HttpRequest.Headers;
|
||||
Updated?.Invoke(this, new EventArgs());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a different <see cref="System.Uri"/> to the same file targeted by this instance of <see cref="NetworkFileStream"/>
|
||||
/// </summary>
|
||||
/// <param name="uriToSameFile">New <see cref="System.Uri"/> host must match existing host.</param>
|
||||
public void SetUriForSameFile(Uri uriToSameFile)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(uriToSameFile?.AbsoluteUri, nameof(uriToSameFile));
|
||||
|
||||
if (uriToSameFile.Host != Uri.Host)
|
||||
throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}");
|
||||
if (hasBegunDownloading && !finishedDownloading)
|
||||
throw new Exception("Cannot change Uri during a download operation.");
|
||||
|
||||
Uri = uriToSameFile;
|
||||
HttpRequest = WebRequest.CreateHttp(Uri);
|
||||
|
||||
HttpRequest.CookieContainer = CookieContainer;
|
||||
HttpRequest.Headers = RequestHeaders;
|
||||
//If NetworkFileStream is resuming, Header will already contain a range.
|
||||
HttpRequest.Headers.Remove("Range");
|
||||
HttpRequest.AddRange(WritePosition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread.
|
||||
/// </summary>
|
||||
private void BeginDownloading()
|
||||
{
|
||||
if (ContentLength != 0 && WritePosition == ContentLength)
|
||||
{
|
||||
hasBegunDownloading = true;
|
||||
finishedDownloading = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ContentLength != 0 && WritePosition > ContentLength)
|
||||
throw new Exception($"Specified write position (0x{WritePosition:X10}) is larger than the file size.");
|
||||
|
||||
var response = HttpRequest.GetResponse() as HttpWebResponse;
|
||||
|
||||
if (response.StatusCode != HttpStatusCode.PartialContent)
|
||||
throw new Exception($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}.");
|
||||
|
||||
if (response.Headers.GetValues("Accept-Ranges").FirstOrDefault(r => r.EqualsInsensitive("bytes")) is null)
|
||||
throw new Exception($"Server at {Uri.Host} does not support Http ranges");
|
||||
|
||||
//Content length is the length of the range request, and it is only equal
|
||||
//to the complete file length if requesting Range: bytes=0-
|
||||
if (WritePosition == 0)
|
||||
ContentLength = response.ContentLength;
|
||||
|
||||
_networkStream = response.GetResponseStream();
|
||||
|
||||
//Download the file in the background.
|
||||
Thread downloadThread = new Thread(() => DownloadFile());
|
||||
downloadThread.Start();
|
||||
|
||||
hasBegunDownloading = true;
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downlod <see cref="Uri"/> to <see cref="SaveFilePath"/>.
|
||||
/// </summary>
|
||||
private void DownloadFile()
|
||||
{
|
||||
long downloadPosition = WritePosition;
|
||||
long nextFlush = downloadPosition + DATA_FLUSH_SZ;
|
||||
|
||||
byte[] buff = new byte[DOWNLOAD_BUFF_SZ];
|
||||
do
|
||||
{
|
||||
int bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
|
||||
_writeFile.Write(buff, 0, bytesRead);
|
||||
|
||||
downloadPosition += bytesRead;
|
||||
|
||||
if (downloadPosition > nextFlush)
|
||||
{
|
||||
_writeFile.Flush();
|
||||
WritePosition = downloadPosition;
|
||||
Update();
|
||||
nextFlush = downloadPosition + DATA_FLUSH_SZ;
|
||||
}
|
||||
|
||||
} while (downloadPosition < ContentLength && !isCancelled);
|
||||
|
||||
_writeFile.Close();
|
||||
WritePosition = downloadPosition;
|
||||
Update();
|
||||
_networkStream.Close();
|
||||
|
||||
if (!isCancelled && WritePosition < ContentLength)
|
||||
throw new Exception("File download ended before finishing.");
|
||||
|
||||
if (WritePosition > ContentLength)
|
||||
throw new Exception("Downloaded file is larger than expected.");
|
||||
|
||||
finishedDownloading = true;
|
||||
downloadThreadCompleteCallback?.Invoke();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Json Connverters
|
||||
|
||||
public static JsonSerializerSettings GetJsonSerializerSettings()
|
||||
{
|
||||
var settings = new JsonSerializerSettings();
|
||||
settings.Converters.Add(new CookieContainerConverter());
|
||||
settings.Converters.Add(new WebHeaderCollectionConverter());
|
||||
return settings;
|
||||
}
|
||||
|
||||
internal class CookieContainerConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
=> objectType == typeof(SingleUriCookieContainer);
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var jObj = JObject.Load(reader);
|
||||
|
||||
var result = new SingleUriCookieContainer()
|
||||
{
|
||||
Uri = new Uri(jObj["Uri"].Value<string>()),
|
||||
Capacity = jObj["Capacity"].Value<int>(),
|
||||
MaxCookieSize = jObj["MaxCookieSize"].Value<int>(),
|
||||
PerDomainCapacity = jObj["PerDomainCapacity"].Value<int>()
|
||||
};
|
||||
|
||||
var cookieList = jObj["Cookies"].ToList();
|
||||
|
||||
foreach (var cookie in cookieList)
|
||||
{
|
||||
result.Add(
|
||||
new Cookie
|
||||
{
|
||||
Comment = cookie["Comment"].Value<string>(),
|
||||
HttpOnly = cookie["HttpOnly"].Value<bool>(),
|
||||
Discard = cookie["Discard"].Value<bool>(),
|
||||
Domain = cookie["Domain"].Value<string>(),
|
||||
Expired = cookie["Expired"].Value<bool>(),
|
||||
Expires = cookie["Expires"].Value<DateTime>(),
|
||||
Name = cookie["Name"].Value<string>(),
|
||||
Path = cookie["Path"].Value<string>(),
|
||||
Port = cookie["Port"].Value<string>(),
|
||||
Secure = cookie["Secure"].Value<bool>(),
|
||||
Value = cookie["Value"].Value<string>(),
|
||||
Version = cookie["Version"].Value<int>(),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
var cookies = value as SingleUriCookieContainer;
|
||||
var obj = (JObject)JToken.FromObject(value);
|
||||
var container = cookies.GetCookies();
|
||||
var propertyNames = container.Select(c => JToken.FromObject(c));
|
||||
obj.AddFirst(new JProperty("Cookies", new JArray(propertyNames)));
|
||||
obj.WriteTo(writer);
|
||||
}
|
||||
}
|
||||
|
||||
internal class WebHeaderCollectionConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
=> objectType == typeof(WebHeaderCollection);
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var jObj = JObject.Load(reader);
|
||||
var result = new WebHeaderCollection();
|
||||
|
||||
foreach (var kvp in jObj)
|
||||
{
|
||||
result.Add(kvp.Key, kvp.Value.Value<string>());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
JObject jObj = new JObject();
|
||||
Type type = value.GetType();
|
||||
var headers = value as WebHeaderCollection;
|
||||
var jHeaders = headers.AllKeys.Select(k => new JProperty(k, headers[k]));
|
||||
jObj.Add(jHeaders);
|
||||
jObj.WriteTo(writer);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Download Stream Reader
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool CanRead => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool CanSeek => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool CanWrite => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override long Length => ContentLength;
|
||||
|
||||
[JsonIgnore]
|
||||
public override long Position { get => _readFile.Position; set => Seek(value, SeekOrigin.Begin); }
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool CanTimeout => base.CanTimeout;
|
||||
|
||||
[JsonIgnore]
|
||||
public override int ReadTimeout { get => base.ReadTimeout; set => base.ReadTimeout = value; }
|
||||
|
||||
[JsonIgnore]
|
||||
public override int WriteTimeout { get => base.WriteTimeout; set => base.WriteTimeout = value; }
|
||||
|
||||
public override void Flush() => throw new NotImplementedException();
|
||||
public override void SetLength(long value) => throw new NotImplementedException();
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (!hasBegunDownloading)
|
||||
BeginDownloading();
|
||||
|
||||
long toRead = Math.Min(count, Length - Position);
|
||||
long requiredPosition = Position + toRead;
|
||||
|
||||
//read operation will block until file contains enough data
|
||||
//to fulfil the request, or until cancelled.
|
||||
while (requiredPosition > WritePosition && !isCancelled)
|
||||
Thread.Sleep(0);
|
||||
|
||||
return _readFile.Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
long newPosition;
|
||||
|
||||
switch (origin)
|
||||
{
|
||||
case SeekOrigin.Current:
|
||||
newPosition = Position + offset;
|
||||
break;
|
||||
case SeekOrigin.End:
|
||||
newPosition = ContentLength + offset;
|
||||
break;
|
||||
default:
|
||||
newPosition = offset;
|
||||
break;
|
||||
}
|
||||
ReadToPosition(newPosition);
|
||||
|
||||
_readFile.Position = newPosition;
|
||||
return newPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the file has downloaded to at least <paramref name="neededPosition"/>, then returns.
|
||||
/// </summary>
|
||||
/// <param name="neededPosition">The minimum required data length in <see cref="SaveFilePath"/>.</param>
|
||||
private void ReadToPosition(long neededPosition)
|
||||
{
|
||||
byte[] buff = new byte[DOWNLOAD_BUFF_SZ];
|
||||
do
|
||||
{
|
||||
Read(buff, 0, DOWNLOAD_BUFF_SZ);
|
||||
} while (neededPosition > WritePosition);
|
||||
}
|
||||
public override void Close()
|
||||
{
|
||||
isCancelled = true;
|
||||
downloadThreadCompleteCallback = CloseAction;
|
||||
|
||||
//ensure that close will run even if called after callback was fired.
|
||||
if (finishedDownloading)
|
||||
CloseAction();
|
||||
|
||||
}
|
||||
private void CloseAction()
|
||||
{
|
||||
_readFile.Close();
|
||||
_writeFile.Close();
|
||||
_networkStream?.Close();
|
||||
Update();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
23
AaxDecrypter/NetworkFileStreamPersister.cs
Normal file
23
AaxDecrypter/NetworkFileStreamPersister.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Dinah.Core.IO;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
internal class NetworkFileStreamPersister : JsonFilePersister<NetworkFileStream>
|
||||
{
|
||||
|
||||
/// <summary>Alias for Target </summary>
|
||||
public NetworkFileStream NetworkFileStream => Target;
|
||||
|
||||
/// <summary>uses path. create file if doesn't yet exist</summary>
|
||||
public NetworkFileStreamPersister(NetworkFileStream networkFileStream, string path, string jsonPath = null)
|
||||
: base(networkFileStream, path, jsonPath) { }
|
||||
|
||||
/// <summary>load from existing file</summary>
|
||||
public NetworkFileStreamPersister(string path, string jsonPath = null)
|
||||
: base(path, jsonPath) { }
|
||||
|
||||
protected override JsonSerializerSettings GetSerializerSettings() => NetworkFileStream.GetJsonSerializerSettings();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace FileLiberator
|
||||
if (AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId))
|
||||
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
||||
|
||||
var outputAudioFilename = await aaxToM4bConverterDecryptAsync(AudibleFileStorage.DecryptInProgress, libraryBook);
|
||||
var outputAudioFilename = await aaxToM4bConverterDecryptAsync(AudibleFileStorage.DownloadsInProgress, AudibleFileStorage.DecryptInProgress, libraryBook);
|
||||
|
||||
// decrypt failed
|
||||
if (outputAudioFilename is null)
|
||||
@@ -59,7 +59,7 @@ namespace FileLiberator
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> aaxToM4bConverterDecryptAsync(string destinationDir, LibraryBook libraryBook)
|
||||
private async Task<string> aaxToM4bConverterDecryptAsync(string cacheDir, string destinationDir, LibraryBook libraryBook)
|
||||
{
|
||||
DecryptBegin?.Invoke(this, $"Begin decrypting {libraryBook}");
|
||||
|
||||
@@ -92,7 +92,7 @@ namespace FileLiberator
|
||||
));
|
||||
}
|
||||
|
||||
aaxcDownloader = AaxcDownloadConverter.Create(destinationDir, aaxcDecryptDlLic);
|
||||
aaxcDownloader = AaxcDownloadConverter.Create(cacheDir, destinationDir, aaxcDecryptDlLic);
|
||||
|
||||
aaxcDownloader.AppName = "Libation";
|
||||
|
||||
@@ -213,12 +213,11 @@ namespace FileLiberator
|
||||
}
|
||||
|
||||
public bool Validate(LibraryBook libraryBook)
|
||||
=> !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId)
|
||||
&& !AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId);
|
||||
=> !AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
aaxcDownloader.Cancel();
|
||||
aaxcDownloader?.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<!-- <PublishSingleFile>true</PublishSingleFile> -->
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
|
||||
<Version>5.0.0.5</Version>
|
||||
<Version>5.1.2.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,9 +69,13 @@ 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)
|
||||
@@ -93,10 +105,9 @@ 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
|
||||
|
||||
@@ -121,7 +132,7 @@ 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.DecryptBook.Begin -= decryptBookBegin;
|
||||
backupBook.DecryptBook.StatusUpdate -= statusUpdate;
|
||||
@@ -132,7 +143,7 @@ namespace LibationWinForms.BookLiberation
|
||||
};
|
||||
#endregion
|
||||
|
||||
return (automatedBackupsForm, logMe);
|
||||
return (unsubscribe, logMe);
|
||||
}
|
||||
|
||||
public static async Task BackupAllPdfsAsync(EventHandler<LibraryBook> completedAction = null)
|
||||
@@ -367,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;
|
||||
@@ -382,7 +393,7 @@ namespace LibationWinForms.BookLiberation
|
||||
|
||||
public async Task RunBackupAsync()
|
||||
{
|
||||
AutomatedBackupsForm.Show();
|
||||
AutomatedBackupsForm?.Show();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -393,7 +404,7 @@ namespace LibationWinForms.BookLiberation
|
||||
LogMe.Error(ex);
|
||||
}
|
||||
|
||||
AutomatedBackupsForm.FinalizeUI();
|
||||
AutomatedBackupsForm?.FinalizeUI();
|
||||
LogMe.Info("DONE");
|
||||
}
|
||||
|
||||
@@ -454,8 +465,8 @@ An error occurred while trying to process this book. Skip this book permanently?
|
||||
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;
|
||||
}
|
||||
@@ -492,6 +503,9 @@ An error occurred while trying to process this book
|
||||
if (!keepGoing)
|
||||
return;
|
||||
|
||||
if (AutomatedBackupsForm.IsDisposed)
|
||||
break;
|
||||
|
||||
if (!AutomatedBackupsForm.KeepGoing)
|
||||
{
|
||||
if (AutomatedBackupsForm.KeepGoingVisible && !AutomatedBackupsForm.KeepGoingChecked)
|
||||
|
||||
@@ -34,9 +34,11 @@
|
||||
//
|
||||
// approvedBtn
|
||||
//
|
||||
this.approvedBtn.Location = new System.Drawing.Point(15, 25);
|
||||
this.approvedBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.approvedBtn.Location = new System.Drawing.Point(18, 75);
|
||||
this.approvedBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.approvedBtn.Name = "approvedBtn";
|
||||
this.approvedBtn.Size = new System.Drawing.Size(79, 23);
|
||||
this.approvedBtn.Size = new System.Drawing.Size(92, 27);
|
||||
this.approvedBtn.TabIndex = 1;
|
||||
this.approvedBtn.Text = "Approved";
|
||||
this.approvedBtn.UseVisualStyleBackColor = true;
|
||||
@@ -45,27 +47,30 @@
|
||||
// label1
|
||||
//
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Location = new System.Drawing.Point(12, 9);
|
||||
this.label1.Location = new System.Drawing.Point(14, 10);
|
||||
this.label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(104, 13);
|
||||
this.label1.Size = new System.Drawing.Size(314, 45);
|
||||
this.label1.TabIndex = 0;
|
||||
this.label1.Text = "Click after approving";
|
||||
this.label1.Text = "Amazon is sending you an email.\r\n\r\nPlease press this button after you approve the" +
|
||||
" notification.";
|
||||
//
|
||||
// ApprovalNeededDialog
|
||||
//
|
||||
this.AcceptButton = this.approvedBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(149, 60);
|
||||
this.ClientSize = new System.Drawing.Size(345, 115);
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.approvedBtn);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "ApprovalNeededDialog";
|
||||
this.ShowIcon = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Approval Needed";
|
||||
this.Text = "Approval Alert Detected";
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
|
||||
60
LibationWinForms/Dialogs/Login/ApprovalNeededDialog.resx
Normal file
60
LibationWinForms/Dialogs/Login/ApprovalNeededDialog.resx
Normal file
@@ -0,0 +1,60 @@
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -117,7 +117,7 @@ namespace LibationWinForms
|
||||
{
|
||||
if (AudibleFileStorage.Audio.Exists(productId))
|
||||
return AudioFileState.full;
|
||||
if (AudibleFileStorage.AAX.Exists(productId))
|
||||
if (AudibleFileStorage.AAXC.Exists(productId))
|
||||
return AudioFileState.aax;
|
||||
return AudioFileState.none;
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ namespace LibationWinForms
|
||||
[Browsable(false)]
|
||||
public IEnumerable<string> TagsEnumerated => book.UserDefinedItem.TagsEnumerated;
|
||||
|
||||
public enum LiberatedState { NotDownloaded, DRM, Liberated }
|
||||
public enum LiberatedState { NotDownloaded, PartialDownload, Liberated }
|
||||
[Browsable(false)]
|
||||
public LiberatedState Liberated_Status
|
||||
=> FileManager.AudibleFileStorage.Audio.Exists(book.AudibleProductId) ? LiberatedState.Liberated
|
||||
: FileManager.AudibleFileStorage.AAX.Exists(book.AudibleProductId) ? LiberatedState.DRM
|
||||
: FileManager.AudibleFileStorage.AAXC.Exists(book.AudibleProductId) ? LiberatedState.PartialDownload
|
||||
: LiberatedState.NotDownloaded;
|
||||
|
||||
public enum PdfState { NoPdf, Downloaded, NotDownloaded }
|
||||
|
||||
@@ -126,7 +126,7 @@ namespace LibationWinForms
|
||||
var libState = liberatedStatus switch
|
||||
{
|
||||
GridEntry.LiberatedState.Liberated => "Liberated",
|
||||
GridEntry.LiberatedState.DRM => "Downloaded but needs DRM removed",
|
||||
GridEntry.LiberatedState.PartialDownload => "File has been at least\r\npartially downloaded",
|
||||
GridEntry.LiberatedState.NotDownloaded => "Book NOT downloaded",
|
||||
_ => throw new Exception("Unexpected liberation state")
|
||||
};
|
||||
@@ -142,7 +142,7 @@ namespace LibationWinForms
|
||||
var text = libState + pdfState;
|
||||
|
||||
if (liberatedStatus == GridEntry.LiberatedState.NotDownloaded ||
|
||||
liberatedStatus == GridEntry.LiberatedState.DRM ||
|
||||
liberatedStatus == GridEntry.LiberatedState.PartialDownload ||
|
||||
pdfStatus == GridEntry.PdfState.NotDownloaded)
|
||||
text += "\r\nClick to complete";
|
||||
|
||||
@@ -154,7 +154,7 @@ namespace LibationWinForms
|
||||
{
|
||||
var image_lib
|
||||
= liberatedStatus == GridEntry.LiberatedState.NotDownloaded ? "red"
|
||||
: liberatedStatus == GridEntry.LiberatedState.DRM ? "yellow"
|
||||
: liberatedStatus == GridEntry.LiberatedState.PartialDownload ? "yellow"
|
||||
: liberatedStatus == GridEntry.LiberatedState.Liberated ? "green"
|
||||
: throw new Exception("Unexpected liberation state");
|
||||
var image_pdf
|
||||
@@ -182,15 +182,7 @@ namespace LibationWinForms
|
||||
return;
|
||||
}
|
||||
|
||||
// not liberated: liberate
|
||||
var msg
|
||||
= "Liberate entire library instead?"
|
||||
+ "\r\n\r\nClick Yes to begin liberating your entire library"
|
||||
+ "\r\n\r\nClick No to liberate this book only";
|
||||
if (MessageBox.Show(msg, "Liberate entire library?", MessageBoxButtons.YesNo) == DialogResult.Yes)
|
||||
await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync((_, libraryBook) => RefreshRow(libraryBook.Book.AudibleProductId));
|
||||
else
|
||||
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(productId, (_, __) => RefreshRow(productId));
|
||||
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(productId, (_, __) => RefreshRow(productId));
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
//
|
||||
// approvedBtn
|
||||
//
|
||||
this.approvedBtn.Location = new System.Drawing.Point(15, 25);
|
||||
this.approvedBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.approvedBtn.Location = new System.Drawing.Point(18, 75);
|
||||
this.approvedBtn.Name = "approvedBtn";
|
||||
this.approvedBtn.Size = new System.Drawing.Size(79, 23);
|
||||
this.approvedBtn.TabIndex = 1;
|
||||
@@ -46,16 +47,17 @@
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Location = new System.Drawing.Point(12, 9);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(104, 13);
|
||||
this.label1.Size = new System.Drawing.Size(280, 39);
|
||||
this.label1.TabIndex = 0;
|
||||
this.label1.Text = "Click after approving";
|
||||
this.label1.Text = "Amazon is sending you an email.\r\n\r\nPlease press this button after you approve the" +
|
||||
" notification.";
|
||||
//
|
||||
// ApprovalNeededDialog
|
||||
//
|
||||
this.AcceptButton = this.approvedBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(149, 60);
|
||||
this.ClientSize = new System.Drawing.Size(345, 115);
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.approvedBtn);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
@@ -64,7 +66,7 @@
|
||||
this.Name = "ApprovalNeededDialog";
|
||||
this.ShowIcon = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Approval Needed";
|
||||
this.Text = "Approval Alert Detected";
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
|
||||
120
WinFormsDesigner/Dialogs/Login/ApprovalNeededDialog.resx
Normal file
120
WinFormsDesigner/Dialogs/Login/ApprovalNeededDialog.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -182,6 +182,9 @@
|
||||
<EmbeddedResource Include="Dialogs\LibationFilesDialog.resx">
|
||||
<DependentUpon>LibationFilesDialog.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Dialogs\Login\ApprovalNeededDialog.resx">
|
||||
<DependentUpon>ApprovalNeededDialog.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Dialogs\ScanAccountsDialog.resx">
|
||||
<DependentUpon>ScanAccountsDialog.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
|
||||
Reference in New Issue
Block a user