Completely redesigned donload and decrypt.

This commit is contained in:
Michael Bucari-Tovo 2021-06-24 16:48:29 -06:00
parent d619c82fd8
commit 1e88070f3a
19 changed files with 937 additions and 2 deletions

View File

@ -0,0 +1,199 @@
using AaxDecrypter;
using AudibleApi;
using AudibleApiDTOs;
using Dinah.Core;
using Dinah.Core.Diagnostics;
using Dinah.Core.IO;
using Dinah.Core.StepRunner;
using System;
using System.IO;
using System.Threading.Tasks;
namespace FileLiberator.AaxcDownloadDecrypt
{
public interface ISimpleAaxToM4bConverter2
{
event EventHandler<int> DecryptProgressUpdate;
bool Run();
string AppName { get; set; }
string outDir { get; }
string outputFileName { get; }
ChapterInfo chapters { get; }
Tags tags { get; }
void SetOutputFilename(string outFileName);
}
public interface IAdvancedAaxcToM4bConverter : ISimpleAaxToM4bConverter2
{
bool Step1_CreateDir();
bool Step2_DownloadAndCombine();
bool Step3_CreateCue();
bool Step4_CreateNfo();
bool Step5_Cleanup();
}
class AaxcDownloadConverter : IAdvancedAaxcToM4bConverter
{
public string AppName { get; set; } = nameof(AaxcDownloadConverter);
public string outDir { get; private set; }
public string outputFileName { get; private set; }
public ChapterInfo chapters { get; private set; }
public Tags tags { get; private set; }
public event EventHandler<int> DecryptProgressUpdate;
private StepSequence steps { get; }
private DownloadLicense downloadLicense { get; set; }
private string coverArtPath => Path.Combine(outDir, Path.GetFileName(outputFileName) + ".jpg");
private string metadataPath => Path.Combine(outDir, Path.GetFileName(outputFileName) + ".ffmeta");
public static async Task<AaxcDownloadConverter> CreateAsync(string outDirectory, DownloadLicense dlLic, ChapterInfo chapters)
{
var converter = new AaxcDownloadConverter(outDirectory, dlLic, chapters);
await converter.prelimProcessing();
return converter;
}
private AaxcDownloadConverter(string outDirectory, DownloadLicense dlLic, ChapterInfo chapters)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(outDirectory, nameof(outDirectory));
ArgumentValidator.EnsureNotNull(dlLic, nameof(dlLic));
ArgumentValidator.EnsureNotNull(chapters, nameof(chapters));
if (!Directory.Exists(outDirectory))
throw new ArgumentNullException(nameof(outDirectory), "Directory does not exist");
outDir = outDirectory;
steps = new StepSequence
{
Name = "Convert Aax To M4b",
["Step 1: Create Dir"] = Step1_CreateDir,
["Step 2: Download and Combine Audiobook"] = Step2_DownloadAndCombine,
["Step 3 Create Cue"] = Step3_CreateCue,
["Step 4 Create Nfo"] = Step4_CreateNfo,
["Step 5: Cleanup"] = Step5_Cleanup,
};
downloadLicense = dlLic;
this.chapters = chapters;
}
private async Task prelimProcessing()
{
//Get metadata from the file over http
var client = new System.Net.Http.HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", Resources.UserAgent);
var networkFile = await NetworkFileAbstraction.CreateAsync(client, new Uri(downloadLicense.DownloadUrl));
var tagLibFile = await Task.Run(()=>TagLib.File.Create(networkFile, "audio/mp4", TagLib.ReadStyle.Average));
tags = new Tags(tagLibFile);
var defaultFilename = Path.Combine(
outDir,
PathLib.ToPathSafeString(tags.author),
PathLib.ToPathSafeString(tags.title) + ".m4b"
);
SetOutputFilename(defaultFilename);
}
public void SetOutputFilename(string outFileName)
{
outputFileName = PathLib.ReplaceExtension(outFileName, ".m4b");
outDir = Path.GetDirectoryName(outputFileName);
if (File.Exists(outputFileName))
File.Delete(outputFileName);
}
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 bool Step1_CreateDir()
{
ProcessRunner.WorkingDir = outDir;
Directory.CreateDirectory(outDir);
return true;
}
public bool Step2_DownloadAndCombine()
{
File.WriteAllBytes(coverArtPath, tags.coverArt);
var ffmpegTags = tags.GenerateFfmpegTags();
var ffmpegChapters = GenerateFfmpegChapters(chapters);
File.WriteAllText(metadataPath, ffmpegTags + ffmpegChapters);
var aaxcProcesser = new FFMpegAaaxcProcesser(DecryptSupportLibraries.ffmpegPath);
aaxcProcesser.ProgressUpdate += (_, e) =>
DecryptProgressUpdate?.Invoke(this, (int)e.ProgressPercent);
aaxcProcesser.ProcessBook(
downloadLicense.DownloadUrl,
Resources.UserAgent,
downloadLicense.AudibleKey,
downloadLicense.AudibleIV,
coverArtPath,
metadataPath,
outputFileName)
.GetAwaiter()
.GetResult();
return aaxcProcesser.Succeeded;
}
private static string GenerateFfmpegChapters(ChapterInfo chapters)
{
var stringBuilder = new System.Text.StringBuilder();
foreach (AudibleApiDTOs.Chapter c in chapters.Chapters)
{
stringBuilder.Append("[CHAPTER]\n");
stringBuilder.Append("TIMEBASE=1/1000\n");
stringBuilder.Append("START=" + c.StartOffsetMs + "\n");
stringBuilder.Append("END=" + (c.StartOffsetMs + c.LengthMs) + "\n");
stringBuilder.Append("title=" + c.Title + "\n");
}
return stringBuilder.ToString();
}
public bool Step3_CreateCue()
{
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(outputFileName), chapters));
return true;
}
public bool Step4_CreateNfo()
{
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".nfo"), NFO.CreateContents(AppName, tags, chapters));
return true;
}
public bool Step5_Cleanup()
{
FileExt.SafeDelete(coverArtPath);
FileExt.SafeDelete(metadataPath);
return true;
}
}
}

View File

@ -0,0 +1,60 @@
using System;
using System.IO;
using System.Text;
using AudibleApiDTOs;
using Dinah.Core;
namespace FileLiberator.AaxcDownloadDecrypt
{
public static class Cue
{
public static string CreateContents(string filePath, ChapterInfo chapters)
{
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine(GetFileLine(filePath, "MP3"));
var trackCount = 0;
foreach (var c in chapters.Chapters)
{
trackCount++;
var startTime = TimeSpan.FromMilliseconds(c.StartOffsetMs);
stringBuilder.AppendLine($"TRACK {trackCount} AUDIO");
stringBuilder.AppendLine($" TITLE \"{c.Title}\"");
stringBuilder.AppendLine($" INDEX 01 {(int)startTime.TotalMinutes}:{startTime:ss\\:ff}");
}
return stringBuilder.ToString();
}
public static void UpdateFileName(FileInfo cueFileInfo, string audioFilePath)
=> UpdateFileName(cueFileInfo.FullName, audioFilePath);
public static void UpdateFileName(string cueFilePath, FileInfo audioFileInfo)
=> UpdateFileName(cueFilePath, audioFileInfo.FullName);
public static void UpdateFileName(FileInfo cueFileInfo, FileInfo audioFileInfo)
=> UpdateFileName(cueFileInfo.FullName, audioFileInfo.FullName);
public static void UpdateFileName(string cueFilePath, string audioFilePath)
{
var cueContents = File.ReadAllLines(cueFilePath);
for (var i = 0; i < cueContents.Length; i++)
{
var line = cueContents[i];
if (!line.Trim().StartsWith("FILE") || !line.Contains(" "))
continue;
var fileTypeBegins = line.LastIndexOf(" ") + 1;
cueContents[i] = GetFileLine(audioFilePath, line[fileTypeBegins..]);
break;
}
File.WriteAllLines(cueFilePath, cueContents);
}
private static string GetFileLine(string filePath, string audioType) => $"FILE {Path.GetFileName(filePath).SurroundWithQuotes()} {audioType}";
}
}

View File

@ -0,0 +1,19 @@
using DataLayer;
using Dinah.Core.ErrorHandling;
using System.Threading.Tasks;
namespace FileLiberator.AaxcDownloadDecrypt
{
public class DownloadBookDummy : DownloadableBase
{
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
{
return new StatusHandler();
}
public override bool Validate(LibraryBook libraryBook)
{
return true;
}
}
}

View File

@ -0,0 +1,182 @@
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;
namespace FileLiberator.AaxcDownloadDecrypt
{
public class DownloadDecryptBook : IDecryptable
{
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<string> DecryptCompleted;
public event EventHandler<LibraryBook> Completed;
public event EventHandler<string> StatusUpdate;
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.DecryptInProgress, libraryBook);
// decrypt failed
if (outputAudioFilename is null)
return new StatusHandler { "Decrypt failed" };
// moves files and returns dest dir. Do not put inside of if(RetainAaxFiles)
_ = 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 destinationDir, LibraryBook libraryBook)
{
DecryptBegin?.Invoke(this, $"Begin decrypting {libraryBook}");
try
{
validate(libraryBook);
var api = await InternalUtilities.AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale);
var dlLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
var contentMetadata = await api.GetLibraryBookMetadataAsync(libraryBook.Book.AudibleProductId);
var newDownloader = await AaxcDownloadConverter.CreateAsync(Path.GetDirectoryName(destinationDir), dlLic, contentMetadata?.ChapterInfo);
newDownloader.AppName = "Libation";
TitleDiscovered?.Invoke(this, newDownloader.tags.title);
AuthorsDiscovered?.Invoke(this, newDownloader.tags.author);
NarratorsDiscovered?.Invoke(this, newDownloader.tags.narrator);
CoverImageFilepathDiscovered?.Invoke(this, newDownloader.tags.coverArt);
// override default which was set in CreateAsync
var proposedOutputFile = Path.Combine(destinationDir, $"{libraryBook.Book.Title} [{libraryBook.Book.AudibleProductId}].m4b");
newDownloader.SetOutputFilename(proposedOutputFile);
newDownloader.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress);
// REAL WORK DONE HERE
var success = await Task.Run(() => newDownloader.Run());
// decrypt failed
if (!success)
return null;
return newDownloader.outputFileName;
}
finally
{
DecryptCompleted?.Invoke(this, $"Completed downloading and decrypting {libraryBook.Book.Title}");
}
}
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)
&& !AudibleFileStorage.AAX.Exists(libraryBook.Book.AudibleProductId);
}
}

View File

@ -0,0 +1,201 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace FileLiberator.AaxcDownloadDecrypt
{
public class AaxcProgress : EventArgs
{
public TimeSpan ProcessedTime { get; set; }
public TimeSpan AudioDuration { get; set; }
public double ProgressPercent => Math.Round(100 * ProcessedTime.TotalSeconds / AudioDuration.TotalSeconds,2);
}
/// <summary>
/// Download audible aaxc, decrypt, remux, add metadata, and insert cover art.
/// </summary>
class FFMpegAaaxcProcesser
{
public event EventHandler<AaxcProgress> ProgressUpdate;
public string FFMpegPath { get; }
public bool IsRunning { get; set; } = false;
public bool Succeeded { get; private set; }
private static Regex processedTimeRegex = new Regex("time=(\\d{2}):(\\d{2}):(\\d{2}).\\d{2}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static Regex durationRegex = new Regex("Duration: (\\d{2}):(\\d{2}):(\\d{2}).\\d{2}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private TimeSpan duration { get; set; }
private TimeSpan position { get; set; }
public FFMpegAaaxcProcesser(string ffmpegPath)
{
FFMpegPath = ffmpegPath;
}
public async Task ProcessBook(string aaxcUrl, string userAgent, string audibleKey, string audibleIV, string artworkPath, string metadataPath, string outputFile)
{
//This process gets the aaxc from the url and streams the decrypted
//aac stream to standard output
var downloader = new Process
{
StartInfo = getDownloaderStartInfo(aaxcUrl, userAgent, audibleKey, audibleIV)
};
//This process retreves an aac stream from standard input and muxes
// it into an m4b along with the cover art and metadata.
var remuxer = new Process
{
StartInfo = getRemuxerStartInfo(artworkPath, metadataPath, outputFile)
};
IsRunning = true;
downloader.ErrorDataReceived += Downloader_ErrorDataReceived;
remuxer.ErrorDataReceived += Remuxer_ErrorDataReceived;
downloader.Start();
downloader.BeginErrorReadLine();
var pipedOutput = downloader.StandardOutput.BaseStream;
remuxer.Start();
remuxer.BeginErrorReadLine();
var pipedInput = remuxer.StandardInput.BaseStream;
int lastRead = 0;
byte[] buffer = new byte[16 * 1024];
//All the work done here. Copy download standard output into
//remuxer standard input
await Task.Run(() =>
{
do
{
lastRead = pipedOutput.Read(buffer, 0, buffer.Length);
pipedInput.Write(buffer, 0, lastRead);
} while (lastRead > 0 && !remuxer.HasExited);
});
pipedInput.Close();
//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();
remuxer.WaitForExit();
downloader.WaitForExit();
IsRunning = false;
Succeeded = downloader.ExitCode == 0 && remuxer.ExitCode == 0;
}
private void Downloader_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (!string.IsNullOrEmpty(e.Data) && durationRegex.IsMatch(e.Data))
{
//get total audio stream duration
var match = durationRegex.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);
duration = new TimeSpan(hours, minutes, seconds);
}
}
private void Remuxer_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (!string.IsNullOrEmpty(e.Data) && processedTimeRegex.IsMatch(e.Data))
{
//get timestamp of of last processed audio stream position
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);
position = new TimeSpan(hours, minutes, seconds);
ProgressUpdate?.Invoke(sender, new AaxcProgress
{
ProcessedTime = position,
AudioDuration = duration
});
}
}
private ProcessStartInfo getDownloaderStartInfo(string aaxcUrl, string userAgent, string audibleKey, string audibleIV) =>
new ProcessStartInfo
{
FileName = FFMpegPath,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
WorkingDirectory = Path.GetDirectoryName(FFMpegPath),
ArgumentList ={
"-nostdin",
"-audible_key",
audibleKey,
"-audible_iv",
audibleIV,
"-i",
aaxcUrl,
"-user_agent",
userAgent, //user-agent is requied for CDN to serve the file
"-c:a", //audio codec
"copy", //copy stream
"-f", //force output format: adts
"adts",
"pipe:" //pipe output to standard output
}
};
private ProcessStartInfo getRemuxerStartInfo(string artworkPath, string metadataPath, string outputFile) =>
new ProcessStartInfo
{
FileName = FFMpegPath,
RedirectStandardError = true,
RedirectStandardInput = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
WorkingDirectory = Path.GetDirectoryName(FFMpegPath),
ArgumentList =
{
"-thread_queue_size",
"1024",
"-f", //force input format: aac
"aac",
"-i",
"pipe:", //input from standard input
"-i",
artworkPath,
"-i",
metadataPath,
"-map",
"0",
"-map",
"1",
"-map_metadata",
"2",
"-c", //codec copy
"copy",
"-c:v:1", //video codec from file [1] (artwork)
"png", //video codec
"-disposition:v:1",
"attached_pic",
"-f", //force output format: mp4
"mp4",
outputFile,
"-y" //overwritte existing
}
};
}
}

View File

@ -0,0 +1,56 @@
using AudibleApiDTOs;
namespace FileLiberator.AaxcDownloadDecrypt
{
public static class NFO
{
public static string CreateContents(string ripper, Tags tags, ChapterInfo chapters)
{
var _hours = (int)tags.duration.TotalHours;
var myDuration
= (_hours > 0 ? _hours + " hours, " : "")
+ tags.duration.Minutes + " minutes, "
+ tags.duration.Seconds + " seconds";
var header
= "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"
+ $" Duration: {myDuration}\r\n"
+ $" Chapters: {chapters.Chapters.Length}\r\n"
+ "\r\n"
+ "\r\n"
+ "Media Information\r\n"
+ "=================\r\n"
+ " Source Format: Audible AAX\r\n"
+ $" Source Sample Rate: {tags.sampleRate} Hz\r\n"
+ $" Source Channels: {tags.channels}\r\n"
+ $" Source Bitrate: {tags.bitrate} kbits\r\n"
+ "\r\n"
+ " Lossless Encode: Yes\r\n"
+ " Encoded Codec: AAC / M4B\r\n"
+ $" Encoded Sample Rate: {tags.sampleRate} Hz\r\n"
+ $" Encoded Channels: {tags.channels}\r\n"
+ $" Encoded Bitrate: {tags.bitrate} kbits\r\n"
+ "\r\n"
+ $" Ripper: {ripper}\r\n"
+ "\r\n"
+ "\r\n"
+ "Book Description\r\n"
+ "================\r\n"
+ tags.comments;
return s;
}
}
}

View File

@ -0,0 +1,135 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace FileLiberator.AaxcDownloadDecrypt
{
/// <summary>
/// Provides a <see cref="TagLib.File.IFileAbstraction"/> for a file over Http.
/// </summary>
class NetworkFileAbstraction : TagLib.File.IFileAbstraction
{
private NetworkFileStream aaxNetworkStream;
public static async Task<NetworkFileAbstraction> CreateAsync(HttpClient client, Uri webFileUri)
{
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);
}
public string Name { get; private set; }
public Stream ReadStream => aaxNetworkStream;
public Stream WriteStream => throw new NotImplementedException();
public void CloseStream(Stream stream)
{
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 + offset + 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"></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;
}
}
}
}

View File

@ -0,0 +1,81 @@
using System;
using TagLib;
using TagLib.Mpeg4;
using Dinah.Core;
namespace FileLiberator.AaxcDownloadDecrypt
{
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; }
public int channels { get; }
public int bitrate { get; }
public int sampleRate { get; }
public bool hasCoverArt { get; }
public byte[] coverArt { get; }
// input file
public Tags(TagLib.File tagLibFile)
{
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 ?? "";
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;
hasCoverArt = tagLibFile.Tag.Pictures.Length > 0;
if (hasCoverArt)
coverArt = tagLibFile.Tag.Pictures[0].Data.Data;
duration = tagLibFile.Properties.Duration;
bitrate = tagLibFile.Properties.AudioBitrate;
channels = tagLibFile.Properties.AudioChannels;
sampleRate = tagLibFile.Properties.AudioSampleRate;
}
// 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

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using DataLayer;
using Dinah.Core.ErrorHandling;
using FileLiberator.AaxcDownloadDecrypt;
using FileManager;
namespace FileLiberator
@ -21,8 +22,9 @@ 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 DownloadBookDummy DownloadBook { get; } = new DownloadBookDummy();
public DownloadDecryptBook DecryptBook { get; } = new DownloadDecryptBook();
public DownloadPdf DownloadPdf { get; } = new DownloadPdf();
public bool Validate(LibraryBook libraryBook)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.