Merge pull request #51 from Mbucari/master
Replaced FFMpeg decryptor and taglib with AAXClean
This commit is contained in:
commit
8240a97f6d
@ -9,6 +9,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\AAXClean\AAXClean.csproj" />
|
||||||
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
|
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using Dinah.Core;
|
using AAXClean;
|
||||||
|
using Dinah.Core;
|
||||||
using Dinah.Core.Diagnostics;
|
using Dinah.Core.Diagnostics;
|
||||||
using Dinah.Core.IO;
|
using Dinah.Core.IO;
|
||||||
using Dinah.Core.StepRunner;
|
using Dinah.Core.StepRunner;
|
||||||
@ -9,7 +10,7 @@ namespace AaxDecrypter
|
|||||||
{
|
{
|
||||||
public interface ISimpleAaxcToM4bConverter
|
public interface ISimpleAaxcToM4bConverter
|
||||||
{
|
{
|
||||||
event EventHandler<AaxcTagLibFile> RetrievedTags;
|
event EventHandler<AppleTags> RetrievedTags;
|
||||||
event EventHandler<byte[]> RetrievedCoverArt;
|
event EventHandler<byte[]> RetrievedCoverArt;
|
||||||
event EventHandler<TimeSpan> DecryptTimeRemaining;
|
event EventHandler<TimeSpan> DecryptTimeRemaining;
|
||||||
event EventHandler<int> DecryptProgressUpdate;
|
event EventHandler<int> DecryptProgressUpdate;
|
||||||
@ -18,7 +19,7 @@ namespace AaxDecrypter
|
|||||||
string outDir { get; }
|
string outDir { get; }
|
||||||
string outputFileName { get; }
|
string outputFileName { get; }
|
||||||
DownloadLicense downloadLicense { get; }
|
DownloadLicense downloadLicense { get; }
|
||||||
AaxcTagLibFile aaxcTagLib { get; }
|
Mp4File aaxFile { get; }
|
||||||
byte[] coverArt { get; }
|
byte[] coverArt { get; }
|
||||||
void SetCoverArt(byte[] coverArt);
|
void SetCoverArt(byte[] coverArt);
|
||||||
void SetOutputFilename(string outFileName);
|
void SetOutputFilename(string outFileName);
|
||||||
@ -29,14 +30,13 @@ namespace AaxDecrypter
|
|||||||
bool Step1_CreateDir();
|
bool Step1_CreateDir();
|
||||||
bool Step2_GetMetadata();
|
bool Step2_GetMetadata();
|
||||||
bool Step3_DownloadAndCombine();
|
bool Step3_DownloadAndCombine();
|
||||||
bool Step4_RestoreMetadata();
|
|
||||||
bool Step5_CreateCue();
|
bool Step5_CreateCue();
|
||||||
bool Step6_CreateNfo();
|
bool Step6_CreateNfo();
|
||||||
bool Step7_Cleanup();
|
bool Step7_Cleanup();
|
||||||
}
|
}
|
||||||
public class AaxcDownloadConverter : IAdvancedAaxcToM4bConverter
|
public class AaxcDownloadConverter : IAdvancedAaxcToM4bConverter
|
||||||
{
|
{
|
||||||
public event EventHandler<AaxcTagLibFile> RetrievedTags;
|
public event EventHandler<AppleTags> RetrievedTags;
|
||||||
public event EventHandler<byte[]> RetrievedCoverArt;
|
public event EventHandler<byte[]> RetrievedCoverArt;
|
||||||
public event EventHandler<int> DecryptProgressUpdate;
|
public event EventHandler<int> DecryptProgressUpdate;
|
||||||
public event EventHandler<TimeSpan> DecryptTimeRemaining;
|
public event EventHandler<TimeSpan> DecryptTimeRemaining;
|
||||||
@ -45,11 +45,11 @@ namespace AaxDecrypter
|
|||||||
public string cacheDir { get; private set; }
|
public string cacheDir { get; private set; }
|
||||||
public string outputFileName { get; private set; }
|
public string outputFileName { get; private set; }
|
||||||
public DownloadLicense downloadLicense { get; private set; }
|
public DownloadLicense downloadLicense { get; private set; }
|
||||||
public AaxcTagLibFile aaxcTagLib { get; private set; }
|
public Mp4File aaxFile { get; private set; }
|
||||||
public byte[] coverArt { get; private set; }
|
public byte[] coverArt { get; private set; }
|
||||||
|
|
||||||
private StepSequence steps { get; }
|
private StepSequence steps { get; }
|
||||||
private FFMpegAaxcProcesser aaxcProcesser;
|
private NetworkFileStreamPersister nfsPersister;
|
||||||
private bool isCanceled { get; set; }
|
private bool isCanceled { get; set; }
|
||||||
private string jsonDownloadState => Path.Combine(cacheDir, Path.GetFileNameWithoutExtension(outputFileName) + ".json");
|
private string jsonDownloadState => Path.Combine(cacheDir, Path.GetFileNameWithoutExtension(outputFileName) + ".json");
|
||||||
private string tempFile => PathLib.ReplaceExtension(jsonDownloadState, ".aaxc");
|
private string tempFile => PathLib.ReplaceExtension(jsonDownloadState, ".aaxc");
|
||||||
@ -81,15 +81,11 @@ namespace AaxDecrypter
|
|||||||
["Step 1: Create Dir"] = Step1_CreateDir,
|
["Step 1: Create Dir"] = Step1_CreateDir,
|
||||||
["Step 2: Get Aaxc Metadata"] = Step2_GetMetadata,
|
["Step 2: Get Aaxc Metadata"] = Step2_GetMetadata,
|
||||||
["Step 3: Download Decrypted Audiobook"] = Step3_DownloadAndCombine,
|
["Step 3: Download Decrypted Audiobook"] = Step3_DownloadAndCombine,
|
||||||
["Step 4: Restore Aaxc Metadata"] = Step4_RestoreMetadata,
|
|
||||||
["Step 5: Create Cue"] = Step5_CreateCue,
|
["Step 5: Create Cue"] = Step5_CreateCue,
|
||||||
["Step 6: Create Nfo"] = Step6_CreateNfo,
|
["Step 6: Create Nfo"] = Step6_CreateNfo,
|
||||||
["Step 7: Cleanup"] = Step7_Cleanup,
|
["Step 7: Cleanup"] = Step7_Cleanup,
|
||||||
};
|
};
|
||||||
|
|
||||||
aaxcProcesser = new FFMpegAaxcProcesser(dlLic);
|
|
||||||
aaxcProcesser.ProgressUpdate += AaxcProcesser_ProgressUpdate;
|
|
||||||
|
|
||||||
downloadLicense = dlLic;
|
downloadLicense = dlLic;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +116,7 @@ namespace AaxDecrypter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var speedup = (int)(aaxcTagLib.Properties.Duration.TotalSeconds / (long)Elapsed.TotalSeconds);
|
var speedup = (int)(aaxFile.Duration.TotalSeconds / (long)Elapsed.TotalSeconds);
|
||||||
Console.WriteLine("Speedup is " + speedup + "x realtime.");
|
Console.WriteLine("Speedup is " + speedup + "x realtime.");
|
||||||
Console.WriteLine("Done");
|
Console.WriteLine("Done");
|
||||||
return true;
|
return true;
|
||||||
@ -138,7 +134,6 @@ namespace AaxDecrypter
|
|||||||
{
|
{
|
||||||
//Get metadata from the file over http
|
//Get metadata from the file over http
|
||||||
|
|
||||||
NetworkFileStreamPersister nfsPersister;
|
|
||||||
if (File.Exists(jsonDownloadState))
|
if (File.Exists(jsonDownloadState))
|
||||||
{
|
{
|
||||||
nfsPersister = new NetworkFileStreamPersister(jsonDownloadState);
|
nfsPersister = new NetworkFileStreamPersister(jsonDownloadState);
|
||||||
@ -154,18 +149,12 @@ namespace AaxDecrypter
|
|||||||
NetworkFileStream networkFileStream = new NetworkFileStream(tempFile, new Uri(downloadLicense.DownloadUrl), 0, headers);
|
NetworkFileStream networkFileStream = new NetworkFileStream(tempFile, new Uri(downloadLicense.DownloadUrl), 0, headers);
|
||||||
nfsPersister = new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
|
nfsPersister = new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
|
||||||
}
|
}
|
||||||
|
nfsPersister.NetworkFileStream.BeginDownloading();
|
||||||
|
|
||||||
var networkFile = new NetworkFileAbstraction(nfsPersister.NetworkFileStream);
|
aaxFile = new Mp4File(nfsPersister.NetworkFileStream);
|
||||||
aaxcTagLib = new AaxcTagLibFile(networkFile);
|
coverArt = aaxFile.AppleTags.Cover;
|
||||||
nfsPersister.Dispose();
|
|
||||||
|
|
||||||
|
RetrievedTags?.Invoke(this, aaxFile.AppleTags);
|
||||||
if (coverArt is null && aaxcTagLib.AppleTags.Pictures.Length > 0)
|
|
||||||
{
|
|
||||||
coverArt = aaxcTagLib.AppleTags.Pictures[0].Data.Data;
|
|
||||||
}
|
|
||||||
|
|
||||||
RetrievedTags?.Invoke(this, aaxcTagLib);
|
|
||||||
RetrievedCoverArt?.Invoke(this, coverArt);
|
RetrievedCoverArt?.Invoke(this, coverArt);
|
||||||
|
|
||||||
return !isCanceled;
|
return !isCanceled;
|
||||||
@ -175,87 +164,40 @@ namespace AaxDecrypter
|
|||||||
{
|
{
|
||||||
DecryptProgressUpdate?.Invoke(this, int.MaxValue);
|
DecryptProgressUpdate?.Invoke(this, int.MaxValue);
|
||||||
|
|
||||||
NetworkFileStreamPersister nfsPersister;
|
if (File.Exists(outputFileName))
|
||||||
if (File.Exists(jsonDownloadState))
|
FileExt.SafeDelete(outputFileName);
|
||||||
{
|
|
||||||
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);
|
FileStream outFile = File.OpenWrite(outputFileName);
|
||||||
nfsPersister = new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
|
|
||||||
}
|
|
||||||
|
|
||||||
string metadataPath = Path.Combine(outDir, Path.GetFileName(outputFileName) + ".ffmeta");
|
aaxFile.DecryptionProgressUpdate += AaxFile_DecryptionProgressUpdate;
|
||||||
|
var decryptedBook = aaxFile.DecryptAaxc(outFile, downloadLicense.AudibleKey, downloadLicense.AudibleIV, downloadLicense.ChapterInfo);
|
||||||
|
aaxFile.DecryptionProgressUpdate -= AaxFile_DecryptionProgressUpdate;
|
||||||
|
|
||||||
if (downloadLicense.ChapterInfo is null)
|
decryptedBook?.AppleTags?.SetCoverArt(coverArt);
|
||||||
{
|
decryptedBook?.Save();
|
||||||
//If we want to keep the original chapters, we need to get them from the url.
|
decryptedBook?.Close();
|
||||||
//Ffprobe needs to seek to find metadata and it can't seek a pipe. Also, there's
|
|
||||||
//no guarantee that enough of the file will have been downloaded at this point
|
|
||||||
//to be able to use the cache file.
|
|
||||||
downloadLicense.ChapterInfo = new ChapterInfo(downloadLicense.DownloadUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Only write chapters to the metadata file. All other aaxc metadata will be
|
|
||||||
//wiped out but is restored in Step 3.
|
|
||||||
File.WriteAllText(metadataPath, downloadLicense.ChapterInfo.ToFFMeta(true));
|
|
||||||
|
|
||||||
|
|
||||||
aaxcProcesser.ProcessBook(
|
|
||||||
nfsPersister.NetworkFileStream,
|
|
||||||
outputFileName,
|
|
||||||
metadataPath)
|
|
||||||
.GetAwaiter()
|
|
||||||
.GetResult();
|
|
||||||
|
|
||||||
nfsPersister.NetworkFileStream.Close();
|
|
||||||
nfsPersister.Dispose();
|
nfsPersister.Dispose();
|
||||||
|
|
||||||
FileExt.SafeDelete(metadataPath);
|
|
||||||
|
|
||||||
DecryptProgressUpdate?.Invoke(this, 0);
|
DecryptProgressUpdate?.Invoke(this, 0);
|
||||||
|
|
||||||
return aaxcProcesser.Succeeded && !isCanceled;
|
return aaxFile is not null && !isCanceled;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AaxcProcesser_ProgressUpdate(object sender, AaxcProcessUpdate e)
|
private void AaxFile_DecryptionProgressUpdate(object sender, DecryptionProgressEventArgs e)
|
||||||
{
|
{
|
||||||
double remainingSecsToProcess = (aaxcTagLib.Properties.Duration - e.ProcessPosition).TotalSeconds;
|
var duration = aaxFile.Duration;
|
||||||
|
double remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds;
|
||||||
double estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
|
double estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
|
||||||
|
|
||||||
if (double.IsNormal(estTimeRemaining))
|
if (double.IsNormal(estTimeRemaining))
|
||||||
DecryptTimeRemaining?.Invoke(this, TimeSpan.FromSeconds(estTimeRemaining));
|
DecryptTimeRemaining?.Invoke(this, TimeSpan.FromSeconds(estTimeRemaining));
|
||||||
|
|
||||||
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / aaxcTagLib.Properties.Duration.TotalSeconds;
|
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / duration.TotalSeconds;
|
||||||
|
|
||||||
DecryptProgressUpdate?.Invoke(this, (int)progressPercent);
|
DecryptProgressUpdate?.Invoke(this, (int)progressPercent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Copy all aacx metadata to m4b file, including cover art.
|
|
||||||
/// </summary>
|
|
||||||
public bool Step4_RestoreMetadata()
|
|
||||||
{
|
|
||||||
var outFile = new AaxcTagLibFile(outputFileName);
|
|
||||||
outFile.CopyTagsFrom(aaxcTagLib);
|
|
||||||
|
|
||||||
if (outFile.AppleTags.Pictures.Length == 0 && coverArt is not null)
|
|
||||||
{
|
|
||||||
outFile.AddPicture(coverArt);
|
|
||||||
}
|
|
||||||
|
|
||||||
outFile.Save();
|
|
||||||
|
|
||||||
return !isCanceled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Step5_CreateCue()
|
public bool Step5_CreateCue()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -273,7 +215,7 @@ namespace AaxDecrypter
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".nfo"), NFO.CreateContents(AppName, aaxcTagLib, downloadLicense.ChapterInfo));
|
File.WriteAllText(PathLib.ReplaceExtension(outputFileName, ".nfo"), NFO.CreateContents(AppName, aaxFile, downloadLicense.ChapterInfo));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -292,7 +234,7 @@ namespace AaxDecrypter
|
|||||||
public void Cancel()
|
public void Cancel()
|
||||||
{
|
{
|
||||||
isCanceled = true;
|
isCanceled = true;
|
||||||
aaxcProcesser.Cancel();
|
aaxFile?.Cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,75 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using Dinah.Core;
|
|
||||||
using TagLib;
|
|
||||||
using TagLib.Mpeg4;
|
|
||||||
|
|
||||||
namespace AaxDecrypter
|
|
||||||
{
|
|
||||||
public class AaxcTagLibFile : TagLib.Mpeg4.File
|
|
||||||
{
|
|
||||||
// ©
|
|
||||||
private const byte COPYRIGHT = 0xa9;
|
|
||||||
|
|
||||||
private static ReadOnlyByteVector narratorType { get; } = new ReadOnlyByteVector(COPYRIGHT, (byte)'n', (byte)'r', (byte)'t');
|
|
||||||
private static ReadOnlyByteVector descriptionType { get; } = new ReadOnlyByteVector(COPYRIGHT, (byte)'d', (byte)'e', (byte)'s');
|
|
||||||
private static ReadOnlyByteVector publisherType { get; } = new ReadOnlyByteVector(COPYRIGHT, (byte)'p', (byte)'u', (byte)'b');
|
|
||||||
|
|
||||||
public string AsciiTitleSansUnabridged => TitleSansUnabridged?.UnicodeToAscii();
|
|
||||||
public string AsciiFirstAuthor => FirstAuthor?.UnicodeToAscii();
|
|
||||||
public string AsciiNarrator => Narrator?.UnicodeToAscii();
|
|
||||||
public string AsciiComment => Comment?.UnicodeToAscii();
|
|
||||||
public string AsciiLongDescription => LongDescription?.UnicodeToAscii();
|
|
||||||
|
|
||||||
public AppleTag AppleTags => GetTag(TagTypes.Apple) as AppleTag;
|
|
||||||
|
|
||||||
public string Comment => AppleTags.Comment;
|
|
||||||
public string[] Authors => AppleTags.Performers;
|
|
||||||
public string FirstAuthor => Authors?.Length > 0 ? Authors[0] : default;
|
|
||||||
public string TitleSansUnabridged => AppleTags.Title?.Replace(" (Unabridged)", "");
|
|
||||||
|
|
||||||
public string BookCopyright => _copyright is not null && _copyright.Length > 0 ? _copyright[0] : default;
|
|
||||||
public string RecordingCopyright => _copyright is not null && _copyright.Length > 1 ? _copyright[1] : default;
|
|
||||||
private string[] _copyright => AppleTags.Copyright?.Replace("©", string.Empty)?.Replace("(P)", string.Empty)?.Split(';');
|
|
||||||
|
|
||||||
public string Narrator => getAppleTagsText(narratorType);
|
|
||||||
public string LongDescription => getAppleTagsText(descriptionType);
|
|
||||||
public string ReleaseDate => getAppleTagsText("rldt");
|
|
||||||
public string Publisher => getAppleTagsText(publisherType);
|
|
||||||
private string getAppleTagsText(ByteVector byteVector)
|
|
||||||
{
|
|
||||||
string[] text = AppleTags.GetText(byteVector);
|
|
||||||
return text.Length == 0 ? default : text[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
public AaxcTagLibFile(IFileAbstraction abstraction)
|
|
||||||
: base(abstraction, ReadStyle.Average)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public AaxcTagLibFile(string path)
|
|
||||||
: this(new LocalFileAbstraction(path))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// Copy all metadata fields in the source file, even those that TagLib doesn't
|
|
||||||
/// recognize, to the output file.
|
|
||||||
/// NOTE: Chapters aren't stored in MPEG-4 metadata. They are encoded as a Timed
|
|
||||||
/// Text Stream (MPEG-4 Part 17), so taglib doesn't read or write them.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sourceFile">File from which tags will be coppied.</param>
|
|
||||||
public void CopyTagsFrom(AaxcTagLibFile sourceFile)
|
|
||||||
{
|
|
||||||
AppleTags.Clear();
|
|
||||||
|
|
||||||
foreach (var stag in sourceFile.AppleTags)
|
|
||||||
{
|
|
||||||
AppleTags.SetData(stag.BoxType, stag.Children.Cast<AppleDataBox>().ToArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public void AddPicture(byte[] coverArt)
|
|
||||||
{
|
|
||||||
AppleTags.SetData("covr", coverArt, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
using Dinah.Core;
|
|
||||||
using Dinah.Core.Diagnostics;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace AaxDecrypter
|
|
||||||
{
|
|
||||||
public class ChapterInfo
|
|
||||||
{
|
|
||||||
private List<Chapter> _chapterList = new List<Chapter>();
|
|
||||||
public IEnumerable<Chapter> Chapters => _chapterList.AsEnumerable();
|
|
||||||
public int Count => _chapterList.Count;
|
|
||||||
public ChapterInfo() { }
|
|
||||||
public ChapterInfo(string audiobookFile)
|
|
||||||
{
|
|
||||||
var info = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = DecryptSupportLibraries.ffprobePath,
|
|
||||||
Arguments = "-loglevel panic -show_chapters -print_format json \"" + audiobookFile + "\""
|
|
||||||
};
|
|
||||||
|
|
||||||
var jString = info.RunHidden().Output;
|
|
||||||
var chapterJObject = JObject.Parse(jString);
|
|
||||||
var chapters = chapterJObject["chapters"]
|
|
||||||
.Select(c => new Chapter(
|
|
||||||
c["tags"]?["title"]?.Value<string>(),
|
|
||||||
c["start_time"].Value<double>(),
|
|
||||||
c["end_time"].Value<double>()
|
|
||||||
));
|
|
||||||
|
|
||||||
_chapterList.AddRange(chapters);
|
|
||||||
}
|
|
||||||
public void AddChapter(Chapter chapter)
|
|
||||||
{
|
|
||||||
ArgumentValidator.EnsureNotNull(chapter, nameof(chapter));
|
|
||||||
_chapterList.Add(chapter);
|
|
||||||
}
|
|
||||||
public string ToFFMeta(bool includeFFMetaHeader)
|
|
||||||
{
|
|
||||||
var ffmetaChapters = new StringBuilder();
|
|
||||||
|
|
||||||
if (includeFFMetaHeader)
|
|
||||||
ffmetaChapters.AppendLine(";FFMETADATA1");
|
|
||||||
|
|
||||||
foreach (var c in Chapters)
|
|
||||||
{
|
|
||||||
ffmetaChapters.AppendLine(c.ToFFMeta());
|
|
||||||
}
|
|
||||||
return ffmetaChapters.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public class Chapter
|
|
||||||
{
|
|
||||||
public string Title { get; }
|
|
||||||
public TimeSpan StartOffset { get; }
|
|
||||||
public TimeSpan EndOffset { get; }
|
|
||||||
public Chapter(string title, long startOffsetMs, long lengthMs)
|
|
||||||
{
|
|
||||||
ArgumentValidator.EnsureNotNullOrEmpty(title, nameof(title));
|
|
||||||
ArgumentValidator.EnsureGreaterThan(startOffsetMs, nameof(startOffsetMs), -1);
|
|
||||||
|
|
||||||
// do not validate lengthMs for '> 0'. It is valid to set sections this way. eg: 11-22-63 [B005UR3VFO] by Stephen King
|
|
||||||
|
|
||||||
Title = title;
|
|
||||||
StartOffset = TimeSpan.FromMilliseconds(startOffsetMs);
|
|
||||||
EndOffset = StartOffset + TimeSpan.FromMilliseconds(lengthMs);
|
|
||||||
}
|
|
||||||
public Chapter(string title, double startTimeSec, double endTimeSec)
|
|
||||||
: this(title, (long)(startTimeSec * 1000), (long)((endTimeSec - startTimeSec) * 1000))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ToFFMeta()
|
|
||||||
{
|
|
||||||
return "[CHAPTER]\n" +
|
|
||||||
"TIMEBASE=1/1000\n" +
|
|
||||||
"START=" + StartOffset.TotalMilliseconds + "\n" +
|
|
||||||
"END=" + EndOffset.TotalMilliseconds + "\n" +
|
|
||||||
"title=" + Title;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using AAXClean;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
|
|
||||||
namespace AaxDecrypter
|
namespace AaxDecrypter
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using Dinah.Core;
|
using AAXClean;
|
||||||
|
using Dinah.Core;
|
||||||
|
|
||||||
namespace AaxDecrypter
|
namespace AaxDecrypter
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,254 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace AaxDecrypter
|
|
||||||
{
|
|
||||||
class AaxcProcessUpdate
|
|
||||||
{
|
|
||||||
public AaxcProcessUpdate(TimeSpan position, double speed)
|
|
||||||
{
|
|
||||||
ProcessPosition = position;
|
|
||||||
ProcessSpeed = speed;
|
|
||||||
EventTime = DateTime.Now;
|
|
||||||
}
|
|
||||||
public TimeSpan ProcessPosition { get; }
|
|
||||||
public double ProcessSpeed { get; }
|
|
||||||
public DateTime EventTime { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Download audible aaxc, decrypt, and remux with chapters.
|
|
||||||
/// </summary>
|
|
||||||
class FFMpegAaxcProcesser
|
|
||||||
{
|
|
||||||
public event EventHandler<AaxcProcessUpdate> ProgressUpdate;
|
|
||||||
public string FFMpegPath { get; }
|
|
||||||
public DownloadLicense DownloadLicense { get; }
|
|
||||||
public bool IsRunning { get; private set; }
|
|
||||||
public bool Succeeded { get; private set; }
|
|
||||||
public string FFMpegRemuxerStandardError => remuxerError.ToString();
|
|
||||||
public string FFMpegDecrypterStandardError => decrypterError.ToString();
|
|
||||||
|
|
||||||
|
|
||||||
private StringBuilder remuxerError { get; } = new StringBuilder();
|
|
||||||
private StringBuilder decrypterError { get; } = new StringBuilder();
|
|
||||||
private static Regex processedTimeRegex { get; } = new Regex("time=(\\d{2}):(\\d{2}):(\\d{2}).\\d{2}.*speed=\\s{0,1}([0-9]*[.]?[0-9]+)(?:e\\+([0-9]+)){0,1}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
|
||||||
private Process decrypter;
|
|
||||||
private Process remuxer;
|
|
||||||
private Stream inputFile;
|
|
||||||
private bool isCanceled = false;
|
|
||||||
|
|
||||||
public FFMpegAaxcProcesser(DownloadLicense downloadLicense)
|
|
||||||
{
|
|
||||||
FFMpegPath = DecryptSupportLibraries.ffmpegPath;
|
|
||||||
DownloadLicense = downloadLicense;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ProcessBook(Stream inputFile, string outputFile, string ffmetaChaptersPath)
|
|
||||||
{
|
|
||||||
this.inputFile = inputFile;
|
|
||||||
|
|
||||||
//This process gets the aaxc from the url and streams the decrypted
|
|
||||||
//aac stream to standard output
|
|
||||||
decrypter = new Process
|
|
||||||
{
|
|
||||||
StartInfo = getDownloaderStartInfo()
|
|
||||||
};
|
|
||||||
|
|
||||||
//This process retreves an aac stream from standard input and muxes
|
|
||||||
// it into an m4b along with the cover art and metadata.
|
|
||||||
remuxer = new Process
|
|
||||||
{
|
|
||||||
StartInfo = getRemuxerStartInfo(outputFile, ffmetaChaptersPath)
|
|
||||||
};
|
|
||||||
|
|
||||||
IsRunning = true;
|
|
||||||
|
|
||||||
decrypter.ErrorDataReceived += Downloader_ErrorDataReceived;
|
|
||||||
decrypter.Start();
|
|
||||||
decrypter.BeginErrorReadLine();
|
|
||||||
|
|
||||||
remuxer.ErrorDataReceived += Remuxer_ErrorDataReceived;
|
|
||||||
remuxer.Start();
|
|
||||||
remuxer.BeginErrorReadLine();
|
|
||||||
|
|
||||||
//Thic check needs to be placed after remuxer has started.
|
|
||||||
if (isCanceled) return;
|
|
||||||
|
|
||||||
var decrypterInput = decrypter.StandardInput.BaseStream;
|
|
||||||
var decrypterOutput = decrypter.StandardOutput.BaseStream;
|
|
||||||
var remuxerInput = remuxer.StandardInput.BaseStream;
|
|
||||||
|
|
||||||
//Read inputFile into decrypter stdin in the background
|
|
||||||
var t = new Thread(() => CopyStream(inputFile, decrypterInput, decrypter));
|
|
||||||
t.Start();
|
|
||||||
|
|
||||||
//All the work done here. Copy download standard output into
|
|
||||||
//remuxer standard input
|
|
||||||
await Task.Run(() => CopyStream(decrypterOutput, remuxerInput, remuxer));
|
|
||||||
|
|
||||||
//If the remuxer exited due to failure, downloader will still have
|
|
||||||
//data in the pipe. Force kill downloader to continue.
|
|
||||||
if (remuxer.HasExited && !decrypter.HasExited)
|
|
||||||
decrypter.Kill();
|
|
||||||
|
|
||||||
remuxer.WaitForExit();
|
|
||||||
decrypter.WaitForExit();
|
|
||||||
|
|
||||||
IsRunning = false;
|
|
||||||
Succeeded = decrypter.ExitCode == 0 && remuxer.ExitCode == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CopyStream(Stream inputStream, Stream outputStream, Process returnOnProcExit)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
byte[] buffer = new byte[32 * 1024];
|
|
||||||
int lastRead;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
lastRead = inputStream.Read(buffer, 0, buffer.Length);
|
|
||||||
outputStream.Write(buffer, 0, lastRead);
|
|
||||||
} while (lastRead > 0 && !returnOnProcExit.HasExited);
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
//There is no way to tell if the process closed the input stream
|
|
||||||
//before trying to write to it. If it did close, throws IOException.
|
|
||||||
isCanceled = true;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
outputStream.Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Cancel()
|
|
||||||
{
|
|
||||||
isCanceled = true;
|
|
||||||
|
|
||||||
if (IsRunning && !remuxer.HasExited)
|
|
||||||
remuxer.Kill();
|
|
||||||
if (IsRunning && !decrypter.HasExited)
|
|
||||||
decrypter.Kill();
|
|
||||||
inputFile?.Close();
|
|
||||||
}
|
|
||||||
private void Downloader_ErrorDataReceived(object sender, DataReceivedEventArgs e)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(e.Data))
|
|
||||||
return;
|
|
||||||
|
|
||||||
decrypterError.AppendLine(e.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Remuxer_ErrorDataReceived(object sender, DataReceivedEventArgs e)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(e.Data))
|
|
||||||
return;
|
|
||||||
|
|
||||||
remuxerError.AppendLine(e.Data);
|
|
||||||
|
|
||||||
if (processedTimeRegex.IsMatch(e.Data))
|
|
||||||
{
|
|
||||||
//get timestamp of of last processed audio stream position
|
|
||||||
//and processing speed
|
|
||||||
var match = processedTimeRegex.Match(e.Data);
|
|
||||||
|
|
||||||
int hours = int.Parse(match.Groups[1].Value);
|
|
||||||
int minutes = int.Parse(match.Groups[2].Value);
|
|
||||||
int seconds = int.Parse(match.Groups[3].Value);
|
|
||||||
|
|
||||||
var position = new TimeSpan(hours, minutes, seconds);
|
|
||||||
|
|
||||||
double speed = double.Parse(match.Groups[4].Value);
|
|
||||||
int exp = match.Groups[5].Success ? int.Parse(match.Groups[5].Value) : 0;
|
|
||||||
speed *= Math.Pow(10, exp);
|
|
||||||
|
|
||||||
ProgressUpdate?.Invoke(this, new AaxcProcessUpdate(position, speed));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.Data.Contains("aac bitstream error"))
|
|
||||||
{
|
|
||||||
//This happens if input is corrupt (should never happen) or if caller
|
|
||||||
//supplied wrong key/iv
|
|
||||||
var process = sender as Process;
|
|
||||||
process.Kill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ProcessStartInfo getDownloaderStartInfo() =>
|
|
||||||
new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = FFMpegPath,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
RedirectStandardInput = true,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
CreateNoWindow = true,
|
|
||||||
WindowStyle = ProcessWindowStyle.Hidden,
|
|
||||||
UseShellExecute = false,
|
|
||||||
WorkingDirectory = Path.GetDirectoryName(FFMpegPath),
|
|
||||||
ArgumentList ={
|
|
||||||
"-audible_key",
|
|
||||||
DownloadLicense.AudibleKey,
|
|
||||||
"-audible_iv",
|
|
||||||
DownloadLicense.AudibleIV,
|
|
||||||
"-f",
|
|
||||||
"mp4",
|
|
||||||
"-i",
|
|
||||||
"pipe:",
|
|
||||||
"-c:a", //audio codec
|
|
||||||
"copy", //copy stream
|
|
||||||
"-f", //force output format: adts
|
|
||||||
"adts",
|
|
||||||
"pipe:" //pipe output to stdout
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private ProcessStartInfo getRemuxerStartInfo(string outputFile, string ffmetaChaptersPath = null)
|
|
||||||
{
|
|
||||||
var startInfo = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = FFMpegPath,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
RedirectStandardInput = true,
|
|
||||||
CreateNoWindow = true,
|
|
||||||
WindowStyle = ProcessWindowStyle.Hidden,
|
|
||||||
UseShellExecute = false,
|
|
||||||
WorkingDirectory = Path.GetDirectoryName(FFMpegPath),
|
|
||||||
};
|
|
||||||
|
|
||||||
startInfo.ArgumentList.Add("-thread_queue_size");
|
|
||||||
startInfo.ArgumentList.Add("1024");
|
|
||||||
startInfo.ArgumentList.Add("-f"); //force input format: aac
|
|
||||||
startInfo.ArgumentList.Add("aac");
|
|
||||||
startInfo.ArgumentList.Add("-i"); //read input from stdin
|
|
||||||
startInfo.ArgumentList.Add("pipe:");
|
|
||||||
|
|
||||||
//copy metadata from supplied metadata file
|
|
||||||
startInfo.ArgumentList.Add("-f");
|
|
||||||
startInfo.ArgumentList.Add("ffmetadata");
|
|
||||||
startInfo.ArgumentList.Add("-i");
|
|
||||||
startInfo.ArgumentList.Add(ffmetaChaptersPath);
|
|
||||||
|
|
||||||
startInfo.ArgumentList.Add("-map"); //map file 0 (aac audio stream)
|
|
||||||
startInfo.ArgumentList.Add("0");
|
|
||||||
startInfo.ArgumentList.Add("-map_chapters"); //copy chapter data from file metadata file
|
|
||||||
startInfo.ArgumentList.Add("1");
|
|
||||||
startInfo.ArgumentList.Add("-c"); //copy all mapped streams
|
|
||||||
startInfo.ArgumentList.Add("copy");
|
|
||||||
startInfo.ArgumentList.Add("-f"); //force output format: mp4
|
|
||||||
startInfo.ArgumentList.Add("mp4");
|
|
||||||
startInfo.ArgumentList.Add("-movflags");
|
|
||||||
startInfo.ArgumentList.Add("disable_chpl"); //Disable Nero chapters format
|
|
||||||
startInfo.ArgumentList.Add(outputFile);
|
|
||||||
startInfo.ArgumentList.Add("-y"); //overwrite existing
|
|
||||||
|
|
||||||
return startInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +1,29 @@
|
|||||||
|
using AAXClean;
|
||||||
|
using Dinah.Core;
|
||||||
|
|
||||||
namespace AaxDecrypter
|
namespace AaxDecrypter
|
||||||
{
|
{
|
||||||
public static class NFO
|
public static class NFO
|
||||||
{
|
{
|
||||||
public static string CreateContents(string ripper, AaxcTagLibFile aaxcTagLib, ChapterInfo chapters)
|
public static string CreateContents(string ripper, AAXClean.Mp4File aaxcTagLib, ChapterInfo chapters)
|
||||||
{
|
{
|
||||||
var _hours = (int)aaxcTagLib.Properties.Duration.TotalHours;
|
var _hours = (int)aaxcTagLib.Duration.TotalHours;
|
||||||
var myDuration
|
var myDuration
|
||||||
= (_hours > 0 ? _hours + " hours, " : string.Empty)
|
= (_hours > 0 ? _hours + " hours, " : string.Empty)
|
||||||
+ aaxcTagLib.Properties.Duration.Minutes + " minutes, "
|
+ aaxcTagLib.Duration.Minutes + " minutes, "
|
||||||
+ aaxcTagLib.Properties.Duration.Seconds + " seconds";
|
+ aaxcTagLib.Duration.Seconds + " seconds";
|
||||||
|
|
||||||
var nfoString
|
var nfoString
|
||||||
= "General Information\r\n"
|
= "General Information\r\n"
|
||||||
+ "======================\r\n"
|
+ "======================\r\n"
|
||||||
+ $" Title: {aaxcTagLib.AsciiTitleSansUnabridged ?? "[unknown]"}\r\n"
|
+ $" Title: {aaxcTagLib.AppleTags.TitleSansUnabridged?.UnicodeToAscii() ?? "[unknown]"}\r\n"
|
||||||
+ $" Author: {aaxcTagLib.AsciiFirstAuthor ?? "[unknown]"}\r\n"
|
+ $" Author: {aaxcTagLib.AppleTags.FirstAuthor?.UnicodeToAscii() ?? "[unknown]"}\r\n"
|
||||||
+ $" Read By: {aaxcTagLib.AsciiNarrator ?? "[unknown]"}\r\n"
|
+ $" Read By: {aaxcTagLib.AppleTags.Narrator?.UnicodeToAscii() ?? "[unknown]"}\r\n"
|
||||||
+ $" Release Date: {aaxcTagLib.ReleaseDate ?? "[unknown]"}\r\n"
|
+ $" Release Date: {aaxcTagLib.AppleTags.ReleaseDate ?? "[unknown]"}\r\n"
|
||||||
+ $" Book Copyright: {aaxcTagLib.BookCopyright ?? "[unknown]"}\r\n"
|
+ $" Book Copyright: {aaxcTagLib.AppleTags.BookCopyright ?? "[unknown]"}\r\n"
|
||||||
+ $" Recording Copyright: {aaxcTagLib.RecordingCopyright ?? "[unknown]"}\r\n"
|
+ $" Recording Copyright: {aaxcTagLib.AppleTags.RecordingCopyright ?? "[unknown]"}\r\n"
|
||||||
+ $" Genre: {aaxcTagLib.AppleTags.FirstGenre ?? "[unknown]"}\r\n"
|
+ $" Genre: {aaxcTagLib.AppleTags.Generes ?? "[unknown]"}\r\n"
|
||||||
+ $" Publisher: {aaxcTagLib.Publisher ?? "[unknown]"}\r\n"
|
+ $" Publisher: {aaxcTagLib.AppleTags.Publisher ?? "[unknown]"}\r\n"
|
||||||
+ $" Duration: {myDuration}\r\n"
|
+ $" Duration: {myDuration}\r\n"
|
||||||
+ $" Chapters: {chapters.Count}\r\n"
|
+ $" Chapters: {chapters.Count}\r\n"
|
||||||
+ "\r\n"
|
+ "\r\n"
|
||||||
@ -29,22 +31,22 @@ namespace AaxDecrypter
|
|||||||
+ "Media Information\r\n"
|
+ "Media Information\r\n"
|
||||||
+ "======================\r\n"
|
+ "======================\r\n"
|
||||||
+ " Source Format: Audible AAX\r\n"
|
+ " Source Format: Audible AAX\r\n"
|
||||||
+ $" Source Sample Rate: {aaxcTagLib.Properties.AudioSampleRate} Hz\r\n"
|
+ $" Source Sample Rate: {aaxcTagLib.TimeScale} Hz\r\n"
|
||||||
+ $" Source Channels: {aaxcTagLib.Properties.AudioChannels}\r\n"
|
+ $" Source Channels: {aaxcTagLib.AudioChannels}\r\n"
|
||||||
+ $" Source Bitrate: {aaxcTagLib.Properties.AudioBitrate} Kbps\r\n"
|
+ $" Source Bitrate: {aaxcTagLib.AverageBitrate} Kbps\r\n"
|
||||||
+ "\r\n"
|
+ "\r\n"
|
||||||
+ " Lossless Encode: Yes\r\n"
|
+ " Lossless Encode: Yes\r\n"
|
||||||
+ " Encoded Codec: AAC / M4B\r\n"
|
+ " Encoded Codec: AAC / M4B\r\n"
|
||||||
+ $" Encoded Sample Rate: {aaxcTagLib.Properties.AudioSampleRate} Hz\r\n"
|
+ $" Encoded Sample Rate: {aaxcTagLib.TimeScale} Hz\r\n"
|
||||||
+ $" Encoded Channels: {aaxcTagLib.Properties.AudioChannels}\r\n"
|
+ $" Encoded Channels: {aaxcTagLib.AudioChannels}\r\n"
|
||||||
+ $" Encoded Bitrate: {aaxcTagLib.Properties.AudioBitrate} Kbps\r\n"
|
+ $" Encoded Bitrate: {aaxcTagLib.AverageBitrate} Kbps\r\n"
|
||||||
+ "\r\n"
|
+ "\r\n"
|
||||||
+ $" Ripper: {ripper}\r\n"
|
+ $" Ripper: {ripper}\r\n"
|
||||||
+ "\r\n"
|
+ "\r\n"
|
||||||
+ "\r\n"
|
+ "\r\n"
|
||||||
+ "Book Description\r\n"
|
+ "Book Description\r\n"
|
||||||
+ "================\r\n"
|
+ "================\r\n"
|
||||||
+ (!string.IsNullOrWhiteSpace(aaxcTagLib.LongDescription) ? aaxcTagLib.AsciiLongDescription : aaxcTagLib.AsciiComment);
|
+ (!string.IsNullOrWhiteSpace(aaxcTagLib.AppleTags.LongDescription) ? aaxcTagLib.AppleTags.LongDescription.UnicodeToAscii() : aaxcTagLib.AppleTags.Comment?.UnicodeToAscii());
|
||||||
|
|
||||||
return nfoString;
|
return nfoString;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -176,7 +176,7 @@ namespace AaxDecrypter
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread.
|
/// Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void BeginDownloading()
|
public void BeginDownloading()
|
||||||
{
|
{
|
||||||
if (ContentLength != 0 && WritePosition == ContentLength)
|
if (ContentLength != 0 && WritePosition == ContentLength)
|
||||||
{
|
{
|
||||||
@ -393,7 +393,7 @@ namespace AaxDecrypter
|
|||||||
//read operation will block until file contains enough data
|
//read operation will block until file contains enough data
|
||||||
//to fulfil the request, or until cancelled.
|
//to fulfil the request, or until cancelled.
|
||||||
while (requiredPosition > WritePosition && !isCancelled)
|
while (requiredPosition > WritePosition && !isCancelled)
|
||||||
Thread.Sleep(0);
|
Thread.Sleep(2);
|
||||||
|
|
||||||
return _readFile.Read(buffer, offset, count);
|
return _readFile.Read(buffer, offset, count);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,15 +81,10 @@ namespace FileLiberator
|
|||||||
|
|
||||||
if (Configuration.Instance.AllowLibationFixup)
|
if (Configuration.Instance.AllowLibationFixup)
|
||||||
{
|
{
|
||||||
aaxcDecryptDlLic.ChapterInfo = new ChapterInfo();
|
aaxcDecryptDlLic.ChapterInfo = new AAXClean.ChapterInfo();
|
||||||
|
|
||||||
foreach (var chap in contentLic.ContentMetadata?.ChapterInfo?.Chapters)
|
foreach (var chap in contentLic.ContentMetadata?.ChapterInfo?.Chapters)
|
||||||
aaxcDecryptDlLic.ChapterInfo.AddChapter(
|
aaxcDecryptDlLic.ChapterInfo.AddChapter(chap.Title, TimeSpan.FromMilliseconds(chap.LengthMs));
|
||||||
new Chapter(
|
|
||||||
chap.Title,
|
|
||||||
chap.StartOffsetMs,
|
|
||||||
chap.LengthMs
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
aaxcDownloader = AaxcDownloadConverter.Create(cacheDir, destinationDir, aaxcDecryptDlLic);
|
aaxcDownloader = AaxcDownloadConverter.Create(cacheDir, destinationDir, aaxcDecryptDlLic);
|
||||||
@ -132,7 +127,7 @@ namespace FileLiberator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void aaxcDownloader_RetrievedTags(object sender, AaxcTagLibFile e)
|
private void aaxcDownloader_RetrievedTags(object sender, AAXClean.AppleTags e)
|
||||||
{
|
{
|
||||||
TitleDiscovered?.Invoke(this, e.TitleSansUnabridged);
|
TitleDiscovered?.Invoke(this, e.TitleSansUnabridged);
|
||||||
AuthorsDiscovered?.Invoke(this, e.FirstAuthor ?? "[unknown]");
|
AuthorsDiscovered?.Invoke(this, e.FirstAuthor ?? "[unknown]");
|
||||||
|
|||||||
@ -86,6 +86,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine.Tests"
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.EntityFrameworkCore.Tests", "..\Dinah.Core\_Tests\Dinah.EntityFrameworkCore.Tests\Dinah.EntityFrameworkCore.Tests.csproj", "{6F5131A0-09AE-4707-B82B-5E53CB74688E}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.EntityFrameworkCore.Tests", "..\Dinah.Core\_Tests\Dinah.EntityFrameworkCore.Tests\Dinah.EntityFrameworkCore.Tests.csproj", "{6F5131A0-09AE-4707-B82B-5E53CB74688E}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AAXClean", "..\AAXClean\AAXClean.csproj", "{94BEB7CC-511D-45AB-9F09-09BE858EE486}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -208,6 +210,10 @@ Global
|
|||||||
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Release|Any CPU.Build.0 = Release|Any CPU
|
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -242,6 +248,7 @@ Global
|
|||||||
{8447C956-B03E-4F59-9DD4-877793B849D9} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
{8447C956-B03E-4F59-9DD4-877793B849D9} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||||
{6F5131A0-09AE-4707-B82B-5E53CB74688E} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
{6F5131A0-09AE-4707-B82B-5E53CB74688E} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
|
||||||
|
{94BEB7CC-511D-45AB-9F09-09BE858EE486} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}
|
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user